# Changelog

## 2026-07-23

- **Contractor form UX:** Package options are full selectable cards (no visible radios; selected option is highlighted via CSS). “I have a website” toggles a website address field (entered without http/https). Cell requires a valid SA mobile number; email fields use stricter validation. Public form Maps address suggestions use the same async Places loader as the app, include a CSP nonce, and surface a clear message if the Maps API key is missing.
- **License expiry warning (client instances):** When a license has an expiry date, client instances show a header warning icon with days remaining (e.g. “X days before your license expires”). No expiry date means no warning. Expiry is stored from license validation and shown on Administration → License.
- **Contractor contact number:** Company contact number must be a valid 10-digit South African number (`0XXXXXXXXX`). Spaces/dashes and `+27`/`27` forms are normalized before validation.
- **Contractor form validation copy:** Public contractor onboarding validation errors no longer include the technical “when kind is contractor” suffix.
- **Contractor onboarding form fields:** Website is now a have/need dropdown (URL required when they have a website). Required document uploads for Company CK and proof of residence. Company address uses Google Places with city/province. Package selection added (Option 1 R550, Option 2 R850, or Custom).
- **Contractor website URL:** Website entries without a scheme (e.g. `www.test.com`) automatically get `http://` prepended on the form and server.

## 2026-07-20

- **Delivery notes (jobcard-linked):** Added a delivery notes module linked only to jobcards — no sidebar menu entry. Users can create a delivery note from a jobcard (prefilled line items), view/edit/delete it, update status, download PDF, and see related delivery notes on the jobcard detail page. Group permissions include `delivery-notes` for access control.
- **PDF company footers:** Invoice, quote, jobcard, and proforma invoice PDFs now render the footer text configured in Company Settings (`invoice_footer`, `quote_footer`, `jobcard_footer`). Custom PDF templates also receive the configured footer automatically.
- **Customer account name label:** Renamed the customer field label from "Company name" to "Account Name" on Create, Edit, and quick-create customer forms.
- **Company SMTP fallback fix:** Company mail now uses configured SMTP when host + port are set. If a username is set without a saved password, it no longer silently falls back to `.env` (often `mailpit`) without logging. Company Settings no longer exposes the SMTP password to the browser and shows whether a password is already saved.
- **Company SMTP TLS cert mismatch:** Added a “Verify TLS certificate” option in Company Settings. Shared hosts that present a certificate for a different hostname (CN mismatch on STARTTLS) can turn verification off. `starttls` is also normalized to Symfony’s `tls` mode.
- **SMTP password keep-on-blank:** Saving Company Settings with an empty SMTP password field no longer clears the stored password (Laravel was converting blank values to `null`).

## 2026-07-15

- **System email branding:** Document and notification emails now use grey body text, embed the uploaded company logo inline, apply the company's Appearance primary/secondary/accent colours to headers, highlights, totals, links, and action buttons, and use email-client-safe quote header/date spacing.
- **Quote draft autosave:** Quote Create/Edit now autosaves editable draft quotes after field and line-item changes, shows toast notifications when autosave succeeds or fails, and uses dedicated JSON autosave endpoints so drafts persist without triggering reminder emails or manual-save redirects.
- **Quote total profit:** Quote Create/Edit and Show now display total profit, calculated from discounted line totals minus per-line cost.
- **Login page branding:** The auth/login layout now uses the default Revamp logo and matching orange/neutral colour palette instead of the JobCardOnline app icon/company branding.
- **Public query form branding:** Hosted and embedded enquiry/contractor query forms now show the configured company logo above the form heading.

## 2026-07-13

- **Queries Hosted URL on normal releases:** Public Form Integration Hosted URL now still appears when `QUERY_PUBLIC_FORM_KEY` / `QUERY_API_KEY` are unset by falling back to `APP_KEY` for form tokens. Contractor Form URL remains licensing-only. Customer deploys force `IS_LICENSING_INSTANCE=false`; `.env.example` defaults to false.

## 2026-06-29

- **Customer company and contact fields:** Expanded customer records with company registration number, separate company cell/tel, and dedicated contact person name/surname/cell/email fields. Create/Edit/Show forms are grouped into company, contact person, and account sections; existing `phone` values are backfilled to company tel for compatibility.
- **Quote line-item product search dropdown:** Product suggestions on Quote Create/Edit now render in a fixed body overlay so they are not clipped by the horizontal line-item scroll container.
- **Customer quick-create fields:** Quick Create Customer modal (quotes, invoices, jobcards, credit notes) now includes the full company and contact person field set with address autocomplete, matching the main customer form.
- **Address autocomplete field fill:** Google Places selection on customer address now parses street address, city, and country into their respective fields instead of only setting the full formatted address string.
- **Product supplier link + quote line supplier:** Products now have a selectable default supplier on Create/Edit/Show. Quote line items store an optional per-line `supplier_id` (auto-filled from the linked product, overridable via dropdown). Quote Create/Edit use a single-row line grid with horizontal scroll when columns overflow; Quote Show displays supplier per line (not on PDF).
- **Quote email accept/decline workflow:** Added secure signed `Accept Quote` / `Decline Quote` links in customer quote emails. Customer responses now update quote status from the email action page and automatically send a notification email to the quote creator (`salesperson`).
- **Purchase orders supplier quick-create:** Added in-form `Quick Create Supplier` to Purchase Order Create/Edit, including inline modal creation and automatic supplier selection without leaving the PO screen.
- **PDF textarea line breaks:** Updated quote, proforma invoice, invoice, jobcard, and purchase-order PDF templates to preserve textarea line breaks (`description`, `notes`, `terms_conditions`) so multi-line content no longer collapses into a single line.
- **Quote line-item cost/profit:** Added `cost` support on quote line items (DB + backend validation/save), surfaced a compact cost input on Quote Create/Edit with inline per-line profit display, and added cost/profit columns on Quote Show. Quote PDFs remain unchanged (no cost/profit output).
- **Quote line-item column order:** Reordered quote line-item layouts so `Cost` now appears before `Price` on Quote Create/Edit/Show for consistent operator workflow.
- **Quotes customer quick-create shortcut:** Added an explicit `Quick Create Customer` action next to the customer field on Quote Create so users can open the same quick-create modal without relying on search dropdown state.
- **Query attachment loading fix:** Query detail attachments now use relative `/storage/...` URLs instead of `Storage::disk('public')->url(...)`, preventing broken images/videos when `APP_URL` differs from the active instance host.
- **Query → Quote action:** Added a `Quote` action on Query detail for enquiry/contractor queries. It now finds an existing customer first (email, then phone, then name), creates one when missing, and opens Quote Create with that customer preselected.
- **New query email notifications:** When an enquiry or contractor query is submitted (API or hosted URL form), the system now sends a notification email to the target company email address (using company SMTP override when configured).
- **Public query form thank-you state:** After hosted URL submission (enquiry and contractor), the page now shows `Thank you for submitting your enquiry` and hides the form instead of showing the form again; successful redirects also preserve `kind=contractor`.
- **Client email composer:** Customer and contact email modals now use a simple subject + plain-text body form; template selection and the visual email designer were removed.
- **Quote line item column order:** Quantity now appears to the right of unit price on quote Create, Edit, and Show line item tables.
- **Upgrade env backfill:** License-server instance upgrades now backfill `.env` mail and push defaults (`MAIL_*`, `FCM_*`, `APNS_*`, `APP_URL`) when keys are missing or still empty/package placeholders (`mailpit`, `hello@example.com`); existing real values are left unchanged. Fresh deploys also receive licensing-server mail defaults.
- **Per-company SMTP override:** Added company-level SMTP configuration fields in Company Settings and wired outbound email sending to use company SMTP credentials (and sender identity) when configured, falling back to `.env` mail settings otherwise.
- **Sidebar footer logo:** The bottom sidebar logo now always shows the default JobCardOnline branding; only the top sidebar logo updates when a company uploads a custom logo.
- **Deployment push env enforcement:** cPanel deploy now always writes `FCM_CREDENTIALS_PATH` (instance-specific path) and all `APNS_*` keys into the child instance `.env`, including `APNS_USE_SANDBOX`.
- **Administration menu cleanup:** Hid PDF Templates and Email Templates cards from the Administration page menu for now.

## 2026-06-25

- **Queries integration UI simplification:** Removed embeddable snippet fields from the Queries integration panel and now display only copyable hosted public form URLs.
- **API timesheet duration fix:** Fixed `/api/v1/time-entries` and timer endpoints to persist `duration_minutes` correctly (including pause/resume accumulation), preventing valid mobile time entries from appearing as `0m` in production.
- **Contractor query type (licensing-only):** Added `contractor` queries with onboarding fields (contact person details plus company name/registration/address/email/contact/website), available only on licensing instances for hosted and embedded public forms.
- **Contractor acceptance workflow:** Queries can now be accepted as contractor onboarding, which creates/links a `Customer` and primary `Contact`, marks the query accepted/closed, and stores linked record IDs.
- **Public query form (hosted + embeddable):** Added tokenized public query form URLs (`/query-form/{companyId}/{token}`) plus an embeddable script endpoint (`/query-form/embed.js`) that injects an iframe for easy website integration.
- **Query form security hardening:** Added dedicated `QUERY_PUBLIC_FORM_KEY` token validation, route throttling for hosted/embedded endpoints, and a honeypot field to reduce automated spam submissions.
- **Queries UI integration helpers:** Queries index now shows copy-ready hosted form and embed script snippets for the current company.

## 2026-06-16

- **FCM HTTP v1:** Android push notifications now use the Firebase Cloud Messaging HTTP v1 API with a service account (OAuth2), replacing the deprecated legacy server key (`FCM_SERVER_KEY`). Configure `FCM_CREDENTIALS_PATH` to your Firebase service account JSON, or set `FCM_PROJECT_ID`, `FCM_CLIENT_EMAIL`, and `FCM_PRIVATE_KEY`.
- **License deploy push credentials:** License deploy now writes `FCM_CREDENTIALS_PATH` (per-instance path under `storage/app/revampjobcard-8f5fb37b5d39.json`) and `APNS_*` values from the licensing server’s `.env` into each deployed child instance. Release builds bundle `storage/app/revampjobcard-8f5fb37b5d39.json` when present in the repo.
- **License deploy admin account:** Deploying a license instance now seeds the remote admin user with the selected customer’s email as the login username, default password `P@ssw0rd` (configurable via `CPANEL_DEPLOY_DEFAULT_ADMIN_PASSWORD`), and `must_reset_password` so the first login forces a password change. Deploy is blocked when the license customer has no email.
- **Forced password reset:** Added `users.must_reset_password`, `/password/required` page, and middleware that redirects affected users until they set a new password (cannot reuse the default deploy password).
- **Admin CSV import:** Added Administration → CSV Import for customers, suppliers, and products. Upload a CSV, map columns (or set defaults for required fields), preview sample rows, and run the import with created/skipped counts and row-level error reporting. Import upload metadata is stored in an encrypted token (not cache) so run import works reliably when cache is misconfigured or per-server.

## 2026-06-24

- **PDF company logo:** Added missing `Company::getLogoPathForPdf()` used by jobcard, invoice, quote, and other PDF Blade templates. PDFs now embed the same `logo_path` file as the sidebar (base64 data URI for dompdf), with fallback path resolution via `storage/app/public` and `public/storage`. Blade PDF templates also run image path conversion; jobcard PDFs use the jobcard’s company record for branding.

## 2026-06-16

- **Jobcard assignment push notifications:** Assigned users and team members now receive assignment notifications (database, broadcast, and push when devices are registered) when jobcards are assigned via web forms, mobile API, or dispatch bulk/reorder flows—not only from the dispatch assign endpoint.
- **API route binding (Sanctum):** API routes now run `SubstituteBindings` middleware so `{jobcard}` and other model parameters resolve correctly (fixes empty-model 404s on `PATCH /api/v1/jobcards/{id}` and similar endpoints). Company-scoped binding also reads the authenticated user from the request for bearer tokens.
- **`config:cache` fix:** Removed an unused Closure from `config/theme.php` that caused `php artisan config:cache` to fail with “configuration files are not serializable”.
- **APNs HTTP/2:** iOS push delivery now uses HTTP/2 (required by Apple), correct JWT base64url/ES256 signing, optional `APNS_USE_SANDBOX` for debug builds, clearer logging when APNs rejects a notification, automatic removal of invalid device tokens (`BadDeviceToken` / `Unregistered`), and iOS token normalization on registration.
- **Public storage serving:** Company logos and other uploads on the `public` disk are now served via Laravel’s `/storage/{path}` route (`serve => true` on the public disk). The private `local` disk no longer registers that route (it previously caused 403/404 for `/storage/company-logos/*`).

## 2026-06-10

- **CSP — Google Maps:** Allow `*.googleapis.com` and `*.gstatic.com` in `script-src` and `connect-src`, plus `worker-src blob:` and `frame-src *.google.com`, so Dispatch and Places Autocomplete can load the Maps JavaScript API when CSP is enabled.
- **Frontend TypeScript build:** Fixed CI type errors in Appearance settings (theme values from `Record<string, unknown>`), invoice create AI panel (`form.title` instead of missing `invoice_number`), and sidebar Queries nav (`queries` added to `Auth.abilities` types).
- **Dispatch map — location timestamp timezone:** `location_pings.recorded_at` is now `DATETIME` (not MySQL `TIMESTAMP`) with UTC read/write on the model and `SET time_zone = '+00:00'` on connect. Run `php artisan migrate` (adds datetime column + repairs legacy SAST-skewed rows). Restart `php artisan serve` and send a fresh GPS ping. Dispatch resolves company timezone from the dispatch company; “last updated” is formatted server-side (`last_updated_label`).
- **Mobile tracking — GPS ping retention:** `POST /api/v1/tracking/pings` deletes location pings older than 24 hours for that user/company on each ingest (`TRACKING_LOCATION_RETENTION_HOURS`, default 24).
- **Dispatch map — live technician GPS:** Dispatch map now shows real mobile location pings (default 8-hour window via `DISPATCH_LOCATION_WINDOW_MINUTES`) with technician markers, polls `/dispatch/user-locations` every 15s while the map is open, and displays “last updated” (relative + timestamp) when clicking a technician pin or opening full pin details. Test env coordinates still work when `DISPATCH_USE_REAL_LOCATIONS=false` or no recent pings exist. When simulator GPS and the job site are far apart, the map centers on technicians instead of zooming to a world view. Fixed stale pins by awaiting a location refresh before the first render, resetting the map when the modal opens, and re-rendering markers after each poll (pin clicks always read the latest coordinates). Dispatch company resolution and location queries were hardened so `/dispatch/user-locations` no longer returns empty when the session company is unset; mobile tracking pings fall back to the jobcard company when needed.

## 2026-06-03

- **Company dispatch toggle:** Added `companies.enable_dispatch` (default off). Company Settings includes “Enable jobcard dispatch board”; web `/dispatch` routes and `/api/v1/dispatch/*` return 403 when disabled; Jobcards → Dispatch sidebar link is hidden unless enabled for the current company.
- **QuickBooks settings migration (existing table):** `create_quickbooks_settings_table` and `add_refresh_token_expires_at_to_quickbooks_settings` now no-op when the table or column already exists, fixing `migrate` failures on databases that already have `quickbooks_settings` without a migrations row.

## 2026-05-19

- **Appearance settings save target:** Appearance settings now read and write theme columns on the current `companies` row (not `users`), matching the theme migration and fixing "Unknown column 'theme_primary_hue'" errors on save. `app.blade.php` loads theme CSS variables from the current company as well.
- **Blade theme variables syntax:** Fixed corrupted `{{ $getThemeValue(...) }}` expressions in `app.blade.php` that caused a parse error on page load.
- **Database upgrade — duplicate `logo_path` migration:** Removed redundant `add_logo_path_to_companies_table` migration; `logo_path` already exists on `companies` from the original create-table migration, which caused "Duplicate column name" errors during admin database upgrade.

## 2026-05-13

- **Appearance settings page:** Added `/settings/appearance` with color pickers for theme customization (primary, secondary, accent, background colors in HSL format), layout variables (sidebar width, header height, border radius, spacing unit), and logo/favicon upload functionality. Theme settings are now stored per-company in the database instead of environment variables, enabling dynamic white-labeling via the UI.
- **Company theme settings:** Added database migration for theme color columns (HSL values), layout variables, and favicon_path to `companies` table. Updated `Company` model with fillable fields and favicon accessor (falls back to logo if favicon not set). Theme config now reads from authenticated user's current company or falls back to environment variables.
- **Dynamic favicon:** Updated `app.blade.php` to use company-specific favicon/logo when available, with fallback to default favicons.

## 2026-05-12

- **Theme white-labeling support:** Added `config/theme.php` with environment variable support for customizing brand colors (primary, secondary, accent, chart, sidebar). Theme colors are now injected as CSS variables in `app.blade.php` and used in `app.css`, allowing different instances to have unique color schemes via `.env` configuration without code changes. Default values maintain the existing blue/teal JobCardOnline theme.
- **Black and gold theme:** Updated `.env.example` with black and gold color scheme (primary gold hue 51, saturation 26%, lightness 51% - hex #A29862, background lightness 100% for light mode/white, 3% for dark mode/black). Added layout variables (sidebar width, header height, border radius, spacing unit) to theme config for additional customization. CSS now uses configurable background lightness values to support white/light and black/dark themes.
- **Administration — QuickBooks Online:** Added per-company QuickBooks settings (`/administration/quickbooks-settings`), Intuit OAuth (authorize + callback with `realmId`), encrypted credential storage, disconnect, company switcher, and sync-direction toggles matching the Xero settings layout. Manual sync routes return an informational notice until QuickBooks data sync is implemented. Configure sandbox vs production API host with `QUICKBOOKS_USE_SANDBOX` in `config/services.php`. **Intuit-aligned behavior:** revoke tokens at `developer.api.intuit.com` on disconnect (same pattern as `intuit/oauth-jsclient`), `User-Agent` + `Accept` headers on OAuth/API calls, configurable Accounting **`minorversion`** (`QUICKBOOKS_MINOR_VERSION`), persist **`x_refresh_token_expires_in`** as `refresh_token_expires_at`, clear local OAuth tokens on refresh **`invalid_grant`**, and surface refresh expiry in the settings UI.
- Documentation: added root `README.md` with local development setup (PHP/Composer/Node, `.env`, MySQL, migrate/seed, storage link, `composer run dev`, tests); README documents MySQL-only local database (no SQLite path).
- **Mobile API jobcards filtered by user and open status:** `GET /api/v1/jobcards` now returns only jobcards assigned to the authenticated user (directly or via team membership) and excludes `completed` or `cancelled` jobcards. Accepts optional `company_id` request parameter to scope to a specific company.
- **ScopedToCurrentCompanyRouteBinding accepts company_id param:** Route model binding now checks for a `company_id` request parameter before falling back to the user's current company, enabling the mobile app to fetch records across companies.
- **Mobile API profile includes company list:** `GET /api/v1/profile` now returns a `companies` array containing all companies the authenticated user belongs to, for company selector UI.

## 2026-04-21

- **Task notification trigger policy refined:** Task assignment notifications are now sent only when a task is newly created or when `assigned_to_user_id` / `assigned_to_team_id` changes. Pure status/schedule/content updates no longer generate task assignment notifications.
- **Tasks kanban drag-and-drop status updates:** Added drag-and-drop status changes in the Tasks Kanban view. Cards are draggable only when the user can update that task's status, and drops call the same status update endpoint/rules used by task show.
- **Tasks list now supports List/Kanban tabs:** Added a view toggle on the tasks index with `List` and `Kanban` tabs. The kanban board uses the same permission-scoped task dataset as the list view, so fallback visibility and task permission rules are enforced consistently in both views.
- **Task show status UI now matches jobcards pattern:** Replaced the task show status dropdown with a full clickable status bar (all statuses shown as buttons with active/disabled states), mirroring the jobcard show status interaction model.
- **Resolved cross-user visibility on team fallback:** When fallback visibility applies, tasks assigned to a specific user are now visible only to that user; team-based fallback visibility applies only to tasks that are team-assigned without a direct user assignee.
- **Adjusted own-task fallback scope for teams:** Restored fallback visibility to include tasks assigned directly to the user or to any team the user belongs to (web tasks and dispatch), while still hiding unrelated tasks.
- **Task status + fallback visibility corrections:** Task show status updates now follow a dedicated status-only update path (preventing full-edit validation conflicts), and “no list/view permission” fallback visibility is now restricted to tasks directly assigned to the logged-in user (web tasks and dispatch board).
- **Task permission fallback hardened for own assignments:** Users now always get list/view access to tasks assigned directly to them, even with no task permissions. Own-assigned tasks can always have status updated; users with `tasks.edit` but without list/view are limited to editing only their own assigned tasks.
- **Tasks always visible in sidebar:** The Tasks navigation item is now always shown in the sidebar for non-client users, regardless of module visibility toggles or task list permission gating.
- **Task notification links + show status control:** Task notifications now open the task show page (`/tasks/{id}`) instead of edit, and the task show page now includes an inline status selector (for users with task edit permission) to match status controls in other modules.
- **Improved assignment notification wording:** Assignment notifications now include clear context prefixes (for example, “Task assignment: …”) instead of showing only the raw record title.
- **Fixed task notifications from web task pages:** Task create/edit actions in the web `TaskBoardController` now send assignment notifications to the assigned user and/or all users in the assigned team, matching API and dispatch behavior.
- **Fixed tasks list notes counter:** Task list note totals now count shared polymorphic `Record Notes` entries (the same source shown on task pages) instead of legacy task-note rows, so counts are accurate and consistent.
- **Task notes unified with shared Record Notes:** Tasks are now wired into the same notes module mapping as other records. Task show routes (`/tasks/{id}`) now use the standard `Record Notes` panel instead of a task-specific notes layout, so note creation/search/display behavior is consistent across modules.
- **Dispatch task visibility fallback:** When a user lacks tasks list/view permission, dispatch task payloads are now automatically limited to tasks assigned directly to that user or to teams they belong to (instead of showing all tasks).
- **Tasks show/delete + clickable list rows:** Added a dedicated Task Show page with details and notes, added delete routes/actions, updated tasks list rows to open the Show view on click, and aligned list action buttons to the same icon-label button style used by other index views.
- **Notification policy tightened:** In-app database/broadcast notifications are now limited to: (1) new Client Zone registrations (admin recipients), (2) Client Zone information update submissions (admin recipients), and (3) task assignment events when a task is newly assigned to a specific user. Removed broad dispatch/jobcard assignment notification fan-out.
- **Notification recipients + task assignment reliability:** Client Zone registration and information-update notifications now target all users with approve permission on `registered-users` or `customer-update-requests` (not only administrator groups). Task-assignment notifications now resolve assigned users through company-scoped staff selection before notifying.
- **Task assignment notifications (user/team):** Task create/edit flows now notify assignment targets for both assignment modes: direct assigned user and all users in the assigned team.
- **Other Integrations (admin):** Renamed Administration → Google Integration to **Other Integrations** and expanded the singleton integration settings to include encrypted OpenAI API key storage plus AI runtime controls (global enable, admin-only mode, prompt logging toggle, daily per-user/per-company limits).
- **AI platform foundation:** Added shared AI services (OpenAI client wrapper, access policy/limit guard, redacted AI audit logging) and persisted AI usage telemetry via new `ai_usage` table.
- **AI features (phase 1):** Added Dispatch technician suggestions, document/template suggestions for quote/invoice/jobcard create flows, AI drafting helper controls in document/email-template authoring, and a new read-only global **AI Assistant** page (`/ai-assistant`) scoped to current-company records.
- **Dispatch traffic color coding:** ETA traffic severity now renders as color-coded badges on the dispatch map (green = Low, amber = Medium, red = High) in both technician pin InfoWindows and the full technician details modal for faster visual triage.
- **Dispatch map technician filter:** Added a technician dropdown on the dispatch map modal so planners can show pins for all technicians or only one technician; map bounds, empty-state messaging, and marker refresh now respect the selected technician.
- **Dispatch map dialog (a11y):** Map and technician detail `DialogContent` include a screen-reader-only `DialogDescription` so reka-ui no longer warns about missing description / `aria-describedby`.
- **Dispatch ETA unavailable:** `Route.computeRoutes` no longer uses an invalid field mask (`legs.travelAdvisory`). Requests retry with smaller masks (`travelAdvisory` + static duration, then static-only, then minimal duration/distance fields).

- **Dispatch ETA traffic (low / medium / high):** Driving ETA from `Route.computeRoutes` now shows **Traffic: Low | Medium | High** next to duration/distance (technician InfoWindow and “Full details” modal). Uses polyline **speed reading intervals** when `travelAdvisory` is returned (`NORMAL` / `SLOW` / `TRAFFIC_JAM`); if intervals are absent, falls back to comparing traffic-aware **duration** vs **static** duration.

- **Places Autocomplete (jobcards + customers):** Shared `google_maps_api_key` from Google Integration settings via Inertia. Jobcard create/edit **Service address** and customer create/edit **Address** use `AddressAutocompleteInput` (Google Places `address` suggestions when the key is configured; otherwise a normal text field). Suggestions are restricted to **South Africa** (`componentRestrictions: country: za`). Ensure the same browser key has **Places API** enabled in Google Cloud.

- **Dispatch map pin size:** Modal map teardrop pins render slightly smaller (`48×68` vs `56×80` scaled size) for a lighter footprint on the basemap.

- **Dispatch scheduled display (drawer + map InfoWindow):** Introduced `resources/js/lib/dispatchScheduleFormat.ts` (`formatDispatchScheduleInstant`) so **Scheduled:** start/end times never use `toLocaleString()` / `Date.toString()` style output with `GMT+0200 (South Africa Standard Time)`; they match the lineup style (`Mon Apr 20 2026 12:00:00`). Wired into the jobcard drawer, map InfoWindow, and related dispatch labels.

- **Dispatch board lineup format:** Technician lineup rows (map InfoWindow + daily lineup dialog) use a fixed pattern: `Mon Apr 20 2026 13:15:00 - JC… - Est. N Min` (local wall time, English weekday/month; duration from `estimated_duration_minutes`, else scheduled window, else 60 for jobcards). Schedule values are parsed safely when Inertia passes `Date` so raw `GMT+0200 (…)` strings no longer appear.

- **Dispatch map pins & directions:** Modal map markers use large shadowed teardrop SVG pins (initial/JC in the head) with higher z-index so they read clearly over default POIs. Technician InfoWindows include a **Driving directions** block linking to Google Maps turn-by-turn from that pin to the **open jobcard’s job address** (when the drawer has an address).

- **Dispatch map modal InfoWindows:** Clicking a technician or jobcard marker opens a `google.maps.InfoWindow` on the pin (name, coordinates, reverse-geocoded “near” address, optional driving ETA to the open job, and board-date lineup for techs; jobcard shows customer, site, status, schedule, assignee). **Full details…** still opens the existing technician dialog for the full lineup and ETA.

- **Dispatch map on demand:** The day-board jobcard drawer no longer embeds Google Maps inline. It shows a **View map** button that opens the map in a modal; the script, tiles, and geocoding run only after the user opens that modal (fewer API calls when browsing dispatch).

- **Dispatch board drawer map (first load + pins):** After `loading=async`, the script `load` event can fire before `google.maps.importLibrary` exists; the loader now waits for the bootstrap API before resolving. The drawer map waits for `idle` and triggers `resize` before plotting.

- **Dispatch drawer map pins (reliable rendering):** Technician and jobcard pins use legacy `google.maps.Marker` (from `importLibrary('marker')`) with circle icons and labels instead of Advanced Markers + `PinElement`. Advanced Markers require a working Map ID and `isAdvancedMarkersAvailable`; when those are missing or misconfigured the basemap can load while pins stay invisible—legacy markers avoid that.

- **Dispatch / Google Maps console hygiene:** Maps bootstrap URL includes `loading=async` (with the bootstrap wait above).

- **Google Integration (system-wide):** Maps JavaScript API key and optional Map ID for Advanced Markers are stored encrypted in `google_integration_settings` (singleton row) and edited under **Administration → Google Integration**. Dispatch and API route optimization read from this record; `GOOGLE_MAPS_MAP_ID` in `.env` remains an optional fallback when Map ID is left blank in the UI.

- **Dispatch Calendar tab removed:** Dropped the Calendar view mode and the `qalendar` dependency; Dispatch is now **Board** or **Kanban** only. Google Maps on the day-board jobcard drawer (technician pins, job geocode, ETA dialog) is unchanged. Local preferences that stored `viewMode: 'calendar'` still load as **Board**.

- **Dispatch map markers (Advanced Markers):** Replaced deprecated `google.maps.Marker` with `google.maps.marker.AdvancedMarkerElement` and `PinElement` for technician and jobcard pins on the board drawer map; map instances now set `mapId` (configurable via `GOOGLE_MAPS_MAP_ID`, default `DEMO_MAP_ID`). See [Advanced Markers migration](https://developers.google.com/maps/documentation/javascript/advanced-markers/migration).
- **Dispatch Kanban task lanes:** While dragging a **task**, jobcard-only status columns show a no-entry overlay and do not accept drops (tasks may only move to `new`, `scheduled`, `accepted`, `completed`, `cancelled` per API rules).
- **Dispatch map ETA (Routes API):** Replaced deprecated `google.maps.DirectionsService` with `google.maps.routes.Route.computeRoutes` (traffic-aware driving, localized distance/duration when available) for technician-to-job ETA on the dispatch map pin dialog; removed an unused `DirectionsRenderer` that was attached to the former calendar map.
- **Dispatch timeline duration resize preview:** Resizing a scheduled block’s end time (east edge) shows an amber overlay with the snapped start–end clock range; the underlying card fades slightly until release.
- **Dispatch day board drag preview:** While dragging a jobcard from the queue or along the technician timeline, the hovered row shows a dashed highlight for the snapped placement window and a small `HH:mm – HH:mm` helper (shared `dispatchDragSession` ref; `dataTransfer.getData` is not readable during `dragover` in browsers).
- **Dispatch Kanban horizontal scroll:** While dragging a lane card, holding the pointer near the left or right edge of the Kanban strip auto-scrolls horizontally (faster when closer to the edge).
- **Dispatch Kanban columns:** Lane column panels use a light grey background (`bg-gray-100`) for clearer separation from the page.
- **Dispatch day board duration:** Timeline jobcard resize uses the **right edge** (horizontal / east-west drag) so duration follows the time axis; the drawer adds an **Est. duration (min)** field (5–1440, default 60) that updates stored duration and, when scheduled, moves `scheduled_end_at` to match.
- **Dispatch drawer map jobcard pin:** Ignored stale Google Geocoder callbacks when switching queue jobcards so the JC marker always matches the currently selected job.
- **Dispatch map user pins:** Clicking a technician/user pin on the board drawer map opens a dialog with reverse-geocoded address, coordinates, ETA to the relevant job location when an address is available, and that day’s scheduled lineup for the board date.
- **Dispatch Kanban layout:** Kanban view is a single horizontal row of lanes (fixed-width columns) with horizontal scrolling; lane chrome uses neutral borders/white backgrounds with status labels and counts only (no per-status lane colours).
- **Jobcard priority (web UI):** Exposed the existing `priority` column (`low` / `normal` / `high` / `urgent`) on jobcard create/edit forms, jobcard show sidebar, and the jobcards index (filter, sortable column, badges). Server-side validation was already present; this completes end-user editing and listing.
- **Dispatch schedule board (drawer UX):** Technician/team selects in the right-hand drawer now stay aligned with the server after assign (merge assign JSON + resync from refreshed board data). The drawer map is a real Google Map: geocoded job service address, optional technician pins from `DISPATCH_TEST_USER_LOCATIONS`, and inline hints when the address or pins are missing (no more “calendar only” placeholder).
- **Jobcard status `emergency`:** Added as a first-class workflow status (MySQL enum migration), wired through web/API validation, dispatch board/Kanban, company default labels, and reporting filters; the dispatch **Emergency** queue tab now filters `status = emergency` (replacing the old urgent-priority-only filter).
- **In-app notification bell:** Added a header notifications dropdown (unread badge, recent items, mark read / mark all read, deep links to jobcards/tasks), session JSON routes under `/user/notifications`, shared `unreadNotificationCount` via Inertia, and `POST /api/v1/notifications/read-all` for parity with the mobile API.
- **Laravel database notifications table:** Added the standard `notifications` migration so `AssignmentNotification` (database + broadcast channels) can persist when dispatch assigns jobcards/tasks; required on MySQL/production if the table was never created.
- **Jobcard dispatch day board (timeline):** Added a technician-row day timeline with drag-from-queue scheduling, in-row moves/resizes (duration via end time), same-day conflict surfacing, queue tabs (unscheduled/follow-up/parts/emergency), search + priority filters, and a detail drawer with quick status/unschedule actions; board data is loaded via enriched `/dispatch/board-data?date=…` responses (`scheduled_jobcards` / `unscheduled_jobcards`) while Kanban mode remains available.
- **Dispatch assign web route:** Exposed `PATCH /dispatch/jobcards/{jobcard}/assign` for session-authenticated dispatch edits (aligned with existing API behavior), with `JobcardPolicy@update` checks on assign/reschedule paths.
- **Jobcard priority + estimated duration:** Added nullable `priority` (`low|normal|high|urgent`, default `normal`) and `estimated_duration_minutes` on `jobcards`, validated on web jobcard forms and mobile jobcard PATCH/POST where applicable.
- **Sidebar navigation:** Nested **Dispatch** under **Jobcards** in the main sidebar, gated by `dispatch` list permission, and removed the standalone Dispatch top-level item.

## 2026-04-20

- **Dispatch/scheduling module completion pass:** Expanded dispatch APIs with task assignment, bulk schedule updates, conflict detection, reorder support, and technician-focused `my-schedule` data; upgraded the web Dispatch page into an operational scheduling board with date/assignee filters, inline jobcard/task updates, conflict surfacing, and route generation controls to close remaining dispatch workflow gaps.
- **Dispatch Kanban UX refinement:** Reworked the Dispatch board into a status-lane Kanban layout (Pending, In Progress, Completed, Cancelled) with drag/drop status moves for tasks and jobcards, and lane cards sorted by scheduled date to match dispatch-first planning workflows.
- **Dispatch card navigation:** Kanban cards now open their underlying records on click (`jobcards` show view and `tasks` edit view) so planners can jump directly from board context into full record detail/update flows.
- **Dispatch board stabilization + scaling:** Switched Kanban board actions to authenticated web dispatch endpoints (fixing 401 drag/drop moves), included draft/unfiltered dispatch visibility so jobcards are not silently excluded, and added per-status incremental rendering (20 cards at a time with scroll-to-load) for dense boards.
- **Dispatch Google routing integration:** Route generation now attempts Google Directions optimization using customer address-based stops when `GOOGLE_MAPS_API_KEY` is configured, persisting provider metadata as `google` and gracefully falling back to the heuristic provider when Google is unavailable.
- **Dispatch view-mode toggle:** Added a Dispatch UI switch to alternate between Kanban lanes and a Calendar-style day grid, using the same filtered scheduling dataset so planners can choose status-flow or date-driven dispatching in one screen.
- **Dispatch calendar upgraded to Qalendar:** Replaced the custom calendar grid with Qalendar for proper week/month scheduling UX, including drag-and-drop event rescheduling for tasks/jobcards via new authenticated dispatch schedule endpoints.
- **Dispatch quick task creation modal:** Added in-context task creation directly from Dispatch via a Kanban `+ Add Task` action and Qalendar empty-interval clicks, both opening a modal form that creates tasks without leaving the Dispatch page.
- **Dispatch Qalendar payload compatibility fix:** Normalized Dispatch calendar event `time.start`/`time.end` values to Qalendar’s required `YYYY-MM-DD HH:mm` format and explicit timed-event metadata, resolving runtime warnings and `Event has invalid type` calendar mount failures.
- **Dispatch calendar task-create time selection:** Calendar task creation now respects dragged interval selections for start/end times, while single-click creation defaults to a one-hour duration using local/company timezone-safe datetime handling.
- **Dispatch calendar drag-select creation support:** Added interval drag-selection handling on Qalendar week/day cells so dragging across open slots pre-fills task modal start/end using the selected range; single interval clicks remain supported for quick create.
- **Dispatch UI simplification + calendar filters:** Removed Dispatch summary/route/conflict sections from web UI, defaulted Dispatch to calendar view, and added calendar entity filter tags (`Both`, `Tasks`, `Jobcards`) while retaining Kanban/calendar switching and shared filters.
- **Dispatch month-calendar overflow behavior:** Standardized month-day cell heights in Dispatch calendar view and replaced default overflow affordance with clickable `...` expansion, allowing users to reveal all tasks/jobcards for an overflowing day inline.
- **Dispatch month-calendar normalized day width:** Enforced equal-width day columns in month view so each day cell has consistent dimensions alongside the existing fixed-height and expandable-overflow behavior.
- **Dispatch per-user calendar preferences + compact toolbar:** Removed date-range filters from Dispatch, moved remaining assignment filters into the calendar toolbar, added default visible hours (`06:00`-`19:00`) for week/day mode with a `Show all hours` toggle, and persisted view/filter settings per user so they survive refresh/navigation.
- **Dispatch month view square-fit grid sizing:** Updated month calendar layout overrides so day cells scale as square blocks to fit available width, while preserving overflow behavior that expands a day to show all items when `...` is clicked.
- **Dispatch Qalendar day-boundary compatibility fix:** Corrected calendar `dayBoundaries` config to use numeric hour values (`6` to `19`) instead of time strings, resolving the runtime `Invalid day boundary` error in week/day rendering.
- **Dispatch calendar state synchronization hardening:** Wired Qalendar `updated-mode` synchronization, added keyed remounting for mode/hour-boundary changes, and adjusted per-user preference load/save timing to prevent default-state overwrite so mode/toggle/filter selections apply immediately and persist across refresh/navigation.
- **Dispatch split calendar + map layout:** Reworked dispatch calendar view into a two-column layout with Qalendar on the left and a Google Map on the right showing staff user markers (excluding client/info-only users), plus env-driven test coordinates via `DISPATCH_TEST_USER_LOCATIONS` until live mobile location pings are wired in.
- **Dispatch week/day task visibility fix:** Corrected Qalendar event end-time mapping to use `scheduled_end_at` (with safe +1 hour fallback from start) so tasks render properly in week/day timeline views instead of collapsing to zero-duration events.
- **Jobcard address + dispatch operations cockpit layout:** Added editable `service_address` on jobcards (defaulting from selected customer address/city/country when blank), and reworked Dispatch calendar mode into a 3-column operations layout (unscheduled jobs, calendar, selected-job details/map) with Google map overlays for selected jobcard location + user locations, user pin detail drilldown, same-day lineup listing, and ETA-to-jobcard calculation from selected user pin.
- **Dispatch column height/scroll normalization:** Standardized all three dispatch calendar-mode columns to a shared fixed viewport height and moved scrolling inside each panel so the left/center/right columns remain visible together while content scrolls independently.
- **Jobcard status workflow overhaul:** Replaced legacy jobcard statuses with the new operations flow (`new`, `needs_scheduling`, `scheduled`, `dispatched`, `accepted`, `en_route`, `on_site`, `paused`, `waiting_for_parts`, `needs_follow_up`, `completed`, `cancelled`), updated company default labels/validation/controllers/dispatch filtering, mapped existing records via migration, and refreshed jobcard/dispatch UI badge and lane mappings.
- **Jobcard status migration safety fix:** Corrected enum migration order to widen MySQL `jobcards.status` enum before remapping legacy rows, then tighten to the new status set, preventing data-truncation failures during migration runs.
- **Mobile API contract completion pass:** Added missing `/api/v1` endpoints requested for mobile parity across CRM, catalog, documents, timesheet/timer, reporting, notes, teams/assignable users, files/uploads, notifications/device token registration, plus expanded jobcards/messaging/dispatch/tracking/tasks endpoints (including CRUD and action routes) so the app prompt endpoint matrix is now present server-side.
- **Tasks assignment UX alignment:** Task create/edit now mirrors Jobcards assignment interaction with a single assignment field driven by a `User`/`Team` selector toggle, while still enforcing required assignment to one side only.
- **Dispatch/messages web view polish:** Upgraded Dispatch and Message Center pages from placeholder cards to standard list/table-style operational views with clearer summaries and record rows for day-to-day use.
- **Operations modules hardening (messages/dispatch/tasks):** Added first-class module permissions (`messages`, `dispatch`, `tasks`) across web routes, API routes, shared auth abilities, sidebar/module-visibility wiring, and default permission seeding/migration so these new plan modules are no longer coupled to `jobcards` permissions.
- **Tasks web UX parity + assignment rules:** Rebuilt Tasks into standard list/create/edit flows with filters, pagination, dedicated forms, and task edit routes; task assignment now enforces user-or-team requirement on both web and API task create/update paths with company-scoped validation.
- **Detailed Jobcards report + task-status workflow alignment:** Updated Detailed Jobcards report status filters, breakdown chips, status duration bars, and CSV tracker columns to the new jobcard workflow (`new` through `cancelled`), and aligned task workflows to `new/scheduled/accepted/completed/cancelled` across dispatch/task controllers, task UIs, and a migration that remaps legacy task statuses (`pending`→`new`, `in_progress`→`accepted`).
- **Tasks web assignment query fix:** Updated Task board user filtering to use company membership (`user_companies`) instead of a non-existent `users.company_id`, and tightened task-create assignment validation to company-scoped staff/team rules to prevent invalid cross-company IDs.
- **Tasks web create flow:** Added web task creation on the Tasks page (`POST /tasks`) with title/description, optional user/team assignment, and optional schedule fields so staff can create tasks directly from the web module instead of API-only workflows.
- **Dispatch/messaging migration ordering fix:** Renamed the `conversations` migration to an earlier timestamp so it runs before `conversation_participants`, preventing MySQL foreign-key creation failure (`Failed to open the referenced table 'conversations'`) during `php artisan migrate`.
- **Conversation participants migration recovery:** Updated the `conversation_participants` migration to drop any pre-existing partial table before create, allowing retries after an interrupted/failed first run that left the table without completed constraints.
- **Dual-platform foundation (web + mobile):** Added Sanctum-backed `api/v1` mobile API authentication plus non-admin endpoints for profile, jobcards, messaging, dispatch, tracking, and tasks, all aligned with existing module permission checks and tenant company scoping.
- **Messaging, GPS, dispatch, and tasks backbone:** Added new domains/migrations/models for conversations/messages, vehicles/location pings, route plans, tasks/task notes, and dispatch fields on jobcards, with realtime event scaffolding for message, dispatch, and location update broadcasts.
- **Web module expansion for operations:** Added web-accessible Message Center, Dispatch, and Tasks pages with controllers/routes and sidebar navigation entries, providing initial in-app surfaces for cross-platform messaging and scheduling workflows.
- **Routing and assignment infrastructure:** Introduced pluggable route optimization service interfaces with heuristic provider, plus assignment notifications for task/jobcard assignments to support push/broadcast fan-out paths.

## 2026-04-14

- **License API (legacy clients without `X-License-Nonce`):** The licensing server now accepts the **pre-nonce** HMAC used by older `InstanceLicenseService` builds: `timestamp|POST|path|payloadSha256` with **no** nonce in the string and **no** `X-License-Nonce` header. Replay protection uses a cache key keyed by license key, timestamp, and body hash (same TTL as nonce replay). Modern clients that send a nonce are unchanged.

- **License API diagnostics:** On the **licensing server**, failed HMAC checks log a **warning** with redacted JSON excerpt, **`raw_body_sha256_hex`** (hash of exact request bytes), **`json_key_order`**, **`REQUEST_URI` / `PATH_INFO` / `SCRIPT_NAME`**, client IP, User-Agent, and signature/nonce metadata. Set **`LICENSE_API_DEBUG_LOG=true`** on the licensing instance to also **`Log::info`** each **successful** authenticated license API request with the same safe fields. Config: `config('app.license_api_debug_log')`.
- **License validation (customer instance):** When validation fails, **`InstanceLicenseService`** logs **`request_body_sha256_hex`** and **`license_server_host`** so you can match the same **`raw_body_sha256_hex`** line on the licensing server (proves the HTTP request arrived and the body matches).
- **License log file (`storage/logs/license.log`):** All license API / validation diagnostics use the **`license`** log channel so they appear even when **`LOG_LEVEL`** on the default stack hides warnings. **Customer** instances also log **connection exceptions** (previously silent), **HTTP errors**, **throttled** notices when a **cached** failure is returned without a new HTTP call, and **HMAC OK but unknown key** / **replay** on the licensing server. Optional **`LICENSE_LOG_LEVEL`** (default **debug**) controls the license channel only.

- **License API signature (backward compatibility):** Verification tries **multiple HMAC secrets** (exact, trim, upper, lower), **HTTP methods** (`POST`, request method, `post`), **paths** from `fullUrl()`, **REQUEST_URI** / **PATH_INFO** when they differ (proxies/rewrites), **legacy ltrim** and **percent-decoded** path forms, and **payload SHA-256** variants (raw body, BOM/trim, `json_encode` flag sets, **`license_key`/`url` key order**, **ksort**). License rows are resolved with **case-insensitive** `license_key`; replay nonces use a **lowercased** key. Logs include candidate counts on failure.

- **cPanel license deploy/upgrade (no Shell module):** Zip extraction uses **cPanel API 2** `POST /json-api/cpanel` with **`Fileman::fileop` (extract/trash)** — there is **no** UAPI `fileop` (the `/execute/Fileman/fileop` call cannot work). Release zips with a single `jobcardonline-v*` root are **flattened locally** before upload so remote `find`/`cp`/`rm` are not required when Terminal API is missing. Deploy can merge **`.env.example` → `.env`** during flatten when `.env.example` is present. If `Shell::command` still fails for `php artisan`, the flow reports success with **manual** migrate/storage:link instructions. `Shell::command` errors continue to return the API message when the Shell module exists but returns an error.

- **Frontend typecheck stabilization:** Tightened shared page/component typings for app metadata, nullable auth users, badge variants, backup/Xero control states, and email/version helper components so the Vue TypeScript pass can fail on real model-shape issues instead of repeated UI-level typing noise.
- **Frontend typecheck (follow-up):** Aligned document and list page interfaces (products, discounts, tax rates, customer address, polymorphic references) with Inertia payloads; fixed GrapesJS `PdfTemplateEditor` style-manager typing; normalized stock movement `reference` as string-or-loaded-model; hardened credit-note edit form error accessors to avoid runaway template inference; and tightened purchase-order receive rows and status updates for strict `vue-tsc`. `npm run typecheck` completes cleanly.
- **License validation diagnostics:** When the remote license API returns a non-2xx response, the instance now surfaces the server JSON message (and logs status + a short body preview) instead of only a generic “could not validate” line. License API validation accepts longer `url` values (up to 2048 characters) so `APP_URL` longer than 255 characters no longer causes a 422 validation failure against the licensing server.
- **License API + CSP:** License middleware hashes the raw JSON body before reading parsed input, the HTTP client disables redirects for license calls, and signing path matches Laravel/Symfony (`trim` + `rawurldecode`). `style-src` uses `'self' 'unsafe-inline'` and fonts only (no nonce—browsers ignore `unsafe-inline` when a style nonce is present); `script-src` keeps a nonce. See earlier entries for validation diagnostics and URL max length.
- **License API signature (incoming):** The licensing server now accepts an HMAC that matches **any** of several canonical path strings (`Request::path()`, URL-derived path, legacy left-trim-only path) so minor proxy/subdirectory or older-client path differences no longer yield 401. Failed verification logs path candidates and a short payload-hash prefix (no secrets) for support.
- **License API signature (incoming, legacy bodies):** Verification also tries SHA-256 digests for UTF-8 BOM–stripped and trimmed raw bodies plus json_encode variants of the decoded JSON (`JSON_UNESCAPED_SLASHES`, with/without `JSON_UNESCAPED_UNICODE`, default flags) so older instances that hashed a different canonicalization than the exact wire bytes still validate against an updated license server.

## 2026-04-10

- **Coverage program foundation:** Added backend/frontend coverage commands, Vitest reporting outputs and baseline thresholds, CI artifact publication and summaries, SQLite-backed PHPUnit defaults for local repeatability, focused backend coverage across reports/quotes/purchase orders/credit notes/services/policies, focused frontend composable/component tests, and a coverage governance document with ratchet rules and intentional gap tracking.
- **Safe frontend/backend cleanup pass:** Centralized frontend CSRF access and drag-handle UI, made list-column editing target explicit tables, added initial Vitest coverage, moved invoice/jobcard validation into Form Requests, extracted transactional invoice upserts and shared report/list filter helpers, and reduced repeated report/filter/controller logic without changing feature behavior.
- **Code quality hardening:** Moved quote and customer validation into dedicated Form Requests, extracted transactional quote/customer write services, added safer transaction boundaries to quote/jobcard/payment flows, and aligned customer/contact helper routes with module permissions to reduce partial writes and authorization drift without changing user-facing features.
- **Frontend form reuse + CI gates:** Shared customer lookup/quick-create logic across quote and invoice forms via a reusable composable, centralized customer/document TypeScript types, and strengthened CI with frontend typechecking, non-mutating lint/format checks, and PHPStan/Larastan analysis. Release builds now also support tag-triggered packaging.
- **Staff user selectors + jobcard invoice stock deltas:** Staff-facing user pickers and validations now exclude Client Zone users so client accounts cannot be assigned as internal staff or salespeople, and jobcard-origin invoices now adjust stock only for quantity deltas beyond what the source jobcard already consumed, including restoring stock when invoiced quantities are reduced below the jobcard quantities.
- **Document list state + jobcard stock edits:** Invoice and jobcard document tables now keep the user’s selected list columns/order when switching between the main list and recurring tabs, and editing a jobcard now restores or deducts stock only for the net product quantity changes introduced by that edit.

- **Client update approval email sync:** Approving a Client Zone information update now also updates the linked client user's sign-in email when the customer email changes, and approval is blocked cleanly if that new email is already in use by another user. Added focused approval-flow regression coverage.

- **Client Zone security hardening:** Portal access is now bound to the linked `customer_id` instead of mutable email matching, client users can no longer change their sign-in email from profile settings, shared Inertia company props no longer expose all active companies to client accounts, Client Zone now rejects non-client sessions explicitly, client registration uses neutralized error messaging with throttling, and client document signing is enforced server-side only when the company has document signing enabled. Added focused feature coverage for these regressions.

- **Customer account balance & statements:** Customer **Show** and **Edit** display a read-only **account balance** derived in-app from open invoice balances minus **unapplied credit** on credit notes (JCO-only, not synced with Xero). Staff with customer **view** and invoice **view** can **download the same ageing statement PDF** as Client Zone from the customer page. The statement PDF **includes the company logo** when configured. **Client Zone dashboard** lists **net balance per company** for the signed-in client (same calculation).

- **Account statement PDF — credit notes:** The ageing statement now lists **unapplied credit notes** (non-voided with remaining credit), adds an **overall position** block (invoice outstanding, unapplied credit, net due), and renames the invoice-only totals section for clarity.

- **Customers list:** The customers index table includes a **Balance (JCO)** column (net balance for the current company, same rules as the customer detail screen). The column is **sortable** (server-side, same calculation as the displayed balance).

- **Information update request emails:** When staff **approve** or **reject** a Client Zone information update request, the **customer** is emailed at their customer record email (after approval, the record is refreshed so a changed email receives the message). Rejection emails include the **optional reason** when provided. Invalid or missing customer email and SMTP failures are logged without blocking the review action.

- **Information update request (show):** Reworked the pending **Approve / Reject** block so the optional rejection note is full-width above the actions, and both buttons share the same height and alignment (no flex stretch on the approve control).

- **Client Zone signature pad:** Pointer coordinates are mapped using the canvas element’s **display size** vs its **bitmap size** (`width`/`height`), so drawing aligns with the cursor when the pad is CSS-scaled (e.g. `w-full`). Stroke segments are drawn once per move (no cumulative re-stroke), line thickness scales with display scale, and `touch-action: none` reduces scroll interference while signing on touch devices.

- **Client Zone email notifications:** When a client signs a **quote**, **invoice**, or **job card** in Client Zone, staff are notified by email (SMTP): **salesperson** on quotes/invoices, **assigned user** on job cards, with a link to the document; if there is no assignee or they have no email (e.g. older records), the mail goes to the **company email** instead. When a client submits an **information update request**, the **company email** receives a notification with a link to review the request. Failures are logged without blocking the client action.

- **Quotes — salesperson:** Added optional `salesperson_id` on quotes (nullable FK to users), populated for new quotes from the creating staff user, and carried through to invoices created via **Convert quote to invoice** when present.

- **Client Zone document view:** Invoice, quote, and jobcard screens now show line groups (when present) with **per-group subtotals**, plus a **Totals** block (subtotal excl. rounding, discount, tax, rounding adjustment, and total) with currency formatting aligned to company number settings.

- **Client zone user deactivation:** Staff with registered-users **Approve / reject** permission can **deactivate** active client accounts (`approval_status` = `deactivated`) or **reactivate** them. Deactivated clients cannot sign in or use Client Zone (clear messages on client login and middleware). Registered-users list adds **Active** / **Deactivated** tabs; hub shows deactivated count when non-zero.

- **Client zone admin permissions (fix):** `hasModulePermission(..., 'approve')` no longer runs SQL against `can_approve` when that column is missing (e.g. before migrations), so `auth.abilities` is not wiped and the sidebar / Administration buttons stay available. Run `php artisan migrate` to enable approve checks fully.

- **Client zone admin permissions:** Added group-permission controls (List, View, Approve/reject) for **Registered users (Client Zone)** and **Information update requests**, a new `can_approve` flag on `group_permissions`, and route enforcement via module middleware (replacing administrator-only access). Sidebar and hub respect these permissions.

- **Client zone admin lists:** Replaced the single **Registered Users** page with an overview hub and separate paginated list + detail views for approved client accounts, pending registrations, and customer information update requests (search/sort on lists; approve/reject on detail where applicable).

## 2026-04-08

- **Security hardening (critical remediation):** Sanitized `.env.example` placeholders, restricted Administration license read route to admin-only, removed insecure cPanel HTTP web-exec fallback, and added stronger license API request signing with nonce-based anti-replay protection.
- **Report + frontend injection defenses:** Added strict report config allowlists for sortable/groupable columns to block unsafe SQL field injection paths, sanitized user-provided external URLs before rendering, and enforced `rel="noopener noreferrer"` on `_blank` links.
- **Platform and pipeline security controls:** Enabled global CSP/security headers middleware (with nonce support for inline bootstrap script/style), introduced a dedicated security CI workflow (`composer audit`, `npm audit`, gitleaks), tightened workflow permissions, and enforced `npm ci`-only installs in CI jobs.
- **Testing compatibility (SQLite):** Guarded MySQL-specific `purchase_order_items.product_id` migration SQL so SQLite test runs no longer fail on unsupported `ALTER ... MODIFY` syntax during migration bootstrap.
- **Testing compatibility (SQLite user enum migration):** Guarded MySQL-specific `users.user_type` enum alter statements so SQLite-based tests can bootstrap migrations without failing on unsupported `MODIFY COLUMN`.
- **Permission boundary test alignment:** Updated invoice email permission-boundary expectation to match current behavior (view-level access allowed, request proceeds to validation), asserting validation errors instead of forbidden response.

- **Localization date/time preferences:** Added company-level localization settings for timezone, date format, and time format in Administration → Localization, alongside existing number separators.
- **App-wide date/time adherence:** Date/time rendering now follows the selected localization timezone and display format across the web app, and request-time server timezone is aligned to the active company setting for consistent backend-generated timestamps.
- **PDF/signature date-time formatting:** Default PDF templates (invoice, quote, jobcard, proforma, purchase order, and client statement) now format document dates and signature timestamps via company localization settings, showing only configured date + time (`H:i`/12h) without weekday/timezone text.
- **Client-zone signature timestamp formatting:** Client Zone signature history now formats `signed_at` using localization settings instead of rendering raw datetime strings, removing weekday/timezone text like `GMT+0200 (...)`.
- **Localization preview + signature UI formatting:** Date/time preview and document signature timestamps now use explicit localized formatting options (date + hour/minute only), preventing fallback browser strings that include weekday/timezone text.
- **Notes panel timestamp localization:** Record notes now display created timestamps through the shared localization formatter so note history uses selected date/time format without weekday/timezone text.
- **Strict date-time output normalization:** Shared localization formatter now builds datetime as `date + time` explicitly (no locale-added weekday/comma/timezone suffix), ensuring values render like `08/04/2026 09:27` across preview/signature/notes views.
- **Localized datepicker on all date inputs:** Replaced plain browser rendering for all `date`/`datetime-local` inputs with a global localized flatpickr layer that keeps ISO submit values for backend compatibility while consistently displaying the selected localization date/time format in every form field.
- **Reports + timesheet date consistency:** Updated reports date rendering to use localization formatting and hardened date-input reinitialization (mutation observer + delayed reapply) so timesheet filter fields no longer fall back to default browser format after filter updates.

## 2026-04-01

- **Xero invoice/quote discount normalization:** Outbound ACCREC line-item export now clamps discount percentage to 0-100 and caps discount amount to the line subtotal magnitude before calculating/sending `LineAmount`. This prevents fully discounted lines from exporting with negative line totals that Xero rejects as expected `0.00`.
- **Xero ACCREC discounted line export hardening:** Discounted invoice/quote lines now omit explicit `LineAmount` and let Xero derive totals from quantity/unit/discount fields, preventing validation failures where Xero expects a fully discounted line to total `0.00`.
- **Client auth isolation:** Added a dedicated client login endpoint/page (`/client-login`) separate from staff login. Client accounts are blocked from staff login and staff accounts are blocked from client login, enforcing role-specific authentication entrypoints.
- **Client statement local time correction:** Adjusted statement PDF `Generated` timestamp to render in `Africa/Johannesburg` time so generated times align with expected local business time (e.g. UTC+2).
- **Vite/Inertia page resolution hardening:** Updated app Blade Vite includes to load only `resources/js/app.ts` (instead of adding the current page component path), preventing manifest lookup errors for newly added Inertia pages when a stale build manifest is present.
- **Client statement timestamp timezone:** Statement PDF `Generated` timestamp now renders using `config('app.timezone')` instead of UTC so it matches app-wide date/time display behavior.
- **Client documents filter enhancement:** Added a document-type dropdown filter (All, Invoice, Quote, Jobcard, Credit Note) to Client Documents, integrated with company/search filters and pagination query state.
- **Client statement converted to company-specific ageing report:** Statement PDF is now only available when a specific company is selected in Client Documents. The report now excludes paid/cancelled invoices and provides ageing buckets (**Current**, **30 Days**, **60 Days**, **90+ Days**) with per-invoice invoice total vs payments vs balance and summary totals.
- **Client zone list UX upgrades:** Client document statuses now render with company-configured labels where available (with title-case fallback), documents list now supports search + pagination, and list actions use button-style controls for View/PDF.
- **Client zone cross-company route fix:** Switched client-zone document routes from implicit model binding to explicit ID lookup so documents from other valid customer-linked companies no longer 404 due to current-company scoped route binding.
- **Client zone UX restructure:** Redesigned client navigation and pages: sidebar now includes dedicated **Client Documents** and **Request Info Update** items, dashboard now shows document totals with status breakdowns only, and documents moved to a separate list view with company filtering limited to companies where the client actually has documents.
- **Client sidebar cleanup:** Removed the Dashboard menu item for client users so the sidebar only shows Client Zone navigation for client accounts.
- **Client zone document viewing + signing:** Added in-app client document view pages (invoice, quote, jobcard) with line items, existing signatures, and client-side signing support when company document signing is enabled. Client users can now view documents in-browser and sign directly from Client Zone, in addition to PDF downloads.
- **Client zone multi-company document visibility:** Client Zone now hides the company selector for client users, aggregates customer documents across all companies matching the client/customer email, displays the source company beside each document, and only surfaces companies that actually have documents in the client view. Statement PDFs now include company per row.
- **Client zone document access + dashboard redirect:** Client users are now redirected from the staff dashboard to the Client Zone, and Client Zone now provides direct PDF downloads for linked invoices, quotes, and jobcards plus a downloadable customer statement PDF.
- **Client user-type schema fix:** Added `client` to the `users.user_type` enum and aligned user-type validation/UI so client-zone registrations no longer fail with MySQL enum truncation errors.
- **Client zone + approval workflow:** Added a customer-facing Client Zone with self-registration linked to existing customer emails, administrator approval of registered client users via a new **Registered Users** module, client-only login gating until approved, document visibility for linked customer records, and customer-information update requests that require admin approval before applying changes to the customer account.
- **UI / checkbox toggle polish:** Fixed inconsistent checkbox/toggle rendering by enforcing rounded styling for the shared UI checkbox component and improving global checkbox alignment in flex rows (notably in Xero integration settings), so toggle controls are consistently rounded and vertically aligned.
- **UI / toggle shape enforcement:** Strengthened global checkbox toggle CSS with hard overrides for width/height/radius so page-level utility classes (e.g. `rounded`, `h-4`, `w-4`) no longer force square/small variants; toggles now keep the full rounded slider shape consistently.
- **UI / toggle centering refinement:** Corrected slider knob offsets so the knob sits perfectly centered inside the track in both off/on states, improving visual alignment in dense permission tables and settings screens.
- **UI / toggle vertical centering fix:** Switched checkbox toggle knob alignment to true center positioning (`top: 50%` with transform) and balanced inner-track offsets to eliminate visible top/bottom spacing mismatch.
- **UI / checkbox controls:** Restyled native checkbox inputs globally to render as slider-style toggles (including checked, focus, disabled, and dark-mode states) so toggle behavior looks consistent throughout the app without per-page component rewrites.
- **Recurring jobcards/invoices + scheduler:** Added recurring-document management in Jobcards and Invoices via a new **Recurring** tab where users can select a source document, recurrence frequency (`daily`, `weekly`, `monthly`, `quarterly`, `yearly`), and start/end dates. Added scheduled generation (`recurring:generate-documents`) plus backend cloning logic to automatically create due documents from templates.
- **Groups / payment methods:** Each group can enable which recording methods members may use (**Card**, **Cash**, **EFT**) under Administration → Groups → Edit, in the permissions panel. Shared `auth.payment_methods` drives invoice **Add Payment**, **POS** (cash/card/eft only; **Account** stays separate), and credit-note **refund** method buttons. `PaymentsController`, POS checkout, and credit-note refund routes reject disallowed methods server-side. Defaults for existing groups: all three on.

## 2026-03-26

- **License validation cadence (performance):** Instance license checks now use the persisted `instance_licenses` validation state for up to 24 hours and only call the remote license server once per day (unless force-refresh is requested). Updating the license key now clears stored validation metadata so the new key is revalidated immediately on next check.

## 2026-03-25

- **Invoice sync guard + stock log severity:** Invoice stock deduction now logs insufficient-stock skips as `warning` (not `error`) to match non-blocking behavior. Xero outbound invoice sync now short-circuits fully paid invoices (without unsynced payments) before invoice-by-id fetch/export, and skips outbound invoice updates after importing payments from Xero for paid invoices. This reduces `LineItemID` validation noise and cuts high-volume `GET /Invoices?where=InvoiceID==Guid(...)` bursts.
- **Credit note allocation link formatting:** Credit note Show no longer repeats the invoice number when the linked invoice title already includes it; the allocations list now displays a cleaner invoice label.
- **Credit note detail allocation links:** Credit note Show now normalizes allocation payloads to include invoice link metadata (`id`, `invoice_number`, `title`) so allocated invoices consistently render as clickable invoice-number links instead of falling back to raw invoice IDs.
- **Invoice credit-note detail visibility:** Invoice Show now lists each allocated credit note row (linked to the credit note) with the exact allocated amount applied to that invoice, instead of only relying on a rolled-up credited total. This makes multi-credit-note application auditable per invoice.
- **Credit notes / multi-invoice allocations:** Added `credit_note_allocations` with per-invoice allocated amounts, updated invoice balance/paid detection to use allocation amounts (with legacy fallback), synced Xero credit note import/export to persist and push multiple allocations, and added Create/Edit UI controls plus Show-page display for managing/viewing allocations across multiple invoices.
- **Xero credit notes (allocations + inclusive/exclusive):** Credit-note import now resolves and processes **all** Xero allocations (credit notes can be allocated across multiple invoices). When allocations map to multiple invoices, JCO leaves `credit_notes.invoice_id` unset (legacy single-invoice field) and still updates statuses for all affected invoices. Import also respects Xero `LineAmountTypes` and normalizes **Inclusive** credit-note line amounts back to exclusive so local subtotal/tax/total math remains correct.
- **Invoices / payments permission fix:** Allowed view-only invoice users to add payments by aligning `POST /payments` route middleware to `invoices,view` and authorizing against `InvoicePolicy::view` in `PaymentsController::store()`. This prevents the Add Payment flow from failing for non-edit roles.
- **Xero invoice-by-id request reduction:** Added short-lived caching for `getXeroInvoiceCached()` and switched remaining hot paths (invoice hydrate, payment reconciliation, and invoice export pre-read) from direct `getXeroInvoice()` calls to the cached accessor, reducing repeated `GET /Invoices?where=InvoiceID==Guid("...")` bursts that trigger rate limits.
- **Xero PO sync reliability + log noise cleanup:** Fixed undefined `$poBatchSize`/`$poBatchDelayMs` in purchase-order export, and added per-row individual retry when batched PO rows return validation errors so account-code/status fallbacks can recover. Also reduced expected operational noise by downgrading jobcard stock-deduction failures to warning and lowering ReminderService “no WhatsApp service” initialization log level from warning to info.
- **Xero payments rate-limit guard:** Removed per-invoice fallback calls to `GET /Payments?where=Invoice.InvoiceID==Guid(...)` during invoice payment reconciliation in `XeroService`. The flow now uses embedded invoice payment data and the existing bulk payments sync path, reducing high-volume payment GET bursts that were triggering Xero rate limits.
- **Document email route permission fix:** Updated route middleware for invoice, quote, and jobcard email endpoints to require **view** permission (not **edit**) so view-only users can send document emails without hitting Access Denied.
- **Xero invoice fetch rate-limit guard:** removed remaining direct `GET /Invoices/{id}` calls in `XeroService` and switched invoice-by-id lookups to `GET /Invoices?where=InvoiceID==Guid("...")`, including webhook invoice refresh flow. This lowers high-cost per-invoice endpoint usage in production and reduces risk of Xero rate-limit spikes.
- **Document email permissions alignment:** Email send behavior is now consistent across document modules: email actions are authorized by **view** permission (not edit) for invoices and quotes, and the jobcard Show-page Email button no longer depends on edit rights.
- **Invoices / email permission:** Restored the **Email** action for users without invoice edit rights. Invoice show now displays Email when the user can **view** invoices, and `InvoicesController::email` authorizes against **view** instead of **update** so send-email works for view-only roles.
- **Xero / outbound batching expansion:** Extended batched POST exports beyond invoices. Outbound sync now batches **quotes** (`/Quotes`), **purchase orders** (`/PurchaseOrders`), and **suppliers/customers** (`/Contacts`) with `SummarizeErrors=false`, plus per-batch delays and fallback to single-record retries where needed. New env/config knobs: `XERO_QUOTE_EXPORT_BATCH_SIZE`, `XERO_QUOTE_EXPORT_BATCH_DELAY_MS`, `XERO_PURCHASE_ORDER_EXPORT_BATCH_SIZE`, `XERO_PURCHASE_ORDER_EXPORT_BATCH_DELAY_MS`, `XERO_SUPPLIER_EXPORT_BATCH_SIZE`, `XERO_SUPPLIER_EXPORT_BATCH_DELAY_MS`, `XERO_CUSTOMER_EXPORT_BATCH_SIZE`, `XERO_CUSTOMER_EXPORT_BATCH_DELAY_MS`.
- **Xero / outbound invoices:** `syncInvoicesToXero` now sends **multiple ACCREC invoices in one POST** to `/Invoices` (with `SummarizeErrors=false`), like contacts/products batching. Batch size and delay between batches are configurable via **`XERO_INVOICE_EXPORT_BATCH_SIZE`** (default 25, max 50) and **`XERO_INVOICE_EXPORT_BATCH_DELAY_MS`**. On batch HTTP failure, the run **falls back to one invoice per request**. Single-invoice `createOrUpdateInvoiceInXero` uses the same query flag and handles per-row validation in a 200 response.
- **Invoices / permissions + PDF signatures:** Invoice Show now keeps **Add Payment** visible based on outstanding balance even when the user cannot edit invoices. Restored legacy PDF signature lines (**Received by / Date / Signature**) on invoice, quote, and jobcard templates when company **document signing is disabled**; when enabled, captured canvas signatures continue to render.
- **Xero / encryption:** `XeroSettings` OAuth fields (`client_secret`, `access_token`, `refresh_token`) use a cast compatible with **legacy plaintext** values in the database. After 1.9.0, the built-in `encrypted` cast tried to decrypt existing **raw JWT** tokens and threw `DecryptException` (“The payload is invalid”), breaking `xero:sync-payments` and other jobs. The new cast decrypts when the value is Laravel-encrypted and otherwise returns the stored string; saves still encrypt.

## 2026-03-24

- **Invoices / rounding:** `ensureConvertedInvoiceRoundingLine` now **deletes every** “Rounding Adjustment” row then **creates a single** normalized line, so client + server could no longer leave **stacked duplicate** rounding lines (which inflated totals and the rounding summary). Invoice **Show** and default **invoice PDF** **Subtotal** rows now sum line totals **excluding** rounding (same basis as Create/Edit), not only the stored `invoices.subtotal` column.
- **Invoices:** persisted **`subtotal`** from `calculateTotals()` **excludes** the **Rounding Adjustment** line. **Total** remains `sum(all line totals) + tax`.
- **POS / invoices:** nearest-**$0.10** totals now match normal invoices: POS sales call `ensureConvertedInvoiceRoundingLine` before `calculateTotals()`, and the POS screen shows **Rounding** when needed (per-line tax + 10¢ rounding, aligned with the server). `ensureConvertedInvoiceRoundingLine` clears the cached `lineItems` relation after changes so totals include the rounding line.
- **Xero invoice export:** fixed `syncInvoicesToXero` failing with **undefined `$createCandidates`** after a partial refactor (the primary candidate query was still assigned to `$invoices`). The first batch is now correctly stored in `$createCandidates`; merged create + update lists are **deduplicated by invoice id** so the same invoice is not queued twice when it matches both pools.
- **Sidebar:** the **Administration** footer link is shown only when `auth.user.is_administrator` is true (matches `User::isAdministrator()` / administrator groups). Non-admins no longer see a nav item to pages that would 403 anyway.
- **Build:** `vue-email-editor` is required for `EmailTemplateUnlayerEditor.vue` (Unlayer drag-and-drop email templates). It was already listed in `package.json` but could be missing from `node_modules` after a partial clone or skipped install—run `npm install` (lockfile updated) so Vite resolves the import.
- **Suppliers**, **purchase orders** index, and **reports** (saved-report rows + report template cards): row **Edit** / **Delete** actions use the same pill-style controls as customers (indigo / red backgrounds) instead of plain colored text at `md+` breakpoints, which looked like bare links after the responsive action-label change.
- **Quote and jobcard status labels** from Administration → Status Editor now appear everywhere the status is shown or chosen: **index list** (filter dropdown + status column), **create/edit** status fields, and **show** page read-only badges (via `formatStatus` lookup). Previously only the show-page status buttons used `statusOptions` from the server; list and edit views used hard-coded default text.
- List and index row actions (Edit, Delete, View, Email, etc.) are normalized: **text labels from the `md` breakpoint up**, with **icon-only** controls on smaller screens for space. Shared helper `ListTableActionLabel.vue` (slot = icon) keeps screen-reader labels on narrow viewports. Applied across CRM/document index tables, products grid/list, reports templates and saved reports, administration lists (bank accounts, tax rates, chart of accounts, categories, PDF/email templates), company cards, users/groups, teams, timesheet delete, backup schedules and backup rows, and product batch/serial lists.
- Show-page sub-panels now respect the same module permissions as the corresponding index/list routes, so users without **list** (or **view** where that gates the module index) no longer receive or see nested listings they could not open from the main menu. **Customer** show: contacts pagination requires **contacts** list; account history only includes document types the user may list/view (**jobcards** list, **quotes** view, **invoices** view, **credit-notes** list). **Supplier** show: purchase order history requires **purchase-orders** list. **Product** show: recent invoice usage requires **invoices** view. **Jobcard** show: related purchase orders and purchasing totals require **purchase-orders** list (server strips PO data when absent; pricing summary hides purchasing lines in the UI).

## 2026-03-23

- Xero outbound payments: fixed **duplicate payment posts** caused by treating `payments.updated_at > xero_synced_at` as “needs sync”. `updated_at` is always newer than Xero’s `UpdatedDateUTC` stored in `xero_synced_at` after import or push, so payments were pushed repeatedly. Unsynced selection is now **missing `xero_payment_id` or `xero_synced_at` only**; successful invoice/credit-note payment pushes set `xero_synced_at` to **now** and throw if Xero returns no `PaymentID` (avoids silent re-queue). Removed unused `syncPaymentsToXero` invoice payload argument.
- Xero customers (import): when the contacts list omits `ContactPersons`, the integration only calls **`GET /Contacts/{id}`** to load people if that Xero contact’s **`UpdatedDateUTC` is after** the last completed **customers** sync watermark (or it’s the first run / **force full** fetch). Unchanged contacts on a delta run skip that extra request; inline `ContactPersons` on the list payload are still applied when present.
- Xero invoices (import): **`GET /Invoices/{id}`** hydration for thin list rows runs only when **`UpdatedDateUTC` is after** the last completed **invoices** sync watermark, or during **backfill**, **first import**, **missing `UpdatedDateUTC`**, or when a **matching local invoice has no line items** (so line backfill still works). Rows that already include line items and an invoice number/reference never trigger an extra GET.
- Xero outbound payment push: no longer **GET**s the Xero invoice before each `POST /Payments`; the amount sent is the local payment amount (rounded). Removes extra API calls per payment; if Xero already has the invoice paid or amounts disagree, Xero’s validation response applies instead of pre-fetching `AmountDue` or auto-linking from a full-invoice payload.
- Xero scheduling: sync Artisan tasks no longer use `everyMinute()`; they default to **four times per hour** at `:00/:15/:30/:45`, each command staggered by +0…+8 minutes so companies avoid simultaneous API bursts. Configure `XERO_SYNC_BASE_MINUTES` (comma-separated minutes) to change cadence (e.g. `0,30` for twice hourly). Added optional **`XERO_REQUEST_BUDGET_PER_DAY`** (`request_budget_per_day` in config, `0` = off) to stop outbound calls once a per-company daily count is reached; existing **`XERO_REQUEST_BUDGET_PER_MINUTE`** and import/export caps in `config/services.php` still apply—lower those env values to reduce daily totals with large datasets.
- Invoices Create (Vue): fixed CI/build failure by using JavaScript `else if` instead of PHP-style `elseif` in invoice prefill logic.
- Invoice PDFs: default Blade invoice PDF and the built-in HTML PDF template starter no longer print **payment terms** (`terms`); only **notes** and **terms & conditions** appear in that section. `{{invoice.terms}}` remains available in the template picker for custom layouts.
- Invoices: added `terms_conditions` on invoices so **Terms &amp; Conditions** (long body text) is separate from **payment terms** (`terms`: COD, Net 30, etc.). Quote/jobcard conversion and “create invoice from quote/jobcard” prefill no longer copy quote/jobcard `terms_conditions` into payment `terms`. Default company “invoice terms” setting maps to the new field. Updated invoice PDF, customer email, Show/Create/Edit UI, POS payload, and PDF template picker placeholders (`{{invoice.terms}}` = payment terms, `{{invoice.terms_conditions}}` = T&amp;C). **Note:** Custom HTML PDF templates that used `{{invoice.terms}}` for long legal text should switch to `{{invoice.terms_conditions}}`; existing rows only populate the new column for new/edited invoices unless you migrate old data manually.
- Stock movements index: fixed a crash when a movement referenced a missing/deleted product (`Cannot read properties of null (reading 'name')`) by treating `product` as optional and showing a safe label.
- Xero customers: when importing from Xero, **contact persons** (`ContactPersons` on the Xero contact) are now upserted into JCO `contacts` for that customer. Bulk contact list responses often omit people, so the sync loads the single-contact payload when needed. Unchanged customers on a delta sync still refresh contact persons so new people in Xero are picked up.
- Xero payments: fixed outbound payment sync being skipped when JCO and Xero both looked “paid” but local `Payment` rows still lacked `xero_payment_id` (now reconciles via Xero payment import + retry push). Replaced fragile “paid in Xero” detection that could treat `Total` as amount owing. When Xero already shows an invoice fully paid, creating a JCO payment now attempts to **link** to the existing Xero payment instead of only skipping. Webhook and inbound invoice updates (`updateInvoiceFromXero` / `updateInvoiceFromXeroData`) now run payment import so JCO gets real `Payment` records (and correct balances), not only `status = paid`.
- PDF signatures: updated quote/invoice/jobcard signature rendering to fixed 3-column rows and grouped signatures per row so PDF page breaks move an entire signature row to the next page instead of splitting it; applied the same non-breaking row behavior to proforma signature lines.
- PDF signatures follow-up: switched signature row CSS to table-based fixed 3-column layout for DomPDF consistency, fixing cases where only two signatures rendered per row.
- Security remediation completion: tightened `TimeEntryController::convertToLineItems` to require jobcard update permission (not view), moved additional Xero controller/webhook logs to `SafeLog` integration logging, added early OAuth callback regression coverage for missing/invalid `state`, and added webhook signature feature tests plus PDF template output sanitization tests/hardening for script/event-handler stripping before PDF render.
- Test coverage expansion (security plan follow-up): added feature tests `XeroUpgradeCompatibilityTest` (stable cache-key + reset behavior), `TenantIsolationDocumentsTest` (cross-company route-model isolation for jobcards/quotes/invoices/purchase-orders), and `PermissionActionBoundaryTest` (view-only users blocked from convert/email/status/receive-item actions).
- Security regression tests: added `ScopedValidationRegressionTest` (reject foreign-company IDs for quote/invoice/PO/time-entry create flows) and `TimeEntryInferenceSecurityTest` (limited-user timesheet summary/list remains self-scoped even when `user_id` filter is spoofed).
- Security regression expansion: added `TenantIsolationAdditionalModulesTest` (cross-company route-model isolation for customers/contacts/products/suppliers/stock-movements/reports) and `PermissionActionBoundaryAdditionalTest` (view-only users blocked from customer/contact/product updates, stock movement create, credit-note status update, and report update).
- Test hardening fix: updated permission-boundary tests to target real in-company records (so authorization middleware is evaluated before not-found handling) and aligned additional tenant-isolation report seed data with current schema by removing non-existent `reports.filters` insert usage.
- Quotes: added the same document-signing flow as invoices/jobcards (Sign button, name + canvas capture modal, signature list on Show, and quote PDF signature rendering when signatures exist), controlled by Company Settings `enable_document_signing`.
- Documents: added company-level **Enable document signing** setting and end-to-end signature capture for jobcards/invoices (`Sign` action with name + drawn signature), including signature display on both Show pages and automatic inclusion in generated PDFs when signatures exist.
- Jobcards: aligned Status Time Tracker transition datetime rendering with the Show page’s existing `Last updated` format by sending ISO timestamps from the backend and formatting them via the same client-side `formatDateTime()` helper.
- Jobcards: refined Show-page status transition timestamp formatting to use the Laravel app timezone (`config('app.timezone')`) explicitly for Status Time Tracker history rows.
- Jobcards: adjusted Show-page status transition timestamps to render in the server timezone instead of GMT for the Status Time Tracker history table.
- Jobcards: added a **Status Time Tracker** panel to the jobcard Show page, showing time spent per status (stacked bar + legend) and the status transition history table, using the same duration logic as the detailed jobcard report.
- Quotes: fixed a Show-page runtime error for users without quote edit permission where a missing `convertedJobcardId`/`invoice_id` still rendered `View` links and called Wayfinder routes with `null`. The page now only renders those links when valid IDs exist.
- Reports: fixed report totals formatting on the Show page to use company localization separators (decimal and thousands) via the shared `useNumberFormat()` currency formatter instead of hard-coded `toFixed(2)`.
- Added a new Notes module with polymorphic related-record support across current show-page modules, including subject/description capture, optional file attachments, record-scoped searchable + paginated note feeds, and a shared show-page subpanel that auto-loads notes for the active record.
- Updated the notes subpanel UX to use a `Create Note` modal (instead of an inline form), and added automatic print-note logging for quote/invoice/jobcard/purchase-order print actions with the generated PDF attached (e.g. `Quote 123 printed`).
- Fixed notes retrieval to use Eloquent morph type values (e.g. `quote`, `jobcard`) instead of class names, so manually created notes and auto-generated print notes now appear correctly in subpanels and lists.
- Refined Notes UI styling so create modal fields, search input, action buttons, and modal layout now match the system’s existing form/dialog visual patterns used across other modules.

## 2026-03-21

- Administration: added **Status Editor** (`/administration/status-editor`) for company-specific Jobcard and Quote status labels, including per-section **Reset to Default**. Jobcard/Quote show pages now consume server-provided status options so the customized labels appear in status controls without changing underlying status codes.
- Customer and supplier account history: customer show now includes a paginated **Account History** panel that combines **Jobcards**, **Quotes**, **Invoices**, and **Credit Notes** (newest first) with direct links and totals. Supplier show now displays paginated **Purchase Orders** (not just a recent limited list), including direct links and formatted totals.
- **Localization (number formatting):** Administration → **Localization** sets company default **decimal** and **thousands** separators (stored on `companies`, shared to the SPA as `numberFormat`). Vue screens that showed ZAR via `Intl.NumberFormat('en-ZA')` now use `useNumberFormat()` so amounts match those settings. Default PDF views and document emails format quantities and ZAR amounts with `Company::formatNumber()` / `formatCurrencyZar()` so printed/PDF/email output stays consistent with the UI.
- Document conversion (prefill) UX: **Create** invoice, quote, and jobcard from a source document now initialise per-line **discount type** (`amount` vs `%`) from stored `discount_percentage`, so percentage discounts display and edit correctly instead of showing as zero. **Invoice** `store` now persists header `discount_amount` / `discount_percentage` from the form (aligned with quote/jobcard create and with convert-to-invoice model methods).
- Xero: quote export to Xero now sends line **DiscountRate** / **DiscountAmount** (same rules as invoices) via shared `buildXeroAccRecLineItemForExport()`, so discounted quote lines sync correctly instead of only sending list price and a mismatched `LineAmount`.
- Purchase orders: **Create** and **Edit** line items use the same layout and styling as invoice line items (Line Items heading, table header typography, column order Qty → Description → Cost → Tax → Account → Total, dashed “+ Add Line Item”, invoice-style delete icon, ZAR `formatCurrency` for line and summary amounts, separate **Totals** card without green emphasis).
- Invoice PDFs: when **total tax** (`tax_amount`) is zero, the default PDF title and heading use **Invoice** (with existing uppercase styling → **INVOICE**) instead of **Tax Invoice**. Custom HTML PDF templates receive `{{invoice.document_title}}` (same logic); the template editor default and field picker include this placeholder. Older stored templates that still hard-code “Tax Invoice” can be updated in Administration → PDF Templates to use `{{invoice.document_title}}`.
- Purchase orders: supplier on create/edit is now a **searchable vendor field** (same pattern as invoice customer): typeahead against `GET /suppliers/search` (active suppliers, name/email/phone/VAT/city/country). PO create/edit pages no longer load the full supplier list; preselection from `?supplier_id=` or the current PO uses an `initialSupplier` payload.
- Quotes & invoices authorization: `QuotePolicy` and `InvoicePolicy` now require matching group module permissions (`view` / `create` / `edit` / `delete`) in addition to tenant ownership; accepted quotes and paid invoices still need `edit_completed` for update/delete. `QuotesController` and `InvoicesController` call `authorize()` on list/create/show/edit/update/destroy and related actions (PDF/email/status) so permissions cannot be bypassed if a route is misconfigured. Quote→invoice conversion requires **invoices** `create`; jobcard→quote/invoice conversion requires **quotes** / **invoices** `create` respectively (not only jobcards edit). Dashboard quick actions and recent-activity links respect the same abilities; invoice index row **Edit** uses `useAuthAbility('invoices','edit')` with server `canEditCompleted` for paid rows.
- Purchase orders: **Edit** page and `PUT` update (same validation as create; blocked when status is received or any line has `quantity_received > 0`). **Edit** and **Delete** on index row actions and on the PO show header (permission-gated; delete hidden for received POs to match `destroy`). Regenerated Wayfinder routes for `purchase-orders.edit` / `purchase-orders.update`.
- Customers & contacts: added **Delete** actions on index and show pages (confirmation + `router.delete` to `customers.destroy` / `contacts.destroy`), visible only when the user has `customers` / `contacts` **delete** in `auth.abilities`.
- UI: Create / edit / delete (and other mutation controls aligned with routes) are hidden when the user lacks the matching `auth.abilities` flag—across major CRM/document modules (customers, contacts, suppliers, jobcards, quotes, invoices, credit notes, stock movements, purchase orders, reports, timesheet delete, users, groups) on index and show pages where those actions existed. Added `useAuthAbility()` composable wrapping shared Inertia abilities. Document list rows no longer show disabled edit/delete placeholders; invoice row delete now keys off `invoices` delete permission, not only edit. Quote/jobcard/invoice/credit-note status pickers show read-only status when the user cannot edit that module.
- Sidebar: Jobcards, Quotes, Invoices, and Timesheet now respect group permissions (same abilities as their index routes: `jobcards` list, `quotes`/`invoices` view, `timesheet` view). Limited users still only see Jobcards and Timesheet in the main nav, but those links hide when the corresponding permission is off. Inertia `auth.abilities` now includes `timesheet`. Unknown nav items no longer default to visible when `abilities` is present.
- Installer bootstrap secrets (security plan §13): the web installer no longer writes `ADMIN_EMAIL` or `ADMIN_PASSWORD` to `.env`; admin credentials are passed only via runtime `config('installer.*')` for the `db:seed` step, then cleared, and any matching lines are stripped from `.env` after install (including values copied from a template). `AdminUserSeeder` no longer defaults to a weak password—it runs only when credentials come from the installer or when `ADMIN_EMAIL` / `ADMIN_PASSWORD` are present in the environment for that process (e.g. one-off `export` for CLI seeding). Added `config/installer.php` for documented defaults.
- PDF / custom template hardening (security plan §12): custom `PdfTemplate` HTML now escapes `{{field}}` / `{{this.field}}` values by default; use `{{{field}}}` / `{{{this.field}}}` only where stored HTML must render (documented in the PDF template editor UI). DomPDF `enable_javascript` is disabled (no PDF-viewer script injection). Administration PDF template `index` / `create` / `store` / `upload-image` call policies (`viewAny`, `create`); `edit` uses `view` then `update` on save. Default and `templates:update-purchase-order` PO templates use triple braces for notes/terms so rich text still renders. Added `tests/Unit/PdfGenerationServiceHandlebarsTest.php`.
- Time entry / timesheet hardening (security plan §11): added `timesheet.permission` middleware (`EnsureTimesheetPermission`) delegating to the same Inertia-aware checks as `module.permission` but fixed to the `timesheet` module; time-entry routes use it instead of repeating `timesheet` in every route. Introduced `TimeEntryPolicy` for `update`/`delete` (company scope + limited users only on their own rows; limited users cannot delete). `TimeEntryController@index` summary totals and list filters now respect limited users (no company-wide aggregates via `user_id` spoofing; user filter list is self-only; jobcard filter list is assignment-scoped). `store`, `startTimer`, and `convertToLineItems` authorize `view` on the target jobcard; convert adds validation so limited users only convert their own entries. Resume-timer lookup is scoped by `company_id`. Added `tests/Unit/TimeEntryPolicyTest.php`. Renamed `XeroSettingsController::authorize` to `redirectToXero` so it does not override `AuthorizesRequests::authorize()` (route URL `/xero/authorize` unchanged).
- Formal Laravel authorization policies (security plan §10): added `ChecksTenantOwnership` plus model policies for tenant-owned resources (documents, CRM, stock, administration entities, reports/templates, licenses, payments, WhatsApp settings, etc.). Controllers now use `$this->authorize()` instead of ad hoc `company_id` checks; `ProductPolicy` covers nested batch/serial routes via `manageBatch` / `manageSerial`; `CreditNotePolicy::detachRefundPayment` covers credit-note refund rows; `JobcardPolicy` centralizes limited-user visibility with `updateStatus`; `ReportTemplatePolicy` allows global templates (`company_id` null) where the UI already did. Base `Controller` uses `AuthorizesRequests`. Added `tests/Unit/TenantAuthorizationPoliciesTest.php` for core policy behaviour.
- Safer logging for integrations and admin flows (security plan §9): added `App\Support\SafeLog` with recursive redaction for common secret key names, email/phone masking, long text excerpts, and `httpResponseContext()` for HTTP failures without full bodies. Replaced high-risk logs in Xero token exchange, Xero API fetch errors, cPanel API calls, BulkSMS/WhatsApp services, jobcard email send, license deploy/upgrade failures, and removed verbose supplier index / full company `request()->all()` debug logging.
- Xero inbound webhooks (security plan §8): `POST /xero/webhook` now requires a valid `x-xero-signature` HMAC-SHA256 (base64) of the **raw** request body using `XERO_WEBHOOK_KEY` from the Xero Developer Portal; unsigned, invalid, or unconfigured keys yield 401/503 with no sync work. Replaced the route closure with `XeroWebhookController`, excluded the path from CSRF, and removed the unsafe `XeroService::getCurrent()` fallback when no tenant maps to local settings (those requests return `ignored` instead of erroring). Added `App\Support\XeroWebhookSignature` and a small unit test.
- Xero OAuth `state` hardening (security plan §7): each `/xero/authorize` run stores a new cryptographically random value in session and sends it to Xero; `/xero/callback` validates it with `hash_equals`, clears it after success or on mismatch, and handles provider `error` / `error_description` responses without exchanging the code. Removed authorization-code logging from failure logs.
- Encrypted integration credentials at rest (security plan §6): Eloquent `encrypted` casts on Xero (`client_secret`, `access_token`, `refresh_token`), SMS (`bulksms_password`), WhatsApp (`api_key`, `api_secret`), and legacy SMTP `smtp_password` on `companies` and `users`. Added migration `2026_03_21_200000_encrypt_integration_credentials_at_rest` to widen MySQL/PG string columns where needed and encrypt existing plaintext values idempotently (skips payloads that already decrypt). Hid `smtp_password` on `Company` and `User` serialization so it cannot leak via full-model Inertia props. **Deploy:** run this migration before or with the release that adds the casts, or reads of existing rows will fail decryption.
- Stopped sending integration secrets to the browser (security plan §5): `XeroSettings` now hides `client_secret`, `access_token`, and `refresh_token` from serialization; the Xero settings page receives `has_client_secret`, `has_access_token`, `has_refresh_token`, `is_connected`, and `needs_reauthorization` instead of raw tokens, with “leave blank to keep” client secret handling and PUT updates that only replace the secret when a new value is posted. Extended the same pattern to SMS (`has_bulksms_password`, preserve password on update when blank) and WhatsApp (`has_api_key` / `has_api_secret`, hide both in the model, preserve on blank). Tightened WhatsApp store/update validation so secrets stay required only when enabling without an existing stored value.
- Licensing instance hardening (security plan §4): `license.infrastructure` middleware (`EnsureLicenseInfrastructureAccess`) limits deploy, upgrade, and force-SSL POST routes to administrators. Non-admins receive masked license keys in Inertia payloads (list/show/edit), no copy control, and success messaging that omits the raw key after create; shared auth user now includes `is_administrator` for UI. Added `License::maskLicenseKey()` for consistent masking.
- Scoped request validation for tenant-owned foreign keys via `App\Support\CompanyScopedRules` (`Rule::exists` + `company_id`, parent checks where needed): documents (quotes, invoices, credit notes, jobcards), payments, licenses, contacts/customers (including email templates), purchase orders (including receive-item batches/serials resolved per line), stock movements (product/serials + transfer destination access), time entries, chart-of-account parents, product serial batch links, Xero company switch, and report templates (company-owned or global default). Added `after` validators so invoice line serials, PO receive batches/serials, stock serials, and time-entry conversion IDs match the correct product/jobcard.
- Aligned `module.permission` middleware with action type on quotes (explicit routes: view/create/edit/delete per verb), invoices (email + payment create require `edit`), jobcards (email + convert require `edit`), credit-note search endpoints (`view`), and time entries (`timesheet` view/create/edit/delete + timer routes; convert uses `jobcards,edit`). Added `timesheet` to `GroupSeeder` and a migration that seeds default `timesheet` rows for existing groups so the timesheet module stays usable after deploy.
- Hardened multi-tenant isolation for implicit route model binding: added `ScopedToCurrentCompanyRouteBinding` so tenant-owned models resolve only when `company_id` matches the authenticated user’s current company (404 otherwise), and restricted `Company` route binding to companies the user may access via `hasAccessToCompany`.

## 2026-03-20

- Kept quote PDF type selection in the quote print/email flow (Quotation vs Proforma Invoice) and removed the company-level default quote PDF type setting so this remains a per-action choice.
- Fixed quote PDF template filtering so quotation sends/downloads correctly target the `quote` template module (not `quotation`), ensuring Quote templates and defaults are selectable when printing/emailing.
- Updated Quote `Download PDF` UX to always open a selection modal with explicit PDF type choice (Quotation or Proforma Invoice) before downloading, preventing implicit default-to-Quote behavior.
- Set Quote `Download PDF` modal to preselect `Quotation` as the default PDF type while still allowing users to switch to `Proforma Invoice`.

## 2026-03-17 - version 1.7.7

- Added line groups to all document line items: quotes, jobcards, invoices, credit notes, and purchase orders. Each document gets a default "Items" group when created; line groups are displayed on PDFs with group headers. Document conversions (quote→jobcard, quote→invoice, jobcard→invoice) copy line groups and assign line items to the corresponding groups.
- Standardized document conversion traceability on Quote and Jobcard show pages: converted records now persist source metadata (`source_type`/`source_id`), display the original source document link, and hide only conversion actions already completed (showing a view link instead).
- Upgraded customer/contact email send flows with a manual-first composer: no template is preselected by default, users can enter/edit subject and HTML body directly, and selecting a template now prefills editable content with an on-screen preview.
- Added persistent email activity history for customers and contacts, including direct emails and document emails (invoices, quotes, and jobcards), with status tracking and timestamps shown in new Email Activity panels on customer/contact detail pages.
- Replaced the administration email-template editor implementation from GrapesJS to `vue-email-editor` (Unlayer), with a visual designer + manual HTML tabs and image upload support retained for template assets.
- Standardized outbound email identity defaults so sender display name uses the current company name and reply-to uses the current company email across direct customer/contact emails and core document email flows.
- Improved CRM detail pages: Email Activity now has explicit per-page pagination controls on customer and contact views, and the Customer Version History panel has been removed.
- Added line group controls to document create/edit forms (quotes, invoices, jobcards, credit notes, and purchase orders): users can add/remove group names and assign each line item to a group from the form UI.
- Updated PDF rendering for historical documents so line items still print when line groups are missing or mismatched; ungrouped items now fall back into an "Items" section.
- Reworked quote line-group editing UX in Create/Edit: line items now render under their group sections (no per-line group dropdown), and dragging a line item between positions/groups updates both order and group assignment.
- Applied the same grouped line-item + drag/drop UX to invoice, jobcard, credit note, and purchase order forms so line items are managed directly within each group and can be reordered or moved between groups by drop position.
- Fixed edit screens for historic documents so line items with missing/invalid `line_group_id` are auto-assigned to the default group on load and no longer disappear.
- Added drag/drop UI affordances in grouped line-item editors (quotes, invoices, jobcards, credit notes, and purchase orders): visible grab handles plus highlighted drop targets for clearer reordering/move feedback.
- Improved grouped line-item drag UX: custom drag preview chip now appears while dragging, rows get active drag styling, and grab handles use higher-contrast bordered badges for better visibility.
- Fixed Xero invoice export validation for rounding adjustment lines by reconciling outbound `UnitAmount` with stored `LineAmount` on non-discounted lines when totals differ, preventing "line total does not match expected line total" sync failures.
- Simplified PDF line-item tax columns across invoice, quote, proforma invoice, jobcard, and purchase order templates by removing the secondary tax name/rate text under each tax amount.
- Updated the invoice PDF company information block to use aligned label/value rows (address, city, VAT number, phone, email, fax) for a cleaner, more consistent header layout.
- Refined the invoice PDF company information block to use a true HTML table for label/value pairs so value alignment stays consistent across PDF renderers.
- Adjusted the invoice PDF company details table to auto-size its columns/overall width based on content instead of forcing a fixed full-width layout.
- Added trailing colons to invoice PDF company details labels (e.g. `Email:`) for clearer label/value separation in the table layout.
- Applied the same auto-width, table-based company details format (with colon-suffixed labels) to quote, proforma invoice, jobcard, and purchase-order PDFs for consistent header alignment across all core documents.
- Updated all core PDF templates to suppress placeholder migration emails containing `sage-migration.local` (company and document/customer/supplier email fields) so non-production addresses are hidden in printed output.
- Refreshed the app sidebar visual styling with a more modern look (softer card-like header, improved section label/menu item hierarchy, and updated hover/active states) while keeping existing navigation behavior intact.
- Updated the app theme sidebar base color to a clearer light grey for better visual separation from main page content.
- Replaced the credit-note invoice dropdown on Create/Edit with a searchable select-style picker (matching customer lookup behavior) while intentionally excluding quick-create actions for invoices.
- Improved credit-note Create/Edit load performance by removing large preloaded invoice/product datasets from Inertia props and switching to debounced server-side invoice/product search endpoints.
- Fixed invoice permission enforcement so users without `invoices.edit` can no longer access invoice edit/update/status actions; payment creation remains available to invoice-view users, and invoice edit controls are now hidden when edit permission is missing.
- Updated company switching behavior to always redirect to the dashboard after a successful switch, preventing record-page errors when the same URL is invalid in the newly selected company context.
- Added per-user, per-company list-view column preferences across index pages via a new `Edit Columns` control in the app header, supporting column show/hide and drag-to-reorder with server-side persistence.
- Fixed list column editor reordering instability where dragged columns could snap back due to over-aggressive DOM observer reloads; preferences now stay in place immediately after reorder.
- Fixed Xero payment export for fully paid local invoices: removed an incorrect cap that used JCO `remaining_balance` (0 on paid invoices), and expanded invoice export selection to include invoices with unsynced payment records so payment sync retries are not skipped.
- Fixed Vue template parse errors (`Element is missing end tag`) in credit-note edit and purchase-order create pages by restoring missing closing wrapper divs in grouped line-item sections.
- Extended invoice reports with payment-related columns from linked records (`Payments Count`, `Last Payment Date`, `Total Paid`, and `Balance Due`) so report builders can include payment context without leaving report views.
- Added an invoice report column for full payment breakdowns that lists each linked payment method and amount in one cell (e.g. `Cash: R100.00, Card: R50.00`) for clearer per-invoice payment visibility.
- Optimized report performance to reduce timeout risk by removing unnecessary eager-loaded relations and replacing grouped report per-group queries with a single record fetch plus in-memory bucketing.
- Standardized report date-range filtering to use each document's primary date field (including `invoice_date` for invoices) instead of relying on record creation timestamps.
- Fixed report sorting/grouping SQL errors for virtual columns (e.g. `formatted_date`) by mapping them to real database fields before query `ORDER BY`/`GROUP BY`.
- Added invoice UI rounding support to nearest `0.10` by automatically maintaining a `Rounding Adjustment` line item, and introduced a chart-of-accounts `Default Rounding` account flag so rounding lines are posted to the configured account with tax set to `None`.
- Updated invoice presentation to keep `Rounding Adjustment` lines operational but hidden from invoice create/edit/show screens and generated invoice PDFs, while preserving backend totals and Xero sync line exports.
- Added `Rounding Adjustment` breakdown rows to totals sections across document views and core PDFs (invoice, quote, proforma invoice, jobcard, purchase order, and credit note), while keeping rounding persisted as line items for backend calculations/integrations.
- Fixed invoice edit-page startup crash (`Cannot access 'ensureRoundingAdjustmentLine' before initialization`) by hoisting rounding helper functions used by immediate watchers.
- Restored automatic rounding behavior in quote and invoice editors: rounding adjustment lines are now actively maintained again (including fallback account assignment when a default rounding account is not configured) so totals round correctly to the nearest `0.10`.
- Updated all core PDF document tables to suppress `Rounding Adjustment` line items from printed line rows, while still showing rounding in totals sections.
- Enforced Xero export account mapping for `Rounding Adjustment` lines on invoices and quotes so outbound `AccountCode` uses the company’s configured default rounding account when set.
- Fixed Xero rounding-account export edge cases by resolving default rounding/sales account codes using the document’s company context (invoice/quote `company_id`) and matching rounding lines by both description and default-rounding-account assignment.
- Fixed Xero invoice/quote line tax mapping so lines with no `tax_rate_id` now export with `TaxType: NONE` (including rounding lines), instead of inheriting the default sales tax code.
- Improved Xero rounding-account resolution by falling back to any company account flagged `is_default_rounding` (even if inactive) and skipping `LineItemID` reuse for rounding lines on paid/allocated invoice updates so account-code corrections can apply.
- Restored automatic nearest-`0.10` rounding behavior on jobcard Create/Edit by auto-maintaining hidden `Rounding Adjustment` lines (assigned to default rounding account with sales-account fallback), and included rounding in jobcard totals calculations.
- Fixed invoice PDF print/download/email error (`Unknown column 'rounding_adjustment_total' in 'SET'`) by passing rounding totals via a non-persisted relation instead of mutating invoice attributes.
- Updated quote→invoice and jobcard→invoice conversions to enforce nearest-`0.10` rounding on the created invoice by auto-creating/updating a `Rounding Adjustment` line when needed.
- Aligned invoice save/update totals with rounding behavior by re-applying server-side `Rounding Adjustment` normalization before `calculateTotals()`, preventing post-save 1-cent drift from edit-view totals.
- Aligned invoice tax rounding with Xero by switching invoice line tax calculation from always-round-up (`ceil`) to standard 2-decimal rounding, including quote/jobcard-to-invoice conversion tax recalculation, reducing 1-cent VAT mismatches during sync.
- Fixed invoice UI tax totals (Create/Edit and POS) to use standard 2-decimal rounding instead of round-up-per-line, so on-screen VAT matches saved invoice values and Xero sync calculations.
- Standardized tax rounding across all remaining document types (quotes, jobcards, credit notes, and purchase orders) in both backend and UI calculations by replacing round-up (`ceil`) behavior with normal 2-decimal rounding to match Xero.
- Fixed invoice Edit view `NaN` totals on records without discounts by normalizing line-item numeric fields (`quantity`, `unit_price`, `discount_amount`, `discount_percentage`, and related IDs) before calculations, preventing string concatenation in discount/subtotal reducers.
- Fixed invoice Edit startup crash (`Cannot access 'getGroupValueByIndex' before initialization`) by converting `getGroupValueByIndex` to a hoisted function declaration so immediate watchers can safely call `normalizeLineItemOrder` during setup.
- Fixed quote→invoice and jobcard→invoice conversions triggered from document Show pages to assign the currently logged-in user as `salesperson_id` on the created invoice (model-level `convertToInvoice()` path), matching invoice controller conversion behavior.
- Updated Xero document sync payloads to include line-item `ItemCode` when a linked product has an SKU, covering invoices, quotes, credit notes, and purchase orders so item references are preserved in Xero.
- Updated invoice PDF line-item table to include a dedicated `Item Code` column (SKU/barcode fallback) instead of appending item codes to descriptions, improving readability and aligning output with Xero item-code usage.


## v1.8.2
- Expanded list-view column editing on core document index pages (invoices, quotes, jobcards, purchase orders, and credit notes) by exposing additional model-backed fields as hidden-by-default columns so users can add them to their table views without showing internal ID/Xero identifiers.
- Updated document Show pages to better match Edit behavior by rendering grouped line items (line-group headers + grouped rows) across invoices, quotes, jobcards, purchase orders, and credit notes; invoice/purchase-order show payloads now load line-group relations required for grouped display.
- Added `Description` as a selectable hidden-by-default column in the document list column editor for invoices, quotes, and jobcards so users can include document descriptions in index table views when needed.
- Fixed list column editor labels showing all-uppercase by switching header label extraction from rendered `innerText` to source `textContent`, preserving intended casing while still cleaning sort-indicator symbols.
- Fixed list column order/visibility drift after applying filters on the same index page by reloading and reapplying saved column preferences whenever the Inertia page URL/query changes (not only when component name changes).
- Added per-column table filtering on index list views by rendering a filter-input row directly beneath column headers (works with reordered/hidden/custom-added columns from `Edit Columns`) so users can filter each visible column independently.
- Added jobcard→quote conversion support: new backend endpoint/controller action plus Jobcard Show-page action button now create a draft quote from the jobcard (including copied line groups/line items, pricing, tax, notes, terms, and contact fields).
- Changed all document conversion actions (quote↔jobcard/invoice and jobcard→quote/invoice paths) to open the target Create screen with source data pre-populated instead of immediately creating records; saving now performs creation explicitly and invoice saves still apply conversion links/status updates to the source document.
- Added Administration Email Templates with a drag-and-drop editor (image uploads + source HTML editing), exposed customer/contact/company/user/date variable tokens in the template UI, and added template-driven `Email` actions on customer/contact list and detail screens.
- Removed user-level and company-level SMTP configuration from app workflows: outgoing mail now always uses `.env` mail settings, while all customer/contact/document/reminder emails explicitly set sender name to the active company and reply-to to the active company email.
- Added automated reminder SMS activity logging into `sms_activities` (not just reminder logs), including success/failure details and phone/message payloads, so reminder SMS now appears in customer/contact SMS activity tracking.
- Fixed a blank-render issue in the administration email template editor by hardening async `vue-email-editor` module resolution and adding a visible fallback state when the designer fails to initialize.
- Fixed email template editor viewport sizing by switching the designer wrapper to a fixed height and forcing the embedded editor to fill the container, preventing the collapsed "small bar" layout.
- Fixed outbound email delivery behavior by explicitly sending through the SMTP mailer in customer/contact/document/reminder flows (instead of the default mailer), preventing false "sent" success when `MAIL_MAILER=log`.
- Upgraded customer/contact email compose modals to include the visual Unlayer editor in-place (with manual HTML still available), while preserving live preview and stripping embedded design markers before send.
- Fixed Unlayer editor sync so changing template/body content after mount now reloads into the designer view, ensuring preview content is reflected inside the editor instead of remaining stale.
- Simplified the customer/contact email composer UI by removing the separate preview panel, giving the body editor full modal width for a cleaner compose experience.
- Fixed list-view column filters so they now apply across the full dataset (server-side via URL filter params) instead of only filtering the currently loaded page rows.
- Updated list-view column filter inputs to apply only on Enter key press, preventing disruptive table reload/filtering while users are still typing.
- Fixed post-filter table rendering so rows no longer remain hidden until manual refresh; clearing a column filter and pressing Enter now consistently reloads and restores results immediately.
- Hardened column-filter clearing behavior: clearing now triggers an immediate Inertia reload (including native search clear-button events) and forces fresh server-rendered rows to avoid stale filtered state.
- Fixed grouped line-item row identity drift in quote editors (and aligned the same safeguard in invoice, jobcard, credit-note, and purchase-order editors) by using stable per-row client IDs for Vue keys and excluding those IDs from payloads, preventing edits from applying to the wrong row after reordering/normalization.

## 2026-03-16 - version 1.7.6

- Document line items: show item code (SKU/barcode) in brackets with product name/description in PDFs, Show pages, and PDF template editor presets.
- Jobcards list: hide completed and cancelled jobcards by default; add "Show closed" checkbox in filters to include them.
- Mark quote as accepted when converted to jobcard or invoice.
- Quotes list: hide accepted/rejected quotes by default; add "Show closed" checkbox in filters to include them.
- Added Invoice column to jobcards list view (after Customer) showing linked invoice number with link when jobcard was converted to an invoice.
- Added approval row at bottom of quote and proforma invoice PDFs with Received by, Date, and Signature fields (dashed lines for handwritten completion).
- Fixed quote and jobcard Edit pages so line item tax rate and account selections are retained when editing: pass document as array to ensure tax_rate_id and account_id are included, and coerce values to numbers for correct select binding.
- Retained tax_rate_id and account_id on line items when converting quote→jobcard, quote→invoice, and jobcard→invoice; also copy discount_amount and discount_percentage for consistency.
- Added Job Card column to invoices list (after Customer) showing linked jobcard number when invoice was converted from a jobcard.
- Added Date column and sortable columns (Invoice, Customer, Date, Unit Price) to Recent Invoice Usage panel on product Show page.
- Paginated "Recent Invoice Usage" panel on product Show page (10 per page, Previous/Next).
- Added "Recent Invoice Usage" panel on product Show page listing latest invoice line items (invoice number, customer, unit price) where the product was used.
- Moved document description to appear just below customer/contact information in PDFs (quote, proforma invoice, invoice, jobcard); was previously at the bottom in terms/notes section.
- Show linked contact on quote, invoice, and jobcard Show pages (Customer Information section) with link to contact detail.
- Fixed ContactSelector showing empty when editing a document with a contact selected: added `initialContact` prop and ensured Edit controllers load the contact relation.
- Replaced comma-separated email field in quote/invoice/jobcard email modals with a tag-style recipient list: pre-filled from document email, "Add email/contact" button to pick a contact or enter an email manually, and add/remove recipients like tags.
- Removed the purchase-order create line-item overflow wrapper that was clipping product suggestion dropdowns and raised dropdown stacking so product search results stay visible while typing.
- Fixed purchase-order create product lookup dropdown visibility by allowing suggestion menus to overflow above the horizontal line-item scroll container.
- Updated purchase-order create line-item UI rows to match the invoice-style line layout (compact grid columns, aligned field sizing, and consistent per-row totals/actions) instead of card-style blocks.
- Further aligned purchase-order PDF line rows with invoice-style structure by switching to item-code + description columns and matching numeric/tax cell formatting across blade, default templates, and template editor presets.
- Fixed remaining purchase-order line issues by improving PO product search feedback while typing (including SKU matches) and aligning purchase-order PDF/default template line-item columns and styling with invoice-style output.
- Updated outgoing document emails to set `Reply-To` to the active company email (when configured), covering manual sends and automated reminder/confirmation emails.
- Fixed purchase-order line-item behavior by correcting product search input binding so typed text and SKU suggestions display properly, and aligned purchase-order PDF line-item table styling with invoice table styling.
- Added company email output to default PDF headers (invoice, quote, jobcard, proforma invoice, and purchase order) so sender contact details print from the active company profile.
- Added an `Account` payment option to POS that creates the invoice without recording an immediate payment, while still requiring payment capture for cash/card/EFT sales.
- Added Xero contact VAT sync so `vat_number` now maps bidirectionally with Xero `TaxNumber` for both customers and suppliers during import and export.
- Tightened line-item table spacing in PDF outputs and adjusted header proportions by slightly reducing document title size while increasing logo size for better visual balance.
- Fixed invoice PDFs to resolve `Job No.` from the linked source jobcard, added editable document-level phone/email fields on quotes, jobcards, and invoices with customer defaults, routed reminder/SMS lookups through those saved document contacts, and allowed users with invoice view access to add payments without needing full invoice edit permission.
- Updated index/list screens so records open from a clickable row or card instead of separate View buttons, while preserving inline edit/delete and other action controls.
- Fixed quote line-item column alignment so the desktop header now matches the actual row layout in both create and edit screens.
- Fixed line-item product search so SKU lookups work in quote and jobcard forms, and switched product suggestion matching to a shared literal string matcher that safely handles special characters in SKUs.
- Fixed invoice, quote, and proforma PDF templates so document-level descriptions and notes print correctly in both built-in PDFs and existing saved default templates after migration.
- Fixed quote, invoice, and jobcard PDFs to print notes correctly, use product SKU/item codes on invoice and jobcard line items, and hide item codes on quote-style documents while keeping older custom PDF templates compatible.
- Added `order_number` fields to quotes, jobcards, and invoices, surfaced them in the create/edit/show UIs and PDFs, and preserved the value when converting quotes or jobcards into downstream documents.
- Default the jobcard create form to the company's default sales customer when one is configured, matching the existing quote creation flow.
- Fixed company switching for non-admin users by moving the switch endpoint out of the admin-only route group while keeping the controller's per-company access check in place.
- Reduced quote and proforma PDF header spacing and line-item table padding/margins so more document content fits on a single page before overflowing.
- Added a customer "Resync All from Xero" action in Xero settings that bypasses `If-Modified-Since` and refreshes all Xero customer records into JobCardOnline.
- Fixed invoice PDFs so the Description block renders when only `description` is populated (without requiring `notes` or `terms`).
- Allowed negative line-item unit amounts/costs across quotes, jobcards, invoices, credit notes, and purchase orders by removing positive-only validation/UI constraints and preserving negative totals in calculations.
- Added the customer VAT number to the customer detail view so VAT/tax registration information is visible on the show page.
- Fixed POS invoice creation to consistently persist the currently logged-in user as `salesperson_id`.
- Fixed invoice PDFs to render the customer's VAT number in the VAT box, and updated the invoice default template editor preset to include `{{invoice.customer.vat_number}}`.
- Fixed quote/jobcard-to-invoice conversion to persist the currently logged-in user as `salesperson_id` on the created invoice.
- Updated document list defaults to sort by `created_at` descending (newest first) for quotes, invoices, jobcards, purchase orders, and credit notes.
- Updated invoice line fallback account selection to prefer account code `1000` whenever no account is selected, with default sales account as a secondary fallback.
- Fixed invoice list sort persistence by sending resolved sort filters back from the controller and adding a sortable `Created` column in the invoices table.
- Fixed Xero sales-document exports (invoices/quotes/credit notes) to default missing line account codes to `1000`/default-sales-account instead of product-level fallback values like archived `200`, and aligned imported product sales-account fallback to `1000`.
- Added an optional `Order Number` field to POS invoice creation and persist it on the generated invoice.
- Added contact linking to quotes, invoices, and jobcards: `contact_id` stored on documents, searchable contact selector with quick-create on create/edit forms, contact info ("Attn:") in PDFs, recipient_email/recipient_phone preferring linked contact, and send-email supporting multiple comma-separated addresses.
- Fixed Xero payment sync: treat AmountDue ≤ 0.01 as fully paid (rounding), use AmountOwing fallback, invalidate invoice cache after each successful payment so batch syncs get fresh AmountDue, and always fetch fresh invoice per payment instead of reusing stale cached data.
- Capped Xero payment creation to the amount due on the invoice (min of JCO remaining balance and Xero AmountDue) to avoid "Payment amount exceeds the amount outstanding" validation errors.
- Fixed contact_id not persisting on document save by explicitly including it in the form payload via transform on quote, jobcard, and invoice create/edit submits.
- Added one-to-many purchase-order linking from quotes and jobcards: create PO directly from quote/jobcard show pages, carry source linkage onto the PO, show related source on the PO, and surface `Purchasing Total` plus `Total vs Purchasing` with linked related POs on quote/jobcard show pages.
- Fixed PO creation for custom/source line items by allowing `purchase_order_items.product_id` to be nullable, preventing SQL integrity errors when saving non-product lines.
- Fixed quote→invoice conversion lineage so invoices can still resolve the originating jobcard number when the quote came from a jobcard, and linked that originating jobcard to the created invoice while exposing both source quote and origin jobcard on invoice details.
- Fixed invoice list `Job No.` links to also show and open the originating jobcard for invoices created from quotes that themselves originated from jobcards.
- Fixed outbound Xero invoice sync to push invoice notes into Xero `Reference` (with invoice number fallback), so notes are included during invoice export.
- Updated outbound Xero invoice `Reference` formatting to include both invoice number and notes (`<invoice_number> | <notes>`) with safe truncation to Xero limits.

## 2026-03-13 - version 1.7.5

- Added a global toast notification system for Inertia flash messages and removed page-specific flash banners so success, error, warning, info, and status messages display consistently across the app.
- Fixed the dashboard `Your Sales Performance` revenue figures to subtract allocated, non-voided credit notes from invoice totals so sales trends reflect net invoice value.
- Fixed credit notes so opening an invoice-linked note no longer downgrades a `paid` status back to `authorised` during balance refresh.
- Fixed Xero chart-of-accounts imports to relink existing local accounts by code, map more Xero account types correctly, and scope account-code uniqueness per company to prevent duplicate-key sync failures.
- Scoped document-number uniqueness per company for quotes, invoices, jobcards, purchase orders, and credit notes, and updated related Xero matching plus product SKU/barcode validation to avoid cross-company duplicate conflicts.

## 2026-03-14

- Increased the default PDF template logo cap from `200x100` to `220x110` so company branding appears slightly larger in built-in templates and template editor presets.
