mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2026-05-08 09:38:55 -04:00
ddc2b42672
- 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.
156 lines
4.9 KiB
TypeScript
156 lines
4.9 KiB
TypeScript
import { z } from "zod";
|
|
import { eq, and, desc } from "drizzle-orm";
|
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
|
import { expenses, clients, businesses, invoices } from "~/server/db/schema";
|
|
import { TRPCError } from "@trpc/server";
|
|
import { EXPENSE_CATEGORIES } from "~/lib/expense-categories";
|
|
|
|
export { EXPENSE_CATEGORIES };
|
|
|
|
const createExpenseSchema = z.object({
|
|
date: z.date(),
|
|
description: z.string().min(1, "Description is required"),
|
|
amount: z.number().min(0, "Amount must be positive"),
|
|
currency: z.string().length(3).default("USD"),
|
|
category: z.string().optional().or(z.literal("")),
|
|
billable: z.boolean().default(false),
|
|
reimbursable: z.boolean().default(false),
|
|
taxDeductible: z.boolean().default(false),
|
|
notes: z.string().optional().or(z.literal("")),
|
|
clientId: z.string().optional().or(z.literal("")),
|
|
businessId: z.string().optional().or(z.literal("")),
|
|
invoiceId: z.string().optional().or(z.literal("")),
|
|
});
|
|
|
|
const updateExpenseSchema = createExpenseSchema.partial().extend({
|
|
id: z.string(),
|
|
});
|
|
|
|
export const expensesRouter = createTRPCRouter({
|
|
getAll: protectedProcedure.query(async ({ ctx }) => {
|
|
return await ctx.db.query.expenses.findMany({
|
|
where: eq(expenses.createdById, ctx.session.user.id),
|
|
with: { client: true, business: true, invoice: true },
|
|
orderBy: [desc(expenses.date)],
|
|
});
|
|
}),
|
|
|
|
getById: protectedProcedure
|
|
.input(z.object({ id: z.string() }))
|
|
.query(async ({ ctx, input }) => {
|
|
const expense = await ctx.db.query.expenses.findFirst({
|
|
where: and(
|
|
eq(expenses.id, input.id),
|
|
eq(expenses.createdById, ctx.session.user.id),
|
|
),
|
|
with: { client: true, business: true, invoice: true },
|
|
});
|
|
|
|
if (!expense) {
|
|
throw new TRPCError({ code: "NOT_FOUND", message: "Expense not found" });
|
|
}
|
|
|
|
return expense;
|
|
}),
|
|
|
|
create: protectedProcedure
|
|
.input(createExpenseSchema)
|
|
.mutation(async ({ ctx, input }) => {
|
|
const clean = {
|
|
...input,
|
|
clientId: input.clientId?.trim() ?? null,
|
|
businessId: input.businessId?.trim() ?? null,
|
|
invoiceId: input.invoiceId?.trim() ?? null,
|
|
category: input.category?.trim() ?? null,
|
|
notes: input.notes?.trim() ?? null,
|
|
};
|
|
|
|
if (clean.clientId) {
|
|
const client = await ctx.db.query.clients.findFirst({
|
|
where: and(
|
|
eq(clients.id, clean.clientId),
|
|
eq(clients.createdById, ctx.session.user.id),
|
|
),
|
|
});
|
|
if (!client) throw new TRPCError({ code: "FORBIDDEN", message: "Client not found" });
|
|
}
|
|
|
|
if (clean.businessId) {
|
|
const business = await ctx.db.query.businesses.findFirst({
|
|
where: and(
|
|
eq(businesses.id, clean.businessId),
|
|
eq(businesses.createdById, ctx.session.user.id),
|
|
),
|
|
});
|
|
if (!business) throw new TRPCError({ code: "FORBIDDEN", message: "Business not found" });
|
|
}
|
|
|
|
if (clean.invoiceId) {
|
|
const invoice = await ctx.db.query.invoices.findFirst({
|
|
where: and(
|
|
eq(invoices.id, clean.invoiceId),
|
|
eq(invoices.createdById, ctx.session.user.id),
|
|
),
|
|
});
|
|
if (!invoice) throw new TRPCError({ code: "FORBIDDEN", message: "Invoice not found" });
|
|
}
|
|
|
|
const [expense] = await ctx.db
|
|
.insert(expenses)
|
|
.values({ ...clean, createdById: ctx.session.user.id })
|
|
.returning();
|
|
|
|
return expense;
|
|
}),
|
|
|
|
update: protectedProcedure
|
|
.input(updateExpenseSchema)
|
|
.mutation(async ({ ctx, input }) => {
|
|
const { id, ...data } = input;
|
|
|
|
const existing = await ctx.db.query.expenses.findFirst({
|
|
where: and(
|
|
eq(expenses.id, id),
|
|
eq(expenses.createdById, ctx.session.user.id),
|
|
),
|
|
});
|
|
|
|
if (!existing) {
|
|
throw new TRPCError({ code: "NOT_FOUND", message: "Expense not found" });
|
|
}
|
|
|
|
const clean = {
|
|
...data,
|
|
clientId: data.clientId?.trim() ?? null,
|
|
businessId: data.businessId?.trim() ?? null,
|
|
invoiceId: data.invoiceId?.trim() ?? null,
|
|
category: data.category?.trim() ?? null,
|
|
notes: data.notes?.trim() ?? null,
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
await ctx.db.update(expenses).set(clean).where(eq(expenses.id, id));
|
|
|
|
return { success: true };
|
|
}),
|
|
|
|
delete: protectedProcedure
|
|
.input(z.object({ id: z.string() }))
|
|
.mutation(async ({ ctx, input }) => {
|
|
const existing = await ctx.db.query.expenses.findFirst({
|
|
where: and(
|
|
eq(expenses.id, input.id),
|
|
eq(expenses.createdById, ctx.session.user.id),
|
|
),
|
|
});
|
|
|
|
if (!existing) {
|
|
throw new TRPCError({ code: "NOT_FOUND", message: "Expense not found" });
|
|
}
|
|
|
|
await ctx.db.delete(expenses).where(eq(expenses.id, input.id));
|
|
|
|
return { success: true };
|
|
}),
|
|
});
|