Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 45 additions & 6 deletions builds/installer.nsh
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Var POSNIC_KEEP_HARDWARE
Var POSNIC_KEEP_BACKUPS
Var POSNIC_BUSINESS_CHECK
Var POSNIC_HARDWARE_CHECK
Var POSNIC_REMOVE_ALL_CHECK
Var POSNIC_BACKUPS_CHECK
Var POSNIC_REMOVAL_SUMMARY
Var POSNIC_POWERSHELL
Expand Down Expand Up @@ -253,24 +254,62 @@ Function un.PosnicDataPage
!insertmacro MUI_HEADER_TEXT "Remove Posnic?" "The application will be removed. Choose what to keep."
nsDialogs::Create 1018
Pop $0
${NSD_CreateCheckbox} 0 12u 100% 20u "Keep business data"
${NSD_CreateCheckbox} 0 0u 100% 18u "Remove all local data"
Pop $POSNIC_REMOVE_ALL_CHECK
${NSD_OnClick} $POSNIC_REMOVE_ALL_CHECK un.PosnicRemoveAllClick
${NSD_CreateCheckbox} 0 24u 100% 18u "Keep business data"
Pop $POSNIC_BUSINESS_CHECK
${NSD_SetState} $POSNIC_BUSINESS_CHECK $POSNIC_KEEP_BUSINESS
${NSD_CreateLabel} 14u 34u 95% 22u "Sales, stock, customers and pending changes."
${NSD_OnClick} $POSNIC_BUSINESS_CHECK un.PosnicKeepClick
${NSD_CreateLabel} 14u 42u 95% 14u "Sales, stock, customers and pending changes."
Pop $0
${NSD_CreateCheckbox} 0 62u 100% 20u "Keep hardware settings"
${NSD_CreateCheckbox} 0 58u 100% 18u "Keep hardware settings"
Pop $POSNIC_HARDWARE_CHECK
${NSD_SetState} $POSNIC_HARDWARE_CHECK $POSNIC_KEEP_HARDWARE
${NSD_CreateLabel} 14u 84u 95% 22u "Scales, printers and kitchen screens."
${NSD_OnClick} $POSNIC_HARDWARE_CHECK un.PosnicKeepClick
${NSD_CreateLabel} 14u 76u 95% 14u "Scales, printers and kitchen screens."
Pop $0
${NSD_CreateCheckbox} 0 112u 100% 20u "Keep saved backups"
${NSD_CreateCheckbox} 0 94u 100% 18u "Keep saved backups"
Pop $POSNIC_BACKUPS_CHECK
${NSD_SetState} $POSNIC_BACKUPS_CHECK $POSNIC_KEEP_BACKUPS
${NSD_CreateLabel} 0 145u 100% 36u "Cloud data will not be deleted. Unchecked data is permanently removed from this computer. No extra backup is created."
${NSD_OnClick} $POSNIC_BACKUPS_CHECK un.PosnicKeepClick
${NSD_CreateLabel} 0 116u 100% 22u "Cloud data stays unchanged. Unchecked local data is permanently deleted. No extra backup is created."
Pop $0
Call un.PosnicSyncRemoveAll
nsDialogs::Show
FunctionEnd

Function un.PosnicRemoveAllClick
Pop $0
${NSD_GetState} $POSNIC_REMOVE_ALL_CHECK $1
${If} $1 == ${BST_CHECKED}
StrCpy $1 ${BST_UNCHECKED}
${Else}
StrCpy $1 ${BST_CHECKED}
${EndIf}
${NSD_SetState} $POSNIC_BUSINESS_CHECK $1
${NSD_SetState} $POSNIC_HARDWARE_CHECK $1
${NSD_SetState} $POSNIC_BACKUPS_CHECK $1
FunctionEnd

Function un.PosnicKeepClick
Pop $0
Call un.PosnicSyncRemoveAll
FunctionEnd

Function un.PosnicSyncRemoveAll
${NSD_GetState} $POSNIC_BUSINESS_CHECK $1
${NSD_GetState} $POSNIC_HARDWARE_CHECK $2
${NSD_GetState} $POSNIC_BACKUPS_CHECK $3
${If} $1 == ${BST_UNCHECKED}
${AndIf} $2 == ${BST_UNCHECKED}
${AndIf} $3 == ${BST_UNCHECKED}
${NSD_SetState} $POSNIC_REMOVE_ALL_CHECK ${BST_CHECKED}
${Else}
${NSD_SetState} $POSNIC_REMOVE_ALL_CHECK ${BST_UNCHECKED}
${EndIf}
FunctionEnd

Function un.PosnicDataLeave
${NSD_GetState} $POSNIC_BUSINESS_CHECK $POSNIC_KEEP_BUSINESS
${NSD_GetState} $POSNIC_HARDWARE_CHECK $POSNIC_KEEP_HARDWARE
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@
"src/fonts/NotoSansTC-OFL.txt",
"src/simple-setup.css",
"src/simple-setup.js",
"src/setup-region.js",
"src/setup-region-data.js",
"src/setup-recovery.js",
"src/setup-hardware-files.json"
],
Expand Down
9 changes: 9 additions & 0 deletions scripts/generate-setup-regions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const fs=require('node:fs'),path=require('node:path'),vm=require('node:vm');
const root=path.resolve(__dirname,'..');
const read=name=>JSON.parse(fs.readFileSync(path.join(root,'api/src/json',name+'.json'),'utf8'));
const currency=read('country_currency');
const source=fs.readFileSync(path.join(root,'src/install-wizard.html'),'utf8');
const match=source.match(/const countryTimezones = (\{[\s\S]*?\n \});/);
if(!match)throw Error('Setup timezone reference not found');
const data={countries:read('countries').countries.map(c=>[c.id,c.sortname,c.value,currency[c.sortname]||'']),states:read('states').states.map(s=>[s.country_id,s.name]),currencies:[...new Set(read('currency').currency.map(c=>c.text).filter(Boolean))],timezones:vm.runInNewContext('('+match[1]+')')};
fs.writeFileSync(path.join(root,'src/setup-region-data.js'),'// Generated by scripts/generate-setup-regions.js from bundled reference data.\nwindow.PosnicSetupRegionData = '+JSON.stringify(data)+';\n');
242 changes: 140 additions & 102 deletions src/install-wizard.html

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2505,6 +2505,16 @@ function createWindow(showStartupLoader = !isWarmStartup()) {
// Redirect to installation wizard for first time setup
async function redirectToWizard() {
if (mainWindow) {
// Use the current display's usable area (already adjusted for Windows DPI
// scaling and taskbar), rather than leaving setup in the fixed 800px shell.
if (!mainWindow.isMaximized()) {
const area = electron.screen.getDisplayMatching(mainWindow.getBounds()).workArea;
const width = Math.min(1200, area.width - 24);
const height = Math.min(960, area.height - 24);
mainWindow.setMinimumSize(Math.min(760, width), Math.min(540, height));
mainWindow.setBounds({ x: area.x + Math.floor((area.width - width) / 2),
y: area.y + Math.floor((area.height - height) / 2), width, height });
}
await mainWindow.loadFile('src/install-wizard.html');
if (!mainWindow.isVisible()) mainWindow.show();
mainWindow.focus();
Expand Down
2 changes: 2 additions & 0 deletions src/setup-region-data.js

Large diffs are not rendered by default.

79 changes: 79 additions & 0 deletions src/setup-region.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'use strict';
(() => {
const data = window.PosnicSetupRegionData;
const field = id => document.getElementById(id);
const country = field('qCountry'), currency = field('qCurrency');
if (!data || !country) return;
const option = (value, text = value) => new Option(text, value);
data.countries.forEach(row => country.add(option(row[2])));
data.currencies.forEach(code => currency.add(option(code)));
const zones = typeof Intl.supportedValuesOf === 'function' ? Intl.supportedValuesOf('timeZone') : [];
zones.forEach(zone => field('qTimezoneList').appendChild(option(zone)));
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || '';
field('qTimezone').value = timezone;
function updateCountry() {
const row = data.countries.find(row => row[2] === country.value);
field('qState').value = '';
field('qStateList').replaceChildren();
const states = row ? data.states.filter(state => state[0] === row[0]) : [];
states.forEach(state => field('qStateList').appendChild(option(state[1])));
field('qState').required = states.length > 0;
document.querySelector('label[for="qState"]').textContent = states.length ? 'State / province *' : 'State / province';
if (row && data.currencies.includes(row[3])) currency.value = row[3];
else currency.value = '';
}
// Language is not location (many Indian computers use en-US). Only suggest
// a country when the device timezone matches one bundled country mapping.
try {
const canonical = zone => new Intl.DateTimeFormat('en', { timeZone: zone }).resolvedOptions().timeZone;
const matches = Object.entries(data.timezones).filter(([, zone]) => canonical(zone) === canonical(timezone));
if (matches.length === 1 && data.countries.some(row => row[2] === matches[0][0])) {
country.value = matches[0][0]; updateCountry();
}
} catch (_) { /* Country remains an explicit choice. */ }
country.addEventListener('change', updateCountry);
const ids = ['qBusinessName', 'qEmail', 'qPassword', 'qConfirmPassword', 'qCountry', 'qState', 'qCity', 'qCurrency', 'qTimezone'];
ids.forEach(id => {
const input = field(id);
const error = document.createElement('div');
error.id = id + 'Error'; error.className = 'setup-field-error'; error.hidden = true;
input.setAttribute('aria-describedby', error.id);
input.insertAdjacentElement('afterend', error);
input.addEventListener('input', () => { error.hidden = true; input.removeAttribute('aria-invalid'); });
});
function values() {
const row = data.countries.find(row => row[2] === country.value);
return { register_country: country.value, register_countryid: row ? row[0] : '',
register_state: field('qState').value.trim(), register_city: field('qCity').value.trim(),
register_currency: currency.value, register_timezone: field('qTimezone').value.trim() };
}
function validate() {
const messages = {};
if (field('qConfirmPassword').value && field('qConfirmPassword').value !== field('qPassword').value) messages.qConfirmPassword = 'Passwords do not match.';
const required = { qBusinessName: 'Enter your shop name.', qEmail: 'Enter your login email.', qPassword: 'Enter a password.', qConfirmPassword: 'Confirm your password.',
qCountry: 'Select the country where your shop is located.', qCity: 'Enter your city or town.',
qCurrency: 'Select your shop currency.', qTimezone: 'Select a valid timezone.' };
if (field('qState').required) required.qState = 'Enter your state or province.';
Object.entries(required).forEach(([id, message]) => { if (!field(id).value.trim()) messages[id] = message; });
if (field('qEmail').value.trim() && !field('qEmail').validity.valid) messages.qEmail = 'Enter a valid email address, for example name@example.com.';
if (field('qPassword').value && field('qPassword').value.length < 8) messages.qPassword = 'Use at least 8 characters for your password.';
const row = data.countries.find(row => row[2] === country.value);
const states = row ? data.states.filter(state => state[0] === row[0]).map(state => state[1]) : [];
if (states.length && field('qState').value.trim()) {
const match = states.find(state => state.toLowerCase() === field('qState').value.trim().toLowerCase());
if (match) field('qState').value = match;
else messages.qState = 'Choose a state or province from the suggestions for this country.';
}
if (field('qTimezone').value.trim()) {
try { new Intl.DateTimeFormat('en', { timeZone: field('qTimezone').value.trim() }).format(); }
catch (_) { messages.qTimezone = 'Choose a valid timezone, for example Asia/Kolkata.'; }
}
ids.forEach(id => { const error = field(id + 'Error'); error.textContent = messages[id] || ''; error.hidden = !messages[id];
if (messages[id]) field(id).setAttribute('aria-invalid', 'true'); else field(id).removeAttribute('aria-invalid'); });
const first = ids.find(id => messages[id]);
if (first) { field('qError').textContent = 'Please correct the highlighted fields.'; field('qError').style.display = 'block'; field(first).focus(); }
if (!first) { field('qError').textContent = ''; field('qError').style.display = 'none'; }
return !first;
}
window.PosnicSetupRegion = { values, validate };
})();
Loading
Loading