// data.jsx — data layer: vehicles, suburbs, pricing engine, seeds, helpers
// All exported to window.FX

/* ---------------- Sizes (vehicle classes) ----------------
   Kept under the name VEHICLES to stay compatible across the app.
   Ordered ascending by capacity. baseRate (AUD) + perKm (AUD). */
const VEHICLES = [
  { id: "courier", code: "C",  name: "Courier",  icon: "package",   weight: "< 25 kg",    cap: "Up to 25 kg",   desc: "Documents & parcels",      dims: "50 × 50 × 50 cm",        volume: "max 0.13 m\u00b3", loadNote: "",                                                          baseRate: 9,  perKm: 1.10, capPct: 0.14 },
  { id: "wagon",   code: "W",  name: "Wagon",    icon: "car",       weight: "< 125 kg",   cap: "Up to 125 kg",  desc: "Small skid / hand load",   dims: "50 × 50 × 100 cm",       volume: "max 0.5 m\u00b3",  loadNote: "or small skid hand load",                                  baseRate: 16, perKm: 1.65, capPct: 0.4 },
  { id: "halfvan", code: "HV", name: "Half Van", icon: "truck",     weight: "< 500 kg",   cap: "Up to 500 kg",  desc: "Skid / pallet fork load",  dims: "max 100 × 100 × 100 cm", volume: "max 1.5 m\u00b3",  loadNote: "or skid/pallet fork load (max pallet height 120 cm)",       baseRate: 34, perKm: 2.40, capPct: 0.68 },
  { id: "van",     code: "V",  name: "Van",      icon: "container", weight: "< 1000 kg",  cap: "Up to 1,000 kg",desc: "Skid / pallet fork load",  dims: "max 120 × 120 × 120 cm", volume: "max 3.0 m\u00b3",  loadNote: "or skid/pallet fork load (max pallet height 120 cm)",       baseRate: 58, perKm: 3.10, capPct: 1.0 },
];

/* ---------------- Service tiers ---------------- */
const SERVICES = [
  { id: "standard", letter: "S", name: "Standard", window: "Picked up within 3 hrs, delivered same day", eta: "Same-day · by 6 PM",  mult: 1.0,  tag: "Most popular" },
  { id: "premium",  letter: "P", name: "Premium",  window: "Priority same-day with a faster window",     eta: "Within 3 hours",     mult: 1.45, tag: null },
  { id: "direct",   letter: "D", name: "Direct",   window: "Straight there, non-stop, nothing else aboard",eta: "Point-to-point",     mult: 1.95, tag: "Fastest" },
];

// Service code, e.g. ("standard","halfvan") -> "SHV"
function serviceCode(tierId, sizeId) {
  const t = SERVICES.find(x => x.id === tierId), s = VEHICLES.find(x => x.id === sizeId);
  return ((t ? t.letter : "") + (s ? s.code : "")) || "";
}
// Full service name, e.g. "Standard Half Van"
function serviceName(tierId, sizeId) {
  const t = SERVICES.find(x => x.id === tierId), s = VEHICLES.find(x => x.id === sizeId);
  return [t && t.name, s && s.name].filter(Boolean).join(" ");
}

/* ---------------- Suburbs (across Australian states) ---------------- */
const SUBURBS = [
  { name: "Sydney CBD",     lat: -33.8688, lng: 151.2093, state: "NSW" },
  { name: "Surry Hills",    lat: -33.8860, lng: 151.2110, state: "NSW" },
  { name: "Newtown",        lat: -33.8983, lng: 151.1790, state: "NSW" },
  { name: "Bondi Junction", lat: -33.8915, lng: 151.2477, state: "NSW" },
  { name: "Parramatta",     lat: -33.8150, lng: 151.0000, state: "NSW" },
  { name: "Chatswood",      lat: -33.7969, lng: 151.1803, state: "NSW" },
  { name: "North Sydney",   lat: -33.8404, lng: 151.2070, state: "NSW" },
  { name: "Alexandria",     lat: -33.9015, lng: 151.1940, state: "NSW" },
  { name: "Mascot",         lat: -33.9300, lng: 151.1940, state: "NSW" },
  { name: "Botany",         lat: -33.9460, lng: 151.1950, state: "NSW" },
  { name: "Manly",          lat: -33.7969, lng: 151.2880, state: "NSW" },
  { name: "Ryde",           lat: -33.8133, lng: 151.1030, state: "NSW" },
  { name: "Liverpool",      lat: -33.9200, lng: 150.9239, state: "NSW" },
  { name: "Penrith",        lat: -33.7510, lng: 150.6940, state: "NSW" },
  { name: "Macquarie Park", lat: -33.7800, lng: 151.1200, state: "NSW" },
  { name: "Marrickville",   lat: -33.9110, lng: 151.1550, state: "NSW" },
  { name: "Rhodes",         lat: -33.8310, lng: 151.0870, state: "NSW" },
  { name: "Sydney Airport", lat: -33.9399, lng: 151.1753, state: "NSW" },
  // Victoria
  { name: "Melbourne CBD",  lat: -37.8136, lng: 144.9631, state: "VIC" },
  { name: "Richmond (VIC)", lat: -37.8230, lng: 144.9980, state: "VIC" },
  { name: "Geelong",        lat: -38.1499, lng: 144.3617, state: "VIC" },
  // Queensland
  { name: "Brisbane CBD",   lat: -27.4698, lng: 153.0251, state: "QLD" },
  { name: "South Brisbane", lat: -27.4809, lng: 153.0176, state: "QLD" },
  { name: "Gold Coast",     lat: -28.0167, lng: 153.4000, state: "QLD" },
  // Western Australia
  { name: "Perth CBD",      lat: -31.9523, lng: 115.8613, state: "WA" },
  { name: "Fremantle",      lat: -32.0569, lng: 115.7439, state: "WA" },
  // South Australia
  { name: "Adelaide CBD",   lat: -34.9285, lng: 138.6007, state: "SA" },
  { name: "Glenelg",        lat: -34.9810, lng: 138.5150, state: "SA" },
  // Tasmania
  { name: "Hobart",         lat: -42.8821, lng: 147.3272, state: "TAS" },
  // ACT
  { name: "Canberra",       lat: -35.2809, lng: 149.1300, state: "ACT" },
  // Northern Territory
  { name: "Darwin",         lat: -12.4634, lng: 130.8456, state: "NT" },
];

const STATES = ["NSW", "VIC", "QLD", "WA", "SA", "TAS", "ACT", "NT"];

/* Suburb ↔ postcode, for the combined suburb/postcode search field (item 7). */
const SUBURB_POSTCODES = {
  "Sydney CBD": "2000", "Surry Hills": "2010", "Newtown": "2042", "Bondi Junction": "2022",
  "Parramatta": "2150", "Chatswood": "2067", "North Sydney": "2060", "Alexandria": "2015",
  "Mascot": "2020", "Botany": "2019", "Manly": "2095", "Ryde": "2112", "Liverpool": "2170",
  "Penrith": "2750", "Macquarie Park": "2113", "Marrickville": "2204", "Rhodes": "2138",
  "Sydney Airport": "2020", "Melbourne CBD": "3000", "Richmond (VIC)": "3121", "Geelong": "3220",
  "Brisbane CBD": "4000", "South Brisbane": "4101", "Gold Coast": "4217", "Perth CBD": "6000",
  "Fremantle": "6160", "Adelaide CBD": "5000", "Glenelg": "5045", "Hobart": "7000",
  "Canberra": "2600", "Darwin": "0800",
};
SUBURBS.forEach(s => { s.postcode = SUBURB_POSTCODES[s.name] || ""; });
function suburbPostcode(name) { const s = SUBURBS.find(x => x.name === name); return s ? (s.postcode || "") : ""; }
// Combined field: match on suburb name OR postcode; returns suburb records.
function searchSuburbs(query) {
  const q = String(query || "").trim().toLowerCase();
  if (!q) return [];
  const starts = [], contains = [];
  SUBURBS.forEach(s => {
    const nm = s.name.toLowerCase(), pc = s.postcode || "";
    if (nm.startsWith(q) || pc.startsWith(q)) starts.push(s);
    else if (nm.includes(q) || pc.includes(q)) contains.push(s);
  });
  return [...starts, ...contains].slice(0, 8);
}

function suburbNames() { return SUBURBS.map(s => s.name); }
function findSuburb(name) { return SUBURBS.find(s => s.name.toLowerCase() === String(name || "").toLowerCase()); }
function suburbState(name) { const s = findSuburb(name); return s ? s.state : null; }

// Haversine -> km, padded ~1.3x for road distance, min 4km.
function distanceKm(aName, bName) {
  const a = findSuburb(aName), b = findSuburb(bName);
  if (!a || !b) return null;
  if (a.name === b.name) return 4;
  const R = 6371, toRad = d => d * Math.PI / 180;
  const dLat = toRad(b.lat - a.lat), dLng = toRad(b.lng - a.lng);
  const s = Math.sin(dLat/2)**2 + Math.cos(toRad(a.lat))*Math.cos(toRad(b.lat))*Math.sin(dLng/2)**2;
  const c = 2 * Math.atan2(Math.sqrt(s), Math.sqrt(1-s));
  return Math.max(4, Math.round(R * c * 1.35));
}

/* ---------------- Pricing engine ---------------- */
// Returns a breakdown. rateMult lets Tweaks scale all rates.
function quote({ vehicleId, km, serviceId = "standard", isReturn = false, rateMult = 1 }) {
  const v = VEHICLES.find(x => x.id === vehicleId) || VEHICLES[0];
  const s = SERVICES.find(x => x.id === serviceId) || SERVICES[0];
  const dist = km == null ? 0 : km;
  const oneWayBase = (v.baseRate + v.perKm * dist) * rateMult;
  const oneWay = oneWayBase * s.mult;
  const returnLeg = isReturn ? oneWay * 0.85 : 0;   // return discounted 15%
  const subtotal = oneWay + returnLeg;
  const gst = subtotal * 0.1;
  const total = subtotal + gst;
  return {
    vehicle: v, service: s, km: dist,
    oneWay, returnLeg, subtotal, gst, total, isReturn,
  };
}

/* ---------------- Line items → vehicle, quote numbers (shared by quote + booking) ---------------- */
const ITEM_TYPES = ["Carton", "Satchel", "Pallet", "Skid", "Crate", "Tube", "Envelope"];
/* Standard size/weight defaults per item type — auto-filled when a type is picked, editable after. */
const ITEM_DEFAULTS = {
  Carton:   { l: "40",  w: "30",  h: "25",  weight: "5" },
  Satchel:  { l: "35",  w: "28",  h: "5",   weight: "1" },
  Pallet:   { l: "120", w: "100", h: "150", weight: "300" },
  Skid:     { l: "120", w: "100", h: "80",  weight: "180" },
  Crate:    { l: "100", w: "80",  h: "80",  weight: "120" },
  Tube:     { l: "90",  w: "10",  h: "10",  weight: "2" },
  Envelope: { l: "30",  w: "22",  h: "1",   weight: "0.3" },
};
function typeDefaults(type) { const d = (typeof _rules !== "undefined" && _rules && _rules.itemDefaults) || ITEM_DEFAULTS; return d[type] ? { ...d[type] } : {}; }
const FUEL_LEVY_PCT = 0.125;
const VEHICLE_CAPS = [
  { id: "courier", kg: 25,   m3: 0.13 },
  { id: "wagon",   kg: 125,  m3: 0.5 },
  { id: "halfvan", kg: 500,  m3: 1.5 },
  { id: "van",     kg: 1000, m3: 3.0 },
];
/* ---- Configurable booking rules (item sizes + vehicle escalation), editable in CE Settings ---- */
const RULES_LS = "fedexRules_v1";
const DEFAULT_RULES = { itemDefaults: ITEM_DEFAULTS, vehicleCaps: VEHICLE_CAPS, taxiTruck: { minHours: 4, hourly: 145 }, phoneOverKg: 1000 };
function loadRules() { try { const r = JSON.parse(localStorage.getItem(RULES_LS)); return (r && r.vehicleCaps && r.itemDefaults) ? r : DEFAULT_RULES; } catch (e) { return DEFAULT_RULES; } }
function saveRules(r) { try { localStorage.setItem(RULES_LS, JSON.stringify(r)); } catch (e) {} }
let _rules = loadRules();
function activeRules() { return _rules; }
function setActiveRules(r) { _rules = r; saveRules(r); }
function blankItem() { return { qty: "1", type: "Carton", ...typeDefaults("Carton") }; }
function itemTotals(items) {
  let kg = 0, m3 = 0, count = 0;
  (items || []).forEach(it => { const q = Math.max(1, parseInt(it.qty, 10) || 1); count += q; kg += (parseFloat(it.weight) || 0) * q; m3 += ((parseFloat(it.l) || 0) * (parseFloat(it.w) || 0) * (parseFloat(it.h) || 0)) / 1e6 * q; });
  return { kg: Math.round(kg * 10) / 10, m3: Math.round(m3 * 1000) / 1000, count };
}
function autoVehicleForItems(items) { const caps = (_rules && _rules.vehicleCaps) || VEHICLE_CAPS; const t = itemTotals(items); const fit = caps.find(c => t.kg <= c.kg && t.m3 <= c.m3); return { vehicleId: fit ? fit.id : "van", kg: t.kg, m3: t.m3, over: !fit }; }
function genQuoteNo() { return "Q-" + Math.floor(100000 + Math.random() * 899999); }
function quoteExpiryISO(fromISO) { const d = fromISO ? new Date(fromISO) : new Date(); d.setDate(d.getDate() + SAVED_QUOTE_VALID_DAYS); return d.toISOString(); }
function quoteDaysLeft(expiryISO) { if (!expiryISO) return 0; return Math.ceil((new Date(expiryISO) - new Date()) / 86400000); }

/* ---------------- Address book ---------------- */
const ADDRESS_BOOK = [
  { id: "a1", label: "Meridian DC — Mascot",      name: "Meridian Distribution Centre", company: "Meridian Group", line: "12 Coward St", suburb: "Mascot",       postcode: "2020", contact: "Dispatch Desk", phone: "+61 2 8503 1100", email: "dispatch@meridiangroup.com.au", fav: true },
  { id: "a2", label: "Sydney Head Office — CBD",       name: "Meridian Group HQ",        company: "Meridian Group", line: "Level 18, 580 George St", suburb: "Sydney CBD", postcode: "2000", contact: "Mail Room",   phone: "+61 2 9210 4000", email: "mailroom@meridiangroup.com.au", fav: true },
  { id: "a3", label: "Acme Retail — Surry Hills",name: "Acme Retail Co.",          company: "Acme Retail",   line: "88 Crown St", suburb: "Surry Hills",  postcode: "2010", contact: "Priya Sharma",  phone: "+61 412 778 220", email: "priya@acmeretail.com.au", fav: false },
  { id: "a4", label: "Royal North Shore Hospital", name: "Royal North Shore Hospital", company: "NSW Health",  line: "Reserve Rd, St Leonards", unit: "Receiving Dock C", suburb: "North Sydney", postcode: "2065", contact: "Receiving Dock", phone: "+61 2 9463 5000", email: "receiving@rnsh.health.nsw.gov.au", fav: false },
  { id: "a5", label: "Westfield Parramatta",    name: "Westfield Loading Dock",    company: "Scentre Group", line: "159–175 Church St", suburb: "Parramatta", postcode: "2150", contact: "Dock Manager", phone: "+61 2 8836 9100", email: "dock.parra@scentre.com.au", fav: false },
  { id: "a6", label: "Bayside Cellars",         name: "Bayside Cellars",           company: "Bayside",       line: "210 Bay St",  suburb: "Botany",       postcode: "2019", contact: "Tom Nguyen",    phone: "+61 437 901 552", email: "orders@baysidecellars.com.au", fav: false },
  { id: "a7", label: "Melbourne Office — CBD", name: "Meridian Group Melbourne", company: "Meridian Group", line: "Level 9, 385 Bourke St", suburb: "Melbourne CBD", postcode: "3000", contact: "Reception", phone: "+61 3 9600 1200", email: "melbourne@meridiangroup.com.au", unit: "", instructions: "" },
];
function emptyAddr() { return { contact: "", company: "", email: "", phone: "", unit: "", line: "", suburb: "", postcode: "", instructions: "" }; }

/* ---------------- Delivery history (with POD) ---------------- */
const HISTORY = [
  { id: "FX-7K2D-9043", date: "2026-06-13", time: "14:22", vehicleId: "halfvan",  serviceId: "premium",  from: "Sydney CBD", to: "Chatswood",    price: 78.40, status: "Delivered",
    pod: { recipient: "R. Patel", signedAt: "14:22", photo: true, sig: true, note: "Left with reception, ID checked", lat: -33.7969, lng: 151.1803 } },
  { id: "FX-3M8P-1187", date: "2026-06-13", time: "10:05", vehicleId: "courier", serviceId: "direct", from: "Surry Hills", to: "North Sydney", price: 41.30, status: "Delivered",
    pod: { recipient: "J. Davies", signedAt: "10:05", photo: true, sig: true, note: "Handed to recipient", lat: -33.8404, lng: 151.2070 } },
  { id: "FX-9QX4-2256", date: "2026-06-12", time: "16:48", vehicleId: "halfvan", serviceId: "standard", from: "Mascot", to: "Parramatta",   price: 132.55, status: "Delivered",
    pod: { recipient: "Dock Manager", signedAt: "16:48", photo: true, sig: false, note: "Pallet unloaded at dock 3", lat: -33.8150, lng: 151.0000 } },
  { id: "FX-5T1B-6620", date: "2026-06-12", time: "09:14", vehicleId: "wagon",  serviceId: "standard",  from: "Newtown",    to: "Bondi Junction", price: 36.85, status: "Delivered",
    pod: { recipient: "A. Brown", signedAt: "09:14", photo: false, sig: true, note: "Signature on delivery", lat: -33.8915, lng: 151.2477 } },
  { id: "FX-2W7H-0091", date: "2026-06-11", time: "13:31", vehicleId: "van", serviceId: "standard", from: "Penrith", to: "Liverpool",   price: 318.20, status: "Delivered",
    pod: { recipient: "Site Foreman", signedAt: "13:31", photo: true, sig: true, note: "Pallet fork unload, 6 pallets", lat: -33.9200, lng: 150.9239 } },
  { id: "FX-8N3K-4412", date: "2026-06-10", time: "11:57", vehicleId: "halfvan",  serviceId: "premium",   from: "Alexandria", to: "Manly",        price: 91.10, status: "Delivered",
    pod: { recipient: "L. Fischer", signedAt: "11:57", photo: true, sig: true, note: "Photo + signature captured", lat: -33.7969, lng: 151.2880 } },
];

/* ---------------- Notices ---------------- */
const NOTICES = [
  { id: "n1", title: "Public holiday — Queen's Birthday", body: "Same-day pickups close at 12:00 PM on Mon 8 June. Standard service resumes Tue 9 June.", channel: "Banner + Email", audience: "All customers", date: "2026-06-05", status: "Published", pinned: true },
  { id: "n2", title: "New Van capacity in Western Sydney", body: "Additional Van-class vehicles now serving Penrith, Liverpool and Parramatta — book larger skid and pallet loads with shorter lead times.", channel: "Banner", audience: "All customers", date: "2026-05-28", status: "Published", pinned: false },
];

/* ---------------- Accounts (one customer, several division accounts) ---------------- */
const COMPANY = "Meridian Group";
const ACCOUNTS = [
  { id: "acc1", name: "Meridian Retail",     number: "FX-AU-100482", type: "Division · Primary" },
  { id: "acc2", name: "Meridian Healthcare", number: "FX-AU-205518", type: "Division" },
  { id: "acc3", name: "Meridian Industrial", number: "FX-AU-330090", type: "Division" },
];

/* ---------------- Invoices ---------------- */
const INVOICES = [
  { id: "INV-2026-0612", date: "2026-06-01", period: "May 2026",      deliveries: 32, amount: 1284.50, status: "Due",  due: "2026-06-30", accountId: "acc1" },
  { id: "INV-2026-0541", date: "2026-05-01", period: "April 2026",    deliveries: 28, amount: 1102.20, status: "Paid", due: "2026-05-31", accountId: "acc1" },
  { id: "INV-2026-0488", date: "2026-04-01", period: "March 2026",    deliveries: 41, amount: 1583.75, status: "Paid", due: "2026-04-30", accountId: "acc1" },
  { id: "INV-2026-0420", date: "2026-03-01", period: "February 2026", deliveries: 19, amount: 742.10,  status: "Paid", due: "2026-03-31", accountId: "acc1" },
  { id: "INV-2026-0355", date: "2026-02-01", period: "January 2026",  deliveries: 24, amount: 968.40,  status: "Paid", due: "2026-02-28", accountId: "acc1" },
];

/* ---------------- Team users ---------------- */
const USERS = [
  { id: "u1", name: "Sarah Chen",    email: "sarah.chen@meridiangroup.com.au",    role: "Admin",     status: "Active",  initials: "SC" },
  { id: "u2", name: "Marcus Lee",    email: "marcus.lee@meridiangroup.com.au",    role: "Booking",   status: "Active",  initials: "ML" },
  { id: "u3", name: "Priya Sharma",  email: "priya.sharma@meridiangroup.com.au",  role: "Booking",   status: "Active",  initials: "PS" },
  { id: "u4", name: "Tom Nguyen",    email: "tom.nguyen@meridiangroup.com.au",    role: "View only", status: "Invited", initials: "TN" },
];

/* ---------------- Drivers ---------------- */
const DRIVERS = [
  { id: "dr1", name: "Marcus Reyes",  initials: "MR", vehicle: "Van",         rego: "CV-4827", phone: "+61 412 555 901" },
  { id: "dr2", name: "Aisha Okafor",  initials: "AO", vehicle: "Courier bike",rego: "BK-1180", phone: "+61 413 220 778" },
  { id: "dr3", name: "Liam Nguyen",   initials: "LN", vehicle: "Wagon",       rego: "WG-3391", phone: "+61 414 667 200" },
  { id: "dr4", name: "Sofia Almeida", initials: "SA", vehicle: "Half Van",    rego: "HV-7742", phone: "+61 415 889 330" },
  { id: "dr5", name: "Tom Walsh",     initials: "TW", vehicle: "Van",         rego: "VN-2055", phone: "+61 416 119 884" },
  { id: "dr6", name: "Priya Kapoor",  initials: "PK", vehicle: "Courier",     rego: "CR-9043", phone: "+61 417 334 612" },
  { id: "dr7", name: "Daniel Cho",    initials: "DC", vehicle: "Wagon",       rego: "WG-6620", phone: "+61 418 552 109" },
  { id: "dr8", name: "Grace Mbeki",   initials: "GM", vehicle: "Half Van",    rego: "HV-2291", phone: "+61 419 770 145" },
];

/* ---------------- Payment methods (seed) ---------------- */
const PAYMENT_METHODS = [
  { id: "pm1", brand: "Visa", last4: "4242", exp: "08/27", name: "Meridian Group Pty Ltd", primary: true },
];

/* ================= CSR CONSOLE DATA ================= */

/* ---------------- Customers (searchable by name or ref) ---------------- */
const CUSTOMERS = [
  { id: "c1", ref: "CUST-10482", name: "Meridian Group", account: "FX-AU-100482", terms: "Account · Net 30", phone: "+61 2 9210 4000", email: "dispatch@meridiangroup.com.au", suburb: "Sydney CBD", priority: true,
    contacts: [ { name: "Sarah Chen", role: "Operations Mgr", phone: "+61 412 008 110", email: "sarah.chen@meridiangroup.com.au" }, { name: "Mail Room", role: "Dispatch", phone: "+61 2 9210 4000", email: "mailroom@meridiangroup.com.au" } ],
    addresses: [ { label: "Head Office — CBD", line: "580 George St", unit: "Level 18", suburb: "Sydney CBD", postcode: "2000", contact: "Mail Room", phone: "+61 2 9210 4000" }, { label: "Distribution Centre", line: "12 Coward St", unit: "", suburb: "Mascot", postcode: "2020", contact: "Dispatch Desk", phone: "+61 2 8503 1100" } ] },
  { id: "c2", ref: "CUST-20771", name: "Royal North Shore Hospital", account: "FX-AU-300145", terms: "Account · Net 14", phone: "+61 2 9463 5000", email: "receiving@rnsh.health.nsw.gov.au", suburb: "North Sydney", priority: true,
    contacts: [ { name: "Receiving Dock", role: "Goods inwards", phone: "+61 2 9463 5000", email: "receiving@rnsh.health.nsw.gov.au" }, { name: "Pathology", role: "Lab", phone: "+61 2 9463 5222", email: "path@rnsh.health.nsw.gov.au" } ],
    addresses: [ { label: "Main Receiving Dock", line: "Reserve Rd, St Leonards", unit: "Dock C", suburb: "North Sydney", postcode: "2065", contact: "Receiving Dock", phone: "+61 2 9463 5000" }, { label: "Pathology Lab", line: "Reserve Rd, St Leonards", unit: "Level 2 Lab", suburb: "North Sydney", postcode: "2065", contact: "Pathology", phone: "+61 2 9463 5222" } ] },
  { id: "c3", ref: "CUST-18204", name: "Acme Retail Co.", account: "FX-AU-208830", terms: "Account · Net 30", phone: "+61 412 778 220", email: "priya@acmeretail.com.au", suburb: "Surry Hills", priority: false,
    contacts: [ { name: "Priya Sharma", role: "Store Mgr", phone: "+61 412 778 220", email: "priya@acmeretail.com.au" } ],
    addresses: [ { label: "Flagship Store", line: "88 Crown St", unit: "", suburb: "Surry Hills", postcode: "2010", contact: "Priya Sharma", phone: "+61 412 778 220" } ] },
  { id: "c4", ref: "CUST-33910", name: "Helix BioResearch", account: "FX-AU-411207", terms: "Account · Net 7", phone: "+61 2 8001 4400", email: "logistics@helixbio.com.au", suburb: "Macquarie Park", priority: true,
    contacts: [ { name: "Dr. Owen Wells", role: "Trial Coordinator", phone: "+61 437 220 091", email: "owen.wells@helixbio.com.au" } ],
    addresses: [ { label: "Research Lab", line: "11 Talavera Rd", unit: "Building 3", suburb: "Macquarie Park", postcode: "2113", contact: "Dr. Owen Wells", phone: "+61 437 220 091" } ] },
  { id: "c5", ref: "CUST-12055", name: "Bayside Cellars", account: "FX-AU-119884", terms: "Prepaid · Card", phone: "+61 437 901 552", email: "orders@baysidecellars.com.au", suburb: "Botany", priority: false,
    contacts: [ { name: "Tom Nguyen", role: "Owner", phone: "+61 437 901 552", email: "orders@baysidecellars.com.au" } ],
    addresses: [ { label: "Cellar Door", line: "210 Bay St", unit: "", suburb: "Botany", postcode: "2019", contact: "Tom Nguyen", phone: "+61 437 901 552" } ] },
];

/* CE account attributes (item 20–21, 24–26): customer type, broker flag, account access
   phrase, trading status, and third-party booking authorisations. */
const _CUST_ACCOUNT = {
  c1: { type: "Corporate account", broker: false, accessPhrase: "MERIDIAN-4417", status: "active",
        bookingAlert: "", refConfig: { ref1: { label: "PO number", required: true, prefix: "45" }, ref2: { label: "Cost centre", required: false, prefix: "" } },
        thirdPartyAuth: [ { name: "Blue Ribbon Logistics", account: "FX-AU-556210", direction: "authorised" } ] },
  c2: { type: "Government / Health", broker: false, accessPhrase: "NORTHSIDE-2065", status: "active", bookingAlert: "Health account — quote the ward & department on every booking. Pathology pickups after 6 PM need a security escort; confirm with the dock before booking.", refConfig: { ref1: { label: "Requisition no.", required: true, prefix: "" }, ref2: { label: "Ward / department", required: true, prefix: "" } }, thirdPartyAuth: [] },
  c3: { type: "Retail account", broker: true, brokerName: "Acme Global Forwarding", accessPhrase: "CROWNST-1808", status: "active", thirdPartyAuth: [] },
  c4: { type: "Clinical / Research", broker: false, accessPhrase: "HELIX-3391", status: "stop-trade", stopReason: "Account on credit hold — refer to Finance before quoting.", thirdPartyAuth: [] },
  c5: { type: "Small business", broker: false, accessPhrase: "BAYSIDE-1205", status: "inactive", stopReason: "Account inactive — not currently trading.", thirdPartyAuth: [] },
};
CUSTOMERS.forEach(c => Object.assign(c, { bookingAlert: "", refConfig: null }, _CUST_ACCOUNT[c.id] || { type: "Account", broker: false, accessPhrase: "", status: "active", thirdPartyAuth: [] }));

/* ---------------- Booking types (each with operator script / special instructions) ---------------- */
const BOOKING_TYPES = [
  { id: "standard",  name: "Standard booking", short: "Standard", icon: "package",      tone: "brand",  mult: 1.0,  blurb: "Regular same-day pickup and delivery.",
    instructions: ["Confirm pickup-ready time and contact on site.", "Standard SLA applies — same-day by 6:00 PM."] },
  { id: "nextflight", name: "Next Flight delivery", short: "Next Flight", icon: "navigation", tone: "accent", mult: 2.6, surcharge: 85, blurb: "On the next available flight, airport-to-airport with couriers each end.",
    instructions: ["Capture earliest pickup time — must reach airport freight cut-off.", "Record contents & weight for air manifest; no dangerous goods without declaration.", "Advise customer of flight ETA and destination-airport handover."] },
  { id: "clinical",  name: "Clinical trial", short: "Clinical", icon: "shield-check",  tone: "brand",  mult: 1.8, surcharge: 45, blurb: "Temperature-sensitive trial samples with full chain of custody.",
    instructions: ["Confirm temperature requirement (ambient / 2–8°C / frozen) and packaging.", "Chain-of-custody form must be signed at every handover.", "Record trial / protocol number and site contact.", "Driver must not leave consignment unattended at any point."] },
  { id: "failsafe-secure", name: "Fail-Safe — Secure", short: "Fail-Safe Secure", icon: "shield-check", tone: "warn", mult: 2.2, surcharge: 60, blurb: "High-value / sensitive goods. Tracked, signed, ID-verified.",
    instructions: ["Photo ID of recipient required and recorded on delivery.", "Live GPS tracking shared with customer; tamper-evident seal applied.", "Two failed attempts → return to secure depot, do not leave with neighbour."] },
  { id: "failsafe-critical", name: "Fail-Safe — Mission Critical", short: "Fail-Safe Critical", icon: "zap", tone: "danger", mult: 3.2, surcharge: 140, blurb: "Cannot-fail consignment. Dedicated driver, monitored end-to-end.",
    instructions: ["Dedicated vehicle — nothing else aboard, non-stop direct.", "Operations desk monitors live; driver checks in at pickup and drop-off.", "Backup driver on standby; escalate immediately on any delay.", "Recipient phoned 15 min before arrival; signature + photo ID mandatory."] },
];

/* ---------------- Standardized service charges (add-ons) ---------------- */
const SERVICE_CHARGES = [
  { id: "sc1",  label: "Waiting time (per 15 min)", amount: 12.00, group: "Time" },
  { id: "sc2",  label: "After-hours surcharge",     amount: 35.00, group: "Time" },
  { id: "sc3",  label: "Weekend / public holiday",  amount: 45.00, group: "Time" },
  { id: "sc4",  label: "Hand unload",               amount: 25.00, group: "Handling" },
  { id: "sc5",  label: "Tail-lift required",        amount: 40.00, group: "Handling" },
  { id: "sc6",  label: "Two-person lift",           amount: 55.00, group: "Handling" },
  { id: "sc7",  label: "Temperature controlled",    amount: 38.00, group: "Special" },
  { id: "sc8",  label: "Dangerous goods handling",  amount: 65.00, group: "Special" },
  { id: "sc9",  label: "Transit insurance (per $1k)", amount: 9.00, group: "Special" },
  { id: "sc10", label: "Proof of ID on delivery",   amount: 8.00,  group: "Special" },
  { id: "sc11", label: "Re-delivery attempt",       amount: 22.00, group: "Other" },
  { id: "sc12", label: "Road tolls",                amount: 7.50,  group: "Other" },
];

/* ---------------- Next Flight (interstate air freight) ----------------
   Dangerous-goods types offered on the customer booking form when a
   shipment crosses state lines. Values are stable ids; labels match the
   IATA/UN descriptions shown to the customer. */
const DANGEROUS_GOODS = [
  { value: "un3090",           label: "Lithium Metal Batteries loose (UN3090)" },
  { value: "un3091-contained", label: "Lithium Metal Batteries contained in equipment (UN3091)" },
  { value: "un3091-packed",    label: "Lithium Metal Batteries packed with equipment (UN3091)" },
  { value: "un3480",           label: "Lithium-Ion Batteries loose (UN3480)" },
  { value: "un3481-contained", label: "Lithium-Ion Batteries contained in equipment (UN3481)" },
  { value: "un3481-packed",    label: "Lithium-Ion Batteries packed with equipment (UN3481)" },
  { value: "un3373",           label: "Biological substance, category B (UN3373)" },
  { value: "un3245",           label: "Genetically modified organisms (UN3245)" },
  { value: "excepted",         label: "Dangerous goods in excepted quantity" },
  { value: "un1845",           label: "Dry ice (UN1845)" },
  { value: "other",            label: "Other" },
];

/* Legal small-print shown below the booking at the point of placing an order. */
const BOOKING_DISCLAIMER = [
  "Final prices are determined based on actual weights and dimensions as measured by TNT and may differ from the price shown here. You may receive an additional invoice.",
  "All prices are VAT exclusive and also exclusive of any taxes, duties, or associated government agency charges.",
  "Please note that this is intended for Business Users.",
  "We reserve the right to assess Additional Handling charges for packages that require special handling or that require us to apply additional packaging during transit. Package shape and dimensions may change during transit, which can affect the package\u2019s surcharge eligibility. If the package shape and dimensions change during transit, we may make appropriate adjustments to the shipment charges at any time. To read more about our Additional Handling Surcharges, please visit tnt.com/surcharges",
];

/* ---------------- Delivery runs (start → ordered stops → end) ---------------- */
const _runDay = new Date().toISOString().slice(0, 10);
const _runDayMinus = n => new Date(Date.now() - n * 86400000).toISOString().slice(0, 10);
const DELIVERY_RUNS = [
  {
    id: "RUN-4471", name: "Morning CBD & East loop", date: _runDay, driverId: "dr1", status: "Active",
    start: { label: "Meridian DC — Mascot", suburb: "Mascot", line: "12 Coward St" },
    end:   { label: "Meridian DC — Mascot", suburb: "Mascot", line: "12 Coward St" },
    stops: [
      { seq: 1, company: "Acme Retail Co.",    contact: "Priya Sharma", suburb: "Surry Hills",   line: "88 Crown St",   tracking: "FX-RN41-3081", status: "Delivered",  time: "08:42" },
      { seq: 2, company: "Bayside Cellars",     contact: "Tom Nguyen",   suburb: "Botany",        line: "210 Bay St",    tracking: "FX-RN41-3082", status: "Delivered",  time: "09:15" },
      { seq: 3, company: "Harbour Legal",       contact: "Front Desk",   suburb: "Sydney CBD",    line: "1 Bligh St",    tracking: "FX-RN41-3083", status: "Delivered",  time: "09:58" },
      { seq: 4, company: "Bondi Wellness",      contact: "Reception",    suburb: "Bondi Junction",line: "12 Oxford St",  tracking: "FX-RN41-3084", status: "In transit", time: "—" },
      { seq: 5, company: "Eastside Pharmacy",   contact: "Dispensary",   suburb: "Alexandria",    line: "44 Botany Rd",  tracking: "FX-RN41-3085", status: "Pending",    time: "—" },
      { seq: 6, company: "Meridian HQ",         contact: "Mail Room",    suburb: "Sydney CBD",    line: "580 George St",  tracking: "FX-RN41-3086", status: "Pending",    time: "—" },
    ],
  },
  {
    id: "RUN-4468", name: "North Shore medical", date: _runDayMinus(1), driverId: "dr4", status: "Active",
    start: { label: "Meridian DC — Mascot", suburb: "Mascot", line: "12 Coward St" },
    end:   { label: "Chatswood Depot", suburb: "Chatswood", line: "5 Help St" },
    stops: [
      { seq: 1, company: "Royal North Shore Hospital", contact: "Receiving Dock", suburb: "North Sydney", line: "Reserve Rd", tracking: "FX-RN68-7741", status: "Delivered",  time: "08:20" },
      { seq: 2, company: "Northside Pathology",        contact: "Lab Intake",     suburb: "Chatswood",    line: "9 Help St",   tracking: "FX-RN68-7742", status: "Delivered",  time: "08:55" },
      { seq: 3, company: "Crows Nest Clinic",          contact: "Reception",      suburb: "North Sydney", line: "60 Falcon St",tracking: "FX-RN68-7743", status: "In transit", time: "—" },
      { seq: 4, company: "Ryde Day Surgery",           contact: "Goods Inwards",  suburb: "Ryde",         line: "2 Smith St",  tracking: "FX-RN68-7744", status: "Pending",    time: "—" },
    ],
  },
  {
    id: "RUN-4475", name: "West Sydney pallets", date: _runDay, driverId: "dr5", status: "Scheduled",
    start: { label: "Meridian DC — Mascot", suburb: "Mascot", line: "12 Coward St" },
    end:   { label: "Penrith Yard", suburb: "Penrith", line: "100 Mulgoa Rd" },
    stops: [
      { seq: 1, company: "Westfield Parramatta", contact: "Dock Manager", suburb: "Parramatta", line: "159 Church St", tracking: "FX-RN75-9901", status: "Pending", time: "—" },
      { seq: 2, company: "Liverpool Trade Co.",  contact: "Warehouse",    suburb: "Liverpool",  line: "30 Macquarie St", tracking: "FX-RN75-9902", status: "Pending", time: "—" },
      { seq: 3, company: "Penrith Builders",     contact: "Site Office",  suburb: "Penrith",    line: "100 Mulgoa Rd",  tracking: "FX-RN75-9903", status: "Pending", time: "—" },
    ],
  },
  {
    id: "RUN-4460", name: "Inner West parcels", date: _runDayMinus(2), driverId: "dr3", status: "Completed",
    start: { label: "Meridian DC — Mascot", suburb: "Mascot", line: "12 Coward St" },
    end:   { label: "Meridian DC — Mascot", suburb: "Mascot", line: "12 Coward St" },
    stops: [
      { seq: 1, company: "Newtown Books",     contact: "Counter",   suburb: "Newtown",      line: "200 King St",   tracking: "FX-RN60-2201", status: "Delivered", time: "07:50" },
      { seq: 2, company: "Marrickville Café", contact: "Manager",   suburb: "Marrickville", line: "5 Illawarra Rd",tracking: "FX-RN60-2202", status: "Delivered", time: "08:18" },
      { seq: 3, company: "Rhodes Logistics",  contact: "Dock 2",    suburb: "Rhodes",       line: "1 Walker St",   tracking: "FX-RN60-2203", status: "Delivered", time: "09:05" },
      { seq: 4, company: "Macquarie Park Tech",contact: "Reception",suburb: "Macquarie Park",line: "11 Talavera Rd",tracking: "FX-RN60-2204", status: "Delivered", time: "09:48" },
    ],
  },
];
function runProgress(run) {
  const total = run.stops.length;
  const done = run.stops.filter(s => s.status === "Delivered").length;
  const current = run.stops.find(s => s.status === "In transit");
  return { done, total, pct: total ? done / total : 0, current };
}

/* ================= NEXT FLIGHT / AIR FREIGHT ================= */
/* Airports keyed by state (primary capital-city airport / station). */
const AIRPORTS = {
  NSW: { code: "SYD", city: "Sydney",    name: "Sydney Airport",    lat: -33.95, lng: 151.18 },
  VIC: { code: "MEL", city: "Melbourne", name: "Melbourne Airport", lat: -37.67, lng: 144.84 },
  QLD: { code: "BNE", city: "Brisbane",  name: "Brisbane Airport",  lat: -27.38, lng: 153.12 },
  WA:  { code: "PER", city: "Perth",     name: "Perth Airport",     lat: -31.94, lng: 115.97 },
  SA:  { code: "ADL", city: "Adelaide",  name: "Adelaide Airport",  lat: -34.95, lng: 138.53 },
  ACT: { code: "CBR", city: "Canberra",  name: "Canberra Airport",  lat: -35.31, lng: 149.20 },
  TAS: { code: "HBA", city: "Hobart",    name: "Hobart Airport",    lat: -42.84, lng: 147.51 },
  NT:  { code: "DRW", city: "Darwin",    name: "Darwin Airport",    lat: -12.41, lng: 130.88 },
};
Object.keys(AIRPORTS).forEach(st => { AIRPORTS[st].dgOutbound = !(["HBA", "DRW"].includes(AIRPORTS[st].code)); });
const AIRPORT_BY_CODE = {};
Object.keys(AIRPORTS).forEach(st => { AIRPORT_BY_CODE[AIRPORTS[st].code] = { ...AIRPORTS[st], state: st }; });
const AIRPORT_CODES = Object.values(AIRPORTS).map(a => a.code);

const AIRLINES = [
  { iata: "QF", name: "Qantas",           prefix: "081" },
  { iata: "VA", name: "Virgin Australia", prefix: "795" },
  { iata: "JQ", name: "Jetstar",          prefix: "041" },
  { iata: "ZL", name: "Rex",              prefix: "947" },
  { iata: "QQ", name: "Alliance",         prefix: "556" },
];

/* Approx block time (minutes) between ports, unordered pair. */
const _ROUTE_MINS = {
  "MEL-SYD": 90, "BNE-SYD": 95, "BNE-MEL": 135, "PER-SYD": 305, "MEL-PER": 245, "BNE-PER": 335,
  "ADL-SYD": 140, "ADL-MEL": 80, "ADL-BNE": 160, "CBR-SYD": 55, "CBR-MEL": 70, "BNE-CBR": 110,
  "HBA-SYD": 120, "HBA-MEL": 80, "BNE-DRW": 255, "DRW-PER": 215, "ADL-PER": 195, "ADL-DRW": 205, "CBR-ADL": 100, "DRW-SYD": 260,
};
function routeMins(a, b) { return _ROUTE_MINS[[a, b].sort().join("-")] || 150; }

/* Build a deterministic domestic schedule for "today" (AirLabs /schedules shape). */
function buildFlights() {
  let seed = 20260701;
  const rnd = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff; };
  const pick = arr => arr[Math.floor(rnd() * arr.length)];
  const pad = n => String(n).padStart(2, "0");
  const hm = mins => pad(Math.floor((mins % 1440) / 60)) + ":" + pad(mins % 60);
  const now = new Date();
  const nowMin = now.getHours() * 60 + now.getMinutes();
  const routes = [
    ["SYD", "MEL"], ["SYD", "BNE"], ["MEL", "BNE"], ["SYD", "PER"], ["MEL", "PER"], ["SYD", "ADL"],
    ["MEL", "ADL"], ["SYD", "CBR"], ["MEL", "CBR"], ["BNE", "CBR"], ["SYD", "HBA"], ["MEL", "HBA"],
    ["BNE", "PER"], ["ADL", "PER"], ["BNE", "DRW"], ["PER", "DRW"], ["ADL", "DRW"], ["BNE", "ADL"],
  ];
  const flights = [];
  let serial = 400;
  routes.forEach(([x, y]) => {
    [[x, y], [y, x]].forEach(([dep, arr]) => {
      const nDaily = 2 + Math.floor(rnd() * 3);
      const dur = routeMins(dep, arr);
      for (let i = 0; i < nDaily; i++) {
        const al = pick(AIRLINES);
        const depMin = 360 + Math.floor(rnd() * 780);
        const delay = rnd() > 0.82 ? 10 + Math.floor(rnd() * 45) : 0;
        const arrMin = depMin + dur;
        let status;
        if (nowMin < depMin + delay - 30) status = "scheduled";
        else if (nowMin < depMin + delay) status = "boarding";
        else if (nowMin < arrMin + delay) status = "en-route";
        else status = "landed";
        serial += 1 + Math.floor(rnd() * 40);
        const num = String(serial);
        flights.push({
          flight_iata: al.iata + num, flight_number: num, airline_iata: al.iata, airline_name: al.name,
          dep_iata: dep, dep_city: AIRPORT_BY_CODE[dep].city, arr_iata: arr, arr_city: AIRPORT_BY_CODE[arr].city,
          dep_hm: hm(depMin), arr_hm: hm(arrMin), dep_est_hm: hm(depMin + delay), arr_est_hm: hm(arrMin + delay),
          depMin, arrMin, status, delay, dur,
          aircraft: pick(["B738", "A320", "A321", "E190", "B737", "A332"]),
          gate: String.fromCharCode(65 + Math.floor(rnd() * 5)) + (1 + Math.floor(rnd() * 24)),
          terminal: pick(["T1", "T2", "T3", "DOM"]),
        });
      }
    });
  });
  return flights.sort((a, b) => a.depMin - b.depMin);
}
const FLIGHTS = buildFlights();

function flightByIata(iata) { const q = (iata || "").toUpperCase().replace(/\s/g, ""); return FLIGHTS.find(f => f.flight_iata === q); }
function searchFlights({ number, port, dir = "all" }) {
  let list = FLIGHTS;
  if (number) { const q = number.toUpperCase().replace(/\s/g, ""); list = list.filter(f => f.flight_iata.includes(q)); }
  if (port) { const p = port.toUpperCase(); list = list.filter(f => dir === "dep" ? f.dep_iata === p : dir === "arr" ? f.arr_iata === p : (f.dep_iata === p || f.arr_iata === p)); }
  return list;
}
const FLIGHT_STATUS = {
  scheduled: { label: "Scheduled", tone: "neutral", dot: "var(--fg-mute)" },
  boarding:  { label: "Boarding",  tone: "warn",    dot: "var(--warn)" },
  "en-route":{ label: "En route",  tone: "brand",   dot: "var(--brand)" },
  landed:    { label: "Landed",    tone: "success", dot: "var(--success)" },
};

/* ================= USERS, ROLES & SERVICE AGENTS ================= */
const USER_ROLES = [
  { id: "admin",    label: "Administrator", desc: "Full access — bookings, settings, users and billing.", access: ["Bookings", "Pre-Alerts", "Flights", "Service agents", "Users & roles", "Billing"] },
  { id: "operator", label: "Operator",      desc: "Take bookings, manage the queue and pre-alerts.",       access: ["Bookings", "Pre-Alerts", "Flights"] },
  { id: "dispatch", label: "Dispatcher",    desc: "Allocate drivers and service agents, manage runs.",      access: ["Bookings", "Pre-Alerts", "Service agents"] },
  { id: "finance",  label: "Finance",       desc: "View bookings, manage invoices and charges.",            access: ["Bookings (read)", "Billing"] },
  { id: "readonly", label: "Read-only",     desc: "View bookings and tracking only — no edits.",           access: ["Bookings (read)"] },
];
function roleById(id) { return USER_ROLES.find(r => r.id === id) || USER_ROLES[USER_ROLES.length - 1]; }

const CONSOLE_USERS = [
  { id: "u1", name: "Jordan Diaz",     email: "jordan.diaz@fedex.com.au",     roleId: "operator", desk: "Desk 04",  active: true,  workTypes: null, timezone: "Australia/Sydney" },
  { id: "u2", name: "Amara Osei",      email: "amara.osei@fedex.com.au",      roleId: "admin",    desk: "Ops Lead", active: true,  workTypes: null, timezone: "Australia/Brisbane" },
  { id: "u3", name: "Chen Wu",         email: "chen.wu@fedex.com.au",         roleId: "dispatch", desk: "Dispatch", active: true,  workTypes: ["standard", "nextflight"], timezone: "Australia/Melbourne" },
  { id: "u4", name: "Sofia Marchetti", email: "sofia.marchetti@fedex.com.au", roleId: "finance",  desk: "Finance",  active: true,  workTypes: null, timezone: "Australia/Perth" },
  { id: "u5", name: "Liam Byrne",      email: "liam.byrne@fedex.com.au",      roleId: "readonly", desk: "Support",  active: false, workTypes: ["standard"], timezone: "Australia/Adelaide" },
];

/* AU capital-city timezones (distinct offsets first). Used for the CE header world-clock
   and the per-agent timezone setting. */
const AU_ZONES = [
  { city: "Perth",     tz: "Australia/Perth",    abbr: "AWST" },
  { city: "Darwin",    tz: "Australia/Darwin",   abbr: "ACST" },
  { city: "Adelaide",  tz: "Australia/Adelaide", abbr: "ACST/ACDT" },
  { city: "Brisbane",  tz: "Australia/Brisbane", abbr: "AEST" },
  { city: "Sydney",    tz: "Australia/Sydney",   abbr: "AEST/AEDT" },
  { city: "Melbourne", tz: "Australia/Melbourne", abbr: "AEST/AEDT" },
  { city: "Canberra",  tz: "Australia/Sydney",   abbr: "AEST/AEDT" },
  { city: "Hobart",    tz: "Australia/Hobart",   abbr: "AEST/AEDT" },
];
function tzTime(tz, opts) { try { return new Date().toLocaleTimeString("en-AU", { hour: "2-digit", minute: "2-digit", timeZone: tz, ...(opts || {}) }); } catch (e) { return ""; } }
function tzCity(tz) { const z = AU_ZONES.find(z => z.tz === tz); return z ? z.city : (tz || "").split("/").pop().replace(/_/g, " "); }

const SERVICE_AGENTS = [
  { id: "sa1", name: "Metro Freight Services", state: "VIC", serviceAreas: ["VIC", "TAS"], city: "Melbourne", email: "ops@metrofreight.com.au", contact: "Raj Patel", phone: "+61 3 9412 8800", contacts: [{ name: "Raj Patel", phone: "+61 3 9412 8800" }, { name: "Dispatch desk", phone: "+61 3 9412 8801" }], notes: "Covers metro Melbourne + regional VIC and Hobart runs. After-hours by arrangement. DG-approved to IATA. Invoices weekly.", active: true },
  { id: "sa2", name: "Harbour City Couriers",  state: "NSW", serviceAreas: ["NSW", "ACT"], city: "Sydney", email: "dispatch@harbourcity.com.au", contact: "Elena Ruiz", phone: "+61 2 9210 4471", contacts: [{ name: "Elena Ruiz", phone: "+61 2 9210 4471" }, { name: "Canberra depot", phone: "+61 2 6100 2210" }], notes: "Sydney metro + Canberra corridor. Tail-lift and two-person lifts available on request.", active: true },
  { id: "sa3", name: "Sunstate Logistics",     contact: "Mark Boland", state: "QLD", serviceAreas: ["QLD"], city: "Brisbane",  email: "bookings@sunstatelog.com.au", phone: "+61 7 3140 2299", contacts: [{ name: "Mark Boland", phone: "+61 7 3140 2299" }], notes: "", active: true },
  { id: "sa4", name: "Westline Distribution",  contact: "Priya Naidu", state: "WA",  serviceAreas: ["WA"], city: "Perth",     email: "ops@westline.com.au",         phone: "+61 8 6210 5533", contacts: [{ name: "Priya Naidu", phone: "+61 8 6210 5533" }], notes: "", active: true },
  { id: "sa5", name: "Adelaide Metro Freight", contact: "Tom Fischer", state: "SA",  serviceAreas: ["SA", "NT"], city: "Adelaide",  email: "hub@adlmetro.com.au",         phone: "",                contacts: [{ name: "Tom Fischer", phone: "" }], notes: "Adelaide + Darwin line-haul. Email only — no SMS.", active: true },
];
function serviceAgentById(list, id) { return (list || SERVICE_AGENTS).find(a => a.id === id) || null; }

/* Force a specific flight for known demo consignments (applied at render time,
   independent of any stored seed). Key = tracking code, value = flight IATA. */
const NF_FLIGHT_OVERRIDES = { "FX-5MS9-3120": "QF445" };

function isNextFlight(b) {
  if (!b) return false;
  if (b.transport === "road") return false;                      // interstate but switched to road line-haul
  if (b.pickup && b.dropoff) {
    const a = suburbState(b.pickup.suburb), c = suburbState(b.dropoff.suburb);
    if (a && c) return a !== c;                                  // both states known -> genuine interstate only
  }
  return b.typeId === "nextflight" || b.interstate === true;     // fallback when a state is unknown
}

/* Resolve flight + couriers + AWB for a Next Flight booking (deterministic per tracking). */
function nextFlightInfo(b) {
  if (!b) return null;
  const oState = b.pickup ? suburbState(b.pickup.suburb) : null;
  const dState = (b.dropoff ? suburbState(b.dropoff.suburb) : null) || b.state;
  let dep = (oState && AIRPORTS[oState]) ? { ...AIRPORTS[oState], state: oState } : { ...AIRPORTS.NSW, state: "NSW" };
  let arr = (dState && AIRPORTS[dState]) ? { ...AIRPORTS[dState], state: dState } : { ...AIRPORTS.VIC, state: "VIC" };
  // Operator overrides for which port it flies out of / into (item: change airport when editing).
  if (b.originPort && AIRPORT_BY_CODE[b.originPort]) dep = { ...AIRPORT_BY_CODE[b.originPort] };
  if (b.destPort && AIRPORT_BY_CODE[b.destPort]) arr = { ...AIRPORT_BY_CODE[b.destPort] };
  let s = 0; const code = b.tracking || "FX"; for (let i = 0; i < code.length; i++) s = (s * 31 + code.charCodeAt(i)) >>> 0;
  const onRoute = FLIGHTS.filter(f => f.dep_iata === dep.code && f.arr_iata === arr.code);
  const fallback = onRoute.length ? onRoute[s % onRoute.length] : {
    flight_iata: "QF" + (400 + s % 500), airline_iata: "QF", airline_name: "Qantas", dep_iata: dep.code, arr_iata: arr.code,
    dep_hm: "10:30", arr_hm: "12:10", dep_est_hm: "10:30", arr_est_hm: "12:10", status: "scheduled", dur: routeMins(dep.code, arr.code),
  };
  const forcedIata = b.flightIata || NF_FLIGHT_OVERRIDES[b.tracking];
  let flight;
  if (forcedIata) {
    flight = flightByIata(forcedIata);
    if (!flight) {
      const al2 = AIRLINES.find(a => forcedIata.toUpperCase().startsWith(a.iata)) || AIRLINES[0];
      flight = { ...fallback, flight_iata: forcedIata.toUpperCase(), airline_iata: al2.iata, airline_name: al2.name };
    }
    // If the port was changed and the forced flight no longer serves the route, re-derive.
    if (flight && (flight.dep_iata !== dep.code || flight.arr_iata !== arr.code)) flight = fallback;
  } else {
    flight = fallback;
  }
  const pickupDriver = b.driver || DRIVERS[s % DRIVERS.length];
  const deliveryDriver = DRIVERS[(s + 3) % DRIVERS.length];
  const al = AIRLINES.find(a => a.iata === (flight.airline_iata || "QF")) || AIRLINES[0];
  const awb = b.awb || (al.prefix + "-" + String(10000000 + (s % 89999999)));
  const decl = b.declaration || {};
  const items = decl.qty ? Math.max(1, parseInt(decl.qty, 10) || 1) : (1 + (s % 5));
  const declW = parseFloat(decl.weight);
  const wr = ({ courier: [2, 25], wagon: [20, 120], halfvan: [60, 480], van: [120, 900] })[b.vehicleId] || [2, 25];
  const weightKg = (!isNaN(declW) && declW > 0) ? declW : Math.round((wr[0] + (s % (wr[1] - wr[0]))) * 10) / 10;
  // Line-haul override — the interstate leg can travel by truck or train instead of a flight.
  // Truck legs carry a registration; trains carry a train / service number. Both may carry a tracking no.
  const lhMode = (b.transportMode === "truck" || b.transportMode === "train") ? b.transportMode : "flight";
  if (lhMode !== "flight") {
    const lh = b.linehaul || {};
    const refLabel = ((lhMode === "truck" ? (lh.rego || lh.trackingNo) : (lh.ref || lh.trackingNo)) || (lhMode === "truck" ? "TRUCK" : "TRAIN")).toUpperCase();
    const lhFlight = { ...flight, flight_iata: refLabel, airline_name: lh.carrier || (lhMode === "truck" ? "Road line haul" : "Rail line haul"), dep_hm: lh.pickupHm || "", arr_hm: lh.deliveryHm || "", dep_est_hm: lh.pickupHm || "", arr_est_hm: lh.deliveryHm || "", status: "scheduled", aircraft: "", gate: "", terminal: "" };
    return { originState: dep.state, destState: arr.state, dep, arr, mode: lhMode, linehaul: lh, flight: lhFlight, flightIata: refLabel, pickupDriver, deliveryDriver, awb, items, weightKg, eta: lh.deliveryHm || "" };
  }
  return { originState: dep.state, destState: arr.state, dep, arr, mode: "flight", linehaul: null, flight, flightIata: flight.flight_iata, pickupDriver, deliveryDriver, awb, items, weightKg, eta: flight.arr_est_hm || flight.arr_hm };
}

/* Registry of airway bills currently in use across NF bookings, grouped by AWB.
   Each record: awb, flight, route, dep time, and the consignments already on it —
   so an operator can judge whether to add another booking to that flight. */
function airwayBillRegistry(bookings, opts) {
  const activeOnly = !opts || opts.activeOnly !== false;
  const byAwb = {};
  (bookings || []).forEach(b => {
    if (!isNextFlight(b)) return;
    if (b.status === "Cancelled") return;
    if (activeOnly && b.status === "Delivered") return;
    const nf = nextFlightInfo(b);
    const awb = b.awb || nf.awb;
    if (!byAwb[awb]) byAwb[awb] = { awb, flightIata: nf.flightIata, flight: nf.flight, dep: nf.dep, arr: nf.arr, depHm: nf.flight.dep_hm, eta: nf.eta, status: nf.flight.status, consignments: [] };
    byAwb[awb].consignments.push({ tracking: b.tracking, customerName: b.customerName, from: b.pickup ? b.pickup.suburb : "", to: b.dropoff ? b.dropoff.suburb : "", items: nf.items, weightKg: nf.weightKg });
  });
  return Object.values(byAwb).sort((a, b) => (a.depHm || "").localeCompare(b.depHm || ""));
}

/* Resolve a typed flight number to a flight in the schedule (for adding a new AWB). */
function resolveFlight(iata) {
  const f = flightByIata(iata);
  if (!f) return null;
  return { flightIata: f.flight_iata, flight: f, dep: AIRPORT_BY_CODE[f.dep_iata], arr: AIRPORT_BY_CODE[f.arr_iata], depHm: f.dep_hm, eta: f.arr_est_hm || f.arr_hm, status: f.status };
}

/* Generate a fresh AWB for an airline prefix (or QF default). */
function genAwb(airlineIata) {
  const al = AIRLINES.find(a => a.iata === airlineIata) || AIRLINES[0];
  return al.prefix + "-" + String(10000000 + Math.floor(Math.random() * 89999999));
}

/* ---------------- Helpers ---------------- */
const CURRENCIES = {
  AUD: { symbol: "$", code: "AUD", locale: "en-AU" },
  NZD: { symbol: "$", code: "NZD", locale: "en-NZ" },
  USD: { symbol: "$", code: "USD", locale: "en-US" },
};
function money(n, cur = "AUD") {
  const c = CURRENCIES[cur] || CURRENCIES.AUD;
  return c.symbol + (Number(n) || 0).toLocaleString(c.locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function genTracking() {
  const block = () => Math.random().toString(36).slice(2, 6).toUpperCase();
  return "FX-" + block() + "-" + Math.floor(1000 + Math.random() * 8999);
}
function fmtDate(iso) {
  try { return new Date(iso + "T00:00:00").toLocaleDateString("en-AU", { day: "numeric", month: "short", year: "numeric" }); }
  catch (e) { return iso; }
}
function todayISO() { return new Date().toISOString().slice(0, 10); }

/* ---------------- DG acceptance rules (OPEN — placeholder pending Michael Young) ----------------
   Approved UN types each carry a limited-quantity ceiling. At/under the ceiling the platform
   auto-accepts; above it (or for blocked types) the booking is stopped and routed to CE so the
   shipment can be checked for travel. */
const DG_RULES = {
  approvedTypes: ["un3090", "un3091-contained", "un3091-packed", "un3480", "un3481-contained", "un3481-packed", "un3373", "un1845", "excepted"],
  limitedQty: { un3090: 2, "un3091-contained": 4, "un3091-packed": 4, un3480: 2, "un3481-contained": 4, "un3481-packed": 4, un3373: 4, un1845: 2, excepted: 1 },
  blockedTypes: ["un3245", "other"],
};
function dgAssess(typeValue, qty) {
  if (!typeValue) return { status: "none" };
  const q = Math.max(1, parseInt(qty, 10) || 1);
  if (DG_RULES.blockedTypes.includes(typeValue)) return { status: "blocked", reason: "This dangerous-goods type must be checked for travel by Customer Service before it can be booked on the platform." };
  if (!DG_RULES.approvedTypes.includes(typeValue)) return { status: "blocked", reason: "Not an approved dangerous-goods type for self-service booking. Refer the caller to Customer Service." };
  const ceil = DG_RULES.limitedQty[typeValue];
  if (ceil != null && q <= ceil) return { status: "accept", ceil, reason: "Within the limited quantity (max " + ceil + "). Booking can be auto-accepted." };
  return { status: "blocked", ceil, reason: "Above the limited quantity (max " + ceil + "). Booking must go through Customer Service so the shipment is checked for travel." };
}

/* ---------------- Depots (OPEN — placeholder pending national list from ops) ---------------- */
const DEPOTS = [
  { id: "dep-syd", state: "NSW", name: "Sydney — Mascot Freight Depot", address: "Unit 3, 12 Coward St, Mascot NSW 2020", hours: "Mon–Fri 6:00 AM – 8:00 PM · Sat 7:00 AM – 2:00 PM", services: ["Drop-off", "Collection"], note: "Oversize freight collection by appointment." },
  { id: "dep-mel", state: "VIC", name: "Melbourne — Tullamarine Depot", address: "45 South Centre Rd, Tullamarine VIC 3043", hours: "Mon–Fri 6:00 AM – 9:00 PM · Sat 7:00 AM – 3:00 PM", services: ["Drop-off", "Collection"], note: "" },
  { id: "dep-bne", state: "QLD", name: "Brisbane — Eagle Farm Depot", address: "120 Lomandra Dr, Eagle Farm QLD 4009", hours: "Mon–Fri 6:30 AM – 7:00 PM · Sat 8:00 AM – 12:00 PM", services: ["Drop-off", "Collection"], note: "No dangerous-goods drop-off at this depot." },
  { id: "dep-per", state: "WA", name: "Perth — Perth Airport Depot", address: "5 Fauntleroy Ave, Redcliffe WA 6104", hours: "Mon–Fri 7:00 AM – 6:00 PM", services: ["Drop-off", "Collection"], note: "" },
  { id: "dep-adl", state: "SA", name: "Adelaide — Airport Depot", address: "3 Sir Richard Williams Ave, Adelaide Airport SA 5950", hours: "Mon–Fri 7:00 AM – 6:00 PM", services: ["Drop-off", "Collection"], note: "" },
  { id: "dep-cbr", state: "ACT", name: "Canberra — Fyshwick Depot", address: "18 Kembla St, Fyshwick ACT 2609", hours: "Mon–Fri 7:30 AM – 5:30 PM", services: ["Drop-off"], note: "Collection routed via Sydney depot." },
];

/* ---------------- Inquiry query types (editable in the background) ---------------- */
const INQUIRY_TYPES = ["ETA / where is my delivery", "Cancel or change booking", "Damaged item", "Delayed delivery", "Unable to locate"];

/* ---------------- Quote retention ---------------- */
const SAVED_QUOTE_VALID_DAYS = 14;   // customer-facing quote validity
const QUOTE_RETENTION_DAYS = 180;    // CE saved-quote repository retention

/* ---------------- Operating hours (item 18) ---------------- */
const OPERATING_HOURS = {
  availability: "24 / 7 / 365",
  core: "6:00 AM – 6:00 PM",
  note: "Booking, tracking and inquiries are available around the clock, every day of the year. Standard service hours are 6:00 AM – 6:00 PM; outside these hours after-hours surcharges may apply.",
};

/* ---------------- Inquiries (shared across customer portal + CE console) ----------------
   Stored under one key so an inquiry lodged in the customer portal shows in the CE console. */
const INQUIRY_LS = "fedexInquiries_v1";
const INQUIRY_STATUS = {
  "in-progress": { label: "In progress", tone: "warn", dot: "var(--warn)" },
  "resolved":    { label: "Resolved",    tone: "success", dot: "#22b86e" },
};
function genInquiryRef() { return "INQ-" + Math.floor(100000 + Math.random() * 899999); }
function _iso(daysAgo, h = 10) { const d = new Date(); d.setDate(d.getDate() - daysAgo); d.setHours(h, 0, 0, 0); return d.toISOString(); }
const INQUIRY_SEED = [
  { ref: "INQ-204815", tracking: "FX-8LM3-5521", product: "Next Flight", customerId: "c3", customerName: "Acme Retail Co.", customerEmail: "priya@acmeretail.com.au", type: "ETA / where is my delivery", message: "Client needs an updated ETA for the Brisbane leg — end customer is waiting on site.", status: "in-progress", createdAt: _iso(1, 9), updatedAt: _iso(1, 9), comments: [], rating: null, agent: null },
  { ref: "INQ-204790", tracking: "FX-2KP8-1190", product: "Courier", customerId: "c1", customerName: "Meridian Group", customerEmail: "dispatch@meridiangroup.com.au", type: "Delayed delivery", message: "Pickup was 40 min late this morning, please advise what happened.", status: "resolved", createdAt: _iso(5, 8), updatedAt: _iso(4, 15), comments: [{ author: "Jordan Diaz", role: "agent", body: "Driver was held up in traffic on the M4; parcel delivered 14:20. Apologies for the delay.", at: _iso(4, 15) }], rating: 4, agent: "Jordan Diaz" },
  { ref: "INQ-204711", tracking: "FX-6QW2-3380", product: "Fail-Safe", customerId: "c5", customerName: "Bayside Cellars", customerEmail: "orders@baysidecellars.com.au", type: "Damaged item", message: "One carton arrived crushed. Photos attached.", status: "resolved", createdAt: _iso(9, 11), updatedAt: _iso(7, 13), comments: [{ author: "Amara Osei", role: "agent", body: "Lodged a claim and arranged redelivery of replacement stock at no charge.", at: _iso(7, 13) }], rating: 5, agent: "Amara Osei" },
];
function loadInquiries() { try { const s = JSON.parse(localStorage.getItem(INQUIRY_LS)); return Array.isArray(s) ? s : INQUIRY_SEED.slice(); } catch (e) { return INQUIRY_SEED.slice(); } }
function saveInquiries(list) { try { localStorage.setItem(INQUIRY_LS, JSON.stringify(list)); } catch (e) {} }

/* ---------------- Announcements (CE posts → customer portal banner) ---------------- */
const ANNOUNCE_LS = "fedexAnnouncements_v1";
function genAnnId() { return "A-" + Math.floor(100000 + Math.random() * 899999); }
function _isoPlus(days) { const d = new Date(); d.setDate(d.getDate() + days); d.setHours(23, 59, 0, 0); return d.toISOString(); }
const ANNOUNCE_SEED = [
  { id: "A-100240", title: "Public holiday hours", body: "Depots close 1:00 PM this Friday for the public holiday. Next Flight freight cut-offs move two hours earlier — please book early.", audience: "All customers", createdAt: _iso(1, 9), validUntil: _isoPlus(6) },
];
function loadAnnouncements() { try { const s = JSON.parse(localStorage.getItem(ANNOUNCE_LS)); return Array.isArray(s) ? s : ANNOUNCE_SEED.slice(); } catch (e) { return ANNOUNCE_SEED.slice(); } }
function saveAnnouncements(list) { try { localStorage.setItem(ANNOUNCE_LS, JSON.stringify(list)); } catch (e) {} }
function activeAnnouncements(list) { const now = Date.now(); return (list || []).filter(a => !a.validUntil || new Date(a.validUntil).getTime() >= now); }

/* ---------------- Persistence ---------------- */const LS_KEY = "fedexPortal_v1";
function loadState() {
  try { return JSON.parse(localStorage.getItem(LS_KEY)) || {}; } catch (e) { return {}; }
}
function saveState(patch) {
  const cur = loadState();
  const next = { ...cur, ...patch };
  try { localStorage.setItem(LS_KEY, JSON.stringify(next)); } catch (e) {}
  return next;
}

Object.assign(window, {
  FX: {
    VEHICLES, SERVICES, SUBURBS, ADDRESS_BOOK, HISTORY, NOTICES, CURRENCIES,
    ACCOUNTS, INVOICES, USERS, PAYMENT_METHODS, COMPANY,
    CUSTOMERS, BOOKING_TYPES, SERVICE_CHARGES, DRIVERS, DELIVERY_RUNS, runProgress,
    DANGEROUS_GOODS, BOOKING_DISCLAIMER,
    AIRPORTS, AIRPORT_BY_CODE, AIRPORT_CODES, AIRLINES, FLIGHTS, FLIGHT_STATUS,
    flightByIata, searchFlights, nextFlightInfo, isNextFlight, routeMins,
    airwayBillRegistry, resolveFlight, genAwb,
    USER_ROLES, roleById, CONSOLE_USERS, SERVICE_AGENTS, serviceAgentById,
    AU_ZONES, tzTime, tzCity,
    SUBURB_POSTCODES, suburbPostcode, searchSuburbs,
    ITEM_TYPES, ITEM_DEFAULTS, typeDefaults, FUEL_LEVY_PCT, VEHICLE_CAPS, blankItem, itemTotals, autoVehicleForItems, genQuoteNo, quoteExpiryISO, quoteDaysLeft,
    DEFAULT_RULES, loadRules, saveRules, activeRules, setActiveRules,
    DG_RULES, dgAssess, DEPOTS, INQUIRY_TYPES, SAVED_QUOTE_VALID_DAYS, QUOTE_RETENTION_DAYS, OPERATING_HOURS,
    INQUIRY_STATUS, genInquiryRef, loadInquiries, saveInquiries,
    genAnnId, loadAnnouncements, saveAnnouncements, activeAnnouncements,
    suburbNames, findSuburb, suburbState, distanceKm, quote, money, genTracking, fmtDate, todayISO, emptyAddr, serviceCode, serviceName,
    STATES,
    loadState, saveState,
  },
});
