feat: add template checkpoint edit dialog and vendor management components

- Implemented TemplateCheckpointEditDialog for creating and editing inspection checkpoints.
- Added VendorActions component for managing vendor actions including edit, activate/deactivate, and delete.
- Created VendorContext for managing vendor state across components.
- Developed VendorGeneralInfo component to display detailed vendor information.
- Introduced AedSymbol and Money components for consistent currency representation.
- Added PromptDialog for user input prompts throughout the application.
- Implemented RelationLink component for unified related-data display in CRUD tables.
- Created InspectionTemplatesClient for API interactions related to inspection templates.
This commit is contained in:
2026-05-18 12:08:42 +04:00
parent 54d11f01b4
commit 4bfd8c84a9
111 changed files with 5002 additions and 1047 deletions
+100
View File
@@ -0,0 +1,100 @@
"use client"
import { useAuthApi } from "@/shared/useApi"
import { useRouter } from "next/navigation"
import { Button } from "@/shared/components/ui/button"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/shared/components/ui/dialog"
import { ScrollArea } from "@/shared/components/ui/scroll-area"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/shared/components/ui/dropdown-menu"
import { Ellipsis, Pencil, Power, Trash2 } from "lucide-react"
import { confirm } from "@/shared/components/confirm-dialog"
import { toast } from "sonner"
import { useFormDialog } from "@/shared/components/form-dialog"
import { VendorForm } from "./vendor-form"
type VendorActionsProps = {
vendorId: string
isActive?: boolean
}
export function VendorActions({ vendorId, isActive }: VendorActionsProps) {
const api = useAuthApi()
const router = useRouter()
const editDialog = useFormDialog("vendor-details-edit")
const handleDelete = async () => {
const confirmed = await confirm({
title: "Delete Vendor",
description: "Are you sure you want to delete this vendor? This action cannot be undone.",
confirmLabel: "Delete",
variant: "destructive",
})
if (!confirmed) return
try {
await api.vendors.destroy(vendorId)
toast.success("Vendor deleted successfully.")
router.push("/purchase/vendor")
} catch {
toast.error("Failed to delete vendor.")
}
}
const handleToggleStatus = async () => {
try {
await api.vendors.toggleStatus({ id: Number(vendorId) } as any)
toast.success(isActive ? "Vendor deactivated." : "Vendor activated.")
router.refresh()
} catch {
toast.error("Failed to update vendor status.")
}
}
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<Ellipsis className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => editDialog.open(vendorId)}>
<Pencil className="size-4" />
Edit
</DropdownMenuItem>
<DropdownMenuItem onClick={handleToggleStatus}>
<Power className="size-4" />
{isActive ? "Deactivate" : "Activate"}
</DropdownMenuItem>
<DropdownMenuItem variant="destructive" onClick={handleDelete}>
<Trash2 className="size-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Dialog open={editDialog.isOpen} onOpenChange={(v) => { if (!v) editDialog.close() }}>
<DialogContent className="min-w-xl lg:min-w-2xl">
<DialogHeader>
<DialogTitle className="text-2xl font-bold">Edit Vendor</DialogTitle>
</DialogHeader>
<ScrollArea className="max-h-[80vh] px-4">
<VendorForm
resourceId={editDialog.resourceId}
onSuccess={() => {
editDialog.close()
router.refresh()
}}
/>
</ScrollArea>
</DialogContent>
</Dialog>
</>
)
}
+28
View File
@@ -0,0 +1,28 @@
"use client"
import { createContext, useContext } from "react"
type VendorContextValue = {
id: string
label: string
}
const VendorContext = createContext<VendorContextValue | null>(null)
export function VendorProvider({
vendor,
children,
}: {
vendor: VendorContextValue
children: React.ReactNode
}) {
return (
<VendorContext.Provider value={vendor}>
{children}
</VendorContext.Provider>
)
}
export function useVendor() {
return useContext(VendorContext)
}
+135
View File
@@ -0,0 +1,135 @@
import {
Building2,
CreditCard,
DollarSign,
Globe,
Mail,
Phone,
User,
} from "lucide-react"
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/shared/components/ui/card"
import { Badge } from "@/shared/components/ui/badge"
import { Separator } from "@/shared/components/ui/separator"
import { Money } from "@/shared/components/money"
type VendorData = {
id?: number
salutation?: string | null
first_name?: string | null
last_name?: string | null
company_name?: string | null
email?: string | null
phone?: string | null
alternate_phone?: string | null
website?: string | null
opening_balance?: number | string | null
credit_limit?: number | string | null
status?: string | null
created_at?: string
updated_at?: string
}
type VendorGeneralInfoProps = {
vendor: VendorData
}
function InfoItem({
icon: Icon,
label,
value,
}: {
icon: React.ComponentType<{ className?: string }>
label: string
value?: React.ReactNode
}) {
const isEmpty = value == null || value === ""
return (
<div className="flex items-start gap-3">
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
<Icon className="size-4" />
</div>
<div className="flex flex-col gap-0.5">
<span className="text-xs text-muted-foreground">{label}</span>
<span className="text-sm font-medium">
{isEmpty ? <span className="text-muted-foreground">—</span> : value}
</span>
</div>
</div>
)
}
export function VendorGeneralInfo({ vendor }: VendorGeneralInfoProps) {
const fullName = [vendor.salutation, vendor.first_name, vendor.last_name]
.filter(Boolean)
.join(" ")
.trim()
return (
<div className="grid gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Building2 className="size-4" />
Vendor Information
</CardTitle>
</CardHeader>
<CardContent className="grid gap-4">
<div className="flex flex-wrap items-center gap-2">
<Badge variant="secondary">{vendor.company_name || fullName || "Unknown vendor"}</Badge>
{vendor.status && (
<Badge variant={vendor.status === "active" ? "default" : "outline"} className="capitalize">
{vendor.status}
</Badge>
)}
</div>
<Separator />
<div className="grid gap-4 sm:grid-cols-2">
<InfoItem icon={Building2} label="Company" value={vendor.company_name} />
<InfoItem icon={User} label="Contact Name" value={fullName} />
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Phone className="size-4" />
Contact Details
</CardTitle>
</CardHeader>
<CardContent className="grid gap-4 sm:grid-cols-2">
<InfoItem icon={Mail} label="Email" value={vendor.email} />
<InfoItem icon={Phone} label="Phone" value={vendor.phone} />
<InfoItem icon={Phone} label="Alternate Phone" value={vendor.alternate_phone} />
<InfoItem icon={Globe} label="Website" value={vendor.website} />
</CardContent>
</Card>
<Card className="md:col-span-2">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<DollarSign className="size-4" />
Financial Details
</CardTitle>
</CardHeader>
<CardContent className="grid gap-4 sm:grid-cols-2">
<InfoItem
icon={DollarSign}
label="Opening Balance"
value={vendor.opening_balance != null ? <Money value={vendor.opening_balance} /> : null}
/>
<InfoItem
icon={CreditCard}
label="Credit Limit"
value={vendor.credit_limit != null ? <Money value={vendor.credit_limit} /> : null}
/>
</CardContent>
</Card>
</div>
)
}