shoc-frontend-new/src/components/layout/app-sidebar.tsx

210 lines
6.3 KiB
TypeScript
Raw Normal View History

Feat/vite typescript migration (#16) * chore: add eslint, prettier, husky and commitlint tooling * ci: add GitHub Actions workflow for lint and build * build: migrate from CRA to Vite with TypeScript config * docs: add architecture plan and design system documentation * fix: scope ESLint to new components and hooks directories * feat: add HTTP client, query cache and shared utilities * feat: add theme system and global application styles * feat: add shared UI, layout and domain badge components * feat: add auth domain, provider and login page * feat: add app shell, file-based routing and bootstrap * feat: add protected layout and dashboard module * feat: add accounts CRUD module * feat: add assets CRUD module * feat: add contacts CRUD module * feat: add employees CRUD module * feat: add locations CRUD module * feat: add calendar events module * feat: add follow-ups CRUD module * feat: add PM schedules CRUD module * feat: add work orders module with dispatch modals * feat: add vendors CRUD and portal token panel * feat: add vendor purchase orders module * feat: add uplifts queue module * feat: add settings for dropdowns and task templates * feat: add vendor portal routes and signature capture * test: add Vitest setup and Playwright login e2e spec * chore: remove legacy CRA pages, Redux store and JS hooks * chore: update gitignore and env example for Vite * docs: translate pt-BR docs and Cursor rules to English * fix(ci): fix login e2e session mock and prettier formatting * fix(build): use mjs router script for Node 20 CI compatibility * chore: remove migration scripts and unused generouted router * fix(auth): restore JWT and align CRUD with backend routes * fix(api): add no-content helpers and align paths with backend * fix(accounts): align detail and mutation payloads with backend * fix(contacts): resolve detail via GetContacts and map address DTOs * fix(work-orders): align routes, delete body, and create payload * fix(vendors): delete vendors via REST route * fix(pm-schedules): handle empty save and delete responses * fix(employees): add fallbacks for detail and dropdown calls * chore(calendar): disable routes until backend exists * chore(env): switch tracked env vars to Vite prefixes * fix(api): align frontend contracts with backend review findings Correct Work Order getById query param, asset site options via Location API, Employee JobTitleId payload, and remove stale API paths. * fix(employees,pm-schedules): align forms with backend API contracts Align PM Schedule form and save payload with PMSchedule_DTO fields. Bind employee Job Title select to jobTitleId for create/update payloads. Add regression tests for both flows. * fix(pm-schedules): gate edit/delete for Dev API contract Production Dev API exposes only GetList and Save. Hide unsupported edit/delete UI and block the edit route. Add regression tests for disabled actions. * docs(env): document VITE_API_URL must include /api suffix * refactor(api): extract shared API prefix URL resolution * feat(build): fail build on misconfigured absolute VITE_API_URL * test(api): cover API URL contract and prefix resolution * feat(auth): disable login submit until email and password are valid * test(auth): align login tests with disabled submit behavior * Feat/ab/menu-and-header (#17) * chore(deps): add lucide-react for layout icon migration * feat(auth): add getPrimaryUserRole helper for header display * style(theme): add sidebar active tokens and nav group typography * refactor(menu): migrate nav icons to lucide and trim menu groups * feat(layout): redesign sidebar, topbar, and admin shell viewport * chore(menu): hide Reports and Documents from sidebar --------- Co-authored-by: Arthur Bassi <arthur.winiarski.ranger@outlook.com> * ci: add frontend PR quality baseline (#18) * chore(deps): add lucide-react for layout icon migration * feat(auth): add getPrimaryUserRole helper for header display * style(theme): add sidebar active tokens and nav group typography * refactor(menu): migrate nav icons to lucide and trim menu groups * feat(layout): redesign sidebar, topbar, and admin shell viewport * chore(menu): hide Reports and Documents from sidebar * ci: add PR quality baseline checks * ci: avoid self-matching standards guard * ci: split frontend quality gates --------- Co-authored-by: Arthur Bassi <arthur.winiarski.ranger@outlook.com> --------- Co-authored-by: Arthur Bassi <arthur.winiarski.ranger@outlook.com> Co-authored-by: Alexandre Brandizzi <alex_brandizzi@hotmail.com>
2026-06-18 14:41:17 -03:00
import { useState } from "react";
import { NavLink } from "react-router-dom";
import { Box, Drawer } from "@mui/material";
import { BrandLockup, BrandMark } from "@/components/common/brand-lockup";
import { getSidebarWidth, navGroups, type NavItemDef } from "@/config/menu";
function getNavItemAppearance(isActive: boolean, isHover: boolean) {
if (isActive) {
return {
itemColor: "var(--primary)",
iconColor: "var(--primary)",
itemBg: "var(--color-sidebar-item-active-bg)",
};
}
if (isHover) {
return {
itemColor: "var(--primary-hover)",
iconColor: "var(--primary-hover)",
itemBg: "var(--sidebar-accent)",
};
}
return {
itemColor: "var(--foreground)",
iconColor: "var(--muted-foreground)",
itemBg: "transparent",
};
}
type NavItemProps = NavItemDef & {
collapsed: boolean;
end?: boolean;
};
function NavItem({ label, icon: Icon, path, collapsed, end, badge }: NavItemProps) {
const [hovered, setHovered] = useState(false);
return (
<NavLink to={path} end={end} className="no-underline" title={collapsed ? label : undefined}>
{({ isActive }) => {
const isHover = hovered && !isActive;
const { itemColor, iconColor, itemBg } = getNavItemAppearance(isActive, isHover);
return (
<Box
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
sx={{
position: "relative",
display: "flex",
alignItems: "center",
width: "100%",
height: 40,
gap: "12px",
px: collapsed ? 0 : "12px",
justifyContent: collapsed ? "center" : "flex-start",
borderRadius: "6px",
cursor: "pointer",
fontFamily: "var(--font-sans)",
fontSize: 14,
fontWeight: isActive ? 600 : 500,
color: itemColor,
backgroundColor: itemBg,
boxShadow: isActive ? "var(--color-sidebar-item-active-shadow)" : "none",
transition: "all var(--duration-fast, 150ms) var(--ease-default, ease)",
mb: "4px",
textAlign: "left",
}}
>
{isActive && (
<Box
component="span"
sx={{
position: "absolute",
left: 0,
top: 8,
bottom: 8,
width: 3,
borderRadius: "0 3px 3px 0",
backgroundColor: "var(--primary)",
}}
/>
)}
<Icon size={19} className="shrink-0" style={{ color: iconColor }} />
{!collapsed && (
<>
<Box className="truncate flex-1">{label}</Box>
{badge != null && (
<Box
component="span"
className="inline-flex shrink-0 items-center justify-center"
sx={{
height: 18,
minWidth: 18,
px: "6px",
borderRadius: "9999px",
backgroundColor: "var(--accent)",
color: "#ffffff",
fontFamily: "var(--font-sans)",
fontSize: 10,
fontWeight: 700,
lineHeight: 1,
}}
>
{badge}
</Box>
)}
</>
)}
</Box>
);
}}
</NavLink>
);
}
type AppSidebarProps = {
collapsed: boolean;
};
export function AppSidebar({ collapsed }: AppSidebarProps) {
const width = getSidebarWidth(collapsed);
return (
<Drawer
variant="permanent"
sx={{
width,
flexShrink: 0,
"& .MuiDrawer-paper": {
width,
boxSizing: "border-box",
backgroundColor: "var(--color-sidebar-bg)",
borderRight: "1px solid var(--color-sidebar-border)",
borderRadius: 0,
transition: "width 200ms var(--ease-default, ease)",
overflowX: "hidden",
},
}}
>
<Box className="flex h-full flex-col overflow-hidden">
<Box
className="flex shrink-0 items-center"
sx={{
height: "var(--spacing-topbar)",
gap: "10px",
px: collapsed ? 0 : "16px",
justifyContent: collapsed ? "center" : "flex-start",
backgroundColor: "var(--card)",
borderBottom: "1px solid var(--color-sidebar-border)",
}}
>
{collapsed ? <BrandMark size="compact" /> : <BrandLockup size="compact" />}
</Box>
<Box component="nav" className="flex-1 overflow-y-auto overflow-x-hidden" sx={{ py: 1 }}>
{navGroups.map((group, groupIndex) => (
<Box key={group.title || `group-${groupIndex}`}>
{collapsed ? (
groupIndex > 0 && (
<Box
sx={{
height: 1,
backgroundColor: "var(--color-sidebar-border)",
mx: "14px",
my: "10px",
}}
/>
)
) : group.title ? (
<Box
component="p"
className="text-nav-group"
sx={{
m: 0,
px: "10px",
pt: "12px",
pb: "6px",
}}
>
{group.title}
</Box>
) : (
<Box sx={{ height: 8 }} />
)}
<Box component="ul" sx={{ listStyle: "none", m: 0, p: "0 8px" }}>
{group.items.map(({ label, icon, path, badge }) => (
<Box component="li" key={path} sx={{ listStyle: "none" }}>
<NavItem
label={label}
icon={icon}
path={path}
badge={badge}
collapsed={collapsed}
end={path === "/"}
/>
</Box>
))}
</Box>
</Box>
))}
</Box>
</Box>
</Drawer>
);
}