Add shift planning, month closure + payroll, and a dark modern redesign
Ports two modules from the friend's Google Apps Script build (kept as reference in gscript/) onto the TypeScript stack, rewritten cleanly against this app's own data model rather than copied 1:1: - Shift planning: weekly template (Sun-Thu evening, Fri/Sat two slots), lazy idempotent generation per period (no cron needed), employee signup/cancel with collision + capacity checks, admin calendar view with slot editing and manual assignment. - Month closure + payroll: employee confirms the month (blocked while a shift is still open), admin locks and finalizes pay (base hours * rate + tips/bonus/other), reopen to undo a premature lock, mark paid. Pay rates are versioned by date, defaulting the first-ever rate to apply retroactively to the employee's whole history. - A shift left open more than 12h (forgotten clock-out) is auto-closed at clock_in + 12h, checked lazily on read instead of a background job. - Full dark, sharp-edged modern restyle (theme.css replaces tui.css) with an amber accent, keeping every existing class name so no component logic needed to change. Backend test coverage (jest) for all three workflows: shift planning, closure/payroll, and the forgotten-clock-out auto-close.
This commit is contained in:
128
gscript/Setup.js
Normal file
128
gscript/Setup.js
Normal file
@@ -0,0 +1,128 @@
|
||||
function setupSystem() {
|
||||
const props = PropertiesService.getScriptProperties();
|
||||
let id = props.getProperty('DB_SPREADSHEET_ID');
|
||||
let ss;
|
||||
|
||||
if (id) {
|
||||
ss = SpreadsheetApp.openById(id);
|
||||
} else {
|
||||
ss = SpreadsheetApp.create('MeStaff Database');
|
||||
props.setProperty('DB_SPREADSHEET_ID', ss.getId());
|
||||
}
|
||||
|
||||
Object.keys(CFG.SHEETS).forEach(k => {
|
||||
const name = CFG.SHEETS[k];
|
||||
let s = ss.getSheetByName(name);
|
||||
if (!s) s = ss.insertSheet(name);
|
||||
const headers = HEADERS[k];
|
||||
if (s.getLastRow() === 0) s.getRange(1,1,1,headers.length).setValues([headers]);
|
||||
s.setFrozenRows(1);
|
||||
s.getRange(1,1,1,headers.length).setFontWeight('bold');
|
||||
});
|
||||
|
||||
const defaultSheet = ss.getSheetByName('Sheet1') || ss.getSheetByName('List1');
|
||||
if (defaultSheet && Object.values(CFG.SHEETS).indexOf(defaultSheet.getName()) === -1) {
|
||||
ss.deleteSheet(defaultSheet);
|
||||
}
|
||||
|
||||
if (!props.getProperty('PASSWORD_PEPPER')) {
|
||||
props.setProperty('PASSWORD_PEPPER', Utilities.getUuid() + Utilities.getUuid());
|
||||
}
|
||||
|
||||
seedSettings_();
|
||||
return {ok:true, spreadsheetId:ss.getId(), url:ss.getUrl()};
|
||||
}
|
||||
|
||||
function seedSettings_() {
|
||||
const current = rows_(CFG.SHEETS.SETTINGS);
|
||||
const map = {};
|
||||
current.forEach(r => map[r.key] = r.value);
|
||||
const defaults = {
|
||||
APP_NAME: 'MeStaff',
|
||||
CURRENCY: 'CZK',
|
||||
TIMEZONE: CFG.TZ,
|
||||
NIGHT_START: '22:00',
|
||||
NIGHT_END: '06:00'
|
||||
};
|
||||
Object.keys(defaults).forEach(k => {
|
||||
if (map[k] === undefined) append_(CFG.SHEETS.SETTINGS, {key:k, value:defaults[k]});
|
||||
});
|
||||
}
|
||||
|
||||
function seedFirstAdmin(email, firstName, lastName) {
|
||||
email = emailNorm_(email);
|
||||
if (!email) throw new Error('Zadej e-mail.');
|
||||
const lock = LockService.getScriptLock();
|
||||
lock.waitLock(10000);
|
||||
try {
|
||||
let company = findOne_(CFG.SHEETS.COMPANIES, r => r.active === true || String(r.active).toUpperCase() === 'TRUE');
|
||||
if (!company) {
|
||||
company = {company_id:'COMPANY_MAIN', name:'Hlavní společnost', ico:'', active:true};
|
||||
append_(CFG.SHEETS.COMPANIES, company);
|
||||
}
|
||||
let loc = findOne_(CFG.SHEETS.LOCATIONS, r => String(r.company_id) === String(company.company_id));
|
||||
if (!loc) {
|
||||
loc = {location_id:'LOCATION_MAIN', company_id:company.company_id, name:'Hlavní provozovna', active:true};
|
||||
append_(CFG.SHEETS.LOCATIONS, loc);
|
||||
}
|
||||
let emp = findOne_(CFG.SHEETS.EMPLOYEES, r => emailNorm_(r.email) === email);
|
||||
if (!emp) {
|
||||
emp = {
|
||||
employee_id: uuid_('EMP'),
|
||||
first_name:firstName || 'Admin',
|
||||
last_name:lastName || '',
|
||||
email,
|
||||
company_id:company.company_id,
|
||||
location_id:loc.location_id,
|
||||
position:'Administrátor',
|
||||
employment_type:'',
|
||||
terminal_pin:'',
|
||||
start_date:isoDate_(now_()),
|
||||
end_date:'',
|
||||
active:true
|
||||
};
|
||||
append_(CFG.SHEETS.EMPLOYEES, emp);
|
||||
}
|
||||
let user = findOne_(CFG.SHEETS.USERS, r => emailNorm_(r.email) === email);
|
||||
if (!user) {
|
||||
user = {
|
||||
user_id:uuid_('USR'),
|
||||
email,
|
||||
role:'ADMIN',
|
||||
employee_id:emp.employee_id,
|
||||
password_salt:'',
|
||||
password_hash:'',
|
||||
active:true,
|
||||
created_at:now_(),
|
||||
last_login:''
|
||||
};
|
||||
append_(CFG.SHEETS.USERS, user);
|
||||
}
|
||||
return {ok:true, email, message:'Admin založen. Teď použij „Aktivovat účet“ na přihlašovací stránce.'};
|
||||
} finally {
|
||||
lock.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
function createSystemTriggers() {
|
||||
ScriptApp.getProjectTriggers().forEach(t => {
|
||||
if (['nightlyMaintenance','monthlyMaintenance'].includes(t.getHandlerFunction())) {
|
||||
ScriptApp.deleteTrigger(t);
|
||||
}
|
||||
});
|
||||
|
||||
ScriptApp.newTrigger('nightlyMaintenance')
|
||||
.timeBased().everyDays(1).atHour(3).inTimezone(CFG.TZ).create();
|
||||
|
||||
ScriptApp.newTrigger('monthlyMaintenance')
|
||||
.timeBased().onMonthDay(1).atHour(4).inTimezone(CFG.TZ).create();
|
||||
|
||||
return {ok:true};
|
||||
}
|
||||
function createMyAdmin() {
|
||||
return seedFirstAdmin(
|
||||
'filip.thurrigl@gmail.com',
|
||||
'Philipp',
|
||||
'Thürrigl'
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user