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:
+100
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user