import Database from "better-sqlite3" import path from "path" import fs from "fs" const DB_PATH = path.join(process.cwd(), "data", "quotes.db") // Ensure data directory exists const dataDir = path.dirname(DB_PATH) if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }) } const db = new Database(DB_PATH) // Enable WAL mode for better concurrent read performance db.pragma("journal_mode = WAL") // Create schema if not exists db.exec(` CREATE TABLE IF NOT EXISTS quotes ( id INTEGER PRIMARY KEY AUTOINCREMENT, postal_code TEXT NOT NULL, address TEXT, area REAL NOT NULL, height REAL NOT NULL, include_floor_heating INTEGER DEFAULT 1, flooring_type TEXT DEFAULT 'STANDARD', customer_name TEXT NOT NULL, customer_email TEXT NOT NULL, customer_phone TEXT NOT NULL, remarks TEXT, total_excl_vat REAL NOT NULL, total_incl_vat REAL NOT NULL, status TEXT DEFAULT 'new', created_at TEXT DEFAULT CURRENT_TIMESTAMP, email_opened_at TEXT ) `) // Ensure quote IDs start at 1000 const countResult = db.prepare("SELECT COUNT(*) as count FROM quotes").get() as { count: number } if (countResult.count === 0) { try { const seqExists = db.prepare("SELECT seq FROM sqlite_sequence WHERE name = 'quotes'").get() if (!seqExists) { db.exec( "INSERT INTO quotes (id, postal_code, area, height, customer_name, customer_email, customer_phone, total_excl_vat, total_incl_vat) VALUES (999, '0000', 0, 0, 'init', 'init', 'init', 0, 0)" ) db.exec("DELETE FROM quotes WHERE id = 999") } } catch { // sqlite_sequence may not exist yet on fresh DB — that's fine } } // ─── Types ───────────────────────────────────────────────────────── export interface QuoteInput { postalCode: string address?: string area: number height: number includeFloorHeating: boolean flooringType: string customerName: string customerEmail: string customerPhone: string remarks?: string totalExclVat: number totalInclVat: number } export type QuoteStatus = "new" | "contacted" | "accepted" | "rejected" export interface StoredQuote extends QuoteInput { id: number status: QuoteStatus createdAt: string emailOpenedAt: string | null } // ─── Queries ─────────────────────────────────────────────────────── export function saveQuote(quote: QuoteInput): { id: number; slug: string } { const stmt = db.prepare(` INSERT INTO quotes ( postal_code, address, area, height, include_floor_heating, flooring_type, customer_name, customer_email, customer_phone, remarks, total_excl_vat, total_incl_vat ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `) const result = stmt.run( quote.postalCode, quote.address || null, quote.area, quote.height, quote.includeFloorHeating ? 1 : 0, quote.flooringType, quote.customerName, quote.customerEmail, quote.customerPhone, quote.remarks || null, quote.totalExclVat, quote.totalInclVat ) const id = result.lastInsertRowid as number const slug = `${quote.postalCode}-${id}` return { id, slug } } export function getQuoteBySlug(slug: string): StoredQuote | null { const match = slug.match(/^(\d{4})-(\d+)$/) if (!match) return null const [, postalCode, idStr] = match const id = parseInt(idStr, 10) const row = db.prepare("SELECT * FROM quotes WHERE id = ? AND postal_code = ?").get(id, postalCode) as any if (!row) return null return rowToQuote(row) } export function getQuoteById(id: number): StoredQuote | null { const row = db.prepare("SELECT * FROM quotes WHERE id = ?").get(id) as any if (!row) return null return rowToQuote(row) } export function getAllQuotes(): StoredQuote[] { const rows = db.prepare("SELECT * FROM quotes ORDER BY id DESC").all() as any[] return rows.map(rowToQuote) } export function updateQuoteStatus(id: number, status: QuoteStatus): boolean { const result = db.prepare("UPDATE quotes SET status = ? WHERE id = ?").run(status, id) return result.changes > 0 } export function markEmailOpened(id: number): boolean { const result = db.prepare( "UPDATE quotes SET email_opened_at = ? WHERE id = ? AND email_opened_at IS NULL" ).run(new Date().toISOString(), id) return result.changes > 0 } function rowToQuote(row: any): StoredQuote { return { id: row.id, postalCode: row.postal_code, address: row.address, area: row.area, height: row.height, includeFloorHeating: row.include_floor_heating === 1, flooringType: row.flooring_type, customerName: row.customer_name, customerEmail: row.customer_email, customerPhone: row.customer_phone, remarks: row.remarks, totalExclVat: row.total_excl_vat, totalInclVat: row.total_incl_vat, status: row.status || "new", createdAt: row.created_at, emailOpenedAt: row.email_opened_at || null, } }