mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2026-05-08 09:38:55 -04:00
Add bulk actions, multi-currency, expenses, templates, and reports
Schema (migration 0001): - clients: add currency column (default USD) - invoices: add currency column (default USD) - New expenses table: amount, currency, category, billable, reimbursable, client/invoice/business relations, notes - New invoice_templates table: name, type (notes|terms), content, isDefault API: - invoices: add bulkUpdateStatus and bulkDelete procedures (ownership-safe) - invoices: currency field threaded through create/update schemas - clients: currency field added to create/update schemas - New expenses router: full CRUD with authorization - New invoiceTemplates router: full CRUD, isDefault management per type - Root router: wire in expenses and invoiceTemplates Currency (src/lib/currency.ts): - Shared formatCurrency(amount, currency) utility replacing hardcoded USD - SUPPORTED_CURRENCIES list (17 currencies) - Invoice form: currency selector in Config card, auto-fills from client - Client form: currency selector in Billing Information card Bulk actions (invoices list): - Checkbox column with select-all support - Selection toolbar: Mark as Sent/Paid/Draft dropdown, Delete (N) button - DataTable: new selectionActions prop renders toolbar when rows selected Notes templates: - Invoice form: Notes card with textarea in Details tab - Template dropdown button appears when templates exist - /dashboard/invoices/templates: full CRUD page for notes and terms templates New pages: - /dashboard/expenses: expense list with summary cards, add/edit dialog - /dashboard/reports: KPI cards, 12-month revenue area chart, top clients bar chart, status breakdown, recent activity - Navigation: Expenses and Reports added to Main section https://claude.ai/code/session_012sqEgNQpx676isepeoX4Mi
This commit is contained in:
+103
-1
@@ -39,7 +39,9 @@ export const usersRelations = relations(users, ({ many }) => ({
|
||||
clients: many(clients),
|
||||
businesses: many(businesses),
|
||||
invoices: many(invoices),
|
||||
sessions: many(sessions), // Added missing relation
|
||||
sessions: many(sessions),
|
||||
expenses: many(expenses),
|
||||
invoiceTemplates: many(invoiceTemplates),
|
||||
}));
|
||||
|
||||
export const accounts = createTable(
|
||||
@@ -140,6 +142,7 @@ export const clients = createTable(
|
||||
postalCode: d.varchar({ length: 20 }),
|
||||
country: d.varchar({ length: 100 }),
|
||||
defaultHourlyRate: d.real(),
|
||||
currency: d.varchar({ length: 3 }).default("USD").notNull(),
|
||||
createdById: d
|
||||
.varchar({ length: 255 })
|
||||
.notNull()
|
||||
@@ -238,6 +241,7 @@ export const invoices = createTable(
|
||||
totalAmount: d.real().notNull().default(0),
|
||||
taxRate: d.real().notNull().default(0.0),
|
||||
notes: d.varchar({ length: 1000 }),
|
||||
currency: d.varchar({ length: 3 }).default("USD").notNull(),
|
||||
createdById: d
|
||||
.varchar({ length: 255 })
|
||||
.notNull()
|
||||
@@ -309,3 +313,101 @@ export const invoiceItemsRelations = relations(invoiceItems, ({ one }) => ({
|
||||
references: [invoices.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const expenses = createTable(
|
||||
"expense",
|
||||
(d) => ({
|
||||
id: d
|
||||
.varchar({ length: 255 })
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
businessId: d.varchar({ length: 255 }).references(() => businesses.id),
|
||||
clientId: d.varchar({ length: 255 }).references(() => clients.id),
|
||||
invoiceId: d
|
||||
.varchar({ length: 255 })
|
||||
.references(() => invoices.id, { onDelete: "set null" }),
|
||||
date: d.timestamp().notNull(),
|
||||
description: d.varchar({ length: 500 }).notNull(),
|
||||
amount: d.real().notNull(),
|
||||
currency: d.varchar({ length: 3 }).default("USD").notNull(),
|
||||
category: d.varchar({ length: 100 }),
|
||||
billable: d.boolean().default(false).notNull(),
|
||||
reimbursable: d.boolean().default(false).notNull(),
|
||||
notes: d.varchar({ length: 500 }),
|
||||
createdById: d
|
||||
.varchar({ length: 255 })
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
createdAt: d
|
||||
.timestamp()
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: d.timestamp().$onUpdate(() => new Date()),
|
||||
}),
|
||||
(t) => [
|
||||
index("expense_created_by_idx").on(t.createdById),
|
||||
index("expense_client_id_idx").on(t.clientId),
|
||||
index("expense_invoice_id_idx").on(t.invoiceId),
|
||||
index("expense_date_idx").on(t.date),
|
||||
index("expense_billable_idx").on(t.billable),
|
||||
],
|
||||
);
|
||||
|
||||
export const expensesRelations = relations(expenses, ({ one }) => ({
|
||||
business: one(businesses, {
|
||||
fields: [expenses.businessId],
|
||||
references: [businesses.id],
|
||||
}),
|
||||
client: one(clients, {
|
||||
fields: [expenses.clientId],
|
||||
references: [clients.id],
|
||||
}),
|
||||
invoice: one(invoices, {
|
||||
fields: [expenses.invoiceId],
|
||||
references: [invoices.id],
|
||||
}),
|
||||
createdBy: one(users, {
|
||||
fields: [expenses.createdById],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const invoiceTemplates = createTable(
|
||||
"invoice_template",
|
||||
(d) => ({
|
||||
id: d
|
||||
.varchar({ length: 255 })
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
name: d.varchar({ length: 255 }).notNull(),
|
||||
type: d.varchar({ length: 50 }).notNull().default("notes"), // "notes" | "terms"
|
||||
content: d.text().notNull(),
|
||||
isDefault: d.boolean().default(false).notNull(),
|
||||
createdById: d
|
||||
.varchar({ length: 255 })
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
createdAt: d
|
||||
.timestamp()
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: d.timestamp().$onUpdate(() => new Date()),
|
||||
}),
|
||||
(t) => [
|
||||
index("invoice_template_created_by_idx").on(t.createdById),
|
||||
index("invoice_template_type_idx").on(t.type),
|
||||
],
|
||||
);
|
||||
|
||||
export const invoiceTemplatesRelations = relations(
|
||||
invoiceTemplates,
|
||||
({ one }) => ({
|
||||
createdBy: one(users, {
|
||||
fields: [invoiceTemplates.createdById],
|
||||
references: [users.id],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user