diff --git a/app/routes/authenticated/_layout.tsx b/app/routes/authenticated/_layout.tsx index 0634bd2..959568c 100644 --- a/app/routes/authenticated/_layout.tsx +++ b/app/routes/authenticated/_layout.tsx @@ -1,61 +1,111 @@ +import { Dashboard, Logout, RateReview } from "@mui/icons-material"; import { Box } from "@mui/material"; import { styled } from "@mui/material/styles"; import { Link, NavLink, Outlet } from "react-router"; - import phlasklogo from "~/assets/PHLASK_v2.svg"; +import { ThemeToggle } from "~/components/ThemeToggle"; +import { WaveDivider } from "~/components/WaveDivider"; +import { brand, navy } from "~/theme/theme"; export const action = () => {}; -const SidebarNavLink = styled(NavLink)({ - display: "block", - borderRadius: 8, - paddingInline: 16, - paddingBlock: 10, +const SidebarNavLink = styled(NavLink)(({ theme }) => ({ + display: "flex", + alignItems: "center", + gap: theme.spacing(1.25), + borderRadius: 12, + padding: `${theme.spacing(1.25)} ${theme.spacing(2)}`, fontSize: "0.875rem", fontWeight: 500, textDecoration: "none", - color: "#4b5563", - transition: "background-color 0.2s ease, color 0.2s ease", - "&:hover": { backgroundColor: "#f3f4f6" }, + color: navy[600], + transition: "color 0.2s ease, background-color 0.2s ease", + "&:hover": { backgroundColor: brand[50] }, "&.active": { - color: "#1d4ed8", - backgroundColor: "#eff6ff", + color: brand[700], + backgroundColor: brand[50], }, -}); + ...theme.applyStyles("dark", { + color: `${brand[100]}cc`, + "&:hover": { backgroundColor: navy[800] }, + "&.active": { + color: brand[300], + backgroundColor: navy[800], + }, + }), +})); export default function DashboardLayout() { return ( ({ display: "flex", height: "100vh", - bgcolor: "#f9fafb", - color: "#111827", - }} + bgcolor: brand[50], + color: navy[900], + ...theme.applyStyles("dark", { bgcolor: navy[950], color: brand[50] }), + })} > ({ display: "flex", width: 256, flexDirection: "column", bgcolor: "#ffffff", - borderRight: "1px solid #e5e7eb", - }} + ...theme.applyStyles("dark", { bgcolor: navy[900] }), + })} > - + ({ + position: "relative", + overflow: "hidden", + backgroundImage: `linear-gradient(to bottom right, ${brand[400]}, ${brand[600]})`, + pt: 3, + pb: 4.5, + pl: 3, + ...theme.applyStyles("dark", { + backgroundImage: `linear-gradient(to bottom right, ${navy[600]}, ${navy[800]})`, + }), + })} + > + ({ + display: "inline-flex", + borderRadius: 3, + bgcolor: "rgba(255,255,255,0.9)", + p: 1, + boxShadow: 1, + ...theme.applyStyles("dark", { bgcolor: `${navy[950]}b3` }), + })} + > + PHLASK Logo + - PHLASK Logo + Admin dashboard + ({ + bottom: 0, + color: "#ffffff", + ...theme.applyStyles("dark", { color: navy[900] }), + })} + /> - - - Dashboard - + li + li": { mt: 0.5 } }} + > +
  • + + + Dashboard + +
  • +
  • + + + Reviews + +
  • ({ + display: "flex", + flexDirection: "column", + gap: 1, + borderTop: `1px solid ${brand[100]}`, + pt: 1.5, + ...theme.applyStyles("dark", { borderTopColor: navy[700] }), + })} > - Logout + + ({ + fontSize: "0.75rem", + fontWeight: 600, + ...theme.applyStyles("dark", { color: `${brand[100]}b3` }), + })} + > + Theme + + + + ({ + display: "flex", + alignItems: "center", + gap: 1.25, + borderRadius: 3, + px: 2, + py: 1.25, + fontSize: "0.875rem", + fontWeight: 500, + textDecoration: "none", + color: navy[600], + transition: "background-color 0.2s ease", + "&:hover": { bgcolor: brand[50] }, + ...theme.applyStyles("dark", { + color: `${brand[100]}cc`, + "&:hover": { bgcolor: navy[800] }, + }), + })} + > + + Logout +
    diff --git a/app/routes/authenticated/dashboard.tsx b/app/routes/authenticated/dashboard.tsx index 83ac4cf..845c394 100644 --- a/app/routes/authenticated/dashboard.tsx +++ b/app/routes/authenticated/dashboard.tsx @@ -1,14 +1,235 @@ -import type { MiddlewareFunction } from "react-router"; +import { HourglassTop, PendingActions, WaterDrop } from "@mui/icons-material"; +import { + Box, + Chip, + Paper, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, + Tooltip, + Typography, +} from "@mui/material"; +import type { LoaderFunction } from "react-router"; +import { Link, useLoaderData } from "react-router"; +import { getDatabaseClient } from "~/api/client.server"; +import { getResourceRevisionAPI } from "~/api/resource-revisions/methods"; import { authMiddleware } from "~/middleware/auth"; +import type { ResourceType } from "~/types/ResourceEntry"; +import { + resourceTypeChipColor, + resourceTypeChipIcon, +} from "~/utils/chipColors"; -export const middleware: MiddlewareFunction[] = [authMiddleware]; +export const middleware = [authMiddleware]; + +const RESOURCE_TYPES: ResourceType[] = ["WATER", "FOOD", "FORAGE", "BATHROOM"]; + +export const loader: LoaderFunction = async ({ request }) => { + const { client } = getDatabaseClient(request); + const revisionAPI = getResourceRevisionAPI(client); + const revisions = await revisionAPI.getList(); + + const submitterCounts = new Map(); + const outstandingByType = new Map(); + let pendingCount = 0; + + for (const revision of revisions) { + const creator = revision.creator || "Unknown"; + submitterCounts.set(creator, (submitterCounts.get(creator) ?? 0) + 1); + + if (revision.status === "PENDING") { + pendingCount += 1; + outstandingByType.set( + revision.resource_type, + (outstandingByType.get(revision.resource_type) ?? 0) + 1, + ); + } + } + + const topSubmitters = [...submitterCounts.entries()] + .sort((a, b) => b[1] - a[1]) + .slice(0, 5) + .map(([creator, count]) => ({ creator, count })); + + const outstanding = RESOURCE_TYPES.map((type) => ({ + type, + count: outstandingByType.get(type) ?? 0, + })); + + return { + totalRevisions: revisions.length, + pendingCount, + topSubmitters, + outstanding, + }; +}; + +type LoaderData = { + totalRevisions: number; + pendingCount: number; + topSubmitters: { creator: string; count: number }[]; + outstanding: { type: string; count: number }[]; +}; + +const StatCard = ({ + label, + value, + icon, +}: { + label: string; + value: string | number; + icon: React.ReactNode; +}) => ( + + + {icon} + + + + {label} + + + {value} + + + +); + +const Dashboard = () => { + const { totalRevisions, pendingCount, topSubmitters, outstanding } = + useLoaderData(); -export default function Dashboard() { return ( -
    -

    - Dashboard Overview -

    -
    + + + + Dashboard overview + + + Snapshot of resource edit submissions and review activity. + + + + + } + /> + } + /> + + + + + + + + Outstanding by resource type + + + + + + {outstanding.map(({ type, count }) => ( + + + + + + {count} + + + ))} + +
    +
    + + View pending reviews → + +
    + + + + Top submitters + + {topSubmitters.length === 0 ? ( + + No submissions yet. + + ) : ( + + + + {topSubmitters.map(({ creator, count }) => ( + + {creator} + + {count} + + + ))} + +
    +
    + )} +
    + + + + + Top approvers + + + + Not trackable yet — approvals don't record who approved them. + + +
    +
    ); -} +}; + +export default Dashboard;