This commit is contained in:
97
src/pages/api/invoices/[id]/convert.ts
Normal file
97
src/pages/api/invoices/[id]/convert.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { db } from "../../../../db";
|
||||
import { invoices, members } from "../../../../db/schema";
|
||||
import { eq, and, desc } from "drizzle-orm";
|
||||
|
||||
export const POST: APIRoute = async ({ redirect, locals, params }) => {
|
||||
const user = locals.user;
|
||||
if (!user) {
|
||||
return redirect("/login");
|
||||
}
|
||||
|
||||
const { id: invoiceId } = params;
|
||||
if (!invoiceId) {
|
||||
return new Response("Invoice ID required", { status: 400 });
|
||||
}
|
||||
|
||||
// Fetch invoice to verify existence
|
||||
const invoice = await db
|
||||
.select()
|
||||
.from(invoices)
|
||||
.where(eq(invoices.id, invoiceId))
|
||||
.get();
|
||||
|
||||
if (!invoice) {
|
||||
return new Response("Invoice not found", { status: 404 });
|
||||
}
|
||||
|
||||
if (invoice.type !== "quote") {
|
||||
return new Response("Only quotes can be converted to invoices", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
// Verify membership
|
||||
const membership = await db
|
||||
.select()
|
||||
.from(members)
|
||||
.where(
|
||||
and(
|
||||
eq(members.userId, user.id),
|
||||
eq(members.organizationId, invoice.organizationId),
|
||||
),
|
||||
)
|
||||
.get();
|
||||
|
||||
if (!membership) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate next invoice number
|
||||
const lastInvoice = await db
|
||||
.select()
|
||||
.from(invoices)
|
||||
.where(
|
||||
and(
|
||||
eq(invoices.organizationId, invoice.organizationId),
|
||||
eq(invoices.type, "invoice"),
|
||||
),
|
||||
)
|
||||
.orderBy(desc(invoices.createdAt))
|
||||
.limit(1)
|
||||
.get();
|
||||
|
||||
let nextInvoiceNumber = "INV-001";
|
||||
if (lastInvoice) {
|
||||
const match = lastInvoice.number.match(/(\d+)$/);
|
||||
if (match) {
|
||||
const num = parseInt(match[1]) + 1;
|
||||
let prefix = lastInvoice.number.replace(match[0], "");
|
||||
if (prefix === "EST-") prefix = "INV-";
|
||||
nextInvoiceNumber =
|
||||
prefix + num.toString().padStart(match[0].length, "0");
|
||||
}
|
||||
}
|
||||
|
||||
// Convert quote to invoice:
|
||||
// 1. Change type to 'invoice'
|
||||
// 2. Set status to 'draft' (so user can review before sending)
|
||||
// 3. Update number to next invoice sequence
|
||||
// 4. Update issue date to today
|
||||
await db
|
||||
.update(invoices)
|
||||
.set({
|
||||
type: "invoice",
|
||||
status: "draft",
|
||||
number: nextInvoiceNumber,
|
||||
issueDate: new Date(),
|
||||
})
|
||||
.where(eq(invoices.id, invoiceId));
|
||||
|
||||
return redirect(`/dashboard/invoices/${invoiceId}`);
|
||||
} catch (error) {
|
||||
console.error("Error converting quote to invoice:", error);
|
||||
return new Response("Internal Server Error", { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -69,7 +69,9 @@ export const GET: APIRoute = async ({ params, locals }) => {
|
||||
// Generate PDF using Vue PDF
|
||||
// Suppress verbose logging from PDF renderer
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleWarn = console.warn;
|
||||
console.log = () => {};
|
||||
console.warn = () => {};
|
||||
|
||||
try {
|
||||
const pdfDocument = createInvoiceDocument({
|
||||
@@ -83,6 +85,7 @@ export const GET: APIRoute = async ({ params, locals }) => {
|
||||
|
||||
// Restore console.log
|
||||
console.log = originalConsoleLog;
|
||||
console.warn = originalConsoleWarn;
|
||||
|
||||
const filename = `${invoice.type}_${invoice.number.replace(/[^a-zA-Z0-9]/g, "_")}.pdf`;
|
||||
|
||||
@@ -95,6 +98,7 @@ export const GET: APIRoute = async ({ params, locals }) => {
|
||||
} catch (pdfError) {
|
||||
// Restore console.log on error
|
||||
console.log = originalConsoleLog;
|
||||
console.warn = originalConsoleWarn;
|
||||
throw pdfError;
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
79
src/pages/api/invoices/[id]/update-tax.ts
Normal file
79
src/pages/api/invoices/[id]/update-tax.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { db } from "../../../../db";
|
||||
import { invoices, members } from "../../../../db/schema";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { recalculateInvoiceTotals } from "../../../../utils/invoice";
|
||||
|
||||
export const POST: APIRoute = async ({
|
||||
request,
|
||||
redirect,
|
||||
locals,
|
||||
params,
|
||||
}) => {
|
||||
const user = locals.user;
|
||||
if (!user) {
|
||||
return redirect("/login");
|
||||
}
|
||||
|
||||
const { id: invoiceId } = params;
|
||||
if (!invoiceId) {
|
||||
return new Response("Invoice ID required", { status: 400 });
|
||||
}
|
||||
|
||||
// Fetch invoice to verify existence
|
||||
const invoice = await db
|
||||
.select()
|
||||
.from(invoices)
|
||||
.where(eq(invoices.id, invoiceId))
|
||||
.get();
|
||||
|
||||
if (!invoice) {
|
||||
return new Response("Invoice not found", { status: 404 });
|
||||
}
|
||||
|
||||
// Verify membership
|
||||
const membership = await db
|
||||
.select()
|
||||
.from(members)
|
||||
.where(
|
||||
and(
|
||||
eq(members.userId, user.id),
|
||||
eq(members.organizationId, invoice.organizationId)
|
||||
)
|
||||
)
|
||||
.get();
|
||||
|
||||
if (!membership) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const taxRateStr = formData.get("taxRate") as string;
|
||||
|
||||
if (taxRateStr === null) {
|
||||
return new Response("Tax rate is required", { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const taxRate = parseFloat(taxRateStr);
|
||||
|
||||
if (isNaN(taxRate) || taxRate < 0) {
|
||||
return new Response("Invalid tax rate", { status: 400 });
|
||||
}
|
||||
|
||||
await db
|
||||
.update(invoices)
|
||||
.set({
|
||||
taxRate,
|
||||
})
|
||||
.where(eq(invoices.id, invoiceId));
|
||||
|
||||
// Recalculate totals since tax rate changed
|
||||
await recalculateInvoiceTotals(invoiceId);
|
||||
|
||||
return redirect(`/dashboard/invoices/${invoiceId}`);
|
||||
} catch (error) {
|
||||
console.error("Error updating invoice tax rate:", error);
|
||||
return new Response("Internal Server Error", { status: 500 });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user