mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-10-21 06:30:33 +08:00
* add rate limits
* fix lint
* better
generate prompt
update examples
* fixes
* fixes
* new api
* update docs
* fixes
* fixes
* generated prompt
* Add tests for catalog validation and prompt generation
- Test generateSystemPrompt with components, actions, custom rules
- Test new defineCatalog API from schema system
- Test catalog.prompt() method with custom rules
- Test catalog.validate() for valid and invalid specs
- Test catalog.zodSchema() for custom validation
- Test catalog.jsonSchema() for structured outputs
- Add tests for nested specs with children
- Add tests for rejecting invalid component types
* Fix lint: pass children as nested elements in renderer
Change from children={...} prop to nested children pattern
to satisfy react/no-children-prop rule.
* Fix lint errors in dashboard and web apps
- Disable react/prop-types in both eslint configs (TypeScript handles this)
- Allow styled-jsx 'jsx' property in dashboard
- Add DATABASE_URL to turbo env allowlist
- Remove unused drizzle-orm imports (and, sql)
- Suppress unused variable warnings where intentional
* Add server entry point for @json-render/remotion
The main package entry includes React components that require
client-side context (React.createContext). This causes build
failures when importing in server-side API routes.
Added `@json-render/remotion/server` entry point that exports
only schema and catalog definitions without React dependencies:
- schema, RemotionSchema, RemotionSpec
- standardComponentDefinitions, standardTransitionDefinitions
- standardEffectDefinitions
- Type exports for catalogs
Updated remotion example to import from /server in catalog.ts
* Make dashboard database connection lazy-initialized
The database connection threw an error at module load time if
DATABASE_URL was not set, causing builds to fail in CI where
the database is not available.
Changed to lazy initialization using a Proxy so the error only
occurs when the database is actually used at runtime, not at
build time.
* Fix previousSpec property name and lazy rate limiting
- Fix property name mismatch: playground now passes previousSpec
instead of previousTree to match what useUIStream hook expects
- Make rate limiting lazy-initialized to avoid runtime errors when
Redis env vars (KV_REST_API_URL, KV_REST_API_TOKEN) are not set
- Rate limiting gracefully becomes a no-op when Redis is unavailable
1110 lines
26 KiB
TypeScript
1110 lines
26 KiB
TypeScript
/**
|
|
* Database store for the dashboard API using Drizzle ORM
|
|
*/
|
|
|
|
import { eq, ilike, or, desc, asc, max } from "drizzle-orm";
|
|
import { db } from "./connection";
|
|
import {
|
|
customers,
|
|
invoices,
|
|
expenses,
|
|
accounts,
|
|
transactions,
|
|
widgets,
|
|
type Customer,
|
|
type Invoice,
|
|
type Expense,
|
|
type InvoiceItem,
|
|
type WidgetSpec,
|
|
} from "./schema";
|
|
|
|
// Helper to generate IDs
|
|
const generateId = (prefix: string) => `${prefix}-${Date.now()}`;
|
|
|
|
// Customer methods
|
|
export async function getCustomers(filters?: {
|
|
status?: string;
|
|
search?: string;
|
|
limit?: number;
|
|
sort?: "newest" | "oldest";
|
|
}) {
|
|
let query = db.select().from(customers);
|
|
|
|
if (filters?.status) {
|
|
query = query.where(
|
|
eq(customers.status, filters.status as "active" | "inactive"),
|
|
) as typeof query;
|
|
}
|
|
|
|
if (filters?.search) {
|
|
const searchPattern = `%${filters.search}%`;
|
|
query = query.where(
|
|
or(
|
|
ilike(customers.name, searchPattern),
|
|
ilike(customers.email, searchPattern),
|
|
),
|
|
) as typeof query;
|
|
}
|
|
|
|
// Default to newest first
|
|
const sortOrder = filters?.sort === "oldest" ? asc : desc;
|
|
query = query.orderBy(sortOrder(customers.createdAt)) as typeof query;
|
|
|
|
if (filters?.limit) {
|
|
query = query.limit(filters.limit) as typeof query;
|
|
}
|
|
|
|
return query;
|
|
}
|
|
|
|
export async function getCustomer(id: string) {
|
|
const result = await db
|
|
.select()
|
|
.from(customers)
|
|
.where(eq(customers.id, id))
|
|
.limit(1);
|
|
return result[0] || null;
|
|
}
|
|
|
|
export async function createCustomer(data: {
|
|
name: string;
|
|
email: string;
|
|
phone?: string;
|
|
}) {
|
|
const id = generateId("cust");
|
|
const [customer] = await db
|
|
.insert(customers)
|
|
.values({
|
|
id,
|
|
name: data.name,
|
|
email: data.email,
|
|
phone: data.phone || null,
|
|
balance: "0",
|
|
status: "active",
|
|
})
|
|
.returning();
|
|
return customer;
|
|
}
|
|
|
|
export async function updateCustomer(id: string, data: Partial<Customer>) {
|
|
const [customer] = await db
|
|
.update(customers)
|
|
.set(data)
|
|
.where(eq(customers.id, id))
|
|
.returning();
|
|
return customer || null;
|
|
}
|
|
|
|
export async function deleteCustomer(id: string) {
|
|
const result = await db
|
|
.delete(customers)
|
|
.where(eq(customers.id, id))
|
|
.returning({ id: customers.id });
|
|
return { success: result.length > 0 };
|
|
}
|
|
|
|
// Invoice methods
|
|
export async function getInvoices(filters?: {
|
|
status?: string;
|
|
customerId?: string;
|
|
}) {
|
|
let query = db.select().from(invoices);
|
|
|
|
if (filters?.status) {
|
|
query = query.where(
|
|
eq(invoices.status, filters.status as Invoice["status"]),
|
|
) as typeof query;
|
|
}
|
|
|
|
if (filters?.customerId) {
|
|
query = query.where(
|
|
eq(invoices.customerId, filters.customerId),
|
|
) as typeof query;
|
|
}
|
|
|
|
return query;
|
|
}
|
|
|
|
export async function getInvoice(id: string) {
|
|
const result = await db
|
|
.select()
|
|
.from(invoices)
|
|
.where(eq(invoices.id, id))
|
|
.limit(1);
|
|
return result[0] || null;
|
|
}
|
|
|
|
export async function createInvoice(data: {
|
|
customerId: string;
|
|
dueDate: string;
|
|
items?: InvoiceItem[];
|
|
}) {
|
|
const customer = await getCustomer(data.customerId);
|
|
if (!customer) return null;
|
|
|
|
const items = data.items || [];
|
|
const amount = items.reduce((sum, item) => sum + item.amount, 0);
|
|
|
|
const id = generateId("inv");
|
|
const [invoice] = await db
|
|
.insert(invoices)
|
|
.values({
|
|
id,
|
|
customerId: data.customerId,
|
|
customerName: customer.name,
|
|
amount: amount.toString(),
|
|
status: "draft",
|
|
dueDate: new Date(data.dueDate),
|
|
items,
|
|
})
|
|
.returning();
|
|
return invoice;
|
|
}
|
|
|
|
export async function updateInvoice(id: string, data: Partial<Invoice>) {
|
|
const [invoice] = await db
|
|
.update(invoices)
|
|
.set(data)
|
|
.where(eq(invoices.id, id))
|
|
.returning();
|
|
return invoice || null;
|
|
}
|
|
|
|
export async function deleteInvoice(id: string) {
|
|
const result = await db
|
|
.delete(invoices)
|
|
.where(eq(invoices.id, id))
|
|
.returning({ id: invoices.id });
|
|
return result.length > 0;
|
|
}
|
|
|
|
export async function sendInvoice(id: string) {
|
|
const invoice = await getInvoice(id);
|
|
if (!invoice) return { error: "Invoice not found" };
|
|
if (invoice.status !== "draft")
|
|
return { error: "Only draft invoices can be sent" };
|
|
return updateInvoice(id, { status: "sent" });
|
|
}
|
|
|
|
export async function markInvoicePaid(id: string) {
|
|
const invoice = await getInvoice(id);
|
|
if (!invoice) return { error: "Invoice not found" };
|
|
if (invoice.status === "paid") return { error: "Invoice is already paid" };
|
|
|
|
// Update customer balance
|
|
const customer = await getCustomer(invoice.customerId);
|
|
if (customer) {
|
|
const newBalance =
|
|
parseFloat(customer.balance as string) -
|
|
parseFloat(invoice.amount as string);
|
|
await updateCustomer(invoice.customerId, {
|
|
balance: newBalance.toString(),
|
|
});
|
|
}
|
|
|
|
// Add transaction
|
|
await db.insert(transactions).values({
|
|
id: generateId("txn"),
|
|
accountId: "acc-001",
|
|
type: "income",
|
|
amount: invoice.amount,
|
|
description: `Invoice #${invoice.id} payment from ${invoice.customerName}`,
|
|
category: "Sales",
|
|
date: new Date(),
|
|
});
|
|
|
|
return updateInvoice(id, { status: "paid" });
|
|
}
|
|
|
|
// Expense methods
|
|
export async function getExpenses(filters?: {
|
|
status?: string;
|
|
category?: string;
|
|
}) {
|
|
let query = db.select().from(expenses);
|
|
|
|
if (filters?.status) {
|
|
query = query.where(
|
|
eq(expenses.status, filters.status as Expense["status"]),
|
|
) as typeof query;
|
|
}
|
|
|
|
if (filters?.category) {
|
|
query = query.where(
|
|
eq(expenses.category, filters.category),
|
|
) as typeof query;
|
|
}
|
|
|
|
return query;
|
|
}
|
|
|
|
export async function getExpense(id: string) {
|
|
const result = await db
|
|
.select()
|
|
.from(expenses)
|
|
.where(eq(expenses.id, id))
|
|
.limit(1);
|
|
return result[0] || null;
|
|
}
|
|
|
|
export async function createExpense(data: {
|
|
vendor: string;
|
|
category: string;
|
|
amount: number;
|
|
date?: string;
|
|
description?: string;
|
|
}) {
|
|
const id = generateId("exp");
|
|
const [expense] = await db
|
|
.insert(expenses)
|
|
.values({
|
|
id,
|
|
vendor: data.vendor,
|
|
category: data.category,
|
|
amount: data.amount.toString(),
|
|
date: data.date ? new Date(data.date) : new Date(),
|
|
status: "pending",
|
|
description: data.description || null,
|
|
})
|
|
.returning();
|
|
return expense;
|
|
}
|
|
|
|
export async function updateExpense(id: string, data: Partial<Expense>) {
|
|
const [expense] = await db
|
|
.update(expenses)
|
|
.set(data)
|
|
.where(eq(expenses.id, id))
|
|
.returning();
|
|
return expense || null;
|
|
}
|
|
|
|
export async function deleteExpense(id: string) {
|
|
const result = await db
|
|
.delete(expenses)
|
|
.where(eq(expenses.id, id))
|
|
.returning({ id: expenses.id });
|
|
return result.length > 0;
|
|
}
|
|
|
|
export async function approveExpense(id: string) {
|
|
const expense = await getExpense(id);
|
|
if (!expense) return { error: "Expense not found" };
|
|
if (expense.status !== "pending")
|
|
return { error: "Only pending expenses can be approved" };
|
|
|
|
// Add transaction
|
|
await db.insert(transactions).values({
|
|
id: generateId("txn"),
|
|
accountId: "acc-001",
|
|
type: "expense",
|
|
amount: expense.amount,
|
|
description: `${expense.vendor} - ${expense.description || ""}`,
|
|
category: expense.category,
|
|
date: expense.date,
|
|
});
|
|
|
|
return updateExpense(id, { status: "approved" });
|
|
}
|
|
|
|
export async function rejectExpense(id: string) {
|
|
const expense = await getExpense(id);
|
|
if (!expense) return { error: "Expense not found" };
|
|
if (expense.status !== "pending")
|
|
return { error: "Only pending expenses can be rejected" };
|
|
return updateExpense(id, { status: "rejected" });
|
|
}
|
|
|
|
// Account methods
|
|
export async function getAccounts() {
|
|
const accountList = await db.select().from(accounts);
|
|
|
|
// Get recent transactions for each account
|
|
const result = await Promise.all(
|
|
accountList.map(async (account) => {
|
|
const recentTxns = await db
|
|
.select()
|
|
.from(transactions)
|
|
.where(eq(transactions.accountId, account.id))
|
|
.orderBy(desc(transactions.date))
|
|
.limit(5);
|
|
return { ...account, recentTransactions: recentTxns };
|
|
}),
|
|
);
|
|
|
|
return result;
|
|
}
|
|
|
|
export async function getAccount(id: string) {
|
|
const result = await db
|
|
.select()
|
|
.from(accounts)
|
|
.where(eq(accounts.id, id))
|
|
.limit(1);
|
|
|
|
if (!result[0]) return null;
|
|
|
|
const txns = await db
|
|
.select()
|
|
.from(transactions)
|
|
.where(eq(transactions.accountId, id))
|
|
.orderBy(desc(transactions.date));
|
|
|
|
return { ...result[0], transactions: txns };
|
|
}
|
|
|
|
// Dashboard summary
|
|
export async function getDashboardSummary() {
|
|
// Get all invoices
|
|
const allInvoices = await db.select().from(invoices);
|
|
const paidInvoices = allInvoices.filter((i) => i.status === "paid");
|
|
const totalRevenue = paidInvoices.reduce(
|
|
(sum, i) => sum + parseFloat(i.amount as string),
|
|
0,
|
|
);
|
|
|
|
const outstandingInvoices = allInvoices
|
|
.filter((i) => i.status === "sent" || i.status === "overdue")
|
|
.reduce((sum, i) => sum + parseFloat(i.amount as string), 0);
|
|
|
|
const overdueAmount = allInvoices
|
|
.filter((i) => i.status === "overdue")
|
|
.reduce((sum, i) => sum + parseFloat(i.amount as string), 0);
|
|
|
|
// Get all expenses
|
|
const allExpenses = await db.select().from(expenses);
|
|
const approvedExpenses = allExpenses.filter((e) => e.status === "approved");
|
|
const totalExpenses = approvedExpenses.reduce(
|
|
(sum, e) => sum + parseFloat(e.amount as string),
|
|
0,
|
|
);
|
|
|
|
const cashFlow = totalRevenue - totalExpenses;
|
|
|
|
// Get accounts
|
|
const allAccounts = await db.select().from(accounts);
|
|
const totalBankBalance = allAccounts
|
|
.filter((a) => a.type === "bank")
|
|
.reduce((sum, a) => sum + parseFloat(a.balance as string), 0);
|
|
|
|
// Get customers
|
|
const allCustomers = await db.select().from(customers);
|
|
|
|
// Get recent transactions
|
|
const recentTxns = await db
|
|
.select()
|
|
.from(transactions)
|
|
.orderBy(desc(transactions.date))
|
|
.limit(5);
|
|
|
|
// Group expenses by category
|
|
const expensesByCategory = approvedExpenses.reduce(
|
|
(acc, e) => {
|
|
acc[e.category] = (acc[e.category] || 0) + parseFloat(e.amount as string);
|
|
return acc;
|
|
},
|
|
{} as Record<string, number>,
|
|
);
|
|
|
|
return {
|
|
revenue: {
|
|
total: totalRevenue,
|
|
outstanding: outstandingInvoices,
|
|
overdue: overdueAmount,
|
|
},
|
|
expenses: {
|
|
total: totalExpenses,
|
|
pending: allExpenses.filter((e) => e.status === "pending").length,
|
|
},
|
|
cashFlow,
|
|
bankBalance: totalBankBalance,
|
|
customers: {
|
|
total: allCustomers.length,
|
|
active: allCustomers.filter((c) => c.status === "active").length,
|
|
},
|
|
invoices: {
|
|
total: allInvoices.length,
|
|
draft: allInvoices.filter((i) => i.status === "draft").length,
|
|
sent: allInvoices.filter((i) => i.status === "sent").length,
|
|
paid: allInvoices.filter((i) => i.status === "paid").length,
|
|
overdue: allInvoices.filter((i) => i.status === "overdue").length,
|
|
},
|
|
recentTransactions: recentTxns.map((t) => ({
|
|
...t,
|
|
amount: parseFloat(t.amount as string),
|
|
})),
|
|
expensesByCategory: Object.entries(expensesByCategory).map(
|
|
([label, value]) => ({
|
|
label,
|
|
value,
|
|
}),
|
|
),
|
|
revenueByMonth: [
|
|
{ label: "Oct", value: 18000 },
|
|
{ label: "Nov", value: 22000 },
|
|
{ label: "Dec", value: 28000 },
|
|
{ label: "Jan", value: 35000 },
|
|
{ label: "Feb", value: totalRevenue },
|
|
],
|
|
};
|
|
}
|
|
|
|
// Reports
|
|
export async function getProfitLossReport(
|
|
startDate?: string,
|
|
endDate?: string,
|
|
) {
|
|
const start = startDate ? new Date(startDate) : new Date("2024-01-01");
|
|
const end = endDate ? new Date(endDate) : new Date("2024-12-31");
|
|
|
|
const allInvoices = await db.select().from(invoices);
|
|
const paidInvoices = allInvoices.filter(
|
|
(i) => i.status === "paid" && i.createdAt >= start && i.createdAt <= end,
|
|
);
|
|
|
|
const allExpenses = await db.select().from(expenses);
|
|
const approvedExpenses = allExpenses.filter(
|
|
(e) => e.status === "approved" && e.date >= start && e.date <= end,
|
|
);
|
|
|
|
const totalIncome = paidInvoices.reduce(
|
|
(sum, i) => sum + parseFloat(i.amount as string),
|
|
0,
|
|
);
|
|
const totalExpensesAmount = approvedExpenses.reduce(
|
|
(sum, e) => sum + parseFloat(e.amount as string),
|
|
0,
|
|
);
|
|
const netProfit = totalIncome - totalExpensesAmount;
|
|
|
|
const expensesByCategory = approvedExpenses.reduce(
|
|
(acc, e) => {
|
|
acc[e.category] = (acc[e.category] || 0) + parseFloat(e.amount as string);
|
|
return acc;
|
|
},
|
|
{} as Record<string, number>,
|
|
);
|
|
|
|
return {
|
|
period: {
|
|
startDate: start.toISOString().split("T")[0],
|
|
endDate: end.toISOString().split("T")[0],
|
|
},
|
|
income: {
|
|
total: totalIncome,
|
|
invoiceCount: paidInvoices.length,
|
|
},
|
|
expenses: {
|
|
total: totalExpensesAmount,
|
|
expenseCount: approvedExpenses.length,
|
|
byCategory: Object.entries(expensesByCategory).map(
|
|
([category, amount]) => ({
|
|
category,
|
|
amount,
|
|
}),
|
|
),
|
|
},
|
|
netProfit,
|
|
profitMargin: totalIncome > 0 ? (netProfit / totalIncome) * 100 : 0,
|
|
};
|
|
}
|
|
|
|
// Clear database (delete all data)
|
|
export async function clearDatabase() {
|
|
await db.delete(transactions);
|
|
await db.delete(invoices);
|
|
await db.delete(expenses);
|
|
await db.delete(customers);
|
|
await db.delete(accounts);
|
|
return { message: "Database cleared successfully" };
|
|
}
|
|
|
|
// Reset/seed database
|
|
export async function resetDatabase() {
|
|
// Delete all data
|
|
await clearDatabase();
|
|
|
|
// Seed accounts first
|
|
await db.insert(accounts).values([
|
|
{
|
|
id: "acc-001",
|
|
name: "Business Checking",
|
|
type: "bank",
|
|
balance: "125000",
|
|
lastSync: new Date(),
|
|
},
|
|
{
|
|
id: "acc-002",
|
|
name: "Business Savings",
|
|
type: "bank",
|
|
balance: "75000",
|
|
lastSync: new Date(),
|
|
},
|
|
{
|
|
id: "acc-003",
|
|
name: "Corporate Card",
|
|
type: "credit_card",
|
|
balance: "-8500",
|
|
lastSync: new Date(),
|
|
},
|
|
{
|
|
id: "acc-004",
|
|
name: "Petty Cash",
|
|
type: "cash",
|
|
balance: "500",
|
|
lastSync: new Date(),
|
|
},
|
|
]);
|
|
|
|
// Seed customers with dates spread over past 30 days
|
|
const now = new Date();
|
|
const daysAgo = (days: number) =>
|
|
new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
|
|
|
|
const seedCustomers = [
|
|
// Enterprise customers (older)
|
|
{
|
|
id: "cust-001",
|
|
name: "Acme Corporation",
|
|
email: "billing@acme.com",
|
|
phone: "(555) 123-4567",
|
|
balance: "15000",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(28),
|
|
},
|
|
{
|
|
id: "cust-002",
|
|
name: "Globex Industries",
|
|
email: "ap@globex.com",
|
|
phone: "(555) 234-5678",
|
|
balance: "8500",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(27),
|
|
},
|
|
{
|
|
id: "cust-003",
|
|
name: "Initech LLC",
|
|
email: "finance@initech.com",
|
|
phone: "(555) 345-6789",
|
|
balance: "2300",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(25),
|
|
},
|
|
{
|
|
id: "cust-004",
|
|
name: "Umbrella Corp",
|
|
email: "accounts@umbrella.com",
|
|
phone: "(555) 456-7890",
|
|
balance: "45000",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(24),
|
|
},
|
|
{
|
|
id: "cust-005",
|
|
name: "Stark Industries",
|
|
email: "billing@stark.com",
|
|
phone: "(555) 567-8901",
|
|
balance: "0",
|
|
status: "inactive" as const,
|
|
createdAt: daysAgo(23),
|
|
},
|
|
// Week 3 signups
|
|
{
|
|
id: "cust-006",
|
|
name: "Wayne Enterprises",
|
|
email: "finance@wayne.com",
|
|
phone: "(555) 678-9012",
|
|
balance: "32000",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(21),
|
|
},
|
|
{
|
|
id: "cust-007",
|
|
name: "Oscorp",
|
|
email: "billing@oscorp.com",
|
|
phone: "(555) 789-0123",
|
|
balance: "12500",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(20),
|
|
},
|
|
{
|
|
id: "cust-008",
|
|
name: "LexCorp",
|
|
email: "accounts@lexcorp.com",
|
|
phone: "(555) 890-1234",
|
|
balance: "67000",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(19),
|
|
},
|
|
{
|
|
id: "cust-009",
|
|
name: "Cyberdyne Systems",
|
|
email: "ap@cyberdyne.com",
|
|
phone: "(555) 901-2345",
|
|
balance: "4500",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(18),
|
|
},
|
|
{
|
|
id: "cust-010",
|
|
name: "Weyland-Yutani",
|
|
email: "billing@weyland.com",
|
|
phone: "(555) 012-3456",
|
|
balance: "89000",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(17),
|
|
},
|
|
// Week 2 signups (more activity)
|
|
{
|
|
id: "cust-011",
|
|
name: "Tyrell Corp",
|
|
email: "finance@tyrell.com",
|
|
phone: "(555) 111-2222",
|
|
balance: "23000",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(14),
|
|
},
|
|
{
|
|
id: "cust-012",
|
|
name: "Soylent Corp",
|
|
email: "billing@soylent.com",
|
|
phone: "(555) 222-3333",
|
|
balance: "5600",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(14),
|
|
},
|
|
{
|
|
id: "cust-013",
|
|
name: "Massive Dynamic",
|
|
email: "accounts@massive.com",
|
|
phone: "(555) 333-4444",
|
|
balance: "41000",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(13),
|
|
},
|
|
{
|
|
id: "cust-014",
|
|
name: "Aperture Science",
|
|
email: "billing@aperture.com",
|
|
phone: "(555) 444-5555",
|
|
balance: "0",
|
|
status: "inactive" as const,
|
|
createdAt: daysAgo(12),
|
|
},
|
|
{
|
|
id: "cust-015",
|
|
name: "Black Mesa",
|
|
email: "finance@blackmesa.com",
|
|
phone: "(555) 555-6666",
|
|
balance: "18500",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(11),
|
|
},
|
|
{
|
|
id: "cust-016",
|
|
name: "Vault-Tec",
|
|
email: "sales@vaulttec.com",
|
|
phone: "(555) 666-7777",
|
|
balance: "72000",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(10),
|
|
},
|
|
{
|
|
id: "cust-017",
|
|
name: "Abstergo Industries",
|
|
email: "billing@abstergo.com",
|
|
phone: "(555) 777-8888",
|
|
balance: "9800",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(10),
|
|
},
|
|
{
|
|
id: "cust-018",
|
|
name: "Shinra Electric",
|
|
email: "accounts@shinra.com",
|
|
phone: "(555) 888-9999",
|
|
balance: "125000",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(9),
|
|
},
|
|
// Week 1 signups (high activity)
|
|
{
|
|
id: "cust-019",
|
|
name: "Monsters Inc",
|
|
email: "billing@monsters.com",
|
|
phone: "(555) 999-0000",
|
|
balance: "34500",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(7),
|
|
},
|
|
{
|
|
id: "cust-020",
|
|
name: "Buy n Large",
|
|
email: "finance@bnl.com",
|
|
phone: "(555) 000-1111",
|
|
balance: "56000",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(7),
|
|
},
|
|
{
|
|
id: "cust-021",
|
|
name: "Omni Consumer",
|
|
email: "ap@omni.com",
|
|
phone: "(555) 121-2121",
|
|
balance: "8900",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(6),
|
|
},
|
|
{
|
|
id: "cust-022",
|
|
name: "Rekall Inc",
|
|
email: "billing@rekall.com",
|
|
phone: "(555) 131-3131",
|
|
balance: "15600",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(5),
|
|
},
|
|
{
|
|
id: "cust-023",
|
|
name: "Virtucon",
|
|
email: "accounts@virtucon.com",
|
|
phone: "(555) 141-4141",
|
|
balance: "28000",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(5),
|
|
},
|
|
{
|
|
id: "cust-024",
|
|
name: "Wonka Industries",
|
|
email: "finance@wonka.com",
|
|
phone: "(555) 151-5151",
|
|
balance: "47000",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(4),
|
|
},
|
|
{
|
|
id: "cust-025",
|
|
name: "Dunder Mifflin",
|
|
email: "sales@dundermifflin.com",
|
|
phone: "(555) 161-6161",
|
|
balance: "3200",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(4),
|
|
},
|
|
{
|
|
id: "cust-026",
|
|
name: "Sterling Cooper",
|
|
email: "billing@sterlingcooper.com",
|
|
phone: "(555) 171-7171",
|
|
balance: "19500",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(3),
|
|
},
|
|
// Recent signups (last few days)
|
|
{
|
|
id: "cust-027",
|
|
name: "Prestige Worldwide",
|
|
email: "boats@prestige.com",
|
|
phone: "(555) 181-8181",
|
|
balance: "0",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(2),
|
|
},
|
|
{
|
|
id: "cust-028",
|
|
name: "Pied Piper",
|
|
email: "richard@piedpiper.com",
|
|
phone: "(555) 191-9191",
|
|
balance: "500",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(2),
|
|
},
|
|
{
|
|
id: "cust-029",
|
|
name: "Hooli",
|
|
email: "billing@hooli.com",
|
|
phone: "(555) 202-0202",
|
|
balance: "250000",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(1),
|
|
},
|
|
{
|
|
id: "cust-030",
|
|
name: "Aviato",
|
|
email: "erlich@aviato.com",
|
|
phone: "(555) 212-1212",
|
|
balance: "1200",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(1),
|
|
},
|
|
{
|
|
id: "cust-031",
|
|
name: "Bluth Company",
|
|
email: "gob@bluth.com",
|
|
phone: "(555) 222-2222",
|
|
balance: "0",
|
|
status: "inactive" as const,
|
|
createdAt: daysAgo(0),
|
|
},
|
|
{
|
|
id: "cust-032",
|
|
name: "Los Pollos Hermanos",
|
|
email: "gus@lospollos.com",
|
|
phone: "(555) 232-3232",
|
|
balance: "98000",
|
|
status: "active" as const,
|
|
createdAt: daysAgo(0),
|
|
},
|
|
];
|
|
|
|
await db.insert(customers).values(seedCustomers);
|
|
|
|
// Seed invoices
|
|
await db.insert(invoices).values([
|
|
{
|
|
id: "inv-001",
|
|
customerId: "cust-001",
|
|
customerName: "Acme Corporation",
|
|
amount: "5000",
|
|
status: "paid",
|
|
dueDate: new Date("2024-02-15"),
|
|
items: [
|
|
{
|
|
description: "Consulting Services",
|
|
quantity: 20,
|
|
rate: 250,
|
|
amount: 5000,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "inv-002",
|
|
customerId: "cust-002",
|
|
customerName: "Globex Industries",
|
|
amount: "3500",
|
|
status: "sent",
|
|
dueDate: new Date("2024-02-28"),
|
|
items: [
|
|
{
|
|
description: "Software License",
|
|
quantity: 7,
|
|
rate: 500,
|
|
amount: 3500,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "inv-003",
|
|
customerId: "cust-001",
|
|
customerName: "Acme Corporation",
|
|
amount: "10000",
|
|
status: "overdue",
|
|
dueDate: new Date("2024-01-31"),
|
|
items: [
|
|
{
|
|
description: "Annual Support Contract",
|
|
quantity: 1,
|
|
rate: 10000,
|
|
amount: 10000,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "inv-004",
|
|
customerId: "cust-003",
|
|
customerName: "Initech LLC",
|
|
amount: "2300",
|
|
status: "draft",
|
|
dueDate: new Date("2024-03-15"),
|
|
items: [
|
|
{
|
|
description: "Training Session",
|
|
quantity: 1,
|
|
rate: 2300,
|
|
amount: 2300,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "inv-005",
|
|
customerId: "cust-004",
|
|
customerName: "Umbrella Corp",
|
|
amount: "45000",
|
|
status: "sent",
|
|
dueDate: new Date("2024-03-01"),
|
|
items: [
|
|
{
|
|
description: "Enterprise License",
|
|
quantity: 1,
|
|
rate: 45000,
|
|
amount: 45000,
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
// Seed expenses
|
|
await db.insert(expenses).values([
|
|
{
|
|
id: "exp-001",
|
|
vendor: "AWS",
|
|
category: "Software",
|
|
amount: "2500",
|
|
date: new Date("2024-02-01"),
|
|
status: "approved",
|
|
description: "Cloud hosting - February",
|
|
},
|
|
{
|
|
id: "exp-002",
|
|
vendor: "Office Depot",
|
|
category: "Office Supplies",
|
|
amount: "350",
|
|
date: new Date("2024-02-05"),
|
|
status: "approved",
|
|
description: "Printer paper and toner",
|
|
},
|
|
{
|
|
id: "exp-003",
|
|
vendor: "Delta Airlines",
|
|
category: "Travel",
|
|
amount: "890",
|
|
date: new Date("2024-02-10"),
|
|
status: "pending",
|
|
description: "Flight to client meeting",
|
|
},
|
|
{
|
|
id: "exp-004",
|
|
vendor: "WeWork",
|
|
category: "Rent",
|
|
amount: "4500",
|
|
date: new Date("2024-02-01"),
|
|
status: "approved",
|
|
description: "Office space - February",
|
|
},
|
|
{
|
|
id: "exp-005",
|
|
vendor: "Uber",
|
|
category: "Travel",
|
|
amount: "125",
|
|
date: new Date("2024-02-12"),
|
|
status: "pending",
|
|
description: "Client site visits",
|
|
},
|
|
]);
|
|
|
|
// Seed transactions
|
|
await db.insert(transactions).values([
|
|
{
|
|
id: "txn-001",
|
|
accountId: "acc-001",
|
|
type: "income",
|
|
amount: "5000",
|
|
description: "Invoice #inv-001 payment",
|
|
category: "Sales",
|
|
date: new Date("2024-02-10"),
|
|
},
|
|
{
|
|
id: "txn-002",
|
|
accountId: "acc-001",
|
|
type: "expense",
|
|
amount: "2500",
|
|
description: "AWS hosting",
|
|
category: "Software",
|
|
date: new Date("2024-02-01"),
|
|
},
|
|
{
|
|
id: "txn-003",
|
|
accountId: "acc-003",
|
|
type: "expense",
|
|
amount: "350",
|
|
description: "Office supplies",
|
|
category: "Office",
|
|
date: new Date("2024-02-05"),
|
|
},
|
|
{
|
|
id: "txn-004",
|
|
accountId: "acc-001",
|
|
type: "income",
|
|
amount: "15000",
|
|
description: "Consulting retainer",
|
|
category: "Sales",
|
|
date: new Date("2024-02-01"),
|
|
},
|
|
{
|
|
id: "txn-005",
|
|
accountId: "acc-001",
|
|
type: "expense",
|
|
amount: "4500",
|
|
description: "Office rent",
|
|
category: "Rent",
|
|
date: new Date("2024-02-01"),
|
|
},
|
|
]);
|
|
|
|
return { message: "Database reset and seeded successfully" };
|
|
}
|
|
|
|
// Widget methods
|
|
export async function getWidgets() {
|
|
return db.select().from(widgets).orderBy(asc(widgets.order));
|
|
}
|
|
|
|
export async function getWidget(id: string) {
|
|
const result = await db
|
|
.select()
|
|
.from(widgets)
|
|
.where(eq(widgets.id, id))
|
|
.limit(1);
|
|
return result[0] || null;
|
|
}
|
|
|
|
export async function createWidget(data: { prompt: string; spec: WidgetSpec }) {
|
|
const id = generateId("widget");
|
|
|
|
// Get the next order value
|
|
const [result] = await db
|
|
.select({ maxOrder: max(widgets.order) })
|
|
.from(widgets);
|
|
const nextOrder = (result?.maxOrder ?? -1) + 1;
|
|
|
|
const [widget] = await db
|
|
.insert(widgets)
|
|
.values({
|
|
id,
|
|
prompt: data.prompt,
|
|
spec: data.spec,
|
|
order: nextOrder,
|
|
})
|
|
.returning();
|
|
return widget;
|
|
}
|
|
|
|
export async function updateWidget(
|
|
id: string,
|
|
data: { prompt?: string; spec?: WidgetSpec },
|
|
) {
|
|
const [widget] = await db
|
|
.update(widgets)
|
|
.set({ ...data, updatedAt: new Date() })
|
|
.where(eq(widgets.id, id))
|
|
.returning();
|
|
return widget || null;
|
|
}
|
|
|
|
export async function deleteWidget(id: string) {
|
|
const result = await db
|
|
.delete(widgets)
|
|
.where(eq(widgets.id, id))
|
|
.returning({ id: widgets.id });
|
|
return result.length > 0;
|
|
}
|
|
|
|
export async function reorderWidgets(orderedIds: string[]) {
|
|
// Update each widget's order based on position in array
|
|
for (let i = 0; i < orderedIds.length; i++) {
|
|
const id = orderedIds[i];
|
|
if (!id) continue;
|
|
await db.update(widgets).set({ order: i }).where(eq(widgets.id, id));
|
|
}
|
|
}
|