function clockAction(token, action) { const user = requireUser_(token,['EMPLOYEE','MANAGER','ADMIN']); if (!user.employee_id) throw new Error('Účet není propojený se zaměstnancem.'); return clockForEmployee_(user.employee_id, action, 'PORTAL', user.user_id); } function terminalClock(employeeId, pin, action, locationId) { const emp = findOne_(CFG.SHEETS.EMPLOYEES, r => String(r.employee_id) === String(employeeId)); if (!emp || String(emp.terminal_pin) !== String(pin)) throw new Error('Neplatný PIN.'); if (!(emp.active === true || String(emp.active).toUpperCase() === 'TRUE')) throw new Error('Zaměstnanec není aktivní.'); return clockForEmployee_(employeeId, action, 'TERMINAL', 'TERMINAL:'+String(locationId || emp.location_id)); } function clockForEmployee_(employeeId, action, source, createdBy) { const allowed = ['CLOCK_IN','BREAK_START','BREAK_END','CLOCK_OUT']; if (!allowed.includes(action)) throw new Error('Neplatná akce.'); const lock = LockService.getScriptLock(); lock.waitLock(10000); try { const open = getOpenShift_(employeeId); const t = now_(); if (action === 'CLOCK_IN') { if (open) throw new Error('Směna už probíhá.'); const emp = findOne_(CFG.SHEETS.EMPLOYEES, r => String(r.employee_id) === String(employeeId)); const shift = { shift_id:uuid_('SHIFT'), employee_id:employeeId, work_date:isoDate_(t), clock_in:t, clock_out:'', break_minutes:0, worked_minutes:0, night_minutes:0, weekend_minutes:0, holiday_minutes:0, status:'OPEN', location_id:emp ? emp.location_id : '', created_at:t, updated_at:t }; append_(CFG.SHEETS.SHIFTS,shift); attendanceEvent_(employeeId,t,action,shift.location_id,source,createdBy); return {ok:true,status:'OPEN',shift}; } if (!open) throw new Error('Žádná otevřená směna.'); const events = rows_(CFG.SHEETS.ATTENDANCE_EVENTS) .filter(e => String(e.employee_id) === String(employeeId) && new Date(e.timestamp) >= new Date(open.clock_in)) .sort((a,b) => new Date(a.timestamp)-new Date(b.timestamp)); const last = events[events.length-1]; if (action === 'BREAK_START' && last && String(last.event_type) === 'BREAK_START') throw new Error('Pauza už běží.'); if (action === 'BREAK_END' && (!last || String(last.event_type) !== 'BREAK_START')) throw new Error('Pauza neběží.'); if (action === 'CLOCK_OUT' && last && String(last.event_type) === 'BREAK_START') throw new Error('Nejdřív ukonči pauzu.'); attendanceEvent_(employeeId,t,action,open.location_id,source,createdBy); if (action === 'CLOCK_OUT') { const allEvents = events.concat([{timestamp:t,event_type:'CLOCK_OUT'}]); const calc = calculateShift_(new Date(open.clock_in),t,allEvents); updateBy_(CFG.SHEETS.SHIFTS,'shift_id',open.shift_id,{ clock_out:t, break_minutes:calc.breakMinutes, worked_minutes:calc.workedMinutes, night_minutes:calc.nightMinutes, weekend_minutes:calc.weekendMinutes, status:'COMPLETED', updated_at:t }); return {ok:true,status:'COMPLETED',shift_id:open.shift_id,worked_minutes:calc.workedMinutes}; } return {ok:true,status:action,shift_id:open.shift_id}; } finally { lock.releaseLock(); } } function attendanceEvent_(employeeId,timestamp,type,locationId,source,createdBy) { append_(CFG.SHEETS.ATTENDANCE_EVENTS,{ event_id:uuid_('EVT'), employee_id:employeeId, timestamp, event_type:type, location_id:locationId || '', source:source || '', created_by:createdBy || '' }); } function getOpenShift_(employeeId) { const list = rows_(CFG.SHEETS.SHIFTS) .filter(s => String(s.employee_id) === String(employeeId) && String(s.status) === 'OPEN') .sort((a,b) => new Date(b.clock_in)-new Date(a.clock_in)); return list[0] || null; } function calculateShift_(clockIn, clockOut, events) { let breakMinutes = 0, breakStart = null; (events || []).forEach(e => { if (String(e.event_type) === 'BREAK_START') breakStart = new Date(e.timestamp); if (String(e.event_type) === 'BREAK_END' && breakStart) { breakMinutes += Math.max(0, Math.round((new Date(e.timestamp)-breakStart)/60000)); breakStart = null; } }); const total = Math.max(0, Math.round((clockOut-clockIn)/60000)); const worked = Math.max(0,total-breakMinutes); // Základní výpočet nočních minut 22:00–06:00 po jednotlivých minutách. let night = 0, weekend = 0; let p = new Date(clockIn); while (p < clockOut) { const n = new Date(Math.min(p.getTime()+60000, clockOut.getTime())); const hh = Number(Utilities.formatDate(p,CFG.TZ,'H')); const dow = Number(Utilities.formatDate(p,CFG.TZ,'u')); if (hh >= 22 || hh < 6) night += (n-p)/60000; if (dow >= 6) weekend += (n-p)/60000; p = n; } return { breakMinutes:Math.round(breakMinutes), workedMinutes:Math.round(worked), nightMinutes:Math.round(night), weekendMinutes:Math.round(weekend) }; }