Mobile module v1.25.1 - Perfect CRM
===================================

URL:  https://<your-crm>/mobile   (sidebar item "Mobile")

WHAT'S NEW IN 1.25.1  (WHITE-LABEL FIX)
  The mobile app now follows the site's own branding from Settings >
  General instead of showing the vendor name:
  - Home screen header, browser tab title and the "Add to home screen"
    name (manifest name / short_name, apple-mobile-web-app-title) all use
    Company name (then Site name). BH_APP_NAME is only a last resort when
    neither is set. The home title was previously hard-coded.
  - The uploaded company Logo is shown in the home header next to the
    name and on the mobile Settings screen (with a note that name/logo/
    icon come from Settings > General).
  - The PWA home-screen icon is built from the company logo (centered on
    the header color, inside the maskable safe zone) when one is
    uploaded; otherwise the initials icon as before. SVG logos fall back
    to initials (GD cannot rasterise SVG).
  - Icon caching keyed to the branding: changing the company name, logo
    or header color regenerates the icon and bumps the ?v= on the
    manifest and apple-touch-icon links, so phones stop showing the old
    icon. Old icon-192.png / icon-512.png files are removed.
  - Title separator is plain ASCII ("Tickets - Company").
  Deploy: overwrite modules/mobile, bust OPcache, hard-refresh. Users who
  already saved the app to their home screen should remove and re-add it
  once to pick up the new name and icon.

WHAT'S NEW IN 1.25.0
  Invoice send now MIRRORS the desktop Invoices module (InvoicesController@
  ajaxSend) so a mobile-sent invoice is identical to a desktop-sent one:
  - Attaches the PDF invoice (via the real InvoicePDF class), generating it
    first if missing and stamping pdf_path, exactly like the desktop.
  - Uses the same email layout: header bar with number/due, a secure
    online-payment block listing the real enabled gateways
    (Invoice::activeGateways -> Stripe / PayPal) with a "Pay $X online"
    button, item table, totals with Balance due, notes, and a footer with
    the company contact from settings. Accent color reads accent_color
    (falls back to the mobile header color).
  - Sends through BHMailer with the PDF attachment and the correct
    from_name / from_email (invoice_company_email -> smtp_from_email ->
    company_email), falling back to the mobile BhMail helper and then
    native mail(), same chain as the desktop.
  - All real classes (Invoice, InvoicePDF, Client, BHMailer, bh_setting)
    are used only when present; if any is missing it degrades gracefully.
  Items are still read strictly from the one invoice being sent. Combined
  with 1.24.1 (which removed the leaky scan-to-pay QR block), the emailed
  invoice now contains only that invoice's data, plus its PDF.
  NOTE (ASCII): the desktop gateway block uses a lock emoji; mobile keeps
  pure-ASCII text ("Secure online payment") per the project convention.
  The layout is otherwise the same.

WHAT'S NEW IN 1.24.1  (CRITICAL FIX)
  - Invoice email no longer embeds the "scan to pay another way" QR image
    block. That block pasted the staff-uploaded payment QR images into the
    client email; when one of those uploads was a screenshot containing
    another invoice's line items (or a screenshot of a different invoice's
    pay page), that unrelated data went out to the customer. The invoice
    email now sends ONLY this invoice: header, amount/paid/due, this
    invoice's line items, and the correct per-invoice View & pay link --
    matching what the desktop Invoices module sends.
  - Invoice + estimate emails: the line-item header row used <th> cells,
    which some mail clients (Outlook) rendered as literal "<th ...>Amount"
    text. Switched to styled <td> cells so the header renders everywhere.
  - Invoice email now uses the real Invoice model helpers when present
    (Invoice::number / items / balance / payToken) so the emailed data
    matches the desktop exactly. Items are still read strictly from the
    one invoice being sent.
  NOTE: the scan-to-pay QR codes still show on the in-app invoice SCREEN
  for staff; they are simply no longer emailed to clients. If any of those
  saved QR images are actually screenshots of other data, replace them in
  Settings > Payments.

WHAT'S NEW IN 1.24.0
  EXPENSES (shares bh_expenses / bh_expense_categories with the desktop
  Expenses module, so entries show up in both places and roll into the
  same tax/reports center):
  - New Expenses tile on the springboard + a "Log expense" quick action, an
    Expenses settings group (color/shape/border, default orange #C2410C),
    and two screens: list and log-expense form.
  - RECEIPT PHOTOS: the log form has a "Snap a receipt" capture tile
    (camera on mobile, or pick from library) with an instant preview. The
    photo is downscaled (<=1400px, JPEG q82) and written to
    assets/uploads/mobile/rcpt-<rand>.jpg; its filename is stored in the
    expense's spare `reference` column, so NOTHING in the shared schema
    changes and the desktop just sees a filename string it ignores. The
    list shows a thumbnail when a receipt exists, a placeholder icon when
    not. Receipts are served login-gated at /mobile/expense/receipt/{id}.
    If assets/ is not writable the photo is skipped and the expense still
    saves.
  - Category + subcategory are two linked dropdowns fed from
    bh_expense_categories (subcategory list filters to the chosen parent,
    same as desktop) and stored as strings in category/subcategory to
    match the desktop exactly.
  - Also captures: amount, description (name), date (defaults today),
    optional customer link (client_id), billable toggle, and a note.
    created_by is set to the logged-in staff user. List is grouped by
    month with a month-to-date total card up top.
  - Boot self-heals both tables (IF NOT EXISTS) and seeds the 16 parent
    categories ONLY when the categories table is empty (fresh install
    without the desktop module); the desktop module owns the full
    subcategory seed.
  ROUTES: GET /mobile/expenses, GET /mobile/expense/new,
  POST /mobile/expense/save, GET /mobile/expense/subcats?cat=,
  GET /mobile/expense/receipt/{id}.

WHAT'S NEW IN 1.23.0
  MILEAGE (shares bh_mileage with the desktop Mileage Tracker module, so
  trips show up in both places and roll into the same reports):
  - New Mileage tile on the springboard + a "Log trip" quick action, a
    Mileage settings group (color/shape/border, default purple #7C4DB8),
    and two screens: list and log-trip form.
  - List shows current user's trips ONLY (filed under Auth user_id),
    grouped by month, newest first, with two stat cards up top: miles and
    reimbursement for the current month.
  - Log-trip form: date (defaults today), from, to, ROUND TRIP toggle
    (doubles the miles on save AND in the live readout), miles, rate
    ($/mi, prefilled from the mileage_irs_rate setting, 0.67 fallback),
    purpose, optional customer dropdown (links client_id), vehicle, notes,
    and a live Reimbursement readout (miles x rate, doubled if round trip)
    that updates as you type. Reimbursement is computed and stored on save
    the same way the desktop does (round(miles * rate, 2)).
  - Boot self-heals bh_mileage with the exact desktop install.sql schema
    (IF NOT EXISTS), so it works even if the desktop Mileage module is not
    installed.
  ROUTES: GET /mobile/mileage, GET /mobile/mileage/new,
  POST /mobile/mileage/save.

WHAT'S NEW IN 1.22.0
  - Line-item card readability (invoice AND estimate forms, shared builder):
    Qty and Rate each get their own full-width labeled line instead of two
    bare unlabeled boxes side by side, and the row below reads
    "Line total  $X  [x]". No data or POST changes; same field names.

WHAT'S NEW IN 1.21.0
  ESTIMATES (shares bh_proposals with the desktop Proposals module, type
  'estimate', so everything shows up in both places with the same EST-#####
  numbers):
  - New Estimates tile on the springboard + a "New estimate" quick action,
    an Estimates settings group (color/shape/border), and full screens:
    list, detail, and a create form that reuses the invoice line-item
    builder (product picker included). Items are stored in the desktop
    proposals format (name/desc/qty/rate/tax/amount).
  - Detail screen: total, status pill, valid-until, itemized table, and
    actions: Send to client (email), Mark approved / Mark rejected, and
    CONVERT TO INVOICE. Convert creates a Draft invoice (next INV-#####,
    items + notes + tax/discount carried over; falls back to the minimal
    column set if tax_rate/discount columns are missing), stamps the
    estimate Approved + converted_to, and lands on the invoice screen.
    Hitting Send there flips it Draft -> Unpaid, same as before. Once
    converted, the estimate shows a green "Converted to invoice" link
    instead of the buttons.
  - Send email: itemized table, total, valid-until, notes, and a "reply to
    approve" line. On success stamps last_sent (desktop format) and flips
    Draft -> Sent. POINTER: there is no public client-facing estimate URL,
    so the email has no view button. If you add a public estimate page,
    tell me and I will wire the link in sendEstimate().
  - New-estimate form pre-fills Valid until from bh_proposal_settings
    default_expiry_days (30 if unset).
  - Boot self-heals bh_proposals + bh_proposal_settings with the exact
    desktop install.sql schema (IF NOT EXISTS), so estimates work even if
    the desktop Proposals module is not installed.
  ROUTES: GET /mobile/estimates, GET /mobile/estimate?id=, GET
  /mobile/estimate/new, POST /mobile/estimate/save|send|status|convert.

WHAT'S NEW IN 1.16.0
  PAYMENT QR CODES (scan to pay; no integration, just images you upload):
  - Settings has a new "Payments (scan to pay)" section. Upload a QR image for
    Zelle, Venmo, Cash App, PayPal, plus two free-form Custom slots you can name
    anything (Bitcoin, Square, Wise, Apple Cash, Google Pay). Each shows a brand
    icon, an optional handle line (your $Cashtag, @username, email/phone), and a
    Remove option. One master checkbox shows or hides the whole set.
  - Configured codes appear on the invoice screen when a balance is due, under
    the card scan-to-pay block, each with its icon + handle, and they are also
    added to the invoice email so a client can scan and pay from their phone.
  - Images are stored as PNG files under assets/uploads/mobile and served as
    static files (same spot as the background image). Nothing is transmitted to
    any third party; these are just your own static QR images. If assets/ is not
    writable the upload is skipped.

WHAT'S NEW IN 1.15.0
  - Invoice line items now show on the invoice screen (item/desc, qty, rate,
    amount) with discount + tax rows when present, parsed from bh_invoices.items
    the same way the desktop does (description normalized to desc).
  - The invoice email now includes the same itemized table + discount/tax.
  - Fixed the stale "not connected to mailer" message on a failed send.

WHAT'S NEW IN 1.14.0
  EMAIL (all via core BhMail::send -- no PDF attachment; mobile sends links):
  - Invoice send now actually emails the client. The send form has a "Send to"
    field prefilled with the client email; the email shows amount/paid/due and a
    "View & pay invoice" button + link to the real /invoices/pay/{id}?tok= page.
    On success it stamps last_sent and flips Draft -> Unpaid (same as desktop).
  - Ticket replies email the ticket contact (replies only, NOT internal notes).
  - Creating a customer sends a welcome email to the client (if email given).
  All email is best-effort and fails safe -- if BhMail/SMTP is unavailable it just
  does nothing rather than erroring.

  SAVE TO DESKTOP (installable PWA):
  - New "Save to desktop" button at the top of Settings. On Android/Brave it fires
    the native install prompt; otherwise it shows the "Add to Home screen" hint.
  - Adds a web app manifest (/mobile/manifest), a minimal service worker
    (/mobile/sw.js) so the install prompt qualifies, and auto-generated app icons
    (/mobile/icon/192, /mobile/icon/512 -- colored tile + monogram from your
    company name, cached into assets/uploads/mobile/). Head now carries the
    manifest link, theme-color, and apple-touch-icon so it installs full-screen.

WHAT'S NEW IN 1.13.0
  - Create customers: "New customer" button on the customers list -> a form
    (name required; email/phone/website/address/city/state/zip/notes), saved to
    bh_clients, then lands on the new customer's detail page.
  - Background image now stored as a file, not in the DB. It's written to
    assets/uploads/mobile/bg-<rand>.jpg (downscaled <=1080, JPEG q82) and served
    as a STATIC file straight from Apache -- no PHP, no DB read -- so it loads
    fast. Replacing the image deletes the old file; the old DB blob is cleared.
    Falls back to the DB method automatically if assets/ ever isn't writable,
    and the /mobile/bg endpoint still serves legacy DB-stored images.

WHAT'S NEW IN 1.12.0
  - Ticket reply composer on the ticket screen: textarea + Send, with an
    "Internal note" checkbox (type=note vs reply). Mirrors Ticket::addReply —
    inserts into bh_ticket_replies (body wrapped as <p>nl2br(escaped)</p>,
    author from Auth::user()/Auth::id()) then bumps last_reply_at + updated_at.
    Skips the desktop's client email notification (mobile is staff-side only).
    Hidden on encrypted tickets.
  - Closing a ticket now returns to the tickets list (was: back to the ticket).

WHAT'S NEW IN 1.11.0  (wired to the real codebase — no guessing)
  - Password reveal/copy now WORK. decryptSecret() mirrors PasswordsController
    exactly: AES-256-GCM (CBC fallback), base64 of GCM:/CBC: + iv(+tag) + cipher,
    keyed on BH_ENCRYPTION_KEY. Reveals are logged to bh_password_audit.
    NOTE: the desktop gates reveal behind a 15-min re-auth "vault"; mobile reveals
    on staff login alone (no re-auth prompt). Say the word and I'll add a mobile
    unlock step for full parity.
  - Passwords list cleaned up: title / username (truncates with ellipsis instead
    of running under the icons) / password on its own line (long values wrap),
    eye + copy fixed on the right so they never overlap.
  - Pay QR fixed to the REAL pay URL the desktop uses:
    BH_APP_URL/invoices/pay/{id}?tok={token}  where
    token = substr(hash_hmac('sha256','pay:'+id, BH_SECRET_KEY), 0, 24).
    (The old /pay/{token} guess didn't exist; pay_token column is unused here.)
    Removed the now-unneeded "Invoice pay link" setting.
  - Ticket author/created_by now use Auth::user()/Auth::id() (confirmed real).

WHAT'S NEW IN 1.10.0
  - The pay QR target is now a setting (Settings > Appearance > "Invoice pay link"),
    since /pay/{token} 404'd. Set it to your real public pay URL using placeholders:
      {base}   -> your app URL (no trailing slash)
      {token}  -> invoice pay_token
      {id}     -> invoice id
      {number} -> invoice number
    e.g.  {base}/invoices/pay/{token}   or   {base}/portal/pay/{token}
    The link is shown under the QR so you can confirm it resolves.

WHAT'S NEW IN 1.9.0
  - Scan-to-pay QR on the invoice detail (shown when a balance is due). It encodes
    the invoice's public pay URL; the client scans and pays. QR is rendered in the
    browser (qrcodejs from cdnjs) — nothing about the invoice is sent to any third
    party. Includes the visible link + a "Copy pay link" button. A pay_token is
    generated for the invoice if it doesn't already have one.

  POINTER: the pay URL currently assumes the public pay page is at /pay/{token}.
  Tell me your real public pay/Stripe-Connect pay-by-link URL pattern and I'll fix
  that one line (in invoiceView()). The link is shown under the QR so you can
  verify where it points.

WHAT'S NEW IN 1.8.0
  - Adjustable font size (Settings > Appearance > Font size: Small / Normal /
    Large / Larger / Largest). It scales every text element in the app
    proportionally via a single CSS multiplier (--fs) that all font sizes
    reference — class-based and inline alike.

WHAT'S NEW IN 1.7.0
  - Ticket bodies render as HTML now (they're stored as <p>…</p>); they were being
    escaped and showing raw tags. Rendered through a sanitizer that strips
    script/style, on* handlers, and javascript: URLs (staff-only, but safe).
  - Create ticket: "New ticket" on the Tickets list and the home tile open a form
    (customer, subject, priority, message). Creates the ticket + first reply.
  - Close ticket: button on the ticket view (uses your first is_closed status,
    else 'Closed'; stamps resolved_at).
  - Assign to: dropdown of active staff (bh_users, role admin/staff/contractor)
    on the ticket view; sets bh_tickets.assigned_to.

  POINTER: current-staff lookup for new tickets/replies is best-effort
  (Auth::user()/Auth::id()/session). If your Auth has a canonical accessor, tell
  me and I'll use it so author/created_by are always right.

WHAT'S NEW IN 1.6.0
  - Open a ticket: ticket rows are now tappable and open a conversation view
    (subject, status/priority, client, and the reply/note thread from
    bh_ticket_replies). Encrypted tickets show a notice instead of bodies.
  - New invoice form: Amount and Due date fields are now full-width/large like
    the Customer dropdown (they were native number/date inputs my width rule
    missed).
  - Invoice list subtitle (INV-#### / client) is darker and a point larger.

WHAT'S NEW IN 1.5.0
  - Borders darkened a bit more across cards/rows.
  - Tickets fix: the bh_ticket_statuses JOIN was on the critical path and could
    take the whole screen down. It's now off the critical path (status colors are
    fetched separately, best-effort), with a fallback query — tickets always loads.
  - Create invoice: "New invoice" button on the Invoices screen -> form (customer,
    amount, due date, notes) -> creates an Unpaid invoice (auto INV-##### number).
  - Tap an invoice for a detail page (amount, status, client, due date, notes) with
    a "Send to client" button.

TWO POINTERS FOR INVOICES
  1. Numbering: mobile generates the next INV-##### as max+1 (PHP-side). If the
     desktop Invoices module owns numbering (prefix/counter in settings), point me
     at it so mobile-created numbers can't collide. (see nextInvoiceNumber())
  2. Send: the "Send to client" button currently shows "not connected" — it does
     NOT email anything yet, on purpose. Sending should build the PDF + email the
     client + stamp last_sent the same way the desktop does. Point me at the
     desktop send route or mailer method and I'll wire sendInvoice() to it.

WHAT'S NEW IN 1.4.0
  - Darker, visible borders on all cards/rows/metrics.
  - Customers list: dropped the "N contacts" subtitle (name only).
  - Fixed run-together text (amount/number, name/subtitle) — .t1/.t2 now block.
  - Contact detail: removed the Phone/Email/Company/Address text labels; the
    icons carry it (icon + value only).
  - Per-section tile styling: Customers, Tickets, and Invoices each have their
    own shape (Round/Square), border style (None/Solid/Dotted), border color,
    and thickness — plus their color. Quick-action tiles inherit their related
    section's style (New/Open tickets -> Tickets; Overdue -> Invoices).
  - Home springboard left as-is (looks great).

WHAT'S NEW IN 1.3.0
  - module.json version corrected (it was stuck at 1.0.0 — that's why the
    marketplace kept showing 1.0.0). Now 1.3.0.
  - Whole UI scaled up for touch: larger rows, fonts, tap targets, buttons.
  - Tap a contact for a full contact page: big header, tap-to-call, tap-to-email,
    company (links to the customer), and the company address (opens in Maps).
  - Create / edit contacts with big touch-friendly forms (pencil icon to edit,
    "New contact" button on the Contacts tab). Setting a primary contact clears
    the previous primary for that client.

WHAT'S NEW IN 1.2.0
  - Home screen is now a colorful springboard launcher: 3 section tiles
    (Customers, Tickets, Invoices) + a Quick actions row (New ticket, Open
    tickets, Overdue). Tap a tile to open the section; back arrow returns home.
  - Themed header across all screens (color + font color configurable).
  - New Settings screen (gear icon, top-right of home): /mobile/settings
       • Home background image (upload; auto-resized via GD, stored in the DB so
         it needs no writable uploads dir; "Remove" to clear)
       • Tile shape: Square / Round
       • Border: None / Solid / Dotted, + color + thickness (px)
       • Customers / Tickets / Invoices tile colors
       • Home background color
       • Header color + header font color
    Defaults reproduce the approved mockup, so it looks identical until changed.

ROUTES
  GET  /mobile                  springboard home
  GET  /mobile/customers        list (?q=) → /mobile/customers/:id (contacts/passwords)
  GET  /mobile/contact          full contact info  (?c=clientId&id=contactId)
  GET  /mobile/contact/new      new contact form   (?c=clientId)
  GET  /mobile/contact/edit     edit contact form  (?id=contactId)
  POST /mobile/contact/save     create / update contact
  GET  /mobile/tickets          list
  GET  /mobile/ticket           open a ticket (?id=)
  GET  /mobile/invoices
  GET  /mobile/settings         appearance settings
  POST /mobile/settings/save    save settings + background upload
  GET  /mobile/bg               streams the stored background (login-gated)
  POST /mobile/password/:id/reveal

STORAGE
  bh_mobile_settings (skey, sval LONGTEXT) — self-healed on boot. The background
  image is stored base64 in this table (downscaled to 1080px wide, JPEG q82), so
  no filesystem permissions are involved.

STILL PENDING — password reveal
  bh_passwords.password_enc is encrypted; the reveal endpoint returns
  "decryptor needed" until you point me at the platform decrypt helper. Then it's
  a one-method fill in decryptSecret().

INSTALL
  Marketplace > Update > Install.  Bust OPcache (MultiPHP Manager toggle).

FILES
  module.json
  MobileModule.php
  controllers/MobileController.php
  views/app.php
