interchange document generation and acknowledgement

This commit is contained in:
hagiye
2026-06-27 13:49:41 +03:00
50 changed files with 2149 additions and 1878 deletions

View File

@@ -20,7 +20,7 @@
"@mantine/hooks": "^9.3.0",
"@tabler/icons-react": "^3.44.0",
"@tanstack/react-query": "^5.100.11",
"@tria-plc/iamui": "file:../../../local-packages/tria-plc-iamui-0.0.3.tgz",
"@tria-plc/iamui": "file:../../../local-packages/tria-plc-iamui-0.1.1.tgz",
"axios": "^1.7.7",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",

View File

@@ -77,6 +77,7 @@ import WarehouseInventoryPage from "./pages/warehouses/WarehouseInventoryPage";
import WarehouseInvoicesPage from "./pages/warehouses/WarehouseInvoicesPage";
import WarehouseListPage from "./pages/warehouses/WarehouseListPage";
import WarehouseRulesPage from "./pages/warehouses/WarehouseRulesPage";
import { HealthCheck } from "./features/health/HealthCheck";
const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
{
@@ -374,6 +375,7 @@ const App = () => {
return (
<Routes>
<Route path="/um/*" element={<UserManagementHostPage />} />
<Route path="/health" element={<HealthCheck />} />
<Route path="/" element={<Navigate to="/dashboard/overview" replace />} />
<Route path="/dashboard" element={<Navigate to="/dashboard/overview" replace />} />
<Route path="/dashboard" element={<DashboardShell />}>

View File

@@ -22,7 +22,7 @@ import {
LayoutGrid,
Package,
} from "lucide-react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import { useNavigate, useParams } from "react-router-dom";
@@ -84,9 +84,6 @@ export default function CustomerDetailPage() {
enabled: Boolean(id),
}),
);
const approveMutation = useMutation(
api.customers.setCompanyStatus.mutationOptions(),
);
const bookingsQuery = useQuery(
api.customers.bookings.queryOptions({
input: { id: id ?? "" },
@@ -394,28 +391,12 @@ export default function CustomerDetailPage() {
]}
backTo="/dashboard/customers"
title={company.name}
subtitle={`TIN ${company.tin}${
company.country ? ` · ${company.country}` : ""
}`}
subtitle={`TIN ${company.tin}${company.country ? ` · ${company.country}` : ""
}`}
meta={
<Group gap="xs" wrap="nowrap">
<CompanyTypeBadge type={company.type} />
<CompanyStatusBadge status={company.status} />
{company.status === "pending" && (
<Button
size="xs"
color="green"
loading={approveMutation.isPending}
onClick={() =>
approveMutation.mutate({
companyId: company.id,
status: "active",
})
}
>
Approve
</Button>
)}
</Group>
}
/>
@@ -546,9 +527,9 @@ export default function CustomerDetailPage() {
error={
bookingsQuery.isError
? {
message: "Failed to load bookings.",
onRetry: () => void bookingsQuery.refetch(),
}
message: "Failed to load bookings.",
onRetry: () => void bookingsQuery.refetch(),
}
: undefined
}
/>
@@ -567,9 +548,9 @@ export default function CustomerDetailPage() {
error={
documentsQuery.isError
? {
message: "Failed to load documents.",
onRetry: () => void documentsQuery.refetch(),
}
message: "Failed to load documents.",
onRetry: () => void documentsQuery.refetch(),
}
: undefined
}
/>
@@ -588,9 +569,9 @@ export default function CustomerDetailPage() {
error={
paymentsQuery.isError
? {
message: "Failed to load payments.",
onRetry: () => void paymentsQuery.refetch(),
}
message: "Failed to load payments.",
onRetry: () => void paymentsQuery.refetch(),
}
: undefined
}
/>

View File

@@ -1,23 +1,34 @@
import { useEffect, useRef } from "react";
import { createRoot, type Root } from "react-dom/client";
import { useEffect, useRef } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import {
UserManagementApp,
type UserManagementRuntimeOptions,
type UserManagementSessionSeed,
} from "@tria-plc/iamui";
} from '@tria-plc/iamui';
import { iamConfig } from './iamConfig';
import { getCookie } from "@/auth/cookies";
function readCookieValue(name: string): string | null {
const escaped = name.replace(/([.$?*|{}()[\]\\/+^])/g, '\\$1');
const match = document.cookie.match(
new RegExp(`(?:^|; )${escaped}=([^;]*)`),
);
import { iamConfig } from "./iamConfig";
return match ? decodeURIComponent(match[1]) : null;
}
function readInitialSession(): UserManagementSessionSeed | null {
const token = getCookie("auth-token");
const token =
localStorage.getItem('fhc-backoffice-auth-token') ??
readCookieValue('auth-token');
if (!token) {
return null;
}
const refreshToken = getCookie("refresh-token") ?? undefined;
const refreshToken =
localStorage.getItem('fhc-backoffice-auth-refresh-token') ??
readCookieValue('refresh-token') ??
undefined;
return {
token,
@@ -47,15 +58,14 @@ export default function UserManagementHostPage() {
rootRef.current = createRoot(mountNode);
}
const apiBaseUrl = import.meta.env.VITE_BASE_API_URL.replace(/\/+$/, "");
const iamApiUrl = "/um-api";
const apiBaseUrl = import.meta.env.VITE_BASE_API_URL.replace(/\/+$/, '');
const runtime: UserManagementRuntimeOptions = {
basename: "/um",
basename: '/um',
apiBaseUrl,
apiUrl: iamApiUrl,
recordApiUrl: iamApiUrl,
chronicleUrl: iamApiUrl,
auditApiUrl: iamApiUrl,
apiUrl: `${apiBaseUrl}/api`,
recordApiUrl: `${apiBaseUrl}/api`,
chronicleUrl: `${apiBaseUrl}/api`,
auditApiUrl: `${apiBaseUrl}/api`,
};
rootRef.current.render(
@@ -78,5 +88,5 @@ export default function UserManagementHostPage() {
};
}, []);
return <div ref={mountRef} style={{ position: "fixed", inset: 0 }} />;
return <div ref={mountRef} style={{ position: 'fixed', inset: 0 }} />;
}

View File

@@ -379,6 +379,9 @@ const FirstMilePage = () => {
mutationFn: async ({ reference, vehicleId }: { reference: string; vehicleId: string | null }) => {
const res = await firstMileService.accept(reference);
const created = res.data;
if (!created?.id) {
throw new Error("First-mile leg was not created for this booking.");
}
if (vehicleId) await firstMileService.update(created.id, { vehicleId });
return created;
},
@@ -387,8 +390,11 @@ const FirstMilePage = () => {
toast({ title: "Booking accepted", description: "First-mile leg created successfully." });
closeAccept();
},
onError: () => {
toast({ title: "Accept failed", variant: "destructive" });
onError: (err: unknown) => {
const description =
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
(err instanceof Error ? err.message : undefined);
toast({ title: "Accept failed", description, variant: "destructive" });
},
});
@@ -407,7 +413,6 @@ const FirstMilePage = () => {
paidBookings.filter(
(booking) =>
booking.tradeDirection === "EXPORT" &&
Boolean(booking.firstMilePickupAddress?.trim()) &&
!existingFirstMileBookingIds.has(booking.id),
),
[existingFirstMileBookingIds, paidBookings],

View File

@@ -11,97 +11,9 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
const streamBrowserifyPath = require.resolve("stream-browserify");
function createIamApiAdapter(apiBaseUrl: string): Plugin {
const upstreamBaseUrl = `${apiBaseUrl.replace(/\/+$/, "")}/api`;
return {
name: "iam-api-adapter",
configureServer(server) {
server.middlewares.use("/um-api", async (req, res) => {
const requestPath = req.url ?? "/";
const normalizedPath = requestPath.replace(/^\/+/, "");
const targetUrl = new URL(normalizedPath, `${upstreamBaseUrl}/`);
try {
const headers = new Headers();
for (const [key, value] of Object.entries(req.headers)) {
if (!value || key.toLowerCase() === "host") {
continue;
}
if (Array.isArray(value)) {
for (const item of value) {
headers.append(key, item);
}
continue;
}
headers.set(key, value);
}
const body =
req.method === "GET" || req.method === "HEAD"
? undefined
: await new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = [];
req.on("data", (chunk) =>
chunks.push(
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk),
),
);
req.on("end", () => resolve(Buffer.concat(chunks)));
req.on("error", reject);
});
const upstreamResponse = await fetch(targetUrl, {
method: req.method,
headers,
body,
});
if (targetUrl.pathname.endsWith("/auth/me")) {
const payload = await upstreamResponse.json();
const unwrappedPayload =
payload &&
typeof payload === "object" &&
"success" in payload &&
"data" in payload
? payload.data
: payload;
res.statusCode = upstreamResponse.status;
res.setHeader("content-type", "application/json; charset=utf-8");
res.end(JSON.stringify(unwrappedPayload));
return;
}
res.statusCode = upstreamResponse.status;
upstreamResponse.headers.forEach((value, key) => {
res.setHeader(key, value);
});
res.end(Buffer.from(await upstreamResponse.arrayBuffer()));
} catch (error) {
server.ssrFixStacktrace(error as Error);
res.statusCode = 502;
res.setHeader("content-type", "application/json; charset=utf-8");
res.end(
JSON.stringify({
message: "Failed to forward IAM request",
}),
);
}
});
},
};
}
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, __dirname, "");
const apiBaseUrl =
env.VITE_BASE_API_URL?.trim() || "http://localhost:3000";
return {
plugins: [react(), tailwindcss(), createIamApiAdapter(apiBaseUrl)],
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),