diff --git a/src/components/forms/invoice-form.tsx b/src/components/forms/invoice-form.tsx
index 4b5c1ac..911fff5 100644
--- a/src/components/forms/invoice-form.tsx
+++ b/src/components/forms/invoice-form.tsx
@@ -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) {
))}
+ {invoiceId && hasOnlyStatusChanged && (
+
+ Only status will be updated
+
+ )}
@@ -839,3 +919,5 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
>
);
}
+
+export { InvoiceForm };
diff --git a/src/styles/globals.css b/src/styles/globals.css
index 4433331..4a0ac9a 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -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;
}
}