@@ -3,6 +3,8 @@
|
||||
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,
|
||||
@@ -12,6 +14,8 @@ import {
|
||||
import { Ellipsis, Pencil, Trash2 } from "lucide-react"
|
||||
import { confirm } from "@/shared/components/confirm-dialog"
|
||||
import { toast } from "sonner"
|
||||
import { useFormDialog } from "@/shared/components/form-dialog"
|
||||
import { CustomerForm } from "./customer-form"
|
||||
|
||||
type CustomerActionsProps = {
|
||||
customerId: string
|
||||
@@ -20,10 +24,7 @@ type CustomerActionsProps = {
|
||||
export function CustomerActions({ customerId }: CustomerActionsProps) {
|
||||
const api = useAuthApi()
|
||||
const router = useRouter()
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(`/sales/customers/${customerId}/edit`)
|
||||
}
|
||||
const editDialog = useFormDialog("customer-details-edit")
|
||||
|
||||
const handleDelete = async () => {
|
||||
const confirmed = await confirm({
|
||||
@@ -44,22 +45,41 @@ export function CustomerActions({ customerId }: CustomerActionsProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Ellipsis className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={handleEdit}>
|
||||
<Pencil className="size-4" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem variant="destructive" onClick={handleDelete}>
|
||||
<Trash2 className="size-4" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Ellipsis className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => editDialog.open(customerId)}>
|
||||
<Pencil className="size-4" />
|
||||
Edit
|
||||
</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-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-bold">Edit Customer</DialogTitle>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="max-h-[80vh] px-4">
|
||||
<CustomerForm
|
||||
resourceId={editDialog.resourceId}
|
||||
onSuccess={() => {
|
||||
editDialog.close()
|
||||
router.refresh()
|
||||
}}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,13 +51,16 @@ const CUSTOMER_DEFAULT_VALUES: CustomerFormValues = {
|
||||
payment_terms: null,
|
||||
country: null,
|
||||
state: null,
|
||||
salutation: "",
|
||||
salutation: "Mr.",
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
company_name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
alternate_phone: "",
|
||||
opening_balance: undefined,
|
||||
credit_limit: undefined,
|
||||
website: "",
|
||||
address_line_1: "",
|
||||
address_line_2: "",
|
||||
city: "",
|
||||
@@ -82,6 +85,9 @@ function mapCustomerToFormValues(data: unknown): CustomerFormValues {
|
||||
email: c.email || "",
|
||||
phone: c.phone || "",
|
||||
alternate_phone: c.alternate_phone || "",
|
||||
opening_balance: c.opening_balance ?? undefined,
|
||||
credit_limit: c.credit_limit ?? undefined,
|
||||
website: c.website || "",
|
||||
address_line_1: c.address_line_1 || "",
|
||||
address_line_2: c.address_line_2 || "",
|
||||
city: c.city || "",
|
||||
@@ -96,13 +102,16 @@ function mapFormToPayload(values: CustomerFormValues) {
|
||||
payment_terms_id: toId(values.payment_terms),
|
||||
country_id: toId(values.country),
|
||||
state_id: toId(values.state),
|
||||
salutation: values.salutation || undefined,
|
||||
salutation: values.salutation,
|
||||
first_name: values.first_name,
|
||||
last_name: values.last_name,
|
||||
company_name: values.company_name || undefined,
|
||||
email: values.email || undefined,
|
||||
email: values.email,
|
||||
phone: values.phone || undefined,
|
||||
alternate_phone: values.alternate_phone || undefined,
|
||||
opening_balance: values.opening_balance,
|
||||
credit_limit: values.credit_limit,
|
||||
website: values.website || undefined,
|
||||
address_line_1: values.address_line_1 || undefined,
|
||||
address_line_2: values.address_line_2 || undefined,
|
||||
city: values.city || undefined,
|
||||
@@ -169,6 +178,7 @@ export function CustomerForm({ resourceId, initialData, onSuccess }: CustomerFor
|
||||
{/* Basic Info */}
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<RhfSelectField
|
||||
required
|
||||
name="salutation"
|
||||
label="Salutation"
|
||||
placeholder="Select salutation"
|
||||
@@ -201,12 +211,19 @@ export function CustomerForm({ resourceId, initialData, onSuccess }: CustomerFor
|
||||
|
||||
{/* Contact */}
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<RhfTextField name="email" label="Email" placeholder="john@example.com" type="email" />
|
||||
<RhfTextField name="email" label="Email" placeholder="john@example.com" type="email" required />
|
||||
<RhfTextField name="phone" label="Phone" placeholder="0501234567" type="tel" />
|
||||
</div>
|
||||
|
||||
<RhfTextField name="alternate_phone" label="Alternate Phone" placeholder="0551234567" type="tel" />
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<RhfTextField name="opening_balance" label="Opening Balance" placeholder="0" type="number" />
|
||||
<RhfTextField name="credit_limit" label="Credit Limit" placeholder="0" type="number" />
|
||||
</div>
|
||||
|
||||
<RhfTextField name="website" label="Website" placeholder="https://example.com" type="url" />
|
||||
|
||||
{/* Relations */}
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<RhfAsyncSelectField
|
||||
|
||||
@@ -10,6 +10,30 @@ const relationFieldSchema = z
|
||||
|
||||
type RelationField = z.infer<typeof relationFieldSchema>
|
||||
|
||||
const optionalTrimmedString = (max: number) =>
|
||||
z.preprocess(
|
||||
(value) => {
|
||||
if (typeof value !== "string") {
|
||||
return value
|
||||
}
|
||||
|
||||
const trimmed = value.trim()
|
||||
return trimmed === "" ? undefined : trimmed
|
||||
},
|
||||
z.string().max(max).optional(),
|
||||
)
|
||||
|
||||
const optionalInteger = z.preprocess(
|
||||
(value) => {
|
||||
if (value === "" || value == null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return Number(value)
|
||||
},
|
||||
z.number().int().optional(),
|
||||
)
|
||||
|
||||
const customerFormSchema = z.object({
|
||||
// ── Relations (stored as objects, mapped to IDs on submit) ──
|
||||
customer_type: relationFieldSchema.refine((val) => !!val?.value, "Customer type is required"),
|
||||
@@ -19,23 +43,32 @@ const customerFormSchema = z.object({
|
||||
state: relationFieldSchema,
|
||||
|
||||
// ── Basic info ──
|
||||
salutation: z.string().optional(),
|
||||
first_name: z.string().min(1, "First name is required"),
|
||||
last_name: z.string().min(1, "Last name is required"),
|
||||
company_name: z.string().optional(),
|
||||
salutation: z.string().trim().min(1, "Salutation is required").max(50, "Salutation cannot exceed 50 characters"),
|
||||
first_name: z.string().trim().min(1, "First name is required").max(50, "First name cannot exceed 50 characters"),
|
||||
last_name: z.string().trim().min(1, "Last name is required").max(50, "Last name cannot exceed 50 characters"),
|
||||
company_name: optionalTrimmedString(50),
|
||||
|
||||
// ── Contact ──
|
||||
email: z
|
||||
.union([z.string().email("Enter a valid email address"), z.literal("")])
|
||||
.optional(),
|
||||
phone: z.string().optional(),
|
||||
alternate_phone: z.string().optional(),
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Email is required")
|
||||
.email("Enter a valid email address")
|
||||
.max(100, "Email cannot exceed 100 characters"),
|
||||
phone: optionalTrimmedString(20),
|
||||
alternate_phone: optionalTrimmedString(20),
|
||||
opening_balance: optionalInteger,
|
||||
credit_limit: optionalInteger,
|
||||
website: optionalTrimmedString(255).refine(
|
||||
(value) => !value || z.string().url().safeParse(value).success,
|
||||
"Enter a valid website URL",
|
||||
),
|
||||
|
||||
// ── Address ──
|
||||
address_line_1: z.string().optional(),
|
||||
address_line_2: z.string().optional(),
|
||||
city: z.string().optional(),
|
||||
zip_code: z.string().optional(),
|
||||
address_line_1: optionalTrimmedString(255),
|
||||
address_line_2: optionalTrimmedString(255),
|
||||
city: optionalTrimmedString(100),
|
||||
zip_code: optionalTrimmedString(20),
|
||||
})
|
||||
|
||||
type CustomerFormValues = z.infer<typeof customerFormSchema>
|
||||
|
||||
Reference in New Issue
Block a user