export interface BaseRecord { id: string; created: string; updated: string; collectionId: string; collectionName: string; } export interface Customer extends BaseRecord { customer: string; email: string; alternative_email: string; buyer_email: string; number: string; company: string; dateadded: string; lastvisiteddate: string; rep: string; coordinates: string; description: string; } export interface Company extends BaseRecord { company: string; address: string; vendor: string; } export interface CompanyDetails extends BaseRecord { company_name: string; address: string; number: string; email: string; vat_number: string; registration_number: string; } export interface Supplier extends BaseRecord { suppliername: string; email: string; suppliernumber: string; supplieraddress: string; supply_tags: string; coordinates: string; closingtime: string; } export type QuoteStatus = | "new" | "claimed" | "processed" | "emailed" | "approval_pending" | "approved" | "rejected" | "problem" | "complete" | "cancelled" | "follow_up" | "email_failed" | "unmatched_bounce" | "template" /** * Transient: the operator clicked Approve & Send Email. Next.js * set this; n8n is about to (or currently) attempting the SMTP * send. Cleared on success (→ emailed) or failure (→ mail_failed). * Visible in the UI as a spinner pill. If the quote sits here for * more than ~2 min the UI flags it as stuck. */ | "sending" /** * Terminal: the outbound send was attempted and failed. The * reason is on `email_error`. Operator can retry from the modal, * which flips status back to `sending`. Distinct from * `email_failed` (which is the inbound bounce path). */ | "mail_failed"; export interface Quote extends BaseRecord { quote_number: string; quote_reference: string; external_number: string; rfq_number: string; markup: number; customer: string; company: string; claimed_by: string; description: string; status: QuoteStatus; approved_by: string; notes: string; email_sender: string; email_subject: string; email_body: string; has_attachments: boolean; company_letterhead: string; email_delivered: boolean; email_delivered_at: string; /** When the original inbound email was received (Date header / IMAP envelope date). */ email_received_at: string; email_opened: boolean; email_opened_at: string; email_tracking_id: string; has_new_reply: boolean; last_reply_at: string; email_failed_at: string; pdf_generated_at: string; email_message_id: string; /** Date printed on the PDF (the quote's "official" date). Blank → fall back to `created`. */ quote_date: string; /** Validity period in days (e.g. 30, 60). Blank/null → default to QUOTE_VALID_DAYS in the PDF. */ validity_days: number | null; /** True for templates — reusable scaffolding, not a real customer quote. */ is_template: boolean; /** If this template was cloned from a real quote, the source quote id. */ template_source_id: string; /** If this quote was generated from a template, the template quote id. */ generated_from_template_id: string; /** User-set follow-up reminder date. Dashboard widget flags overdue. */ follow_up_due: string; /** * Email-composition fields persisted by the Approve & Send modal * (added 2026-06-23). Each is the operator's last-used value for * this quote — so opening the modal a second time shows what they * sent before, instead of starting from scratch. * * `email_subject_outbound` overrides the default `Re: {email_subject}`; * `email_message` overrides the default "please find attached" body. * Both empty falls back to the defaults at send time (n8n side). * * `attach_auth_letters` defaults true — toggles the per-letterhead * supplier authorization letter (CNL or ISHERWOOD). Requires the * `letterhead_settings.auth_letter` field to be uploaded; otherwise * it's a no-op and the modal surfaces a note. */ email_cc: string; email_bcc: string; email_subject_outbound: string; email_message: string; attach_auth_letters: boolean; /** * RFC 5322 Message-ID stamped on the outbound email by the SMTP * send. Lets the inbound monitor match future bounce / DSN * messages back to this specific outbound attempt. Empty until * the first send completes. */ email_outbound_message_id: string; /** * JSON array of `quote_attachments` record IDs that were attached * to the outbound send. Captured at send time so retry sends the * same set (not whatever is attached at retry time). Empty until * the first send completes. */ email_outbound_attachment_ids: string; /** * Last failure reason from the outbound send. Empty on success. * Cleared on every retry attempt. Visible in the modal banner * and the quote card timeline. */ email_error: string; /** * Wall-clock when status flipped to `sending`. Lets the UI flag * "stuck for >2 min" sends and operators measure SMTP turnaround. */ sending_started_at: string; expand?: { customer?: Customer; company?: Company; }; } export type QuoteAttachmentKind = "pdf" | "image" | "doc" | "other"; /** * Where a `quote_attachments` row came from / where it's going. * - "inbound" — file arrived with the original RFQ email * - "outbound" — operator uploaded it on the quote detail page to * be sent with the quote email * - "generated" — PDF rendered by `/api/v1/quotes/[id]/pdf` * * `direction` is null/undefined for rows that pre-date the * 2026-06-23 migration; the UI must tolerate that and treat them * as "inbound" by default (the migration backfills new rows, but * very old rows may still be null in dev). */ export type QuoteAttachmentDirection = "inbound" | "outbound" | "generated"; export interface QuoteAttachment extends BaseRecord { quote: string; /** * User-facing filename, e.g. "Quote-CNL-Q009638.pdf" — what we set * when uploading. NOT what PB stores on disk; PB normalises to a * slugged version in `file`. */ filename: string; /** * PB's stored file tokens. For a single-file field PB returns an * array (it allows multiple per record); we treat it as an array * here. The first element is the token to use in `/api/files/...` URLs. */ file: string[]; content_type: string; size_bytes: number; email_message_id: string; /** * Classification used by the dashboard to pick the right icon / preview. * Older attachments (and a few mid-2026 imports) were uploaded without * `kind` being set — the UI must tolerate `null` / `undefined`. */ kind?: QuoteAttachmentKind | null; uploaded_at?: string; /** * See `QuoteAttachmentDirection`. Tolerate null/undefined for * rows created before the 2026-06-23 direction-field migration. */ direction?: QuoteAttachmentDirection | null; expand?: { quote?: Quote; }; } export interface QuoteReply extends BaseRecord { quote: string; from_email: string; from_name: string; subject: string; body_plain: string; body_html: string; received_at: string; email_message_id: string; in_reply_to: string; quoted_text: string; expand?: { quote?: Quote; }; } export interface QuoteItem extends BaseRecord { quote: string; description: string; quote_number: string; quote_reference: string; quantity: number; supplier: string; cost_price: number; selling_price: number; notes: string; is_processed: boolean; markup: number; quote_pdf_id: string; /** * Flipped to true the first time the user saves the item from the * process page or quote-form. Gates the AI rewrite feature — we * don't let the LLM rewrite an item the user hasn't put their own * touch on yet (the AI-extracted descriptions from the email * pipeline are treated as drafts until a human confirms them). */ user_edited: boolean; expand?: { supplier?: Supplier; }; } export type OrderStatus = | "new" | "processing" | "order_ready" | "po_generated" | "completed" | "cancelled"; export type ItemStatus = | "pending" | "processed" | "po_generated" | "driver_pool" | "assigned" | "collected" | "stock_verified" | "picking" | "ready_delivery" | "delivered" | "cancelled"; export interface Order extends BaseRecord { order_number: string; company: string; customer: string; quote: string; status: OrderStatus; notes: string; quote_matching_attempted: boolean; quote_match_confidence: number; potential_quote: string; potential_quote_confidence: number; is_repair_order: boolean; expand?: { company?: Company; customer?: Customer; quote?: Quote; }; } export interface OrderItem extends BaseRecord { order: string; description: string; po_description: string; quantity: number; order_qty: number; selling_price: number; supplier: string; cost_price: number; markup: number; purchase_order: string; item_status: ItemStatus; drv_status?: "unassigned" | "assigned" | "en_route" | "collected" | "failed"; drv_id?: string; notes: string; expand?: { supplier?: Supplier; }; } export interface CollectionPool extends BaseRecord { order: string; order_item: string; supplier?: string; drv_id?: string; pool_status: "queued" | "assigned" | "in_progress" | "completed" | "failed"; priority?: "low" | "normal" | "high" | "urgent"; pool_notes?: string; assigned_at?: string; completed_at?: string; expand?: { order?: Order; order_item?: OrderItem; supplier?: Supplier; }; } export interface PurchaseOrder extends BaseRecord { po_number: string; order: string; supplier: string; status: string; total_amount: number; expand?: { order?: Order; supplier?: Supplier; }; } export interface DeliveryNote extends BaseRecord { delivery_note_number: string; order: string; company?: string; customer?: string; status: "draft" | "ready" | "delivered" | "cancelled"; delivery_date?: string; notes?: string; expand?: { order?: Order; company?: Company; customer?: Customer; }; } export interface DeliveryNoteItem extends BaseRecord { delivery_note: string; order_item: string; description: string; quantity: number; expand?: { delivery_note?: DeliveryNote; order_item?: OrderItem; }; } export interface Repair extends BaseRecord { repair_number: string; customer?: string; company?: string; order?: string; status: "new" | "assessing" | "quoted" | "approved" | "in_progress" | "completed" | "cancelled"; item_description: string; fault_description: string; notes?: string; expand?: { customer?: Customer; company?: Company; order?: Order; }; } export interface DashboardStats { totalCustomers: number; activeQuotes: number; ordersThisMonth: number; totalRevenue: number; }