POS frontend
Stack: Vue 3, Vuetify 3, Vue Router, Pinia, Vite; Electron for desktop; optional Laravel Echo (Pusher/Reverb) for live updates.
This is the app store staff actually use to run the till, manage inventory, and handle day-to-day operations. Unlike Admin, everything here runs in the context of one tenant.
Tenant resolution
Section titled “Tenant resolution”A user has to resolve which tenant they’re operating as before anything else works:
- Desktop/manual: user enters a client code on the TenantSetup screen →
POST api/tenant/resolve/client-code→ tenant ID stored → redirect to Auth. - Web (by domain): an Axios interceptor / bootstrap step calls
GET api/tenant/resolve/domainand caches the result.
Tenant ID lives in localStorage (or the Electron store) under key x-tenant-id (a legacy x_tenant_id key is still read once, for migration). src/Services/TenantService.js owns this: getTenantId(), setTenantId(), clearTenantId(), resolveTenant(), getBaseURL(). The router redirects to /tenant-setup if no tenant is saved and the current route isn’t already TenantSetup, and the reverse once one is.
Once resolved, src/axios/index.js’s request interceptor attaches X-Tenant-ID to every API call automatically. You never set it by hand.
Live updates (WebSockets)
Section titled “Live updates (WebSockets)”src/Composables/useWebSocket.js subscribes to tenant-scoped channels once tenant and user are available (wired in views/Home.vue):
tenant.{tenantId}: tenant-wide (admin notices, config, force refresh)tenant.{tenantId}.user.{userId}: per-user (payment failures, permission changes)tenant.{tenantId}.store.{storeId}: per-store (sales, payments, stock transfer)
src/echo/echo.js sends X-Tenant-ID for broadcast auth the same way Axios does for regular requests.
Frontend modules
Section titled “Frontend modules”Path: src/modules/. Each module follows the same shape: index.js, pages/, services/, routes/, store/, components/, composables/. src/modules/index.js is the registry: it imports each module and exports modules, moduleRoutes (merged into the main router), and piniaStores. Not every module scaffolded here is active; check the registry for what’s actually wired in.
Restaurant vs. retail
Section titled “Restaurant vs. retail”A store’s business_type (retail | restaurant) changes frontend behavior without being a separate build: restaurant mode enables accompanying-stock and hides certain sidebar items (Warehouses, Stock Transfers, GRNs, Quotations). Exposed via systemConfigStore.isRestaurantMode. See Architecture.
Electron desktop app
Section titled “Electron desktop app”Entry: electron/main.js, wrapping this same SPA. Thermal receipt printing goes through IPC via electron-pos-printer. Auto-updates pull from https://download.dg-smartpos.co.ke. Build with pnpm run electron-build.
Local setup
Section titled “Local setup”pnpm installpnpm run dev # :3001pnpm run buildpnpm run lintpnpm run test # Vitestpnpm run electron # Electron dev wrapperNeeds VITE_TITAN_API (API base URL) and, for live updates, VITE_REVERB_APP_KEY/VITE_REVERB_HOST/VITE_REVERB_PORT/VITE_REVERB_SCHEME.
UI patterns to follow
Section titled “UI patterns to follow”Before building a new page or modal here, see Conventions → Frontend UI patterns. Page height (fillMainHeight) and modal header truncation both have a specific right answer that isn’t obvious from Vuetify’s defaults.