mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
fix ui
This commit is contained in:
@@ -71,7 +71,7 @@ const ArchiveUsersPage = () => {
|
||||
value={selectedUnitId}
|
||||
onValueChange={(value) => setSelectedUnitId(value)}
|
||||
>
|
||||
<SelectTrigger className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm [&>span]:truncate">
|
||||
<SelectTrigger className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm [&>span]:truncate">
|
||||
<SelectValue placeholder="Select a Unit" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -203,8 +203,8 @@ const ArchivedUnitsPositionsPage = () => {
|
||||
header: t("organization.name", "Name"),
|
||||
cell: ({ row }: any) => (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0">
|
||||
<UserSquare2 className="h-5 w-5 text-blue-600" />
|
||||
<div className="w-10 h-10 rounded-full bg-primary-100 flex items-center justify-center flex-shrink-0">
|
||||
<UserSquare2 className="h-5 w-5 text-primary-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">
|
||||
@@ -397,7 +397,7 @@ const ArchivedUnitsPositionsPage = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UserSquare2 className="h-5 w-5 text-blue-600" />
|
||||
<UserSquare2 className="h-5 w-5 text-primary-600" />
|
||||
{t("archive.archivedPositions", "Archived Positions")}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -118,7 +118,7 @@ const OrgAdminDashboard = () => {
|
||||
title: t("dashboard.totalEmployees"),
|
||||
value: activeReport?.employeesCount?.toLocaleString() || "0",
|
||||
icon: Users,
|
||||
color: "from-blue-500 to-blue-600",
|
||||
color: "from-primary-500 to-primary-600",
|
||||
},
|
||||
...(!isUnitAdmin
|
||||
? [{
|
||||
@@ -147,7 +147,7 @@ const OrgAdminDashboard = () => {
|
||||
description: t("dashboard.userManagementDesc"),
|
||||
icon: Users,
|
||||
action: () => navigate("/user-management/user_management"),
|
||||
color: "bg-gradient-to-r from-blue-500 to-cyan-600",
|
||||
color: "bg-gradient-to-r from-primary-500 to-primary-600",
|
||||
},
|
||||
{
|
||||
id: "content-mgmt",
|
||||
@@ -155,7 +155,7 @@ const OrgAdminDashboard = () => {
|
||||
description: t("dashboard.contentManagementDesc"),
|
||||
icon: Files,
|
||||
action: () => navigate("/user-management/content-management"),
|
||||
color: "bg-gradient-to-r from-purple-500 to-indigo-600",
|
||||
color: "bg-gradient-to-r from-purple-500 to-primary-600",
|
||||
},
|
||||
{
|
||||
id: "excel-upload",
|
||||
@@ -187,7 +187,7 @@ const OrgAdminDashboard = () => {
|
||||
description: t("dashboard.webManagementDesc"),
|
||||
icon: FileText,
|
||||
action: () => navigate("/user-management/web-management"),
|
||||
color: "bg-gradient-to-r from-teal-500 to-cyan-600",
|
||||
color: "bg-gradient-to-r from-teal-500 to-primary-600",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -278,7 +278,7 @@ const OrgAdminDashboard = () => {
|
||||
: getStats().map((stat) => (
|
||||
<Card
|
||||
key={`stat-${stat.id}`}
|
||||
className="hover:shadow-lg transition-shadow duration-200 border-l-4 border-l-blue-500 dark:bg-gray-800 dark:border-gray-700 dark:border-l-blue-500"
|
||||
className="hover:shadow-lg transition-shadow duration-200 border-l-4 border-l-primary-500 dark:bg-gray-800 dark:border-gray-700 dark:border-l-primary-500"
|
||||
>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@@ -234,7 +234,10 @@ export const AppMenuTabs = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="fixed top-16 left-0 right-0 z-30 bg-white dark:bg-gray-900 border-b dark:border-gray-800 overflow-hidden flex flex-col">
|
||||
// Sticky (not fixed) so it stays in flow: content below never needs a
|
||||
// magic offset matching this bar's responsive height. top-16 keeps it
|
||||
// pinned just below the fixed 64px <Top> header while scrolling.
|
||||
<div className="sticky top-16 z-30 bg-white dark:bg-gray-900 border-b dark:border-gray-800 overflow-hidden flex flex-col">
|
||||
{/* Mobile View - Two separate rows */}
|
||||
<div className="md:hidden flex flex-col pt-5 pb-3 px-4 space-y-3 mt-2">
|
||||
{/* Primary items row */}
|
||||
|
||||
@@ -27,10 +27,12 @@ export const AppLayout = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-dvh overflow-y-auto flex flex-col bg-background text-foreground">
|
||||
// pt-16 clears the fixed 64px <Top>; AppMenuTabs is sticky and in flow,
|
||||
// so content starts right below it at any tabs height (mobile/desktop).
|
||||
<div className="w-full h-dvh overflow-y-auto flex flex-col pt-16 bg-background text-foreground">
|
||||
<Top onToggleSidebar={toggleSidebar} showRecordManagementShortcut={!isSuperAdmin} />
|
||||
<AppMenuTabs />
|
||||
<div className="px-2 pb-2 pt-8 sm:px-4 sm:pb-4 sm:pt-10 flex-1">
|
||||
<div className="px-2 pb-2 pt-4 sm:px-4 sm:pb-4 sm:pt-6 flex-1">
|
||||
<div className="w-full overflow-x-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,67 @@ interface ErrorResponseBody {
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface PgDriverError {
|
||||
code?: string;
|
||||
detail?: string;
|
||||
column?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
const toSentenceCase = (snake: string): string =>
|
||||
snake.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
|
||||
/**
|
||||
* Translate common Postgres constraint failures (surfaced by TypeORM as
|
||||
* QueryFailedError, which is NOT an HttpException and would otherwise fall
|
||||
* through as an opaque 500) into actionable 400s. Clients rely on the
|
||||
* "still referenced" wording to show friendly cannot-delete messages.
|
||||
*/
|
||||
const translatePgError = (
|
||||
driver: PgDriverError,
|
||||
): { status: number; message: string } | null => {
|
||||
const detail = driver.detail ?? "";
|
||||
switch (driver.code) {
|
||||
case "23503": {
|
||||
const stillReferenced = detail.match(
|
||||
/is still referenced from table "(.*?)"/,
|
||||
);
|
||||
if (stillReferenced) {
|
||||
const table = toSentenceCase(stillReferenced[1]);
|
||||
return {
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
message: `Cannot delete: this record is still referenced by ${table}.`,
|
||||
};
|
||||
}
|
||||
const notPresent = detail.match(/is not present in table "(.*?)"/);
|
||||
const entity = toSentenceCase(notPresent?.[1] ?? "referenced entity");
|
||||
return {
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
message: `The specified ${entity} does not exist.`,
|
||||
};
|
||||
}
|
||||
case "23505": {
|
||||
const pair = detail.match(/\((.*?)\)=\((.*?)\)/);
|
||||
return {
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
message: `Duplicate entry: '${pair?.[2] ?? "value"}' already exists for '${toSentenceCase(pair?.[1] ?? "field")}'.`,
|
||||
};
|
||||
}
|
||||
case "23502":
|
||||
return {
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
message: `Missing required field: ${toSentenceCase(driver.column ?? "field")}.`,
|
||||
};
|
||||
case "22P02":
|
||||
return {
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
message: "Invalid input format (e.g., wrong UUID or number).",
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@Catch()
|
||||
export class HttpExceptionFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger(HttpExceptionFilter.name);
|
||||
@@ -25,13 +86,23 @@ export class HttpExceptionFilter implements ExceptionFilter {
|
||||
const response = ctx.getResponse();
|
||||
const request = ctx.getRequest();
|
||||
|
||||
const status =
|
||||
exception instanceof HttpException
|
||||
const pgTranslated =
|
||||
(exception as Error)?.name === "QueryFailedError"
|
||||
? translatePgError(
|
||||
((exception as { driverError?: PgDriverError }).driverError ??
|
||||
{}) as PgDriverError,
|
||||
)
|
||||
: null;
|
||||
|
||||
const status = pgTranslated
|
||||
? pgTranslated.status
|
||||
: exception instanceof HttpException
|
||||
? exception.getStatus()
|
||||
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
const messageRaw =
|
||||
exception instanceof HttpException
|
||||
const messageRaw = pgTranslated
|
||||
? pgTranslated.message
|
||||
: exception instanceof HttpException
|
||||
? exception.getResponse()
|
||||
: "Internal server error";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user