import { z } from "zod"; import { eq, and } from "drizzle-orm"; import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import { users, clients, businesses, invoices, invoiceItems } from "~/server/db/schema"; // Validation schemas for backup data const ClientBackupSchema = z.object({ name: z.string(), email: z.string().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 BusinessBackupSchema = z.object({ name: z.string(), email: z.string().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(), website: z.string().optional(), taxId: z.string().optional(), logoUrl: z.string().optional(), isDefault: z.boolean().default(false), }); const InvoiceItemBackupSchema = z.object({ date: z.date(), description: z.string(), hours: z.number(), rate: z.number(), amount: z.number(), position: z.number().default(0), }); const InvoiceBackupSchema = z.object({ invoiceNumber: z.string(), businessName: z.string().optional(), clientName: z.string(), issueDate: z.date(), dueDate: z.date(), status: z.string().default("draft"), totalAmount: z.number().default(0), taxRate: z.number().default(0), notes: z.string().optional(), items: z.array(InvoiceItemBackupSchema), }); const BackupDataSchema = z.object({ exportDate: z.string(), version: z.string().default("1.0"), user: z.object({ name: z.string().optional(), email: z.string(), }), clients: z.array(ClientBackupSchema), businesses: z.array(BusinessBackupSchema), invoices: z.array(InvoiceBackupSchema), }); export const settingsRouter = createTRPCRouter({ // Get user profile information getProfile: protectedProcedure.query(async ({ ctx }) => { const user = await ctx.db.query.users.findFirst({ where: eq(users.id, ctx.session.user.id), columns: { id: true, name: true, email: true, image: true, }, }); return user; }), // Update user profile updateProfile: protectedProcedure .input( z.object({ name: z.string().min(1, "Name is required"), }) ) .mutation(async ({ ctx, input }) => { await ctx.db .update(users) .set({ name: input.name, }) .where(eq(users.id, ctx.session.user.id)); return { success: true }; }), // Export user data (backup) exportData: protectedProcedure.query(async ({ ctx }) => { const userId = ctx.session.user.id; // Get user info const user = await ctx.db.query.users.findFirst({ where: eq(users.id, userId), columns: { name: true, email: true, }, }); // Get all clients const userClients = await ctx.db.query.clients.findMany({ where: eq(clients.createdById, userId), columns: { id: true, name: true, email: true, phone: true, addressLine1: true, addressLine2: true, city: true, state: true, postalCode: true, country: true, }, }); // Get all businesses const userBusinesses = await ctx.db.query.businesses.findMany({ where: eq(businesses.createdById, userId), columns: { id: true, name: true, email: true, phone: true, addressLine1: true, addressLine2: true, city: true, state: true, postalCode: true, country: true, website: true, taxId: true, logoUrl: true, isDefault: true, }, }); // Get all invoices with their items const userInvoices = await ctx.db.query.invoices.findMany({ where: eq(invoices.createdById, userId), with: { client: { columns: { name: true, }, }, business: { columns: { name: true, }, }, items: { columns: { date: true, description: true, hours: true, rate: true, amount: true, position: true, }, orderBy: (items, { asc }) => [asc(items.position)], }, }, }); // Format the data for export const backupData = { exportDate: new Date().toISOString(), version: "1.0", user: { name: user?.name ?? "", email: user?.email ?? "", }, clients: userClients.map(client => ({ name: client.name, email: client.email ?? undefined, phone: client.phone ?? undefined, addressLine1: client.addressLine1 ?? undefined, addressLine2: client.addressLine2 ?? undefined, city: client.city ?? undefined, state: client.state ?? undefined, postalCode: client.postalCode ?? undefined, country: client.country ?? undefined, })), businesses: userBusinesses.map(business => ({ name: business.name, email: business.email ?? undefined, phone: business.phone ?? undefined, addressLine1: business.addressLine1 ?? undefined, addressLine2: business.addressLine2 ?? undefined, city: business.city ?? undefined, state: business.state ?? undefined, postalCode: business.postalCode ?? undefined, country: business.country ?? undefined, website: business.website ?? undefined, taxId: business.taxId ?? undefined, logoUrl: business.logoUrl ?? undefined, isDefault: business.isDefault ?? false, })), invoices: userInvoices.map(invoice => ({ invoiceNumber: invoice.invoiceNumber, businessName: invoice.business?.name, clientName: invoice.client.name, issueDate: invoice.issueDate, dueDate: invoice.dueDate, status: invoice.status, totalAmount: invoice.totalAmount, taxRate: invoice.taxRate, notes: invoice.notes ?? undefined, items: invoice.items, })), }; return backupData; }), // Import user data (restore) importData: protectedProcedure .input(BackupDataSchema) .mutation(async ({ ctx, input }) => { const userId = ctx.session.user.id; return await ctx.db.transaction(async (tx) => { // Create a map to track old to new client IDs const clientIdMap = new Map(); const businessIdMap = new Map(); // Import clients for (const clientData of input.clients) { const [newClient] = await tx .insert(clients) .values({ ...clientData, createdById: userId, }) .returning({ id: clients.id }); if (newClient) { clientIdMap.set(clientData.name, newClient.id); } } // Import businesses for (const businessData of input.businesses) { const [newBusiness] = await tx .insert(businesses) .values({ ...businessData, createdById: userId, }) .returning({ id: businesses.id }); if (newBusiness) { businessIdMap.set(businessData.name, newBusiness.id); } } // Import invoices for (const invoiceData of input.invoices) { const clientId = clientIdMap.get(invoiceData.clientName); if (!clientId) { throw new Error(`Client ${invoiceData.clientName} not found`); } const businessId = invoiceData.businessName ? businessIdMap.get(invoiceData.businessName) : null; const [newInvoice] = await tx .insert(invoices) .values({ invoiceNumber: invoiceData.invoiceNumber, businessId, clientId, issueDate: invoiceData.issueDate, dueDate: invoiceData.dueDate, status: invoiceData.status, totalAmount: invoiceData.totalAmount, taxRate: invoiceData.taxRate, notes: invoiceData.notes, createdById: userId, }) .returning({ id: invoices.id }); if (newInvoice && invoiceData.items.length > 0) { // Import invoice items await tx.insert(invoiceItems).values( invoiceData.items.map(item => ({ ...item, invoiceId: newInvoice.id, })) ); } } return { success: true, imported: { clients: input.clients.length, businesses: input.businesses.length, invoices: input.invoices.length, items: input.invoices.reduce((sum, inv) => sum + inv.items.length, 0), } }; }); }), // Get data statistics getDataStats: protectedProcedure.query(async ({ ctx }) => { const userId = ctx.session.user.id; const [clientCount, businessCount, invoiceCount] = await Promise.all([ ctx.db .select({ count: clients.id }) .from(clients) .where(eq(clients.createdById, userId)) .then(result => result.length), ctx.db .select({ count: businesses.id }) .from(businesses) .where(eq(businesses.createdById, userId)) .then(result => result.length), ctx.db .select({ count: invoices.id }) .from(invoices) .where(eq(invoices.createdById, userId)) .then(result => result.length), ]); return { clients: clientCount, businesses: businessCount, invoices: invoiceCount, }; }), // Delete all user data (for account deletion) deleteAllData: protectedProcedure .input(z.object({ confirmText: z.string().refine(val => val === "DELETE ALL DATA", { message: "You must type 'DELETE ALL DATA' to confirm", }), })) .mutation(async ({ ctx }) => { const userId = ctx.session.user.id; return await ctx.db.transaction(async (tx) => { // Delete in order due to foreign key constraints // 1. Invoice items (cascade should handle this, but being explicit) const userInvoiceIds = await tx .select({ id: invoices.id }) .from(invoices) .where(eq(invoices.createdById, userId)); if (userInvoiceIds.length > 0) { for (const invoice of userInvoiceIds) { await tx.delete(invoiceItems).where(eq(invoiceItems.invoiceId, invoice.id)); } } // 2. Invoices await tx.delete(invoices).where(eq(invoices.createdById, userId)); // 3. Clients await tx.delete(clients).where(eq(clients.createdById, userId)); // 4. Businesses await tx.delete(businesses).where(eq(businesses.createdById, userId)); return { success: true }; }); }), });