feat: implement permissions and actions management
- 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:
1 parent
708da6b9f7
commit
3f6dff5f24
94 files changed
+7213
-643
No files matched your search
@@ -0,0 +1,94 @@
|
||||
import { Link, useOutletContext, useSearchParams } from 'react-router-dom'
|
||||
import { hasRole } from '../../shared/enums.ts'
|
||||
import { OverdueBadge } from '../components/Badges'
|
||||
import { ActionStatusControl } from '../components/incident/ActionsSection'
|
||||
import { Page } from '../components/Page'
|
||||
import { PageIntro } from '../components/PageIntro'
|
||||
import { Empty, ErrorMessage, Loading } from '../components/QueryState'
|
||||
import { panel } from '../components/styles'
|
||||
import { formatDay } from '../lib/dates'
|
||||
import { useActions, useUpdateAction, type Me } from '../lib/queries'
|
||||
|
||||
const filters = [
|
||||
{ value: 'open', label: 'Open' },
|
||||
{ value: 'overdue', label: 'Overdue' },
|
||||
{ value: 'done', label: 'Done' },
|
||||
{ value: 'all', label: 'All' },
|
||||
] as const
|
||||
|
||||
const tab = (active: boolean) => `rounded-[5px] px-3 py-1.5 text-2xs font-semibold ${active ? 'bg-forest-950 text-white' : 'text-muted hover:bg-white hover:text-ink'}`
|
||||
|
||||
export function Actions() {
|
||||
const me = useOutletContext<Me>()
|
||||
const [params, setParams] = useSearchParams()
|
||||
const canSeeAll = hasRole(me.user.role, 'supervisor')
|
||||
const scope = canSeeAll && params.get('scope') === 'all' ? 'all' : 'mine'
|
||||
const filter = filters.find((f) => f.value === params.get('filter'))?.value ?? 'open'
|
||||
const actions = useActions(scope, filter)
|
||||
const update = useUpdateAction()
|
||||
|
||||
const set = (key: string, value: string) => {
|
||||
const next = new URLSearchParams(params)
|
||||
next.set(key, value)
|
||||
setParams(next, { replace: true })
|
||||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageIntro
|
||||
eyebrow="Corrective actions"
|
||||
title={scope === 'mine' ? 'My actions' : 'All actions'}
|
||||
copy={scope === 'mine' ? 'Corrective actions assigned to you. Mark them done when the fix is in place.' : 'Every corrective action across the organization.'}
|
||||
/>
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex gap-1 rounded-md bg-line-soft p-1" role="tablist" aria-label="Filter actions">
|
||||
{filters.map((f) => (
|
||||
<button key={f.value} type="button" role="tab" aria-selected={filter === f.value} className={tab(filter === f.value)} onClick={() => set('filter', f.value)}>
|
||||
{f.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{canSeeAll && (
|
||||
<div className="flex gap-1 rounded-md bg-line-soft p-1" role="tablist" aria-label="Whose actions">
|
||||
<button type="button" role="tab" aria-selected={scope === 'mine'} className={tab(scope === 'mine')} onClick={() => set('scope', 'mine')}>
|
||||
Assigned to me
|
||||
</button>
|
||||
<button type="button" role="tab" aria-selected={scope === 'all'} className={tab(scope === 'all')} onClick={() => set('scope', 'all')}>
|
||||
Everyone
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<section className={panel}>
|
||||
<ErrorMessage error={actions.error ?? update.error} className="m-4" />
|
||||
{actions.isPending ? (
|
||||
<Loading />
|
||||
) : !actions.data?.length ? (
|
||||
<Empty>{filter === 'open' ? 'Nothing open. Nice work.' : 'No actions here.'}</Empty>
|
||||
) : (
|
||||
<ul className="divide-y divide-line-soft">
|
||||
{actions.data.map((action) => (
|
||||
<li key={action.id} className="flex flex-wrap items-center gap-x-4 gap-y-2 px-5 py-4 sm:px-6">
|
||||
<div className="min-w-[240px] flex-1">
|
||||
<p className="text-xs font-semibold text-ink">{action.title}</p>
|
||||
<p className="mt-1 text-3xs text-muted">
|
||||
<Link to={`/incidents/${action.incident.number}`} className="font-mono text-coral hover:underline">
|
||||
{action.incident.reference}
|
||||
</Link>{' '}
|
||||
{action.incident.title}
|
||||
{scope === 'all' && ` · ${action.assignee?.name ?? 'Unassigned'}`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-2xs text-body">
|
||||
{action.overdue && <OverdueBadge />}
|
||||
<span className="w-[100px] text-right">{action.dueDate ? `Due ${formatDay(action.dueDate)}` : 'No due date'}</span>
|
||||
<ActionStatusControl action={action} disabled={update.isPending} onChange={(status) => update.mutate({ id: action.id, status })} />
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user