fix(dashboard): use color-coded PoStatusBadge on submitter and manager dashboards
The Technical/Manning dashboard's Recent Orders table was rendering all PO statuses as a hardcoded gray pill span. The Manager dashboard's Recent Approved Orders table was similarly hardcoded to success-green for every row, regardless of actual state (Approved vs Sent for Payment vs Paid). Replace both with the existing PoStatusBadge component which maps each PO status to the correct CVA variant (success/warning/danger/default/outline) per the design-system colour palette defined in PO_STATUS_VARIANTS. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c97e4597dd
commit
4737edcee9
2 changed files with 103 additions and 7 deletions
|
|
@ -2,7 +2,8 @@ import { auth } from "@/auth";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { StatCard } from "@/components/dashboard/stat-card";
|
import { StatCard } from "@/components/dashboard/stat-card";
|
||||||
import { SpendCharts } from "@/components/dashboard/spend-charts";
|
import { SpendCharts } from "@/components/dashboard/spend-charts";
|
||||||
import { formatCurrency, formatDate, PO_STATUS_LABELS } from "@/lib/utils";
|
import { PoStatusBadge } from "@/components/po/po-status-badge";
|
||||||
|
import { formatCurrency, formatDate } from "@/lib/utils";
|
||||||
import { FileText, Clock, CheckCircle, DollarSign } from "lucide-react";
|
import { FileText, Clock, CheckCircle, DollarSign } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
@ -89,9 +90,7 @@ async function SubmitterDashboard({ userId }: { userId: string }) {
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-neutral-900 max-w-xs truncate">{po.title}</td>
|
<td className="px-4 py-3 text-neutral-900 max-w-xs truncate">{po.title}</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<span className="rounded-full bg-neutral-100 px-2.5 py-0.5 text-xs font-medium text-neutral-700">
|
<PoStatusBadge status={po.status} />
|
||||||
{PO_STATUS_LABELS[po.status]}
|
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-right font-mono text-xs">{formatCurrency(Number(po.totalAmount))}</td>
|
<td className="px-4 py-3 text-right font-mono text-xs">{formatCurrency(Number(po.totalAmount))}</td>
|
||||||
<td className="px-4 py-3 text-neutral-500">{formatDate(po.updatedAt)}</td>
|
<td className="px-4 py-3 text-neutral-500">{formatDate(po.updatedAt)}</td>
|
||||||
|
|
@ -207,9 +206,7 @@ async function ManagerDashboard() {
|
||||||
<td className="px-4 py-3 text-neutral-900 max-w-xs truncate">{po.title}</td>
|
<td className="px-4 py-3 text-neutral-900 max-w-xs truncate">{po.title}</td>
|
||||||
<td className="px-4 py-3 text-neutral-600">{po.vessel.name}</td>
|
<td className="px-4 py-3 text-neutral-600">{po.vessel.name}</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<span className="rounded-full bg-success-100 px-2.5 py-0.5 text-xs font-medium text-success-700">
|
<PoStatusBadge status={po.status} />
|
||||||
{PO_STATUS_LABELS[po.status]}
|
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-right font-mono text-xs font-semibold">{formatCurrency(Number(po.totalAmount))}</td>
|
<td className="px-4 py-3 text-right font-mono text-xs font-semibold">{formatCurrency(Number(po.totalAmount))}</td>
|
||||||
<td className="px-4 py-3 text-neutral-500">{po.approvedAt ? formatDate(po.approvedAt) : "—"}</td>
|
<td className="px-4 py-3 text-neutral-500">{po.approvedAt ? formatDate(po.approvedAt) : "—"}</td>
|
||||||
|
|
|
||||||
99
App/pelagia-portal/tests/e2e/dashboard-status-badges.spec.ts
Normal file
99
App/pelagia-portal/tests/e2e/dashboard-status-badges.spec.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test.describe("Dashboard – PO status badges", () => {
|
||||||
|
test("Technical user dashboard shows color-coded status badges", async ({ page }) => {
|
||||||
|
// Log in as a technical user
|
||||||
|
await page.goto("/login");
|
||||||
|
await page.getByLabel("Email address").fill("tech@pelagia.local");
|
||||||
|
await page.getByLabel("Password").fill("tech1234");
|
||||||
|
await page.getByRole("button", { name: /sign in/i }).click();
|
||||||
|
await expect(page).toHaveURL(/\/dashboard/);
|
||||||
|
|
||||||
|
// The Recent Orders table must be present
|
||||||
|
const table = page.locator("table");
|
||||||
|
const rowCount = await table.locator("tbody tr").count();
|
||||||
|
|
||||||
|
if (rowCount === 0) {
|
||||||
|
// No orders seeded for this user – skip badge assertion but confirm page renders
|
||||||
|
await expect(page.getByText("Dashboard")).toBeVisible();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every status cell must contain a badge rendered by PoStatusBadge.
|
||||||
|
// PoStatusBadge renders a <span> with rounded-full and one of the
|
||||||
|
// CVA variant classes. The old hardcoded span used bg-neutral-100;
|
||||||
|
// the new component will never emit that class for non-secondary/default states.
|
||||||
|
// We verify that at least one badge is NOT neutral (i.e. coloured).
|
||||||
|
const statusCells = table.locator("tbody tr td:nth-child(3) span");
|
||||||
|
const count = await statusCells.count();
|
||||||
|
expect(count).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Take a screenshot for visual confirmation
|
||||||
|
await page.screenshot({ path: "test-results/dashboard-tech-badges.png", fullPage: false });
|
||||||
|
|
||||||
|
// Verify each badge has the rounded-full pill shape (all PoStatusBadge variants share this)
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
await expect(statusCells.nth(i)).toHaveClass(/rounded-full/);
|
||||||
|
}
|
||||||
|
|
||||||
|
// At least one badge should carry a colour class that is NOT neutral-100
|
||||||
|
// (proving the new component is active, not the old hardcoded span)
|
||||||
|
const allClasses: string[] = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const cls = await statusCells.nth(i).getAttribute("class") ?? "";
|
||||||
|
allClasses.push(cls);
|
||||||
|
}
|
||||||
|
const hasColoredBadge = allClasses.some(
|
||||||
|
(cls) =>
|
||||||
|
cls.includes("success") ||
|
||||||
|
cls.includes("warning") ||
|
||||||
|
cls.includes("danger") ||
|
||||||
|
cls.includes("primary") ||
|
||||||
|
cls.includes("border") // outline variant
|
||||||
|
);
|
||||||
|
// If all POs happen to be in a secondary/closed state this may be false;
|
||||||
|
// in that case we at least confirm the old bg-neutral-100 inline span is gone.
|
||||||
|
const hasOldHardcodedBadge = allClasses.some(
|
||||||
|
(cls) => cls === "rounded-full bg-neutral-100 px-2.5 py-0.5 text-xs font-medium text-neutral-700"
|
||||||
|
);
|
||||||
|
expect(hasOldHardcodedBadge).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Manager dashboard shows color-coded status badges on approved orders", async ({ page }) => {
|
||||||
|
await page.goto("/login");
|
||||||
|
await page.getByLabel("Email address").fill("manager@pelagia.local");
|
||||||
|
await page.getByLabel("Password").fill("manager1234");
|
||||||
|
await page.getByRole("button", { name: /sign in/i }).click();
|
||||||
|
await expect(page).toHaveURL(/\/dashboard/);
|
||||||
|
|
||||||
|
await page.screenshot({ path: "test-results/dashboard-manager-badges.png", fullPage: false });
|
||||||
|
|
||||||
|
const table = page.locator("table");
|
||||||
|
const rowCount = await table.locator("tbody tr").count();
|
||||||
|
|
||||||
|
if (rowCount === 0) {
|
||||||
|
await expect(page.getByText("Dashboard")).toBeVisible();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status column is 4th column in manager table (PO | Title | Cost Centre | Status | Amount | Approved)
|
||||||
|
const statusCells = table.locator("tbody tr td:nth-child(4) span");
|
||||||
|
const count = await statusCells.count();
|
||||||
|
expect(count).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
await expect(statusCells.nth(i)).toHaveClass(/rounded-full/);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The old hardcoded manager badge was always success — verify it's gone
|
||||||
|
const allClasses: string[] = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const cls = await statusCells.nth(i).getAttribute("class") ?? "";
|
||||||
|
allClasses.push(cls);
|
||||||
|
}
|
||||||
|
const hasOldHardcoded = allClasses.some(
|
||||||
|
(cls) => cls === "rounded-full bg-success-100 px-2.5 py-0.5 text-xs font-medium text-success-700"
|
||||||
|
);
|
||||||
|
expect(hasOldHardcoded).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue