168 lines
5 KiB
TypeScript
168 lines
5 KiB
TypeScript
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,
|
|
}
|
|
}
|