Changelog

All notable changes to Visilion will be documented in this file.

[Unreleased] 2026-08-13

Added

  • Inventory Add/Search moved into popups: The tall "Add New Frame/Lens/Lentiles" form and the "Search & Filters" panel no longer sit above the inventory list forcing you to scroll past them. The Add (labelled for the active tab) and Search buttons now sit in the list card's header next to Scan (the "Items Found" counter chip was removed), and each opens its panel as a centered modal overlay. The list is now the first thing you see.
  • Vendor bundle split: Vite now separates the heavy libraries into their own cacheable chunks (vendor-react, vendor-icons, vendor-charts [recharts], vendor-pdf [jspdf], vendor-moment). The main index chunk dropped from 2,886 kB to 2,672 kB and the vendor libs no longer ship inside it, so app updates don't re-download React/recharts/jspdf/moment.
  • Component + helper test suites for the extracted modules: Vitest now covers the extracted code — pure-helper tests for patientDashboard/helpers.js (fmt, parseNum, parseNumNullable, classifyCondition) and lensOptions/helpers.js (rowKey, lineKey, catOf, catLabel), plus renderToString smoke tests proving the extracted tasks/Card, inventory/SummaryStats and patientDashboard/StatBadge components render without crashing. vitest.config.ts now loads the React plugin so JSX components are testable in the node environment (no jsdom/testing-library needed). Total: 31 Vitest tests + 26 Electron DB tests.
  • License activation shows a "checking…" state: Activating a Pro/Ultra key in the onboarding wizard was silent — the button gave no feedback while the key was validated remotely, so it looked like nothing happened. The activate button now shows an inline spinner + "Checking license…" banner (new onboarding.checkingLicense key in en/fr/ar, others fall back to English), disables itself while validating, and only then shows the valid/invalid result.
  • Onboarding "Who do you work with?" step: The first-run setup wizard gained a new Lens brands step (right after tax, before license). A grid of the 17 starter brands (ABLENS, Essilor, Zeiss, Hoya, Rodenstock, Nikon, Shamir, BBGR, Indo, Seiko, Younger Optics, Optifak, Conant Optical, Chemiglas, IOT, Kodak Lens, TOKAI Optical) lets you tick exactly the brands you sell, with select all / clear shortcuts and a live count. Continue creates each picked brand as an active lab, loads its full starter product catalog, and registers it in the app's lens brands — so the brands appear on the invoice immediately. Skip leaves everything untouched for later (Settings → Lens Options).
  • Unified Lens Options (Settings): The separate Brand Catalogs card is gone — everything now lives in one Lens Options card. The brand list is the hub: each brand row has an activate/deactivate toggle (only active brands appear in the invoice brand picker), a default star, contacts pencil, and an inline Products editor where each row is a purchasable variant (coating × type × index × tint × optional fixed price) — every field accepts an existing value or a brand-new one. The shared value pools (treatments, indices, types, tints, diameters) and the treatment↔brand link stay as-is.
  • No template lens prices: Starter templates ship no prices (prices differ per country, currency and shop). Each variant has an optional fixed price — leave it empty to charge your own price at invoicing, or set one to auto-fill the invoice lens price.
  • Cascading lens selection (Brand first): The invoice lens section now starts from the brand: Brand → Treatment → Lens Type → Index → Tint → Diameter, each pick narrowing the next list to what that brand actually sells (pick ABLENS → Organic Chroma → index 1.56 → tint Gray/Brown). A list reduced to one value auto-selects; the lens price pre-fills from the variant's optional fixed price when one is set and no custom price was typed. Brands without a catalog fall back to the global lists, so nothing breaks.
  • Tint on the invoice: A new Tint dropdown per vision section is saved with the invoice, carried into Send-to-Lab job cards and the Tasks board lens line, and shown in Prescription History and Patient Dashboard. New lensTints default list (Gray, Brown, Green, Blue, Purple, Rose, Yellow, Transparent).
  • Tint in Inventory (lenses): Lens stock items now carry a Tint field — in the add-lens form, editable in the detail sidebar, shown on the stock table (index · tint) and in the auto-generated lens model name — with a Tint filter in the lens filter bar. The database column migrates automatically, so existing stock is preserved.
  • Lens Options link visibility: In Settings → Lens Options, each lens brand now shows the treatments/coatings linked to it and each treatment shows the brands that offer it, rendered as clear purple chips with a count. Opening a brand's pencil (contacts) editor also lists all of its linked treatments inside the modal — so you always know exactly what is linked to what.
  • Lens Options help tooltip: The long instructions paragraph is replaced by a compact info icon — hovering it shows the guide for starring defaults, brand contacts, and linking treatments to brands.
  • Lens Options full-screen popup: The Lens Options card is now a launcher (live stats: active brands, product variants, product lines) that opens a dedicated 3-tab popupBrands (search, add, activate/default/contacts, clone a catalog into another brand, confirmed delete), Products (per-brand editor grouped by product line with conflict detection, bulk edit/delete, duplicate variants, optional fixed-price affordance, starter-template loader) and Values (the shared treatment/index/type/tint/DIA pools with the treatments↔brands link). Unsaved edits are protected by a confirm-on-close dialog and per-brand drafts.
  • Invoice guards for catalogs: Saved lens picks are never wiped just because the current catalog no longer lists them — only re-validated after you actively change the brand. A fixed catalog price shows a "From catalog" badge on the price field, and choosing a brand with no product catalog shows a hint that generic options are being used.
  • Shared lens-catalog store: Lens catalog data now lives in a single shared store used by the invoice form, Settings and the Lens Options popup — one fetch instead of several, and popup edits appear in the invoice instantly.
  • Inventory catalog cascade & price flag: The lens add form and detail-sidebar editor now drive lens type / index / treatment / tint from the selected brand's catalog when one exists, and flag a price mismatch when the selling price differs from the catalog's fixed price (with a one-click "From catalog" fill).
  • Lens categories (Stocks / Progressive / Solar / Fabrication): In Settings → Lens Options → Products, every brand now shows four category tabsStocks, Progressive, Solar and Fabrication — each with a live variant count. Every variant row carries a category select so any lens can be moved between tabs, the bulk bar gained a Move to category action, and new variants default to the active tab. Starter templates load pre-categorized by product line and are fully editable. Existing catalogs migrate automatically — saved variants default to Stocks so nothing disappears.
  • Lens name (product line): Products now have an optional Lens name alongside the coating — a real lens is brand + lens name + coating (e.g. ABLENS Camber Anti-reflect), while lenses without a name (coating-only, e.g. Organic Chroma) fall back to their coating. In Settings → Lens Options → Products every variant row gained a Lens column with a suggestion list, and rows now group by lens name — so all Camber coatings (Anti-reflect, Anti-Lumière Bleue, …) stay together in one Camber family instead of splitting into separate sets. In the invoice the treatment dropdown shows the lens name and coating together (e.g. "Camber Anti-reflect") and the selected value is what the invoice stores and prints, so PDFs and lab job cards carry the full product name; the cascade and fixed-price lookup match on the combined value. lab_catalog gains a line column (schema v7 migration — existing rows keep working as coating-only), and the ABLENS starter template now ships the Camber progressive line as a two-coating family.
  • Products editor bulk select + drag to reorder: In Settings → Lens Options → Products the variant list gained a select-all checkbox in the header (ticks every variant in the current category tab), selected rows are highlighted in purple, and every row has a drag handle — drag a row to reorder it within its product line, or drag a line header to reorder the whole line (Anti-reflect before Organic Chroma, etc.). The new order is saved with the catalog and restored on reload (lab_catalog now loads in saved order instead of an alphabetical sort). The "Add stock" button is gone: it created inventory items with no prescription, so it was useless — when you need stock you go to the Inventory tab and add it there.
  • Datalist dropdowns no longer turn white (or black text): Picking a value from a suggestion dropdown (datalist) in the Products editor painted the input's background an opaque white and the text black — Chrome treats datalist selection like autofill and overrides translucent glass inputs with its own !important UA styles. A global -webkit-autofill override now forces the theme's text colour (white in dark mode, dark in light mode) and delays the background transition, so every input keeps its glass tint and readable text when a suggestion is selected or a field is autofilled.
  • Prescription on lab order cards: Each order card on the Tasks board now shows the patient's prescription just like the patient cards — OD / OS lines with sphere, cylinder and axis, the near (reading) values when present, and the PD — so the Rx is visible at a glance before dispatching, without opening the job card.
  • WhatsApp "choose a contact" dispatch: The Send-to-brand dialog now offers an "Or — choose a contact in WhatsApp" option alongside the saved number. It copies the order text, opens WhatsApp's own contact picker with the message pre-filled, and marks the orders as sent — ideal for companies with several contacts or for contacts that aren't saved in the app yet.
  • "Auto-create lab order draft" setting: A new toggle in Settings → Lens Options (default off). When enabled, saving a new invoice that contains lens work silently creates a draft order on the Tasks board instead of showing the "Send this order to a lab?" prompt — the order goes to the brand-matching lab (or Shared (default) when no lab matches), the invoice is remembered as linked so it never asks again. New lab.autoCreateOrder / lab.autoCreateOrderDesc keys are translated in all 33 languages (1,505 keys per locale).
  • "Clear done" on the Tasks board: A red Clear done button appears in the board summary bar whenever there are picked-up orders. It asks for confirmation (showing the exact count) and then permanently removes every picked-up order from the board — ready orders are kept for their pickup alerts. New tasks.clearDone / tasks.clearDoneTitle / tasks.clearDoneMessage / tasks.doneCleared keys are translated in all 33 languages.
  • Tasks board workflow completed: The missing picked-up stage is now reachable — every column has a working advance arrow (Draft → Sent → In Lab → Ready → Picked up) and the job-card panel has a status-advance button, so Clear done finally has something to clear. Bulk actions gained Mark ready and Mark picked up, and the old bulk PDF bug is fixed — it no longer stamps every card with the first selected order's lab; the PDF is now grouped per lab, each section headed by its lab and order count.
  • Edit, duplicate & quote on the Tasks board: Orders can be edited (patient, phone, due date, priority, lab/brand, delivery note, recorded lab price) via a new modal, duplicated into a fresh draft, and any recorded lab price shows as a chip on the card and in the edit modal.
  • Tasks board signals: Cards show a days-in-status aging chip, the patient phone, the lens tint and the frame line; the summary chips (columns + overdue) are now clickable filters; a sort menu (priority / due date / recently added / patient) sits in the filter bar.
  • Archive on the Tasks board: Any order (single or bulk) can be archived instead of deleted — archived orders hide from the board and live in an Archived view with per-order restore / delete and a confirmed Remove all. The archive flag is stored inside the order's data_json, so it survives on both browser and Electron storage with no schema change. Every job-card panel gained a Print button (single-card PDF).
  • Tasks board new translations: The new board labels (edit, duplicate, quote, mark ready/picked up, archive, restore, print, sort, filters, aging) are available in all 33 languages — new keys were added to the English locale and fall back automatically elsewhere (1,543 keys per locale).
  • Unit test harness for pure logic: Vitest is now a dev dependency with a test:unit script (and runs inside npm test alongside the existing Electron DB tests). The first suite — src/utils/invoiceCalc.test.js — pins the exact behaviour of the invoice totals engine (HT/TTC modes, tax rounding, frame-discount clamping, zero-VAT, empty invoices) and the prepareInvoiceData / prepareReceiptData PDF payload builders, so pricing and PDF data can never silently drift during refactors.
  • Invoice math extracted to src/utils/invoiceCalc.js: The totals computation, the invoice/receipt payload builders and the date parsers moved out of the 2,600-line InvoiceForm.jsx into a pure, unit-tested module. The form now builds one flat formData snapshot and hands it to the payload builders — identical output, with the risky pricing/PDF logic testable in isolation.
  • Vision section refactored (dropdowns + Rx grids): VisionSection.jsx (1,458 lines) shed ~745 lines. The 8 duplicated dropdown-positioning useLayoutEffect blocks became one reusable useDropdownPosition hook; the 6 inline dropdown pickers (treatment, index, lens type, tint, brand, diameter) became a data-driven PickerField component; and the two OD/OS Rx input panels became an EyePanel component fed by declarative field configs — distance/near mode, OD/OS ref indices, mirror settings and per-field character filters all preserved. No visual or behavioral change: same dropdowns, same keyboard handling, same tab order and Enter-chaining across the Rx grid.
  • WhatsApp receipt fixed (date crash): Sharing a receipt via WhatsApp hit a ReferenceError — the message template's {receiptDate} placeholder referenced a variable that never existed in the handler's scope, so the message could not be composed. The invoice date is now resolved from the current invoice date for the template. In the same pass, the four receipt handlers (save to device, email, WhatsApp, other-app share) now share a single generateReceiptPdf() helper for their identical "load settings → build payload → render PDF" preamble, removing the duplicated boilerplate.
  • "Send to lab?" dialog no longer resets (remount fix): LabOrderKicker — the "Send this order to a lab?" prompt — was defined inside InvoiceForm, so React treated it as a new component type on every render and unmounted/remounted the whole dialog on each keystroke, resetting the chosen lab and due date mid-input. It's now a standalone component, so its internal form state persists.
  • Localized frame-scan feedback colors: The frame barcode-scan toast picked its green/red styling by checking whether the message text contained the English words "No" or "Invalid" — a scan error in French/Arabic would have rendered green. The toast now tracks success/error explicitly instead of sniffing strings. The "DIA" lens-field label and the PD button tooltip also now use their existing translation keys instead of hardcoded English.
  • One dropdown-positioning hook everywhere: The duplicated useLayoutEffect positioning blocks — measure the button, place the overlay 8px below it, flip above when the viewport is tight — were copied across AppointmentForm, BackupSettings, CustomerSearch, Header, LabelSettings, InvoiceSettingsModal and OnboardingModal. All of them now use the shared useDropdownPosition hook introduced in the Vision-section refactor, deleting ~90 lines of near-identical positioning code. Behavior is unchanged: every dropdown still anchors below its button and flips upward when there is more room above. The Header search dropdown keeps its scroll/resize-aware repositioning through the hook's new live mode, and the same sweep covered all seven dropdowns in Settings and the SalesChart period picker; Sidebar's profile menu is untouched (a distinct upward-anchored, viewport-clamped design).
  • One dropdown style app-wide (Tasks included): The Tasks tab (lab-orders board) still used native browser <select> elements — the option list was rendered by the OS, so it looked nothing like the app's glass dropdowns. A shared Dropdown component now backs every dropdown in the app (the invoice PickerField is now an alias for it), and all 8 Tasks selects — the board's lab/sort filters and the lab/priority pickers in the Reorder, Edit and From-Invoice modals — were converted to it. Every dropdown now shares the same glass trigger, floating .dropdown-menu panel, keyboard navigation (↑/↓/Enter/Esc), blur-to-close and viewport-flip positioning.
  • One dropdown style app-wide (Inventory included): The Inventory tab still used native browser <select> elements — the option list was rendered by the OS, so it looked nothing like the app's glass dropdowns. All 22 Inventory selects now use the shared Dropdown component: the lens filter bar (DIA, lens type, index, treatment, tint), the general filters (brand, supplier, stock status, category), the add/edit form's optical pickers (diameter, lens type, index, treatment, tint, brand, replacement, category), and — through DetailItem's select mode, which now renders the shared dropdown too — the detail-sidebar edit pickers (category, index, treatment, tint, replacement). Every dropdown now shares the same glass trigger, floating .dropdown-menu panel, keyboard navigation (↑/↓/Enter/Esc), blur-to-close and viewport-flip positioning — the filter bar even gains the chevron affordance those selects were missing.
  • Invoice PDF/print/share extracted to src/hooks/useInvoicePdf.js: The ~700-line PDF subsystem — worker initialization, logo-to-base64 settings preparation, the Main-process invoiceToPDF / invoiceToHTML / printInvoice calls, the preview/print modal state machine, the share-options flow (save to device, email, WhatsApp, other-app share) for both invoices and receipts, and the zero-total confirmation guard — moved out of InvoiceForm.jsx (2,435 lines) into a single custom hook that returns all handlers and owns its own state/refs/effects. The form now consumes it with one destructured call. Dead code dropped along the way: handlePrintInvoice / handlePrintReceipt were never invoked by any UI. Identical behavior, ~700 fewer lines in the form.
  • One dropdown style app-wide (final sweep): The last native <select>s outside the invoice's own pickers now use the shared Dropdown component — 6 more across four files: the Products editor in Settings → Lens Options (the bulk-bar Move to category picker and each variant row's category select), the Onboarding first-run country step, the Team Members editor (role picker for a user and the role-template picker in the Add Role form), and the invoice "Send this order to a lab?" lab picker. The shared Dropdown gained a disabled prop (the role picker stays locked when editing the admin account, preserving the old guarded behaviour). The only intentionally-untouched select is the InvoiceBodyDemo preview's language toggle — it lives on a plain white demo bar where the glass dropdown (white text) would be unreadable, so it keeps its native control.
  • Invoice lab-order bridge extracted to src/hooks/useInvoiceLabOrders.js: The "Send this order to a lab?" flow — lab-list loading, the prompt/remember localStorage guard, buildLabOrderFromInvoice (order number, lens, Rx incl. near vision, frame), lab-target resolution (matchLabByBrand / shared fallback / auto-created Shared lab), manual draft creation and the autoCreateLabOrder setting — moved out of InvoiceForm.jsx into a custom hook that owns the lab state, loads the labs and returns the handlers the form and the LabOrderKicker modal need. The hook derives canUseLabs from auth itself, so the form no longer imports the lab data-manager functions at all. Identical behavior, ~145 fewer lines in the form.
  • Invoice vision state extracted to src/hooks/useVisionState.js: The entire vision domain of InvoiceForm.jsx — the 8 treatment/index/diameter/tint pickers, the 16 OD/OS prescription fields, the 4 lens prices, the 4 frame price/discount fields, the 4 lens type/brand pickers, the 4 frame type/selected-frame states, the PD fields, and the brand-catalog option derivation (brandOptions, brandHasCatalog, longVisionOptions / nearVisionOptions cascades via useLensCatalogs) — moved into a custom hook that owns every vision state/setter and returns the exact state / handlers aggregates VisionSection reads (customer fields are composed back in by the form). The form now builds one flat formData snapshot by spreading the same state aggregate, so the PDF payload stays byte-for-byte identical. Identical behavior, ~57 fewer lines in the form.
  • Invoice floating toolbar extracted to src/components/InvoiceToolbar.jsx: The bottom floating action bar — financial summary (tax/total/remaining with the advance-payment button), New Invoice, the AI prescription scanner button (incl. the free-plan 20-scans/month gate), Save, print/receipt/share group, Suggested Prices trigger, the Prescription History modal, PD measurement and Delete — moved out of InvoiceForm.jsx into a pure presentational component that owns no state. The form passes the shared state aggregate plus explicit callbacks, so the toolbar's PrescriptionHistoryModal props collapse to state.* reads. Dead imports dropped along the way (12 unused lucide icons, getMonthlyFeatureUsage no longer imported by the form). Identical behavior, ~188 fewer lines in the form.
  • Invoice header card extracted to src/components/InvoiceHeader.jsx: The top "Main Info" card — patient name (with the autocomplete overlay: arrow/Enter/Esc keyboard handling, highlighted match rendering, the clear button), invoice number, date picker, phone, address and email — moved out of InvoiceForm.jsx into a pure view. The form still owns the useCustomerSearch hook and passes the whole result down as one search prop plus the customer values and setters. Identical behavior, ~133 fewer lines in the form.
  • Invoice overlays extracted to src/components/InvoiceModals.jsx: The bottom-of-form overlay block — Share Options, the two Upgrade prompts, the delete and zero-total confirmations, the PDF preview, the AI prescription scanner, the PD and advance-payment dialogs, and the "Send this order to a lab?" kicker — moved out of InvoiceForm.jsx into a single pure-view component fed by explicit callbacks plus the shared state / handlers aggregates (the PD dialog now reads state.odPd / handlers.setOdPd etc. instead of raw tuples). Identical behavior, ~69 fewer lines in the form.
  • Vision sections rendered through src/components/VisionBlocks.jsx: The two stacked VisionSection calls (distance + near) — ~70 lines of per-section props each — now go through a small wrapper that reads every value out of the shared state / handlers aggregates and the derived options, leaving one compact component call in the form. The wrapper preserves the exact long/near prop contract (the near section deliberately gets no vision/isDark, exactly as before). Identical behavior, ~66 fewer lines in the form.
  • Tasks board component-extracted to src/components/tasks/: The 1,989-line Tasks.jsx had its Kanban Card, the JobCardBody detail panel, and the four large modals (SendModal, ReorderModal, EditOrderModal, AddFromInvoiceModal) moved into files under src/components/tasks/. A shared common.jsx now holds ModalShell, Panel, RxBlock, helper functions (parseData, lensSummary, copyToClipboard, sentInfo, daysInStatus, etc.) and style helpers (chipClass, inputCls, boxCls, chip) — all consumed by the extracted components. Constants and workflow functions (GROUPS, STATUS_ORDER, NEXT_STATUS, completionOf, advanceTitleFor, stageAt) moved to src/utils/labJobCard.js for reuse. The main Tasks component now imports from these modules instead of holding inline definitions. Identical behavior, ~984 fewer lines in Tasks.jsx.
  • Tasks page view-blocks extracted too: The remaining 1,005-line Tasks.jsx had its four big presentational JSX blocks moved out — TasksHeader (title bar, error/flash toasts, search + lab + sort filters, and the selected-orders bulk toolbar), BoardSummary (per-column counts, overdue filter, copy-pickup-alerts / clear-done), ArchivedView (the archived-orders list with view/restore/delete), and TaskBoard (the Kanban columns with drag-and-drop). The main component keeps all state and handlers and feeds them down as props; derived selectedOrders / pickedUpCount are memoized for the toolbar. Identical behavior, ~164 fewer lines in Tasks.jsx.
  • Inventory view-blocks extracted to src/components/inventory/: The 1,291-line Inventory.jsx (its state/logic already lives in useInventory) had its three biggest JSX blocks moved into files under src/components/inventory/SummaryStats (the five KPI cards), InventoryTable (the title/scan/print toolbar, sticky column header and every item row with hover actions), and ItemDetailsSidebar (the ~320-line slide-in detail panel with quick actions, stock/pricing summary, read/edit specs via DetailItem and movement history). Dead imports dropped (9 unused lucide icons, moment, DetailItem). Identical behavior, ~564 fewer lines in Inventory.jsx.
  • Inventory search filters + add forms extracted too: The remaining add/search card was split further — SearchFiltersPanel (the optical-spec and general filters with their dropdowns, search box and clear button) and AddItemForm (the three per-tab add forms: lens with catalog-cascade options and price-mismatch affordance, frame, and lentile). Dead imports dropped again (7 unused lucide icons, the dataManager functions, Dropdown). Identical behavior, ~378 fewer lines in Inventory.jsx.
  • Settings cards extracted to src/components/settings/: The 1,157-line Settings.jsx had its three inline SettingCard blocks moved into src/components/settings/AppearanceCard (theme switch, accent/live colors, PRO-gated film-grain / spotlight / hide-footer toggles, and the live-colors animation-speed + noise + gradient controls), CurrencyTaxCard (language, searchable country, custom currency with lock/reset, currency-in-words, tax rate and tax mode — now owning its currency state and handlers), and PrinterSettingsCard (default printer, paper size, quality). A reusable OverlaySelect control replaced the seven duplicated custom dropdown blocks (button + floating overlay + keyboard handling). Dead imports dropped along the way (getCurrencyWords, countries, several lucide icons). Identical behavior, ~762 fewer lines in Settings.jsx.
  • Team Members editor extracted to src/components/teamMembers/: The 1,157-line TeamMembers.jsx had its permission-grid editor (PermissionsEditor) and the shared permission/role helpers (GROUP_LABELS, ROLE_TEMPLATES, groupMeta, groupMetaFor, permLabel, permDesc) moved into src/components/teamMembers/ (a permissions.js constants/helpers module and a PermissionsEditor.jsx component). Dead imports dropped (7 group icons, the flat translations import). Identical behavior, ~149 fewer lines in TeamMembers.jsx.
  • Lens Options popup helpers + editors extracted to src/components/lensOptions/: The 1,148-line LensOptionsModal.jsx had its expandable value-pool editor (OptionGroup), the treatment↔brands linker modal (TreatmentBrandsModal) and the shared helpers (inputCls, smallInputCls, rowKey, lineKey, emptyVariant, LENS_CATEGORIES, catOf, catLabel) moved into src/components/lensOptions/. Dead imports dropped (ChevronDown, Info, Check). Identical behavior, ~196 fewer lines in LensOptionsModal.jsx.
  • Patient Dashboard sub-components extracted to src/components/patientDashboard/: The 1,139-line PatientDashboard.jsx had its six presentational pieces (StatBadge, TargetEyeIcon, PDCard, RxCardSimple, CustomTooltip, DetailRow) and the four pure helpers (fmt, parseNum, parseNumNullable, classifyCondition) moved into src/components/patientDashboard/. Dead import dropped (Target). Identical behavior, ~208 fewer lines in PatientDashboard.jsx.
  • Onboarding wizard state extracted to src/components/onboarding/useOnboarding.js: The 993-line OnboardingModal.jsx had every state variable, effect (open reset, click-outside close, currency-words sync, brands-step sync) and step handler (handleNameNext, handleCountryChange, the currency lock/reset handlers, handleBrandsNext brand seeding, license activation, etc.) moved into a dedicated useOnboarding hook. The modal component now calls the hook, keeps the step JSX and the shell, and is under the 700-line budget. Identical behavior, ~383 fewer lines in OnboardingModal.jsx.

Changed

  • Starter catalog rebuilt from the Lens Brands dataset: The lens starter template in Settings → Lens Options no longer ships a handful of example lines — it now covers 17 lens brands (ABLENS, Essilor, Zeiss, Hoya, Rodenstock, Nikon, Shamir, BBGR, Indo, Seiko, Younger Optics, Optifak, Conant Optical, Chemiglas, IOT, Kodak Lens, TOKAI Optical) across all four categories, with every variant from the Lens Brands Product Lineup & Specification Dataset: each product lineup × premium coating × lens type (Organique / Mineral / Trivex) × refractive index. Zeiss and Rodenstock stock lines now ship Mineral (glass) variants alongside Organic at their own indices, Shamir's 1.53 is a separate Trivex line, and solar lines carry the standard Gray / Brown / Green tints. Templates still load without prices, and Load starter template stays idempotent per brand.
  • Lens catalog schema (migration v5): lab_catalog becomes a variant table — new tint and price columns, primary key now (lab_id, treatment, index_val, lens_type, material, tint); existing treatment↔brand links are preserved. labs gains is_active (default on, so every existing brand stays visible after upgrade).
  • Lens Options popup contrast: The Brand contacts (pencil) and Link to brands popups now adapt to the active theme — in light mode they use a light panel with dark text and inputs instead of white-on-glass, matching the readable look they already had in dark mode.
  • Lens Options popup consistency: Both popups were rebuilt to match the app's standard modal shell (same as the payment/suggested-price modals) — rendered through a Portal with a full-window blurred overlay, centered glass-morphism panel, and the same fade/scale close animation.
  • Tasks popup contrast & consistency: The four Lab Orders popups — the Job Card detail panel, Send to brand (WhatsApp/email), Replace a lens, and Create order from an invoice — now adapt to the active theme (light mode uses light panels, dark text and readable inputs instead of white-on-glass), and were rebuilt to match the standard modal shell used across the app: rendered through a Portal with a full-window blurred overlay, centered glass-morphism panel, and the same fade/scale close animation.
  • Lab orders pull near vision too: Draft lab orders created From Invoice (Tasks tab) or from the save-invoice "Send this order to a lab?" prompt now capture the near (reading) prescription as well as the distant one, and the lens line uses the correct invoice field for the lens type. Job-card messages now print OD (near) / OS (near) lines whenever the invoice carries near-vision values, so orders are no longer empty when only near vision was entered.
  • Duplicate lab contacts merged: The labs table no longer accumulates duplicate rows for the same brand name. Saving a brand that already exists updates its contacts instead of inserting a new row (idempotent by name, case-insensitive), and a one-time migration merges any previously-created duplicates, reassigning their orders/catalogs to the kept row. The Tasks send dialog now shows exactly one contact per lens brand.
  • Shared (default) lab deletable: The built-in Shared (default) placeholder lab can now be deleted from Settings → Lens Options (it appears there as a removable entry with a Shared badge) as soon as at least one real lens brand exists. It is only undeletable while it is the last lab in the app — you must add a real brand first. A lab that has orders attached cannot be deleted until those orders are deleted or reassigned.
  • Bulk delete on the Tasks board: The multi-select toolbar now has a Delete action. Select several order cards (or a whole column) and delete them all at once — a confirmation dialog shows how many orders will be permanently removed before anything is deleted.
  • Lab board renamed to Tasks: The board that turns lens invoices into Lab Job Cards is now the Tasks tab (was Lab Orders). The underlying module (lab companies, orders, dispatch, permissions, Settings → Lab Companies & Orders) is unchanged.
  • Missing translations filled in all 30 languages: The newer UI strings — the Lens Options popup (catalog editor, products, categories, values, bulk actions), label settings, barcode scanning, permission labels/descriptions, the "Add a note" job-card prompt, onboarding brand picker, print/preview keys and the Tasks bulk delete + WhatsApp contact picker — were missing from the 30 non-English non-French locales, so they silently fell back to English. All 225 new keys (208 unique per language; French needed only 67) are now translated in every language — including the permission maps read directly by the Team Members editor, the 22 trademark treatment names (kept identical to English), and the flat "Add a note" key. Every locale now covers exactly the same keys as English.
  • Last batch of untranslated strings filled in all 33 languages: A deeper sweep found 121 more UI strings still missing from the locale files — in every non-English language they silently fell back to English. They covered the Export wizard (filters, first-50/invoice counters, preparing, index), the AI Prescription Scanner (scanning overlays, API-key hint, upload, extraction), the Invoice Settings preview window (template/header/footer, preview-requires-app, rendering, logo change/remove/format hint/background watermark, header/footer help), the Backup & Restore card (last backup, folder, backing-up, select-folder-first), the barcode scanner modal (scan, point-at, starting, no-camera, camera-error, manual fallback), the Prescription History timeline (OD/OG, previous/current/difference, prescription/frame/timeline titles, total comparison, lifetime, period), the updates/upgrade surfaces (cancelled, new version, auto-update, download/cancel, usage message, free/Pro plans, unlimited, upgrade, maybe-later), the Tasks form validation (invalid email/WhatsApp, OD-only/OS-only, replacement tag), the inventory lenses editor (purchase/selling price, min quantity, material, box size, pricing & stock, selling value, current stock, supplier, stock status), and assorted leftovers (confirmation, amount, discount, import error, feedback, never-expires, deleted). All 121 keys are now translated in all 33 languages with the same structural parity check as before (1,499 keys per locale) and zero mojibake markers.

Fixed

  • Duplicate delete-confirm dialogs in Inventory: Inventory.jsx rendered two identical <ConfirmModal isOpen={confirmOpen}> instances, so confirming a delete stacked two dialogs. The duplicate was removed.
  • Inventory sort crash on null values: The inventory table sort comparator crashed when items had null/undefined values in the sorted column, producing a TypeError. The comparator now handles null values gracefully — sorting them to the end — and returns 0 for equal values.
  • Inventory delete handler crashed silently: handleConfirmDeleteFrame had no try/catch around the delete call, so a failed delete left the confirmation dialog open with no feedback. Errors are now caught and shown as a temporary message.
  • Inventory price crashes on null/undefined: ItemDetailsSidebar, InventoryTable and SummaryStats called .toFixed(2) directly on potentially-null price/purchase_price fields, crashing the component. All price accesses now use (value ?? 0) guards.
  • Invoice VisionBlocks missing isDark for Near section: The near-vision VisionSection did not receive isDark, so it always rendered with light-mode colors regardless of the user's theme. Both sections now receive the prop.
  • Invoice VisionBlocks missing vision prop for Near section: The near section did not receive vision: 'nearVision', so VisionSection could not determine the correct field count or tab indices for near vision. The prop is now passed.
  • PD modal could not save changes: useVisionState.js handlers did not include setOdPd/setOgPd, so the PD modal received undefined for these setters and crashed when saving. Both setters are now in the handlers object.
  • Invoice tint not reset on New Invoice: refreshForm() reset treatment, index, lens type, brand and diameter but never cleared the tint fields, so the tint from a previous invoice persisted into the next form. Tint fields are now reset to empty.
  • Invoice advance payment stale after clearing prices: The auto-advance-payment effect returned early when totalTTC <= 0 without clearing advancePayment, leaving a non-zero advance payment when all prices were cleared. The effect now clears the field when total drops to zero.
  • Rx fields accepted letters in numeric inputs: Left-eye SPH, CYL and all near-vision SPH/CYL/AXE fields used FILTER_ALPHA (allows letters) instead of FILTER_NUM (numeric only), so users could type arbitrary text into sphere, cylinder and axis fields. All numeric Rx fields now use FILTER_NUM.
  • isInvoiceNumberExists used wrong Electron guard: The function checked window.electronAPI?.getMaxInvoiceNumber instead of window.electronAPI, so if the preload ever renamed that method, duplicate detection would silently use a different code path. The guard is now window.electronAPI.
  • labSetOrderStatus crashed without extra parameter: The browser-fallback path accessed extra.message_sent_via without a default value, so any direct caller omitting the parameter would crash. Default parameter extra = {} added.
  • Permissions editor missing lab group label: GROUP_LABELS had no lab entry, so the lab permissions group displayed as raw string "lab" instead of a translated label. Entry added.
  • Team Members invisible inputs in light mode: The input class helper used text-white for both dark and light modes, making input text invisible on light backgrounds. Light mode now uses text-gray-900.
  • PatientDashboard variable shadowing t translation function: State updater callbacks used t as the parameter name, shadowing the imported t() function. Renamed to prev.
  • CustomerSearch totalSpentValue crash: .toFixed(2) was called directly on potentially-null totalSpentValue. Now guarded with (Number(value) || 0).
  • PD modal showed "0 mm" instead of "—": When no PD values were entered, totalPd was 0 (truthy as a number), so the display showed "0 mm". Now returns null for zero, showing "—" correctly.
  • Label size dropdown showed "undefinedmm × undefinedmm": LabelSettings.jsx treated LABEL_SIZES as an object with Object.keys() and .width/.height, but it's an array with widthMm/heightMm properties. Converted to LABEL_SIZES.map() with correct property access.
  • Label Settings invisible inputs in light mode: Same white-on-white text issue as Team Members. Light mode now uses proper dark text colors.
  • Label Settings closeOnScroll hook anti-pattern: closeOnScroll was a non-hook function wrapping useEffect, violating Rules of Hooks. Extracted into two proper useEffect calls.
  • ConfirmModal Enter key double-fire: Pressing Enter while the confirm button was focused triggered both the keydown handler and the button's onClick, calling onConfirm twice. Removed the keydown listener entirely — the button click is the sole confirm action.
  • Tasks PDF print showed "PDF ready" on failure: The flash condition was !res || res.error which is true when printing fails — so "PDF ready" was shown on error and nothing on success. Fixed to res && !res.error.
  • Tasks drag-and-drop allowed backward status transitions: Users could drag an order from "Processing" back to "Draft" or any earlier stage. The drop handler now validates that the target stage is ahead of the current stage.
  • Invoice barcode scan crashed on items without price: item.price.toString() threw TypeError when price was null/undefined. Now uses String(item.price ?? '').
  • Invoice barcode scan gave no feedback for non-frame items: Scanning a lens barcode silently returned with no message. Now shows "Scanned item is not a frame".
  • Invoice totalTVA returned as string instead of number: calcInvoiceTotals returned totalTVA as a string (from .toFixed(2)) while all other totals were numbers. Now returns parseFloat(...) for consistency.
  • InvoiceSettings zero opacity impossible: settings.logoOpacity || 1 treated 0 as falsy and overrode it to 1. Changed to ?? nullish coalescing so explicit zero is preserved.
  • MiscSettings clear cache removed wrong key: localStorage.removeItem('appSettings') targeted a non-existent key. Changed to 'invoiceSettings'.
  • AppointmentForm timezone date shift: Editing an existing appointment parsed the date via new Date() which interpreted date-only strings as UTC, shifting the displayed time by the timezone offset. Now passes the raw string directly to the datetime-local input.
  • "getPrinters is not a function" when listing printers: electron-main-handlers.cjs called event.sender.getPrinters(), but event.sender is a WebContents — that method was removed in modern Electron. It now uses event.sender.getPrintersAsync() (falling back to the sync call on older runtimes), so the Settings printer dropdown loads again without spamming the console.
  • Dead isDark ? 'text-white' : 'text-white' ternaries removed: Always-white label ternaries across Appointments, CustomerSearch, InvoiceHeader, PriceInput, ProfilePage and Settings were collapsed to text-white.
  • Hardcoded strings localized: The Suggested-Prices blocked reason and the archived-order "Unnamed patient" label now go through t() (the latter reuses lab.unnamed).
  • Onboarding dead state removed: useOnboarding dropped the unused prevStep, animating, slideDir and isVisible state and the ignored 'right' slide-direction argument on goToStep.
  • Dropdown menus hidden behind modals: Shared dropdown overlays (.dropdown-menu) portal to document.body with a z-index: 1000, but every modal in the app stacks from z-[99999] up to z-[200000] — so a dropdown opened inside a modal (e.g. the "Create order from an invoice" lab/priority pickers, the Lens Options category/bulk-category pickers) rendered behind the modal. The overlay class now floats at z-[210000], above every modal, so any dropdown opened inside any dialog appears on top. Build verified the rule is emitted.
  • Send-to-lab message preview unreadable in light mode + no email look: The WhatsApp/email dispatch popup's live preview was always drawn as a WhatsApp chat — and in light mode the theme's text/glass overrides could turn its text dark against the dark chat background. The WhatsApp mockup is now locked to white text in both themes via a dedicated .wa-preview-card CSS rule (dark #0b141a card, #005c4b bubble, white/emerald text, muted timestamp), and the email channel now renders a proper email preview — a paper card with a recipient header and subject line and a plain light body — instead of a WhatsApp-style bubble. The email card is pinned light (white paper, dark text) in both themes via an .email-preview-card rule, so the theme's dark-mode gray→white text overrides can't wash it out.
  • Zero-total confirm dialog crashed the invoice (payload never exposed): After the PDF/print extraction, the "save a 0.00 invoice?" confirmation lived inside useInvoicePdf — but the hook returned the open flag and the payload setter while never exposing the payload value, so the dialog's title/message were unreachable (ReferenceError as soon as the overlay was raised; the extraction into InvoiceModals.jsx made it crash on mount). The hook now returns zeroTotalConfirmPayload, and the dialog renders the intended title/message.
  • Tasks tab crashed with "useCallback is not defined": The Tasks board used useCallback but its React import only listed useState, useEffect, useMemo and useRef — so opening the tab threw a runtime error. The missing useCallback import was added.
  • Chinese and other languages showed as garbled "alien text" (mojibake): Switching the app to Chinese, Hindi, Arabic-script, Cyrillic, Thai, Korean, Japanese or any of the other 30 non-English/French locales rendered most of the UI as unreadable symbols (仪表æ¿, দূর…, ΤÏπος…, משתמש…) that belong to no language. The pre-existing translation files for those 30 languages had been double-encoded: the correct UTF-8 bytes of each translation were first misread as Windows-1252 characters and then re-saved as UTF-8, so every accented or non-Latin string became garbage — English and French were unaffected because their text is mostly ASCII. The repair reversed the double-encoding losslessly across all 30 files (18,298 values), mapping every character back through the Windows-1252/Latin-1 tables (including the undefined 0x80-0x9F control bytes) into the original UTF-8 byte stream. After the pass, a scan for every known double-encoding marker finds zero mojibake values in any locale, sample checks confirm coherent text in every language (回访用户, Введите лицензионный ключ, सब तैयार!…), and all 33 files keep exact key parity with English (1,378 keys each; the 5 missing Arabic keys were translated and added).
  • Lab order target no longer lands on an arbitrary brand: When creating a lab order from an invoice (Tasks → From Invoice, or the save-invoice "Send this order to a lab?" prompt), the auto-picked lab only matched the invoice's lens brand by exact name — renamed labs or case/space differences fell through to the first lab alphabetically, so the order was assigned to an unrelated brand. The brand→lab match is now case-insensitive and whitespace-trimmed, and when no lab matches the brand the fallback is always the built-in Shared (default) lab — which is auto re-created on the fly if it was deleted (Settings → Lens Options), so an order never silently lands on an arbitrary brand. The Send and Replace a lens modals also prefer Shared (default) over the first lab when no lab is assigned.
  • Lens Options card no longer nested in a duplicate card: The launcher card in Settings → Lens Options rendered a second card inside the settings card — with its own border, padding, and a repeated "Lens Prescription Options" title and icon box — so the section looked boxed-in and cluttered. The launcher is now a flat content block inside the settings card (single title/icon from the card header): description + live stat chips + Open Lens Options button, stacked neatly on narrow widths and theme-aware in light/dark mode.
  • Whole app showed raw translation keys even in English: A batch of keys (the lens.* catalog strings, the "Add a note..." job-card placeholder) were stored in the locale files as flat root keys whose names contain dots ("lens.catalogEmpty" at the top level). The translator splits keys on . and only walks nested objects, so those entries were unreachable — it returned null in English itself, and the UI fell back to showing the raw key string (lens.catalogEmpty, …) in every language. The dead flat keys were normalized into the proper nested lens object in en and ar (the only two files that still had them), and the translator now tries an exact literal lookup first before the dotted-path walk, so a key whose own name contains dots or an ellipsis ("Add a note...") resolves instead of being swallowed by the dot-split. The five feedback.* receipt/share templates that only existed in en/fr/ar were also translated in the other 30 languages — so every key that resolves in English now resolves identically in all 33 languages.
  • Onboarding brands step turns brands off and on: Re-running the setup wizard's "Who do you work with?" step now reflects the real state — it pre-selects the starter brands that are currently active; Continue activates every selected brand (seeding its starter catalog the first time it's picked) and deactivates any previously-active starter brand you leave unchecked, so unchecking a brand truly removes it from the invoice brand picker. The active-state checks now use truthy values (SQLite/browser stores keep is_active as numeric 0/1) instead of comparing against a boolean — which previously kept is_active = 0 brands selected on re-run. The same truthy check was applied to the Lens Options launcher's active-brand count, the brand activate/deactivate toggle, and the shared lens-catalog hook, so a numeric-0 (inactive) brand is consistently treated as inactive everywhere (previously only Electron normalized it to a boolean, hiding the issue in browser mode). Skip still leaves everything untouched.
  • Lens Options card no longer stretches to its row partner: In Settings → Lens Options, the launcher card stretched to match the taller Price Suggestions PRO card in the same grid row, leaving a large empty glass area below the button. The Lens Options card now hugs its own content (self-start), so the section is compact again; the launcher content is a vertical stack (description → stat chips → button) that stays clean on narrow widths.
  • Treatment dropdown shows raw key names: The vision-section treatment button and dropdown re-ran the translator without the value fallback, so any treatment lacking a treatments.* key (catalog treatments like Organic Chroma, Crizal Rock, DuraVision Platinum, …) displayed the literal key (treatments.organic_chroma). Both now use the resolved label and fall back to the raw value. The missing treatments.* keys for all 22 shipped starter-catalog treatments were added to en/fr/ar.
  • Lens options editor loses focus while typing: Editing a variant's treatment or lens type in Settings → Lens Options → Products dropped the input's focus after one character, because those fields are part of a variant's group key and the row moved to a different group card. Rows are now rendered in a flat list keyed by their stable row index (group headers stay visually grouped), so a row simply slides to its new group while the caret keeps typing. The redundant "Add line" button (identical to "Add variant") was removed, and the column headers now render once above the list instead of inside every group.
  • Custom treatment names show raw translation keys: A newly added treatment like "Blue max" appeared in the invoice treatment dropdown as treatments.blue_max instead of the typed name. The translator returned the literal key (a truthy string) whenever a key was missing, which silently defeated every || raw value fallback across the app. The translator now returns null when a key exists in neither the chosen language nor English, so the existing fallbacks kick in and custom treatments/lens types display exactly as typed (the underscore is only the internal lookup key — the display keeps the space). Known-key translations (e.g. treatments.anti_reflective) are unchanged, and the Export calendar's weekday labels no longer rely on the key-return behavior.
  • Lens Type dropdown shows a placeholder when empty: On the invoice VisionSection the Lens Type picker was the only dropdown without a placeholder — when nothing was selected it displayed the first option in the list (e.g. "Organic") even though nothing was chosen, or stayed blank when the selected brand's catalog had no lens types for the current treatment, so it looked narrower and emptier than its neighbours. It now shows a "Select Lens Type" placeholder (same pattern as "Select Treatment" / "Select Index", en/fr/ar with English fallback) and only renders a value once one is actually picked — previously-saved values still display even when the current catalog no longer lists them.
  • Tint auto-selected by the catalog cascade: When a brand's catalog left exactly one tint available for the current treatment/lens-type/index combination, the invoice VisionSection silently auto-picked it — so changing e.g. the lens type could flip the tint to Gray or Brown the user never chose, and that tint then printed on the invoice (vl_od_tint/vl_og_tint/…). Tint is the one lens attribute where "None" is always a valid choice, so the cascade no longer auto-selects it: a tint only ever appears on an invoice if it was picked explicitly (the other single-option auto-selects for treatment/lens type/index are unchanged, and the fixed-price auto-fill still resolves tint-less variants).
  • Brand snapped back to the default after saving: Choosing None as the lens brand and saving an invoice made the brand jump back to the default brand (e.g. ABLENS). Saving updates the selected customer, which re-runs the invoice pre-fill — and the brand prefill used || defaultBrand, so an intentionally-empty saved brand (vl_od_brand = '') fell back to the default and re-populated the dropdown (a second save then persisted that default). The lens prefill now uses nullish semantics: a stored empty value stays empty (only a missing field falls back to the default). The same || default pattern was fixed for lens type, frame type, treatment and index, so a saved blank field is never silently replaced by a default.
  • Lens Options default-brand star was stuck on: In Settings → Lens Options → Brands the default star could be set but never removed — clicking the star on the currently-default brand did nothing, and there was no way to make None the default, so new invoices always opened with a brand pre-selected. The star now toggles: clicking the current default clears it back to None, and a None row sits at the top of the brand list (highlighted when it is the default, same pattern as the treatment/index/type/tint pools) so you can explicitly choose "no default brand". With None as the default, new invoices open with the brand dropdown on None.
  • Lens-in-stock indicator instead of the barcode toast: While filling the prescription fields, pressing Enter to jump to the next field sometimes fired the global barcode scanner (fast-typed Rx values looked like a scan) and popped a blue "No item with this barcode" notification every time. That notification is gone for unmatched codes — real scans still auto-select frames and the out-of-stock toast still appears. In its place, each OD / OS eye panel on the invoice now shows a green eye icon whenever a lens matching the currently selected variant (brand + treatment + lens type + index + tint) is in stock (quantity > 0) in Inventory; otherwise the icon stays as it was. It re-evaluates live as you change the lens dropdowns.
  • Inventory edit-save could silently do nothing: Editing an item in the inventory detail sidebar and pressing Save could appear to work (the success toast showed and the form exited edit mode) while the change was never written — because the Main-process permission guard returns { success: false } (not a thrown error) when the current session lacks inventory.create/inventory.update, and the renderer's saveInventoryItem only read response.id, treating a denied save as a success. The renderer now detects success === false and surfaces the real reason ("Permission denied — you do not have permission to edit inventory" or the actual error) instead of silently discarding the edit; the same guard was added to deleteInventoryItem.
  • Inventory table duplicate-key React warning: InventoryTable keyed each row by item.id, but two items could end up with the same id (from a browser-mode Date.now() collision when two adds happen in the same millisecond, or legacy data from a previous app version), producing a React "duplicate key" warning and potentially causing invisible or duplicated rows. The inventory array is now deduped on load (first occurrence wins), and the browser-mode id generator now uses Date.now() + random so two saves in the same millisecond can never collide. The table row key also falls back to barcode + index for any items that still have a null id.
  • Inventory add/save/delete feedback was invisible: showTemporaryMessage set the message state, but Inventory.jsx never rendered it — only scanMessage had a toast. This meant every Add validation error, Save success/error, and Delete confirmation was completely invisible to the user, making it look like buttons did nothing at all. The message state is now rendered as a toast (green for success, red for errors) in a Portal at the same position as the scan toast.
  • Inventory edit sidebar missing lens fields: The edit sidebar showed a plain text input for brand (should be a dropdown of lens brands for lens items) and was missing the Lens Type dropdown entirely for lens items. The brand field now renders as a Dropdown with the lens brands list when editing a lens, and Lens Type has been added as a dropdown between DIA and Index.
  • Batch export now uses the real invoice templates: Exporting invoices from the Customers search (batch PDF export) previously rendered through the legacy jsPDF pipeline, which had its own hand-written template code that did not match the Invoice Settings preview. Batch export now runs through the same Main-process rendering pipeline as printing and previews — the exported PDFs respect your chosen template (Minimal / Elegant / Modern / Classic / Professional), header, logo, watermark, footer, accent color and language, in both single merged file and one file per invoice modes, and the export progress bar stays live. The print template now accepts an array of invoices and renders one A4 sheet per invoice for merged exports.

[1.0.73] 2026-08-12

Added

  • Lab Orders (Connected Supplier): A brand-new Lab Orders tab turns lens invoices into Lab Job Cards tracked on a kanban board. Register your lens companies (Settings → Lab Companies & Orders) with WhatsApp/email contact; a seeded Shared (default) lab lets you use it immediately. Dispatching is one tap — a job card (Rx + lens + frame + patient) can be sent via WhatsApp, emailed, copied, or printed as a PDF, with multi-select bulk dispatch folding several orders into one message and one PDF.
  • Status tracking: Orders flow Draft → Sent → In Lab → Ready → Picked Up; every transition is timestamped in the card's history. Cards carry priority, due date, and tags; columns show % completion, overdue orders are flagged, and drag-and-drop between columns updates status. When an order becomes Ready, a copy patient alert action lets you message the patient for pickup.
  • Invoice bridge: Saving an invoice that contains lens work prompts "Send this order to a lab?" — with one click it builds a draft Lab Job Card from the invoice (Rx, lens brand/index/treatment, frame, patient phone) and lands you on the board, ready to dispatch. The prompt appears once per invoice and is dismissible.
  • New permissions: lab.read (view the Lab Orders board) and lab.manage (manage labs and orders) permission chips in user-role management; the Administrator and Staff roles include them by default.

Changed

  • Settings: New Lab Companies & Orders card to manage lab profiles.
  • Sidebar: Settings moved into the profile menu (click your account card at the bottom) to free vertical space — previously the last sidebar item could scroll out of view.
  • Translations: All lab.* strings (Lab Orders board, job cards, lab settings, invoice kicker) are now real translation keys across all 33 languages (French and Arabic fully translated, others inherit English).

[1.0.72] 2026-08-12

Added

  • Barcode & QR inventory scanning: Every inventory item can carry a Code128 barcode, stored in the SQLite database. Codes are auto-generated on save (configurable prefix + sequential counter, e.g. VS000042) or entered manually. Find items instantly by scanning with a USB barcode scanner (keyboard-emulation HID, zero drivers) or by reading a QR/1D code with the camera through a new modal powered by @zxing/library. A dedicated Barcode column makes labelled items easy to spot.
  • Label printing (single + batch): Print a physical price/barcode label for a single item (detail sidebar or row hover action) or batch-print labels for several selected rows. Labels render as Code128 SVG via jsbarcode and print through the existing hidden-window native print pipeline. Label size presets (40×20, 50×25, 60×40 mm), show-price toggle, copy count, and default printer are configurable.
  • POS stock deduction: When a stock frame is selected on an invoice and the invoice is saved for the first time, the item's quantity is decremented and a sell movement is recorded in its history (re-saves/edits never double-decrement). Scans at checkout auto-select the matched frame for the active vision section.

Changed

  • Settings → Label Settings: New card to configure barcode prefix, next counter value, label size, copies, show-price, auto-generation, and the default label printer.
  • Inventory: Added a barcode input with a Generate button to all add-item forms, barcode filter support in search, a Scan button that opens the camera modal, and Print Label / batch-select actions per row.

[1.0.71] 2026-08-11

Added

  • Native GDI fast-printing (PrintImage.exe): Invoice/receipt printing now has a dedicated fast path that replicates the way legacy Windows software printed in under a second. The document is rendered once to a high-resolution bitmap in the hidden print window, then spooled through a tiny native helper (PrintImage.exe, compiled with the OS's built-in .NET Framework compiler and shipped inside the installer) that talks directly to the printer through the Windows GDI pipeline — no PDF generation, no SumatraPDF/PDF-viewer subprocess, no temporary PDF round-trip. Pre-warming still happens on save and when you open a patient's invoice, so the print click itself is just a capture + GDI spool. If the native helper is missing or a driver rejects it, printing automatically falls back to the previous ~11s PDF-spool pipeline (pdf-to-printer / unix-print) and then to built-in Electron printing, so nothing regresses on any printer.
  • PrintImage.exe is now shipped with the app: The compiled helper is added as a packaged resource (extraResources), and the Main process resolves it from the installed resources folder, the project folder, or the app root.

Fixed

  • Full translations for all 33 languages: Every UI string (~1,100 keys) is now translated in all 30 non-English/FR/AR languages (Chinese, Hindi, Spanish, Bengali, Portuguese, Russian, Urdu, Indonesian, Japanese, German, Turkish, Korean, Vietnamese, Italian, Thai, Persian, Polish, Ukrainian, Dutch, Greek, Czech, Swedish, Hungarian, Romanian, Hebrew, Danish, Finnish, Norwegian, Malay, Swahili) — previously only the first ~137 keys existed per language and the rest of the interface silently fell back to English. English placeholder tokens like {customerName} are preserved in every translation so email/WhatsApp templates keep working.
  • No more hardcoded English text in the interface: After switching languages, several screens still showed English text that bypassed the translation system. All hardcoded UI strings across every tab (Dashboard, Invoices, Patients, Inventory, Settings — including the invoice-logo modal, notification popup, confirm dialogs, PDF preview, upgrade prompt, prescription history modal and AI prescription scanner) now go through the translation function. RTL printing also now covers Urdu, Persian and Hebrew in addition to Arabic.

[1.0.70] 2026-08-10

Added

  • Languages: Added 30 new languages to the Currency, Tax & Language card (now 33 total: en, fr, ar + zh, hi, es, bn, pt, ru, ur, id, ja, de, tr, ko, vi, it, th, fa, pl, uk, nl, el, cs, sv, hu, ro, he, da, fi, no, ms, sw). Core UI strings are translated per language; untranslated keys fall back to English. RTL support for Urdu, Persian and Hebrew, and locale-aware date/number formatting through a shared language registry.

Fixed

  • Currency words are now correct for every country: The settings screen had a hard-coded lookup for only 13 common currencies, so picking most countries (e.g. Egypt, Algeria, Saudi Arabia) produced a nonsense "amount in words" value like egp or sar instead of pounds or riyals. A complete word map now covers all 154 currency codes used by the 237 supported countries, in English and French, and both the Settings page and onboarding wizard share the same lookup. You can still override the words at any time with Custom Currency In Words.
  • Auto-backup no longer floods the backup folder: With automatic backup enabled, the backup schedule could silently reset itself, so the hourly check would create a new Visilion-backup-*.sqlite file far more often than the configured frequency (e.g. every hour instead of once a day). The Settings backup card and the global auto-backup loop both wrote the lastBackupTime bookkeeping to the same storage, and the card could overwrite the fresh timestamp with a stale one from its own in-memory state. The card now never regresses that timestamp (it keeps the most recent of the two), and a hard one-hour floor guarantees at most one backup per hour even if the recorded time is ever lost.
  • Backup folder is now kept tidy: After every scheduled or manual backup, the app deletes the oldest Visilion-backup-*.sqlite files in the chosen folder, keeping only the 14 most recent, so the folder can no longer fill up with hundreds of files.

Changed

  • Custom currency fields are now shown unlocked-by-default values with a lock: The Custom Currency and Custom Currency In Words fields in Settings now display the current country-derived values (e.g. USD, dollars) but locked. Click the padlock to unlock the field and type your own override; the override is saved on blur. Locking again (or clearing the field) reverts to the country default. This replaces the previously blank "custom" inputs, making it obvious what the current currency is and how to customize it.
  • Backup settings are now a dedicated, self-contained card: The backup & data-transfer settings were extracted from the Settings page into their own BackupSettings component (import/export, choose folder, backup now, automatic backup toggle and frequency). The Settings page stays functionally identical but is much smaller and easier to maintain.
  • Backup card redesign: The backup card now uses a modern section-based layout with an auto-backup toggle + frequency in a subtle panel, a visible backup folder indicator, a single prominent "Backup Now" action button with loading state, and a clean two-column import/export footer. The card also shows a live "last backup" relative timestamp.

[1.0.69] 2026-08-08

Changed

  • Invoice printing is now (almost) instant, even on slow PCs: The print pipeline no longer creates a fresh hidden Chromium window for every request — the renderer window is created once and pooled for all previews and prints. The invoice's HTML is pre-rendered into that pooled window ahead of the print (as soon as the Print button is on screen, after a save, or as soon as you click a patient / open one of their past invoices), so the actual Print click prints the already-rendered page straight to the OS spooler — no PDF generation, no temp file, no spawned helper process. Re-printing the same invoice is effectively instant.
  • Direct OS spooling with an automatic fallback: The primary print path hands the pre-rendered HTML page directly to the OS printer driver (the same approach legacy GDI software used). If a driver rejects that path, printing automatically falls back to the fast PDF spool pipeline (pdf-to-printer on Windows, unix-print on macOS/Linux) and finally to built-in Electron printing — so nothing regresses on printers that don't support direct spooling.

[1.0.68] 2026-08-07

Changed

  • Settings → Application Update is now a single, self-contained card: The update card no longer feels like a placeholder. The whole update flow now lives in one compact component — current version, status, an Automatic updates toggle, live download progress, and the action buttons — so the Settings page stays small and tidy.
  • Automatic update toggle: You can now switch automatic background downloads on or off directly in Settings → Application Update. The choice is saved with your settings and honored both by the in-app updater and at the next launch.
  • Cancel button while updating: Downloading (or checking for) an update can now be cancelled with a Cancel button, instead of forcing the download to finish.
  • Version "from → to" display: When an update is found or is downloading, the card shows exactly which version you are moving to (e.g. 1.0.67 → 1.0.68) alongside the current version.
  • Manual download when automatic updates are off: With automatic updates disabled, an update check that finds a new version shows a Download button, so you stay in control of when updates install.

[1.0.67] 2026-08-06

Fixed

  • Prescription Scanner buttons and labels are now always readable: The Smart Prescription Scanner is a camera overlay that must stay dark in both light and dark themes, but the app's theme system was recoloring its white text and translucent buttons, producing dark-on-dark text in light mode and white-on-white text in dark mode (especially the Save / Start Camera / Capture Now buttons). The scanner modal now forces its own dark camera UI — white labels, dark text on solid white buttons, brighter status pills (detected / scanning / tips), visible borders, and a slightly darker backdrop — so every button and label keeps high contrast in either theme.

Changed

  • Removed the unused legacy AIPrescriptionScanner component: AIPrescriptionScanner.jsx was orphaned dead code (not imported or rendered anywhere). The active SmartPrescriptionScanner already provides the same Gemini AI extraction with the OCR fallback, plus the API-key configuration, auto-capture and gallery upload, so the redundant component was deleted.
  • Prescription Scanner now flags itself as a beta feature: The subtitle under the camera now reads "This feature is still in beta" (localized in English, French and Arabic) so users know the scanner is still experimental.
  • Scanner controls moved to the top-right corner: The API-key and close buttons are now pinned to the top-right corner of the modal instead of sitting inline after the title.
  • API-key button now shows a key icon: The check mark that appeared once an API key was configured is replaced with a key icon, making it clearer that the button manages the API key.
  • Scanner popup now matches the invoice-form modal design: The Smart Prescription Scanner was rebuilt with the same popup language as the PD / advance-payment modals — a max-w-md rounded glass card with the shared purple header icon, small corner close + API-key buttons, theme-aware header/inputs (light and dark), the same blurred backdrop, and the same fade/scale closing animation. Only the live camera viewport stays dark, keeping the scan HUD readable in both themes.

[1.0.66] 2026-08-06

Changed

  • Modern and Professional templates now use their own color + design language: Modern left the shared purple accent behind for a full electric-blue editorial identity — a solid blue masthead spine, a huge uppercase blue title, a solid blue invoice-number pill, blue ribbon section headings and blue table top rules. Professional was redesigned as a centered formal emerald corporate layout — a centered letterhead with the logo and business name, an uppercase emerald document title with a double centered underline, emerald section rules and emerald-tinted table headers, and an emerald double-line total box. Both stay low-ink: drama comes from thin strong rules, colored text and small solid fills rather than heavy ink areas.
  • Previews now scroll instantly: The invoice/receipt preview modal used to embed the rendered PDF, and Chromium's built-in PDF viewer makes scrolling and zooming feel slow and janky. The preview now renders the same HTML document (identical to what is printed) in a plain HTML iframe, so scrolling is native and fluid; the download button still fetches the exact PDF.
  • Print directly from the preview: The preview modal now has a Print button, so you no longer have to close the preview and use the Print button underneath the form. It prints the same document you're looking at and closes the preview on success.
  • Template preview in Invoice Settings now shows the real design: The live preview panel previously used a hand-written approximation that did not match the printed result. It now renders the exact print template in a scaled iframe with your current settings (template, header, logo, watermark, footer, accent color, language) and sample invoice data — so what you see while choosing a template is identical to what prints.

Fixed

  • Previews that went blank in packaged builds now work again: The invoice/receipt preview (in the invoice form) and the Invoice Settings live template preview render the print template inside an iframe. The template draws itself with an inline script, but the app's production Content-Security-Policy (script-src 'self') blocks inline scripts — so previews rendered blank in the installed app (dev was unaffected because it allows 'unsafe-inline'). Previews are now generated in the Main process's dedicated CSP-free print partition and returned as static, script-free HTML, so they render the exact same design everywhere. If the HTML preview is ever unavailable, the invoice preview gracefully falls back to the PDF.
  • Recolor of Modern/Professional no longer uses the purple accent: Both variants previously fell back to var(--accent, #9333ea); Modern and Professional now use fixed brand colors (electric blue / emerald) that do not shift when the app's theme color changes.

[1.0.65] 2026-08-05

Changed

  • Printing is now canvas-free and never freezes the app: Invoices, receipts, previews, and saved PDFs used to be generated on the interface thread with jsPDF + html2canvas (building several megabytes of pixel geometry), which could lock the UI for seconds. All print, preview, save-to-device, email, WhatsApp, and share flows now send only the raw invoice data to the background (Main) process over IPC. The Main process formats the document, renders it in a hidden HTML window, and prints natively via the operating system or returns a compact PDF — so the print and preview buttons respond instantly.
  • Printing pipeline now runs in the Main (Node) process: A new hidden-window spooler owns document formatting, number-to-words conversion, settings/logo resolution, and native printing (print-invoice / invoice-to-pdf IPC channels), reusing the existing SQLite settings store and permission guards.
  • Reusable PrintButton component: A single reusable component now triggers native printing with one IPC call and is used across the invoice views.
  • All 5 invoice templates are preserved: The new fast engine honors the template selected in Invoice Settings (Minimal, Elegant, Modern, Classic, Professional), replicating each design (Elegant serif header, Modern accent side bar, Classic double border, Professional dark header band) in the canvas-free HTML renderer.

Fixed

  • Invoice settings now fully control the printed document again: The canvas-free engine originally rendered a fixed layout and ignored several settings that the legacy jsPDF path applied. The new renderer now honors the configured header font size, the logo size (restored to the legacy ~35 mm header width, scaled by the logo size setting), logo opacity/rotation, and the legacy footer style (10 pt italic). Template, accent color, background watermark, tax/tax-mode, currency, and amount-in-words were already wired through.
  • Prescription sections now come before frame sections again: The legacy engine rendered prescription tables, then pupillary distance, then frames. The first canvas-free builds swapped this order; the document now restores prescription → PD → frames.

Changed

  • Invoice layout redesigned with a modern look: Section headings now use an accent-colored left bar with hairline underlines, tables use soft rounded headers with an accent rule and subtle row striping, the patient info labels are compact small-caps, the total summary box is accent-highlighted, and the amount-in-words sits in a tinted band. Each of the 5 templates (Minimal, Elegant, Modern, Classic, Professional) was refreshed to stay consistent (gold accents for Elegant, monochrome for Classic, accent-bordered dark band for Professional).
  • Printed invoices now match the app's card aesthetic: The hidden print window renders the same premium language as the interface — the header sits in a glassy gradient card, section titles are accent-colored pills, customer info and tables are soft rounded cards with subtle shadows, and the total summary box is an accent gradient card with white text. A faint accent-tinted glow replaces the plain white page background.
  • All five invoice templates share the card design with their own fancy touch — and stay printer-friendly: Elegant got a luxury gold/serif treatment with ornamental corner flourishes and a double gold title underline; Modern became an editorial masthead — a full-width band with a solid accent spine on the left, a huge accent uppercase title, a solid accent invoice-number pill and ribbon-style section headings, with accent top rules on every table; Classic became a typewriter ledger with a stamped section heading (offset shadow), a solid black table header and a low-ink double-line total box; Professional became a corporate letterhead — a white band with a dark top rule and accent bottom rule, an uppercase letter-spaced business name, a dark-to-accent document title underline and a double-line accent total box. Drama comes from geometry, colored text and thin strong rules rather than heavy solid fills, so ink usage stays low. Each template also styles the receipt's vertical breakdown table in its own theme.
  • Footer now sticks to the bottom of the page: The page is laid out as a full-height flex column, so the footer (or thank-you line) is pinned to the bottom of the A4 sheet instead of floating right after the content.
  • Low-ink Total box, no duplicated amount-in-words: The total summary box switched from a heavy solid/ink-heavy accent fill to a light accent-tinted card with a thin accent top border, and the amount in words is now shown exactly once (only in the third summary box on invoices) instead of appearing twice.
  • Receipts now show a vertical cost-breakdown table: Instead of the hard-to-read three-column summary, payment receipts list each cost on its own row (label left, amount right) with the Total à payer row highlighted in the accent color at the bottom.

[1.0.64] 2026-08-05

Fixed

  • Licenses are now limited to one active device: The license gate Worker previously forwarded every activation straight to the WordPress DLM API without tracking devices, so the same key could stay active on multiple machines and the "activated on another device" demotion prompt never appeared. The Worker now enforces a single-active-device policy (backed by Cloudflare KV): the newest device to activate takes ownership, the previous owner is permanently evicted (403 + demotion message on its next validate/activate), and evicted devices can never re-take the key — so the desktop app reliably drops to the Free plan and shows the prompt instead of silently self-healing back to Pro.
  • The 24-hour heartbeat now also resolves ownership by license key: The app previously sent only the activation token on /validate, so devices activated before the Worker tracked anything could never be matched to their license key and were never revoked. The Worker now uses the license_key to evict non-owner devices even for pre-existing activations and lazily bootstraps ownership on the first check-in, so the second machine gets demoted automatically on its next heartbeat.

[1.0.63] 2026-08-03

Fixed

  • Editing your display name now really updates your account: Renaming yourself in Profile > Account & Security previously only saved to local settings, so the new name could be lost. It now also updates the signed-in user's record in the database and appears everywhere immediately (header, sidebar, team list) — and survives a restart.
  • Sessions now survive an app restart: The active session was written to the database, but the read-back treated the async result as plain JSON, so the token was never recovered and users were logged out after closing and reopening Visilion. Sessions now persist until they expire (12 hours) across restarts.
  • Team-member edits no longer rewrite a whole role's permissions: The user wizard's second step was writing the permission chips back to the role itself, silently changing permissions for everyone assigned to that role. It is now a read-only preview; role permissions are managed centrally in Manage Roles.

Changed

  • Seamless admin sign-in after setup: Completing the Sign-In Setup wizard now logs you in immediately as the new admin — no more landing on the login screen and re-typing your password.
  • setupAdmin is bootstrap-only: Re-running admin setup is rejected once sign-in is enabled, keeping the security model consistent (use Change Password / team management instead).

[1.0.62] 2026-08-02

Fixed

  • Deactivating a team member now actually saves: The Active toggle was silently ignored when saving a user (a camelCase/snake_case field mismatch), so disabling an account never persisted. Deactivated accounts are now disabled immediately and blocked from signing in.

Added

  • Login lockout protection: After 5 consecutive failed sign-in attempts, the account is temporarily locked (15 minutes, doubling on repeated lockouts) to deter brute-force attacks. A clear message appears on the login screen, and each lockout is recorded in the audit log.
  • Audit coverage for team changes: Creating, editing, or deleting users and roles is now written to the audit log (previously only role-permission changes were logged).
  • Warning before enabling sign-in: The Sign-In Setup wizard now opens with a warning screen that reminds you to save your admin password and create a database backup first -- because there is no password recovery, a lost password can lock you out and require a full app reset. You must confirm you understand before continuing.

Changed

  • "Member permissions" is now clearly "Role permissions": Editing permissions for a user now shows exactly which role is being updated and that the change applies to everyone with that role (permissions stay role-based).
  • The Admin role is read-only in Manage Roles: Since the Admin role always holds every permission, its permission panel is now shown as read-only with a "full access" notice instead of silently ignoring edits.

[1.0.61] 2026-08-02

Changed

  • Pick your account from a dropdown: The login screen no longer asks you to type a username. Active accounts appear in a dropdown — just select your name and type your password.
  • Window controls on the login screen: The login window now shows the minimize / maximize / close buttons again, so you can minimize or close the app from the sign-in screen.

Added

  • New auth:list-users IPC channel and listUsers bridge for the login dropdown (identity fields of active users only — no password hashes are exposed).

[1.0.60] 2026-08-02

Fixed

  • Account & Security popups stay usable on small windows: The Change Password, Sign-In Setup, Add/Edit Team Member, Manage Roles, and Confirm modals no longer push their close (×) button off-screen when the popup is taller than the window. They now scroll instead of overflowing, so the header and × are always clickable on short windows.
  • Change Password is now a compact 3-step wizard: It was redesigned into a small step-by-step flow (verify current password → set new password → done) so it stays short on small windows, with a progress bar, step dots, and a scroll-safe layout.
  • Add/Edit User is now a compact 2-step wizard: Account details (username, display name, role, active, password) and permission chips are split into separate steps (account → permissions → done), so the popup no longer gets tall on small windows.
  • Manage Roles is now a compact 2-step wizard: Pick a role (and add/edit roles) in step one, then fine-tune its permission chips in step two (role → permissions → done), replacing the old side-by-side two-pane layout.

[1.0.59] 2026-08-02

Changed

  • One card for Account & Security: Profile now shows a single Account & Security card that contains both your own account actions and (for admins) the full team. The separate segmented Users / Roles management view is gone.
  • All users in one list, settings in popups: Every user account is listed on one row in the card (name, username, role, active status). Clicking a user opens a popup with all of that user's settings — display name, username, role, active toggle, password, and their permission chips. Roles are managed in a separate popup with an add/edit form and a per-role permission picker.
  • Simpler permission chips: Permissions are shown as colored chip buttons grouped by area (no more drag-and-drop), so granting or revoking is a single click per permission and stays readable for novice users.

[1.0.58] 2026-08-02

Added

  • New profile.view permission: The Profile & account tab can now be restricted per role like any other feature. It is granted to Staff by default and appears in Profile → Users & Roles → Roles & Permissions (grouped under Settings). The header Profile menu item is hidden for users without it.
  • Permission catalog self-heals: The catalog is now reconciled on every database open, so newly added permissions appear in existing databases without wiping admin-customized role grants.

Changed

  • Drag-and-drop permission editor: The Roles & Permissions screen now uses a visual drag-and-drop builder. Pre-made permission chips are grouped by area in a Permission library on the left; drag them into the Granted permissions box (or click a chip) to grant them, and tap × or drag them back to revoke. Group-level Add all shortcuts and a role quick-picker were added too.
  • Friendlier permission buttons: Permission chips now carry a colored icon per area (blue dashboard, violet invoices, green patients, amber stock, pink appointments, cyan settings, red administration) so novice users can scan them at a glance, plus plain-language labels (e.g. "View stock history", "Change settings", "Backup & restore data").

Fixed

  • Blocking the Dashboard actually blocks it: Previously, denying dashboard.view hid the sidebar tab but the content still fell back to the dashboard. Tab switching now redirects to the first permitted tab, and the guard now also covers the Dashboard and Profile tabs.

[1.0.57] 2026-08-02

Fixed

  • Sign-In Setup works without Electron: When running the renderer in a browser (npm run dev), the auth/admin system now uses a localStorage fallback that mirrors the SQLite schema. Enabling sign-in, setting the admin password, logging in, and managing users/roles all work in dev mode instead of returning "Unavailable".
  • Sidebar no longer flashes on navigation: The sidebar and header are now pinned during tab-switch view transitions, so they no longer fade out and reappear when switching tabs.

[1.0.56] 2026-08-02

Changed

  • Guided Sign-In Setup Wizard: Enabling sign-in from Profile → Account & Security now opens a step-by-step setup wizard (welcome → create admin account → done) instead of immediately toggling auth. The wizard lets you choose the admin username and password, then hands you off to the login screen.
  • Simplified Account & Security card: When sign-in is off, the card shows a single Enable Sign-In & User Accounts button (no more form clutter). Users & Roles management only appears once sign-in is enabled.

Added

  • New auth:setup-admin IPC channel and setupAdmin bridge for configuring the admin account while sign-in is disabled (local mode).

[1.0.55] 2026-08-02

Fixed

  • Sign-in is now opt-in: The login screen no longer locks users out by default. Sign-in and user accounts are only enforced after an administrator explicitly enables them from Profile → Account & Security → Enable Sign-In. Until then the app opens directly in Local Mode (full local-admin access), and all permission checks are bypassed.

Changed

  • Users & Roles moved into the Profile: The two separate "Users & Roles" and "Roles & Permissions" sidebar tabs are gone. User accounts and role permissions are now managed together from Profile > Users & Roles, with a segmented Users / Roles & Permissions switcher.
  • User switcher in the header: Clicking the profile icon in the header opens a dropdown showing the current user (name + role) with quick actions -- Profile, Users & Roles (admins), and Switch User (signs out to the login screen when sign-in is enabled).

[1.0.54] 2026-08-02

Added

  • User Roles & Permissions (Admin Panel): Visilion now ships with a full role-based access control system.
    • Multi-user accounts: Create an account for each staff member instead of sharing one login. A default admin / admin account is created on first run and must change its password at first login.
    • Granular permissions: Control access per area — Dashboard, Invoices (create/read/update/delete/export/print), Patients, Inventory (including stock movements), Appointments, Settings, and Admin functions.
    • Built-in roles: Administrator (full access) and Staff (day-to-day operations) ship ready to use; system roles cannot be deleted and the last admin cannot be removed or demoted.
    • Custom roles: Build roles with any combination of permissions via a visual permission matrix in the new Roles admin tab.
    • Security: scrypt password hashing with per-user salt, token-based 12-hour sessions, single active session per app, and full audit logging of auth and admin actions.

[1.0.53] 2026-08-01

Added

  • Anonymous product telemetry: The desktop app now reports an anonymous "App Launch" event (plus a privacy-preserving profile) to Mixpanel to track active users and platform breakdowns (OS, architecture, app version). No personal or business data is sent — only a persistent random client ID generated and stored locally in the app's user-data folder. The tracker is fully offline-safe: network failures are swallowed and never crash or block the app.

[1.0.52] 2026-08-01

Changed

  • Generous Free tier: Cranked up the free plan limits to attract more users:
    • Invoices: 30 → 100 per month.
    • Suggested Prices: 20 → 50 opens per month.
    • AI Prescription Scanner unlocked for free users with a 20 scans/month allowance (previously Pro/Ultra only).
  • Upgrade prompts now describe the right feature: The upgrade dialog shows the actual limit reached (invoices, AI scans, or price suggestions) instead of always saying "invoices".
  • Accent Color moved to Free: The Appearance accent color picker is now available on the Free plan. Live Colors (custom gradients), Film Grain, Spotlight Glow, and Hide Footer remain Pro/Ultra.

[1.0.51] 2026-08-01

Fixed

  • False "License Reverted" while online: The 24-hour license heartbeat treated every validation failure (including token rejections) as "offline". A stale activation token was never refreshed, so after the 20-day grace period the app showed "License Reverted — Please connect to the internet to re-verify your license." even while online, forcing users to re-paste and re-activate their key.
  • License self-healing: The heartbeat now distinguishes a definitive server rejection from a network problem and automatically re-activates with the stored license key when the token check fails. A license is only reverted when the server definitively rejects the key; genuine network issues still use the 20-day offline grace period.
  • Startup re-verification: If the offline grace period has lapsed, Visilion now attempts an automatic online re-verification at launch before showing the offline warning.

[1.0.44] 2026-04-23

Fixed

  • Workflow Optimization: Removed YML artifacts from release uploads and fixed workflow artifact path patterns for cleaner builds.
  • Database Import Fix: Restored essential DELETE operation during invoice import to prevent duplicate entries.
  • Test Coverage: Expanded inventory test data to include all new fields (purchase_price, min_quantity, supplier, etc.) for better schema validation.

[1.0.43] 2026-04-23

Changed

  • Version Bump: Internal version alignment and dependency updates.

[1.0.42] 2026-04-23

Fixed

  • UI Consistency: Refactored the Search interface in Inventory to match the newly redesigned "Add Item" form layout, providing a unified experience across different modes.
  • Enhanced Contrast: Improved visibility of labels and placeholders across all inventory forms and filters, ensuring better readability in both light and dark modes.
  • Form Optimization: Removed redundant action buttons and refined the visual grouping of specifications and general filters for better focus.

[1.0.41] 2026-04-23

Fixed

  • Form Layout Improvements: Redesigned the "Add Item" forms in Inventory with grouped sections and a more professional grid layout, specifically optimizing the Lens form for better usability.
  • Table Alignment: Fixed data misalignment in the inventory result tables by implementing fixed column widths and ensuring consistent padding between headers and data rows.
  • Stock Indicators: Replaced bulky status badges with subtle, pulsing visual indicators to maintain a clean interface while still highlighting critical stock levels.

[1.0.40] 2026-04-23

Removed

  • Inventory History Tab: Removed the "History" tab from the inventory module to simplify the interface and focus on real-time stock management.

[1.0.39] 2026-04-23

Added

  • Advanced Inventory Filtering: Added new filter options for Supplier and Stock Status (In Stock, Low Stock, Out of Stock) to improve product discoverability.
  • Enhanced Inventory Analytics: Added Purchase Value tracking to summary cards, allowing owners to see total investment alongside total selling value.
  • Dynamic Supplier Filtering: Supplier dropdown now automatically populates based on unique suppliers in the current category.
  • Visual Stock Indicators: Improved row highlighting and badges for low-stock and out-of-stock items across all inventory tabs.

[1.0.38] 2026-04-22

Fixed

  • Resolved Duplicate Object Keys: Fixed multiple "Duplicate object key" errors in localization files (en.json, fr.json, ar.json).
  • JSON Structure Correction: Fixed broken `invoiceAppearance` object structure in locale files.
  • Prescription Field Localization: Renamed ambiguous `add` key to `lensAdd` in localization files and updated all component references.
  • Onboarding Wizard: Fixed wizard reset logic and white text consistency for language symbols.
  • Inventory UI Cleanup: Removed redundant header and Save/Export/Import buttons from the Inventory tab.
  • Legacy Code Cleanup: Removed unused legacy IPC handlers and data management functions related to bulk JSON inventory saving.
  • Animation Optimization: Adjusted animation delays in the Inventory tab for a smoother user experience.
  • Database Import: Removed unnecessary premium license check for returning user database imports.

[1.0.37] 2026-04-22

Maintenance

  • Optimized Release Workflow: Standardized download links to use GitHub's latest release redirect for improved reliability and automatic updates.
  • Version Alignment: Synchronized application version across all platforms and documentation.

[0.2.3] 2026-04-12

Added

  • Enhanced Inventory Management: Added specialized sections for Lenses and Lentilles stock tracking, allowing for more organized product management.
  • Inventory Movement History: New "History" tab that records every addition, sale, edit, and deletion, providing a full audit trail of inventory changes.
  • Low Stock Alerts: Enhanced visual indicators for low and out-of-stock items, including color-coded labels and a "Low Stock" badge for better visibility.
  • Inline Inventory Editing: Added the ability to edit quantity, price, model, brand, and color directly within the inventory table.

Changed

  • Updated the inventory database schema to support product types (frames, lenses, lentilles) and movement tracking.
  • Refined the inventory UI with a tabbed interface and improved form layouts for different product types.

[0.2.2] 2026-04-06

Added

  • AI Prescription Scanner (Premium): New AI-powered OCR feature for Pro and Ultra users. Capture prescriptions using the laptop camera and automatically fill invoice details (Patient Name, OD/OS, SPH, CYL, AXIS, ADD) using Gemini AI.
  • Comprehensive End-User Documentation: Created Documentation.md and Visilion_User_Manual.doc with professional formatting, features catalog, and troubleshooting.
  • External Link Support: Implemented IPC handlers to open links in the user's default web browser instead of the application window.
  • Conversion-Focused Copy: Updated localization files with professional, benefit-driven copy for English, French, and Arabic.
  • Sidebar Interactivity: Made sidebar logo and plan labels clickable, linking to the main website and pricing page.
  • Branding Consistency: Updated all support links and messages to point to visilion.com and contact@visilion.com.

Changed

  • Unified Data Migration: Standardized all backup and restore operations to use SQL files (.sqlite, .db). Removed legacy JSON-based import/export functionality and the "Data Folder" import wizard to improve data integrity and performance.
  • Improved AI Prescription Scanner UI by introducing a distinct "Camera + Sparkle" icon, switching to a clean white theme, and refining all interface copy for better professional appeal.
  • Removed the pulsing dot from the AI Scanner toolbar button to avoid confusion with active recording.
  • Refined footer branding text to a punchier "Vision. Style. Precision." (and its equivalents in AR/FR) for improved aesthetics.
  • Updated footer layout in Dashboard.jsx for better visibility and interactivity.
  • Changed "Get in touch for a personalized demo:" to a cleaner "Get in touch" across all languages.

Fixed

  • Fixed "No handler registered" error for open-external IPC call.
  • Corrected various broken URLs in the onboarding and profile sections.

[0.1.7] 2026-03-29

Added

  • Initial release with core optical management features.
  • Glassmorphism UI design.
  • Local SQLite database persistence.
  • Multilingual support (EN, AR, FR).