/**
 * The shape of every prop the CRM controllers send to Inertia.
 *
 * These mirror App\Support\Presenters\* on the backend. Server and client are
 * kept in step by hand, so when a presenter changes, change the matching type
 * here in the same commit.
 */

export type Option = {
    value: string;
    label: string;
    description?: string | null;
};

export type NumberOption = {
    value: number;
    label: string;
};

export type StaffOption = {
    value: number;
    label: string;
    role: string | null;
};

/**
 * A crew from the installer register, as a picker offers them. The note is the
 * state they work in and whether the register still books them, so two crews
 * with the same name can be told apart.
 */
export type InstallerOption = {
    value: number;
    label: string;
    note: string | null;
};

export type RoleOption = {
    value: string;
    label: string;
    description: string | null;
    users_count: number;
};

export type CustomerPortalAccount = {
    id: number;
    name: string;
    email: string;
    phone: string | null;
    verified: boolean;
    is_locked: boolean;
    locked_at: string | null;
    last_login_at: string | null;
    created_at: string | null;
    customer: {
        id: number;
        name: string;
        customer_number: string;
        is_active: boolean;
    } | null;
};

export type Paginated<T> = {
    data: T[];
    current_page: number;
    last_page: number;
    per_page: number;
    total: number;
    from: number | null;
    to: number | null;
    links: Array<{ url: string | null; label: string; active: boolean }>;
};

export type SimpleRef = {
    id: number;
    name: string;
};

/* ------------------------------------------------------------------ jobs */

export type JobStatusInfo = {
    value: string;
    label: string;
    description: string;
    group: string;
    group_label: string;
    color: string;
    order: number;
    progress: number;
    waiting_on: string;
    is_terminal: boolean;
    is_cancelled: boolean;
};

export type JobSummary = {
    id: number;
    job_number: string;
    title: string | null;
    type: { value: string; label: string };
    status: JobStatusInfo;
    priority: { value: string; label: string; color: string };
    customer: {
        id: number;
        name: string;
        customer_number: string;
        phone: string | null;
        email: string | null;
    } | null;
    site: {
        id: number;
        /** Whose house it is now — the site's owner, not necessarily the job's. */
        owner_id: number;
        label: string | null;
        address: string;
        suburb: string;
        state: string | null;
        state_label: string | null;
        postcode: string;
        nmi: string | null;
        distribution_network: string | null;
        phase: string;
    } | null;
    install_scheduled_for: string | null;
    status_changed_at: string | null;
    days_in_status: number;
    is_stalled: boolean;
    progress: number;
    requires_inspection: boolean;
    inspection: { value: string; label: string };
    grid_connection: { value: string; label: string; is_approved: boolean };
    stc_claim: { value: string; label: string };
    owner: SimpleRef | null;
    sales_rep: SimpleRef | null;
    operations_manager: SimpleRef | null;
    installer: SimpleRef | null;
    /**
     * The equipment named on the job, each with the warehouse line behind it
     * where one was identified, and the stock position against the quantity
     * the job needs. A line with `product: null` was named but not matched.
     */
    equipment: {
        summary: string | null;
        linked: number;
        short: number;
        lines: JobEquipmentLine[];
    };
    contract_value: number | null;
    created_at: string | null;
};

export type JobEquipmentLine = {
    kind: "panel" | "inverter" | "battery";
    label: string;
    name: string | null;
    capacity: string | null;
    needed: number | null;
    unit: string;
    product: {
        id: number;
        sku: string;
        name: string;
        brand: string | null;
        model: string | null;
        on_hand: number;
        available: number;
        minimum: number;
        state: "ok" | "low" | "out";
        active: boolean;
    } | null;
    /** Units needed beyond what is in stock. Null where it cannot be worked out. */
    shortfall: number | null;
};

export type JobPart = {
    id: number;
    category: string | null;
    description: string;
    /** Null means the design did not give enough to calculate it — not zero. */
    quantity: number | null;
    unit: string;
    basis: string | null;
    needs_review: boolean;
};

export type JobDetail = JobSummary & {
    /**
     * The property's owners, including whoever held it when this job was
     * created. A job done under one owner and chased under another is the
     * ordinary case after a house is sold.
     */
    owners: SiteOwnership | null;
    description: string | null;
    /** The same value, sanitized server-side and ready to render as HTML. */
    description_html: string | null;
    notes: string | null;
    notes_html: string | null;
    internal_notes: string | null;
    internal_notes_html: string | null;
    cancelled_reason: string | null;
    cancelled_reason_html: string | null;

    credentials: {
        saa_installer_number: string | null;
        saa_designer_number: string | null;
        electrical_licence_number: string | null;
    };

    /**
     * The draft parts list prepared for this job, with the basis for each
     * quantity. `needs_review` marks rows the design did not answer.
     */
    parts: JobPart[];

    /**
     * Whether the warehouse can actually pick this job. `no_design` means no
     * equipment on the job is linked to a catalogue line, so nothing has been
     * checked — it is not the same as ready.
     */
    readiness: {
        state: "ready" | "short" | "no_design";
        summary: string;
        short_lines: number;
        short_units: number;
        needed_by: string | null;
        lines: Array<{
            label: string;
            sku: string;
            name: string;
            unit: string;
            needed: number;
            available: number;
            shortfall: number;
        }>;
    };

    system: {
        installation_config: { value: string; label: string };
        system_size_kw: number | null;
        panel_count: number | null;
        panel_make: string | null;
        panel_model: string | null;
        panel_wattage: number | null;
        string_count: number | null;
        inverter_make: string | null;
        inverter_model: string | null;
        inverter_serial: string | null;
        inverter_capacity_kw: number | null;
        battery_make: string | null;
        battery_model: string | null;
        battery_capacity_kwh: number | null;
        export_limit_kw: number | null;
    };

    financials: {
        contract_value: number | null;
        deposit_amount: number | null;
        deposit_received_at: string | null;
        stc_discount_amount: number | null;
        finance_provider: string | null;
        finance_term_months: number | null;
    } | null;

    dates: Record<string, string | null>;

    grid: {
        status: { value: string; label: string; is_approved: boolean };
        application_reference: string | null;
        application_submitted_at: string | null;
        approved_at: string | null;
        approval_expires_on: string | null;
        approved_export_limit_kw: number | null;
        conditions: string | null;
        conditions_html: string | null;
        meter_type: string | null;
        meter_serial: string | null;
        meter_installed_at: string | null;
        export_enabled: boolean;
        dnsp: string | null;
        dnsp_website: string | null;
        nmi: string | null;
        requires_pre_approval: boolean;
    } | null;

    inspection: {
        required: boolean;
        required_by_state: boolean;
        authority: string | null;
        status: {
            value: string;
            label: string;
            is_passed: boolean;
            blocks_energising: boolean;
        };
        inspector_name: string | null;
        inspector_licence: string | null;
        booked_for: string | null;
        passed_at: string | null;
        defects: string | null;
        defects_html: string | null;
        notes: string | null;
        notes_html: string | null;
        certificate: {
            type: string | null;
            number: string | null;
            issued_at: string | null;
        };
    } | null;

    solar_victoria: {
        applies: boolean;
        program: string | null;
        application_ref: string | null;
        rebate_amount: number | null;
        audit_selected: boolean;
        audit_status: string | null;
        audit_decided_at: string | null;
        has_sampled_audit: boolean;
        audit_program: string | null;
    } | null;

    stc: {
        eligible: boolean;
        quantity: number | null;
        zone_rating: number | null;
        assignment_signed: boolean;
        agent: string | null;
        status: { value: string; label: string; is_open: boolean };
        submitted_at: string | null;
        registered_at: string | null;
        requires_notice_of_completion: boolean;
    } | null;

    state_compliance: {
        state: string;
        state_label: string;
        certificate_name: string;
        requires_independent_inspection: boolean;
        rebate_program: string | null;
    } | null;
};

export type WorkflowOption = {
    value: string;
    label: string;
    group: string;
    group_label: string;
    allowed: boolean;
    reason: string | null;
    requires_note: boolean;
    is_next: boolean;
};

export type StatusHistoryEntry = {
    id: number;
    from: { value: string; label: string; color: string } | null;
    to: { value: string; label: string; color: string };
    summary: string;
    notes: string | null;
    notes_html: string | null;
    user: SimpleRef | null;
    created_at: string | null;
};

export type ActivityEntry = {
    id: number;
    type: { value: string; label: string };
    subject: string | null;
    body: string | null;
    body_html: string | null;
    job?: { id: number; job_number: string; title: string | null } | null;
    user: SimpleRef | null;
    occurred_at: string | null;
    created_at?: string | null;
};

/* ------------------------------------------------------------- customers */

export type CustomerSummary = {
    id: number;
    customer_number: string;
    name: string;
    type: { value: string; label: string; is_organisation: boolean };
    company_name: string | null;
    first_name: string | null;
    last_name: string | null;
    abn: string | null;
    email: string | null;
    phone: string | null;
    alternate_phone: string | null;
    preferred_contact_method: { value: string; label: string } | null;
    billing: {
        line1: string | null;
        line2: string | null;
        suburb: string | null;
        state: string | null;
        state_label: string | null;
        postcode: string | null;
    };
    lead_source: { value: string; label: string } | null;
    lead_source_detail: string | null;
    owner: SimpleRef | null;
    /**
     * The builder account this customer came through, when their work came
     * from one. A client always says whose client it is.
     */
    builder: { id: number; name: string; customer_number: string } | null;
    is_active: boolean;
    sites_count: number | null;
    jobs_count: number | null;
    active_jobs_count: number | null;
    primary_site: {
        id: number;
        address: string;
        state: string;
        distribution_network: string | null;
    } | null;
    last_contacted_at: string | null;
    created_at: string | null;
};

export type CustomerContactSummary = {
    id: number;
    name: string;
    position: string | null;
    email: string | null;
    phone: string | null;
    is_primary: boolean;
    receives_invoices: boolean;
    notes: string | null;
    notes_html: string | null;
};

export type CustomerDetail = CustomerSummary & {
    notes: string | null;
    notes_html: string | null;
    acn: string | null;
    contacts: CustomerContactSummary[];
    sites: SiteSummary[];
    updated_at: string | null;
    deleted_at: string | null;
};

/**
 * How complete a client's record is: a percentage for the ring, and the short
 * list of what is actually missing underneath it.
 */
export type ProfileHealth = {
    percent: number;
    done: number;
    total: number;
    missing: string[];
};

/* ----------------------------------------------------------------- sites */

/* ------------------------------------------------------------- ownership */

/** One owner's tenure of one property: who held it, and from when to when. */
export type OwnershipEntry = {
    id: number;
    owner: { id: number | null; name: string; number: string | null };
    /** Null on the row that opened the ledger for a site. */
    previous_owner: { id: number | null; name: string } | null;
    started_on: string | null;
    ended_on: string | null;
    /** "12 Mar 2026 – now", ready to print. */
    period: string;
    is_current: boolean;
    is_transfer: boolean;
    reason: {
        value: string;
        label: string;
        is_correction: boolean;
    } | null;
    notes: string | null;
    /** How many jobs moved with the house on this handover. */
    jobs_moved: number;
    recorded_by: string | null;
    recorded_at: string | null;
};

/**
 * A property's owners, newest first, plus who held it at a given moment — the
 * day a job was created, on the job page.
 */
export type SiteOwnership = {
    current: OwnershipEntry | null;
    history: OwnershipEntry[];
    transfers: number;
    held_on: OwnershipEntry | null;
};

export type SiteSummary = {
    id: number;
    label: string | null;
    address: string;
    ownership: SiteOwnership;
    address_line1: string;
    address_line2: string | null;
    suburb: string;
    state: string;
    state_label: string;
    postcode: string;
    is_primary: boolean;
    nmi: string | null;
    meter_number: string | null;
    distribution_network: {
        id: number;
        name: string;
        website: string | null;
        notes: string | null;
        notes_html: string | null;
    } | null;
    electricity_retailer: string | null;
    phase: { value: string; label: string };
    roof: {
        type: string | null;
        material: string | null;
        storeys: number | null;
        has_asbestos: boolean;
        wind_region: string | null;
    };
    switchboard: {
        type: string | null;
        notes: string | null;
        notes_html: string | null;
    };
    internet_available: boolean | null;
    access_notes: string | null;
    access_notes_html: string | null;
    hazard_notes: string | null;
    hazard_notes_html: string | null;
    notes: string | null;
    notes_html: string | null;
    latitude: number | null;
    longitude: number | null;
};

export type ComplianceContext = {
    state: string;
    state_label: string;
    dnsp: string | null;
    requires_pre_approval: boolean;
    requires_independent_inspection: boolean;
    inspection_authority: string | null;
    certificate_name: string;
    rebate_program: string | null;
    has_sampled_safety_audit: boolean;
    requires_notice_of_completion: boolean;
};

/* ------------------------------------------------------------- documents */

export type DocumentSummary = {
    id: number;
    title: string;
    category: { value: string; label: string; is_stc_evidence: boolean };
    notes: string | null;
    original_name: string;
    mime_type: string | null;
    size: number | null;
    human_size: string;
    is_verified: boolean;
    verified_at: string | null;
    uploaded_by: SimpleRef | null;
    created_at: string | null;
};

/* ----------------------------------------------------------------- staff */

export type UserSummary = {
    id: number;
    name: string;
    email: string;
    employee_code: string | null;
    phone: string | null;
    job_title: string | null;
    department: string | null;
    is_active: boolean;
    last_login_at: string | null;
    email_verified_at: string | null;
    created_at: string | null;
    roles: string[];
    primary_role: string | null;
};

export type RoleSummary = {
    id: number;
    name: string;
    description: string | null;
    permissions: string[];
    users_count: number;
    is_protected: boolean;
    is_system: boolean;
};

/* --------------------------------------------------------------- options */

export type StatusOptionGroup = {
    group: string;
    group_label: string;
    description: string;
    statuses: Array<{
        value: string;
        label: string;
        description: string;
        color: string;
        order: number;
        waiting_on: string;
    }>;
};

export type NetworkGroup = {
    state: string;
    state_label: string;
    networks: Array<{ value: number; label: string; notes: string | null }>;
};

export type OptionsBundle = {
    states: Option[];
    customerTypes: Option[];
    contactMethods: Option[];
    leadSources: Option[];
    jobTypes: Option[];
    installationConfigs: Array<Option & { description: string }>;
    priorities: Option[];
    phases: Option[];
    jobStatuses: StatusOptionGroup[];
    jobStatusGroups: Option[];
    documentCategories: Array<Option & { is_stc_evidence: boolean }>;
    activityTypes: Option[];
    /** Why a property changed hands, each with the note that explains it. */
    ownershipReasons: Array<Option & { description: string }>;
    /**
     * Every status, carrying the block of the lifecycle it belongs to so the
     * pickers can group them rather than list a dozen in a row.
     */
    ticketStatuses: Array<
        Option & { color: string; group?: string; group_label?: string }
    >;
    ticketSources: Option[];
    /**
     * What a ticket is about. The description is written for whoever is
     * choosing between them, so the form can explain the pick without
     * repeating it.
     */
    ticketCategories: TicketCategoryOption[];
    installers: InstallerOption[];
    gridConnectionStatuses: Option[];
    inspectionStatuses: Option[];
    stcClaimStatuses: Option[];
    distributionNetworks: NetworkGroup[];
    /**
     * The builder accounts a client can be put under, with their customer
     * number as the second line. Few enough to send whole.
     */
    builders: Array<{ value: number; label: string; note: string | null }>;
    staff: StaffOption[];
    roles: string[];
};

/* --------------------------------------------------------------- tickets */

/** One of the kinds of work a support ticket can be. */
export type TicketCategoryOption = Option & {
    description: string;
    color: string;
    /** The crew who did the install is the one who answers for it. */
    needs_crew: boolean;
    is_warranty: boolean;
};

export type TicketStatusInfo = {
    value: string;
    label: string;
    description: string;
    color: string;
    order: number;
    waiting_on: string;
    is_open: boolean;
    is_terminal: boolean;
    needs_installer: boolean;
    /** The block of the lifecycle: desk, waiting, warranty or finished. */
    group: string;
    group_label: string;
    is_warranty: boolean;
    is_waiting_on_third_party: boolean;
};

/**
 * A support ticket. Tickets arrive from the SMS system, so they may only be
 * linked to some of a customer, a job and an installer.
 */
export type TicketSummary = {
    id: number;
    /** The number as it is stored: seven digits, no punctuation. */
    ticket_number: string;
    /** The same number as a person reads it, with the leading "#". */
    display_number: string;
    ticket_import_id: string | null;
    source: { value: string; label: string; color: string };
    issue: string;
    priority: { value: string; label: string; color: string };
    status: TicketStatusInfo;
    /** What kind of work it is, or null when nobody has decided yet. */
    category: TicketCategoryOption | null;
    client: {
        id: number;
        name: string;
        customer_number: string;
        phone: string | null;
        email: string | null;
    } | null;
    job: {
        id: number;
        job_number: string;
        title: string | null;
        status_label: string;
        status_color: string;
    } | null;
    /**
     * Where the client is: the site of the job the ticket is about, or their
     * billing address when no job with a site is linked. Null when neither is
     * recorded.
     */
    address: {
        line: string;
        /** True when the line is the installation site, not the billing address. */
        is_site: boolean;
    } | null;
    installer: SimpleRef | null;
    /**
     * The staff and support members holding the ticket in the office, in the
     * order they were put on it.
     */
    assignees: SimpleRef[];
    /**
     * The first of them, for the places with room for one name only — a list
     * row, or the panel on a job.
     */
    assignee: SimpleRef | null;
    contact: {
        name: string | null;
        phone: string | null;
        email: string | null;
    };
    days_open: number;
    days_in_status: number;
    is_overdue: boolean;
    reported_at: string | null;
    created_at: string | null;
};

/**
 * One entry in a ticket's conversation. Imported entries keep the identifier
 * they had in the system they came from, and say so.
 */
export type TicketComment = {
    id: number;
    body: string;
    /** The comment as stored, sanitized and ready to render as HTML. */
    body_html: string;
    type: {
        value: string;
        label: string;
        color: string;
        is_customer: boolean;
    };
    author: {
        id: number | null;
        name: string;
        user: SimpleRef | null;
        role: string | null;
    };
    attachment: {
        name: string;
        /** Served through the ticket policy, never a storage path. */
        url: string;
        download_url: string;
        is_image: boolean;
    } | null;
    imported: boolean;
    legacy_id: string | null;
    posted_at: string | null;
    created_at: string | null;
    updated_at: string | null;
};

/** The two kinds of entry a person can write themselves. */
export type WritableCommentType = "internal_note" | "customer_reply";

/**
 * A photo, screenshot or PDF attached to the ticket itself. The links come from
 * the application so every read passes the ticket policy first.
 */
export type TicketAttachment = {
    id: number;
    name: string;
    mime_type: string | null;
    size: number | null;
    human_size: string;
    /** Whether the browser can render it as a thumbnail. */
    is_image: boolean;
    url: string;
    download_url: string;
    delete_url: string;
    uploaded_by: string | null;
    created_at: string | null;
};

export type TicketDetail = TicketSummary & {
    body: string | null;
    attachments: TicketAttachment[];
    /** The description as stored, sanitized and ready to render as HTML. */
    body_html: string;
    /** The same description as words, for the one-line places. */
    body_text: string;
    resolution: string | null;
    resolution_html: string | null;
    /**
     * The outcome as a record rather than a paragraph: which kind of thing
     * fixed it. Null on tickets closed before outcomes were recorded.
     */
    resolution_code: {
        value: string;
        label: string;
        description: string;
        color: string;
        is_crew_work: boolean;
        is_manufacturer_claim: boolean;
    } | null;
    /** Who signed the outcome off, when somebody did. */
    resolved_by: SimpleRef | null;
    creator: SimpleRef | null;
    comment_count: number;
    last_comment_at: string | null;
    dates: {
        reported_at: string | null;
        status_changed_at: string | null;
        acknowledged_at: string | null;
        resolved_at: string | null;
        closed_at: string | null;
        created_at: string | null;
        updated_at: string | null;
    };
};

/** Another ticket on the same job, customer or installer. */
export type RelatedTicket = TicketSummary & { match: string };

export type TicketHistoryEntry = {
    label: string;
    detail: string | null;
    at: string | null;
    tone: string;
};

export type TicketCounts = {
    open: number;
    total: number;
};

export type TicketSummaryCounts = TicketCounts & {
    /**
     * Still work rather than finished or parked: not resolved, closed,
     * cancelled, and not waiting on the customer.
     */
    active: number;
    unassigned: number;
    waitingOnInstaller: number;
    /** Every ticket in a manufacturer or warranty state. */
    warranty: number;
    overdue: number;
    resolvedThisWeek: number;
};

export type TicketCustomerOption = {
    value: number;
    label: string;
    note: string | null;
    /**
     * What the ticket's contact fields are filled with when this customer is
     * picked: their primary contact, or the customer's own details.
     */
    contact: {
        name: string;
        phone: string | null;
        email: string | null;
    };
};

export type TicketJobOption = {
    value: number;
    label: string;
    customer_id: number;
    customer: string | null;
    /** The register crew whose name the job carries, when the register knows it. */
    installer_id: number | null;
    installer: string | null;
    /** The installer as the install schedule wrote it, matched or not. */
    installer_company: string | null;
    status: string;
    status_color: string;
};

/** A ticket on the dashboard's support list, with the reason it is there. */
export type SupportTicket = TicketSummary & {
    reason: string;
    reason_tone: string;
};

export type DashboardSupport = {
    open: number;
    unassigned: number;
    waitingOnInstaller: number;
    overdue: number;
    recent: SupportTicket[];
};

export type TicketFilters = {
    search: string | null;
    status: string | null;
    priority: string | null;
    category: string | null;
    installer: number | null;
    customer: number | null;
    scope: string;
};

export type TicketIndexProps = {
    tickets: Paginated<TicketSummary>;
    filters: TicketFilters;
    summary: TicketSummaryCounts;
    filterOptions: {
        statuses: Array<
            Option & { color: string; group?: string; group_label?: string }
        >;
        priorities: Option[];
        categories: TicketCategoryOption[];
        installers: InstallerOption[];
        scopes: Option[];
    };
    can: { create: boolean };
};

/* -------------------------------------------------- internal discussion */

/** A member of staff on a ticket's internal thread. */
export type DiscussionPerson = {
    id: number;
    name: string;
    avatar: string | null;
    job_title: string | null;
};

/**
 * One line of the ticket's internal discussion. `system` entries are the
 * status moves the application wrote down, which is why they have no author.
 */
export type TicketDiscussionMessage = {
    id: number;
    body: string;
    created_at: string | null;
    mine: boolean;
    system: boolean;
    author: DiscussionPerson | null;
};

/**
 * The private staff conversation about a ticket. It is the same conversation
 * the inbox shows — opening it there shows the same history, because it is one
 * thread rather than a copy.
 */
export type TicketDiscussion = {
    /** Null until the first message, which is what starts it. */
    id: number | null;
    /** The same conversation in the staff inbox, once it exists. */
    url: string | null;
    subject: string;
    messages: TicketDiscussionMessage[];
    participants: DiscussionPerson[];
    /** Messages this person has not read in it yet. */
    unread: number;
    can_post: boolean;
    /**
     * People holding the ticket who are not on the thread yet, and would be
     * added by the next message.
     */
    to_add: string[];
};

/* ------------------------------------------------------ ticket activity */

/** One kind of outcome a ticket can be closed with. */
export type TicketResolutionOption = Option & {
    description: string;
    color: string;
};

/**
 * One entry in the ticket's activity feed, whatever it started life as: a
 * customer reply, an internal note from the staff conversation, or a change the
 * application wrote down.
 *
 * `audience` is the distinction the screen draws: `customer` is something the
 * customer said or was told, `staff` is the office talking among itself, and
 * `system` is a change rather than something anybody said.
 */
export type TicketActivityEntry = {
    id: string;
    kind: "customer_reply" | "internal_note" | "note" | "change" | "resolution";
    label: string;
    tone: string;
    audience: "customer" | "staff" | "system";
    author: {
        id: number | null;
        name: string;
        role: string | null;
        avatar: string | null;
    } | null;
    /** Plain text, for the entries that were stored as plain text. */
    body: string | null;
    /** Sanitized markup, for the entries that were stored as markup. */
    body_html: string | null;
    at: string | null;
    imported: boolean;
    legacy_id: string | null;
    attachment: {
        name: string;
        url: string;
        download_url: string;
        is_image: boolean;
    } | null;
    /** Only a comment can be removed; the discussion is history. */
    delete_url: string | null;
    mine: boolean;
    /** The same conversation in the staff inbox, on internal notes. */
    inbox_url: string | null;
    /** The outcome, on the one entry that records it. */
    resolution?: { value: string; label: string; color: string } | null;
};

export type TicketActivity = {
    entries: TicketActivityEntry[];
    counts: { all: number; customer: number; internal: number; changes: number };
    last_at: string | null;
};

export type TicketShowProps = {
    ticket: TicketDetail;
    /** The conversation and the changes, merged into one reading order. */
    activity: TicketActivity;
    resolutionCodes: TicketResolutionOption[];
    comments: TicketComment[];
    related: RelatedTicket[];
    history: TicketHistoryEntry[];
    options: OptionsBundle;
    /** Null when this person may not use the staff inbox. */
    discussion: TicketDiscussion | null;
    can: {
        update: boolean;
        delete: boolean;
        assign: boolean;
        /** Commenting rides on the same permission as working the ticket. */
        comment: boolean;
        /** Whether the internal discussion can be written to. */
        discuss: boolean;
    };
};

export type TicketFormProps = {
    options: OptionsBundle;
    /**
     * Only the customer the ticket is about: the full list runs to thousands of
     * rows, so the picker searches it through /lookups/customers instead.
     */
    customers: TicketCustomerOption[];
    /**
     * Only the job the ticket already points at. A customer's own jobs are
     * fetched from /lookups/jobs as soon as a customer is named.
     */
    jobs: TicketJobOption[];
};

export type TicketCreateProps = TicketFormProps & {
    selected: { customer_id: number | null; job_id: number | null };
    /** Working a ticket and assigning one are separate permissions. */
    can: { assign: boolean };
    /**
     * The person logging the ticket. The assignee picker starts on them, and the
     * server assigns the ticket to them when the form names nobody.
     */
    current_staff_id: number | null;
};

export type TicketEditProps = TicketFormProps & {
    ticket: TicketDetail;
    resolutionCodes: TicketResolutionOption[];
    /** Working a ticket and assigning one are separate permissions. */
    can: { assign: boolean };
};

/* ------------------------------------------------ global (top bar) search */

/** One record the top bar's search found. */
export type SearchResult = {
    id: number;
    title: string;
    subtitle: string;
    /** The field the term matched, when the server could tell: NMI, Address, Phone… */
    badge: string | null;
    url: string;
};

/** One kind of record, with a link to the full list filtered by the same term. */
export type SearchGroup = {
    key: string;
    label: string;
    total: number;
    more_url: string;
    results: SearchResult[];
};

export type GlobalSearchResponse = {
    term: string;
    total: number;
    groups: SearchGroup[];
};

/* ------------------------------------------------------------- dashboard */

export type PipelineStage = {
    value: string;
    label: string;
    group: string;
    group_label: string;
    color: string;
    order: number;
    waiting_on: string;
    count: number;
};

export type StateBreakdown = {
    state: string;
    label: string;
    jobsCount: number;
    inspectionsCount: number;
    awaitingInspectionCount: number;
    requiresIndependentInspection: boolean;
    requiresNoticeOfCompletion: boolean;
    rebateProgram: string | null;
};

export type DashboardMetrics = {
    activeJobs: number;
    pipelineValue: number | null;
    scheduledInstallations: number;
    awaitingInspection: number;
    awaitingGridCloseout: number;
    completedThisMonth: number;
    stalledJobs: number;
    newLeads: number;
};

/**
 * A job on the attention list, with the reason it needs a person today.
 */
export type AttentionItem = JobSummary & {
    reason: string;
    reason_tone: string;
    reason_type: "blocked" | "warning" | "notice";
};

export type DashboardTrends = {
    completed: number[];
    created: number[];
    labels: string[];
};

export type DashboardDeltas = {
    newJobs: number;
    completed: number;
};

export type DashboardProps = {
    metrics: DashboardMetrics;
    pipeline: PipelineStage[];
    byState: StateBreakdown[];
    attention: AttentionItem[];
    trends: DashboardTrends;
    deltas: DashboardDeltas;
    upcomingInstallations: JobSummary[];
    awaitingInspection: JobSummary[];
    awaitingGridCloseout: JobSummary[];
    stalledJobs: JobSummary[];
    myJobs: JobSummary[];
    recentActivity: ActivityEntry[];
    stalledThresholdDays: number;
    /** Null when the viewer is not allowed to see support tickets at all. */
    support: DashboardSupport | null;
    can: { viewFinancials: boolean; viewCompliance: boolean };
};

/* ------------------------------------------------------- backup & restore */

/**
 * One archive on the Backup & Restore screen. `downloadable` says a file exists
 * on disk, whatever verification said about it. `restorable` is the stricter
 * one: complete and verified, the only state that writes over live data.
 */
export type BackupRecord = {
    id: number;
    backup_id: string;
    type: string;
    type_label: string;
    status: string;
    status_label: string;
    status_color: string;
    label: string | null;
    size_bytes: number | null;
    size: string | null;
    checksum: string | null;
    integrity: "unverified" | "verified" | "failed";
    error: string | null;
    requested_by: string | null;
    components: string[];
    tables: number;
    files: number | null;
    created_at: string | null;
    completed_at: string | null;
    downloadable: boolean;
    /** Verified, on disk and complete: the only archives offered for restoring. */
    restorable: boolean;
};

/**
 * A portable package on disk: the whole installation — code, database, stored
 * files and an installer — read back from the manifest inside the archive.
 */
export type PortablePackageRecord = {
    id: string;
    label: string | null;
    size_bytes: number;
    size: string;
    created_at: string | null;
    /** Everything copied in, code included. */
    files: number;
    /** The stored files alone: uploads and documents, not the code. */
    stored_files: number;
    tables: number;
    records: number;
    with_vendor: boolean;
    with_assets: boolean;
    /** False when the manifest could not be read out of the archive. */
    readable: boolean;
};

/**
 * How a restore treats what is already in the database: write the archive over
 * it, or only add the tables and rows it does not already hold.
 */
export type RestoreModeOption = {
    value: "replace" | "merge";
    label: string;
    description: string;
};

export type BackupSettingsProps = {
    backups: BackupRecord[];
    types: Option[];
    storage: {
        /** A server path, never a URL: backups are not reachable over HTTP. */
        path: string;
        outside_public_root: boolean;
        count: number;
        total_bytes: number;
        total_human: string;
        retention: number;
    };
    /**
     * The switch this screen owns: whether a new archive is read back and
     * checksummed before it is recorded as complete.
     */
    settings: {
        verify: boolean;
    };
    /**
     * Automatic backups. Configured on the server, so this is read-only on the
     * screen: it says what the scheduler is set to do, not what to change.
     */
    schedule: {
        enabled: boolean;
        type: string;
        type_label: string;
        cron: string;
        /** The cron expression in words, e.g. "Daily at 02:00". */
        cadence: string;
        prunes: boolean;
        retention: number;
        /** Whether a scheduled run reads its archive back, following settings.verify. */
        verifies: boolean;
        command: string;
    };
    runningBackup: boolean;
    can: {
        create: boolean;
        delete: boolean;
        restore: boolean;
        /** Build and download portable packages of the whole installation. */
        package: boolean;
    };
    restoreCommand: string;
    /**
     * The reusable restore link: a private address that puts a backup back with
     * no sign-in, open until it expires or is closed. Its state lives in a file
     * rather than the database, because a restore replaces the database.
     */
    restoreLink: {
        exists: boolean;
        open: boolean;
        /**
         * Armed without an expiry: it works until somebody closes it, rather
         * than running out on its own. Its address can be renewed without
         * changing that.
         */
        permanent: boolean;
        expired: boolean;
        closed: boolean;
        /** The full address while it is open; null once it is not. */
        url: string | null;
        path: string;
        expires_at: string | null;
        created_at: string | null;
        created_by_name: string | null;
        note: string | null;
        closed_at: string | null;
        closed_by_name: string | null;
        /** When the address was last renewed; null when it never has been. */
        renewed_at: string | null;
        renewed_by_name: string | null;
        /** How many times the address has been renewed. */
        renewals: number;
        hours: number;
        default_hours: number;
        max_hours: number;
        use_count: number;
        last_used_at: string | null;
        uses: {
            at?: string;
            archive?: string;
            outcome?: string;
            safety?: string | number | null;
            tables?: number;
            files?: number;
            message?: string;
        }[];
    };
    restoreModes: RestoreModeOption[];
    packages: PortablePackageRecord[];
    packageStorage: {
        path: string;
        outside_public_root: boolean;
        count: number;
        total_human: string;
    };
};
