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.
1345 lines
18 KiB
JavaScript
1345 lines
18 KiB
JavaScript
/* ============================================================
|
||
EATME PORTÁL – ATTENDANCE CORRECTION ADMIN SERVICE
|
||
|
||
Admin / manager:
|
||
- seznam čekajících žádostí o opravu docházky
|
||
- schválení -> přepis SHIFTS + přepočet minut
|
||
- zamítnutí -> pouze změna stavu žádosti
|
||
- uzamčenou směnu nelze měnit
|
||
============================================================ */
|
||
|
||
|
||
/* ============================================================
|
||
SEZNAM ŽÁDOSTÍ
|
||
============================================================ */
|
||
|
||
function getPendingAttendanceCorrections(token) {
|
||
|
||
requireUser_(
|
||
token,
|
||
[
|
||
'ADMIN',
|
||
'MANAGER'
|
||
]
|
||
);
|
||
|
||
|
||
const requests =
|
||
displayRows_(
|
||
CFG.SHEETS.SHIFT_CHANGE_REQUESTS
|
||
);
|
||
|
||
|
||
const shifts =
|
||
displayRows_(
|
||
CFG.SHEETS.SHIFTS
|
||
);
|
||
|
||
|
||
const employees =
|
||
displayRows_(
|
||
CFG.SHEETS.EMPLOYEES
|
||
);
|
||
|
||
|
||
const shiftMap =
|
||
{};
|
||
|
||
|
||
shifts.forEach(
|
||
function(shift) {
|
||
|
||
shiftMap[
|
||
String(
|
||
shift.shift_id ||
|
||
''
|
||
)
|
||
] =
|
||
shift;
|
||
|
||
}
|
||
);
|
||
|
||
|
||
const employeeMap =
|
||
{};
|
||
|
||
|
||
employees.forEach(
|
||
function(employee) {
|
||
|
||
employeeMap[
|
||
String(
|
||
employee.employee_id ||
|
||
''
|
||
)
|
||
] =
|
||
employee;
|
||
|
||
}
|
||
);
|
||
|
||
|
||
/*
|
||
* Jedna oprava může být v SHIFT_CHANGE_REQUESTS uložena
|
||
* jako více řádků (např. clock_in + clock_out).
|
||
* Seskládáme je podle request_id.
|
||
*/
|
||
|
||
const groups =
|
||
{};
|
||
|
||
|
||
requests.forEach(
|
||
function(request,index) {
|
||
|
||
if (
|
||
String(
|
||
request.status ||
|
||
''
|
||
).trim() !==
|
||
'PENDING'
|
||
) {
|
||
|
||
return;
|
||
|
||
}
|
||
|
||
|
||
const groupId =
|
||
String(
|
||
request.request_id ||
|
||
''
|
||
) ||
|
||
(
|
||
'ROW_' +
|
||
index
|
||
);
|
||
|
||
|
||
if (
|
||
!groups[
|
||
groupId
|
||
]
|
||
) {
|
||
|
||
groups[
|
||
groupId
|
||
] =
|
||
{
|
||
|
||
request_id:
|
||
groupId,
|
||
|
||
shift_id:
|
||
String(
|
||
request.shift_id ||
|
||
''
|
||
),
|
||
|
||
employee_id:
|
||
String(
|
||
request.employee_id ||
|
||
''
|
||
),
|
||
|
||
reason:
|
||
String(
|
||
request.reason ||
|
||
''
|
||
),
|
||
|
||
created_at:
|
||
String(
|
||
request.created_at ||
|
||
''
|
||
),
|
||
|
||
fields:
|
||
[],
|
||
|
||
rows:
|
||
[]
|
||
|
||
};
|
||
|
||
}
|
||
|
||
|
||
groups[
|
||
groupId
|
||
].rows.push(
|
||
request
|
||
);
|
||
|
||
|
||
groups[
|
||
groupId
|
||
].fields.push(
|
||
{
|
||
|
||
field:
|
||
String(
|
||
request.field ||
|
||
''
|
||
),
|
||
|
||
old_value:
|
||
String(
|
||
request.old_value ||
|
||
''
|
||
),
|
||
|
||
requested_value:
|
||
String(
|
||
request.requested_value ||
|
||
''
|
||
)
|
||
|
||
}
|
||
);
|
||
|
||
|
||
if (
|
||
!groups[
|
||
groupId
|
||
].reason &&
|
||
request.reason
|
||
) {
|
||
|
||
groups[
|
||
groupId
|
||
].reason =
|
||
String(
|
||
request.reason
|
||
);
|
||
|
||
}
|
||
|
||
}
|
||
);
|
||
|
||
|
||
const result =
|
||
Object.keys(
|
||
groups
|
||
)
|
||
.map(
|
||
function(groupId) {
|
||
|
||
const group =
|
||
groups[
|
||
groupId
|
||
];
|
||
|
||
|
||
const shift =
|
||
shiftMap[
|
||
String(
|
||
group.shift_id
|
||
)
|
||
] ||
|
||
null;
|
||
|
||
|
||
const employeeId =
|
||
group.employee_id ||
|
||
(
|
||
shift
|
||
? String(
|
||
shift.employee_id ||
|
||
''
|
||
)
|
||
: ''
|
||
);
|
||
|
||
|
||
const employee =
|
||
employeeMap[
|
||
employeeId
|
||
] ||
|
||
null;
|
||
|
||
|
||
const requested =
|
||
attendanceCorrectionRequestedValues_(
|
||
group.fields
|
||
);
|
||
|
||
|
||
return {
|
||
|
||
request_id:
|
||
group.request_id,
|
||
|
||
shift_id:
|
||
group.shift_id,
|
||
|
||
employee_id:
|
||
employeeId,
|
||
|
||
employee_name:
|
||
employee
|
||
? (
|
||
String(
|
||
employee.first_name ||
|
||
''
|
||
) +
|
||
' ' +
|
||
String(
|
||
employee.last_name ||
|
||
''
|
||
)
|
||
).trim()
|
||
: employeeId,
|
||
|
||
work_date:
|
||
shift
|
||
? String(
|
||
shift.work_date ||
|
||
''
|
||
)
|
||
: '',
|
||
|
||
current_clock_in:
|
||
shift
|
||
? String(
|
||
shift.clock_in ||
|
||
''
|
||
)
|
||
: '',
|
||
|
||
current_clock_out:
|
||
shift
|
||
? String(
|
||
shift.clock_out ||
|
||
''
|
||
)
|
||
: '',
|
||
|
||
requested_clock_in:
|
||
requested.clock_in,
|
||
|
||
requested_clock_out:
|
||
requested.clock_out,
|
||
|
||
reason:
|
||
group.reason,
|
||
|
||
created_at:
|
||
group.created_at,
|
||
|
||
shift_status:
|
||
shift
|
||
? String(
|
||
shift.status ||
|
||
''
|
||
)
|
||
: '',
|
||
|
||
locked:
|
||
shift
|
||
? String(
|
||
shift.status ||
|
||
''
|
||
).toUpperCase() ===
|
||
'LOCKED'
|
||
: false
|
||
|
||
};
|
||
|
||
}
|
||
)
|
||
.sort(
|
||
function(a,b) {
|
||
|
||
return String(
|
||
b.created_at ||
|
||
''
|
||
).localeCompare(
|
||
String(
|
||
a.created_at ||
|
||
''
|
||
)
|
||
);
|
||
|
||
}
|
||
);
|
||
|
||
|
||
return {
|
||
|
||
count:
|
||
result.length,
|
||
|
||
rows:
|
||
result
|
||
|
||
};
|
||
|
||
}
|
||
|
||
|
||
|
||
/* ============================================================
|
||
SCHVÁLENÍ ŽÁDOSTI
|
||
============================================================ */
|
||
|
||
function approveAttendanceCorrection(
|
||
token,
|
||
requestId
|
||
) {
|
||
|
||
const user =
|
||
requireUser_(
|
||
token,
|
||
[
|
||
'ADMIN',
|
||
'MANAGER'
|
||
]
|
||
);
|
||
|
||
|
||
const lock =
|
||
LockService
|
||
.getScriptLock();
|
||
|
||
|
||
lock.waitLock(
|
||
10000
|
||
);
|
||
|
||
|
||
try {
|
||
|
||
const requests =
|
||
displayRows_(
|
||
CFG.SHEETS.SHIFT_CHANGE_REQUESTS
|
||
);
|
||
|
||
|
||
const requestRows =
|
||
requests.filter(
|
||
function(request) {
|
||
|
||
return (
|
||
String(
|
||
request.request_id ||
|
||
''
|
||
) ===
|
||
String(
|
||
requestId
|
||
) &&
|
||
|
||
String(
|
||
request.status ||
|
||
''
|
||
).trim() ===
|
||
'PENDING'
|
||
);
|
||
|
||
}
|
||
);
|
||
|
||
|
||
if (
|
||
!requestRows.length
|
||
) {
|
||
|
||
throw new Error(
|
||
'Žádost nebyla nalezena nebo už byla vyřešena.'
|
||
);
|
||
|
||
}
|
||
|
||
|
||
const shiftId =
|
||
String(
|
||
requestRows[0].shift_id ||
|
||
''
|
||
);
|
||
|
||
|
||
const shifts =
|
||
displayRows_(
|
||
CFG.SHEETS.SHIFTS
|
||
);
|
||
|
||
|
||
const shift =
|
||
shifts.find(
|
||
function(row) {
|
||
|
||
return (
|
||
String(
|
||
row.shift_id ||
|
||
''
|
||
) ===
|
||
shiftId
|
||
);
|
||
|
||
}
|
||
);
|
||
|
||
|
||
if (
|
||
!shift
|
||
) {
|
||
|
||
throw new Error(
|
||
'Směna uvedená v žádosti nebyla nalezena.'
|
||
);
|
||
|
||
}
|
||
|
||
|
||
if (
|
||
String(
|
||
shift.status ||
|
||
''
|
||
).toUpperCase() ===
|
||
'LOCKED'
|
||
) {
|
||
|
||
throw new Error(
|
||
'Tato směna je už uzamčená a předaná do mzdy. Nejdřív je nutné řešit mzdovou uzávěrku.'
|
||
);
|
||
|
||
}
|
||
|
||
|
||
const requested =
|
||
attendanceCorrectionRequestedValues_(
|
||
requestRows.map(
|
||
function(request) {
|
||
|
||
return {
|
||
|
||
field:
|
||
request.field,
|
||
|
||
old_value:
|
||
request.old_value,
|
||
|
||
requested_value:
|
||
request.requested_value
|
||
|
||
};
|
||
|
||
}
|
||
)
|
||
);
|
||
|
||
|
||
let newClockIn =
|
||
requested.clock_in ||
|
||
String(
|
||
shift.clock_in ||
|
||
''
|
||
);
|
||
|
||
|
||
let newClockOut =
|
||
requested.clock_out ||
|
||
String(
|
||
shift.clock_out ||
|
||
''
|
||
);
|
||
|
||
|
||
const clockInDate =
|
||
parseAttendanceCorrectionDate_(
|
||
newClockIn
|
||
);
|
||
|
||
|
||
const clockOutDate =
|
||
parseAttendanceCorrectionDate_(
|
||
newClockOut
|
||
);
|
||
|
||
|
||
if (
|
||
!clockInDate
|
||
) {
|
||
|
||
throw new Error(
|
||
'Navrhovaný čas příchodu není platný.'
|
||
);
|
||
|
||
}
|
||
|
||
|
||
if (
|
||
newClockOut &&
|
||
!clockOutDate
|
||
) {
|
||
|
||
throw new Error(
|
||
'Navrhovaný čas odchodu není platný.'
|
||
);
|
||
|
||
}
|
||
|
||
|
||
if (
|
||
clockOutDate &&
|
||
clockOutDate <=
|
||
clockInDate
|
||
) {
|
||
|
||
throw new Error(
|
||
'Odchod musí být později než příchod.'
|
||
);
|
||
|
||
}
|
||
|
||
|
||
const breakMinutes =
|
||
correctionNumber_(
|
||
shift.break_minutes
|
||
);
|
||
|
||
|
||
let workedMinutes =
|
||
correctionNumber_(
|
||
shift.worked_minutes
|
||
);
|
||
|
||
|
||
let nightMinutes =
|
||
correctionNumber_(
|
||
shift.night_minutes
|
||
);
|
||
|
||
|
||
if (
|
||
clockOutDate
|
||
) {
|
||
|
||
workedMinutes =
|
||
Math.max(
|
||
0,
|
||
Math.round(
|
||
(
|
||
clockOutDate.getTime() -
|
||
clockInDate.getTime()
|
||
) /
|
||
60000
|
||
) -
|
||
breakMinutes
|
||
);
|
||
|
||
|
||
nightMinutes =
|
||
calculateCorrectionNightMinutes_(
|
||
clockInDate,
|
||
clockOutDate
|
||
);
|
||
|
||
}
|
||
|
||
|
||
const workDate =
|
||
Utilities.formatDate(
|
||
clockInDate,
|
||
CFG.TZ,
|
||
'yyyy-MM-dd'
|
||
);
|
||
|
||
|
||
const update =
|
||
{
|
||
|
||
clock_in:
|
||
formatAttendanceCorrectionDate_(
|
||
clockInDate
|
||
),
|
||
|
||
work_date:
|
||
workDate,
|
||
|
||
worked_minutes:
|
||
workedMinutes,
|
||
|
||
night_minutes:
|
||
nightMinutes,
|
||
|
||
updated_at:
|
||
now_()
|
||
|
||
};
|
||
|
||
|
||
if (
|
||
clockOutDate
|
||
) {
|
||
|
||
update.clock_out =
|
||
formatAttendanceCorrectionDate_(
|
||
clockOutDate
|
||
);
|
||
|
||
|
||
/*
|
||
* Pokud měla směna odchod a není uzamčená,
|
||
* má být po opravě uzavřená.
|
||
*/
|
||
|
||
if (
|
||
String(
|
||
shift.status ||
|
||
''
|
||
) ===
|
||
'OPEN'
|
||
) {
|
||
|
||
update.status =
|
||
'COMPLETED';
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
updateBy_(
|
||
CFG.SHEETS.SHIFTS,
|
||
'shift_id',
|
||
shiftId,
|
||
update
|
||
);
|
||
|
||
|
||
requestRows.forEach(
|
||
function(request) {
|
||
|
||
updateBy_(
|
||
CFG.SHEETS.SHIFT_CHANGE_REQUESTS,
|
||
'request_id',
|
||
request.request_id,
|
||
{
|
||
|
||
status:
|
||
'APPROVED',
|
||
|
||
resolved_at:
|
||
now_(),
|
||
|
||
resolved_by:
|
||
user.user_id
|
||
|
||
}
|
||
);
|
||
|
||
}
|
||
);
|
||
|
||
|
||
audit_(
|
||
user.user_id,
|
||
'ATTENDANCE_CORRECTION_APPROVED',
|
||
'SHIFT',
|
||
shiftId,
|
||
{
|
||
|
||
clock_in:
|
||
shift.clock_in,
|
||
|
||
clock_out:
|
||
shift.clock_out,
|
||
|
||
worked_minutes:
|
||
shift.worked_minutes
|
||
|
||
},
|
||
{
|
||
|
||
clock_in:
|
||
update.clock_in,
|
||
|
||
clock_out:
|
||
update.clock_out ||
|
||
shift.clock_out,
|
||
|
||
worked_minutes:
|
||
workedMinutes
|
||
|
||
}
|
||
);
|
||
|
||
|
||
notifyEmployee_(
|
||
shift.employee_id,
|
||
'Oprava docházky schválena',
|
||
'Tvoje žádost o opravu docházky ze dne ' +
|
||
workDate +
|
||
' byla schválena.'
|
||
);
|
||
|
||
|
||
return {
|
||
|
||
ok:
|
||
true,
|
||
|
||
shift_id:
|
||
shiftId,
|
||
|
||
worked_minutes:
|
||
workedMinutes
|
||
|
||
};
|
||
|
||
}
|
||
|
||
finally {
|
||
|
||
lock.releaseLock();
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
/* ============================================================
|
||
ZAMÍTNUTÍ ŽÁDOSTI
|
||
============================================================ */
|
||
|
||
function rejectAttendanceCorrection(
|
||
token,
|
||
requestId
|
||
) {
|
||
|
||
const user =
|
||
requireUser_(
|
||
token,
|
||
[
|
||
'ADMIN',
|
||
'MANAGER'
|
||
]
|
||
);
|
||
|
||
|
||
const requests =
|
||
displayRows_(
|
||
CFG.SHEETS.SHIFT_CHANGE_REQUESTS
|
||
);
|
||
|
||
|
||
const requestRows =
|
||
requests.filter(
|
||
function(request) {
|
||
|
||
return (
|
||
String(
|
||
request.request_id ||
|
||
''
|
||
) ===
|
||
String(
|
||
requestId
|
||
) &&
|
||
|
||
String(
|
||
request.status ||
|
||
''
|
||
).trim() ===
|
||
'PENDING'
|
||
);
|
||
|
||
}
|
||
);
|
||
|
||
|
||
if (
|
||
!requestRows.length
|
||
) {
|
||
|
||
throw new Error(
|
||
'Žádost nebyla nalezena nebo už byla vyřešena.'
|
||
);
|
||
|
||
}
|
||
|
||
|
||
requestRows.forEach(
|
||
function(request) {
|
||
|
||
updateBy_(
|
||
CFG.SHEETS.SHIFT_CHANGE_REQUESTS,
|
||
'request_id',
|
||
request.request_id,
|
||
{
|
||
|
||
status:
|
||
'REJECTED',
|
||
|
||
resolved_at:
|
||
now_(),
|
||
|
||
resolved_by:
|
||
user.user_id
|
||
|
||
}
|
||
);
|
||
|
||
}
|
||
);
|
||
|
||
|
||
const shiftId =
|
||
String(
|
||
requestRows[0].shift_id ||
|
||
''
|
||
);
|
||
|
||
|
||
const employeeId =
|
||
String(
|
||
requestRows[0].employee_id ||
|
||
''
|
||
);
|
||
|
||
|
||
audit_(
|
||
user.user_id,
|
||
'ATTENDANCE_CORRECTION_REJECTED',
|
||
'SHIFT',
|
||
shiftId,
|
||
'',
|
||
''
|
||
);
|
||
|
||
|
||
if (
|
||
employeeId
|
||
) {
|
||
|
||
notifyEmployee_(
|
||
employeeId,
|
||
'Oprava docházky zamítnuta',
|
||
'Tvoje žádost o opravu docházky byla zamítnuta.'
|
||
);
|
||
|
||
}
|
||
|
||
|
||
return {
|
||
ok:true
|
||
};
|
||
|
||
}
|
||
|
||
|
||
|
||
/* ============================================================
|
||
REQUESTED VALUES – ROBUSTNÍ PARSER
|
||
============================================================ */
|
||
|
||
function attendanceCorrectionRequestedValues_(
|
||
fields
|
||
) {
|
||
|
||
const result =
|
||
{
|
||
|
||
clock_in:
|
||
'',
|
||
|
||
clock_out:
|
||
''
|
||
|
||
};
|
||
|
||
|
||
(
|
||
fields ||
|
||
[]
|
||
).forEach(
|
||
function(item) {
|
||
|
||
const field =
|
||
String(
|
||
item.field ||
|
||
''
|
||
)
|
||
.trim()
|
||
.toLowerCase();
|
||
|
||
|
||
const value =
|
||
String(
|
||
item.requested_value ||
|
||
''
|
||
).trim();
|
||
|
||
|
||
if (
|
||
field ===
|
||
'clock_in'
|
||
) {
|
||
|
||
result.clock_in =
|
||
value;
|
||
|
||
return;
|
||
|
||
}
|
||
|
||
|
||
if (
|
||
field ===
|
||
'clock_out'
|
||
) {
|
||
|
||
result.clock_out =
|
||
value;
|
||
|
||
return;
|
||
|
||
}
|
||
|
||
|
||
/*
|
||
* Pro jistotu podporujeme i případ, že requested_value
|
||
* obsahuje JSON s oběma časy.
|
||
*/
|
||
|
||
if (
|
||
value &&
|
||
(
|
||
value.charAt(
|
||
0
|
||
) ===
|
||
'{'
|
||
)
|
||
) {
|
||
|
||
try {
|
||
|
||
const parsed =
|
||
JSON.parse(
|
||
value
|
||
);
|
||
|
||
|
||
if (
|
||
parsed.clock_in
|
||
) {
|
||
|
||
result.clock_in =
|
||
String(
|
||
parsed.clock_in
|
||
);
|
||
|
||
}
|
||
|
||
|
||
if (
|
||
parsed.clock_out
|
||
) {
|
||
|
||
result.clock_out =
|
||
String(
|
||
parsed.clock_out
|
||
);
|
||
|
||
}
|
||
|
||
}
|
||
|
||
catch(error) {
|
||
|
||
/*
|
||
* Není JSON, ignorujeme.
|
||
*/
|
||
|
||
}
|
||
|
||
}
|
||
|
||
}
|
||
);
|
||
|
||
|
||
return result;
|
||
|
||
}
|
||
|
||
|
||
|
||
/* ============================================================
|
||
DATUM / ČAS
|
||
============================================================ */
|
||
|
||
function parseAttendanceCorrectionDate_(
|
||
value
|
||
) {
|
||
|
||
if (
|
||
!value
|
||
) {
|
||
|
||
return null;
|
||
|
||
}
|
||
|
||
|
||
if (
|
||
value instanceof Date
|
||
) {
|
||
|
||
return value;
|
||
|
||
}
|
||
|
||
|
||
const text =
|
||
String(
|
||
value
|
||
).trim();
|
||
|
||
|
||
let match =
|
||
text.match(
|
||
/^(\d{4})-(\d{2})-(\d{2})T(\d{1,2}):(\d{2})(?::(\d{2}))?$/
|
||
);
|
||
|
||
|
||
if (
|
||
match
|
||
) {
|
||
|
||
return new Date(
|
||
Number(
|
||
match[1]
|
||
),
|
||
Number(
|
||
match[2]
|
||
) - 1,
|
||
Number(
|
||
match[3]
|
||
),
|
||
Number(
|
||
match[4]
|
||
),
|
||
Number(
|
||
match[5]
|
||
),
|
||
Number(
|
||
match[6] ||
|
||
0
|
||
)
|
||
);
|
||
|
||
}
|
||
|
||
|
||
match =
|
||
text.match(
|
||
/^(\d{4})-(\d{2})-(\d{2})\s+(\d{1,2}):(\d{2})(?::(\d{2}))?$/
|
||
);
|
||
|
||
|
||
if (
|
||
match
|
||
) {
|
||
|
||
return new Date(
|
||
Number(
|
||
match[1]
|
||
),
|
||
Number(
|
||
match[2]
|
||
) - 1,
|
||
Number(
|
||
match[3]
|
||
),
|
||
Number(
|
||
match[4]
|
||
),
|
||
Number(
|
||
match[5]
|
||
),
|
||
Number(
|
||
match[6] ||
|
||
0
|
||
)
|
||
);
|
||
|
||
}
|
||
|
||
|
||
match =
|
||
text.match(
|
||
/^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:\s+(\d{1,2}):(\d{2})(?::(\d{2}))?)?$/
|
||
);
|
||
|
||
|
||
if (
|
||
match
|
||
) {
|
||
|
||
return new Date(
|
||
Number(
|
||
match[3]
|
||
),
|
||
Number(
|
||
match[1]
|
||
) - 1,
|
||
Number(
|
||
match[2]
|
||
),
|
||
Number(
|
||
match[4] ||
|
||
0
|
||
),
|
||
Number(
|
||
match[5] ||
|
||
0
|
||
),
|
||
Number(
|
||
match[6] ||
|
||
0
|
||
)
|
||
);
|
||
|
||
}
|
||
|
||
|
||
const date =
|
||
new Date(
|
||
text
|
||
);
|
||
|
||
|
||
return isNaN(
|
||
date.getTime()
|
||
)
|
||
? null
|
||
: date;
|
||
|
||
}
|
||
|
||
|
||
function formatAttendanceCorrectionDate_(
|
||
date
|
||
) {
|
||
|
||
return Utilities.formatDate(
|
||
date,
|
||
CFG.TZ,
|
||
'yyyy-MM-dd HH:mm:ss'
|
||
);
|
||
|
||
}
|
||
|
||
|
||
|
||
/* ============================================================
|
||
NOČNÍ MINUTY 22:00–06:00
|
||
============================================================ */
|
||
|
||
function calculateCorrectionNightMinutes_(
|
||
start,
|
||
end
|
||
) {
|
||
|
||
if (
|
||
!start ||
|
||
!end ||
|
||
end <=
|
||
start
|
||
) {
|
||
|
||
return 0;
|
||
|
||
}
|
||
|
||
|
||
let total =
|
||
0;
|
||
|
||
|
||
let cursor =
|
||
new Date(
|
||
start
|
||
);
|
||
|
||
|
||
cursor.setSeconds(
|
||
0,
|
||
0
|
||
);
|
||
|
||
|
||
while (
|
||
cursor <
|
||
end
|
||
) {
|
||
|
||
const hour =
|
||
cursor.getHours();
|
||
|
||
|
||
if (
|
||
hour >=
|
||
22 ||
|
||
hour <
|
||
6
|
||
) {
|
||
|
||
total++;
|
||
|
||
}
|
||
|
||
|
||
cursor =
|
||
new Date(
|
||
cursor.getTime() +
|
||
60000
|
||
);
|
||
|
||
}
|
||
|
||
|
||
return total;
|
||
|
||
}
|
||
|
||
|
||
|
||
/* ============================================================
|
||
ČÍSLO
|
||
============================================================ */
|
||
|
||
function correctionNumber_(
|
||
value
|
||
) {
|
||
|
||
const number =
|
||
Number(
|
||
String(
|
||
value == null
|
||
? 0
|
||
: value
|
||
)
|
||
.replace(
|
||
',',
|
||
'.'
|
||
)
|
||
.replace(
|
||
/\s/g,
|
||
''
|
||
)
|
||
);
|
||
|
||
|
||
return isFinite(
|
||
number
|
||
)
|
||
? number
|
||
: 0;
|
||
|
||
}
|