store.ts 27 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109
  1. /**
  2. * Database store for the dashboard API using Drizzle ORM
  3. */
  4. import { eq, ilike, or, desc, asc, max } from "drizzle-orm";
  5. import { db } from "./connection";
  6. import {
  7. customers,
  8. invoices,
  9. expenses,
  10. accounts,
  11. transactions,
  12. widgets,
  13. type Customer,
  14. type Invoice,
  15. type Expense,
  16. type InvoiceItem,
  17. type WidgetSpec,
  18. } from "./schema";
  19. // Helper to generate IDs
  20. const generateId = (prefix: string) => `${prefix}-${Date.now()}`;
  21. // Customer methods
  22. export async function getCustomers(filters?: {
  23. status?: string;
  24. search?: string;
  25. limit?: number;
  26. sort?: "newest" | "oldest";
  27. }) {
  28. let query = db.select().from(customers);
  29. if (filters?.status) {
  30. query = query.where(
  31. eq(customers.status, filters.status as "active" | "inactive"),
  32. ) as typeof query;
  33. }
  34. if (filters?.search) {
  35. const searchPattern = `%${filters.search}%`;
  36. query = query.where(
  37. or(
  38. ilike(customers.name, searchPattern),
  39. ilike(customers.email, searchPattern),
  40. ),
  41. ) as typeof query;
  42. }
  43. // Default to newest first
  44. const sortOrder = filters?.sort === "oldest" ? asc : desc;
  45. query = query.orderBy(sortOrder(customers.createdAt)) as typeof query;
  46. if (filters?.limit) {
  47. query = query.limit(filters.limit) as typeof query;
  48. }
  49. return query;
  50. }
  51. export async function getCustomer(id: string) {
  52. const result = await db
  53. .select()
  54. .from(customers)
  55. .where(eq(customers.id, id))
  56. .limit(1);
  57. return result[0] || null;
  58. }
  59. export async function createCustomer(data: {
  60. name: string;
  61. email: string;
  62. phone?: string;
  63. }) {
  64. const id = generateId("cust");
  65. const [customer] = await db
  66. .insert(customers)
  67. .values({
  68. id,
  69. name: data.name,
  70. email: data.email,
  71. phone: data.phone || null,
  72. balance: "0",
  73. status: "active",
  74. })
  75. .returning();
  76. return customer;
  77. }
  78. export async function updateCustomer(id: string, data: Partial<Customer>) {
  79. const [customer] = await db
  80. .update(customers)
  81. .set(data)
  82. .where(eq(customers.id, id))
  83. .returning();
  84. return customer || null;
  85. }
  86. export async function deleteCustomer(id: string) {
  87. const result = await db
  88. .delete(customers)
  89. .where(eq(customers.id, id))
  90. .returning({ id: customers.id });
  91. return { success: result.length > 0 };
  92. }
  93. // Invoice methods
  94. export async function getInvoices(filters?: {
  95. status?: string;
  96. customerId?: string;
  97. }) {
  98. let query = db.select().from(invoices);
  99. if (filters?.status) {
  100. query = query.where(
  101. eq(invoices.status, filters.status as Invoice["status"]),
  102. ) as typeof query;
  103. }
  104. if (filters?.customerId) {
  105. query = query.where(
  106. eq(invoices.customerId, filters.customerId),
  107. ) as typeof query;
  108. }
  109. return query;
  110. }
  111. export async function getInvoice(id: string) {
  112. const result = await db
  113. .select()
  114. .from(invoices)
  115. .where(eq(invoices.id, id))
  116. .limit(1);
  117. return result[0] || null;
  118. }
  119. export async function createInvoice(data: {
  120. customerId: string;
  121. dueDate: string;
  122. items?: InvoiceItem[];
  123. }) {
  124. const customer = await getCustomer(data.customerId);
  125. if (!customer) return null;
  126. const items = data.items || [];
  127. const amount = items.reduce((sum, item) => sum + item.amount, 0);
  128. const id = generateId("inv");
  129. const [invoice] = await db
  130. .insert(invoices)
  131. .values({
  132. id,
  133. customerId: data.customerId,
  134. customerName: customer.name,
  135. amount: amount.toString(),
  136. status: "draft",
  137. dueDate: new Date(data.dueDate),
  138. items,
  139. })
  140. .returning();
  141. return invoice;
  142. }
  143. export async function updateInvoice(id: string, data: Partial<Invoice>) {
  144. const [invoice] = await db
  145. .update(invoices)
  146. .set(data)
  147. .where(eq(invoices.id, id))
  148. .returning();
  149. return invoice || null;
  150. }
  151. export async function deleteInvoice(id: string) {
  152. const result = await db
  153. .delete(invoices)
  154. .where(eq(invoices.id, id))
  155. .returning({ id: invoices.id });
  156. return result.length > 0;
  157. }
  158. export async function sendInvoice(id: string) {
  159. const invoice = await getInvoice(id);
  160. if (!invoice) return { error: "Invoice not found" };
  161. if (invoice.status !== "draft")
  162. return { error: "Only draft invoices can be sent" };
  163. return updateInvoice(id, { status: "sent" });
  164. }
  165. export async function markInvoicePaid(id: string) {
  166. const invoice = await getInvoice(id);
  167. if (!invoice) return { error: "Invoice not found" };
  168. if (invoice.status === "paid") return { error: "Invoice is already paid" };
  169. // Update customer balance
  170. const customer = await getCustomer(invoice.customerId);
  171. if (customer) {
  172. const newBalance =
  173. parseFloat(customer.balance as string) -
  174. parseFloat(invoice.amount as string);
  175. await updateCustomer(invoice.customerId, {
  176. balance: newBalance.toString(),
  177. });
  178. }
  179. // Add transaction
  180. await db.insert(transactions).values({
  181. id: generateId("txn"),
  182. accountId: "acc-001",
  183. type: "income",
  184. amount: invoice.amount,
  185. description: `Invoice #${invoice.id} payment from ${invoice.customerName}`,
  186. category: "Sales",
  187. date: new Date(),
  188. });
  189. return updateInvoice(id, { status: "paid" });
  190. }
  191. // Expense methods
  192. export async function getExpenses(filters?: {
  193. status?: string;
  194. category?: string;
  195. }) {
  196. let query = db.select().from(expenses);
  197. if (filters?.status) {
  198. query = query.where(
  199. eq(expenses.status, filters.status as Expense["status"]),
  200. ) as typeof query;
  201. }
  202. if (filters?.category) {
  203. query = query.where(
  204. eq(expenses.category, filters.category),
  205. ) as typeof query;
  206. }
  207. return query;
  208. }
  209. export async function getExpense(id: string) {
  210. const result = await db
  211. .select()
  212. .from(expenses)
  213. .where(eq(expenses.id, id))
  214. .limit(1);
  215. return result[0] || null;
  216. }
  217. export async function createExpense(data: {
  218. vendor: string;
  219. category: string;
  220. amount: number;
  221. date?: string;
  222. description?: string;
  223. }) {
  224. const id = generateId("exp");
  225. const [expense] = await db
  226. .insert(expenses)
  227. .values({
  228. id,
  229. vendor: data.vendor,
  230. category: data.category,
  231. amount: data.amount.toString(),
  232. date: data.date ? new Date(data.date) : new Date(),
  233. status: "pending",
  234. description: data.description || null,
  235. })
  236. .returning();
  237. return expense;
  238. }
  239. export async function updateExpense(id: string, data: Partial<Expense>) {
  240. const [expense] = await db
  241. .update(expenses)
  242. .set(data)
  243. .where(eq(expenses.id, id))
  244. .returning();
  245. return expense || null;
  246. }
  247. export async function deleteExpense(id: string) {
  248. const result = await db
  249. .delete(expenses)
  250. .where(eq(expenses.id, id))
  251. .returning({ id: expenses.id });
  252. return result.length > 0;
  253. }
  254. export async function approveExpense(id: string) {
  255. const expense = await getExpense(id);
  256. if (!expense) return { error: "Expense not found" };
  257. if (expense.status !== "pending")
  258. return { error: "Only pending expenses can be approved" };
  259. // Add transaction
  260. await db.insert(transactions).values({
  261. id: generateId("txn"),
  262. accountId: "acc-001",
  263. type: "expense",
  264. amount: expense.amount,
  265. description: `${expense.vendor} - ${expense.description || ""}`,
  266. category: expense.category,
  267. date: expense.date,
  268. });
  269. return updateExpense(id, { status: "approved" });
  270. }
  271. export async function rejectExpense(id: string) {
  272. const expense = await getExpense(id);
  273. if (!expense) return { error: "Expense not found" };
  274. if (expense.status !== "pending")
  275. return { error: "Only pending expenses can be rejected" };
  276. return updateExpense(id, { status: "rejected" });
  277. }
  278. // Account methods
  279. export async function getAccounts() {
  280. const accountList = await db.select().from(accounts);
  281. // Get recent transactions for each account
  282. const result = await Promise.all(
  283. accountList.map(async (account) => {
  284. const recentTxns = await db
  285. .select()
  286. .from(transactions)
  287. .where(eq(transactions.accountId, account.id))
  288. .orderBy(desc(transactions.date))
  289. .limit(5);
  290. return { ...account, recentTransactions: recentTxns };
  291. }),
  292. );
  293. return result;
  294. }
  295. export async function getAccount(id: string) {
  296. const result = await db
  297. .select()
  298. .from(accounts)
  299. .where(eq(accounts.id, id))
  300. .limit(1);
  301. if (!result[0]) return null;
  302. const txns = await db
  303. .select()
  304. .from(transactions)
  305. .where(eq(transactions.accountId, id))
  306. .orderBy(desc(transactions.date));
  307. return { ...result[0], transactions: txns };
  308. }
  309. // Dashboard summary
  310. export async function getDashboardSummary() {
  311. // Get all invoices
  312. const allInvoices = await db.select().from(invoices);
  313. const paidInvoices = allInvoices.filter((i) => i.status === "paid");
  314. const totalRevenue = paidInvoices.reduce(
  315. (sum, i) => sum + parseFloat(i.amount as string),
  316. 0,
  317. );
  318. const outstandingInvoices = allInvoices
  319. .filter((i) => i.status === "sent" || i.status === "overdue")
  320. .reduce((sum, i) => sum + parseFloat(i.amount as string), 0);
  321. const overdueAmount = allInvoices
  322. .filter((i) => i.status === "overdue")
  323. .reduce((sum, i) => sum + parseFloat(i.amount as string), 0);
  324. // Get all expenses
  325. const allExpenses = await db.select().from(expenses);
  326. const approvedExpenses = allExpenses.filter((e) => e.status === "approved");
  327. const totalExpenses = approvedExpenses.reduce(
  328. (sum, e) => sum + parseFloat(e.amount as string),
  329. 0,
  330. );
  331. const cashFlow = totalRevenue - totalExpenses;
  332. // Get accounts
  333. const allAccounts = await db.select().from(accounts);
  334. const totalBankBalance = allAccounts
  335. .filter((a) => a.type === "bank")
  336. .reduce((sum, a) => sum + parseFloat(a.balance as string), 0);
  337. // Get customers
  338. const allCustomers = await db.select().from(customers);
  339. // Get recent transactions
  340. const recentTxns = await db
  341. .select()
  342. .from(transactions)
  343. .orderBy(desc(transactions.date))
  344. .limit(5);
  345. // Group expenses by category
  346. const expensesByCategory = approvedExpenses.reduce(
  347. (acc, e) => {
  348. acc[e.category] = (acc[e.category] || 0) + parseFloat(e.amount as string);
  349. return acc;
  350. },
  351. {} as Record<string, number>,
  352. );
  353. return {
  354. revenue: {
  355. total: totalRevenue,
  356. outstanding: outstandingInvoices,
  357. overdue: overdueAmount,
  358. },
  359. expenses: {
  360. total: totalExpenses,
  361. pending: allExpenses.filter((e) => e.status === "pending").length,
  362. },
  363. cashFlow,
  364. bankBalance: totalBankBalance,
  365. customers: {
  366. total: allCustomers.length,
  367. active: allCustomers.filter((c) => c.status === "active").length,
  368. },
  369. invoices: {
  370. total: allInvoices.length,
  371. draft: allInvoices.filter((i) => i.status === "draft").length,
  372. sent: allInvoices.filter((i) => i.status === "sent").length,
  373. paid: allInvoices.filter((i) => i.status === "paid").length,
  374. overdue: allInvoices.filter((i) => i.status === "overdue").length,
  375. },
  376. recentTransactions: recentTxns.map((t) => ({
  377. ...t,
  378. amount: parseFloat(t.amount as string),
  379. })),
  380. expensesByCategory: Object.entries(expensesByCategory).map(
  381. ([label, value]) => ({
  382. label,
  383. value,
  384. }),
  385. ),
  386. revenueByMonth: [
  387. { label: "Oct", value: 18000 },
  388. { label: "Nov", value: 22000 },
  389. { label: "Dec", value: 28000 },
  390. { label: "Jan", value: 35000 },
  391. { label: "Feb", value: totalRevenue },
  392. ],
  393. };
  394. }
  395. // Reports
  396. export async function getProfitLossReport(
  397. startDate?: string,
  398. endDate?: string,
  399. ) {
  400. const start = startDate ? new Date(startDate) : new Date("2024-01-01");
  401. const end = endDate ? new Date(endDate) : new Date("2024-12-31");
  402. const allInvoices = await db.select().from(invoices);
  403. const paidInvoices = allInvoices.filter(
  404. (i) => i.status === "paid" && i.createdAt >= start && i.createdAt <= end,
  405. );
  406. const allExpenses = await db.select().from(expenses);
  407. const approvedExpenses = allExpenses.filter(
  408. (e) => e.status === "approved" && e.date >= start && e.date <= end,
  409. );
  410. const totalIncome = paidInvoices.reduce(
  411. (sum, i) => sum + parseFloat(i.amount as string),
  412. 0,
  413. );
  414. const totalExpensesAmount = approvedExpenses.reduce(
  415. (sum, e) => sum + parseFloat(e.amount as string),
  416. 0,
  417. );
  418. const netProfit = totalIncome - totalExpensesAmount;
  419. const expensesByCategory = approvedExpenses.reduce(
  420. (acc, e) => {
  421. acc[e.category] = (acc[e.category] || 0) + parseFloat(e.amount as string);
  422. return acc;
  423. },
  424. {} as Record<string, number>,
  425. );
  426. return {
  427. period: {
  428. startDate: start.toISOString().split("T")[0],
  429. endDate: end.toISOString().split("T")[0],
  430. },
  431. income: {
  432. total: totalIncome,
  433. invoiceCount: paidInvoices.length,
  434. },
  435. expenses: {
  436. total: totalExpensesAmount,
  437. expenseCount: approvedExpenses.length,
  438. byCategory: Object.entries(expensesByCategory).map(
  439. ([category, amount]) => ({
  440. category,
  441. amount,
  442. }),
  443. ),
  444. },
  445. netProfit,
  446. profitMargin: totalIncome > 0 ? (netProfit / totalIncome) * 100 : 0,
  447. };
  448. }
  449. // Clear database (delete all data)
  450. export async function clearDatabase() {
  451. await db.delete(transactions);
  452. await db.delete(invoices);
  453. await db.delete(expenses);
  454. await db.delete(customers);
  455. await db.delete(accounts);
  456. return { message: "Database cleared successfully" };
  457. }
  458. // Reset/seed database
  459. export async function resetDatabase() {
  460. // Delete all data
  461. await clearDatabase();
  462. // Seed accounts first
  463. await db.insert(accounts).values([
  464. {
  465. id: "acc-001",
  466. name: "Business Checking",
  467. type: "bank",
  468. balance: "125000",
  469. lastSync: new Date(),
  470. },
  471. {
  472. id: "acc-002",
  473. name: "Business Savings",
  474. type: "bank",
  475. balance: "75000",
  476. lastSync: new Date(),
  477. },
  478. {
  479. id: "acc-003",
  480. name: "Corporate Card",
  481. type: "credit_card",
  482. balance: "-8500",
  483. lastSync: new Date(),
  484. },
  485. {
  486. id: "acc-004",
  487. name: "Petty Cash",
  488. type: "cash",
  489. balance: "500",
  490. lastSync: new Date(),
  491. },
  492. ]);
  493. // Seed customers with dates spread over past 30 days
  494. const now = new Date();
  495. const daysAgo = (days: number) =>
  496. new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
  497. const seedCustomers = [
  498. // Enterprise customers (older)
  499. {
  500. id: "cust-001",
  501. name: "Acme Corporation",
  502. email: "billing@acme.com",
  503. phone: "(555) 123-4567",
  504. balance: "15000",
  505. status: "active" as const,
  506. createdAt: daysAgo(28),
  507. },
  508. {
  509. id: "cust-002",
  510. name: "Globex Industries",
  511. email: "ap@globex.com",
  512. phone: "(555) 234-5678",
  513. balance: "8500",
  514. status: "active" as const,
  515. createdAt: daysAgo(27),
  516. },
  517. {
  518. id: "cust-003",
  519. name: "Initech LLC",
  520. email: "finance@initech.com",
  521. phone: "(555) 345-6789",
  522. balance: "2300",
  523. status: "active" as const,
  524. createdAt: daysAgo(25),
  525. },
  526. {
  527. id: "cust-004",
  528. name: "Umbrella Corp",
  529. email: "accounts@umbrella.com",
  530. phone: "(555) 456-7890",
  531. balance: "45000",
  532. status: "active" as const,
  533. createdAt: daysAgo(24),
  534. },
  535. {
  536. id: "cust-005",
  537. name: "Stark Industries",
  538. email: "billing@stark.com",
  539. phone: "(555) 567-8901",
  540. balance: "0",
  541. status: "inactive" as const,
  542. createdAt: daysAgo(23),
  543. },
  544. // Week 3 signups
  545. {
  546. id: "cust-006",
  547. name: "Wayne Enterprises",
  548. email: "finance@wayne.com",
  549. phone: "(555) 678-9012",
  550. balance: "32000",
  551. status: "active" as const,
  552. createdAt: daysAgo(21),
  553. },
  554. {
  555. id: "cust-007",
  556. name: "Oscorp",
  557. email: "billing@oscorp.com",
  558. phone: "(555) 789-0123",
  559. balance: "12500",
  560. status: "active" as const,
  561. createdAt: daysAgo(20),
  562. },
  563. {
  564. id: "cust-008",
  565. name: "LexCorp",
  566. email: "accounts@lexcorp.com",
  567. phone: "(555) 890-1234",
  568. balance: "67000",
  569. status: "active" as const,
  570. createdAt: daysAgo(19),
  571. },
  572. {
  573. id: "cust-009",
  574. name: "Cyberdyne Systems",
  575. email: "ap@cyberdyne.com",
  576. phone: "(555) 901-2345",
  577. balance: "4500",
  578. status: "active" as const,
  579. createdAt: daysAgo(18),
  580. },
  581. {
  582. id: "cust-010",
  583. name: "Weyland-Yutani",
  584. email: "billing@weyland.com",
  585. phone: "(555) 012-3456",
  586. balance: "89000",
  587. status: "active" as const,
  588. createdAt: daysAgo(17),
  589. },
  590. // Week 2 signups (more activity)
  591. {
  592. id: "cust-011",
  593. name: "Tyrell Corp",
  594. email: "finance@tyrell.com",
  595. phone: "(555) 111-2222",
  596. balance: "23000",
  597. status: "active" as const,
  598. createdAt: daysAgo(14),
  599. },
  600. {
  601. id: "cust-012",
  602. name: "Soylent Corp",
  603. email: "billing@soylent.com",
  604. phone: "(555) 222-3333",
  605. balance: "5600",
  606. status: "active" as const,
  607. createdAt: daysAgo(14),
  608. },
  609. {
  610. id: "cust-013",
  611. name: "Massive Dynamic",
  612. email: "accounts@massive.com",
  613. phone: "(555) 333-4444",
  614. balance: "41000",
  615. status: "active" as const,
  616. createdAt: daysAgo(13),
  617. },
  618. {
  619. id: "cust-014",
  620. name: "Aperture Science",
  621. email: "billing@aperture.com",
  622. phone: "(555) 444-5555",
  623. balance: "0",
  624. status: "inactive" as const,
  625. createdAt: daysAgo(12),
  626. },
  627. {
  628. id: "cust-015",
  629. name: "Black Mesa",
  630. email: "finance@blackmesa.com",
  631. phone: "(555) 555-6666",
  632. balance: "18500",
  633. status: "active" as const,
  634. createdAt: daysAgo(11),
  635. },
  636. {
  637. id: "cust-016",
  638. name: "Vault-Tec",
  639. email: "sales@vaulttec.com",
  640. phone: "(555) 666-7777",
  641. balance: "72000",
  642. status: "active" as const,
  643. createdAt: daysAgo(10),
  644. },
  645. {
  646. id: "cust-017",
  647. name: "Abstergo Industries",
  648. email: "billing@abstergo.com",
  649. phone: "(555) 777-8888",
  650. balance: "9800",
  651. status: "active" as const,
  652. createdAt: daysAgo(10),
  653. },
  654. {
  655. id: "cust-018",
  656. name: "Shinra Electric",
  657. email: "accounts@shinra.com",
  658. phone: "(555) 888-9999",
  659. balance: "125000",
  660. status: "active" as const,
  661. createdAt: daysAgo(9),
  662. },
  663. // Week 1 signups (high activity)
  664. {
  665. id: "cust-019",
  666. name: "Monsters Inc",
  667. email: "billing@monsters.com",
  668. phone: "(555) 999-0000",
  669. balance: "34500",
  670. status: "active" as const,
  671. createdAt: daysAgo(7),
  672. },
  673. {
  674. id: "cust-020",
  675. name: "Buy n Large",
  676. email: "finance@bnl.com",
  677. phone: "(555) 000-1111",
  678. balance: "56000",
  679. status: "active" as const,
  680. createdAt: daysAgo(7),
  681. },
  682. {
  683. id: "cust-021",
  684. name: "Omni Consumer",
  685. email: "ap@omni.com",
  686. phone: "(555) 121-2121",
  687. balance: "8900",
  688. status: "active" as const,
  689. createdAt: daysAgo(6),
  690. },
  691. {
  692. id: "cust-022",
  693. name: "Rekall Inc",
  694. email: "billing@rekall.com",
  695. phone: "(555) 131-3131",
  696. balance: "15600",
  697. status: "active" as const,
  698. createdAt: daysAgo(5),
  699. },
  700. {
  701. id: "cust-023",
  702. name: "Virtucon",
  703. email: "accounts@virtucon.com",
  704. phone: "(555) 141-4141",
  705. balance: "28000",
  706. status: "active" as const,
  707. createdAt: daysAgo(5),
  708. },
  709. {
  710. id: "cust-024",
  711. name: "Wonka Industries",
  712. email: "finance@wonka.com",
  713. phone: "(555) 151-5151",
  714. balance: "47000",
  715. status: "active" as const,
  716. createdAt: daysAgo(4),
  717. },
  718. {
  719. id: "cust-025",
  720. name: "Dunder Mifflin",
  721. email: "sales@dundermifflin.com",
  722. phone: "(555) 161-6161",
  723. balance: "3200",
  724. status: "active" as const,
  725. createdAt: daysAgo(4),
  726. },
  727. {
  728. id: "cust-026",
  729. name: "Sterling Cooper",
  730. email: "billing@sterlingcooper.com",
  731. phone: "(555) 171-7171",
  732. balance: "19500",
  733. status: "active" as const,
  734. createdAt: daysAgo(3),
  735. },
  736. // Recent signups (last few days)
  737. {
  738. id: "cust-027",
  739. name: "Prestige Worldwide",
  740. email: "boats@prestige.com",
  741. phone: "(555) 181-8181",
  742. balance: "0",
  743. status: "active" as const,
  744. createdAt: daysAgo(2),
  745. },
  746. {
  747. id: "cust-028",
  748. name: "Pied Piper",
  749. email: "richard@piedpiper.com",
  750. phone: "(555) 191-9191",
  751. balance: "500",
  752. status: "active" as const,
  753. createdAt: daysAgo(2),
  754. },
  755. {
  756. id: "cust-029",
  757. name: "Hooli",
  758. email: "billing@hooli.com",
  759. phone: "(555) 202-0202",
  760. balance: "250000",
  761. status: "active" as const,
  762. createdAt: daysAgo(1),
  763. },
  764. {
  765. id: "cust-030",
  766. name: "Aviato",
  767. email: "erlich@aviato.com",
  768. phone: "(555) 212-1212",
  769. balance: "1200",
  770. status: "active" as const,
  771. createdAt: daysAgo(1),
  772. },
  773. {
  774. id: "cust-031",
  775. name: "Bluth Company",
  776. email: "gob@bluth.com",
  777. phone: "(555) 222-2222",
  778. balance: "0",
  779. status: "inactive" as const,
  780. createdAt: daysAgo(0),
  781. },
  782. {
  783. id: "cust-032",
  784. name: "Los Pollos Hermanos",
  785. email: "gus@lospollos.com",
  786. phone: "(555) 232-3232",
  787. balance: "98000",
  788. status: "active" as const,
  789. createdAt: daysAgo(0),
  790. },
  791. ];
  792. await db.insert(customers).values(seedCustomers);
  793. // Seed invoices
  794. await db.insert(invoices).values([
  795. {
  796. id: "inv-001",
  797. customerId: "cust-001",
  798. customerName: "Acme Corporation",
  799. amount: "5000",
  800. status: "paid",
  801. dueDate: new Date("2024-02-15"),
  802. items: [
  803. {
  804. description: "Consulting Services",
  805. quantity: 20,
  806. rate: 250,
  807. amount: 5000,
  808. },
  809. ],
  810. },
  811. {
  812. id: "inv-002",
  813. customerId: "cust-002",
  814. customerName: "Globex Industries",
  815. amount: "3500",
  816. status: "sent",
  817. dueDate: new Date("2024-02-28"),
  818. items: [
  819. {
  820. description: "Software License",
  821. quantity: 7,
  822. rate: 500,
  823. amount: 3500,
  824. },
  825. ],
  826. },
  827. {
  828. id: "inv-003",
  829. customerId: "cust-001",
  830. customerName: "Acme Corporation",
  831. amount: "10000",
  832. status: "overdue",
  833. dueDate: new Date("2024-01-31"),
  834. items: [
  835. {
  836. description: "Annual Support Contract",
  837. quantity: 1,
  838. rate: 10000,
  839. amount: 10000,
  840. },
  841. ],
  842. },
  843. {
  844. id: "inv-004",
  845. customerId: "cust-003",
  846. customerName: "Initech LLC",
  847. amount: "2300",
  848. status: "draft",
  849. dueDate: new Date("2024-03-15"),
  850. items: [
  851. {
  852. description: "Training Session",
  853. quantity: 1,
  854. rate: 2300,
  855. amount: 2300,
  856. },
  857. ],
  858. },
  859. {
  860. id: "inv-005",
  861. customerId: "cust-004",
  862. customerName: "Umbrella Corp",
  863. amount: "45000",
  864. status: "sent",
  865. dueDate: new Date("2024-03-01"),
  866. items: [
  867. {
  868. description: "Enterprise License",
  869. quantity: 1,
  870. rate: 45000,
  871. amount: 45000,
  872. },
  873. ],
  874. },
  875. ]);
  876. // Seed expenses
  877. await db.insert(expenses).values([
  878. {
  879. id: "exp-001",
  880. vendor: "AWS",
  881. category: "Software",
  882. amount: "2500",
  883. date: new Date("2024-02-01"),
  884. status: "approved",
  885. description: "Cloud hosting - February",
  886. },
  887. {
  888. id: "exp-002",
  889. vendor: "Office Depot",
  890. category: "Office Supplies",
  891. amount: "350",
  892. date: new Date("2024-02-05"),
  893. status: "approved",
  894. description: "Printer paper and toner",
  895. },
  896. {
  897. id: "exp-003",
  898. vendor: "Delta Airlines",
  899. category: "Travel",
  900. amount: "890",
  901. date: new Date("2024-02-10"),
  902. status: "pending",
  903. description: "Flight to client meeting",
  904. },
  905. {
  906. id: "exp-004",
  907. vendor: "WeWork",
  908. category: "Rent",
  909. amount: "4500",
  910. date: new Date("2024-02-01"),
  911. status: "approved",
  912. description: "Office space - February",
  913. },
  914. {
  915. id: "exp-005",
  916. vendor: "Uber",
  917. category: "Travel",
  918. amount: "125",
  919. date: new Date("2024-02-12"),
  920. status: "pending",
  921. description: "Client site visits",
  922. },
  923. ]);
  924. // Seed transactions
  925. await db.insert(transactions).values([
  926. {
  927. id: "txn-001",
  928. accountId: "acc-001",
  929. type: "income",
  930. amount: "5000",
  931. description: "Invoice #inv-001 payment",
  932. category: "Sales",
  933. date: new Date("2024-02-10"),
  934. },
  935. {
  936. id: "txn-002",
  937. accountId: "acc-001",
  938. type: "expense",
  939. amount: "2500",
  940. description: "AWS hosting",
  941. category: "Software",
  942. date: new Date("2024-02-01"),
  943. },
  944. {
  945. id: "txn-003",
  946. accountId: "acc-003",
  947. type: "expense",
  948. amount: "350",
  949. description: "Office supplies",
  950. category: "Office",
  951. date: new Date("2024-02-05"),
  952. },
  953. {
  954. id: "txn-004",
  955. accountId: "acc-001",
  956. type: "income",
  957. amount: "15000",
  958. description: "Consulting retainer",
  959. category: "Sales",
  960. date: new Date("2024-02-01"),
  961. },
  962. {
  963. id: "txn-005",
  964. accountId: "acc-001",
  965. type: "expense",
  966. amount: "4500",
  967. description: "Office rent",
  968. category: "Rent",
  969. date: new Date("2024-02-01"),
  970. },
  971. ]);
  972. return { message: "Database reset and seeded successfully" };
  973. }
  974. // Widget methods
  975. export async function getWidgets() {
  976. return db.select().from(widgets).orderBy(asc(widgets.order));
  977. }
  978. export async function getWidget(id: string) {
  979. const result = await db
  980. .select()
  981. .from(widgets)
  982. .where(eq(widgets.id, id))
  983. .limit(1);
  984. return result[0] || null;
  985. }
  986. export async function createWidget(data: { prompt: string; spec: WidgetSpec }) {
  987. const id = generateId("widget");
  988. // Get the next order value
  989. const [result] = await db
  990. .select({ maxOrder: max(widgets.order) })
  991. .from(widgets);
  992. const nextOrder = (result?.maxOrder ?? -1) + 1;
  993. const [widget] = await db
  994. .insert(widgets)
  995. .values({
  996. id,
  997. prompt: data.prompt,
  998. spec: data.spec,
  999. order: nextOrder,
  1000. })
  1001. .returning();
  1002. return widget;
  1003. }
  1004. export async function updateWidget(
  1005. id: string,
  1006. data: { prompt?: string; spec?: WidgetSpec },
  1007. ) {
  1008. const [widget] = await db
  1009. .update(widgets)
  1010. .set({ ...data, updatedAt: new Date() })
  1011. .where(eq(widgets.id, id))
  1012. .returning();
  1013. return widget || null;
  1014. }
  1015. export async function deleteWidget(id: string) {
  1016. const result = await db
  1017. .delete(widgets)
  1018. .where(eq(widgets.id, id))
  1019. .returning({ id: widgets.id });
  1020. return result.length > 0;
  1021. }
  1022. export async function reorderWidgets(orderedIds: string[]) {
  1023. // Update each widget's order based on position in array
  1024. for (let i = 0; i < orderedIds.length; i++) {
  1025. const id = orderedIds[i];
  1026. if (!id) continue;
  1027. await db.update(widgets).set({ order: i }).where(eq(widgets.id, id));
  1028. }
  1029. }