import 'server-only'; import { z } from 'zod'; import { db } from './db'; import { recordAudit } from './audit'; import { canDelete, canImport, canWrite } from '@/lib/rbac'; import { parseCsv, toCsv, validateCustomerRows } from '@/lib/csv'; export const customerSchema = z.object({ name: z.string().min(1).max(160), email: z.string().email().max(200), company: z.string().max(160).optional().or(z.literal('')), status: z.enum(['active', 'lead', 'churned']).optional(), }); export const dealSchema = z.object({ title: z.string().min(1).max(160), customerId: z.string().optional().nullable(), value: z.number().int().min(0), stage: z.enum(['prospecting', 'proposal', 'won', 'lost']).optional(), owner: z.string().max(120).optional().or(z.literal('')), }); type Actor = { id: string; role: string }; export async function listCustomers({ q, status, sort, page = 1, pageSize = 10 }: { q?: string; status?: string; sort?: string; page?: number; pageSize?: number }) { const where = { ...(status ? { status } : {}), ...(q ? { OR: [{ name: { contains: q } }, { email: { contains: q } }, { company: { contains: q } }] } : {}), }; const orderBy = sort === 'name' ? { name: 'asc' as const } : sort === 'company' ? { company: 'asc' as const } : { createdAt: 'desc' as const }; const [items, total] = await Promise.all([ db.customer.findMany({ where, orderBy, skip: (page - 1) * pageSize, take: pageSize }), db.customer.count({ where }), ]); return { items, total, page, pageSize, pages: Math.max(1, Math.ceil(total / pageSize)) }; } export function getCustomer(id: string) { return db.customer.findUnique({ where: { id }, include: { deals: true } }); } export async function createCustomer(input: z.infer, actor: Actor) { if (!canWrite(actor.role)) return { ok: false as const, error: 'Forbidden' }; try { const customer = await db.customer.create({ data: { name: input.name, email: input.email.toLowerCase(), company: input.company || null, status: input.status ?? 'active' }, }); await recordAudit({ actorId: actor.id, action: 'customer.create', target: customer.email }); return { ok: true as const, customer }; } catch { return { ok: false as const, error: 'A customer with that email already exists.' }; } } export async function updateCustomer(id: string, input: z.infer, actor: Actor) { if (!canWrite(actor.role)) return { ok: false as const, error: 'Forbidden' }; const customer = await db.customer.update({ where: { id }, data: { name: input.name, email: input.email.toLowerCase(), company: input.company || null, status: input.status ?? 'active' }, }); await recordAudit({ actorId: actor.id, action: 'customer.update', target: customer.email }); return { ok: true as const, customer }; } export async function deleteCustomer(id: string, actor: Actor) { if (!canDelete(actor.role)) return { ok: false as const, error: 'Only admins can delete records.' }; const customer = await db.customer.delete({ where: { id } }); await recordAudit({ actorId: actor.id, action: 'customer.delete', target: customer.email }); return { ok: true as const }; } export async function listDeals({ stage, page = 1, pageSize = 10 }: { stage?: string; page?: number; pageSize?: number }) { const where = stage ? { stage } : {}; const [items, total] = await Promise.all([ db.deal.findMany({ where, include: { customer: true }, orderBy: { createdAt: 'desc' }, skip: (page - 1) * pageSize, take: pageSize }), db.deal.count({ where }), ]); return { items, total, page, pageSize, pages: Math.max(1, Math.ceil(total / pageSize)) }; } export async function createDeal(input: z.infer, actor: Actor) { if (!canWrite(actor.role)) return { ok: false as const, error: 'Forbidden' }; const deal = await db.deal.create({ data: { title: input.title, customerId: input.customerId || null, value: input.value, stage: input.stage ?? 'prospecting', owner: input.owner || null, }, }); await recordAudit({ actorId: actor.id, action: 'deal.create', target: deal.title }); return { ok: true as const, deal }; } export async function updateDeal(id: string, input: z.infer, actor: Actor) { if (!canWrite(actor.role)) return { ok: false as const, error: 'Forbidden' }; const deal = await db.deal.update({ where: { id }, data: { title: input.title, customerId: input.customerId || null, value: input.value, stage: input.stage ?? 'prospecting', owner: input.owner || null, }, }); await recordAudit({ actorId: actor.id, action: 'deal.update', target: deal.title, meta: { stage: deal.stage } }); return { ok: true as const, deal }; } export async function deleteDeal(id: string, actor: Actor) { if (!canDelete(actor.role)) return { ok: false as const, error: 'Only admins can delete records.' }; const deal = await db.deal.delete({ where: { id } }); await recordAudit({ actorId: actor.id, action: 'deal.delete', target: deal.title }); return { ok: true as const }; } export async function importCustomers(csvText: string, commit: boolean, actor: Actor) { if (!canImport(actor.role)) return { ok: false as const, error: 'Forbidden' }; const { rows } = parseCsv(csvText); const parsed = validateCustomerRows(rows); if (!commit) { return { ok: true as const, preview: true, total: parsed.total, errors: parsed.errors, duplicatesInFile: parsed.duplicatesInFile, valid: parsed.valid.length, sample: parsed.valid.slice(0, 5) }; } let imported = 0; let updated = 0; for (const row of parsed.valid) { const existing = await db.customer.findUnique({ where: { email: row.email } }); if (existing) { await db.customer.update({ where: { email: row.email }, data: { name: row.name, company: row.company || null, status: row.status } }); updated++; } else { await db.customer.create({ data: { name: row.name, email: row.email, company: row.company || null, status: row.status } }); imported++; } } await recordAudit({ actorId: actor.id, action: 'customer.import', target: `${imported} created, ${updated} updated`, meta: { errors: parsed.errors.length } }); return { ok: true as const, preview: false, imported, updated, errors: parsed.errors, duplicatesInFile: parsed.duplicatesInFile }; } export async function exportCustomersCsv() { const rows = await db.customer.findMany({ orderBy: { createdAt: 'desc' } }); return toCsv( rows.map((c) => ({ id: c.id, name: c.name, email: c.email, company: c.company ?? '', status: c.status, createdAt: c.createdAt.toISOString() })), ['id', 'name', 'email', 'company', 'status', 'createdAt'], ); } export async function overview() { const [customers, leads, deals, won, recent] = await Promise.all([ db.customer.count(), db.customer.count({ where: { status: 'lead' } }), db.deal.count(), db.deal.aggregate({ where: { stage: 'won' }, _sum: { value: true } }), db.auditLog.findMany({ orderBy: { createdAt: 'desc' }, take: 6, include: { actor: true } }), ]); const byStage = await Promise.all( ['prospecting', 'proposal', 'won', 'lost'].map(async (stage) => ({ stage, count: await db.deal.count({ where: { stage } }) })), ); return { customers, leads, deals, wonValue: won._sum.value ?? 0, byStage, recent: recent.map((r) => ({ id: r.id, action: r.action, target: r.target, actor: r.actor?.email ?? 'system', createdAt: r.createdAt })), }; } export function listAudit({ action, take = 100 }: { action?: string; take?: number } = {}) { return db.auditLog.findMany({ where: action ? { action: { contains: action } } : {}, orderBy: { createdAt: 'desc' }, take, include: { actor: true }, }); }