update forms

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-05-07 21:02:15 +03:00
co-authored by Copilot
parent dd32658500
commit 38565298fc
32 changed files with 797 additions and 236 deletions
+41 -21
View File
@@ -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,
@@ -10,6 +12,8 @@ import {
DropdownMenuTrigger,
} from "@/shared/components/ui/dropdown-menu"
import { Ellipsis, Pencil, Trash2 } from "lucide-react"
import { useFormDialog } from "@/shared/components/form-dialog"
import { BillForm } from "./bill-form"
type BillActionsProps = {
billId: string
@@ -18,10 +22,7 @@ type BillActionsProps = {
export function BillActions({ billId }: BillActionsProps) {
const api = useAuthApi()
const router = useRouter()
const handleEdit = () => {
router.push(`/purchase/bill/${billId}/edit`)
}
const editDialog = useFormDialog("bill-details-edit")
const handleDelete = async () => {
await api.bills.destroy(billId)
@@ -29,22 +30,41 @@ export function BillActions({ billId }: BillActionsProps) {
}
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(billId)}>
<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 Bill</DialogTitle>
</DialogHeader>
<ScrollArea className="max-h-[80vh] px-4">
<BillForm
resourceId={editDialog.resourceId}
onSuccess={() => {
editDialog.close()
router.refresh()
}}
/>
</ScrollArea>
</DialogContent>
</Dialog>
</>
)
}
+4 -1
View File
@@ -61,6 +61,7 @@ const DEFAULT_VALUES: BillFormValues = {
discount: "no",
discount_amount: undefined,
notes: "",
label_ids: [],
part_items: [],
service_items: [],
expense_items: [],
@@ -102,6 +103,7 @@ function mapToFormValues(data: unknown): BillFormValues {
discount: d.discount_type || "no",
discount_amount: d.discount_amount != null ? Number(d.discount_amount) : undefined,
notes: d.notes || "",
label_ids: Array.isArray(d.label_ids) ? d.label_ids.map((id: any) => Number(id)).filter((id: number) => Number.isFinite(id)) : [],
part_items: (d.parts ?? []).map((p: any) => ({
part_id: p.part_id ?? p.id,
title: p.part?.name ?? p.part_name ?? p.title ?? "",
@@ -146,6 +148,7 @@ function mapFormToPayload(values: BillFormValues) {
discount_type: values.discount || undefined,
discount_amount: values.discount === "transaction_level" ? (values.discount_amount ?? 0) : undefined,
notes: values.notes || undefined,
label_ids: values.label_ids?.length ? values.label_ids : undefined,
part_items: (values.part_items ?? []).map((item) => ({
part_id: item.part_id,
quantity: item.quantity,
@@ -276,7 +279,7 @@ export function BillForm({ resourceId, initialData, onSuccess }: BillFormProps)
<CardContent>
<FieldGroup>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-1">
<RhfDateField name="bill_date" label="Bill Date" />
<RhfDateField name="bill_date" label="Bill Date" required />
<RhfDateField name="bill_due_date" label="Due Date" />
</div>
+23 -6
View File
@@ -1,9 +1,25 @@
import { z } from "zod"
import { BillStatus, DiscountType } from "@garage/api"
const relationFieldSchema = z
.object({ value: z.string(), label: z.string() })
.nullable()
const optionalDateSchema = z.preprocess(
(value) => (value === "" || value == null ? undefined : value),
z.string().date().optional(),
)
const optionalStringMax255Schema = z.preprocess(
(value) => (value === "" || value == null ? undefined : value),
z.string().max(255, "Must be at most 255 characters").optional(),
)
const requiredDateSchema = z
.string()
.min(1, "Bill date is required")
.date("Enter a valid date")
const billPartItemSchema = z.object({
part_id: z.number(),
title: z.string(),
@@ -33,7 +49,7 @@ const billExpenseItemSchema = z.object({
const billFormSchema = z.object({
// ── Required ──
title: z.string().min(1, "Title is required"),
title: z.string().trim().min(1, "Title is required").max(255, "Title must be at most 255 characters"),
// ── Relations ──
vendor: relationFieldSchema,
@@ -45,13 +61,14 @@ const billFormSchema = z.object({
tax: relationFieldSchema,
// ── Optional fields ──
bill_number: z.string().optional(),
bill_date: z.string().optional(),
bill_due_date: z.string().optional(),
status: z.string().optional(),
discount: z.string().optional(),
bill_number: optionalStringMax255Schema,
bill_date: requiredDateSchema,
bill_due_date: optionalDateSchema,
status: z.enum(BillStatus).optional(),
discount: z.enum(DiscountType).optional(),
discount_amount: z.coerce.number().min(0).optional(),
notes: z.string().optional(),
label_ids: z.array(z.number()).optional(),
// ── Line items ──
part_items: z.array(billPartItemSchema).optional(),