mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 09:13:11 +00:00
- SH-336: add the dispatcher-scope picker (All / My WOs / individual) for Scheduler/Admin via a new viewAllDispatchersOnDashboard role gate, a static 'My WOs' label for Dispatcher, and hide the dispatcher tables (and skip their queries) when the viewer cannot see all dispatchers. Scope drives dispatcherId on stats, workload, performance, and regions (SH-347 contract). - SH-352: add the Status Distribution module with its empty state and status drill-down. - SH-347: show Assigned/Completed counts and colour the completion rate (green >=90, amber 70-89, red <70) on Dispatcher Performance. - SH-348: drill a region bar into the board region filter; hold the unconfirmed 'Unmapped/Other' bucket off the chart. - SH-349: label Vendor Insights as all-time, company-wide. - Make the drilldown-filters effect fire only on URL change; add a deterministic clock to the status-drilldown test; add the dashboard visual regression spec and baselines.
256 lines
6.9 KiB
TypeScript
256 lines
6.9 KiB
TypeScript
import { expect, test, type Page, type Route } from "@playwright/test";
|
|
|
|
const FROZEN_NOW = "2026-08-19T15:00:00.000Z";
|
|
|
|
function testUser(userRoles: string) {
|
|
return {
|
|
token: "dashboard-visual-token",
|
|
expiration: "2030-01-01T00:00:00.000Z",
|
|
email: "admin@seahavenind.com",
|
|
userRoles,
|
|
phoneNumber: "5551234567",
|
|
fullname: "Dashboard Visual User",
|
|
id: "1",
|
|
};
|
|
}
|
|
|
|
const DISPATCHERS = [
|
|
{ id: "1", name: "Pat Dispatcher" },
|
|
{ id: "2", name: "Riley Chen" },
|
|
{ id: "3", name: "Sam Okafor" },
|
|
];
|
|
|
|
const STATS = {
|
|
total: 128,
|
|
open: 52,
|
|
notDispatched: 8,
|
|
completed: 64,
|
|
dueCount: 40,
|
|
completedDueCount: 30,
|
|
completionRate: 82,
|
|
averageResolutionDays: 3.4,
|
|
scheduledTomorrow: 6,
|
|
pendingUplifts: 4,
|
|
avetaPending: 3,
|
|
breakdown: [
|
|
{ status: "PM", count: 24 },
|
|
{ status: "Emergency", count: 6 },
|
|
{ status: "Reactive", count: 12 },
|
|
{ status: "Overdue", count: 9 },
|
|
],
|
|
statusDistribution: [
|
|
{ status: "Scheduled", count: 30 },
|
|
{ status: "In Progress", count: 18 },
|
|
{ status: "Completed", count: 64 },
|
|
{ status: "Pending", count: 16 },
|
|
],
|
|
};
|
|
|
|
const WORKLOAD = {
|
|
items: [
|
|
{
|
|
dispatcherId: "1",
|
|
dispatcherName: "Pat Dispatcher",
|
|
openWorkOrders: 12,
|
|
totalWorkOrders: 20,
|
|
},
|
|
{ dispatcherId: "2", dispatcherName: "Riley Chen", openWorkOrders: 7, totalWorkOrders: 15 },
|
|
],
|
|
page: 1,
|
|
pageSize: 10,
|
|
totalDispatchers: 2,
|
|
};
|
|
|
|
const PERFORMANCE = {
|
|
items: [
|
|
{
|
|
dispatcherId: "1",
|
|
dispatcherName: "Pat Dispatcher",
|
|
completionRate: 94,
|
|
averageResolutionDays: 2.5,
|
|
assignedCount: 20,
|
|
completedCount: 18,
|
|
},
|
|
{
|
|
dispatcherId: "2",
|
|
dispatcherName: "Riley Chen",
|
|
completionRate: 78,
|
|
averageResolutionDays: 3.1,
|
|
assignedCount: 15,
|
|
completedCount: 11,
|
|
},
|
|
{
|
|
dispatcherId: "3",
|
|
dispatcherName: "Sam Okafor",
|
|
completionRate: 61,
|
|
averageResolutionDays: 4.4,
|
|
assignedCount: 9,
|
|
completedCount: 5,
|
|
},
|
|
],
|
|
page: 1,
|
|
pageSize: 10,
|
|
totalDispatchers: 3,
|
|
};
|
|
|
|
const REGIONS = {
|
|
items: [
|
|
{ region: "East", workOrderCount: 44 },
|
|
{ region: "Central", workOrderCount: 31 },
|
|
{ region: "West", workOrderCount: 22 },
|
|
{ region: "California", workOrderCount: 18 },
|
|
{ region: "Unmapped/Other", workOrderCount: 5 },
|
|
],
|
|
};
|
|
|
|
const VENDOR_INSIGHTS = {
|
|
items: [
|
|
{
|
|
vendorId: "101",
|
|
vendorName: "Gateway Plumbing",
|
|
completionRate: 88,
|
|
rescheduleRate: 12,
|
|
averageResolutionDays: 2.9,
|
|
totalJobs: 40,
|
|
},
|
|
{
|
|
vendorId: "102",
|
|
vendorName: "Summit HVAC",
|
|
completionRate: 76,
|
|
rescheduleRate: 20,
|
|
averageResolutionDays: 3.6,
|
|
totalJobs: 28,
|
|
},
|
|
],
|
|
};
|
|
|
|
const TREND = {
|
|
granularity: "week",
|
|
buckets: [
|
|
{
|
|
date: "2026-07-27",
|
|
label: "Jul 27",
|
|
total: 18,
|
|
open: 4,
|
|
completed: 12,
|
|
canceled: 1,
|
|
overdue: 1,
|
|
isCurrent: false,
|
|
},
|
|
{
|
|
date: "2026-08-03",
|
|
label: "Aug 3",
|
|
total: 22,
|
|
open: 6,
|
|
completed: 13,
|
|
canceled: 1,
|
|
overdue: 2,
|
|
isCurrent: false,
|
|
},
|
|
{
|
|
date: "2026-08-10",
|
|
label: "Aug 10",
|
|
total: 26,
|
|
open: 8,
|
|
completed: 15,
|
|
canceled: 0,
|
|
overdue: 3,
|
|
isCurrent: false,
|
|
},
|
|
{
|
|
date: "2026-08-17",
|
|
label: "Aug 17",
|
|
total: 20,
|
|
open: 9,
|
|
completed: 9,
|
|
canceled: 1,
|
|
overdue: 1,
|
|
isCurrent: true,
|
|
},
|
|
],
|
|
};
|
|
|
|
async function fulfillJson(route: Route, body: unknown, status = 200) {
|
|
await route.fulfill({ status, contentType: "application/json", body: JSON.stringify(body) });
|
|
}
|
|
|
|
async function mockDashboardApi(page: Page, userRoles: string) {
|
|
await page.clock.setFixedTime(new Date(FROZEN_NOW));
|
|
await page.addInitScript(
|
|
(user) => localStorage.setItem("auth", JSON.stringify(user)),
|
|
testUser(userRoles),
|
|
);
|
|
|
|
await page.route("**/api/**", async (route) => {
|
|
const pathname = new URL(route.request().url()).pathname;
|
|
|
|
// Only stub real backend calls; let Vite dev module requests (e.g.
|
|
// /src/domain/auth/api/*.ts) through untouched.
|
|
if (!pathname.startsWith("/api/")) {
|
|
await route.continue();
|
|
return;
|
|
}
|
|
|
|
if (pathname.includes("/Dashboard/Stats")) return fulfillJson(route, { data: STATS });
|
|
if (pathname.includes("/Dashboard/Workload")) return fulfillJson(route, { data: WORKLOAD });
|
|
if (pathname.includes("/Dashboard/Performance"))
|
|
return fulfillJson(route, { data: PERFORMANCE });
|
|
if (pathname.includes("/Dashboard/Regions")) return fulfillJson(route, { data: REGIONS });
|
|
if (pathname.includes("/Dashboard/VendorInsights")) {
|
|
return fulfillJson(route, { data: VENDOR_INSIGHTS });
|
|
}
|
|
if (pathname.includes("/Dashboard/Trend")) return fulfillJson(route, { data: TREND });
|
|
if (pathname.includes("/workorders/lookups/dispatchers")) {
|
|
return fulfillJson(route, { data: DISPATCHERS });
|
|
}
|
|
|
|
return fulfillJson(route, { data: [] });
|
|
});
|
|
}
|
|
|
|
async function openDashboard(page: Page, userRoles: string) {
|
|
await mockDashboardApi(page, userRoles);
|
|
await page.goto("/");
|
|
await page.evaluate(() => document.fonts.ready);
|
|
await page.addStyleTag({
|
|
content: `
|
|
*, *::before, *::after { transition: none !important; animation: none !important; }
|
|
.MuiDrawer-paper { transform: none !important; }
|
|
.Toastify__toast-container { display: none !important; }
|
|
.tsqd-parent-container { display: none !important; }
|
|
`,
|
|
});
|
|
}
|
|
|
|
async function expectStableScreenshot(page: Page, name: string) {
|
|
await page.waitForTimeout(250);
|
|
await page.evaluate(
|
|
() =>
|
|
new Promise<void>((resolve) =>
|
|
requestAnimationFrame(() => requestAnimationFrame(() => resolve())),
|
|
),
|
|
);
|
|
await expect(page).toHaveScreenshot(name, { fullPage: true });
|
|
}
|
|
|
|
test.describe("Dashboard deterministic pixel regression", () => {
|
|
test("admin dashboard", async ({ page }) => {
|
|
await openDashboard(page, "Admin");
|
|
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
|
|
await expect(page.getByText("Status Distribution")).toBeVisible();
|
|
await expect(page.getByText("Dispatcher Performance")).toBeVisible();
|
|
// The scope picker is available to Admin/Scheduler (SH-336).
|
|
await expect(page.getByRole("button", { name: /All dispatchers/ })).toBeVisible();
|
|
await expectStableScreenshot(page, "dashboard-admin.png");
|
|
});
|
|
|
|
test("dispatcher scoped dashboard", async ({ page }) => {
|
|
await openDashboard(page, "Dispatcher");
|
|
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
|
|
// A Dispatcher gets the static "My WOs" label and no dispatcher tables (SH-336).
|
|
await expect(page.getByText("My WOs")).toBeVisible();
|
|
await expect(page.getByText("Dispatcher Performance")).toHaveCount(0);
|
|
await expect(page.getByText("Status Distribution")).toBeVisible();
|
|
await expectStableScreenshot(page, "dashboard-dispatcher.png");
|
|
});
|
|
});
|