293 lines
13 KiB
TypeScript
293 lines
13 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
|
import { useAuthApi } from "@/shared/useApi"
|
|
import { EMPLOYEE_ROUTES } from "@garage/api"
|
|
import { Button } from "@/shared/components/ui/button"
|
|
import { Checkbox } from "@/shared/components/ui/checkbox"
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
|
import { Badge } from "@/shared/components/ui/badge"
|
|
import { toast } from "sonner"
|
|
import { Save, ShieldCheck } from "lucide-react"
|
|
import DashboardPage from "@/base/components/layout/dashboard/dashboard-page"
|
|
|
|
// ── Permission resource groups ──
|
|
|
|
const PERMISSION_GROUPS: { label: string; key: string }[] = [
|
|
{ label: "Appointments", key: "appointments" },
|
|
{ label: "Bills", key: "bills" },
|
|
{ label: "Check Point Labels", key: "check_point_labels" },
|
|
{ label: "Credit Notes", key: "credit_notes" },
|
|
{ label: "Customers", key: "customers" },
|
|
{ label: "Departments", key: "departments" },
|
|
{ label: "Document Types", key: "document_types" },
|
|
{ label: "Employees", key: "employees" },
|
|
{ label: "Estimates", key: "estimates" },
|
|
{ label: "Expense Items", key: "expense_items" },
|
|
{ label: "Expenses", key: "expenses" },
|
|
{ label: "Holidays", key: "holidays" },
|
|
{ label: "Inspection Categories", key: "inspection_categories" },
|
|
{ label: "Inspection Check Points", key: "inspection_check_points" },
|
|
{ label: "Inspections", key: "inspections" },
|
|
{ label: "Insurance Types", key: "insurance_types" },
|
|
{ label: "Inventory Adjustments", key: "inventory_adjustments" },
|
|
{ label: "Inventory Categories", key: "inventory_categories" },
|
|
{ label: "Invoice Documents", key: "invoice_documents" },
|
|
{ label: "Invoice Labels", key: "invoice_labels" },
|
|
{ label: "Invoice Notes", key: "invoice_notes" },
|
|
{ label: "Invoice Sequences", key: "invoice_sequences" },
|
|
{ label: "Invoices", key: "invoices" },
|
|
{ label: "Job Cards", key: "job_cards" },
|
|
{ label: "Labels", key: "labels" },
|
|
{ label: "Labor Rates", key: "labor_rates" },
|
|
{ label: "Make & Models", key: "make_and_models" },
|
|
{ label: "Parts", key: "parts" },
|
|
{ label: "Payment Mades", key: "payment_mades" },
|
|
{ label: "Payment Modes", key: "payment_modes" },
|
|
{ label: "Payment Received", key: "payment_recieveds" },
|
|
{ label: "Payment Terms", key: "payment_terms" },
|
|
{ label: "Purchase Orders", key: "purchase_orders" },
|
|
{ label: "Quick Notes", key: "quick_notes" },
|
|
{ label: "Quick Remarks", key: "quick_remarks" },
|
|
{ label: "Reasons", key: "reasons" },
|
|
{ label: "Referral Sources", key: "referral_sources" },
|
|
{ label: "Service Group Includes", key: "service_group_includes" },
|
|
{ label: "Service Group Parts", key: "service_group_parts" },
|
|
{ label: "Service Group Pricings", key: "service_group_pricings" },
|
|
{ label: "Service Group Services", key: "service_group_services" },
|
|
{ label: "Service Groups", key: "service_groups" },
|
|
{ label: "Services", key: "services" },
|
|
{ label: "Settings", key: "settings" },
|
|
{ label: "Shop Calendars", key: "shop_calenders" },
|
|
{ label: "Shop Timings", key: "shop_timings" },
|
|
{ label: "Shop Types", key: "shop_types" },
|
|
{ label: "Task Sections", key: "task_sections" },
|
|
{ label: "Task Types", key: "task_types" },
|
|
{ label: "Tasks", key: "tasks" },
|
|
{ label: "Taxes", key: "taxes" },
|
|
{ label: "Time Sheets", key: "time_sheets" },
|
|
{ label: "Unit Types", key: "unit_types" },
|
|
{ label: "Vehicle Body Types", key: "vehicle_body_types" },
|
|
{ label: "Vehicle Colors", key: "vehicle_colors" },
|
|
{ label: "Vehicle Documents", key: "vehicle_documents" },
|
|
{ label: "Vehicle Fuel Types", key: "vehicle_fuel_types" },
|
|
{ label: "Vehicle Mile & Kms", key: "vehicle_mile_and_kms" },
|
|
{ label: "Vehicle Transmissions", key: "vehicle_transmissions" },
|
|
{ label: "Vehicles", key: "vehicles" },
|
|
{ label: "Vendor Credits", key: "vendor_credits" },
|
|
{ label: "Vendors", key: "vendors" },
|
|
]
|
|
|
|
const ACTIONS = ["view", "create", "update", "delete"] as const
|
|
|
|
type PermissionAction = typeof ACTIONS[number]
|
|
|
|
type PermissionsState = Record<string, boolean>
|
|
|
|
function buildPermissionsPayload(state: PermissionsState) {
|
|
return state
|
|
}
|
|
|
|
function extractPermissions(data: Record<string, unknown> | null | undefined): PermissionsState {
|
|
if (!data) return {}
|
|
const result: PermissionsState = {}
|
|
for (const group of PERMISSION_GROUPS) {
|
|
for (const action of ACTIONS) {
|
|
const key = `can_${action}_${group.key}`
|
|
result[key] = Boolean(data[key])
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
type EmployeePermissionsFormProps = {
|
|
employeeId: string
|
|
}
|
|
|
|
export function EmployeePermissionsForm({ employeeId }: EmployeePermissionsFormProps) {
|
|
const api = useAuthApi()
|
|
const queryClient = useQueryClient()
|
|
|
|
const queryKey = [EMPLOYEE_ROUTES.BY_ID, employeeId]
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey,
|
|
queryFn: () => api.employees.getById(employeeId),
|
|
})
|
|
|
|
const employeeData = (data as any)?.data as Record<string, unknown> | undefined
|
|
|
|
const [permissions, setPermissions] = useState<PermissionsState | null>(null)
|
|
|
|
const currentPermissions = permissions ?? extractPermissions(employeeData)
|
|
|
|
const { mutate, isPending } = useMutation({
|
|
mutationFn: (payload: PermissionsState) =>
|
|
api.employees.updatePermissions(employeeId, payload as never),
|
|
onSuccess: () => {
|
|
toast.success("Permissions updated successfully")
|
|
queryClient.invalidateQueries({ queryKey })
|
|
},
|
|
onError: () => {
|
|
toast.error("Failed to update permissions")
|
|
},
|
|
})
|
|
|
|
const handleToggle = (key: string, value: boolean) => {
|
|
setPermissions((prev) => ({
|
|
...(prev ?? extractPermissions(employeeData)),
|
|
[key]: value,
|
|
}))
|
|
}
|
|
|
|
const handleRowToggle = (groupKey: string, checked: boolean) => {
|
|
const updates: PermissionsState = {}
|
|
for (const action of ACTIONS) {
|
|
updates[`can_${action}_${groupKey}`] = checked
|
|
}
|
|
setPermissions((prev) => ({
|
|
...(prev ?? extractPermissions(employeeData)),
|
|
...updates,
|
|
}))
|
|
}
|
|
|
|
const handleColumnToggle = (action: PermissionAction, checked: boolean) => {
|
|
const updates: PermissionsState = {}
|
|
for (const group of PERMISSION_GROUPS) {
|
|
updates[`can_${action}_${group.key}`] = checked
|
|
}
|
|
setPermissions((prev) => ({
|
|
...(prev ?? extractPermissions(employeeData)),
|
|
...updates,
|
|
}))
|
|
}
|
|
|
|
const isRowChecked = (groupKey: string) =>
|
|
ACTIONS.every((action) => currentPermissions[`can_${action}_${groupKey}`])
|
|
|
|
const isRowIndeterminate = (groupKey: string) => {
|
|
const values = ACTIONS.map((action) => currentPermissions[`can_${action}_${groupKey}`])
|
|
return values.some(Boolean) && !values.every(Boolean)
|
|
}
|
|
|
|
const isColumnChecked = (action: PermissionAction) =>
|
|
PERMISSION_GROUPS.every((g) => currentPermissions[`can_${action}_${g.key}`])
|
|
|
|
const isColumnIndeterminate = (action: PermissionAction) => {
|
|
const values = PERMISSION_GROUPS.map((g) => currentPermissions[`can_${action}_${g.key}`])
|
|
return values.some(Boolean) && !values.every(Boolean)
|
|
}
|
|
|
|
const isAllChecked = PERMISSION_GROUPS.every((g) =>
|
|
ACTIONS.every((action) => currentPermissions[`can_${action}_${g.key}`])
|
|
)
|
|
|
|
const isAllIndeterminate = (() => {
|
|
const values = PERMISSION_GROUPS.flatMap((g) =>
|
|
ACTIONS.map((action) => currentPermissions[`can_${action}_${g.key}`])
|
|
)
|
|
return values.some(Boolean) && !values.every(Boolean)
|
|
})()
|
|
|
|
const handleToggleAll = (checked: boolean) => {
|
|
const updates: PermissionsState = {}
|
|
for (const group of PERMISSION_GROUPS) {
|
|
for (const action of ACTIONS) {
|
|
updates[`can_${action}_${group.key}`] = checked
|
|
}
|
|
}
|
|
setPermissions(updates)
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<DashboardPage header={null}>
|
|
<div className="text-muted-foreground text-sm">Loading permissions...</div>
|
|
</DashboardPage>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<DashboardPage header={null}>
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between gap-4">
|
|
<CardTitle className="flex items-center gap-2">
|
|
<ShieldCheck className="size-4" />
|
|
Permissions
|
|
</CardTitle>
|
|
<Button
|
|
size="sm"
|
|
onClick={() => mutate(buildPermissionsPayload(currentPermissions))}
|
|
disabled={isPending}
|
|
>
|
|
<Save className="size-4" />
|
|
Save Permissions
|
|
</Button>
|
|
</CardHeader>
|
|
<CardContent className="overflow-auto">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b">
|
|
<th className="py-2 pr-4 text-left font-medium">
|
|
<div className="flex items-center gap-2">
|
|
<Checkbox
|
|
className="border-primary"
|
|
checked={isAllChecked || (isAllIndeterminate ? "indeterminate" : false)}
|
|
onCheckedChange={(v) => handleToggleAll(Boolean(v))}
|
|
/>
|
|
Resource
|
|
</div>
|
|
</th>
|
|
{ACTIONS.map((action) => (
|
|
<th key={action} className="py-2 px-4 text-center font-medium capitalize">
|
|
<div className="flex flex-col items-center gap-1">
|
|
<Checkbox
|
|
className="border border-primary"
|
|
checked={isColumnChecked(action) || (isColumnIndeterminate(action) ? "indeterminate" : false)}
|
|
onCheckedChange={(v) => handleColumnToggle(action, Boolean(v))}
|
|
/>
|
|
<Badge variant="outline" className="capitalize text-xs">
|
|
{action}
|
|
</Badge>
|
|
</div>
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{PERMISSION_GROUPS.map((group) => (
|
|
<tr key={group.key} className="border-b last:border-0 hover:bg-muted/30 transition-colors">
|
|
<td className="py-2 pr-4">
|
|
<div className="flex items-center gap-2">
|
|
<Checkbox
|
|
className="border border-primary"
|
|
checked={isRowChecked(group.key) || (isRowIndeterminate(group.key) ? "indeterminate" : false)}
|
|
onCheckedChange={(v) => handleRowToggle(group.key, Boolean(v))}
|
|
/>
|
|
<span className="text-sm">{group.label}</span>
|
|
</div>
|
|
</td>
|
|
{ACTIONS.map((action) => {
|
|
const key = `can_${action}_${group.key}`
|
|
return (
|
|
<td key={action} className="py-2 px-4 text-center ">
|
|
<Checkbox
|
|
className="mx-auto border border-primary"
|
|
|
|
checked={Boolean(currentPermissions[key])}
|
|
onCheckedChange={(v) => handleToggle(key, Boolean(v))}
|
|
/>
|
|
</td>
|
|
)
|
|
})}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</CardContent>
|
|
</Card>
|
|
</DashboardPage>
|
|
)
|
|
}
|