Merge pull request #670 from Tria-plc/alpha

Alpha
This commit is contained in:
Abubeker Yasin
2026-07-14 12:44:43 +03:00
committed by GitHub
9 changed files with 110 additions and 22 deletions

View File

@@ -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>

View File

@@ -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")}
</>
)}

View File

@@ -113,12 +113,14 @@ const OrgAdminDashboard = () => {
// Define stats structure for the dashboard
const getStats = () => [
// One brand hue, stepped depth per tile — identity comes from the icon
// and title, so the chips stay cohesive instead of competing hues.
{
id: "employees",
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
? [{
@@ -126,7 +128,7 @@ const OrgAdminDashboard = () => {
title: t("dashboard.totalUnits"),
value: report?.unitsCount?.toLocaleString() || "0",
icon: Building2,
color: "from-primary-500 to-primary-600",
color: "from-primary-600 to-primary-700",
}]
: []),
{
@@ -134,12 +136,14 @@ const OrgAdminDashboard = () => {
title: t("dashboard.totalPositions"),
value: activeReport?.positionsCount?.toLocaleString() || "0",
icon: FileText,
color: "from-purple-500 to-purple-600",
color: "from-primary-700 to-primary-800",
},
];
// Use report activities if available, otherwise use static data
// Brand gradient for all navigation actions; archive alone stays neutral
// gray to read as the "dormant" destination. Icons carry the identity.
const quickActions = [
{
id: "user-mgmt",
@@ -147,7 +151,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-700",
},
{
id: "content-mgmt",
@@ -155,7 +159,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-primary-500 to-primary-700",
},
{
id: "excel-upload",
@@ -163,7 +167,7 @@ const OrgAdminDashboard = () => {
description: t("dashboard.excelUploaderDesc"),
icon: Upload,
action: () => navigate("/user-management/bulk-upload"),
color: "bg-gradient-to-r from-primary-500 to-primary-600",
color: "bg-gradient-to-r from-primary-500 to-primary-700",
},
{
id: "position-settings",
@@ -171,7 +175,7 @@ const OrgAdminDashboard = () => {
description: t("dashboard.positionSettingsDesc"),
icon: FileText,
action: () => navigate("/user-management/position-management"),
color: "bg-gradient-to-r from-orange-500 to-red-600",
color: "bg-gradient-to-r from-primary-500 to-primary-700",
},
{
id: "archive-users",
@@ -187,7 +191,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-primary-500 to-primary-700",
},
];
@@ -278,7 +282,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">

View File

@@ -234,7 +234,12 @@ 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.
// shrink-0 is load-bearing: as a flex item with overflow-hidden this bar
// would otherwise be flex-squashed to zero height when the page overflows.
<div className="sticky top-16 z-30 shrink-0 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 */}

View File

@@ -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 mt-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>

View File

@@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuthStore } from '@/lib/auth-store';
import { useBookingStore } from '@/lib/booking-store';
import { LogIn, UserPlus, ChevronLeft } from 'lucide-react';
import { UserPlus, ChevronLeft } from 'lucide-react';
function Tooltip({ children, content }: { children: React.ReactNode; content: string[] }) {
const [visible, setVisible] = useState(false);
@@ -95,6 +95,7 @@ export default function AuthCheckPage() {
</button>
</Tooltip>
{/* TODO: re-enable once auth is integrated
<Tooltip content={[
'Saved passenger details',
'View booking history',
@@ -108,6 +109,7 @@ export default function AuthCheckPage() {
SignIn or Register
</button>
</Tooltip>
*/}
</div>
<div className="mt-8 text-center">

View File

@@ -21,6 +21,9 @@ class ApiClient {
if (token && token !== "null" && token !== "undefined") {
config.headers.Authorization = `Bearer ${token}`;
}
if (typeof window !== "undefined") {
config.headers["X-Frontend-Base-URL"] = window.location.origin;
}
return config;
});

View File

@@ -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";

View File

@@ -26,6 +26,7 @@ declare -A SERVICE_ENV_TARGET=(
["freight-api"]="apps/edr-freight-api/.env"
["freight-portal"]="apps/edr-freight-web/portal/.env"
["freight-backoffice"]="apps/edr-freight-web/backoffice/.env"
["gps-tracker"]="apps/edr-gps-tracker/.env"
["passenger-api"]="apps/edr-passenger-api/.env"
["passenger-portal"]="apps/edr-passenger-web/portal/.env"
["passenger-backoffice"]="apps/edr-passenger-web/backoffice/.env"
@@ -65,4 +66,4 @@ for service in "$@"; do
grep -E '^[[:space:]]*(NEXT_PUBLIC_|VITE_)[A-Za-z0-9_]+=' "${src}" \
| sed -E 's/^[[:space:]]*//' >> "${GITHUB_ENV}" || true
fi
done
done