refactor(ui): real ids in client/order detail URLs; drop mock route stubs

Client and order detail were addressed by in-session state only, with
hardcoded CUS-4702 / ORD-10519 placeholder paths — a browser refresh lost
the entity. Now the URL carries the numeric id like employees/products:
- routeFromPath/pathFromRoute parse and emit /departments/sales/clients/:id
  and /departments/sales/orders/:id; RouteState carries clientId/orderId.
- App seeds selected ids from the initial route and restores them on
  popstate, so deep-link + back/forward + refresh work.
- routes.ts breadcrumb labels for these are now generic ("Клиент"/"Заказ")
  instead of the fake name/code.

Memory: recorded the "don't propose FirePage content" preference.
Typecheck + vitest (17) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
AndreyandClaude Opus 4.8 committed 2026-06-30 00:04:05 +03:00
1 parent c99bfdd828
commit e35e00ea8c
4 files changed
+38 -13

No files matched your search

+4 -2
View File
@@ -26,8 +26,8 @@ export function App() {
const [selectedAgentId, setSelectedAgentId] = useState<number | null>(initialRoute.agentId);
const [selectedReleaseId, setSelectedReleaseId] = useState<number | null>(initialRoute.releaseId);
const [selectedConversationId, setSelectedConversationId] = useState<number | null>(null);
const [selectedClientId, setSelectedClientId] = useState<number | null>(null);
const [selectedOrderId, setSelectedOrderId] = useState<number | null>(null);
const [selectedClientId, setSelectedClientId] = useState<number | null>(initialRoute.clientId);
const [selectedOrderId, setSelectedOrderId] = useState<number | null>(initialRoute.orderId);
const [data, setData] = useState<AppData>({ employees: [], departments: [], products: [], agents: [], releases: [] });
const [dataError, setDataError] = useState(false);
@@ -103,6 +103,8 @@ export function App() {
setSelectedProductCode(nextRoute.productCode);
setSelectedAgentId(nextRoute.agentId);
setSelectedReleaseId(nextRoute.releaseId);
setSelectedClientId(nextRoute.clientId);
setSelectedOrderId(nextRoute.orderId);
};
window.addEventListener("popstate", onPopState);
return () => window.removeEventListener("popstate", onPopState);
+19 -4
View File
@@ -4,7 +4,7 @@ import { pathFromRoute, routeFromPath } from "./router";
describe("product routes", () => {
it("parses a product detail URL", () => {
expect(routeFromPath("/products/42")).toEqual({ route: "productDetail", employeeId: null, productId: 42, productCode: null, agentId: null, releaseId: null });
expect(routeFromPath("/products/42")).toEqual({ route: "productDetail", employeeId: null, productId: 42, productCode: null, agentId: null, releaseId: null, clientId: null, orderId: null });
});
it("creates a product detail URL", () => {
@@ -12,9 +12,24 @@ describe("product routes", () => {
});
});
describe("sales detail routes", () => {
it("parses a client detail URL", () => {
expect(routeFromPath("/departments/sales/clients/15")).toEqual({ route: "salesClientDetail", employeeId: null, productId: null, productCode: null, agentId: null, releaseId: null, clientId: 15, orderId: null });
});
it("parses an order detail URL", () => {
expect(routeFromPath("/departments/sales/orders/8")).toEqual({ route: "salesOrderDetail", employeeId: null, productId: null, productCode: null, agentId: null, releaseId: null, clientId: null, orderId: 8 });
});
it("creates client and order detail URLs", () => {
expect(pathFromRoute("salesClientDetail", 15)).toBe("/departments/sales/clients/15");
expect(pathFromRoute("salesOrderDetail", 8)).toBe("/departments/sales/orders/8");
});
});
describe("ai agent routes", () => {
it("parses an AI agent detail URL", () => {
expect(routeFromPath("/ai/agents/7")).toEqual({ route: "aiAgentDetail", employeeId: null, productId: null, productCode: null, agentId: 7, releaseId: null });
expect(routeFromPath("/ai/agents/7")).toEqual({ route: "aiAgentDetail", employeeId: null, productId: null, productCode: null, agentId: 7, releaseId: null, clientId: null, orderId: null });
});
it("creates an AI agent detail URL", () => {
@@ -22,7 +37,7 @@ describe("ai agent routes", () => {
});
it("parses an AI agent creation URL", () => {
expect(routeFromPath("/ai/agents/new", "?product=academy")).toEqual({ route: "aiAgentCreate", employeeId: null, productId: null, productCode: "academy", agentId: null, releaseId: null });
expect(routeFromPath("/ai/agents/new", "?product=academy")).toEqual({ route: "aiAgentCreate", employeeId: null, productId: null, productCode: "academy", agentId: null, releaseId: null, clientId: null, orderId: null });
});
it("creates an AI agent creation URL", () => {
@@ -32,7 +47,7 @@ describe("ai agent routes", () => {
describe("ai release routes", () => {
it("parses a release URL", () => {
expect(routeFromPath("/ai/releases/12")).toEqual({ route: "aiRelease", employeeId: null, productId: null, productCode: null, agentId: null, releaseId: 12 });
expect(routeFromPath("/ai/releases/12")).toEqual({ route: "aiRelease", employeeId: null, productId: null, productCode: null, agentId: null, releaseId: 12, clientId: null, orderId: null });
});
it("creates a release URL", () => {
+13 -5
View File
@@ -7,19 +7,27 @@ export type RouteState = {
productCode: string | null;
agentId: number | null;
releaseId: number | null;
clientId: number | null;
orderId: number | null;
};
export function routeFromPath(pathname: string, search = ""): RouteState {
const path = pathname.replace(/\/+$/, "") || "/";
const base = { employeeId: null, productId: null, productCode: null, agentId: null, releaseId: null };
const base = { employeeId: null, productId: null, productCode: null, agentId: null, releaseId: null, clientId: null, orderId: null };
if (path === "/" || path === "/command") return { route: "command", ...base };
if (path === "/departments") return { route: "departments", ...base };
if (path === "/departments/sales") return { route: "salesOverview", ...base };
if (path === "/departments/sales/clients") return { route: "salesClients", ...base };
if (path === "/departments/sales/clients/CUS-4702") return { route: "salesClientDetail", ...base };
if (path.startsWith("/departments/sales/clients/")) {
const id = Number(path.split("/")[4]);
return Number.isInteger(id) && id > 0 ? { ...base, route: "salesClientDetail", clientId: id } : { route: "salesClients", ...base };
}
if (path === "/departments/sales/dialogs") return { route: "salesDialogs", ...base };
if (path === "/departments/sales/orders") return { route: "salesOrders", ...base };
if (path === "/departments/sales/orders/ORD-10519") return { route: "salesOrderDetail", ...base };
if (path.startsWith("/departments/sales/orders/")) {
const id = Number(path.split("/")[4]);
return Number.isInteger(id) && id > 0 ? { ...base, route: "salesOrderDetail", orderId: id } : { route: "salesOrders", ...base };
}
if (path === "/employees") return { route: "employees", ...base };
if (path.startsWith("/employees/")) {
const id = Number(path.split("/")[2]);
@@ -55,10 +63,10 @@ export function pathFromRoute(route: RouteKey, entityId: number | null = null, p
if (route === "departments") return "/departments";
if (route === "salesOverview") return "/departments/sales";
if (route === "salesClients") return "/departments/sales/clients";
if (route === "salesClientDetail") return "/departments/sales/clients/CUS-4702";
if (route === "salesClientDetail") return entityId ? `/departments/sales/clients/${entityId}` : "/departments/sales/clients";
if (route === "salesDialogs") return "/departments/sales/dialogs";
if (route === "salesOrders") return "/departments/sales/orders";
if (route === "salesOrderDetail") return "/departments/sales/orders/ORD-10519";
if (route === "salesOrderDetail") return entityId ? `/departments/sales/orders/${entityId}` : "/departments/sales/orders";
if (route === "employees") return "/employees";
if (route === "employeeDetail") return entityId ? `/employees/${entityId}` : "/employees";
if (route === "products") return "/products";
+2 -2
View File
@@ -8,10 +8,10 @@ export const routes: Record<RouteKey, string> = {
productDetail: "Продукты",
products: "Продукты",
profile: "Профиль",
salesClientDetail: "Елена Кузнецова",
salesClientDetail: "Клиент",
salesClients: "Клиенты",
salesDialogs: "Диалоги",
salesOrderDetail: "ORD-10519",
salesOrderDetail: "Заказ",
salesOrders: "Продажи",
salesOverview: "Обзор",
aiAgents: "AI-агенты",