feat: implement complete invoicing application with CSV import and PDF export

- Add comprehensive CSV import system with drag-and-drop upload and validation
- Create UniversalTable component with advanced filtering, searching, and batch actions
- Implement invoice management (view, edit, delete) with professional PDF export
- Add client management with full CRUD operations
- Set up authentication with NextAuth.js and email/password login
- Configure database schema with users, clients, invoices, and invoice_items tables
- Build responsive UI with shadcn/ui components and emerald branding
- Add type-safe API layer with tRPC and Zod validation
- Include proper error handling and user feedback with toast notifications
- Set up development environment with Bun, TypeScript, and Tailwind CSS
This commit is contained in:
2025-07-10 04:07:19 -04:00
commit 2d217fab47
85 changed files with 17074 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
import { z } from "zod";
import { eq } from "drizzle-orm";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { clients } from "~/server/db/schema";
const createClientSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email").optional(),
phone: z.string().optional(),
addressLine1: z.string().optional(),
addressLine2: z.string().optional(),
city: z.string().optional(),
state: z.string().optional(),
postalCode: z.string().optional(),
country: z.string().optional(),
});
const updateClientSchema = createClientSchema.partial().extend({
id: z.string(),
});
export const clientsRouter = createTRPCRouter({
getAll: protectedProcedure.query(async ({ ctx }) => {
return await ctx.db.query.clients.findMany({
where: eq(clients.createdById, ctx.session.user.id),
orderBy: (clients, { desc }) => [desc(clients.createdAt)],
});
}),
getById: protectedProcedure
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {
return await ctx.db.query.clients.findFirst({
where: eq(clients.id, input.id),
with: {
invoices: {
orderBy: (invoices, { desc }) => [desc(invoices.createdAt)],
},
},
});
}),
create: protectedProcedure
.input(createClientSchema)
.mutation(async ({ ctx, input }) => {
return await ctx.db.insert(clients).values({
...input,
createdById: ctx.session.user.id,
});
}),
update: protectedProcedure
.input(updateClientSchema)
.mutation(async ({ ctx, input }) => {
const { id, ...data } = input;
return await ctx.db
.update(clients)
.set({
...data,
updatedAt: new Date(),
})
.where(eq(clients.id, id));
}),
delete: protectedProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
return await ctx.db.delete(clients).where(eq(clients.id, input.id));
}),
});