Commit 30587791 by krds-arun

feat(panel): add module dashboards and header module search

- Add Modules page with search/filter and card grid
- Add header center module search with autocomplete dropdown
- Add module dashboards (admin/incidents/permit/audits/inspection/checklist/suggestion/tracking)
- Add module dashboard menu navigation component and stories

Made-with: Cursor
parent 7d260d1c
import { AdminDashboard } from "@/components/panel/admin/AdminDashboard/AdminDashboard";
export default function Page() {
return <AdminDashboard />;
}
import { AuditsDashboard } from "@/components/panel/audits/AuditsDashboard/AuditsDashboard";
export default function Page() {
return <AuditsDashboard />;
}
import { ChecklistDashboard } from "@/components/panel/checklist/ChecklistDashboard/ChecklistDashboard";
export default function Page() {
return <ChecklistDashboard />;
}
import { IncidentsDashboard } from "@/components/panel/incidents/IncidentsDashboard/IncidentsDashboard";
export default function Page() {
return <IncidentsDashboard />;
}
import { InspectionDashboard } from "@/components/panel/inspection/InspectionDashboard/InspectionDashboard";
export default function Page() {
return <InspectionDashboard />;
}
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import { Sidebar } from "@/components/layout/Sidebar/Sidebar";
import { Header } from "@/components/layout/Header/Header";
import { Footer } from "@/components/layout/Footer/Footer";
import { BottomTab } from "@/components/layout/BottomTab/BottomTab";
import { NotificationsPanel } from "@/components/widgets/NotificationsPanel/NotificationsPanel";
import { ProfilePanelHeader } from "@/components/widgets/ProfilePanelHeader/ProfilePanelHeader";
import { QuickAction } from "@/components/widgets/QuickAction/QuickAction";
import { LocationSelector } from "@/components/widgets/LocationSelector/LocationSelector";
import { PanelModuleSearch } from "@/components/widgets/PanelModuleSearch/PanelModuleSearch";
import { panelNotificationSections } from "./panel-notifications";
import "./panel.css";
export default function PanelLayout({
children,
}: {
children: React.ReactNode;
}) {
const router = useRouter();
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const isInteractiveTarget = useMemo(() => {
return (target: EventTarget | null) => {
if (!(target instanceof Element)) return false;
return Boolean(target.closest("a, button, input, textarea, select, [role='button']"));
};
}, []);
useEffect(() => {
let startX = 0;
let startY = 0;
let tracking = false;
const onTouchStart = (event: TouchEvent) => {
if (mobileSidebarOpen) return;
const touch = event.touches[0];
if (!touch) return;
// Left-edge swipe zone (tablet + mobile friendly)
if (touch.clientX > 32) return;
startX = touch.clientX;
startY = touch.clientY;
tracking = true;
};
const onTouchMove = (event: TouchEvent) => {
if (!tracking) return;
const touch = event.touches[0];
if (!touch) return;
const dx = touch.clientX - startX;
const dy = touch.clientY - startY;
if (dx > 64 && Math.abs(dy) < 42) {
tracking = false;
const useDrawer = window.matchMedia("(max-width: 1024px)").matches;
if (useDrawer) {
setMobileSidebarOpen(true);
} else if (sidebarCollapsed) {
setSidebarCollapsed(false);
}
}
};
const onTouchEnd = () => {
tracking = false;
};
window.addEventListener("touchstart", onTouchStart, { passive: true });
window.addEventListener("touchmove", onTouchMove, { passive: true });
window.addEventListener("touchend", onTouchEnd, { passive: true });
window.addEventListener("touchcancel", onTouchEnd, { passive: true });
return () => {
window.removeEventListener("touchstart", onTouchStart);
window.removeEventListener("touchmove", onTouchMove);
window.removeEventListener("touchend", onTouchEnd);
window.removeEventListener("touchcancel", onTouchEnd);
};
}, [mobileSidebarOpen, sidebarCollapsed]);
const panelHeaderRight = (
<>
<LocationSelector />
<NotificationsPanel
count={5}
sections={panelNotificationSections}
onMarkAllRead={() => {}}
onViewAll={() => {}}
onGoToPage={() => {}}
onForward={() => {}}
/>
<ProfilePanelHeader
name="Nino Lester Cruz"
role="ADMIN"
initials="NL"
onSignOut={() => router.push("/")}
/>
</>
);
return (
<div className={`maf-panel ${sidebarCollapsed ? "maf-panel--sidebar-collapsed" : ""}`.trim()}>
<aside className="maf-panel__sidebarRail">
<Sidebar />
<aside
className="maf-panel__sidebarRail"
onClick={(event) => {
if (!sidebarCollapsed) return;
if (isInteractiveTarget(event.target)) return;
setSidebarCollapsed(false);
}}
aria-label="Sidebar rail"
>
<Sidebar onLogout={() => router.push("/")} />
</aside>
<div className="maf-panel__main">
<Header hideNavMenu hideActions logoSrc="/panel-monogram-logo.png" />
<Header
hideNavMenu
hideActions
logoSrc="/maf-logo.png"
centerSlot={<PanelModuleSearch />}
panelRight={panelHeaderRight}
/>
<button
type="button"
......@@ -78,7 +179,7 @@ export default function PanelLayout({
>
×
</button>
<Sidebar />
<Sidebar onLogout={() => router.push("/")} />
</aside>
</>
) : null}
......
import { ModulesPage } from "@/components/panel/modules/ModulesPage/ModulesPage";
export default function Page() {
return <ModulesPage />;
}
import { PanelDashboard } from "@/components/panel/dashboard/PanelDashboard/PanelDashboard";
export default function Page() {
return (
<div>
<h1>Panel</h1>
</div>
);
return <PanelDashboard />;
}
\ No newline at end of file
import type { NotificationSection } from "@/components/widgets/NotificationsPanel/notifications-types";
export const panelNotificationSections: NotificationSection[] = [
{
label: "Today",
items: [
{
id: "n1",
title: "Incident #INC-2024-001 assigned to you",
description: "Requires review by 15 Apr 2024",
timeLabel: "2m ago",
tone: "alert",
},
{
id: "n2",
title: "User added successfully",
description: "Ahmed.AlAlawi@maf.ae is now active",
timeLabel: "1h ago",
tone: "success",
},
],
},
{
label: "Yesterday",
items: [
{
id: "n3",
title: "Audit submission pending approval",
description: "LEC Cashier Spot Audit — VOX AHM",
timeLabel: "Yesterday",
tone: "info",
},
],
},
{
label: "Older",
items: [
{
id: "n4",
title: "System maintenance scheduled",
description: "Planned window 02:00–04:00 GST",
timeLabel: "Mar 1",
tone: "warning",
},
],
},
];
.maf-panel {
/* MAF Theme tokens (scoped to panel) — LIGHT (white + brown) */
--maroon: #8b1538;
--maroon-deep: #6b0f2a;
--maroon-bright: #a91d47;
--gold: #c9a961;
--gold-light: #dfc07a;
--gold-pale: rgba(201, 169, 97, 0.12);
--gold-border: rgba(201, 169, 97, 0.2);
--gold-border-strong: rgba(201, 169, 97, 0.45);
/* Light neutrals to match sidebar */
--ink: #f8f6f2;
--ink-2: #ffffff;
--ink-3: #faf6f0;
--ink-4: #f1ece4;
/* Text becomes brown/ink on light surfaces */
--text-1: rgba(110, 90, 68, 0.96);
--text-2: rgba(110, 90, 68, 0.78);
--text-3: rgba(110, 90, 68, 0.62);
/* Surface tints (subtle) */
--white: rgba(110, 90, 68, 0.06);
--white-2: rgba(110, 90, 68, 0.1);
--surface-base: var(--ink);
--surface-raised: var(--ink-2);
--surface-overlay: var(--ink-3);
--border-subtle: var(--gold-border);
--border-strong: var(--gold-border-strong);
--shadow-maroon: 0 8px 32px rgba(139, 21, 56, 0.4);
--shadow-deep: 0 24px 64px rgba(16, 12, 8, 0.14);
--shadow-focus: 0 0 0 3px rgba(201, 169, 97, 0.35);
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--r-full: 9999px;
height: 100dvh;
min-height: 100dvh;
display: grid;
grid-template-columns: 280px 1fr;
background: #070a12;
background:
radial-gradient(720px 300px at 12% 0%, rgba(167, 138, 104, 0.14), transparent 62%),
linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(250, 246, 240, 0.98) 100%);
overflow: hidden;
}
......@@ -26,7 +68,9 @@
grid-template-rows: auto 1fr auto;
position: relative;
overflow: visible;
background: #05070d;
background:
radial-gradient(860px 340px at 18% 0%, rgba(167, 138, 104, 0.12), transparent 60%),
linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(250, 246, 240, 0.98) 100%);
}
.maf-panel .landing-footer {
......@@ -53,14 +97,40 @@
}
.maf-panel .landing-header__logoWrap {
width: 34px;
height: 34px;
/* Panel header uses full wordmark logo */
width: 150px;
height: 36px;
}
.maf-panel .landing-header__brand {
gap: 0.4rem;
}
/* Panel header: three-column grid (brand + module search + right widgets) */
.maf-panel .landing-header__inner {
grid-template-columns: auto 1fr auto;
}
.maf-panel .landing-header__center {
display: flex;
justify-content: center;
}
.maf-panel .landing-header__centerSlot {
width: 100%;
display: flex;
justify-content: center;
}
.maf-panel .landing-header__right {
width: 100%;
justify-content: flex-end;
}
.maf-panel .landing-header__panelRight {
flex-wrap: nowrap;
}
/* Panel footer should not show any refresh action/icon */
.maf-panel .landing-footer [aria-label*="refresh" i],
.maf-panel .landing-footer [title*="refresh" i],
......@@ -84,10 +154,10 @@
align-items: center;
justify-content: center;
border-radius: 999px;
border: 1px solid rgba(110, 90, 68, 0.3);
border: 1px solid var(--gold-border);
background: rgba(255, 255, 255, 0.96);
color: rgba(110, 90, 68, 0.9);
box-shadow: 0 8px 20px rgba(110, 90, 68, 0.18);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25);
z-index: 20;
cursor: pointer;
}
......@@ -97,21 +167,19 @@
min-height: 0;
padding: 1.35rem 1.5rem 2rem;
background: transparent;
color: rgba(255, 255, 255, 0.9);
color: var(--text-1);
overflow: auto;
-webkit-overflow-scrolling: touch;
}
.maf-panel__contentInner {
width: min(1180px, 100%);
width: 100%;
min-height: fit-content;
margin: 0 auto;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.07);
background:
radial-gradient(880px 320px at 10% 0%, rgba(196, 165, 116, 0.08), transparent 58%),
rgba(7, 10, 18, 0.62);
padding: 1rem 1.1rem;
margin: 0;
border-radius: 0;
border: 0;
background: transparent;
padding: 0;
}
@media (max-width: 1366px) and (orientation: landscape) {
......@@ -191,17 +259,48 @@
}
.maf-panel .landing-header__inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
min-height: 52px;
padding-left: 48px;
padding-right: 10px;
padding: 10px 12px 10px 48px;
box-sizing: border-box;
overflow: visible;
}
/* Hide nav column when no desktop nav (panel mode) — more reliable than :empty */
.maf-panel .landing-header__center:not(:has(nav)) {
display: none;
}
.maf-panel .landing-header__left {
flex: 0 0 auto;
min-width: 0;
}
.maf-panel .landing-header__right {
flex: 1 1 auto;
min-width: 0;
justify-content: flex-end;
}
.maf-panel .landing-header__panelRight {
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: flex-end;
gap: 6px;
max-width: min(100%, calc(100vw - 56px));
}
.maf-panel .landing-header {
z-index: 46;
overflow: visible;
}
.maf-panel .landing-header__logoWrap {
width: 32px;
width: 132px;
height: 32px;
}
......@@ -248,12 +347,16 @@
}
.maf-panel__content {
padding: 0.8rem 0.65rem 6.2rem;
/*
Safe scroll space for the floating QuickAction.
Ensures the last dashboard row can scroll fully above the FAB.
*/
padding: 0.8rem 0.65rem 10.2rem;
}
.maf-panel__contentInner {
border-radius: 10px;
padding: 0.75rem 0.8rem;
border-radius: 0;
padding: 0;
}
.maf-panel .landing-footer {
......@@ -262,7 +365,8 @@
.maf-panel .maf-quick-action__floatingWrap {
right: 0.9rem;
bottom: calc(5.7rem + env(safe-area-inset-bottom));
/* Lift well above bottom tab + content */
bottom: calc(9.2rem + env(safe-area-inset-bottom));
}
}
import { PermitDashboard } from "@/components/panel/permit/PermitDashboard/PermitDashboard";
export default function Page() {
return <PermitDashboard />;
}
import { SuggestionDashboard } from "@/components/panel/suggestion/SuggestionDashboard/SuggestionDashboard";
export default function Page() {
return <SuggestionDashboard />;
}
import { TrackingDashboard } from "@/components/panel/tracking/TrackingDashboard/TrackingDashboard";
export default function Page() {
return <TrackingDashboard />;
}
......@@ -24,6 +24,10 @@ export type HeaderProps = {
};
hideNavMenu?: boolean;
hideActions?: boolean;
/** Optional center slot (e.g. global search). Overrides nav when provided. */
centerSlot?: React.ReactNode;
/** Right-side slot for panel mode (e.g. notifications + profile). */
panelRight?: React.ReactNode;
};
function SupportGlyph({ className }: { className?: string }) {
......@@ -54,6 +58,8 @@ export function Header({
cta = { label: "Login", href: "#" },
hideNavMenu = false,
hideActions = false,
centerSlot,
panelRight,
}: HeaderProps) {
const [open, setOpen] = React.useState(false);
const openedAtRef = React.useRef(0);
......@@ -96,7 +102,9 @@ export function Header({
</div>
<div className="landing-header__center">
{!hideNavMenu ? (
{centerSlot ? (
<div className="landing-header__centerSlot">{centerSlot}</div>
) : !hideNavMenu ? (
<nav
className="landing-header__nav landing-header__nav--desktop"
aria-label="Primary"
......@@ -115,6 +123,7 @@ export function Header({
</div>
<div className="landing-header__right">
{panelRight ? <div className="landing-header__panelRight">{panelRight}</div> : null}
{!hideActions && supportCta ? (
supportCta.onClick ? (
<button
......
......@@ -71,6 +71,14 @@
gap: 10px;
}
.landing-header__panelRight {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
}
.landing-header__support {
gap: 10px;
}
......
......@@ -10,6 +10,10 @@ export type SidebarItem = {
export type SidebarProps = {
items?: SidebarItem[];
/** Called when logout is clicked; if omitted, logout is a link to `logoutHref`. */
onLogout?: () => void;
logoutHref?: string;
logoutLabel?: string;
};
function SidebarIcon({ label }: { label: string }) {
......@@ -103,6 +107,14 @@ function SidebarIcon({ label }: { label: string }) {
</svg>
);
}
if (icon === "logout") {
return (
<svg className="maf-sidebar__iconSvg" viewBox="0 0 24 24" aria-hidden>
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
<path d="M16 17l5-5-5-5M21 12H9" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
return (
<svg className="maf-sidebar__iconSvg" viewBox="0 0 24 24" aria-hidden>
......@@ -113,17 +125,20 @@ function SidebarIcon({ label }: { label: string }) {
export function Sidebar({
items = [
{ label: "Home", href: "#", active: true },
{ label: "Modules", href: "#" },
{ label: "Admin", href: "#" },
{ label: "Incidents", href: "#" },
{ label: "Permit", href: "#" },
{ label: "Audits", href: "#" },
{ label: "Inspection", href: "#" },
{ label: "Checklist", href: "#" },
{ label: "Suggestion", href: "#" },
{ label: "Tracking", href: "#" },
{ label: "Home", href: "/panel", active: true },
{ label: "Modules", href: "/panel/modules" },
{ label: "Admin", href: "/panel/admin" },
{ label: "Incidents", href: "/panel/incidents" },
{ label: "Permit", href: "/panel/permit" },
{ label: "Audits", href: "/panel/audits" },
{ label: "Inspection", href: "/panel/inspection" },
{ label: "Checklist", href: "/panel/checklist" },
{ label: "Suggestion", href: "/panel/suggestion" },
{ label: "Tracking", href: "/panel/tracking" },
],
onLogout,
logoutHref = "/",
logoutLabel = "Logout",
}: SidebarProps) {
return (
<aside className="maf-sidebar" aria-label="Sidebar navigation">
......@@ -149,6 +164,21 @@ export function Sidebar({
</span>
<span>Settings</span>
</a>
{onLogout ? (
<button type="button" className="maf-sidebar__item maf-sidebar__logout" onClick={onLogout}>
<span className="maf-sidebar__icon" aria-hidden>
<SidebarIcon label="logout" />
</span>
<span>{logoutLabel}</span>
</button>
) : (
<a className="maf-sidebar__item maf-sidebar__logout" href={logoutHref}>
<span className="maf-sidebar__icon" aria-hidden>
<SidebarIcon label="logout" />
</span>
<span>{logoutLabel}</span>
</a>
)}
</div>
</aside>
);
......
......@@ -71,5 +71,23 @@
padding-top: 0.75rem;
border-top: 1px solid rgba(110, 90, 68, 0.2);
background: linear-gradient(180deg, rgba(250, 246, 240, 0), rgba(250, 246, 240, 0.98) 30%);
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.maf-sidebar__logout {
width: 100%;
border: 0;
background: transparent;
font: inherit;
text-align: left;
cursor: pointer;
color: rgba(110, 90, 68, 0.92);
}
.maf-sidebar__logout:hover {
background: rgba(180, 60, 60, 0.08);
color: rgba(130, 45, 45, 0.95);
}
......@@ -19,6 +19,8 @@ export default meta;
type Story = StoryObj<typeof meta>;
export const Interactive: Story = {
// Render-only story; satisfy Storybook/TS when `args` is required on the inferred type.
args: {} as Story["args"],
render: () => {
const [query, setQuery] = useState("");
const [countries, setCountries] = useState<string[]>(["UAE"]);
......
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { AdminDashboard } from "./AdminDashboard";
const meta = {
title: "Panel/Modules/Admin/Dashboard",
component: AdminDashboard,
parameters: { layout: "fullscreen" },
} satisfies Meta<typeof AdminDashboard>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Tablet: Story = { parameters: { viewport: { defaultViewport: "tablet" } } };
export const Mobile: Story = { parameters: { viewport: { defaultViewport: "mobile1" } } };
"use client";
import React from "react";
import { ModuleCard } from "@/components/panel/modules/ModuleCard/ModuleCard";
import { ModuleDashboardMenu } from "@/components/panel/modules/ModuleDashboardMenu/ModuleDashboardMenu";
import "./admin-dashboard.css";
type AdminTile = {
title: string;
description: string;
href: string;
badge?: string;
};
const tiles: AdminTile[] = [
{
title: "Manage Structure & Hierarchy",
description: "Org tree, locations and access boundaries (root admin only).",
href: "/panel/admin/structure",
badge: "Root",
},
{
title: "Manage Forms, Groups & Questions",
description: "Create and maintain forms with versioned question sets.",
href: "/panel/admin/forms",
},
{
title: "Manage Incidents",
description: "Incident types, severity matrix and workflow rules.",
href: "/panel/admin/incidents",
},
{
title: "Manage Contractors",
description: "Contractor profiles, approvals, and compliance requirements.",
href: "/panel/admin/contractors",
},
{
title: "Manage Suggestion Group",
description: "Suggestion categories, routing rules and moderation settings.",
href: "/panel/admin/suggestions",
},
{
title: "Manage Notifications",
description: "Templates, recipients and delivery channels.",
href: "/panel/admin/notifications",
},
{
title: "Manage User Role",
description: "Roles, permissions and assignment policies.",
href: "/panel/admin/roles",
},
{
title: "Manage Approval Schema",
description: "Approval steps, escalation paths and SLA thresholds.",
href: "/panel/admin/approvals",
},
{
title: "Manage Training & Feedback",
description: "Training catalog, feedback loops and follow-ups.",
href: "/panel/admin/training",
},
];
export function AdminDashboard() {
return (
<div className="maf-adminDash">
<header className="maf-adminDash__header">
<div>
<h1 className="maf-adminDash__title">Admin Management</h1>
<p className="maf-adminDash__subtitle">
Configure modules, policies and system behavior. Changes here affect everyone.
</p>
</div>
</header>
<ModuleDashboardMenu
ariaLabel="Admin navigation"
items={[
{ label: "Dashboard", href: "/panel/admin", active: true },
{ label: "Users listing", href: "/panel/admin/users" },
{ label: "Add user", href: "/panel/admin/users/new" },
]}
/>
<section className="maf-adminDash__grid" aria-label="Admin management tiles">
{tiles.map((t) => (
<ModuleCard
key={t.title}
title={t.title}
description={t.description}
href={t.href}
badge={t.badge}
meta="Admin tools"
/>
))}
</section>
</div>
);
}
.maf-adminDash {
width: 100%;
padding: 1.1rem 1.1rem 1.2rem;
box-sizing: border-box;
min-width: 0;
}
.maf-adminDash__header {
padding: 0.2rem 0.2rem 0.8rem;
}
.maf-adminDash__title {
margin: 0;
font-size: 1.35rem;
font-weight: 920;
letter-spacing: -0.02em;
color: rgba(46, 39, 32, 0.96);
}
.maf-adminDash__subtitle {
margin: 6px 0 0;
font-size: 0.92rem;
line-height: 1.35;
color: rgba(110, 90, 68, 0.74);
}
.maf-adminDash__grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
padding: 0.2rem;
}
@media (max-width: 1024px) {
.maf-adminDash__grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 520px) {
.maf-adminDash {
padding: 0.9rem 0.65rem 1.1rem;
}
.maf-adminDash__grid {
grid-template-columns: 1fr;
}
}
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { AuditsDashboard } from "./AuditsDashboard";
const meta = {
title: "Panel/Modules/Audits/Dashboard",
component: AuditsDashboard,
parameters: { layout: "fullscreen" },
} satisfies Meta<typeof AuditsDashboard>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Tablet: Story = { parameters: { viewport: { defaultViewport: "tablet" } } };
export const Mobile: Story = { parameters: { viewport: { defaultViewport: "mobile1" } } };
"use client";
import React from "react";
import { TopStatCard } from "@/components/panel/dashboard/TopStatCard/TopStatCard";
import { DashboardCard } from "@/components/panel/dashboard/DashboardCard/DashboardCard";
import { DashboardTrendChart } from "@/components/shared/charts/DashboardTrendChart/DashboardTrendChart";
import { ModuleDashboardMenu } from "@/components/panel/modules/ModuleDashboardMenu/ModuleDashboardMenu";
import "./audits-dashboard.css";
const months = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
export function AuditsDashboard() {
const completed = [4300, 3600, 3520, 0, 0, 0, 0, 0, 0, 0, 0, 0];
const incomplete = [120, 140, 360, 0, 0, 0, 0, 0, 0, 0, 0, 0];
const upcoming = [
{ name: "FIRE EXTINGUISHER", location: "MAF CITY CENTRE MALL", date: "01-01-2023", status: "ACTIVE" },
{ name: "MALL", location: "NAZAR 02", date: "19-03-2023", status: "ACTIVE" },
{ name: "FIRE ALARM", location: "MAF CITY CENTRE MALL", date: "27-01-2023", status: "ACTIVE" },
];
return (
<div className="maf-auditDash">
<header className="maf-auditDash__header">
<div>
<h1 className="maf-auditDash__title">Audits</h1>
<p className="maf-auditDash__subtitle">Progress, actions and upcoming audits at a glance.</p>
</div>
</header>
<ModuleDashboardMenu
ariaLabel="Audits navigation"
items={[
{ label: "Dashboard", href: "/panel/audits", active: true },
{ label: "Conduct audit", href: "/panel/audits/conduct" },
{ label: "Audit tracking", href: "/panel/audits/tracking" },
{ label: "Audit listing", href: "/panel/audits/list" },
]}
/>
<section className="maf-auditDash__kpis" aria-label="Audit KPIs">
<TopStatCard label="Total audits" value={12036} helper="All time" underlineTone="neutral" />
<TopStatCard label="Completed audits" value={11271} helper="All time" underlineTone="good" />
<TopStatCard label="Incomplete audits" value={765} helper="All time" underlineTone="warn" />
<TopStatCard label="Actions" value={1420} helper="All time" underlineTone="info" />
<TopStatCard label="Open actions" value={1041} helper="Pending" underlineTone="warn" />
<TopStatCard label="Closed actions" value={379} helper="Resolved" underlineTone="good" />
</section>
<section className="maf-auditDash__grid" aria-label="Audits dashboard content">
<DashboardCard title="Audit Dashboard Progress" subtitle="Completed vs incomplete">
<DashboardTrendChart
title="Audit Dashboard Progress"
labels={months}
completed={completed}
incomplete={incomplete}
height={320}
showSeriesLabels
/>
</DashboardCard>
<DashboardCard title="Upcoming audits" subtitle="Next scheduled">
<div className="maf-auditDash__table" role="table" aria-label="Upcoming audits table">
<div className="maf-auditDash__thead" role="row">
<div role="columnheader">Audit</div>
<div role="columnheader">Location</div>
<div role="columnheader">Date</div>
<div role="columnheader">Status</div>
</div>
{upcoming.map((u) => (
<div key={`${u.name}-${u.date}`} className="maf-auditDash__row" role="row">
<div role="cell" className="maf-auditDash__cellTitle">{u.name}</div>
<div role="cell">{u.location}</div>
<div role="cell">{u.date}</div>
<div role="cell">
<span className="maf-auditDash__pill">{u.status}</span>
</div>
</div>
))}
</div>
</DashboardCard>
</section>
</div>
);
}
.maf-auditDash {
width: 100%;
padding: 1.1rem 1.1rem 1.2rem;
box-sizing: border-box;
min-width: 0;
}
.maf-auditDash__header {
padding: 0.2rem 0.2rem 0.8rem;
}
.maf-auditDash__title {
margin: 0;
font-size: 1.35rem;
font-weight: 920;
letter-spacing: -0.02em;
color: rgba(46, 39, 32, 0.96);
}
.maf-auditDash__subtitle {
margin: 6px 0 0;
font-size: 0.92rem;
line-height: 1.35;
color: rgba(110, 90, 68, 0.74);
}
.maf-auditDash__kpis {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 12px;
padding: 0.2rem;
}
.maf-auditDash__grid {
margin-top: 12px;
display: grid;
grid-template-columns: 1.6fr 1fr;
gap: 12px;
padding: 0.2rem;
align-items: start;
}
.maf-auditDash__table {
display: grid;
gap: 8px;
}
.maf-auditDash__thead {
display: grid;
grid-template-columns: 1.1fr 1fr 0.7fr 0.55fr;
gap: 10px;
font-size: 0.75rem;
font-weight: 900;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(110, 90, 68, 0.62);
}
.maf-auditDash__row {
display: grid;
grid-template-columns: 1.1fr 1fr 0.7fr 0.55fr;
gap: 10px;
border-radius: 14px;
border: 1px solid rgba(110, 90, 68, 0.14);
background: rgba(255, 255, 255, 0.84);
padding: 10px 10px;
color: rgba(110, 90, 68, 0.86);
}
.maf-auditDash__cellTitle {
font-weight: 900;
color: rgba(46, 39, 32, 0.96);
}
.maf-auditDash__pill {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 6px 10px;
border-radius: 999px;
border: 1px solid rgba(34, 197, 94, 0.24);
background: rgba(34, 197, 94, 0.1);
color: rgba(21, 128, 61, 0.92);
font-size: 0.75rem;
font-weight: 900;
letter-spacing: 0.04em;
}
@media (max-width: 1200px) {
.maf-auditDash__kpis {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 1024px) {
.maf-auditDash__kpis {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.maf-auditDash__grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 620px) {
.maf-auditDash__thead {
display: none;
}
.maf-auditDash__row {
grid-template-columns: 1fr;
gap: 6px;
}
.maf-auditDash__pill {
justify-self: start;
}
}
@media (max-width: 520px) {
.maf-auditDash {
padding: 0.9rem 0.65rem 1.1rem;
}
}
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { ChecklistDashboard } from "./ChecklistDashboard";
const meta = {
title: "Panel/Modules/Checklist/Dashboard",
component: ChecklistDashboard,
parameters: { layout: "fullscreen" },
} satisfies Meta<typeof ChecklistDashboard>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Tablet: Story = { parameters: { viewport: { defaultViewport: "tablet" } } };
export const Mobile: Story = { parameters: { viewport: { defaultViewport: "mobile1" } } };
"use client";
import React from "react";
import { TopStatCard } from "@/components/panel/dashboard/TopStatCard/TopStatCard";
import { DashboardCard } from "@/components/panel/dashboard/DashboardCard/DashboardCard";
import { DashboardTrendChart } from "@/components/shared/charts/DashboardTrendChart/DashboardTrendChart";
import { ModuleDashboardMenu } from "@/components/panel/modules/ModuleDashboardMenu/ModuleDashboardMenu";
import "./checklist-dashboard.css";
const months = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
export function ChecklistDashboard() {
const completed = [5000, 4700, 4750, 0, 0, 0, 0, 0, 0, 0, 0, 0];
const incomplete = [80, 70, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0];
const upcoming = [
{ name: "HSE DAILY", location: "MAF CITY CENTRE MALL", date: "01-01-2023", status: "ACTIVE" },
{ name: "NEW STORE CHECK", location: "MAF CITY CENTRE MALL", date: "19-03-2023", status: "ACTIVE" },
{ name: "FOOD COURT", location: "MAF CITY CENTRE MALL", date: "27-01-2023", status: "ACTIVE" },
];
return (
<div className="maf-checkDash">
<header className="maf-checkDash__header">
<div>
<h1 className="maf-checkDash__title">Checklist</h1>
<p className="maf-checkDash__subtitle">Checklist completion, action plan status and upcoming checks.</p>
</div>
</header>
<ModuleDashboardMenu
ariaLabel="Checklist navigation"
items={[
{ label: "Dashboard", href: "/panel/checklist", active: true },
{ label: "Conduct checklist", href: "/panel/checklist/conduct" },
{ label: "Checklist action plan", href: "/panel/checklist/action-plan" },
{ label: "Checklist status", href: "/panel/checklist/status" },
]}
/>
<section className="maf-checkDash__kpis" aria-label="Checklist KPIs">
<TopStatCard label="Total checklist" value={14330} helper="All time" underlineTone="neutral" />
<TopStatCard label="Completed checklist" value={14246} helper="All time" underlineTone="good" />
<TopStatCard label="Incomplete checklist" value={84} helper="All time" underlineTone="warn" />
<TopStatCard label="Actions" value={44} helper="All time" underlineTone="info" />
<TopStatCard label="Open actions" value={43} helper="Pending" underlineTone="warn" />
<TopStatCard label="Closed actions" value={1} helper="Resolved" underlineTone="good" />
</section>
<section className="maf-checkDash__grid" aria-label="Checklist dashboard content">
<DashboardCard title="Checklist Dashboard Progress" subtitle="Completed vs incomplete">
<DashboardTrendChart
title="Checklist Dashboard Progress"
labels={months}
completed={completed}
incomplete={incomplete}
height={320}
showSeriesLabels
/>
</DashboardCard>
<DashboardCard title="Upcoming checklist" subtitle="Next scheduled">
<div className="maf-checkDash__table" role="table" aria-label="Upcoming checklist table">
<div className="maf-checkDash__thead" role="row">
<div role="columnheader">Checklist</div>
<div role="columnheader">Location</div>
<div role="columnheader">Date</div>
<div role="columnheader">Status</div>
</div>
{upcoming.map((u) => (
<div key={`${u.name}-${u.date}`} className="maf-checkDash__row" role="row">
<div role="cell" className="maf-checkDash__cellTitle">{u.name}</div>
<div role="cell">{u.location}</div>
<div role="cell">{u.date}</div>
<div role="cell">
<span className="maf-checkDash__pill">{u.status}</span>
</div>
</div>
))}
</div>
</DashboardCard>
</section>
</div>
);
}
.maf-checkDash {
width: 100%;
padding: 1.1rem 1.1rem 1.2rem;
box-sizing: border-box;
min-width: 0;
}
.maf-checkDash__header {
padding: 0.2rem 0.2rem 0.8rem;
}
.maf-checkDash__title {
margin: 0;
font-size: 1.35rem;
font-weight: 920;
letter-spacing: -0.02em;
color: rgba(46, 39, 32, 0.96);
}
.maf-checkDash__subtitle {
margin: 6px 0 0;
font-size: 0.92rem;
line-height: 1.35;
color: rgba(110, 90, 68, 0.74);
}
.maf-checkDash__kpis {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 12px;
padding: 0.2rem;
}
.maf-checkDash__grid {
margin-top: 12px;
display: grid;
grid-template-columns: 1.6fr 1fr;
gap: 12px;
padding: 0.2rem;
align-items: start;
}
.maf-checkDash__table {
display: grid;
gap: 8px;
}
.maf-checkDash__thead {
display: grid;
grid-template-columns: 1.1fr 1fr 0.7fr 0.55fr;
gap: 10px;
font-size: 0.75rem;
font-weight: 900;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(110, 90, 68, 0.62);
}
.maf-checkDash__row {
display: grid;
grid-template-columns: 1.1fr 1fr 0.7fr 0.55fr;
gap: 10px;
border-radius: 14px;
border: 1px solid rgba(110, 90, 68, 0.14);
background: rgba(255, 255, 255, 0.84);
padding: 10px 10px;
color: rgba(110, 90, 68, 0.86);
}
.maf-checkDash__cellTitle {
font-weight: 900;
color: rgba(46, 39, 32, 0.96);
}
.maf-checkDash__pill {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 6px 10px;
border-radius: 999px;
border: 1px solid rgba(34, 197, 94, 0.24);
background: rgba(34, 197, 94, 0.1);
color: rgba(21, 128, 61, 0.92);
font-size: 0.75rem;
font-weight: 900;
letter-spacing: 0.04em;
}
@media (max-width: 1200px) {
.maf-checkDash__kpis {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 1024px) {
.maf-checkDash__kpis {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.maf-checkDash__grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 620px) {
.maf-checkDash__thead {
display: none;
}
.maf-checkDash__row {
grid-template-columns: 1fr;
gap: 6px;
}
.maf-checkDash__pill {
justify-self: start;
}
}
@media (max-width: 520px) {
.maf-checkDash {
padding: 0.9rem 0.65rem 1.1rem;
}
}
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { ActivityFeed } from "./ActivityFeed";
const meta = {
title: "Panel/Dashboard/ActivityFeed",
component: ActivityFeed,
parameters: { layout: "padded" },
} satisfies Meta<typeof ActivityFeed>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
items: [
{
id: "a1",
title: "Incident reported — HVAC: Camera room",
meta: "Priority escalated · Awaiting acknowledgement",
time: "2m",
tone: "warn",
},
{
id: "a2",
title: "Monthly audit completed — Carrefour",
meta: "All controls passed · Export available",
time: "1h",
tone: "good",
},
{
id: "a3",
title: "Corrective action overdue — Fire exit checks",
meta: "Assigned to EHS · Reminder sent",
time: "3h",
tone: "bad",
},
],
},
render: (args) => (
<div style={{ maxWidth: 620 }}>
<ActivityFeed {...args} />
</div>
),
};
export const Mobile: Story = {
args: {
items: [
{
id: "a1",
title: "Incident reported — HVAC: Camera room",
meta: "Priority escalated · Awaiting acknowledgement",
time: "2m",
tone: "warn",
},
{
id: "a2",
title: "Monthly audit completed — Carrefour",
meta: "All controls passed · Export available",
time: "1h",
tone: "good",
},
{
id: "a3",
title: "Corrective action overdue — Fire exit checks",
meta: "Assigned to EHS · Reminder sent",
time: "3h",
tone: "bad",
},
{
id: "a4",
title: "Supervisor assigned — Safety improvement",
meta: "Due date updated · Waiting for approval",
time: "Yesterday",
tone: "info",
},
],
},
parameters: { viewport: { defaultViewport: "mobile1" } },
render: (args) => (
<div style={{ maxWidth: 620 }}>
<ActivityFeed {...args} />
</div>
),
};
import "./activity-feed.css";
export type ActivityTone = "neutral" | "good" | "warn" | "bad" | "info";
export type ActivityItem = {
id: string;
title: string;
meta?: string;
time: string;
tone?: ActivityTone;
};
type ActivityFeedProps = {
title?: string;
items: ActivityItem[];
};
export function ActivityFeed({ title = "Recent activity", items }: ActivityFeedProps) {
return (
<section className="maf-activity">
<header className="maf-activity__head">
<h3 className="maf-activity__title">{title}</h3>
<span className="maf-activity__count">{items.length} items</span>
</header>
<ol className="maf-activity__list">
{items.map((item) => (
<li key={item.id} className="maf-activity__item">
<span
className={`maf-activity__dot maf-activity__dot--${item.tone ?? "neutral"}`.trim()}
aria-hidden="true"
/>
<div className="maf-activity__content">
<div className="maf-activity__row">
<span className="maf-activity__itemTitle">{item.title}</span>
<span className="maf-activity__time">{item.time}</span>
</div>
{item.meta ? <span className="maf-activity__meta">{item.meta}</span> : null}
</div>
</li>
))}
</ol>
</section>
);
}
.maf-activity {
display: grid;
gap: 12px;
min-width: 0;
}
.maf-activity__head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
}
.maf-activity__title {
margin: 0;
font-size: 0.95rem;
font-weight: 700;
color: var(--text-1, rgba(255, 255, 255, 0.9));
}
.maf-activity__count {
font-size: 0.8rem;
color: var(--text-3, rgba(255, 255, 255, 0.55));
}
.maf-activity__list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 10px;
}
.maf-activity__item {
display: grid;
grid-template-columns: 10px 1fr;
gap: 10px;
padding: 10px 10px;
border-radius: 14px;
border: 1px solid rgba(110, 90, 68, 0.14);
background: rgba(255, 255, 255, 0.8);
}
.maf-activity__dot {
width: 10px;
height: 10px;
border-radius: 999px;
margin-top: 6px;
box-shadow: 0 0 0 3px rgba(167, 138, 104, 0.12);
}
.maf-activity__dot--neutral {
background: rgba(110, 90, 68, 0.78);
}
.maf-activity__dot--good {
background: rgba(34, 197, 94, 0.95);
}
.maf-activity__dot--info {
background: rgba(59, 130, 246, 0.95);
}
.maf-activity__dot--warn {
background: rgba(245, 158, 11, 0.98);
}
.maf-activity__dot--bad {
background: rgba(239, 68, 68, 0.98);
}
.maf-activity__content {
min-width: 0;
display: grid;
gap: 4px;
}
.maf-activity__row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
min-width: 0;
}
.maf-activity__itemTitle {
font-size: 0.9rem;
line-height: 1.25;
color: var(--text-1, rgba(255, 255, 255, 0.88));
font-weight: 650;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.maf-activity__time {
font-size: 0.78rem;
color: var(--text-3, rgba(255, 255, 255, 0.55));
flex: 0 0 auto;
}
.maf-activity__meta {
font-size: 0.82rem;
color: var(--text-2, rgba(255, 255, 255, 0.62));
line-height: 1.35;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { DashboardCard } from "./DashboardCard";
const meta = {
title: "Panel/Dashboard/DashboardCard",
component: DashboardCard,
parameters: { layout: "padded" },
} satisfies Meta<typeof DashboardCard>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
title: "Card title",
description: "A refined container for dashboard sections.",
children: (
<div style={{ display: "grid", gap: 10 }}>
<div
style={{
height: 64,
borderRadius: 14,
border: "1px solid rgba(255,255,255,0.08)",
background: "rgba(255,255,255,0.03)",
}}
/>
<div
style={{
height: 64,
borderRadius: 14,
border: "1px solid rgba(255,255,255,0.08)",
background: "rgba(255,255,255,0.03)",
}}
/>
</div>
),
},
render: (args) => (
<div style={{ maxWidth: 760 }}>
<DashboardCard {...args} />
</div>
),
};
import type React from "react";
import "./dashboard-card.css";
type DashboardCardProps = {
title?: string;
description?: string;
actions?: React.ReactNode;
children: React.ReactNode;
className?: string;
};
export function DashboardCard({
title,
description,
actions,
children,
className = "",
}: DashboardCardProps) {
return (
<section className={`maf-dash-card ${className}`.trim()}>
{title || actions ? (
<header className="maf-dash-card__head">
<div className="maf-dash-card__headText">
{title ? <h2 className="maf-dash-card__title">{title}</h2> : null}
{description ? (
<p className="maf-dash-card__desc">{description}</p>
) : null}
</div>
{actions ? <div className="maf-dash-card__actions">{actions}</div> : null}
</header>
) : null}
<div className="maf-dash-card__body">{children}</div>
</section>
);
}
.maf-dash-card {
border-radius: var(--r-md, 16px);
border: 1px solid rgba(110, 90, 68, 0.18);
background:
radial-gradient(700px 220px at 0% 0%, rgba(167, 138, 104, 0.14), transparent 58%),
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(250, 246, 240, 0.98));
box-shadow:
0 18px 50px rgba(16, 12, 8, 0.12),
0 1px 0 rgba(255, 255, 255, 0.6) inset;
overflow: hidden;
min-width: 0;
}
.maf-dash-card__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 14px 16px 0;
}
.maf-dash-card__headText {
min-width: 0;
}
.maf-dash-card__title {
margin: 0;
font-size: 0.92rem;
line-height: 1.2;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-1, rgba(110, 90, 68, 0.96));
}
.maf-dash-card__desc {
margin: 6px 0 0;
font-size: 0.84rem;
line-height: 1.45;
color: var(--text-2, rgba(110, 90, 68, 0.78));
max-width: 60ch;
}
.maf-dash-card__actions {
flex: 0 0 auto;
display: flex;
gap: 8px;
}
.maf-dash-card__body {
padding: 12px 16px 16px;
min-width: 0;
}
@media (max-width: 768px) {
.maf-dash-card {
border-radius: var(--r-md, 14px);
}
.maf-dash-card__head {
padding: 12px 12px 0;
}
.maf-dash-card__body {
padding: 10px 12px 12px;
}
.maf-dash-card__title {
font-size: 0.85rem;
}
.maf-dash-card__desc {
font-size: 0.8rem;
}
}
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { DonutChart } from "./DonutChart";
const meta = {
title: "Panel/Dashboard/DonutChart",
component: DonutChart,
parameters: { layout: "padded" },
} satisfies Meta<typeof DonutChart>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
title: "Incidents by type",
slices: [
{ label: "Slip & fall", value: 38, color: "#2f80ed" },
{ label: "Fire/safety", value: 27, color: "#f2994a" },
{ label: "Food poisoning", value: 19, color: "#f2c94c" },
{ label: "Equipment", value: 34, color: "#27ae60" },
{ label: "Other", value: 24, color: "#9b51e0" },
],
},
render: (args) => (
<div style={{ maxWidth: 520 }}>
<DonutChart {...args} />
</div>
),
};
export const Mobile: Story = {
args: {
title: "Incidents by type",
slices: [
{ label: "Slip & fall", value: 38, color: "#2f80ed" },
{ label: "Fire/safety", value: 27, color: "#f2994a" },
{ label: "Food poisoning", value: 19, color: "#f2c94c" },
{ label: "Equipment", value: 34, color: "#27ae60" },
{ label: "Other", value: 24, color: "#9b51e0" },
],
},
parameters: { viewport: { defaultViewport: "mobile1" } },
render: (args) => (
<div style={{ maxWidth: 520 }}>
<DonutChart {...args} />
</div>
),
};
import "./donut-chart.css";
export type DonutSlice = {
label: string;
value: number;
color: string;
};
type DonutChartProps = {
title?: string;
slices: DonutSlice[];
size?: number;
thickness?: number;
};
function sum(values: number[]) {
return values.reduce((acc, v) => acc + v, 0);
}
export function DonutChart({
title = "Incidents by type",
slices,
size = 126,
thickness = 18,
}: DonutChartProps) {
const total = Math.max(0, sum(slices.map((s) => s.value)));
const radius = (size - thickness) / 2;
const circumference = 2 * Math.PI * radius;
let offset = 0;
const rendered = slices.map((slice) => {
const pct = total === 0 ? 0 : slice.value / total;
const dash = circumference * pct;
const dashArray = `${dash} ${Math.max(0, circumference - dash)}`;
const dashOffset = -offset;
offset += dash;
return {
...slice,
dashArray,
dashOffset,
};
});
return (
<div className="maf-donut">
<div className="maf-donut__top">
<h3 className="maf-donut__title">{title.toUpperCase()}</h3>
</div>
<div className="maf-donut__content">
<svg
className="maf-donut__svg"
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
role="img"
aria-label={`${title}. Total ${total}.`}
>
<g transform={`translate(${size / 2}, ${size / 2}) rotate(-90)`}>
<circle
r={radius}
cx="0"
cy="0"
fill="transparent"
stroke="rgba(110, 90, 68, 0.14)"
strokeWidth={thickness}
/>
{rendered.map((slice) => (
<circle
key={slice.label}
r={radius}
cx="0"
cy="0"
fill="transparent"
stroke={slice.color}
strokeWidth={thickness}
strokeDasharray={slice.dashArray}
strokeDashoffset={slice.dashOffset}
strokeLinecap="butt"
/>
))}
</g>
</svg>
<ul className="maf-donut__legend" aria-label="Legend">
{slices.map((slice) => (
<li key={slice.label} className="maf-donut__legendItem">
<span
className="maf-donut__swatch"
style={{ background: slice.color }}
aria-hidden="true"
/>
<span className="maf-donut__legendLabel">{slice.label}</span>
<span className="maf-donut__legendValue">
{slice.value.toLocaleString()}
</span>
</li>
))}
</ul>
</div>
</div>
);
}
.maf-donut {
display: grid;
gap: 10px;
}
.maf-donut__title {
margin: 0;
font-size: 0.78rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-1, rgba(110, 90, 68, 0.96));
}
.maf-donut__content {
display: grid;
grid-template-columns: auto 1fr;
gap: 14px;
align-items: center;
}
.maf-donut__svg {
display: block;
filter: drop-shadow(0 10px 22px rgba(16, 12, 8, 0.08));
}
.maf-donut__legend {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 8px;
min-width: 0;
}
.maf-donut__legendItem {
display: grid;
grid-template-columns: 10px 1fr auto;
align-items: center;
gap: 8px;
min-width: 0;
}
.maf-donut__swatch {
width: 10px;
height: 10px;
border-radius: 999px;
border: 1px solid rgba(110, 90, 68, 0.16);
}
.maf-donut__legendLabel {
font-size: 0.86rem;
color: var(--text-2, rgba(110, 90, 68, 0.78));
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.maf-donut__legendValue {
font-size: 0.86rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: var(--text-1, rgba(110, 90, 68, 0.96));
}
@media (max-width: 768px) {
.maf-donut__content {
grid-template-columns: 1fr;
justify-items: start;
}
}
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { IncidentsByCountry } from "./IncidentsByCountry";
const meta = {
title: "Panel/Dashboard/IncidentsByCountry",
component: IncidentsByCountry,
parameters: { layout: "padded" },
} satisfies Meta<typeof IncidentsByCountry>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
items: [
{ country: "UAE", value: 38 },
{ country: "KSA", value: 29 },
{ country: "Egypt", value: 21 },
{ country: "Oman", value: 12 },
{ country: "Lebanon", value: 7 },
],
},
render: (args) => (
<div style={{ maxWidth: 520 }}>
<IncidentsByCountry {...args} />
</div>
),
};
export const Mobile: Story = {
args: {
items: [
{ country: "UAE", value: 38 },
{ country: "KSA", value: 29 },
{ country: "Egypt", value: 21 },
{ country: "Oman", value: 12 },
{ country: "Lebanon", value: 7 },
],
},
parameters: { viewport: { defaultViewport: "mobile1" } },
render: (args) => (
<div style={{ maxWidth: 520 }}>
<IncidentsByCountry {...args} />
</div>
),
};
import "./incidents-by-country.css";
export type CountryIncident = {
country: string;
value: number;
};
type IncidentsByCountryProps = {
title?: string;
items: CountryIncident[];
};
export function IncidentsByCountry({
title = "Incidents by country",
items,
}: IncidentsByCountryProps) {
const max = Math.max(1, ...items.map((i) => i.value));
return (
<section className="maf-country">
<header className="maf-country__head">
<h3 className="maf-country__title">{title.toUpperCase()}</h3>
</header>
<ul className="maf-country__list" aria-label={title}>
{items.map((item) => (
<li key={item.country} className="maf-country__row">
<span className="maf-country__name">{item.country}</span>
<span className="maf-country__barWrap" aria-hidden="true">
<span
className="maf-country__bar"
style={{ width: `${(item.value / max) * 100}%` }}
/>
</span>
<span className="maf-country__value">{item.value}</span>
</li>
))}
</ul>
</section>
);
}
.maf-country {
display: grid;
gap: 10px;
}
.maf-country__title {
margin: 0;
font-size: 0.78rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-1, rgba(110, 90, 68, 0.96));
}
.maf-country__list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 10px;
}
.maf-country__row {
display: grid;
grid-template-columns: 64px 1fr 36px;
gap: 10px;
align-items: center;
min-width: 0;
}
.maf-country__name {
font-size: 0.86rem;
color: var(--text-2, rgba(110, 90, 68, 0.78));
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.maf-country__barWrap {
height: 10px;
border-radius: 999px;
background: rgba(110, 90, 68, 0.1);
border: 1px solid rgba(110, 90, 68, 0.12);
overflow: hidden;
}
.maf-country__bar {
height: 100%;
display: block;
border-radius: 999px;
background: linear-gradient(90deg, rgba(110, 90, 68, 0.68), rgba(201, 169, 97, 0.7));
}
.maf-country__value {
text-align: right;
font-size: 0.86rem;
font-weight: 800;
font-variant-numeric: tabular-nums;
color: var(--text-1, rgba(110, 90, 68, 0.96));
}
@media (max-width: 768px) {
.maf-country__row {
grid-template-columns: 56px 1fr 34px;
}
}
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { KpiTile } from "./KpiTile";
const meta = {
title: "Panel/Dashboard/KpiTile",
component: KpiTile,
parameters: { layout: "padded" },
} satisfies Meta<typeof KpiTile>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Neutral: Story = {
args: {
label: "Avg. audit score",
value: "84%",
hint: "Across sites",
tone: "neutral",
sparkline: [78, 81, 79, 83, 85, 84, 84],
},
render: (args) => (
<div style={{ maxWidth: 360 }}>
<KpiTile {...args} />
</div>
),
};
export const Good: Story = {
args: {
label: "Audits completed",
value: 317,
hint: "Last 30 days",
tone: "good",
sparkline: [210, 235, 248, 265, 289, 302, 317],
},
render: (args) => (
<div style={{ maxWidth: 360 }}>
<KpiTile {...args} />
</div>
),
};
export const Mobile: Story = {
args: {
label: "Open actions",
value: 89,
hint: "Needs attention",
tone: "warn",
sparkline: [72, 74, 80, 86, 83, 88, 89],
},
parameters: { viewport: { defaultViewport: "mobile1" } },
render: (args) => (
<div style={{ maxWidth: 360 }}>
<KpiTile {...args} />
</div>
),
};
import type React from "react";
import "./kpi-tile.css";
type KpiTone = "neutral" | "good" | "warn" | "bad" | "info";
type KpiTileProps = {
label: string;
value: string | number;
hint?: string;
tone?: KpiTone;
icon?: React.ReactNode;
sparkline?: number[];
};
function formatValue(value: string | number) {
if (typeof value === "number") return value.toLocaleString();
return value;
}
function Sparkline({ values, tone }: { values: number[]; tone: KpiTone }) {
const width = 96;
const height = 26;
const pad = 2;
const min = Math.min(...values);
const max = Math.max(...values);
const span = Math.max(1, max - min);
const points = values
.map((v, index) => {
const x = (index / Math.max(1, values.length - 1)) * (width - pad * 2) + pad;
const y = height - pad - ((v - min) / span) * (height - pad * 2);
return `${x.toFixed(2)},${y.toFixed(2)}`;
})
.join(" ");
return (
<svg
className={`maf-kpi__spark maf-kpi__spark--${tone}`}
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
aria-hidden="true"
>
<polyline points={points} fill="none" />
</svg>
);
}
export function KpiTile({
label,
value,
hint,
tone = "neutral",
icon,
sparkline,
}: KpiTileProps) {
return (
<article className={`maf-kpi maf-kpi--${tone}`.trim()}>
<div className="maf-kpi__top">
<div className="maf-kpi__meta">
<span className="maf-kpi__label">{label}</span>
{hint ? <span className="maf-kpi__hint">{hint}</span> : null}
</div>
{icon ? <div className="maf-kpi__icon">{icon}</div> : null}
</div>
<div className="maf-kpi__bottom">
<span className="maf-kpi__value">{formatValue(value)}</span>
{sparkline && sparkline.length >= 2 ? (
<Sparkline values={sparkline} tone={tone} />
) : null}
</div>
</article>
);
}
.maf-kpi {
border-radius: var(--r-md, 16px);
border: 1px solid rgba(110, 90, 68, 0.18);
background:
radial-gradient(520px 220px at 20% 0%, rgba(167, 138, 104, 0.14), transparent 58%),
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(250, 246, 240, 0.98));
padding: 14px 14px 12px;
min-width: 0;
display: grid;
gap: 10px;
transition:
transform 160ms ease,
border-color 160ms ease,
background 160ms ease;
}
.maf-kpi:hover {
transform: translateY(-1px);
border-color: rgba(110, 90, 68, 0.28);
background:
radial-gradient(560px 240px at 20% 0%, rgba(167, 138, 104, 0.18), transparent 62%),
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(250, 246, 240, 0.98));
}
.maf-kpi__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
}
.maf-kpi__meta {
min-width: 0;
display: grid;
gap: 4px;
}
.maf-kpi__label {
font-size: 0.74rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-3, rgba(255, 255, 255, 0.62));
}
.maf-kpi__hint {
font-size: 0.82rem;
line-height: 1.2;
color: var(--text-2, rgba(255, 255, 255, 0.82));
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 20rem;
}
.maf-kpi__icon {
width: 38px;
height: 38px;
border-radius: 12px;
display: grid;
place-items: center;
background: rgba(167, 138, 104, 0.12);
border: 1px solid rgba(110, 90, 68, 0.18);
color: rgba(110, 90, 68, 0.92);
flex: 0 0 auto;
}
.maf-kpi__bottom {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 10px;
}
.maf-kpi__value {
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
font-size: 1.9rem;
line-height: 1;
font-weight: 800;
color: var(--text-1, rgba(110, 90, 68, 0.96));
}
.maf-kpi__spark {
opacity: 0.95;
}
.maf-kpi__spark polyline {
stroke-width: 2.25;
stroke-linecap: round;
stroke-linejoin: round;
}
.maf-kpi__spark--neutral polyline {
stroke: rgba(110, 90, 68, 0.82);
}
.maf-kpi__spark--good polyline {
stroke: rgba(34, 197, 94, 0.95);
}
.maf-kpi__spark--info polyline {
stroke: rgba(59, 130, 246, 0.95);
}
.maf-kpi__spark--warn polyline {
stroke: rgba(245, 158, 11, 0.98);
}
.maf-kpi__spark--bad polyline {
stroke: rgba(239, 68, 68, 0.98);
}
.maf-kpi--good {
border-color: rgba(34, 197, 94, 0.22);
background:
radial-gradient(520px 220px at 20% 0%, rgba(34, 197, 94, 0.08), transparent 58%),
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(250, 246, 240, 0.98));
}
.maf-kpi--warn {
border-color: rgba(245, 158, 11, 0.28);
background:
radial-gradient(520px 220px at 20% 0%, rgba(245, 158, 11, 0.09), transparent 58%),
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(250, 246, 240, 0.98));
}
.maf-kpi--bad {
border-color: rgba(239, 68, 68, 0.26);
background:
radial-gradient(520px 220px at 20% 0%, rgba(239, 68, 68, 0.08), transparent 58%),
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(250, 246, 240, 0.98));
}
.maf-kpi--info {
border-color: rgba(59, 130, 246, 0.24);
background:
radial-gradient(520px 220px at 20% 0%, rgba(59, 130, 246, 0.07), transparent 58%),
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(250, 246, 240, 0.98));
}
@media (max-width: 768px) {
.maf-kpi {
border-radius: var(--r-md, 14px);
padding: 12px 12px 11px;
}
.maf-kpi__value {
font-size: 1.55rem;
}
}
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { MetricCard } from "./MetricCard";
const meta = {
title: "Panel/Dashboard/MetricCard",
component: MetricCard,
parameters: { layout: "padded" },
} satisfies Meta<typeof MetricCard>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Incidents: Story = {
args: {
title: "Incidents",
value: 142,
sublabel: "this month",
badge: "14 LTI",
tone: "info",
icon: "△",
},
render: (args) => (
<div style={{ maxWidth: 340 }}>
<MetricCard {...args} />
</div>
),
};
export const Mobile: Story = {
args: {
title: "Tracking",
value: 89,
sublabel: "open actions",
badge: "31 overdue",
tone: "warn",
icon: "▣",
},
parameters: { viewport: { defaultViewport: "mobile1" } },
render: (args) => (
<div style={{ maxWidth: 340 }}>
<MetricCard {...args} />
</div>
),
};
import "./metric-card.css";
type MetricTone =
| "neutral"
| "info"
| "good"
| "warn"
| "bad"
| "lavender"
| "teal"
| "pink";
type MetricCardProps = {
title: string;
value: number | string;
sublabel?: string;
badge?: string;
tone?: MetricTone;
icon?: string;
};
function formatValue(value: number | string) {
if (typeof value === "number") return value.toLocaleString();
return value;
}
export function MetricCard({
title,
value,
sublabel,
badge,
tone = "neutral",
icon,
}: MetricCardProps) {
return (
<article className={`maf-metric maf-metric--${tone}`.trim()}>
<div className="maf-metric__head">
<span className="maf-metric__icon" aria-hidden="true">
{icon ?? "●"}
</span>
<span className="maf-metric__title">{title}</span>
</div>
<div className="maf-metric__body">
<span className="maf-metric__value">{formatValue(value)}</span>
<div className="maf-metric__metaRow">
{sublabel ? <span className="maf-metric__sublabel">{sublabel}</span> : null}
{badge ? <span className="maf-metric__badge">{badge}</span> : null}
</div>
</div>
</article>
);
}
.maf-metric {
border-radius: 14px;
border: 1px solid rgba(110, 90, 68, 0.18);
background: rgba(255, 255, 255, 0.92);
padding: 12px 12px;
display: grid;
gap: 10px;
min-width: 0;
box-shadow: 0 14px 34px rgba(16, 12, 8, 0.08);
}
.maf-metric__head {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.maf-metric__icon {
width: 30px;
height: 30px;
border-radius: 10px;
display: grid;
place-items: center;
font-size: 0.95rem;
font-weight: 800;
border: 1px solid rgba(110, 90, 68, 0.18);
background: rgba(167, 138, 104, 0.14);
color: rgba(110, 90, 68, 0.9);
flex: 0 0 auto;
}
.maf-metric__title {
font-size: 0.92rem;
font-weight: 700;
color: var(--text-1, rgba(110, 90, 68, 0.96));
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.maf-metric__body {
display: grid;
gap: 6px;
}
.maf-metric__value {
font-size: 1.35rem;
font-weight: 800;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
color: rgba(46, 39, 32, 0.96);
}
.maf-metric__metaRow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.maf-metric__sublabel {
font-size: 0.78rem;
color: var(--text-2, rgba(110, 90, 68, 0.78));
}
.maf-metric__badge {
font-size: 0.75rem;
padding: 4px 8px;
border-radius: 999px;
border: 1px solid rgba(110, 90, 68, 0.18);
background: rgba(167, 138, 104, 0.12);
color: rgba(110, 90, 68, 0.9);
font-weight: 700;
flex: 0 0 auto;
}
/* Tones (approx like screenshot chips) */
.maf-metric--info .maf-metric__icon {
background: rgba(59, 130, 246, 0.12);
border-color: rgba(59, 130, 246, 0.22);
color: rgba(37, 99, 235, 0.9);
}
.maf-metric--good .maf-metric__icon {
background: rgba(34, 197, 94, 0.12);
border-color: rgba(34, 197, 94, 0.2);
color: rgba(22, 163, 74, 0.9);
}
.maf-metric--warn .maf-metric__icon {
background: rgba(245, 158, 11, 0.14);
border-color: rgba(245, 158, 11, 0.22);
color: rgba(217, 119, 6, 0.9);
}
.maf-metric--bad .maf-metric__icon {
background: rgba(239, 68, 68, 0.12);
border-color: rgba(239, 68, 68, 0.2);
color: rgba(220, 38, 38, 0.9);
}
.maf-metric--lavender .maf-metric__icon {
background: rgba(99, 102, 241, 0.12);
border-color: rgba(99, 102, 241, 0.2);
color: rgba(79, 70, 229, 0.92);
}
.maf-metric--teal .maf-metric__icon {
background: rgba(20, 184, 166, 0.12);
border-color: rgba(20, 184, 166, 0.2);
color: rgba(13, 148, 136, 0.92);
}
.maf-metric--pink .maf-metric__icon {
background: rgba(236, 72, 153, 0.1);
border-color: rgba(236, 72, 153, 0.2);
color: rgba(219, 39, 119, 0.92);
}
@media (max-width: 768px) {
.maf-metric {
padding: 11px 11px;
}
.maf-metric__value {
font-size: 1.25rem;
}
}
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { PanelDashboard } from "./PanelDashboard";
const meta = {
title: "Panel/Dashboard/PanelDashboard",
component: PanelDashboard,
parameters: {
layout: "padded",
},
} satisfies Meta<typeof PanelDashboard>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Tablet: Story = {
parameters: {
viewport: { defaultViewport: "tablet" },
},
};
export const Mobile: Story = {
parameters: {
viewport: { defaultViewport: "mobile1" },
},
};
"use client";
import React from "react";
import { DashboardTrendChart } from "@/components/shared/charts/DashboardTrendChart/DashboardTrendChart";
import { DashboardCard } from "../DashboardCard/DashboardCard";
import { DonutChart } from "../DonutChart/DonutChart";
import { MetricCard } from "../MetricCard/MetricCard";
import { TopStatCard } from "../TopStatCard/TopStatCard";
import { ActivityFeed } from "../ActivityFeed/ActivityFeed";
import { IncidentsByCountry } from "../IncidentsByCountry/IncidentsByCountry";
import "./panel-dashboard.css";
function IconBox({ children }: { children: React.ReactNode }) {
return (
<span className="maf-dash-icon" aria-hidden="true">
{children}
</span>
);
}
function MiniIcon({ d }: { d: string }) {
return (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d={d}
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
type PanelDashboardProps = {
title?: string;
subtitle?: string;
};
export function PanelDashboard({
title = "MAF Gateway",
subtitle = "Incidents overview and insights",
}: PanelDashboardProps) {
const now = React.useMemo(() => new Date(), []);
const rangeLabel = now.toLocaleString(undefined, {
month: "short",
year: "numeric",
});
const incidentDonut = React.useMemo(
() => [
{ label: "Slip & fall", value: 38, color: "#2f80ed" },
{ label: "Fire/safety", value: 27, color: "#f2994a" },
{ label: "Food poisoning", value: 19, color: "#f2c94c" },
{ label: "Equipment", value: 34, color: "#27ae60" },
{ label: "Other", value: 24, color: "#9b51e0" },
],
[],
);
const activity = React.useMemo(
() => [
{
id: "a1",
title: "Incident reported — HVAC: Camera room",
meta: "Priority escalated · Awaiting acknowledgement",
time: "2m",
tone: "warn" as const,
},
{
id: "a2",
title: "Incident closed — Slip & fall",
meta: "Resolution logged · Follow-up scheduled",
time: "1h",
tone: "good" as const,
},
{
id: "a3",
title: "Corrective action overdue — Fire exit checks",
meta: "Assigned to EHS · Reminder sent",
time: "3h",
tone: "bad" as const,
},
{
id: "a4",
title: "New incident type added — Equipment",
meta: "Classification updated · Reporting aligned",
time: "Yesterday",
tone: "info" as const,
},
],
[],
);
const incidentsByCountry = React.useMemo(
() => [
{ country: "UAE", value: 38 },
{ country: "KSA", value: 29 },
{ country: "Egypt", value: 21 },
{ country: "Oman", value: 12 },
{ country: "Lebanon", value: 7 },
],
[],
);
return (
<div className="maf-panel-dashboard">
<header className="maf-panel-dashboard__header">
<div className="maf-panel-dashboard__headerText">
<p className="maf-panel-dashboard__eyebrow">
Dashboard · {rangeLabel}
</p>
<h1 className="maf-panel-dashboard__title">{title}</h1>
<p className="maf-panel-dashboard__subtitle">{subtitle}</p>
</div>
<div className="maf-panel-dashboard__headerMeta" aria-label="Summary meta">
<div className="maf-panel-dashboard__status" aria-label="Status: Live">
<span className="maf-panel-dashboard__liveDot" aria-hidden="true" />
<span className="maf-panel-dashboard__statusText">Live</span>
</div>
</div>
</header>
<section className="maf-panel-dashboard__topStats" aria-label="Top summary cards">
<TopStatCard
label="Incidents"
value={142}
helper="this month"
deltaText="▲ 12 vs last month"
deltaTone="danger"
underlineTone="danger"
/>
<TopStatCard
label="Open actions"
value={89}
helper=""
deltaText="31 overdue"
deltaTone="danger"
underlineTone="warn"
/>
<TopStatCard
label="Audits done"
value={317}
helper=""
deltaText="▲ 9% vs last month"
deltaTone="good"
underlineTone="good"
/>
<TopStatCard
label="Avg audit score"
value="84%"
helper=""
deltaText="▲ 3pts improvement"
deltaTone="good"
underlineTone="info"
/>
</section>
<section className="maf-panel-dashboard__charts" aria-label="Incident charts">
<DashboardCard title="FR trend — Major vs LTI (rolling 7 months)">
<DashboardTrendChart
title="FR Trend — Major vs LTI (rolling 7 months)"
labels={["Sep", "Oct", "Nov", "Dec", "Jan", "Feb", "Mar"]}
completed={[2.2, 1.9, 2.4, 2.0, 2.2, 1.7, 1.5]}
incomplete={[1.2, 0.9, 1.1, 1.4, 1.0, 0.8, 0.7]}
height={260}
showSeriesLabels={false}
/>
</DashboardCard>
<DashboardCard title="Incidents by type">
<DonutChart title="Incidents by type" slices={incidentDonut} />
</DashboardCard>
</section>
<section className="maf-panel-dashboard__secondary" aria-label="Additional insights">
<DashboardCard title="Recent activity">
<ActivityFeed items={activity} />
</DashboardCard>
<DashboardCard title="Incident by country">
<IncidentsByCountry items={incidentsByCountry} />
</DashboardCard>
</section>
<section className="maf-panel-dashboard__metricGrid" aria-label="Module summary cards">
<MetricCard
title="Incidents"
value={142}
sublabel="this month"
badge="14 LTI"
tone="info"
icon="△"
/>
<MetricCard
title="Audits"
value={317}
sublabel="completed"
badge="84% avg"
tone="good"
icon="✓"
/>
<MetricCard
title="Inspections"
value={188}
sublabel="completed"
badge="5 pending"
tone="warn"
icon="🔎"
/>
<MetricCard
title="Checklists"
value={94}
sublabel="completed"
badge="7 blocked"
tone="lavender"
icon="▦"
/>
<MetricCard
title="Contractors"
value={23}
sublabel="active permits"
badge="4 pending"
tone="bad"
icon="⌁"
/>
<MetricCard
title="Suggestions"
value={11}
sublabel="in review"
badge="3 approved"
tone="teal"
icon="✳"
/>
<MetricCard
title="Tracking"
value={89}
sublabel="open actions"
badge="31 overdue"
tone="warn"
icon="▣"
/>
<MetricCard
title="Notifications"
value={6}
sublabel="unread"
badge="2 escalated"
tone="pink"
icon="✉"
/>
</section>
</div>
);
}
.maf-panel-dashboard {
display: grid;
gap: 16px;
width: min(1180px, 100%);
margin: 0 auto;
}
.maf-panel-dashboard__header {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
padding: 6px 2px 2px;
}
.maf-panel-dashboard__headerText {
min-width: 0;
}
.maf-panel-dashboard__eyebrow {
margin: 0 0 8px;
font-size: 0.78rem;
letter-spacing: 0.09em;
text-transform: uppercase;
color: var(--text-3, rgba(255, 255, 255, 0.56));
}
.maf-panel-dashboard__title {
margin: 0;
font-family: var(--font-serif, inherit);
letter-spacing: -0.02em;
font-size: clamp(1.55rem, 2.2vw, 2.25rem);
line-height: 1.1;
color: var(--text-1, rgba(255, 255, 255, 0.94));
}
.maf-panel-dashboard__subtitle {
margin: 10px 0 0;
font-size: 0.95rem;
line-height: 1.4;
color: var(--text-2, rgba(255, 255, 255, 0.66));
max-width: 72ch;
}
.maf-panel-dashboard__headerMeta {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
flex: 0 0 auto;
}
.maf-panel-dashboard__status {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 999px;
border: 1px solid rgba(110, 90, 68, 0.16);
background: rgba(255, 255, 255, 0.72);
}
.maf-panel-dashboard__liveDot {
width: 10px;
height: 10px;
border-radius: 999px;
background: rgba(34, 197, 94, 0.95);
box-shadow:
0 0 0 3px rgba(34, 197, 94, 0.14),
0 6px 16px rgba(34, 197, 94, 0.18);
}
.maf-panel-dashboard__statusText {
font-size: 0.86rem;
font-weight: 750;
color: var(--text-1, rgba(110, 90, 68, 0.96));
}
.maf-panel-dashboard__kpis {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.maf-panel-dashboard__charts {
display: grid;
grid-template-columns: 1.5fr 1fr;
gap: 12px;
align-items: start;
}
.maf-panel-dashboard__topStats {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.maf-panel-dashboard__secondary {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
align-items: start;
}
.maf-panel-dashboard__metricGrid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.maf-dash-icon {
display: inline-flex;
align-items: center;
justify-content: center;
color: inherit;
}
@media (max-width: 1100px) {
.maf-panel-dashboard__kpis {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.maf-panel-dashboard__charts {
grid-template-columns: 1fr;
}
.maf-panel-dashboard__topStats {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.maf-panel-dashboard__secondary {
grid-template-columns: 1fr;
}
.maf-panel-dashboard__metricGrid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
.maf-panel-dashboard {
gap: 14px;
}
.maf-panel-dashboard__header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.maf-panel-dashboard__headerMeta {
width: 100%;
justify-content: flex-start;
flex-wrap: wrap;
}
.maf-panel-dashboard__pill {
min-width: unset;
flex: 1 1 auto;
}
.maf-panel-dashboard__kpis {
gap: 10px;
}
.maf-panel-dashboard__metricGrid {
gap: 10px;
}
}
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { QuickLinkTile } from "./QuickLinkTile";
const meta = {
title: "Panel/Dashboard/QuickLinkTile",
component: QuickLinkTile,
parameters: { layout: "padded" },
} satisfies Meta<typeof QuickLinkTile>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
title: "Incidents",
description: "Report, triage and resolve",
href: "/panel/incidents",
badge: "new",
},
render: (args) => (
<div style={{ maxWidth: 520 }}>
<QuickLinkTile {...args} />
</div>
),
};
export const Mobile: Story = {
args: {
title: "Reports",
description: "Exports and analytics",
href: "/panel/reports",
badge: "beta",
},
parameters: { viewport: { defaultViewport: "mobile1" } },
render: (args) => (
<div style={{ maxWidth: 520 }}>
<QuickLinkTile {...args} />
</div>
),
};
import Link from "next/link";
import type React from "react";
import "./quick-link-tile.css";
type QuickLinkTileProps = {
title: string;
description?: string;
href: string;
icon?: React.ReactNode;
badge?: string;
};
export function QuickLinkTile({
title,
description,
href,
icon,
badge,
}: QuickLinkTileProps) {
return (
<Link href={href} className="maf-quick-tile">
<div className="maf-quick-tile__icon">{icon}</div>
<div className="maf-quick-tile__text">
<div className="maf-quick-tile__row">
<span className="maf-quick-tile__title">{title}</span>
{badge ? <span className="maf-quick-tile__badge">{badge}</span> : null}
</div>
{description ? (
<span className="maf-quick-tile__desc">{description}</span>
) : null}
</div>
</Link>
);
}
.maf-quick-tile {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
border-radius: var(--r-md, 16px);
border: 1px solid rgba(110, 90, 68, 0.18);
background: rgba(255, 255, 255, 0.86);
text-decoration: none;
color: inherit;
transition:
transform 160ms ease,
border-color 160ms ease,
background 160ms ease;
min-width: 0;
}
.maf-quick-tile:hover {
transform: translateY(-1px);
border-color: rgba(110, 90, 68, 0.28);
background: rgba(250, 246, 240, 0.98);
}
.maf-quick-tile:focus-visible {
outline: none;
box-shadow: var(--shadow-focus, 0 0 0 3px rgba(201, 169, 97, 0.25));
}
.maf-quick-tile__icon {
width: 44px;
height: 44px;
border-radius: var(--r-md, 14px);
display: grid;
place-items: center;
background: rgba(167, 138, 104, 0.14);
border: 1px solid rgba(110, 90, 68, 0.18);
color: rgba(110, 90, 68, 0.9);
flex: 0 0 auto;
}
.maf-quick-tile__text {
min-width: 0;
display: grid;
gap: 4px;
}
.maf-quick-tile__row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
min-width: 0;
}
.maf-quick-tile__title {
font-size: 0.95rem;
line-height: 1.2;
font-weight: 700;
color: var(--text-1, rgba(110, 90, 68, 0.96));
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.maf-quick-tile__badge {
font-size: 0.74rem;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 6px 9px;
border-radius: 999px;
border: 1px solid rgba(110, 90, 68, 0.18);
background: rgba(167, 138, 104, 0.14);
color: rgba(110, 90, 68, 0.86);
flex: 0 0 auto;
}
.maf-quick-tile__desc {
font-size: 0.84rem;
line-height: 1.35;
color: var(--text-2, rgba(110, 90, 68, 0.78));
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
@media (max-width: 768px) {
.maf-quick-tile {
border-radius: var(--r-md, 14px);
padding: 11px 12px;
}
.maf-quick-tile__icon {
width: 42px;
height: 42px;
border-radius: var(--r-md, 13px);
}
.maf-quick-tile__title {
font-size: 0.92rem;
}
}
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { TopStatCard } from "./TopStatCard";
const meta = {
title: "Panel/Dashboard/TopStatCard",
component: TopStatCard,
parameters: { layout: "padded" },
} satisfies Meta<typeof TopStatCard>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Incidents: Story = {
args: {
label: "Incidents",
value: 142,
helper: "this month",
deltaText: "▲ 12 vs last month",
deltaTone: "danger",
underlineTone: "danger",
},
render: (args) => (
<div style={{ maxWidth: 320 }}>
<TopStatCard {...args} />
</div>
),
};
export const Mobile: Story = {
args: {
label: "Avg audit score",
value: "84%",
deltaText: "▲ 3pts improvement",
deltaTone: "good",
underlineTone: "info",
},
parameters: { viewport: { defaultViewport: "mobile1" } },
render: (args) => (
<div style={{ maxWidth: 320 }}>
<TopStatCard {...args} />
</div>
),
};
import "./top-stat-card.css";
type StatTone = "danger" | "warn" | "good" | "neutral" | "info";
type TopStatCardProps = {
label: string;
value: string | number;
helper?: string;
deltaText?: string;
deltaTone?: StatTone;
underlineTone?: StatTone;
};
function formatValue(value: string | number) {
if (typeof value === "number") return value.toLocaleString();
return value;
}
export function TopStatCard({
label,
value,
helper,
deltaText,
deltaTone = "neutral",
underlineTone = "neutral",
}: TopStatCardProps) {
return (
<article className={`maf-topstat maf-topstat--u-${underlineTone}`.trim()}>
<div className="maf-topstat__label">{label}</div>
<div className="maf-topstat__value">{formatValue(value)}</div>
<div className="maf-topstat__bottom">
{helper ? <span className="maf-topstat__helper">{helper}</span> : null}
{deltaText ? (
<span className={`maf-topstat__delta maf-topstat__delta--${deltaTone}`.trim()}>
{deltaText}
</span>
) : null}
</div>
</article>
);
}
.maf-topstat {
border-radius: 14px;
border: 1px solid rgba(110, 90, 68, 0.14);
background: rgba(255, 255, 255, 0.9);
padding: 12px 12px 10px;
display: grid;
gap: 6px;
min-width: 0;
position: relative;
overflow: hidden;
}
.maf-topstat::after {
content: "";
position: absolute;
left: 10px;
right: 10px;
bottom: 0;
height: 3px;
border-radius: 999px;
background: rgba(110, 90, 68, 0.28);
}
.maf-topstat--u-danger::after {
background: rgba(239, 68, 68, 0.7);
}
.maf-topstat--u-warn::after {
background: rgba(245, 158, 11, 0.75);
}
.maf-topstat--u-good::after {
background: rgba(34, 197, 94, 0.7);
}
.maf-topstat--u-info::after {
background: rgba(59, 130, 246, 0.7);
}
.maf-topstat--u-neutral::after {
background: rgba(110, 90, 68, 0.28);
}
.maf-topstat__label {
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-3, rgba(110, 90, 68, 0.62));
}
.maf-topstat__value {
font-size: 1.6rem;
font-weight: 800;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
color: var(--text-1, rgba(110, 90, 68, 0.96));
line-height: 1.05;
}
.maf-topstat__bottom {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
padding-bottom: 6px;
}
.maf-topstat__helper {
font-size: 0.78rem;
color: var(--text-2, rgba(110, 90, 68, 0.78));
}
.maf-topstat__delta {
font-size: 0.76rem;
font-weight: 700;
white-space: nowrap;
}
.maf-topstat__delta--danger {
color: rgba(185, 28, 28, 0.92);
}
.maf-topstat__delta--warn {
color: rgba(180, 83, 9, 0.92);
}
.maf-topstat__delta--good {
color: rgba(21, 128, 61, 0.92);
}
.maf-topstat__delta--info {
color: rgba(29, 78, 216, 0.92);
}
.maf-topstat__delta--neutral {
color: rgba(110, 90, 68, 0.82);
}
@media (max-width: 768px) {
.maf-topstat__value {
font-size: 1.45rem;
}
}
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { IncidentsDashboard } from "./IncidentsDashboard";
const meta = {
title: "Panel/Modules/Incidents/Dashboard",
component: IncidentsDashboard,
parameters: { layout: "fullscreen" },
} satisfies Meta<typeof IncidentsDashboard>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Tablet: Story = { parameters: { viewport: { defaultViewport: "tablet" } } };
export const Mobile: Story = { parameters: { viewport: { defaultViewport: "mobile1" } } };
"use client";
import React from "react";
import { TopStatCard } from "@/components/panel/dashboard/TopStatCard/TopStatCard";
import { DualLineTrendChart } from "@/components/shared/charts/DualLineTrendChart/DualLineTrendChart";
import { DashboardCard } from "@/components/panel/dashboard/DashboardCard/DashboardCard";
import { ModuleDashboardMenu } from "@/components/panel/modules/ModuleDashboardMenu/ModuleDashboardMenu";
import "./incidents-dashboard.css";
export function IncidentsDashboard() {
const months = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
const major = [14, 5, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0];
const lti = [1, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0];
return (
<div className="maf-modDash">
<header className="maf-modDash__header">
<div className="maf-modDash__titleBlock">
<h1 className="maf-modDash__title">Incidents</h1>
<p className="maf-modDash__subtitle">Today’s snapshot and progress trend across the year.</p>
</div>
</header>
<ModuleDashboardMenu
ariaLabel="Incidents navigation"
items={[
{ label: "Dashboard", href: "/panel/incidents", active: true },
{ label: "New incident", href: "/panel/incidents/new" },
{ label: "Incident tracking", href: "/panel/incidents/tracking" },
{ label: "Incident listing", href: "/panel/incidents/list" },
{ label: "Monthly data", href: "/panel/incidents/monthly" },
]}
/>
<section className="maf-modDash__kpis" aria-label="Incident KPIs">
<TopStatCard label="Total incidents" value={770} helper="This month" underlineTone="neutral" />
<TopStatCard label="Major incidents" value={28} helper="This month" underlineTone="warn" deltaTone="warn" deltaText="Focus" />
<TopStatCard label="LTI%" value={6} helper="Rolling 30 days" underlineTone="info" />
<TopStatCard label="Critical severity" value={2} helper="Open now" underlineTone="danger" deltaTone="danger" deltaText="Immediate" />
</section>
<section className="maf-modDash__grid" aria-label="Incidents dashboard content">
<DashboardCard title="Incident Progress" subtitle="Major vs LTI trend">
<DualLineTrendChart
title="Incident Progress"
labels={months}
seriesA={major}
seriesB={lti}
seriesALabel="Major Injury Accident"
seriesBLabel="LTI - Lost Time Injury"
height={320}
/>
</DashboardCard>
</section>
</div>
);
}
.maf-modDash {
width: 100%;
padding: 1.1rem 1.1rem 1.2rem;
box-sizing: border-box;
min-width: 0;
}
.maf-modDash__header {
display: flex;
align-items: end;
justify-content: space-between;
gap: 12px;
padding: 0.2rem 0.2rem 0.8rem;
}
.maf-modDash__title {
margin: 0;
font-size: 1.35rem;
font-weight: 920;
letter-spacing: -0.02em;
color: rgba(46, 39, 32, 0.96);
}
.maf-modDash__subtitle {
margin: 6px 0 0;
font-size: 0.92rem;
line-height: 1.35;
color: rgba(110, 90, 68, 0.74);
}
.maf-modDash__kpis {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
padding: 0.2rem;
}
.maf-modDash__grid {
margin-top: 12px;
display: grid;
grid-template-columns: 1.6fr 1fr;
gap: 12px;
padding: 0.2rem;
align-items: start;
}
.maf-modDash__actions {
display: grid;
gap: 10px;
}
.maf-modDash__actionBtn {
border-radius: 14px;
border: 1px solid rgba(201, 169, 97, 0.45);
background: rgba(139, 21, 56, 0.92);
color: rgba(255, 255, 255, 0.96);
padding: 12px 12px;
font-weight: 900;
cursor: pointer;
box-shadow:
0 18px 44px rgba(139, 21, 56, 0.18),
inset 0 1px 0 rgba(255, 255, 255, 0.28);
text-align: left;
}
.maf-modDash__actionBtn:hover {
background: rgba(107, 15, 42, 0.94);
}
.maf-modDash__actionBtn--ghost {
background: rgba(255, 255, 255, 0.84);
color: rgba(46, 39, 32, 0.96);
border: 1px solid rgba(110, 90, 68, 0.18);
box-shadow: 0 18px 44px rgba(16, 12, 8, 0.08);
}
.maf-modDash__actionBtn--ghost:hover {
background: rgba(250, 246, 240, 0.98);
}
@media (max-width: 1024px) {
.maf-modDash__kpis {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.maf-modDash__grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 520px) {
.maf-modDash {
padding: 0.9rem 0.65rem 1.1rem;
}
}
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { InspectionDashboard } from "./InspectionDashboard";
const meta = {
title: "Panel/Modules/Inspection/Dashboard",
component: InspectionDashboard,
parameters: { layout: "fullscreen" },
} satisfies Meta<typeof InspectionDashboard>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Tablet: Story = { parameters: { viewport: { defaultViewport: "tablet" } } };
export const Mobile: Story = { parameters: { viewport: { defaultViewport: "mobile1" } } };
"use client";
import React from "react";
import { TopStatCard } from "@/components/panel/dashboard/TopStatCard/TopStatCard";
import { DashboardCard } from "@/components/panel/dashboard/DashboardCard/DashboardCard";
import { DashboardTrendChart } from "@/components/shared/charts/DashboardTrendChart/DashboardTrendChart";
import { ModuleDashboardMenu } from "@/components/panel/modules/ModuleDashboardMenu/ModuleDashboardMenu";
import "./inspection-dashboard.css";
const months = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
export function InspectionDashboard() {
const completed = [690, 612, 612, 0, 0, 0, 0, 0, 0, 0, 0, 0];
const incomplete = [22, 28, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0];
const upcoming = [
{ name: "MAF CITY CENTRE MALL", location: "MAF CITY CENTRE MALL", date: "01-01-2023", status: "ACTIVE" },
{ name: "MAF CITY CENTRE MALL", location: "MAF CITY CENTRE MALL", date: "19-03-2023", status: "ACTIVE" },
{ name: "MAF CITY CENTRE MALL", location: "MAF CITY CENTRE MALL", date: "27-01-2023", status: "ACTIVE" },
];
return (
<div className="maf-inspDash">
<header className="maf-inspDash__header">
<div>
<h1 className="maf-inspDash__title">Inspection</h1>
<p className="maf-inspDash__subtitle">Operational inspections, actions and schedule overview.</p>
</div>
</header>
<ModuleDashboardMenu
ariaLabel="Inspection navigation"
items={[
{ label: "Dashboard", href: "/panel/inspection", active: true },
{ label: "Conduct inspection", href: "/panel/inspection/conduct" },
{ label: "Inspection tracking", href: "/panel/inspection/tracking" },
{ label: "Inspection list", href: "/panel/inspection/list" },
]}
/>
<section className="maf-inspDash__kpis" aria-label="Inspection KPIs">
<TopStatCard label="Total inspections" value={1989} helper="All time" underlineTone="neutral" />
<TopStatCard label="Completed inspections" value={1902} helper="All time" underlineTone="good" />
<TopStatCard label="Incomplete inspections" value={87} helper="All time" underlineTone="warn" />
<TopStatCard label="Actions" value={1272} helper="All time" underlineTone="info" />
<TopStatCard label="Open actions" value={905} helper="Pending" underlineTone="warn" />
<TopStatCard label="Closed actions" value={367} helper="Resolved" underlineTone="good" />
</section>
<section className="maf-inspDash__grid" aria-label="Inspection dashboard content">
<DashboardCard title="Inspection Dashboard Progress" subtitle="Completed vs incomplete">
<DashboardTrendChart
title="Inspection Dashboard Progress"
labels={months}
completed={completed}
incomplete={incomplete}
height={320}
showSeriesLabels
/>
</DashboardCard>
<DashboardCard title="Upcoming inspection" subtitle="Next scheduled">
<div className="maf-inspDash__table" role="table" aria-label="Upcoming inspection table">
<div className="maf-inspDash__thead" role="row">
<div role="columnheader">Inspection</div>
<div role="columnheader">Location</div>
<div role="columnheader">Date</div>
<div role="columnheader">Status</div>
</div>
{upcoming.map((u) => (
<div key={`${u.name}-${u.date}`} className="maf-inspDash__row" role="row">
<div role="cell" className="maf-inspDash__cellTitle">{u.name}</div>
<div role="cell">{u.location}</div>
<div role="cell">{u.date}</div>
<div role="cell">
<span className="maf-inspDash__pill">{u.status}</span>
</div>
</div>
))}
</div>
</DashboardCard>
</section>
</div>
);
}
.maf-inspDash {
width: 100%;
padding: 1.1rem 1.1rem 1.2rem;
box-sizing: border-box;
min-width: 0;
}
.maf-inspDash__header {
padding: 0.2rem 0.2rem 0.8rem;
}
.maf-inspDash__title {
margin: 0;
font-size: 1.35rem;
font-weight: 920;
letter-spacing: -0.02em;
color: rgba(46, 39, 32, 0.96);
}
.maf-inspDash__subtitle {
margin: 6px 0 0;
font-size: 0.92rem;
line-height: 1.35;
color: rgba(110, 90, 68, 0.74);
}
.maf-inspDash__kpis {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 12px;
padding: 0.2rem;
}
.maf-inspDash__grid {
margin-top: 12px;
display: grid;
grid-template-columns: 1.6fr 1fr;
gap: 12px;
padding: 0.2rem;
align-items: start;
}
.maf-inspDash__table {
display: grid;
gap: 8px;
}
.maf-inspDash__thead {
display: grid;
grid-template-columns: 1.1fr 1fr 0.7fr 0.55fr;
gap: 10px;
font-size: 0.75rem;
font-weight: 900;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(110, 90, 68, 0.62);
}
.maf-inspDash__row {
display: grid;
grid-template-columns: 1.1fr 1fr 0.7fr 0.55fr;
gap: 10px;
border-radius: 14px;
border: 1px solid rgba(110, 90, 68, 0.14);
background: rgba(255, 255, 255, 0.84);
padding: 10px 10px;
color: rgba(110, 90, 68, 0.86);
}
.maf-inspDash__cellTitle {
font-weight: 900;
color: rgba(46, 39, 32, 0.96);
}
.maf-inspDash__pill {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 6px 10px;
border-radius: 999px;
border: 1px solid rgba(34, 197, 94, 0.24);
background: rgba(34, 197, 94, 0.1);
color: rgba(21, 128, 61, 0.92);
font-size: 0.75rem;
font-weight: 900;
letter-spacing: 0.04em;
}
@media (max-width: 1200px) {
.maf-inspDash__kpis {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 1024px) {
.maf-inspDash__kpis {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.maf-inspDash__grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 620px) {
.maf-inspDash__thead {
display: none;
}
.maf-inspDash__row {
grid-template-columns: 1fr;
gap: 6px;
}
.maf-inspDash__pill {
justify-self: start;
}
}
@media (max-width: 520px) {
.maf-inspDash {
padding: 0.9rem 0.65rem 1.1rem;
}
}
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { ModuleCard } from "./ModuleCard";
const meta = {
title: "Panel/Modules/ModuleCard",
component: ModuleCard,
parameters: {
layout: "padded",
},
} satisfies Meta<typeof ModuleCard>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
title: "Incidents",
description: "Report, classify and resolve incidents with fast escalation paths.",
href: "/panel/incidents",
badge: "Hot",
meta: "Last updated today",
},
};
export const Mobile: Story = {
args: Default.args,
parameters: {
viewport: { defaultViewport: "mobile1" },
},
};
import Link from "next/link";
import type React from "react";
import "./module-card.css";
export type ModuleCardProps = {
title: string;
description: string;
href: string;
icon?: React.ReactNode;
badge?: string;
meta?: string;
};
export function ModuleCard({
title,
description,
href,
icon,
badge,
meta,
}: ModuleCardProps) {
return (
<Link className="maf-module-card" href={href}>
<div className="maf-module-card__head">
<div className="maf-module-card__icon" aria-hidden="true">
{icon ?? "▦"}
</div>
<div className="maf-module-card__headText">
<div className="maf-module-card__row">
<span className="maf-module-card__title">{title}</span>
{badge ? <span className="maf-module-card__badge">{badge}</span> : null}
</div>
{meta ? <span className="maf-module-card__meta">{meta}</span> : null}
</div>
</div>
<p className="maf-module-card__desc">{description}</p>
<span className="maf-module-card__cta" aria-hidden="true">
Open →
</span>
</Link>
);
}
.maf-module-card {
border-radius: 16px;
border: 1px solid rgba(110, 90, 68, 0.18);
background: rgba(255, 255, 255, 0.92);
padding: 14px 14px;
display: grid;
gap: 10px;
text-decoration: none;
color: inherit;
box-shadow: 0 18px 44px rgba(16, 12, 8, 0.08);
transition:
transform 160ms ease,
box-shadow 160ms ease,
border-color 160ms ease,
background 160ms ease;
min-width: 0;
}
.maf-module-card:hover {
transform: translateY(-1px);
border-color: rgba(110, 90, 68, 0.28);
background: rgba(250, 246, 240, 0.98);
box-shadow: 0 22px 54px rgba(16, 12, 8, 0.1);
}
.maf-module-card:focus-visible {
outline: none;
box-shadow:
0 22px 54px rgba(16, 12, 8, 0.1),
0 0 0 3px rgba(201, 169, 97, 0.38);
}
.maf-module-card__head {
display: grid;
grid-template-columns: auto 1fr;
gap: 12px;
align-items: start;
min-width: 0;
}
.maf-module-card__icon {
width: 44px;
height: 44px;
border-radius: 14px;
display: grid;
place-items: center;
background: rgba(167, 138, 104, 0.14);
border: 1px solid rgba(110, 90, 68, 0.18);
color: rgba(110, 90, 68, 0.9);
flex: 0 0 auto;
}
.maf-module-card__headText {
min-width: 0;
display: grid;
gap: 4px;
}
.maf-module-card__row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
min-width: 0;
}
.maf-module-card__title {
font-size: 1rem;
font-weight: 850;
color: rgba(46, 39, 32, 0.96);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.maf-module-card__badge {
font-size: 0.74rem;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 6px 10px;
border-radius: 999px;
border: 1px solid rgba(201, 169, 97, 0.35);
background: rgba(201, 169, 97, 0.14);
color: rgba(110, 90, 68, 0.92);
flex: 0 0 auto;
}
.maf-module-card__meta {
font-size: 0.82rem;
color: rgba(110, 90, 68, 0.7);
}
.maf-module-card__desc {
margin: 0;
font-size: 0.92rem;
line-height: 1.35;
color: rgba(110, 90, 68, 0.78);
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.maf-module-card__cta {
font-size: 0.86rem;
font-weight: 850;
color: rgba(139, 21, 56, 0.9);
}
@media (max-width: 520px) {
.maf-module-card {
border-radius: 14px;
padding: 12px 12px;
}
}
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { ModuleDashboardMenu } from "./ModuleDashboardMenu";
const meta = {
title: "Panel/Modules/ModuleDashboardMenu",
component: ModuleDashboardMenu,
parameters: { layout: "padded" },
} satisfies Meta<typeof ModuleDashboardMenu>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
items: [
{ label: "Dashboard", href: "/panel/incidents", active: true },
{ label: "New incident", href: "/panel/incidents/new" },
{ label: "Incident tracking", href: "/panel/incidents/tracking" },
{ label: "Incident listing", href: "/panel/incidents/list" },
{ label: "Monthly data", href: "/panel/incidents/monthly" },
],
},
};
export const Mobile: Story = {
args: Default.args,
parameters: { viewport: { defaultViewport: "mobile1" } },
};
"use client";
import Link from "next/link";
import React from "react";
import "./module-dashboard-menu.css";
export type ModuleMenuItem = {
label: string;
href: string;
active?: boolean;
};
export type ModuleDashboardMenuProps = {
items: ModuleMenuItem[];
ariaLabel?: string;
};
export function ModuleDashboardMenu({
items,
ariaLabel = "Module navigation",
}: ModuleDashboardMenuProps) {
return (
<nav className="maf-modMenu" aria-label={ariaLabel}>
<div className="maf-modMenu__scroll">
{items.map((item) => (
<Link
key={item.href}
href={item.href}
className={`maf-modMenu__item ${item.active ? "is-active" : ""}`.trim()}
aria-current={item.active ? "page" : undefined}
>
{item.label}
</Link>
))}
</div>
</nav>
);
}
.maf-modMenu {
margin: 0.2rem 0.2rem 0.85rem;
border-radius: 999px;
border: 1px solid rgba(110, 90, 68, 0.16);
background: rgba(255, 255, 255, 0.78);
box-shadow:
0 18px 44px rgba(16, 12, 8, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.68);
overflow: hidden;
}
.maf-modMenu__scroll {
display: flex;
gap: 6px;
padding: 8px 8px;
overflow: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.maf-modMenu__scroll::-webkit-scrollbar {
display: none;
}
.maf-modMenu__item {
text-decoration: none;
border-radius: 999px;
padding: 9px 12px;
font-size: 0.82rem;
font-weight: 900;
letter-spacing: 0.06em;
text-transform: uppercase;
color: rgba(110, 90, 68, 0.86);
border: 1px solid rgba(110, 90, 68, 0.14);
background: rgba(250, 246, 240, 0.72);
white-space: nowrap;
transition:
transform 160ms ease,
background 160ms ease,
border-color 160ms ease;
}
.maf-modMenu__item:hover {
transform: translateY(-0.5px);
background: rgba(250, 246, 240, 0.96);
border-color: rgba(110, 90, 68, 0.22);
}
.maf-modMenu__item.is-active {
background: rgba(201, 169, 97, 0.18);
border-color: rgba(201, 169, 97, 0.46);
color: rgba(46, 39, 32, 0.96);
}
.maf-modMenu__item:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(201, 169, 97, 0.32);
}
@media (max-width: 520px) {
.maf-modMenu {
margin: 0.1rem 0.1rem 0.75rem;
}
}
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { ModulesPage } from "./ModulesPage";
const meta = {
title: "Panel/Modules/ModulesPage",
component: ModulesPage,
parameters: {
layout: "fullscreen",
},
} satisfies Meta<typeof ModulesPage>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Tablet: Story = {
parameters: {
viewport: { defaultViewport: "tablet" },
},
};
export const Mobile: Story = {
parameters: {
viewport: { defaultViewport: "mobile1" },
},
};
"use client";
import React from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { ModuleCard } from "../ModuleCard/ModuleCard";
import { moduleCategories, modules } from "../modules-data";
import "./modules-page.css";
export function ModulesPage() {
const router = useRouter();
const searchParams = useSearchParams();
const urlQuery = (searchParams.get("q") ?? "").trim();
const [query, setQuery] = React.useState(urlQuery);
const [category, setCategory] = React.useState<(typeof moduleCategories)[number]>(
"All",
);
React.useEffect(() => {
setQuery(urlQuery);
}, [urlQuery]);
React.useEffect(() => {
const q = query.trim();
const next = new URLSearchParams(searchParams.toString());
if (q) next.set("q", q);
else next.delete("q");
const qs = next.toString();
router.replace(qs ? `/panel/modules?${qs}` : "/panel/modules");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query]);
const filtered = React.useMemo(() => {
const q = query.trim().toLowerCase();
return modules.filter((m) => {
const inCat = category === "All" ? true : m.category === category;
if (!inCat) return false;
if (!q) return true;
return (
m.title.toLowerCase().includes(q) ||
m.description.toLowerCase().includes(q) ||
m.category.toLowerCase().includes(q)
);
});
}, [query, category]);
return (
<div className="maf-modules">
<header className="maf-modules__header">
<div className="maf-modules__titleBlock">
<h1 className="maf-modules__title">Modules</h1>
<p className="maf-modules__subtitle">
Find what you need fast. Search, filter, then jump into a workflow.
</p>
</div>
<div className="maf-modules__controls" aria-label="Module controls">
<label className="maf-modules__search">
<span className="maf-modules__srOnly">Search modules</span>
<span className="maf-modules__searchIcon" aria-hidden="true">
</span>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search modules…"
inputMode="search"
/>
{query ? (
<button
type="button"
className="maf-modules__clear"
onClick={() => setQuery("")}
aria-label="Clear search"
>
×
</button>
) : null}
</label>
<div className="maf-modules__chips" role="tablist" aria-label="Categories">
{moduleCategories.map((c) => (
<button
key={c}
type="button"
role="tab"
aria-selected={category === c}
className={`maf-modules__chip ${category === c ? "is-active" : ""}`.trim()}
onClick={() => setCategory(c)}
>
{c}
</button>
))}
</div>
</div>
</header>
{filtered.length === 0 ? (
<div className="maf-modules__empty" role="status" aria-live="polite">
<div className="maf-modules__emptyTitle">No modules found</div>
<div className="maf-modules__emptyBody">
Try a different search term or clear filters.
</div>
<div className="maf-modules__emptyActions">
<button
type="button"
className="maf-modules__emptyBtn"
onClick={() => {
setQuery("");
setCategory("All");
}}
>
Reset
</button>
</div>
</div>
) : (
<section className="maf-modules__grid" aria-label="Modules list">
{filtered.map((m) => (
<ModuleCard
key={m.id}
title={m.title}
description={m.description}
href={m.href}
badge={m.badge}
meta={m.meta ?? m.category}
/>
))}
</section>
)}
</div>
);
}
.maf-modules {
width: 100%;
padding: 1.1rem 1.1rem 1.2rem;
box-sizing: border-box;
min-width: 0;
}
.maf-modules__header {
display: grid;
grid-template-columns: 1fr;
gap: 14px;
padding: 0.2rem 0.2rem 0.8rem;
}
.maf-modules__title {
margin: 0;
font-size: 1.35rem;
font-weight: 920;
letter-spacing: -0.02em;
color: rgba(46, 39, 32, 0.96);
}
.maf-modules__subtitle {
margin: 6px 0 0;
font-size: 0.92rem;
line-height: 1.35;
color: rgba(110, 90, 68, 0.74);
}
.maf-modules__controls {
display: grid;
gap: 10px;
}
.maf-modules__search {
border-radius: 999px;
border: 1px solid rgba(110, 90, 68, 0.18);
background: rgba(255, 255, 255, 0.72);
padding: 10px 12px;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 10px;
box-shadow:
0 16px 38px rgba(16, 12, 8, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.72);
}
.maf-modules__search:focus-within {
border-color: rgba(201, 169, 97, 0.55);
box-shadow:
0 16px 38px rgba(16, 12, 8, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.72),
0 0 0 3px rgba(201, 169, 97, 0.32);
}
.maf-modules__searchIcon {
width: 26px;
height: 26px;
border-radius: 999px;
display: grid;
place-items: center;
background: rgba(167, 138, 104, 0.14);
border: 1px solid rgba(110, 90, 68, 0.16);
color: rgba(110, 90, 68, 0.86);
font-weight: 900;
}
.maf-modules__search input {
width: 100%;
min-width: 0;
border: 0;
outline: none;
background: transparent;
color: rgba(46, 39, 32, 0.96);
font-size: 0.95rem;
}
.maf-modules__search input::placeholder {
color: rgba(110, 90, 68, 0.58);
}
.maf-modules__clear {
width: 32px;
height: 32px;
border-radius: 999px;
border: 1px solid rgba(110, 90, 68, 0.16);
background: rgba(255, 255, 255, 0.78);
color: rgba(110, 90, 68, 0.86);
font-size: 1.2rem;
line-height: 1;
display: grid;
place-items: center;
cursor: pointer;
}
.maf-modules__clear:hover {
background: rgba(250, 246, 240, 0.98);
}
.maf-modules__chips {
display: flex;
gap: 8px;
overflow: auto;
padding: 2px 2px 4px;
-webkit-overflow-scrolling: touch;
}
.maf-modules__chip {
border-radius: 999px;
border: 1px solid rgba(110, 90, 68, 0.18);
background: rgba(255, 255, 255, 0.7);
padding: 8px 12px;
font-size: 0.86rem;
font-weight: 820;
color: rgba(110, 90, 68, 0.86);
cursor: pointer;
white-space: nowrap;
transition:
background 160ms ease,
border-color 160ms ease,
transform 160ms ease;
}
.maf-modules__chip:hover {
transform: translateY(-0.5px);
background: rgba(250, 246, 240, 0.98);
border-color: rgba(110, 90, 68, 0.26);
}
.maf-modules__chip.is-active {
background: rgba(201, 169, 97, 0.16);
border-color: rgba(201, 169, 97, 0.45);
color: rgba(46, 39, 32, 0.96);
}
.maf-modules__grid {
display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr));
gap: 12px;
padding: 0.2rem;
min-width: 0;
}
.maf-modules__empty {
margin: 0.3rem 0.2rem 0;
border-radius: 16px;
border: 1px dashed rgba(110, 90, 68, 0.26);
background: rgba(255, 255, 255, 0.62);
padding: 18px 14px;
display: grid;
gap: 8px;
}
.maf-modules__emptyTitle {
font-weight: 900;
color: rgba(46, 39, 32, 0.96);
}
.maf-modules__emptyBody {
color: rgba(110, 90, 68, 0.78);
}
.maf-modules__emptyActions {
margin-top: 4px;
}
.maf-modules__emptyBtn {
border-radius: 12px;
border: 1px solid rgba(201, 169, 97, 0.42);
background: rgba(201, 169, 97, 0.14);
padding: 10px 12px;
font-weight: 850;
color: rgba(110, 90, 68, 0.92);
cursor: pointer;
}
.maf-modules__emptyBtn:hover {
background: rgba(201, 169, 97, 0.2);
}
.maf-modules__srOnly {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
@media (min-width: 560px) {
.maf-modules__grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 900px) {
.maf-modules__header {
grid-template-columns: 1.1fr 1fr;
align-items: end;
gap: 18px;
}
.maf-modules__controls {
justify-self: end;
width: min(520px, 100%);
}
.maf-modules__grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
}
}
@media (min-width: 1200px) {
.maf-modules__grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
export type ModuleCategory = "Operations" | "Assurance" | "Compliance" | "Admin";
export type ModuleItem = {
id: string;
title: string;
description: string;
href: string;
badge?: string;
meta?: string;
category: ModuleCategory;
};
export const modules: ModuleItem[] = [
{
id: "incidents",
title: "Incidents",
description: "Report, classify and resolve incidents with fast escalation paths.",
href: "/panel/incidents",
badge: "Hot",
meta: "Last updated today",
category: "Operations",
},
{
id: "audits",
title: "Audits",
description: "Plan audits, assign checklists, track completion and export reports.",
href: "/panel/audits",
meta: "Rolling 30 days",
category: "Assurance",
},
{
id: "inspections",
title: "Inspection",
description: "Mobile-friendly inspections with offline support and evidence capture.",
href: "/panel/inspection",
badge: "Field",
meta: "Offline ready",
category: "Operations",
},
{
id: "checklist",
title: "Checklist",
description: "Daily/weekly checklists with blockers, approvals and audit trail.",
href: "/panel/checklist",
category: "Compliance",
},
{
id: "permit",
title: "Permit",
description: "Manage permits, approvals and contractor workflows in one place.",
href: "/panel/permit",
category: "Operations",
},
{
id: "suggestion",
title: "Suggestion",
description: "Capture improvement ideas and route them for review and action.",
href: "/panel/suggestion",
category: "Admin",
},
{
id: "tracking",
title: "Tracking",
description: "Track corrective actions, due dates and overdue items across sites.",
href: "/panel/tracking",
category: "Assurance",
},
{
id: "admin",
title: "Admin",
description: "Users, roles, configuration and operational control centre.",
href: "/panel/admin",
badge: "Restricted",
category: "Admin",
},
];
export const moduleCategories: Array<ModuleCategory | "All"> = [
"All",
"Operations",
"Assurance",
"Compliance",
"Admin",
];
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { PermitDashboard } from "./PermitDashboard";
const meta = {
title: "Panel/Modules/Permit/Dashboard",
component: PermitDashboard,
parameters: { layout: "fullscreen" },
} satisfies Meta<typeof PermitDashboard>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Tablet: Story = { parameters: { viewport: { defaultViewport: "tablet" } } };
export const Mobile: Story = { parameters: { viewport: { defaultViewport: "mobile1" } } };
"use client";
import React from "react";
import { TopStatCard } from "@/components/panel/dashboard/TopStatCard/TopStatCard";
import { DashboardCard } from "@/components/panel/dashboard/DashboardCard/DashboardCard";
import { ModuleDashboardMenu } from "@/components/panel/modules/ModuleDashboardMenu/ModuleDashboardMenu";
import "./permit-dashboard.css";
type PermitKpi = {
label: string;
value: number;
helper?: string;
tone: "good" | "warn" | "info" | "danger" | "neutral";
};
const kpis: PermitKpi[] = [
{ label: "Approved permits", value: 1239, tone: "good", helper: "Active" },
{ label: "Needs change", value: 49, tone: "info", helper: "Returned" },
{ label: "Extension requested", value: 422, tone: "info", helper: "In review" },
{ label: "Rejected permits", value: 7, tone: "danger", helper: "This month" },
{ label: "Pending approval (permits)", value: 1476, tone: "warn", helper: "Waiting" },
{ label: "Pending approval (contractors)", value: 11, tone: "warn", helper: "Waiting" },
{ label: "Rejected contractors", value: 6, tone: "danger", helper: "This month" },
{ label: "Violations", value: 9, tone: "neutral", helper: "Open" },
];
export function PermitDashboard() {
return (
<div className="maf-permitDash">
<header className="maf-permitDash__header">
<div>
<h1 className="maf-permitDash__title">Permit</h1>
<p className="maf-permitDash__subtitle">
At-a-glance permit health. Prioritize approvals and exceptions.
</p>
</div>
</header>
<ModuleDashboardMenu
ariaLabel="Permit navigation"
items={[
{ label: "Dashboard", href: "/panel/permit", active: true },
{ label: "Create project", href: "/panel/permit/create-project" },
{ label: "Permit list", href: "/panel/permit/list" },
{ label: "Invite contractor", href: "/panel/permit/invite-contractor" },
{ label: "Contractor list", href: "/panel/permit/contractors" },
]}
/>
<section className="maf-permitDash__kpis" aria-label="Permit KPIs">
{kpis.map((k) => (
<TopStatCard
key={k.label}
label={k.label}
value={k.value}
helper={k.helper}
underlineTone={k.tone}
/>
))}
</section>
<section className="maf-permitDash__grid" aria-label="Permit dashboard content">
<DashboardCard title="Review queue" subtitle="What needs attention">
<div className="maf-permitDash__queue">
<div className="maf-permitDash__queueRow">
<span>Pending approvals</span>
<strong>{(1476 + 11).toLocaleString()}</strong>
</div>
<div className="maf-permitDash__queueRow">
<span>Extensions</span>
<strong>{(422).toLocaleString()}</strong>
</div>
<div className="maf-permitDash__queueRow">
<span>Needs change</span>
<strong>{(49).toLocaleString()}</strong>
</div>
<div className="maf-permitDash__queueRow">
<span>Violations</span>
<strong>{(9).toLocaleString()}</strong>
</div>
</div>
</DashboardCard>
</section>
</div>
);
}
.maf-permitDash {
width: 100%;
padding: 1.1rem 1.1rem 1.2rem;
box-sizing: border-box;
min-width: 0;
}
.maf-permitDash__header {
padding: 0.2rem 0.2rem 0.8rem;
}
.maf-permitDash__title {
margin: 0;
font-size: 1.35rem;
font-weight: 920;
letter-spacing: -0.02em;
color: rgba(46, 39, 32, 0.96);
}
.maf-permitDash__subtitle {
margin: 6px 0 0;
font-size: 0.92rem;
line-height: 1.35;
color: rgba(110, 90, 68, 0.74);
}
.maf-permitDash__kpis {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
padding: 0.2rem;
}
.maf-permitDash__grid {
margin-top: 12px;
display: grid;
grid-template-columns: 1.2fr 0.8fr;
gap: 12px;
padding: 0.2rem;
align-items: start;
}
.maf-permitDash__actions {
display: grid;
gap: 10px;
}
.maf-permitDash__btn {
border-radius: 14px;
border: 1px solid rgba(201, 169, 97, 0.45);
background: rgba(201, 169, 97, 0.24);
color: rgba(46, 39, 32, 0.96);
padding: 12px 12px;
font-weight: 900;
cursor: pointer;
box-shadow: 0 18px 44px rgba(16, 12, 8, 0.08);
text-align: left;
}
.maf-permitDash__btn:hover {
background: rgba(201, 169, 97, 0.32);
}
.maf-permitDash__btn--ghost {
background: rgba(255, 255, 255, 0.84);
border: 1px solid rgba(110, 90, 68, 0.18);
}
.maf-permitDash__btn--ghost:hover {
background: rgba(250, 246, 240, 0.98);
}
.maf-permitDash__queue {
display: grid;
gap: 10px;
}
.maf-permitDash__queueRow {
border-radius: 14px;
border: 1px solid rgba(110, 90, 68, 0.14);
background: rgba(255, 255, 255, 0.84);
padding: 12px 12px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
color: rgba(110, 90, 68, 0.86);
}
.maf-permitDash__queueRow strong {
color: rgba(46, 39, 32, 0.96);
font-variant-numeric: tabular-nums;
}
@media (max-width: 1024px) {
.maf-permitDash__kpis {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.maf-permitDash__grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 520px) {
.maf-permitDash {
padding: 0.9rem 0.65rem 1.1rem;
}
}
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { SuggestionDashboard } from "./SuggestionDashboard";
const meta = {
title: "Panel/Modules/Suggestion/Dashboard",
component: SuggestionDashboard,
parameters: { layout: "fullscreen" },
} satisfies Meta<typeof SuggestionDashboard>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Tablet: Story = { parameters: { viewport: { defaultViewport: "tablet" } } };
export const Mobile: Story = { parameters: { viewport: { defaultViewport: "mobile1" } } };
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment