function getDashboard( token, fromDate, toDate ) { const user = requireUser_( token ); if ( !user.employee_id ) { return getRoleDashboard_( user ); } /* ========================================= ZAMĚSTNANCI ========================================= */ const employees = displayRows_( CFG.SHEETS.EMPLOYEES ); const employee = employees.find( function(item) { return ( String( item.employee_id ) === String( user.employee_id ) ); } ); if ( !employee ) { throw new Error( 'Zaměstnanec nebyl nalezen.' ); } /* ========================================= DATUMOVÝ ROZSAH ========================================= */ const now = new Date(); let from; if ( fromDate ) { from = new Date( fromDate + 'T00:00:00' ); } else { from = new Date( now.getFullYear(), now.getMonth(), 1, 0, 0, 0 ); } let to; if ( toDate ) { to = new Date( toDate + 'T23:59:59' ); } else { to = now; } /* ========================================= SMĚNY ========================================= */ const allShifts = displayRows_( CFG.SHEETS.SHIFTS ); const employeeShifts = allShifts .filter( function(shift) { return ( String( shift.employee_id ) === String( employee.employee_id ) ); } ) .filter( function(shift) { const clockIn = parseEmployeeDate_( shift.clock_in ); if ( !clockIn ) { return false; } return ( clockIn >= from && clockIn <= to ); } ) .sort( function(a,b) { const aDate = parseEmployeeDate_( a.clock_in ); const bDate = parseEmployeeDate_( b.clock_in ); return ( ( bDate ? bDate.getTime() : 0 ) - ( aDate ? aDate.getTime() : 0 ) ); } ); /* ========================================= SAZBY ========================================= */ const payRates = displayRows_( CFG.SHEETS.PAY_RATES ); /* ========================================= VÝPOČET HODIN + PENĚZ ========================================= */ let workedMinutes = 0; let earnedEstimate = 0; let shiftCount = 0; employeeShifts.forEach( function(shift) { const status = String( shift.status || '' ).trim(); /* * Do výdělku počítáme pouze * dokončené směny. */ if ( ![ 'COMPLETED', 'EMPLOYEE_CONFIRMED', 'MANAGER_APPROVED', 'LOCKED' ].includes( status ) ) { return; } const minutes = numberFromSheet_( shift.worked_minutes ); if ( minutes <= 0 ) { return; } const shiftDate = parseEmployeeDate_( shift.clock_in ); const rate = employeeRateAt_( payRates, employee.employee_id, shiftDate ); workedMinutes += minutes; shiftCount++; earnedEstimate += ( minutes / 60 ) * rate; } ); /* ========================================= OTEVŘENÁ SMĚNA ========================================= */ const openShift = allShifts .filter( function(shift) { return ( String( shift.employee_id ) === String( employee.employee_id ) && String( shift.status ).trim() === 'OPEN' ); } ) .sort( function(a,b) { const aDate = parseEmployeeDate_( a.clock_in ); const bDate = parseEmployeeDate_( b.clock_in ); return ( ( bDate ? bDate.getTime() : 0 ) - ( aDate ? aDate.getTime() : 0 ) ); } )[0] || null; let currentShiftMinutes = 0; if ( openShift ) { const clockIn = parseEmployeeDate_( openShift.clock_in ); if ( clockIn ) { currentShiftMinutes = Math.max( 0, Math.floor( ( Date.now() - clockIn.getTime() ) / 60000 ) ); } } /* ========================================= AKTUALITY ========================================= */ let news = []; try { news = getNewsForUser_( user, employee ); /* * Bezpečný převod pro frontend. */ news = news .slice( 0, 8 ) .map( function(item) { return { news_id: String( item.news_id || '' ), title: String( item.title || '' ), content: String( item.content || '' ), category: String( item.category || '' ), require_confirmation: item.require_confirmation === true || String( item.require_confirmation ).toUpperCase() === 'TRUE' }; } ); } catch(error) { news = []; } /* ========================================= BEZPEČNÝ SEZNAM SMĚN ========================================= */ const safeShifts = employeeShifts .slice( 0, 20 ) .map( function(shift) { const clockIn = parseEmployeeDate_( shift.clock_in ); const clockOut = parseEmployeeDate_( shift.clock_out ); return { shift_id: String( shift.shift_id || '' ), employee_id: String( shift.employee_id || '' ), work_date: String( shift.work_date || '' ), clock_in: clockIn ? clockIn.toISOString() : '', clock_out: clockOut ? clockOut.toISOString() : '', break_minutes: numberFromSheet_( shift.break_minutes ), worked_minutes: numberFromSheet_( shift.worked_minutes ), status: String( shift.status || '' ), location_id: String( shift.location_id || '' ) }; } ); /* ========================================= DEBUG INFO Zatím schválně vracíme i sazbu, ať přesně vidíme, co systém používá. ========================================= */ const currentRate = employeeRateAt_( payRates, employee.employee_id, now ); /* ========================================= VÝSLEDEK ========================================= */ return { role: String( user.role || '' ), employee: { employee_id: String( employee.employee_id || '' ), name: ( String( employee.first_name || '' ) + ' ' + String( employee.last_name || '' ) ).trim(), position: String( employee.position || '' ) }, stats: { worked_minutes: Math.round( workedMinutes ), earned_estimate: Math.round( earnedEstimate ), shift_count: shiftCount, current_shift_minutes: currentShiftMinutes, hourly_rate: currentRate }, open_shift: openShift ? { shift_id: String( openShift.shift_id || '' ), clock_in: parseEmployeeDate_( openShift.clock_in ) ? parseEmployeeDate_( openShift.clock_in ).toISOString() : '', status: 'OPEN' } : null, shifts: safeShifts, schedules: [], news: news }; } /* ============================================================ SAZBA PRO KONKRÉTNÍ DATUM ============================================================ */ function employeeRateAt_( rates, employeeId, targetDate ) { if ( !targetDate ) { return 0; } const target = Utilities.formatDate( targetDate, CFG.TZ, 'yyyy-MM-dd' ); const employeeRates = rates .filter( function(rate) { return ( String( rate.employee_id ) === String( employeeId ) ); } ) .filter( function(rate) { const fromDate = parseEmployeeDate_( rate.valid_from ); const toDate = parseEmployeeDate_( rate.valid_to ); const from = fromDate ? Utilities.formatDate( fromDate, CFG.TZ, 'yyyy-MM-dd' ) : '0000-00-00'; const to = toDate ? Utilities.formatDate( toDate, CFG.TZ, 'yyyy-MM-dd' ) : '9999-12-31'; return ( from <= target && to >= target ); } ); if ( !employeeRates.length ) { return 0; } employeeRates.sort( function(a,b) { const aDate = parseEmployeeDate_( a.valid_from ); const bDate = parseEmployeeDate_( b.valid_from ); return ( ( bDate ? bDate.getTime() : 0 ) - ( aDate ? aDate.getTime() : 0 ) ); } ); return numberFromSheet_( employeeRates[0] .hourly_rate ); } /* ============================================================ DATUM Z GOOGLE SHEETS ============================================================ */ function parseEmployeeDate_( value ) { const text = String( value || '' ).trim(); if ( !text ) { return null; } let match; /* * 2026-08-11 * 2026-08-11 23:45:00 */ 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] || 0 ), Number( match[5] || 0 ), Number( match[6] || 0 ) ); } /* * 11.8.2026 23:45:00 */ match = text.match( /^(\d{1,2})\.\s*(\d{1,2})\.\s*(\d{4})(?:\s+(\d{1,2}):(\d{2})(?::(\d{2}))?)?$/ ); if ( match ) { return new Date( Number( match[3] ), Number( match[2] ) - 1, Number( match[1] ), Number( match[4] || 0 ), Number( match[5] || 0 ), Number( match[6] || 0 ) ); } /* * 8/11/2026 23:45:00 */ 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 ); if ( isNaN( date.getTime() ) ) { return null; } return date; } /* ============================================================ ROLE DASHBOARD ============================================================ */ function getRoleDashboard_( user ) { return { role: String( user.role || '' ) }; } /* ============================================================ TEST VÝDĚLKU Spustitelné přímo z editoru. ============================================================ */ function testEmployeeEarnings() { const employees = displayRows_( CFG.SHEETS.EMPLOYEES ); const shifts = displayRows_( CFG.SHEETS.SHIFTS ); const rates = displayRows_( CFG.SHEETS.PAY_RATES ); employees.forEach( function(employee) { const employeeId = String( employee.employee_id ); const employeeShifts = shifts.filter( function(shift) { return ( String( shift.employee_id ) === employeeId ); } ); Logger.log( '--------------------------------' ); Logger.log( 'ZAMĚSTNANEC: ' + employee.first_name + ' ' + employee.last_name ); employeeShifts.forEach( function(shift) { const date = parseEmployeeDate_( shift.clock_in ); const rate = employeeRateAt_( rates, employeeId, date ); const minutes = numberFromSheet_( shift.worked_minutes ); Logger.log( 'SMĚNA: ' + shift.shift_id + ' | stav=' + shift.status + ' | minuty=' + minutes + ' | sazba=' + rate + ' | výdělek=' + ( minutes / 60 * rate ) ); } ); } ); }