feat: implement permissions and actions management
CI / Lint, typecheck, test, build (push) Canceled after 0s
CI / End-to-end tests (push) Canceled after 0s

- Add permissions logic for status options in corrective actions.
- Create tests for permissions functionality.
- Implement queries for user authentication and actions management.
- Develop Actions page to display and manage corrective actions.
- Create authentication pages for login, setup, and password reset.
- Add IncidentDetail page to view incident details and associated actions.
- Implement Settings page for user management and password changes.
This commit is contained in:
jayjennings committed 2026-10-06 11:20:44 -07:00
1 parent 708da6b9f7
commit 3f6dff5f24
94 files changed
+7213 -643

No files matched your search

+32 -26
View File
@@ -1,7 +1,9 @@
import { ChevronDown, FileText, Info, Plus, Settings, ShieldCheck, X } from 'lucide-react'
import { NavLink } from 'react-router-dom'
import { Info, LogOut, Plus, Settings, ShieldCheck, X } from 'lucide-react'
import { NavLink, useNavigate } from 'react-router-dom'
import { initials } from '../lib/initials'
import { roleLabel } from '../lib/labels'
import { primaryNav } from '../lib/navigation'
import { currentUser, workspace } from '../lib/seed'
import { useLogout, type Me } from '../lib/queries'
import { Eyebrow } from './Eyebrow'
import { iconButton } from './styles'
@@ -10,22 +12,25 @@ const navLink = ({ isActive }: { isActive: boolean }) =>
const navSection = 'mb-6 border-b border-forest-700 pb-[23px]'
interface SidebarProps {
me: Me
open: boolean
onClose: () => void
openIncidentCount: number
badges: Partial<Record<string, number>>
}
export function Sidebar({ open, onClose, openIncidentCount }: SidebarProps) {
export function Sidebar({ me, open, onClose, badges }: SidebarProps) {
const logout = useLogout()
const navigate = useNavigate()
return (
<aside
className={`fixed inset-y-0 z-10 flex w-[260px] flex-none flex-col overflow-y-auto bg-forest-950 px-[15px] pt-7 pb-[18px] text-forest-100 shadow-[8px_0_30px_#163c3740] transition-[left] duration-200 sm:static sm:min-h-screen sm:w-[230px] sm:shadow-none lg:w-[246px] ${open ? 'left-0' : '-left-[270px]'}`}
className={`fixed inset-y-0 z-10 flex w-[260px] flex-none flex-col overflow-y-auto bg-forest-950 px-[15px] pt-7 pb-[18px] text-forest-100 shadow-[8px_0_30px_#163c3740] transition-[left] duration-200 sm:static sm:min-h-screen sm:w-[230px] sm:shadow-none lg:w-[246px] print:hidden ${open ? 'left-0' : '-left-[270px]'}`}
>
<div className="flex items-center gap-2.5 px-3 pb-[31px] text-white">
<div className="grid size-[34px] flex-none place-items-center rounded-[9px] bg-coral text-white">
<ShieldCheck size={20} />
</div>
<div>
<strong className="block font-display text-[19px] leading-tight sm:text-[17px] lg:text-[19px] font-bold tracking-[-0.5px] whitespace-nowrap">Open SafetyLog</strong>
<strong className="block font-display text-[19px] leading-tight font-bold tracking-[-0.5px] whitespace-nowrap sm:text-[17px] lg:text-[19px]">Open SafetyLog</strong>
<span className="mt-0.5 block text-3xs text-forest-300">EHS · Incident intelligence</span>
</div>
<button type="button" className={`${iconButton} absolute top-2 right-2 sm:hidden`} onClick={onClose} aria-label="Close menu">
@@ -34,22 +39,19 @@ export function Sidebar({ open, onClose, openIncidentCount }: SidebarProps) {
</div>
<div className="mb-[27px] flex items-center gap-2.5 rounded-[7px] border border-[#316158] p-[11px]">
<div className="grid size-[29px] place-items-center rounded-full bg-[#f1c865] font-mono text-2xs font-bold text-[#143d37]">{workspace.initials}</div>
<div>
<div className="grid size-[29px] flex-none place-items-center rounded-full bg-[#f1c865] font-mono text-2xs font-bold text-[#143d37]">{initials(me.organization.name)}</div>
<div className="min-w-0">
<Eyebrow>Workspace</Eyebrow>
<strong className="block text-[13px] text-white">{workspace.name}</strong>
<strong className="block truncate text-[13px] text-white">{me.organization.name}</strong>
</div>
<ChevronDown size={16} className="ml-auto" />
</div>
<nav className={navSection}>
{primaryNav.map(({ label, to, icon: Icon }) => (
<NavLink key={to} to={to} end onClick={onClose} className={navLink}>
<NavLink key={to} to={to} end={to === '/'} onClick={onClose} className={navLink}>
<Icon size={18} />
<span>{label}</span>
{to === '/incidents' && openIncidentCount > 0 && (
<em className="ml-auto rounded-[10px] bg-coral px-[7px] py-0.5 font-mono text-2xs text-white not-italic">{openIncidentCount}</em>
)}
{!!badges[to] && <em className="ml-auto rounded-[10px] bg-coral px-[7px] py-0.5 font-mono text-2xs text-white not-italic">{badges[to]}</em>}
</NavLink>
))}
</nav>
@@ -60,10 +62,6 @@ export function Sidebar({ open, onClose, openIncidentCount }: SidebarProps) {
<Plus size={18} />
<span>Report an incident</span>
</NavLink>
<a href="#resources" className={navLink({ isActive: false })}>
<FileText size={18} />
<span>Resources</span>
</a>
</div>
<div className="mt-auto">
@@ -71,17 +69,25 @@ export function Sidebar({ open, onClose, openIncidentCount }: SidebarProps) {
<Info size={18} />
<span>About</span>
</NavLink>
<a href="#settings" className={navLink({ isActive: false })}>
<NavLink to="/settings" onClick={onClose} className={navLink}>
<Settings size={18} />
<span>Settings</span>
</a>
</NavLink>
<div className="mt-4 flex items-center gap-2.5 border-t border-forest-700 px-2 pt-5">
<div className="grid size-[31px] place-items-center rounded-full bg-[#5c8274] font-mono text-2xs text-white">{currentUser.initials}</div>
<div>
<strong className="block text-xs text-white">{currentUser.name}</strong>
<span className="mt-0.5 block text-3xs text-forest-300">{currentUser.role}</span>
<div className="grid size-[31px] flex-none place-items-center rounded-full bg-[#5c8274] font-mono text-2xs text-white">{initials(me.user.name)}</div>
<div className="min-w-0">
<strong className="block truncate text-xs text-white">{me.user.name}</strong>
<span className="mt-0.5 block text-3xs text-forest-300">{roleLabel[me.user.role]}</span>
</div>
<span className="ml-auto tracking-[2px] text-[#91aaa0]">•••</span>
<button
type="button"
className={`${iconButton} ml-auto text-[#91aaa0] hover:text-white`}
aria-label="Sign out"
title="Sign out"
onClick={() => logout.mutate(undefined, { onSuccess: () => navigate('/login') })}
>
<LogOut size={17} />
</button>
</div>
</div>
</aside>