/* Inline SVG icons + i18n strings */
const Icon = ({ name, size = 16, ...rest }) => {
const paths = {
dashboard: <>>,
machine: <>>,
inventory: <>>,
transaction: <>>,
bell: <>>,
bottle: <>>,
users: <>>,
settings: <>>,
camera: <>>,
chart: <>>,
search: <>>,
chevdown: ,
chevright: ,
chevleft: ,
close: <>>,
plus: <>>,
download: <>>,
refresh: <>>,
filter: <>>,
lock: <>>,
unlock: <>>,
warning: <>>,
check: ,
pause: <>>,
play: ,
eye: <>>,
door: <>>,
map: <>>,
grid: <>>,
list: <>>,
sliders: <>>,
sun: <>>,
moon: ,
info: <>>,
dot: ,
arrowup: <>>,
arrowdown: <>>,
};
return (
);
};
const I18N = {
no: {
nav_overview: 'Oversikt',
nav_machines: 'Automater',
nav_transactions: 'Transaksjoner',
nav_events: 'Hendelser',
nav_bottles: 'Flasker & priser',
nav_users: 'Brukere',
nav_settings: 'Innstillinger',
overview_title: 'Oversikt',
overview_sub: 'Sanntid på tvers av alle automater i tenanten',
kpi_today_rev: 'Omsetning i dag',
kpi_month_rev: 'Omsetning denne mnd',
kpi_full_total: 'Fulle totalt',
kpi_empty: 'Tomme – klar for henting',
kpi_vacant: 'Ledige hyller',
kpi_offline: 'Offline / med feil',
alarms: 'Aktive varsler',
alarms_sub: 'Alle åpne hendelser på tvers av automater',
all_machines: 'Alle automater',
view_map: 'Kart',
view_grid: 'Kort',
view_list: 'Liste',
add_machine: 'Legg til automat',
full: 'Fulle',
empty: 'Tomme',
vacant: 'Ledige',
today: 'I dag',
month: 'Denne mnd',
online: 'Online',
offline: 'Offline',
warn: 'Varsler',
live: 'LIVE',
tab_statistics: 'Statistikk',
tab_inventory: 'Sanntidsbeholdning',
tab_shelves: 'Hyller',
tab_cameras: 'Kameraer',
tab_transactions: 'Transaksjoner',
tab_events: 'Hendelser',
tab_settings: 'Innstillinger',
overall: 'Totalt',
swap: 'Bytte',
buy: 'Kjøp',
avg_basket: 'Snitt per transaksjon',
avg_dwell: 'Snitt-tid per kunde',
refill_freq: 'Snitt mellom påfyll',
classification_conf: 'Bilde-klassifisering snitt-konfidens',
cabinet_front: 'Skap A (front)',
cabinet_back: 'Skap B (bakside)',
cabinet_sub: '12 hyller · 4 kameraer',
state_full: 'Full',
state_empty: 'Tomflaske',
state_vacant: 'Ledig',
state_disabled: 'Deaktivert',
state_error: 'Feil',
shelf_actions: 'Hyllestyring',
btn_unlock: 'Åpne lås',
btn_lock: 'Lås',
btn_disable: 'Deaktiver',
btn_enable: 'Aktiver',
btn_force_empty: 'Marker som tom',
btn_force_full: 'Marker som full',
btn_capture: 'Ta nytt bilde',
last_capture: 'Siste kamerabilde',
capture_taken: 'Tatt',
last_event: 'Siste hendelse',
calibrate_title: 'Kamerakalibrering',
calibrate_sub: 'Marker utsnittet til hver hylle (2×2) i kamerabildet. Verdiene lagres som ROI per hylle.',
cam: 'Kamera',
coverage: 'Dekker',
new_capture: 'Nytt bilde',
save_rois: 'Lagre ROI',
auto_detect: 'AI-forslag',
snap_grid: 'Snap 2×2',
reset_rois: 'Tilbakestill',
bottles_title: 'Flasker & priser',
bottles_sub: 'Definer flasketyper, bytte- og kjøpspriser',
type: 'Type',
name: 'Navn',
weight: 'Vekt',
swap_price: 'Bytte-pris',
new_price: 'Kjøp-pris (med flaske)',
inventory_total: 'På lager (alle automater)',
edit: 'Endre',
users_title: 'Brukere & tilganger',
users_sub: 'Inviter teammedlemmer og styr roller per tenant',
invite: 'Inviter bruker',
role: 'Rolle',
last_seen: 'Sist aktiv',
search_placeholder: 'Søk automat (navn eller ID)…',
refresh: 'Oppdater',
export: 'Eksporter',
settings: 'Innstillinger',
tx_id: 'Transaksjon',
tx_time: 'Tid',
tx_type: 'Type',
tx_bottle: 'Flaske',
tx_shelf_out: 'Hylle ut',
tx_shelf_in: 'Hylle inn',
tx_amount: 'Beløp',
tx_pay: 'Betaling',
tx_duration: 'Varighet',
ev_time: 'Tid',
ev_sev: 'Alvorlighet',
ev_msg: 'Melding',
ev_machine: 'Automat',
ev_shelf: 'Hylle',
ev_camera: 'Kamera',
sev_danger: 'Kritisk',
sev_warn: 'Varsel',
sev_info: 'Info',
minutes_ago: 'min siden',
hours_ago: 't siden',
days_ago: 'd siden',
last_seen_label: 'Sist sett',
firmware: 'Firmware',
last_service: 'Siste service',
open: 'Åpne',
close: 'Lukk',
revenue: 'Omsetning',
transactions: 'Transaksjoner',
sales_split: 'Salgsfordeling',
top_bottles: 'Mest solgte flasker',
busy_hours: 'Travleste tider',
last7: 'Siste 7 dager',
last30: 'Siste 30 dager',
last90: 'Siste 90 dager',
machine_details: 'Automat-detaljer',
},
en: {
nav_overview: 'Overview',
nav_machines: 'Machines',
nav_transactions: 'Transactions',
nav_events: 'Events',
nav_bottles: 'Bottles & pricing',
nav_users: 'Users',
nav_settings: 'Settings',
overview_title: 'Overview',
overview_sub: 'Realtime across all machines in the tenant',
kpi_today_rev: 'Revenue today',
kpi_month_rev: 'Revenue this month',
kpi_full_total: 'Full total',
kpi_empty: 'Empty – ready for pickup',
kpi_vacant: 'Vacant shelves',
kpi_offline: 'Offline / with errors',
alarms: 'Active alerts',
alarms_sub: 'All open events across machines',
all_machines: 'All machines',
view_map: 'Map',
view_grid: 'Cards',
view_list: 'List',
add_machine: 'Add machine',
full: 'Full',
empty: 'Empty',
vacant: 'Vacant',
today: 'Today',
month: 'This month',
online: 'Online',
offline: 'Offline',
warn: 'Alerts',
live: 'LIVE',
tab_statistics: 'Statistics',
tab_inventory: 'Realtime inventory',
tab_shelves: 'Shelves',
tab_cameras: 'Cameras',
tab_transactions: 'Transactions',
tab_events: 'Events',
tab_settings: 'Settings',
overall: 'Total',
swap: 'Swap',
buy: 'Buy',
avg_basket: 'Avg per transaction',
avg_dwell: 'Avg customer time',
refill_freq: 'Avg between refills',
classification_conf: 'Image-class. avg confidence',
cabinet_front: 'Cabinet A (front)',
cabinet_back: 'Cabinet B (back)',
cabinet_sub: '12 shelves · 4 cameras',
state_full: 'Full',
state_empty: 'Empty bottle',
state_vacant: 'Vacant',
state_disabled: 'Disabled',
state_error: 'Error',
shelf_actions: 'Shelf control',
btn_unlock: 'Unlock',
btn_lock: 'Lock',
btn_disable: 'Disable',
btn_enable: 'Enable',
btn_force_empty: 'Mark as empty',
btn_force_full: 'Mark as full',
btn_capture: 'Capture image',
last_capture: 'Last camera image',
capture_taken: 'Taken',
last_event: 'Last event',
calibrate_title: 'Camera calibration',
calibrate_sub: 'Drag rectangles to mark each shelf (2×2) in the camera frame. Stored as ROI per shelf.',
cam: 'Camera',
coverage: 'Covers',
new_capture: 'New capture',
save_rois: 'Save ROIs',
auto_detect: 'AI suggest',
snap_grid: 'Snap 2×2',
reset_rois: 'Reset',
bottles_title: 'Bottles & pricing',
bottles_sub: 'Define bottle types, swap and new-bottle prices',
type: 'Type',
name: 'Name',
weight: 'Weight',
swap_price: 'Swap price',
new_price: 'New-bottle price',
inventory_total: 'In stock (all machines)',
edit: 'Edit',
users_title: 'Users & access',
users_sub: 'Invite teammates and manage roles per tenant',
invite: 'Invite user',
role: 'Role',
last_seen: 'Last seen',
search_placeholder: 'Search machine (name or ID)…',
refresh: 'Refresh',
export: 'Export',
settings: 'Settings',
tx_id: 'Transaction',
tx_time: 'Time',
tx_type: 'Type',
tx_bottle: 'Bottle',
tx_shelf_out: 'Shelf out',
tx_shelf_in: 'Shelf in',
tx_amount: 'Amount',
tx_pay: 'Payment',
tx_duration: 'Duration',
ev_time: 'Time',
ev_sev: 'Severity',
ev_msg: 'Message',
ev_machine: 'Machine',
ev_shelf: 'Shelf',
ev_camera: 'Camera',
sev_danger: 'Critical',
sev_warn: 'Warning',
sev_info: 'Info',
minutes_ago: 'min ago',
hours_ago: 'h ago',
days_ago: 'd ago',
last_seen_label: 'Last seen',
firmware: 'Firmware',
last_service: 'Last service',
open: 'Open',
close: 'Close',
revenue: 'Revenue',
transactions: 'Transactions',
sales_split: 'Sales split',
top_bottles: 'Top bottles',
busy_hours: 'Busiest hours',
last7: 'Last 7 days',
last30: 'Last 30 days',
last90: 'Last 90 days',
machine_details: 'Machine details',
},
};
const fmtKr = (n, lang = 'no') => {
if (n == null) return '–';
const s = Math.round(n).toLocaleString(lang === 'no' ? 'nb-NO' : 'en-US');
return lang === 'no' ? `${s} kr` : `NOK ${s}`;
};
const fmtNum = (n, lang = 'no') => (n == null ? '–' : n.toLocaleString(lang === 'no' ? 'nb-NO' : 'en-US'));
Object.assign(window, { Icon, I18N, fmtKr, fmtNum });