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:
72
gscript/Database.js
Normal file
72
gscript/Database.js
Normal file
@@ -0,0 +1,72 @@
|
||||
function db_() {
|
||||
const id = PropertiesService.getScriptProperties().getProperty('DB_SPREADSHEET_ID');
|
||||
if (!id) throw new Error('Databáze není inicializovaná. Spusť setupSystem().');
|
||||
return SpreadsheetApp.openById(id);
|
||||
}
|
||||
|
||||
function sh_(name) {
|
||||
const s = db_().getSheetByName(name);
|
||||
if (!s) throw new Error('Chybí list ' + name);
|
||||
return s;
|
||||
}
|
||||
|
||||
function rows_(name) {
|
||||
const s = sh_(name);
|
||||
const values = s.getDataRange().getValues();
|
||||
if (values.length < 2) return [];
|
||||
const headers = values[0].map(String);
|
||||
return values.slice(1).filter(r => r.some(v => v !== '')).map(r => {
|
||||
const o = {};
|
||||
headers.forEach((h, i) => o[h] = r[i]);
|
||||
return o;
|
||||
});
|
||||
}
|
||||
|
||||
function append_(name, obj) {
|
||||
const s = sh_(name);
|
||||
const headers = s.getRange(1,1,1,s.getLastColumn()).getValues()[0];
|
||||
s.appendRow(headers.map(h => obj[h] !== undefined ? obj[h] : ''));
|
||||
return obj;
|
||||
}
|
||||
|
||||
function updateBy_(name, key, value, patch) {
|
||||
const s = sh_(name);
|
||||
const data = s.getDataRange().getValues();
|
||||
if (!data.length) return false;
|
||||
const headers = data[0].map(String);
|
||||
const keyCol = headers.indexOf(key);
|
||||
if (keyCol < 0) throw new Error('Sloupec ' + key + ' neexistuje v ' + name);
|
||||
const row = data.slice(1).findIndex(r => String(r[keyCol]) === String(value));
|
||||
if (row < 0) return false;
|
||||
Object.keys(patch).forEach(k => {
|
||||
const c = headers.indexOf(k);
|
||||
if (c >= 0) s.getRange(row + 2, c + 1).setValue(patch[k]);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function findOne_(name, predicate) {
|
||||
return rows_(name).find(predicate) || null;
|
||||
}
|
||||
|
||||
function uuid_(prefix) {
|
||||
return (prefix || 'ID') + '_' + Utilities.getUuid().replace(/-/g,'').slice(0,16).toUpperCase();
|
||||
}
|
||||
|
||||
function now_() { return new Date(); }
|
||||
|
||||
function isoDate_(d) {
|
||||
return Utilities.formatDate(new Date(d), CFG.TZ, 'yyyy-MM-dd');
|
||||
}
|
||||
|
||||
function period_(d) {
|
||||
return Utilities.formatDate(new Date(d), CFG.TZ, 'yyyy-MM');
|
||||
}
|
||||
|
||||
function emailNorm_(email) {
|
||||
return String(email || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function json_(v) {
|
||||
try { return JSON.stringify(v); } catch(e) { return String(v); }
|
||||
}
|
||||
Reference in New Issue
Block a user