This commit is contained in:
2025-07-17 02:27:36 -04:00
parent e9eb9c03eb
commit 505d47918e
2 changed files with 135 additions and 79 deletions

View File

@@ -231,8 +231,9 @@ function InvoiceFormSkeleton() {
);
}
export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
function InvoiceForm({ invoiceId }: InvoiceFormProps) {
const router = useRouter();
const utils = api.useUtils();
const [formData, setFormData] = useState({
invoiceNumber: `INV-${new Date().toISOString().slice(0, 10).replace(/-/g, "")}-${Date.now().toString().slice(-6)}`,
@@ -418,6 +419,8 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
const createInvoice = api.invoices.create.useMutation({
onSuccess: () => {
toast.success("Invoice created successfully");
// Invalidate related queries to refresh cache
void utils.invoices.getAll.invalidate();
router.push("/dashboard/invoices");
},
onError: (error) => {
@@ -428,44 +431,116 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
const updateInvoice = api.invoices.update.useMutation({
onSuccess: () => {
toast.success("Invoice updated successfully");
// Invalidate related queries to refresh cache
void utils.invoices.getAll.invalidate();
if (invoiceId) {
void utils.invoices.getById.invalidate({ id: invoiceId });
}
router.push("/dashboard/invoices");
},
onError: (error) => {
console.error("Update invoice error:", error);
toast.error(error.message || "Failed to update invoice");
},
});
const updateStatus = api.invoices.updateStatus.useMutation({
onSuccess: () => {
toast.success("Status updated successfully");
// Invalidate related queries to refresh cache
void utils.invoices.getAll.invalidate();
if (invoiceId) {
void utils.invoices.getById.invalidate({ id: invoiceId });
}
router.push("/dashboard/invoices");
},
onError: (error) => {
console.error("Update status error:", error);
toast.error(error.message || "Failed to update status");
},
});
// Check if only status has changed compared to existing invoice
const hasOnlyStatusChanged = React.useMemo(() => {
if (!existingInvoice || !invoiceId) return false;
return (
formData.invoiceNumber === existingInvoice.invoiceNumber &&
formData.businessId === (existingInvoice.businessId ?? "") &&
formData.clientId === existingInvoice.clientId &&
formData.issueDate.getTime() ===
new Date(existingInvoice.issueDate).getTime() &&
formData.dueDate.getTime() ===
new Date(existingInvoice.dueDate).getTime() &&
formData.status !== existingInvoice.status &&
formData.notes === (existingInvoice.notes ?? "") &&
formData.taxRate === existingInvoice.taxRate &&
JSON.stringify(
formData.items.map((item) => ({
date: item.date.getTime(),
description: item.description,
hours: item.hours,
rate: item.rate,
})),
) ===
JSON.stringify(
(existingInvoice.items ?? []).map((item) => ({
date: new Date(item.date).getTime(),
description: item.description,
hours: item.hours,
rate: item.rate,
})),
)
);
}, [formData, existingInvoice, invoiceId]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const invoiceData = {
invoiceNumber: formData.invoiceNumber,
businessId: formData.businessId || undefined,
clientId: formData.clientId,
issueDate: formData.issueDate,
dueDate: formData.dueDate,
status: formData.status,
notes: formData.notes,
taxRate: formData.taxRate,
items: formData.items.map((item) => ({
date: item.date,
description: item.description,
hours: item.hours,
rate: item.rate,
amount: item.hours * item.rate,
})),
};
if (invoiceId) {
await updateInvoice.mutateAsync({ id: invoiceId, ...invoiceData });
if (invoiceId && hasOnlyStatusChanged) {
// Use dedicated status update mutation for status-only changes
console.log("Using status-only update:", {
id: invoiceId,
status: formData.status,
});
await updateStatus.mutateAsync({
id: invoiceId,
status: formData.status,
});
} else {
await createInvoice.mutateAsync(invoiceData);
// Use full update mutation for all other changes
const invoiceData = {
invoiceNumber: formData.invoiceNumber,
businessId: formData.businessId || undefined,
clientId: formData.clientId,
issueDate: formData.issueDate,
dueDate: formData.dueDate,
status: formData.status,
notes: formData.notes,
taxRate: formData.taxRate,
items: formData.items.map((item) => ({
date: item.date,
description: item.description,
hours: item.hours,
rate: item.rate,
amount: item.hours * item.rate,
})),
};
console.log("Submitting invoice data:", invoiceData);
if (invoiceId) {
await updateInvoice.mutateAsync({ id: invoiceId, ...invoiceData });
} else {
await createInvoice.mutateAsync(invoiceData);
}
}
} catch (error) {
console.error("Error saving invoice:", error);
toast.error("Failed to save invoice. Check console for details.");
} finally {
setLoading(false);
}
@@ -567,6 +642,11 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
))}
</SelectContent>
</Select>
{invoiceId && hasOnlyStatusChanged && (
<div className="mt-1 text-xs text-blue-600 dark:text-blue-400">
Only status will be updated
</div>
)}
</div>
</div>
@@ -839,3 +919,5 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
</>
);
}
export { InvoiceForm };

View File

@@ -107,19 +107,19 @@
@media (prefers-color-scheme: dark) {
:root {
--background: oklch(0 0 0);
--background: oklch(0.1 0.05 180);
--foreground: oklch(0.985 0 0);
--card: oklch(0.25 0.08 170);
--card: oklch(0.15 0.03 180);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.25 0.08 170);
--popover: oklch(0.15 0.03 180);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.7 0.15 165);
--primary-foreground: oklch(0.08 0.015 165);
--secondary: oklch(0.3 0.05 170);
--secondary: oklch(0.25 0.02 180);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.3 0.05 170);
--muted: oklch(0.25 0.02 180);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.3 0.05 170);
--accent: oklch(0.25 0.02 180);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--destructive-foreground: oklch(0.985 0 0);
@@ -131,11 +131,11 @@
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0.02 160);
--sidebar: oklch(0.12 0.04 180);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.696 0.17 162.48);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0.015 160);
--sidebar-accent: oklch(0.18 0.03 180);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
@@ -199,31 +199,7 @@
@media (prefers-color-scheme: dark) {
body {
background:
radial-gradient(
circle at 20% 80%,
oklch(0.25 0.15 165 / 0.6) 0%,
transparent 50%
),
radial-gradient(
circle at 80% 20%,
oklch(0.3 0.12 185 / 0.4) 0%,
transparent 50%
),
radial-gradient(
circle at 40% 40%,
oklch(0.35 0.1 205 / 0.3) 0%,
transparent 50%
),
linear-gradient(
135deg,
oklch(0.15 0.12 165) 0%,
oklch(0.18 0.1 175) 25%,
oklch(0.16 0.14 185) 50%,
oklch(0.15 0.12 195) 75%,
oklch(0.18 0.1 205) 100%
)
fixed;
background: hsl(180 50% 8%);
}
}
@@ -427,8 +403,8 @@
.bg-gradient-auth {
background: linear-gradient(
135deg,
oklch(0.88 0.12 165) 0%,
oklch(0.92 0.08 185) 100%
oklch(0.667 0.192 164.206) 0%,
oklch(0.731 0.182 183.061) 100%
);
}
@@ -436,8 +412,8 @@
.bg-gradient-auth {
background: linear-gradient(
135deg,
oklch(0.15 0.12 165) 0%,
oklch(0.18 0.1 185) 100%
oklch(0.15 0.12 180) 0%,
oklch(0.18 0.1 180) 100%
);
}
}
@@ -740,15 +716,15 @@
}
.bg-hero-gradient {
@apply bg-gradient-to-br from-emerald-600 via-teal-700 to-blue-800 dark:from-emerald-600/95 dark:via-teal-700/95 dark:to-blue-800/95;
@apply bg-gradient-to-br from-emerald-600 via-teal-700 to-blue-800 dark:from-teal-700 dark:via-teal-800 dark:to-teal-900;
}
.bg-page-gradient {
@apply bg-gradient-to-br from-white via-emerald-50/50 to-teal-50/30 dark:from-slate-900 dark:via-teal-900/8 dark:to-blue-900/8;
@apply bg-gradient-to-br from-white via-emerald-50/50 to-teal-50/30 dark:from-teal-950 dark:via-teal-900 dark:to-teal-800;
}
.bg-features-gradient {
@apply bg-gradient-to-br from-white via-emerald-50/30 to-teal-50/50 dark:from-slate-900/95 dark:via-teal-900/12 dark:to-blue-900/12;
@apply bg-gradient-to-br from-white via-emerald-50/30 to-teal-50/50 dark:from-teal-950 dark:via-teal-900 dark:to-teal-800;
}
/* Card Utility Classes */
@@ -774,45 +750,43 @@
/* Modern Dark Theme Styling */
@media (prefers-color-scheme: dark) {
/* Page background - rich dark base */
/* Page background - rich dark teal base */
.floating-orbs {
background-color: hsl(
206 12% 8%
) !important; /* Rich dark blue-green background */
background-color: hsl(180 50% 8%) !important; /* Dark teal background */
}
/* All cards - warm neutral with subtle transparency */
/* All cards - dark teal with subtle transparency */
[data-slot="card"] {
background-color: hsl(
206 10% 13% / 0.9
) !important; /* Blue-green dark cards */
border-color: hsl(206 10% 20%) !important; /* Subtle borders */
background-color: hsl(180 30% 10%) !important; /* Dark teal cards */
border-color: hsl(180 25% 15%) !important; /* Subtle teal borders */
}
/* Secondary cards - slightly lighter for hierarchy */
[data-slot="card"].card-secondary,
.card-secondary {
background-color: hsl(
206 9% 16% / 0.85
) !important; /* Lighter secondary */
border-color: hsl(206 9% 24%) !important; /* Softer borders */
180 25% 12%
) !important; /* Lighter teal secondary */
border-color: hsl(180 20% 20%) !important; /* Softer teal borders */
}
/* Navigation elements - cohesive with cards */
.nav-sticky,
aside.bg-background\/60,
header .bg-background\/60 {
background-color: hsl(210 10% 12% / 0.95) !important; /* Navigation bg */
border-color: hsl(210 10% 20%) !important; /* Consistent borders */
background-color: hsl(
180 40% 9% / 0.95
) !important; /* Teal navigation bg */
border-color: hsl(180 30% 18%) !important; /* Consistent teal borders */
}
/* Invoice line item mobile styling */
.dark .bg-gray-200\/30 {
background-color: hsl(210 8% 18% / 0.4) !important;
background-color: hsl(180 20% 15% / 0.4) !important;
}
.dark .border-gray-400\/60 {
border-color: hsl(210 8% 25% / 0.6) !important;
border-color: hsl(180 15% 22% / 0.6) !important;
}
}