Refactor invoice data table and templates page for improved readability and functionality

- Cleaned up imports and formatted code for better readability in invoices-data-table.tsx.
- Enhanced invoice interface definitions for clarity.
- Improved toast messages for bulk delete and update actions.
- Refactored date formatting and status type retrieval for better readability.
- Simplified template management in templates page, extracting TemplateList component.
- Added registration toggle based on environment variable DISABLE_SIGNUPS.
- Updated navbar to conditionally render registration link based on allowRegistration prop.
- Enhanced error handling and validation in expenses and settings routers.
- Improved PDF export footer handling.
- Updated TRPC react integration for cleaner type imports.
This commit is contained in:
2026-04-29 22:49:07 -04:00
parent dbb739b060
commit ddc2b42672
20 changed files with 916 additions and 483 deletions
@@ -23,7 +23,15 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu";
import { Eye, Edit, Trash2, FileText, CheckCircle, Send, ChevronDown } from "lucide-react";
import {
Eye,
Edit,
Trash2,
FileText,
CheckCircle,
Send,
ChevronDown,
} from "lucide-react";
import { api } from "~/trpc/react";
import { toast } from "sonner";
import { getEffectiveInvoiceStatus } from "~/lib/invoice-status";
@@ -45,11 +53,28 @@ interface Invoice {
createdById: string;
createdAt: Date;
updatedAt: Date | null;
client?: { id: string; name: string; email: string | null; phone: string | null } | null;
business?: { id: string; name: string; email: string | null; phone: string | null } | null;
client?: {
id: string;
name: string;
email: string | null;
phone: string | null;
} | null;
business?: {
id: string;
name: string;
email: string | null;
phone: string | null;
} | null;
items?: Array<{
id: string; invoiceId: string; date: Date; description: string;
hours: number; rate: number; amount: number; position: number; createdAt: Date;
id: string;
invoiceId: string;
date: Date;
description: string;
hours: number;
rate: number;
amount: number;
position: number;
createdAt: Date;
}> | null;
}
@@ -58,10 +83,17 @@ interface InvoicesDataTableProps {
}
const getStatusType = (invoice: Invoice): StatusType =>
getEffectiveInvoiceStatus(invoice.status as StoredInvoiceStatus, invoice.dueDate) as StatusType;
getEffectiveInvoiceStatus(
invoice.status as StoredInvoiceStatus,
invoice.dueDate,
) as StatusType;
const formatDate = (date: Date) =>
new Intl.DateTimeFormat("en-US", { month: "short", day: "2-digit", year: "numeric" }).format(new Date(date));
new Intl.DateTimeFormat("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
}).format(new Date(date));
export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
const router = useRouter();
@@ -84,7 +116,9 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
const bulkDelete = api.invoices.bulkDelete.useMutation({
onSuccess: (data) => {
toast.success(`${data.deleted} invoice${data.deleted !== 1 ? "s" : ""} deleted`);
toast.success(
`${data.deleted} invoice${data.deleted !== 1 ? "s" : ""} deleted`,
);
void utils.invoices.getAll.invalidate();
setBulkDeleteDialogOpen(false);
setPendingBulkDelete([]);
@@ -94,7 +128,9 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
const bulkUpdateStatus = api.invoices.bulkUpdateStatus.useMutation({
onSuccess: (data) => {
toast.success(`${data.updated} invoice${data.updated !== 1 ? "s" : ""} updated`);
toast.success(
`${data.updated} invoice${data.updated !== 1 ? "s" : ""} updated`,
);
void utils.invoices.getAll.invalidate();
},
onError: (e) => toast.error(e.message ?? "Failed to update invoices"),
@@ -105,7 +141,10 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
id: "select",
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")}
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(v) => table.toggleAllPageRowsSelected(!!v)}
aria-label="Select all"
data-action-button="true"
@@ -124,7 +163,9 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
},
{
accessorKey: "client.name",
header: ({ column }) => <DataTableColumnHeader column={column} title="Client" />,
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Client" />
),
cell: ({ row }) => {
const invoice = row.original;
return (
@@ -133,10 +174,17 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
<FileText className="text-primary h-4 w-4" />
</div>
<div className="min-w-0 flex-1">
<p className="truncate font-medium">{invoice.client?.name ?? "—"}</p>
<p className="text-muted-foreground truncate text-xs sm:text-sm">{invoice.invoiceNumber}</p>
<p className="truncate font-medium">
{invoice.client?.name ?? "—"}
</p>
<p className="text-muted-foreground truncate text-xs sm:text-sm">
{invoice.invoiceNumber}
</p>
<div className="mt-1 flex items-center gap-2 sm:hidden">
<StatusBadge status={getStatusType(invoice)} className="text-xs" />
<StatusBadge
status={getStatusType(invoice)}
className="text-xs"
/>
<span className="text-foreground text-xs font-semibold">
{formatCurrency(invoice.totalAmount, invoice.currency)}
</span>
@@ -148,38 +196,59 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
},
{
accessorKey: "issueDate",
header: ({ column }) => <DataTableColumnHeader column={column} title="Date" />,
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Date" />
),
cell: ({ row }) => (
<div className="min-w-0">
<p className="truncate text-sm">{formatDate(row.getValue("issueDate") as Date)}</p>
<p className="text-muted-foreground truncate text-xs">Due {formatDate(new Date(row.original.dueDate))}</p>
<p className="truncate text-sm">
{formatDate(row.getValue("issueDate"))}
</p>
<p className="text-muted-foreground truncate text-xs">
Due {formatDate(new Date(row.original.dueDate))}
</p>
</div>
),
},
{
accessorKey: "status",
header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />,
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Status" />
),
cell: ({ row }) => (
<StatusBadge
status={getStatusType(row.original)}
className={getStatusType(row.original) === "sent" ? "status-pending" : ""}
className={
getStatusType(row.original) === "sent" ? "status-pending" : ""
}
/>
),
filterFn: (row, _id, value: string[]) => value.includes(getStatusType(row.original)),
meta: { headerClassName: "hidden sm:table-cell", cellClassName: "hidden sm:table-cell" },
filterFn: (row, _id, value: string[]) =>
value.includes(getStatusType(row.original)),
meta: {
headerClassName: "hidden sm:table-cell",
cellClassName: "hidden sm:table-cell",
},
},
{
accessorKey: "totalAmount",
header: ({ column }) => <DataTableColumnHeader column={column} title="Amount" />,
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Amount" />
),
cell: ({ row }) => (
<div className="text-right">
<p className="text-sm font-semibold">
{formatCurrency(row.getValue("totalAmount") as number, row.original.currency)}
{formatCurrency(row.getValue("totalAmount"), row.original.currency)}
</p>
<p className="text-muted-foreground text-xs">
{row.original.items?.length ?? 0} items
</p>
<p className="text-muted-foreground text-xs">{row.original.items?.length ?? 0} items</p>
</div>
),
meta: { headerClassName: "hidden sm:table-cell", cellClassName: "hidden sm:table-cell" },
meta: {
headerClassName: "hidden sm:table-cell",
cellClassName: "hidden sm:table-cell",
},
},
{
id: "actions",
@@ -188,19 +257,34 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
return (
<div className="flex items-center justify-end gap-1">
<Link href={`/dashboard/invoices/${invoice.id}`}>
<Button variant="ghost" size="sm" className="hover-scale h-8 w-8 p-0" data-action-button="true">
<Button
variant="ghost"
size="sm"
className="hover-scale h-8 w-8 p-0"
data-action-button="true"
>
<Eye className="h-3.5 w-3.5" />
</Button>
</Link>
<Link href={`/dashboard/invoices/${invoice.id}/edit`}>
<Button variant="ghost" size="sm" className="hover-scale h-8 w-8 p-0" data-action-button="true">
<Button
variant="ghost"
size="sm"
className="hover-scale h-8 w-8 p-0"
data-action-button="true"
>
<Edit className="h-3.5 w-3.5" />
</Button>
</Link>
<Button
variant="ghost" size="sm"
variant="ghost"
size="sm"
className="hover-scale text-destructive hover:text-destructive/80 h-8 w-8 p-0"
onClick={(e) => { e.stopPropagation(); setInvoiceToDelete(invoice); setDeleteDialogOpen(true); }}
onClick={(e) => {
e.stopPropagation();
setInvoiceToDelete(invoice);
setDeleteDialogOpen(true);
}}
data-action-button="true"
>
<Trash2 className="h-3.5 w-3.5" />
@@ -237,12 +321,18 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
searchKey="invoiceNumber"
searchPlaceholder="Search invoices..."
filterableColumns={filterableColumns}
onRowClick={(invoice) => router.push(`/dashboard/invoices/${invoice.id}`)}
onRowClick={(invoice) =>
router.push(`/dashboard/invoices/${invoice.id}`)
}
selectionActions={(selected, clear) => (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" disabled={bulkUpdateStatus.isPending}>
<Button
variant="outline"
size="sm"
disabled={bulkUpdateStatus.isPending}
>
<Send className="mr-1.5 h-3.5 w-3.5" />
Mark as
<ChevronDown className="ml-1.5 h-3.5 w-3.5" />
@@ -306,16 +396,24 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
<DialogDescription>
Are you sure you want to delete invoice{" "}
<strong>{invoiceToDelete?.invoiceNumber}</strong> for{" "}
<strong>{invoiceToDelete?.client?.name}</strong>? This action cannot be undone.
<strong>{invoiceToDelete?.client?.name}</strong>? This action
cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setDeleteDialogOpen(false)} disabled={deleteInvoice.isPending}>
<Button
variant="outline"
onClick={() => setDeleteDialogOpen(false)}
disabled={deleteInvoice.isPending}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={() => invoiceToDelete && deleteInvoice.mutate({ id: invoiceToDelete.id })}
onClick={() =>
invoiceToDelete &&
deleteInvoice.mutate({ id: invoiceToDelete.id })
}
disabled={deleteInvoice.isPending}
>
{deleteInvoice.isPending ? "Deleting..." : "Delete Invoice"}
@@ -325,25 +423,40 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
</Dialog>
{/* Bulk delete dialog */}
<Dialog open={bulkDeleteDialogOpen} onOpenChange={setBulkDeleteDialogOpen}>
<Dialog
open={bulkDeleteDialogOpen}
onOpenChange={setBulkDeleteDialogOpen}
>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete {pendingBulkDelete.length} Invoice{pendingBulkDelete.length !== 1 ? "s" : ""}</DialogTitle>
<DialogTitle>
Delete {pendingBulkDelete.length} Invoice
{pendingBulkDelete.length !== 1 ? "s" : ""}
</DialogTitle>
<DialogDescription>
This will permanently delete {pendingBulkDelete.length} invoice{pendingBulkDelete.length !== 1 ? "s" : ""}.
This action cannot be undone.
This will permanently delete {pendingBulkDelete.length} invoice
{pendingBulkDelete.length !== 1 ? "s" : ""}. This action cannot be
undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setBulkDeleteDialogOpen(false)} disabled={bulkDelete.isPending}>
<Button
variant="outline"
onClick={() => setBulkDeleteDialogOpen(false)}
disabled={bulkDelete.isPending}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={() => bulkDelete.mutate({ ids: pendingBulkDelete.map((i) => i.id) })}
onClick={() =>
bulkDelete.mutate({ ids: pendingBulkDelete.map((i) => i.id) })
}
disabled={bulkDelete.isPending}
>
{bulkDelete.isPending ? "Deleting..." : `Delete ${pendingBulkDelete.length} Invoice${pendingBulkDelete.length !== 1 ? "s" : ""}`}
{bulkDelete.isPending
? "Deleting..."
: `Delete ${pendingBulkDelete.length} Invoice${pendingBulkDelete.length !== 1 ? "s" : ""}`}
</Button>
</DialogFooter>
</DialogContent>
+185 -74
View File
@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { api } from "~/trpc/react";
import { api, type RouterOutputs } from "~/trpc/react";
import { PageHeader } from "~/components/layout/page-header";
import { Button } from "~/components/ui/button";
import { Card, CardContent } from "~/components/ui/card";
@@ -18,12 +18,7 @@ import {
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
} from "~/components/ui/tabs";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/ui/tabs";
import { toast } from "sonner";
import { Plus, Pencil, Trash2, FileText, Star } from "lucide-react";
@@ -34,87 +29,81 @@ interface TemplateForm {
isDefault: boolean;
}
const defaultForm: TemplateForm = { name: "", type: "notes", content: "", isDefault: false };
const defaultForm: TemplateForm = {
name: "",
type: "notes",
content: "",
isDefault: false,
};
export default function TemplatesPage() {
const [open, setOpen] = useState(false);
const [editId, setEditId] = useState<string | null>(null);
const [form, setForm] = useState<TemplateForm>(defaultForm);
const [deleteId, setDeleteId] = useState<string | null>(null);
const [tab, setTab] = useState<"notes" | "terms">("notes");
type InvoiceTemplate = RouterOutputs["invoiceTemplates"]["getAll"][number];
const utils = api.useUtils();
const { data: templates = [], isLoading } = api.invoiceTemplates.getAll.useQuery();
interface TemplateListProps {
items: InvoiceTemplate[];
type: "notes" | "terms";
isLoading: boolean;
onCreate: (type: "notes" | "terms") => void;
onEdit: (template: InvoiceTemplate) => void;
onDelete: (id: string) => void;
}
const create = api.invoiceTemplates.create.useMutation({
onSuccess: () => { toast.success("Template created"); void utils.invoiceTemplates.getAll.invalidate(); setOpen(false); setForm(defaultForm); },
onError: (e) => toast.error(e.message),
});
const update = api.invoiceTemplates.update.useMutation({
onSuccess: () => { toast.success("Template updated"); void utils.invoiceTemplates.getAll.invalidate(); setOpen(false); setEditId(null); setForm(defaultForm); },
onError: (e) => toast.error(e.message),
});
const del = api.invoiceTemplates.delete.useMutation({
onSuccess: () => { toast.success("Template deleted"); void utils.invoiceTemplates.getAll.invalidate(); setDeleteId(null); },
onError: (e) => toast.error(e.message),
});
const handleOpen = (type: "notes" | "terms") => {
setEditId(null);
setForm({ ...defaultForm, type });
setOpen(true);
};
const handleEdit = (t: typeof templates[0]) => {
setEditId(t.id);
setForm({ name: t.name, type: t.type as "notes" | "terms", content: t.content, isDefault: t.isDefault });
setOpen(true);
};
const handleSubmit = () => {
if (!form.name.trim()) { toast.error("Name is required"); return; }
if (!form.content.trim()) { toast.error("Content is required"); return; }
if (editId) update.mutate({ id: editId, ...form });
else create.mutate(form);
};
const notesTemplates = templates.filter((t) => t.type === "notes");
const termsTemplates = templates.filter((t) => t.type === "terms");
const TemplateList = ({ items, type }: { items: typeof templates; type: "notes" | "terms" }) => (
function TemplateList({
items,
type,
isLoading,
onCreate,
onEdit,
onDelete,
}: TemplateListProps) {
return (
<div className="space-y-3">
<div className="flex justify-end">
<Button size="sm" onClick={() => handleOpen(type)}>
<Plus className="mr-1.5 h-3.5 w-3.5" /> New {type === "notes" ? "Notes" : "Terms"} Template
<Button size="sm" onClick={() => onCreate(type)}>
<Plus className="mr-1.5 h-3.5 w-3.5" /> New{" "}
{type === "notes" ? "Notes" : "Terms"} Template
</Button>
</div>
{isLoading ? (
<div className="text-muted-foreground py-8 text-center text-sm">Loading</div>
<div className="text-muted-foreground py-8 text-center text-sm">
Loading...
</div>
) : items.length === 0 ? (
<div className="text-muted-foreground py-8 text-center text-sm">
No {type} templates yet.
</div>
) : (
items.map((t) => (
<Card key={t.id}>
items.map((template) => (
<Card key={template.id}>
<CardContent className="p-4">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<p className="font-medium">{t.name}</p>
{t.isDefault && (
<p className="font-medium">{template.name}</p>
{template.isDefault && (
<Badge variant="secondary" className="text-xs">
<Star className="mr-1 h-3 w-3" /> Default
</Badge>
)}
</div>
<p className="text-muted-foreground mt-1 line-clamp-3 text-sm whitespace-pre-wrap">
{t.content}
{template.content}
</p>
</div>
<div className="flex flex-shrink-0 gap-1">
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={() => handleEdit(t)}>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => onEdit(template)}
>
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="sm" className="text-destructive h-8 w-8 p-0" onClick={() => setDeleteId(t.id)}>
<Button
variant="ghost"
size="sm"
className="text-destructive h-8 w-8 p-0"
onClick={() => onDelete(template.id)}
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
@@ -125,6 +114,77 @@ export default function TemplatesPage() {
)}
</div>
);
}
export default function TemplatesPage() {
const [open, setOpen] = useState(false);
const [editId, setEditId] = useState<string | null>(null);
const [form, setForm] = useState<TemplateForm>(defaultForm);
const [deleteId, setDeleteId] = useState<string | null>(null);
const [tab, setTab] = useState<"notes" | "terms">("notes");
const utils = api.useUtils();
const { data: templates = [], isLoading } =
api.invoiceTemplates.getAll.useQuery();
const create = api.invoiceTemplates.create.useMutation({
onSuccess: () => {
toast.success("Template created");
void utils.invoiceTemplates.getAll.invalidate();
setOpen(false);
setForm(defaultForm);
},
onError: (e) => toast.error(e.message),
});
const update = api.invoiceTemplates.update.useMutation({
onSuccess: () => {
toast.success("Template updated");
void utils.invoiceTemplates.getAll.invalidate();
setOpen(false);
setEditId(null);
setForm(defaultForm);
},
onError: (e) => toast.error(e.message),
});
const del = api.invoiceTemplates.delete.useMutation({
onSuccess: () => {
toast.success("Template deleted");
void utils.invoiceTemplates.getAll.invalidate();
setDeleteId(null);
},
onError: (e) => toast.error(e.message),
});
const handleOpen = (type: "notes" | "terms") => {
setEditId(null);
setForm({ ...defaultForm, type });
setOpen(true);
};
const handleEdit = (t: InvoiceTemplate) => {
setEditId(t.id);
setForm({
name: t.name,
type: t.type as "notes" | "terms",
content: t.content,
isDefault: t.isDefault,
});
setOpen(true);
};
const handleSubmit = () => {
if (!form.name.trim()) {
toast.error("Name is required");
return;
}
if (!form.content.trim()) {
toast.error("Content is required");
return;
}
if (editId) update.mutate({ id: editId, ...form });
else create.mutate(form);
};
const notesTemplates = templates.filter((t) => t.type === "notes");
const termsTemplates = templates.filter((t) => t.type === "terms");
return (
<div className="page-enter space-y-6 pb-6">
@@ -137,17 +197,33 @@ export default function TemplatesPage() {
<Tabs value={tab} onValueChange={(v) => setTab(v as "notes" | "terms")}>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="notes">
<FileText className="mr-1.5 h-4 w-4" /> Notes ({notesTemplates.length})
<FileText className="mr-1.5 h-4 w-4" /> Notes (
{notesTemplates.length})
</TabsTrigger>
<TabsTrigger value="terms">
<FileText className="mr-1.5 h-4 w-4" /> Terms ({termsTemplates.length})
<FileText className="mr-1.5 h-4 w-4" /> Terms (
{termsTemplates.length})
</TabsTrigger>
</TabsList>
<TabsContent value="notes" className="mt-4">
<TemplateList items={notesTemplates} type="notes" />
<TemplateList
items={notesTemplates}
type="notes"
isLoading={isLoading}
onCreate={handleOpen}
onEdit={handleEdit}
onDelete={setDeleteId}
/>
</TabsContent>
<TabsContent value="terms" className="mt-4">
<TemplateList items={termsTemplates} type="terms" />
<TemplateList
items={termsTemplates}
type="terms"
isLoading={isLoading}
onCreate={handleOpen}
onEdit={handleEdit}
onDelete={setDeleteId}
/>
</TabsContent>
</Tabs>
@@ -155,16 +231,29 @@ export default function TemplatesPage() {
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>{editId ? "Edit Template" : "New Template"}</DialogTitle>
<DialogTitle>
{editId ? "Edit Template" : "New Template"}
</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-2">
<div className="space-y-2">
<Label>Name *</Label>
<Input value={form.name} onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))} placeholder="e.g. Standard Payment Terms" />
<Input
value={form.name}
onChange={(e) =>
setForm((p) => ({ ...p, name: e.target.value }))
}
placeholder="e.g. Standard Payment Terms"
/>
</div>
<div className="space-y-2">
<Label>Type</Label>
<Tabs value={form.type} onValueChange={(v) => setForm((p) => ({ ...p, type: v as "notes" | "terms" }))}>
<Tabs
value={form.type}
onValueChange={(v) =>
setForm((p) => ({ ...p, type: v as "notes" | "terms" }))
}
>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="notes">Notes</TabsTrigger>
<TabsTrigger value="terms">Terms</TabsTrigger>
@@ -175,20 +264,36 @@ export default function TemplatesPage() {
<Label>Content *</Label>
<Textarea
value={form.content}
onChange={(e) => setForm((p) => ({ ...p, content: e.target.value }))}
onChange={(e) =>
setForm((p) => ({ ...p, content: e.target.value }))
}
placeholder="Template content…"
className="min-h-[120px]"
/>
</div>
<label className="flex cursor-pointer items-center gap-2">
<Checkbox checked={form.isDefault} onCheckedChange={(v) => setForm((p) => ({ ...p, isDefault: !!v }))} />
<Checkbox
checked={form.isDefault}
onCheckedChange={(v) =>
setForm((p) => ({ ...p, isDefault: !!v }))
}
/>
<span className="text-sm">Set as default for {form.type}</span>
</label>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button>
<Button onClick={handleSubmit} disabled={create.isPending || update.isPending}>
{create.isPending || update.isPending ? "Saving…" : editId ? "Update" : "Create"}
<Button variant="outline" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
onClick={handleSubmit}
disabled={create.isPending || update.isPending}
>
{create.isPending || update.isPending
? "Saving…"
: editId
? "Update"
: "Create"}
</Button>
</DialogFooter>
</DialogContent>
@@ -202,8 +307,14 @@ export default function TemplatesPage() {
<DialogDescription>This action cannot be undone.</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setDeleteId(null)}>Cancel</Button>
<Button variant="destructive" onClick={() => deleteId && del.mutate({ id: deleteId })} disabled={del.isPending}>
<Button variant="outline" onClick={() => setDeleteId(null)}>
Cancel
</Button>
<Button
variant="destructive"
onClick={() => deleteId && del.mutate({ id: deleteId })}
disabled={del.isPending}
>
{del.isPending ? "Deleting…" : "Delete"}
</Button>
</DialogFooter>