@@ -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 { ExpenseForm } from "./expense-form"
|
||||
|
||||
type ExpenseActionsProps = {
|
||||
expenseId: string
|
||||
@@ -18,10 +22,7 @@ type ExpenseActionsProps = {
|
||||
export function ExpenseActions({ expenseId }: ExpenseActionsProps) {
|
||||
const api = useAuthApi()
|
||||
const router = useRouter()
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(`/purchase/expense/${expenseId}/edit`)
|
||||
}
|
||||
const editDialog = useFormDialog("expense-details-edit")
|
||||
|
||||
const handleDelete = async () => {
|
||||
await api.expenses.destroy(expenseId)
|
||||
@@ -29,22 +30,41 @@ export function ExpenseActions({ expenseId }: ExpenseActionsProps) {
|
||||
}
|
||||
|
||||
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(expenseId)}>
|
||||
<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 Expense</DialogTitle>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="max-h-[80vh] px-4">
|
||||
<ExpenseForm
|
||||
resourceId={editDialog.resourceId}
|
||||
onSuccess={() => {
|
||||
editDialog.close()
|
||||
router.refresh()
|
||||
}}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -314,6 +314,7 @@ export function ExpenseForm({ resourceId, initialData, onSuccess }: ExpenseFormP
|
||||
name="job_card"
|
||||
label="Job Card"
|
||||
placeholder="Select job card"
|
||||
required
|
||||
queryKey={[JOB_CARD_ROUTES.INDEX]}
|
||||
listFn={() => api.jobCards.list()}
|
||||
mapOption={(item: any) => ({
|
||||
|
||||
@@ -1,9 +1,29 @@
|
||||
import { z } from "zod"
|
||||
import { ExpenseStatus, InvoiceDiscount } from "@garage/api"
|
||||
|
||||
const relationFieldSchema = z
|
||||
.object({ value: z.string(), label: z.string() })
|
||||
.nullable()
|
||||
|
||||
const requiredRelationFieldSchema = relationFieldSchema.superRefine((value, ctx) => {
|
||||
if (!value?.value) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "This field is required",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
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 labelItemSchema = z.object({
|
||||
id: z.number(),
|
||||
title: z.string(),
|
||||
@@ -22,21 +42,22 @@ const expenseLineItemSchema = z.object({
|
||||
|
||||
const expenseFormSchema = z.object({
|
||||
// ── Relations ──
|
||||
job_card: relationFieldSchema,
|
||||
job_card: requiredRelationFieldSchema,
|
||||
category: relationFieldSchema,
|
||||
vendor: relationFieldSchema,
|
||||
department: relationFieldSchema,
|
||||
tax: relationFieldSchema,
|
||||
|
||||
// ── Basic info ──
|
||||
title: z.string().min(1, "Title is required"),
|
||||
invoice_number: z.string().optional(),
|
||||
expense_date: z.string().optional(),
|
||||
title: z.string().trim().min(1, "Title is required").max(255, "Title must be at most 255 characters"),
|
||||
invoice_number: optionalStringMax255Schema,
|
||||
expense_date: optionalDateSchema,
|
||||
notes: z.string().optional(),
|
||||
status: z.string().optional(),
|
||||
status: z.enum(ExpenseStatus).optional(),
|
||||
paid_through: z.coerce.number().int().optional(),
|
||||
|
||||
// ── Discount / Tax ──
|
||||
discount: z.string().optional(),
|
||||
discount: z.enum(InvoiceDiscount).optional(),
|
||||
discount_amount: z.coerce.number().min(0).optional(),
|
||||
|
||||
labels: z.array(labelItemSchema).optional(),
|
||||
|
||||
Reference in New Issue
Block a user