Merge pull request #1192 from Tria-plc/freight/nati-2

Freight/nati 2
This commit is contained in:
Nathnael Wondisha
2026-08-08 20:38:37 +03:00
committed by GitHub
31 changed files with 1395 additions and 3009 deletions

View File

@@ -582,9 +582,7 @@ const App = () => {
<Route
path="routes"
element={
<RequirePermission
permission={[FREIGHT_PERMS.routes.view, FREIGHT_PERMS.fleet.view]}
>
<RequirePermission permission={FREIGHT_PERMS.routes.view}>
<RoutesPage />
</RequirePermission>
}
@@ -592,12 +590,7 @@ const App = () => {
<Route
path="locomotives"
element={
<RequirePermission
permission={[
FREIGHT_PERMS.locomotives.view,
FREIGHT_PERMS.fleet.view,
]}
>
<RequirePermission permission={FREIGHT_PERMS.locomotives.view}>
<FleetResourcePage />
</RequirePermission>
}
@@ -605,9 +598,7 @@ const App = () => {
<Route
path="trains"
element={
<RequirePermission
permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}
>
<RequirePermission permission={FREIGHT_PERMS.trains.view}>
<FleetResourcePage />
</RequirePermission>
}
@@ -615,9 +606,7 @@ const App = () => {
<Route
path="trains/:id"
element={
<RequirePermission
permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}
>
<RequirePermission permission={FREIGHT_PERMS.trains.view}>
<TrainDetailPage />
</RequirePermission>
}
@@ -625,9 +614,7 @@ const App = () => {
<Route
path="train-builder"
element={
<RequirePermission
permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}
>
<RequirePermission permission={FREIGHT_PERMS.trains.view}>
<TrainBuilderListPage />
</RequirePermission>
}
@@ -635,9 +622,7 @@ const App = () => {
<Route
path="train-builder/:id"
element={
<RequirePermission
permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}
>
<RequirePermission permission={FREIGHT_PERMS.trains.view}>
<TrainBuilderDetailPage />
</RequirePermission>
}
@@ -645,9 +630,7 @@ const App = () => {
<Route
path="wagons"
element={
<RequirePermission
permission={[FREIGHT_PERMS.wagons.view, FREIGHT_PERMS.fleet.view]}
>
<RequirePermission permission={FREIGHT_PERMS.wagons.view}>
<FleetResourcePage />
</RequirePermission>
}
@@ -668,12 +651,7 @@ const App = () => {
<Route
path="containers"
element={
<RequirePermission
permission={[
FREIGHT_PERMS.containers.view,
FREIGHT_PERMS.fleet.view,
]}
>
<RequirePermission permission={FREIGHT_PERMS.containers.view}>
<FleetResourcePage />
</RequirePermission>
}
@@ -681,12 +659,7 @@ const App = () => {
<Route
path="cargoes"
element={
<RequirePermission
permission={[
FREIGHT_PERMS.cargoes.view,
FREIGHT_PERMS.fleet.view,
]}
>
<RequirePermission permission={FREIGHT_PERMS.cargoes.view}>
<FleetResourcePage />
</RequirePermission>
}
@@ -774,9 +747,7 @@ const App = () => {
<Route
path="compliance"
element={
<RequirePermission
permission={[FREIGHT_PERMS.compliance.view, FREIGHT_PERMS.fleet.view]}
>
<RequirePermission permission={FREIGHT_PERMS.compliance.view}>
<CompliancePage />
</RequirePermission>
}
@@ -800,9 +771,7 @@ const App = () => {
<Route
path="procurement"
element={
<RequirePermission
permission={[FREIGHT_PERMS.procurement.view, FREIGHT_PERMS.fleet.view]}
>
<RequirePermission permission={FREIGHT_PERMS.procurement.view}>
<ProcurementPage />
</RequirePermission>
}
@@ -825,9 +794,7 @@ const App = () => {
<Route
path="file-settings"
element={
<RequirePermission
permission={[FREIGHT_PERMS.settings.fileUpload.view, FREIGHT_PERMS.admin]}
>
<RequirePermission permission={FREIGHT_PERMS.settings.fileUpload.view}>
<FileUploadSettingsPage />
</RequirePermission>
}
@@ -835,9 +802,7 @@ const App = () => {
<Route
path="dropdown-settings"
element={
<RequirePermission
permission={[FREIGHT_PERMS.settings.dropdown.view, FREIGHT_PERMS.admin]}
>
<RequirePermission permission={FREIGHT_PERMS.settings.dropdown.view}>
<DropdownSettingsPage />
</RequirePermission>
}
@@ -854,10 +819,7 @@ const App = () => {
path="contract-templates"
element={
<RequirePermission
permission={[
FREIGHT_PERMS.settings.contractTemplates.view,
FREIGHT_PERMS.admin,
]}
permission={FREIGHT_PERMS.settings.contractTemplates.view}
>
<ContractTemplatesPage />
</RequirePermission>
@@ -867,10 +829,7 @@ const App = () => {
path="contract-templates/:code"
element={
<RequirePermission
permission={[
FREIGHT_PERMS.settings.contractTemplates.view,
FREIGHT_PERMS.admin,
]}
permission={FREIGHT_PERMS.settings.contractTemplates.view}
>
<ContractTemplateEditorPage />
</RequirePermission>
@@ -883,7 +842,6 @@ const App = () => {
permission={[
FREIGHT_PERMS.settings.supportContent.view,
FREIGHT_PERMS.settings.supportContent.manage,
FREIGHT_PERMS.admin,
]}
>
<PortalContentPage />
@@ -908,9 +866,7 @@ const App = () => {
<Route
path="configuration/trade-access"
element={
<RequirePermission
permission={[FREIGHT_PERMS.tradeAccess.view, FREIGHT_PERMS.admin]}
>
<RequirePermission permission={FREIGHT_PERMS.tradeAccess.view}>
<TradeAccessPage />
</RequirePermission>
}
@@ -918,9 +874,7 @@ const App = () => {
<Route
path="configuration/exchange-rate"
element={
<RequirePermission
permission={[FREIGHT_PERMS.settings.exchangeRate.view, FREIGHT_PERMS.admin]}
>
<RequirePermission permission={FREIGHT_PERMS.settings.exchangeRate.view}>
<div className="p-4">
<ExchangeRateSettingsCard />
</div>

View File

@@ -32,8 +32,6 @@ import { formatDate, humanize } from "./format";
/** Friendly labels for the proposed-change snapshot keys (UpdateProfileDto). */
export const FIELD_LABELS: Record<string, string> = {
companyName: "Company name",
companyEmail: "Company email",
companyPhone: "Company phone",
companyLocation: "Location",
companyAddress: "Address",
tin: "TIN",
@@ -73,8 +71,6 @@ export function currentValue(company: Company, key: string): string {
const attrs = (company.attributes ?? {}) as Record<string, unknown>;
const map: Record<string, unknown> = {
companyName: c.name,
companyEmail: c.email,
companyPhone: c.phone,
companyLocation: c.country,
companyAddress: c.address,
tin: c.tin,
@@ -118,7 +114,8 @@ function FaydaIdentityDiff({
if (!subject) return null;
const current =
subject === "owner" ? company.identity?.owner : company.identity?.poa;
const read = (key: string) => snapshot[`${subject}${key}`] as string | undefined;
const read = (key: string) =>
snapshot[`${subject}${key}`] as string | undefined;
const verifiedAt = read("FaydaVerifiedAt");
const fields: { label: string; from?: string | null; to?: string }[] = [
{ label: "Name", from: current?.name, to: read("Name") },
@@ -131,7 +128,9 @@ function FaydaIdentityDiff({
<Stack gap={8}>
<Group gap={8}>
<Text size="sm" fw={600} c="edr-text">
{subject === "owner" ? "Owner re-verification" : "PoA re-verification"}
{subject === "owner"
? "Owner re-verification"
: "PoA re-verification"}
</Text>
{verifiedAt && (
<Text size="xs" c="dimmed">
@@ -283,8 +282,8 @@ export function ChangeRequestReview({ company }: { company: Company }) {
icon={<AlertTriangle size={16} />}
>
Changes were requested on an earlier round of this same
submission: <strong>{pending.note}</strong> check whether
this resubmission actually addresses it before approving.
submission: <strong>{pending.note}</strong> check whether this
resubmission actually addresses it before approving.
</Alert>
)}
@@ -297,8 +296,8 @@ export function ChangeRequestReview({ company }: { company: Company }) {
from={currentValue(company, key)}
to={
pending.snapshot[key] === null ||
pending.snapshot[key] === undefined ||
pending.snapshot[key] === ""
pending.snapshot[key] === undefined ||
pending.snapshot[key] === ""
? "—"
: String(pending.snapshot[key])
}
@@ -312,7 +311,10 @@ export function ChangeRequestReview({ company }: { company: Company }) {
) : null}
{faydaIdentitySnapshot && (
<FaydaIdentityDiff company={company} snapshot={faydaIdentitySnapshot} />
<FaydaIdentityDiff
company={company}
snapshot={faydaIdentitySnapshot}
/>
)}
{documentChanges.length > 0 && (
@@ -373,9 +375,10 @@ export function ChangeRequestReview({ company }: { company: Company }) {
type="button"
size="sm"
onClick={() =>
void fetchViewableFile(fileId, `Document ${i + 1}`).then(
view,
)
void fetchViewableFile(
fileId,
`Document ${i + 1}`,
).then(view)
}
>
Document {i + 1}
@@ -446,7 +449,10 @@ export function ChangeRequestReview({ company }: { company: Company }) {
variant="light"
color="yellow"
onClick={() => {
setActionTarget({ id: pending.id, kind: "request-changes" });
setActionTarget({
id: pending.id,
kind: "request-changes",
});
setNote("");
}}
>
@@ -525,7 +531,11 @@ export function ChangeRequestReview({ company }: { company: Company }) {
}
/** Compact "N changes pending" pill for the customer list/detail header. */
export function ChangeRequestPendingBadge({ companyId }: { companyId: string }) {
export function ChangeRequestPendingBadge({
companyId,
}: {
companyId: string;
}) {
const query = useQuery(
api.customers.changeRequests.queryOptions({ input: { id: companyId } }),
);

View File

@@ -203,22 +203,19 @@ export const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[]
label: "Routes",
href: "/dashboard/routes",
icon: <Network />,
permission: [FREIGHT_PERMS.routes.view, FREIGHT_PERMS.fleet.view],
permission: FREIGHT_PERMS.routes.view,
},
{
label: "Locomotives",
href: "/dashboard/locomotives",
icon: <Train />,
permission: [
FREIGHT_PERMS.locomotives.view,
FREIGHT_PERMS.fleet.view,
],
permission: FREIGHT_PERMS.locomotives.view,
},
{
label: "Train Builder",
href: "/dashboard/train-builder",
icon: <Hammer />,
permission: [FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view],
permission: FREIGHT_PERMS.trains.view,
},
// {
@@ -230,7 +227,7 @@ export const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[]
label: "Wagons",
href: "/dashboard/wagons",
icon: <Truck />,
permission: [FREIGHT_PERMS.wagons.view, FREIGHT_PERMS.fleet.view],
permission: FREIGHT_PERMS.wagons.view,
},
{
label: "Wagon Transfers",
@@ -287,21 +284,23 @@ export const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[]
label: "Compliance & Alerts",
href: "/dashboard/compliance",
icon: <ShieldCheck />,
permission: [FREIGHT_PERMS.compliance.view, FREIGHT_PERMS.fleet.view],
permission: FREIGHT_PERMS.compliance.view,
},
{
label: "Incidents",
href: "/dashboard/incidents",
icon: <FileText />,
// No dedicated backend key exists for incidents yet — stuck on the
// blanket fleet:view fallback until one is added.
// No dedicated backend key exists for incidents yet. Not part of
// the fleet.view/admin fallback cleanup — removing fleet.view
// here with nothing to replace it would lock the page to
// super-admin only, so it stays as the sole (if coarse) gate.
permission: FREIGHT_PERMS.fleet.view,
},
{
label: "Procurement",
href: "/dashboard/procurement",
icon: <Package />,
permission: [FREIGHT_PERMS.procurement.view, FREIGHT_PERMS.fleet.view],
permission: FREIGHT_PERMS.procurement.view,
},
{
label: "Financial Reports",
@@ -486,23 +485,20 @@ export const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[]
label: "File settings",
href: "/dashboard/file-settings",
icon: <Paperclip />,
permission: [FREIGHT_PERMS.settings.fileUpload.view, FREIGHT_PERMS.admin],
permission: FREIGHT_PERMS.settings.fileUpload.view,
},
{
label: "Dropdown settings",
href: "/dashboard/dropdown-settings",
icon: <Settings />,
permission: [FREIGHT_PERMS.settings.dropdown.view, FREIGHT_PERMS.admin],
permission: FREIGHT_PERMS.settings.dropdown.view,
},
{
label: "Contract templates",
href: "/dashboard/contract-templates",
icon: <ScrollText />,
// `view` opens the page; `read` alone is API-only and shows no menu.
permission: [
FREIGHT_PERMS.settings.contractTemplates.view,
FREIGHT_PERMS.admin,
],
permission: FREIGHT_PERMS.settings.contractTemplates.view,
},
{
label: "Portal content",
@@ -511,7 +507,6 @@ export const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[]
permission: [
FREIGHT_PERMS.settings.supportContent.view,
FREIGHT_PERMS.settings.supportContent.manage,
FREIGHT_PERMS.admin,
],
},
{
@@ -534,12 +529,12 @@ export const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[]
{
label: "Trade access",
href: "/dashboard/configuration/trade-access",
permission: [FREIGHT_PERMS.tradeAccess.view, FREIGHT_PERMS.admin],
permission: FREIGHT_PERMS.tradeAccess.view,
},
{
label: "Exchange rate",
href: "/dashboard/configuration/exchange-rate",
permission: [FREIGHT_PERMS.settings.exchangeRate.view, FREIGHT_PERMS.admin],
permission: FREIGHT_PERMS.settings.exchangeRate.view,
},
],
},

View File

@@ -659,19 +659,13 @@ export type FleetCrudResource =
| "vehicles"
| "drivers";
/**
* Per-resource fleet CRUD check. The legacy coarse fleet:manage key still
* grants every action (mirrors the API's one-of guard fallback).
*/
/** Per-resource fleet CRUD check — each resource needs its own grant. */
export function canFleetAction(
user: AuthUser | null | undefined,
resource: FleetCrudResource,
action: "create" | "update" | "delete",
): boolean {
return (
hasPermission(user, FREIGHT_PERMS[resource][action]) ||
hasPermission(user, FREIGHT_PERMS.fleet.manage)
);
return hasPermission(user, FREIGHT_PERMS[resource][action]);
}
/**

View File

@@ -87,11 +87,7 @@ const TAB_ITEMS: Array<{
icon: TrainFront,
kpiKey: "operations",
metricKey: "wagonsAvailable",
permission: [
FREIGHT_PERMS.fleet.view,
FREIGHT_PERMS.wagons.view,
FREIGHT_PERMS.trainScheduling.view,
],
permission: [FREIGHT_PERMS.wagons.view, FREIGHT_PERMS.trainScheduling.view],
},
{
value: "customers",

View File

@@ -85,9 +85,7 @@ export default function TrainBuilderDetailPage() {
const { user } = useAuth();
const canUpdate = canFleetAction(user, "trains", "update");
const canDelete = canFleetAction(user, "trains", "delete");
const canAssign =
hasPermission(user, FREIGHT_PERMS.trains.assignWagons) ||
hasPermission(user, FREIGHT_PERMS.fleet.manage);
const canAssign = hasPermission(user, FREIGHT_PERMS.trains.assignWagons);
const compositionQuery = useQuery(
api.trainBuilder.composition.queryOptions({ input: { id }, enabled: Boolean(id) }),