garage-erp/apps/dashboard/modules/employees/employee-permissions-form.tsx
2026-04-06 02:32:47 +03:00

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>
)
}