mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2026-02-05 00:06:36 -05:00
Add settings page, port to turso
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { clientsRouter } from "~/server/api/routers/clients";
|
||||
import { businessesRouter } from "~/server/api/routers/businesses";
|
||||
import { invoicesRouter } from "~/server/api/routers/invoices";
|
||||
import { settingsRouter } from "~/server/api/routers/settings";
|
||||
import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
|
||||
|
||||
/**
|
||||
@@ -12,6 +13,7 @@ export const appRouter = createTRPCRouter({
|
||||
clients: clientsRouter,
|
||||
businesses: businessesRouter,
|
||||
invoices: invoicesRouter,
|
||||
settings: settingsRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
395
src/server/api/routers/settings.ts
Normal file
395
src/server/api/routers/settings.ts
Normal file
@@ -0,0 +1,395 @@
|
||||
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<string, string>();
|
||||
const businessIdMap = new Map<string, string>();
|
||||
|
||||
// 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 };
|
||||
});
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user