/* ============================================================ MESTAFF – EMPLOYEE EDIT SERVICE Bez zásahu do existujícího EmployeeService.gs. ============================================================ */ function getEmployeeDetail( token, employeeId ) { const currentUser = requireUser_( token, [ 'ADMIN', 'MANAGER' ] ); const employees = displayRows_( CFG.SHEETS.EMPLOYEES ); const employee = employees.find( function(row) { return ( String( row.employee_id ) === String( employeeId ) ); } ); if ( !employee ) { throw new Error( 'Zaměstnanec nebyl nalezen.' ); } const users = displayRows_( CFG.SHEETS.USERS ); const employeeUser = users.find( function(row) { return ( String( row.employee_id ) === String( employeeId ) ); } ); if ( currentUser.role !== 'ADMIN' && employeeUser && employeeUser.role === 'ADMIN' ) { throw new Error( 'Vedoucí nemůže upravovat administrátora.' ); } const rates = displayRows_( CFG.SHEETS.PAY_RATES ); const currentRate = employeeEditCurrentRate_( rates, employeeId, new Date() ); return { employee_id: String( employee.employee_id || '' ), first_name: String( employee.first_name || '' ), last_name: String( employee.last_name || '' ), email: String( employee.email || ( employeeUser ? employeeUser.email : '' ) || '' ), phone: String( employee.phone || '' ), company_id: String( employee.company_id || '' ), location_id: String( employee.location_id || '' ), position: String( employee.position || '' ), employment_type: String( employee.employment_type || '' ), terminal_pin: String( employee.terminal_pin || '' ), start_date: employeeEditDateOnly_( employee.start_date ), end_date: employeeEditDateOnly_( employee.end_date ), active: employeeEditBool_( employee.active ), role: employeeUser ? String( employeeUser.role || 'EMPLOYEE' ) : 'EMPLOYEE', hourly_rate: currentRate ? numberFromSheet_( currentRate.hourly_rate ) : 0 }; } function updateEmployee( token, data ) { const currentUser = requireUser_( token, [ 'ADMIN', 'MANAGER' ] ); data = data || {}; const employeeId = String( data.employee_id || '' ).trim(); if ( !employeeId ) { throw new Error( 'Chybí employee_id.' ); } const firstName = String( data.first_name || '' ).trim(); const lastName = String( data.last_name || '' ).trim(); const email = String( data.email || '' ) .trim() .toLowerCase(); const role = String( data.role || 'EMPLOYEE' ) .trim() .toUpperCase(); if ( !firstName || !lastName ) { throw new Error( 'Jméno a příjmení jsou povinné.' ); } if ( !email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test( email ) ) { throw new Error( 'Zadej platný e-mail.' ); } if ( ![ 'EMPLOYEE', 'MANAGER', 'ACCOUNTANT', 'ADMIN' ].includes( role ) ) { throw new Error( 'Neplatná role.' ); } if ( currentUser.role !== 'ADMIN' && role === 'ADMIN' ) { throw new Error( 'Pouze administrátor může přidělit roli ADMIN.' ); } const employees = displayRows_( CFG.SHEETS.EMPLOYEES ); const employee = employees.find( function(row) { return ( String( row.employee_id ) === employeeId ); } ); if ( !employee ) { throw new Error( 'Zaměstnanec nebyl nalezen.' ); } const users = displayRows_( CFG.SHEETS.USERS ); const employeeUser = users.find( function(row) { return ( String( row.employee_id ) === employeeId ); } ); if ( currentUser.role !== 'ADMIN' && employeeUser && String( employeeUser.role ) === 'ADMIN' ) { throw new Error( 'Vedoucí nemůže upravovat administrátora.' ); } /* * Kontrola e-mailové duplicity. */ const duplicateEmployee = employees.find( function(row) { return ( String( row.employee_id ) !== employeeId && String( row.email || '' ) .trim() .toLowerCase() === email ); } ); const duplicateUser = users.find( function(row) { return ( String( row.employee_id || '' ) !== employeeId && String( row.email || '' ) .trim() .toLowerCase() === email ); } ); if ( duplicateEmployee || duplicateUser ) { throw new Error( 'Tento e-mail už používá jiný účet.' ); } const pin = String( data.terminal_pin || '' ).trim(); if ( pin && !/^\d{4,6}$/.test( pin ) ) { throw new Error( 'PIN musí obsahovat 4 až 6 číslic.' ); } if ( pin ) { const duplicatePin = employees.find( function(row) { return ( String( row.employee_id ) !== employeeId && employeeEditBool_( row.active ) && String( row.terminal_pin || '' ).trim() === pin ); } ); if ( duplicatePin ) { throw new Error( 'Tento PIN už používá jiný aktivní zaměstnanec.' ); } } const active = data.active === true || String( data.active ).toUpperCase() === 'TRUE'; /* * Základní data zaměstnance. */ updateBy_( CFG.SHEETS.EMPLOYEES, 'employee_id', employeeId, { first_name: firstName, last_name: lastName, email: email, phone: String( data.phone || '' ).trim(), company_id: String( data.company_id || '' ).trim(), location_id: String( data.location_id || '' ).trim(), position: String( data.position || '' ).trim(), employment_type: String( data.employment_type || '' ).trim(), terminal_pin: pin, start_date: String( data.start_date || '' ).trim(), end_date: String( data.end_date || '' ).trim(), active: active } ); /* * Synchronizace přihlašovacího účtu. */ if ( employeeUser ) { updateBy_( CFG.SHEETS.USERS, 'user_id', employeeUser.user_id, { email: email, role: role, active: active } ); } /* * Historická změna hodinové sazby. */ let rateChanged = false; const newRate = employeeEditNumber_( data.hourly_rate ); const validFrom = String( data.rate_valid_from || Utilities.formatDate( new Date(), CFG.TZ, 'yyyy-MM-dd' ) ).trim(); if ( !/^\d{4}-\d{2}-\d{2}$/.test( validFrom ) ) { throw new Error( 'Neplatné datum účinnosti nové sazby.' ); } const rates = displayRows_( CFG.SHEETS.PAY_RATES ); const currentRate = employeeEditCurrentRate_( rates, employeeId, employeeEditParseDate_( validFrom ) ); const currentRateAmount = currentRate ? numberFromSheet_( currentRate.hourly_rate ) : 0; if ( Math.abs( newRate - currentRateAmount ) > 0.0001 ) { /* * Zavřeme předchozí sazbu den před účinností nové. */ if ( currentRate && currentRate.pay_rate_id ) { const previousDay = employeeEditAddDays_( validFrom, -1 ); const currentValidFrom = employeeEditDateOnly_( currentRate.valid_from ); if ( !currentValidFrom || currentValidFrom < validFrom ) { updateBy_( CFG.SHEETS.PAY_RATES, 'pay_rate_id', currentRate.pay_rate_id, { valid_to: previousDay } ); } } append_( CFG.SHEETS.PAY_RATES, { pay_rate_id: uuid_( 'RATE' ), employee_id: employeeId, valid_from: validFrom, valid_to: '', hourly_rate: newRate, created_by: currentUser.user_id, created_at: now_() } ); rateChanged = true; } audit_( currentUser.user_id, 'EMPLOYEE_UPDATED', 'EMPLOYEE', employeeId, '', JSON.stringify({ email: email, role: role, active: active, hourly_rate: newRate, rate_changed: rateChanged }) ); return { ok:true, rate_changed: rateChanged }; } /* ============================================================ HELPERS ============================================================ */ function employeeEditBool_( value ) { if ( value === true ) { return true; } return [ 'TRUE', 'ANO', 'YES', '1' ].includes( String( value || '' ) .trim() .toUpperCase() ); } function employeeEditNumber_( value ) { const number = Number( String( value == null ? 0 : value ) .trim() .replace( ',', '.' ) ); if ( !isFinite( number ) || number < 0 ) { throw new Error( 'Hodinová sazba musí být nezáporné číslo.' ); } return Math.round( number * 100 ) / 100; } function employeeEditParseDate_( value ) { const match = String( value || '' ) .trim() .match( /^(\d{4})-(\d{2})-(\d{2})$/ ); if ( !match ) { return null; } return new Date( Number( match[1] ), Number( match[2] ) - 1, Number( match[3] ) ); } function employeeEditDateOnly_( value ) { if ( !value ) { return ''; } if ( /^\d{4}-\d{2}-\d{2}$/.test( String( value ).trim() ) ) { return String( value ).trim(); } const date = typeof parseEmployeeDate_ === 'function' ? parseEmployeeDate_( value ) : new Date( value ); if ( !date || isNaN( date.getTime() ) ) { return ''; } return Utilities.formatDate( date, CFG.TZ, 'yyyy-MM-dd' ); } function employeeEditCurrentRate_( rates, employeeId, targetDate ) { if ( !targetDate ) { return null; } const target = Utilities.formatDate( targetDate, CFG.TZ, 'yyyy-MM-dd' ); const valid = rates .filter( function(rate) { if ( String( rate.employee_id ) !== String( employeeId ) ) { return false; } const from = employeeEditDateOnly_( rate.valid_from ) || '0000-00-00'; const to = employeeEditDateOnly_( rate.valid_to ) || '9999-12-31'; return ( from <= target && to >= target ); } ) .sort( function(a,b) { return employeeEditDateOnly_( b.valid_from ).localeCompare( employeeEditDateOnly_( a.valid_from ) ); } ); return valid.length ? valid[0] : null; } function employeeEditAddDays_( yyyyMmDd, days ) { const date = employeeEditParseDate_( yyyyMmDd ); if ( !date ) { return ''; } date.setDate( date.getDate() + Number( days || 0 ) ); return Utilities.formatDate( date, CFG.TZ, 'yyyy-MM-dd' ); } /* ============================================================ DIAGNOSTIKA ============================================================ */ function testEmployeeEditService() { Logger.log( 'EMPLOYEES: ' + JSON.stringify( displayRows_( CFG.SHEETS.EMPLOYEES ) ) ); Logger.log( 'USERS: ' + JSON.stringify( displayRows_( CFG.SHEETS.USERS ) ) ); Logger.log( 'PAY_RATES: ' + JSON.stringify( displayRows_( CFG.SHEETS.PAY_RATES ) ) ); Logger.log( 'EmployeeEditService OK' ); }