/* ============================================================
   EATME PORTÁL – WEEKLY PAYROLL SERVICE

   Týdenní výkaz:
   - období Po–Ne
   - zaměstnanec potvrzuje nejpozději 12 hodin po skončení
     poslední plánované směny v daném týdnu
   - MANAGER / ADMIN doplní spropitné, bonus, ostatní
   - ADMIN označí týden jako PAID

   Měsíční PAYROLL / MONTH_CLOSURES zůstávají oddělené.
============================================================ */

const WEEKLY_SHEETS = {
  CLOSURES: 'WEEKLY_CLOSURES'
};


/* ============================================================
   JEDNORÁZOVÝ SETUP
============================================================ */

function setupWeeklyPayroll() {

  ensureWeeklySheet_(
    WEEKLY_SHEETS.CLOSURES,
    [
      'weekly_id',
      'employee_id',
      'week_start',
      'week_end',
      'last_shift_end',
      'deadline_at',
      'worked_minutes',
      'base_amount',
      'tips_amount',
      'card_tip_amount',
      'bonus_amount',
      'sales_bonus_amount',
      'other_amount',
      'final_amount',
      'employee_confirmed_at',
      'status',
      'paid_at',
      'paid_by',
      'employee_notified_at',
      'reminder_sent_at',
      'overdue_notified_at',
      'updated_at'
    ]
  );


  ensureWeeklyPayrollColumns_();


  createWeeklyDeadlineTrigger_();


  Logger.log(
    'Weekly payroll setup OK'
  );

}



/* ============================================================
   DOPLNĚNÍ NOVÝCH SLOUPCŮ DO EXISTUJÍCÍHO LISTU
============================================================ */

function ensureWeeklyPayrollColumns_() {

  const sheet =
    sh_(
      WEEKLY_SHEETS.CLOSURES
    );


  const required =
    [
      'employee_notified_at',
      'reminder_sent_at',
      'overdue_notified_at'
    ];


  const lastColumn =
    Math.max(
      1,
      sheet.getLastColumn()
    );


  const headers =
    sheet
      .getRange(
        1,
        1,
        1,
        lastColumn
      )
      .getDisplayValues()[0]
      .map(
        function(value) {

          return String(
            value
          ).trim();

        }
      );


  required.forEach(
    function(header) {

      if (
        headers.includes(
          header
        )
      ) {

        return;

      }


      const column =
        sheet.getLastColumn() +
        1;


      sheet
        .getRange(
          1,
          column
        )
        .setValue(
          header
        );


      headers.push(
        header
      );

    }
  );

}


/* ============================================================
   HODINOVÝ TRIGGER PRO OVERDUE STAV
============================================================ */

function createWeeklyDeadlineTrigger_() {

  const handler =
    'refreshWeeklyDeadlineStatuses';


  ScriptApp
    .getProjectTriggers()
    .filter(
      function(trigger) {

        return (
          trigger.getHandlerFunction() ===
          handler
        );

      }
    )
    .forEach(
      function(trigger) {

        ScriptApp.deleteTrigger(
          trigger
        );

      }
    );


  ScriptApp
    .newTrigger(
      handler
    )
    .timeBased()
    .everyHours(
      1
    )
    .create();

}



/* ============================================================
   ZAMĚSTNANEC – AKTUÁLNÍ / ZVOLENÝ TÝDEN
============================================================ */

function getMyWeeklyReport(
  token,
  dateValue
) {

  const user =
    requireUser_(
      token,
      [
        'EMPLOYEE',
        'MANAGER',
        'ADMIN'
      ]
    );


  if (
    !user.employee_id
  ) {

    throw new Error(
      'Účet není propojen se zaměstnancem.'
    );

  }


  const week =
    getWeekRange_(
      dateValue
    );


  const data =
    buildWeeklyData_(
      user.employee_id,
      week.start,
      week.end
    );


  if (
    !data.has_activity
  ) {

    return {

      has_activity:false,

      week_start:
        week.start,

      week_end:
        week.end,

      status:
        'NO_ACTIVITY',

      worked_minutes:
        0,

      shift_count:
        0,

      base_amount:
        0,

      tips_amount:
        0,

      card_tip_amount:
        0,

      bonus_amount:
        0,

      sales_bonus_amount:
        0,

      other_amount:
        0,

      final_amount:
        0,

      last_shift_end:
        '',

      deadline_at:
        '',

      employee_confirmed_at:
        '',

      paid_at:
        '',

      can_confirm:
        false,

      overdue:
        false

    };

  }


  const closure =
    ensureWeeklyClosure_(
      user.employee_id,
      week.start,
      week.end,
      data
    );


  const now =
    new Date();


  const deadline =
    parseWeeklyDateTime_(
      closure.deadline_at
    );


  const lastShiftEnd =
    parseWeeklyDateTime_(
      closure.last_shift_end
    );


  const status =
    computeWeeklyStatus_(
      closure,
      now
    );


  if (
    status !==
    String(
      closure.status
    )
  ) {

    updateBy_(
      WEEKLY_SHEETS.CLOSURES,
      'weekly_id',
      closure.weekly_id,
      {

        status:
          status,

        updated_at:
          now_()

      }
    );

  }


  const salesBonusAmount =
    getSalesBonusForEmployeeWeek_(
      user.employee_id,
      week.start,
      week.end
    );


  const cardTipAmount =
    getCardTipForEmployeeWeek_(
      user.employee_id,
      week.start,
      week.end
    );


  return {

    has_activity:true,

    weekly_id:
      String(
        closure.weekly_id ||
        ''
      ),

    week_start:
      String(
        closure.week_start ||
        week.start
      ),

    week_end:
      String(
        closure.week_end ||
        week.end
      ),

    worked_minutes:
      Number(
        data.worked_minutes ||
        0
      ),

    shift_count:
      Number(
        data.shift_count ||
        0
      ),

    base_amount:
      Number(
        data.base_amount ||
        0
      ),

    tips_amount:
      weeklyNumber_(
        closure.tips_amount
      ),

    card_tip_amount:
      cardTipAmount,

    bonus_amount:
      weeklyNumber_(
        closure.bonus_amount
      ),

    sales_bonus_amount:
      salesBonusAmount,

    other_amount:
      weeklyNumber_(
        closure.other_amount
      ),

    final_amount:
      Number(
        data.base_amount ||
        0
      ) +
      weeklyNumber_(
        closure.tips_amount
      ) +
      cardTipAmount +
      weeklyNumber_(
        closure.bonus_amount
      ) +
      salesBonusAmount +
      weeklyNumber_(
        closure.other_amount
      ),

    last_shift_end:
      String(
        closure.last_shift_end ||
        ''
      ),

    deadline_at:
      String(
        closure.deadline_at ||
        ''
      ),

    employee_confirmed_at:
      String(
        closure.employee_confirmed_at ||
        ''
      ),

    paid_at:
      String(
        closure.paid_at ||
        ''
      ),

    status:
      status,

    can_confirm:
      (
        lastShiftEnd &&
        now >=
        lastShiftEnd &&
        ![
          'EMPLOYEE_CONFIRMED',
          'READY_TO_PAY',
          'PAID'
        ].includes(
          status
        )
      ),

    overdue:
      (
        status ===
        'OVERDUE'
      ),

    deadline_remaining_minutes:
      deadline
        ? Math.round(
            (
              deadline.getTime() -
              now.getTime()
            ) /
            60000
          )
        : null

  };

}



/* ============================================================
   ZAMĚSTNANEC – POTVRZENÍ TÝDNE
============================================================ */

function confirmMyWeeklyReport(
  token,
  weekStart
) {

  const user =
    requireUser_(
      token,
      [
        'EMPLOYEE',
        'MANAGER',
        'ADMIN'
      ]
    );


  if (
    !user.employee_id
  ) {

    throw new Error(
      'Účet není propojen se zaměstnancem.'
    );

  }


  const week =
    getWeekRange_(
      weekStart
    );


  const data =
    buildWeeklyData_(
      user.employee_id,
      week.start,
      week.end
    );


  if (
    !data.has_activity
  ) {

    throw new Error(
      'Za tento týden není co potvrdit.'
    );

  }


  const closure =
    ensureWeeklyClosure_(
      user.employee_id,
      week.start,
      week.end,
      data
    );


  if (
    [
      'EMPLOYEE_CONFIRMED',
      'READY_TO_PAY',
      'PAID'
    ].includes(
      String(
        closure.status
      )
    )
  ) {

    return {
      ok:true
    };

  }


  const now =
    new Date();


  const lastShiftEnd =
    parseWeeklyDateTime_(
      closure.last_shift_end
    );


  if (
    !lastShiftEnd ||
    now <
    lastShiftEnd
  ) {

    throw new Error(
      'Týdenní výkaz lze potvrdit až po skončení poslední směny v týdnu.'
    );

  }


  updateBy_(
    WEEKLY_SHEETS.CLOSURES,
    'weekly_id',
    closure.weekly_id,
    {

      worked_minutes:
        data.worked_minutes,

      base_amount:
        data.base_amount,

      final_amount:
        data.base_amount +
        weeklyNumber_(
          closure.tips_amount
        ) +
        weeklyNumber_(
          closure.bonus_amount
        ) +
        weeklyNumber_(
          closure.other_amount
        ),

      employee_confirmed_at:
        now_(),

      status:
        'EMPLOYEE_CONFIRMED',

      updated_at:
        now_()

    }
  );


  audit_(
    user.user_id,
    'WEEKLY_REPORT_CONFIRMED',
    'WEEKLY_CLOSURE',
    closure.weekly_id,
    closure.status,
    'EMPLOYEE_CONFIRMED'
  );


  notifyManagers_(
    user.employee_id,
    'Týdenní výkaz potvrzen',
    'Zaměstnanec potvrdil týdenní výkaz za období ' +
    week.start +
    ' až ' +
    week.end +
    '.'
  );


  return {
    ok:true
  };

}



/* ============================================================
   MANAGER / ADMIN – TÝDENNÍ PŘEHLED
============================================================ */

/* ============================================================
   ADMIN / MANAGER – DETAIL DOCHÁZKY PRO TÝDENNÍ VÝPLATU

   Vrací pouze směny, které vstupují do mzdového základu.
============================================================ */

function getAdminWeeklyAttendanceDetail(
  token,
  employeeId,
  weekStart
) {

  requireUser_(
    token,
    [
      'ADMIN',
      'MANAGER'
    ]
  );


  employeeId =
    String(
      employeeId ||
      ''
    );


  if (
    !employeeId
  ) {

    throw new Error(
      'Chybí zaměstnanec.'
    );

  }


  const week =
    getWeekRange_(
      weekStart
    );


  const context =
    buildWeeklyRequestContext_();


  const employee =
    context.employees
      .find(
        function(row) {

          return (
            String(
              row.employee_id
            ) ===
            employeeId
          );

        }
      );


  if (
    !employee
  ) {

    throw new Error(
      'Zaměstnanec nebyl nalezen.'
    );

  }


  const shifts =
    context.shiftsByEmployee[
      employeeId
    ] ||
    [];


  const rates =
    context.ratesByEmployee[
      employeeId
    ] ||
    [];


  const dayMap =
    {};


  shifts.forEach(
    function(shift) {

      const workDate =
        normalizeDateOnly_(
          shift.work_date
        );


      if (
        !workDate ||
        workDate <
          week.start ||
        workDate >
          week.end
      ) {

        return;

      }


      /*
       * Musíme používat stejnou logiku jako buildWeeklyDataFromContext_(),
       * jinak by se detail mohl lišit od částky ve výplatě.
       */
      if (
        String(
          shift.status
        ) ===
        'OPEN'
      ) {

        return;

      }


      const minutes =
        weeklyNumber_(
          shift.worked_minutes
        );


      const breakMinutes =
        weeklyNumber_(
          shift.break_minutes
        );


      const clockIn =
        parseEmployeeDate_(
          shift.clock_in
        );


      const clockOut =
        parseEmployeeDate_(
          shift.clock_out
        );


      const rate =
        employeeRateAt_(
          rates,
          employeeId,
          clockIn ||
          new Date(
            workDate
          )
        );


      const amount =
        (
          minutes /
          60
        ) *
        rate;


      if (
        !dayMap[
          workDate
        ]
      ) {

        dayMap[
          workDate
        ] =
          {

            work_date:
              workDate,

            first_clock_in:
              null,

            last_clock_out:
              null,

            break_minutes:
              0,

            worked_minutes:
              0,

            base_amount:
              0,

            rates:
              [],

            shift_count:
              0

          };

      }


      const day =
        dayMap[
          workDate
        ];


      if (
        clockIn &&
        (
          !day.first_clock_in ||
          clockIn <
          day.first_clock_in
        )
      ) {

        day.first_clock_in =
          clockIn;

      }


      if (
        clockOut &&
        (
          !day.last_clock_out ||
          clockOut >
          day.last_clock_out
        )
      ) {

        day.last_clock_out =
          clockOut;

      }


      day.break_minutes +=
        breakMinutes;


      day.worked_minutes +=
        minutes;


      day.base_amount +=
        amount;


      day.rates.push(
        rate
      );


      day.shift_count++;

    }
  );


  const rows =
    Object.keys(
      dayMap
    )
      .sort()
      .map(
        function(date) {

          const day =
            dayMap[
              date
            ];


          const uniqueRates =
            Array.from(
              new Set(
                day.rates.map(
                  function(rate) {

                    return Number(
                      rate ||
                      0
                    );

                  }
                )
              )
            );


          return {

            work_date:
              day.work_date,

            clock_in:
              day.first_clock_in
                ? formatWeeklyDateTime_(
                    day.first_clock_in
                  )
                : '',

            clock_out:
              day.last_clock_out
                ? formatWeeklyDateTime_(
                    day.last_clock_out
                  )
                : '',

            break_minutes:
              Math.round(
                day.break_minutes
              ),

            worked_minutes:
              Math.round(
                day.worked_minutes
              ),

            hourly_rate:
              uniqueRates.length ===
                1
                ? uniqueRates[0]
                : null,

            effective_hourly_rate:
              day.worked_minutes >
                0
                ? (
                    day.base_amount /
                    (
                      day.worked_minutes /
                      60
                    )
                  )
                : 0,

            base_amount:
              Math.round(
                day.base_amount
              ),

            shift_count:
              day.shift_count

          };

        }
      );


  const totalWorkedMinutes =
    rows.reduce(
      function(sum,row) {

        return (
          sum +
          Number(
            row.worked_minutes ||
            0
          )
        );

      },
      0
    );


  const totalBreakMinutes =
    rows.reduce(
      function(sum,row) {

        return (
          sum +
          Number(
            row.break_minutes ||
            0
          )
        );

      },
      0
    );


  const totalBaseAmount =
    rows.reduce(
      function(sum,row) {

        return (
          sum +
          Number(
            row.base_amount ||
            0
          )
        );

      },
      0
    );


  return {

    employee_id:
      employeeId,

    employee_name:
      (
        String(
          employee.first_name ||
          ''
        ) +
        ' ' +
        String(
          employee.last_name ||
          ''
        )
      ).trim(),

    week_start:
      week.start,

    week_end:
      week.end,

    rows:
      rows,

    totals:
      {

        worked_minutes:
          totalWorkedMinutes,

        break_minutes:
          totalBreakMinutes,

        base_amount:
          totalBaseAmount,

        day_count:
          rows.length

      }

  };

}


/* ============================================================
   ADMIN / MANAGER – TÝDENNÍ VÝPLATY
============================================================ */

function getAdminWeeklyPayroll(
  token,
  dateValue
) {

  requireUser_(
    token,
    [
      'ADMIN',
      'MANAGER'
    ]
  );


  const week =
    getWeekRange_(
      dateValue
    );


  /*
   * VŠECHNA DATA NAČTEME JEDNOU.
   * Původní verze znovu četla několik listů pro každého zaměstnance.
   */
  const context =
    buildWeeklyRequestContext_();


  const employees =
    context.employees
      .filter(
        function(employee) {

          return (
            String(
              employee.active
            ).toUpperCase() ===
            'TRUE'
          );

        }
      );


  const result =
    [];


  const now =
    new Date();


  employees.forEach(
    function(employee) {

      const data =
        buildWeeklyDataFromContext_(
          employee.employee_id,
          week.start,
          week.end,
          context
        );


      if (
        !data.has_activity
      ) {

        return;

      }


      const closure =
        ensureWeeklyClosureWithContext_(
          employee.employee_id,
          week.start,
          week.end,
          data,
          context
        );


      const status =
        computeWeeklyStatus_(
          closure,
          now
        );


      const salesBonusAmount =
        getSalesBonusForEmployeeWeek_(
          employee.employee_id,
          week.start,
          week.end
        );


      const cardTipAmount =
        getCardTipForEmployeeWeek_(
          employee.employee_id,
          week.start,
          week.end
        );


      result.push(
        {

          weekly_id:
            String(
              closure.weekly_id ||
              ''
            ),

          employee_id:
            String(
              employee.employee_id ||
              ''
            ),

          employee_name:
            (
              String(
                employee.first_name ||
                ''
              ) +
              ' ' +
              String(
                employee.last_name ||
                ''
              )
            ).trim(),

          week_start:
            week.start,

          week_end:
            week.end,

          worked_minutes:
            Number(
              data.worked_minutes ||
              0
            ),

          shift_count:
            Number(
              data.shift_count ||
              0
            ),

          base_amount:
            Number(
              data.base_amount ||
              0
            ),

          tips_amount:
            weeklyNumber_(
              closure.tips_amount
            ),

          card_tip_amount:
            cardTipAmount,

          bonus_amount:
            weeklyNumber_(
              closure.bonus_amount
            ),

          sales_bonus_amount:
            salesBonusAmount,

          other_amount:
            weeklyNumber_(
              closure.other_amount
            ),

          final_amount:
            Number(
              data.base_amount ||
              0
            ) +
            weeklyNumber_(
              closure.tips_amount
            ) +
            cardTipAmount +
            weeklyNumber_(
              closure.bonus_amount
            ) +
            salesBonusAmount +
            weeklyNumber_(
              closure.other_amount
            ),

          last_shift_end:
            String(
              closure.last_shift_end ||
              ''
            ),

          deadline_at:
            String(
              closure.deadline_at ||
              ''
            ),

          employee_confirmed_at:
            String(
              closure.employee_confirmed_at ||
              ''
            ),

          paid_at:
            String(
              closure.paid_at ||
              ''
            ),

          status:
            status

        }
      );

    }
  );


  const sortedRows =
    result.sort(
      function(a,b) {

        return a.employee_name.localeCompare(
          b.employee_name,
          'cs'
        );

      }
    );


  const stats =
    {

      total:
        sortedRows.length,

      waiting:
        sortedRows.filter(
          function(row) {

            return [
              'IN_PROGRESS',
              'WAITING_EMPLOYEE'
            ].includes(
              row.status
            );

          }
        ).length,

      overdue:
        sortedRows.filter(
          function(row) {

            return row.status ===
              'OVERDUE';

          }
        ).length,

      confirmed:
        sortedRows.filter(
          function(row) {

            return [
              'EMPLOYEE_CONFIRMED',
              'READY_TO_PAY'
            ].includes(
              row.status
            );

          }
        ).length,

      paid:
        sortedRows.filter(
          function(row) {

            return row.status ===
              'PAID';

          }
        ).length,

      total_amount:
        sortedRows.reduce(
          function(sum,row) {

            return sum +
              Number(
                row.final_amount ||
                0
              );

          },
          0
        )

    };


  return {

    week_start:
      week.start,

    week_end:
      week.end,

    stats:
      stats,

    rows:
      sortedRows

  };

}



/* ============================================================
   MANAGER / ADMIN – ODMĚNY
============================================================ */

function saveWeeklyAdjustments(
  token,
  weeklyId,
  tipsAmount,
  bonusAmount,
  otherAmount
) {

  const user =
    requireUser_(
      token,
      [
        'ADMIN',
        'MANAGER'
      ]
    );


  const closure =
    findWeeklyClosureById_(
      weeklyId
    );


  if (
    !closure
  ) {

    throw new Error(
      'Týdenní výkaz nebyl nalezen.'
    );

  }


  if (
    String(
      closure.status
    ) ===
    'PAID'
  ) {

    throw new Error(
      'Vyplacený týden už nelze měnit.'
    );

  }


  if (
    ![
      'EMPLOYEE_CONFIRMED',
      'READY_TO_PAY'
    ].includes(
      String(
        closure.status
      )
    )
  ) {

    throw new Error(
      'Odměny lze zadat až po potvrzení výkazu zaměstnancem.'
    );

  }


  tipsAmount =
    weeklyNumber_(
      tipsAmount
    );


  bonusAmount =
    weeklyNumber_(
      bonusAmount
    );


  otherAmount =
    weeklyNumber_(
      otherAmount
    );


  const baseAmount =
    weeklyNumber_(
      closure.base_amount
    );


  const salesBonusAmount =
    getSalesBonusForEmployeeWeek_(
      closure.employee_id,
      closure.week_start,
      closure.week_end
    );


  const cardTipAmount =
    getCardTipForEmployeeWeek_(
      closure.employee_id,
      closure.week_start,
      closure.week_end
    );


  const finalAmount =
    baseAmount +
    tipsAmount +
    cardTipAmount +
    bonusAmount +
    salesBonusAmount +
    otherAmount;


  updateBy_(
    WEEKLY_SHEETS.CLOSURES,
    'weekly_id',
    weeklyId,
    {

      tips_amount:
        tipsAmount,

      card_tip_amount:
        cardTipAmount,

      bonus_amount:
        bonusAmount,

      sales_bonus_amount:
        salesBonusAmount,

      other_amount:
        otherAmount,

      final_amount:
        finalAmount,

      status:
        'READY_TO_PAY',

      updated_at:
        now_()

    }
  );


  audit_(
    user.user_id,
    'WEEKLY_ADJUSTMENTS_SAVED',
    'WEEKLY_CLOSURE',
    weeklyId,
    '',
    JSON.stringify({

      tips_amount:
        tipsAmount,

      card_tip_amount:
        cardTipAmount,

      bonus_amount:
        bonusAmount,

      sales_bonus_amount:
        salesBonusAmount,

      other_amount:
        otherAmount,

      final_amount:
        finalAmount

    })
  );


  return {

    ok:true,

    final_amount:
      finalAmount

  };

}



/* ============================================================
   ADMIN – OZNAČIT VYPLACENO
============================================================ */

function markWeeklyPaid(
  token,
  weeklyId
) {

  const user =
    requireUser_(
      token,
      [
        'ADMIN'
      ]
    );


  const closure =
    findWeeklyClosureById_(
      weeklyId
    );


  if (
    !closure
  ) {

    throw new Error(
      'Týdenní výkaz nebyl nalezen.'
    );

  }


  if (
    ![
      'EMPLOYEE_CONFIRMED',
      'READY_TO_PAY'
    ].includes(
      String(
        closure.status
      )
    )
  ) {

    throw new Error(
      'Výkaz zatím není připravený k výplatě.'
    );

  }


  updateBy_(
    WEEKLY_SHEETS.CLOSURES,
    'weekly_id',
    weeklyId,
    {

      status:
        'PAID',

      paid_at:
        now_(),

      paid_by:
        user.user_id,

      updated_at:
        now_()

    }
  );


  audit_(
    user.user_id,
    'WEEKLY_MARKED_PAID',
    'WEEKLY_CLOSURE',
    weeklyId,
    closure.status,
    'PAID'
  );


  notifyEmployee_(
    closure.employee_id,
    'Týdenní výplata vyplacena',
    'Výplata za týden ' +
    closure.week_start +
    ' až ' +
    closure.week_end +
    ' byla označena jako vyplacená.'
  );


  return {
    ok:true
  };

}



/* ============================================================
   AUTOMATICKÁ KONTROLA DEADLINŮ
============================================================ */

function refreshWeeklyDeadlineStatuses() {

  ensureWeeklyPayrollColumns_();


  const closures =
    displayRows_(
      WEEKLY_SHEETS.CLOSURES
    );


  const now =
    new Date();


  closures.forEach(
    function(closure) {

      const current =
        String(
          closure.status ||
          ''
        );


      if (
        [
          'EMPLOYEE_CONFIRMED',
          'READY_TO_PAY',
          'PAID'
        ].includes(
          current
        )
      ) {

        return;

      }


      const lastShiftEnd =
        parseWeeklyDateTime_(
          closure.last_shift_end
        );


      const deadline =
        parseWeeklyDateTime_(
          closure.deadline_at
        );


      if (
        !lastShiftEnd ||
        !deadline
      ) {

        return;

      }


      const status =
        computeWeeklyStatus_(
          closure,
          now
        );


      const update =
        {};


      /*
       * 1) Jakmile skončí poslední směna, zaměstnanec dostane
       *    upozornění, že má 12 hodin na potvrzení.
       */
      if (
        now >=
          lastShiftEnd &&
        !closure.employee_notified_at
      ) {

        notifyEmployee_(
          closure.employee_id,
          'Potvrď týdenní výkaz',
          'Skončila tvoje poslední směna v týdnu. Týdenní výkaz potvrď nejpozději do ' +
          formatWeeklyDateTime_(
            deadline
          ) +
          '.'
        );


        update.employee_notified_at =
          now_();

      }


      /*
       * 2) Dvě hodiny před deadlinem pošleme jednu připomínku.
       */
      const reminderAt =
        new Date(
          deadline.getTime() -
          2 *
          60 *
          60 *
          1000
        );


      if (
        now >=
          reminderAt &&
        now <
          deadline &&
        !closure.reminder_sent_at
      ) {

        notifyEmployee_(
          closure.employee_id,
          'Týdenní výkaz čeká na potvrzení',
          'Na potvrzení týdenního výkazu zbývají méně než 2 hodiny. Deadline je ' +
          formatWeeklyDateTime_(
            deadline
          ) +
          '.'
        );


        update.reminder_sent_at =
          now_();

      }


      /*
       * 3) Po překročení deadlinu upozorníme manažery.
       */
      if (
        now >
          deadline &&
        !closure.overdue_notified_at
      ) {

        notifyManagers_(
          closure.employee_id,
          'Týdenní výkaz po termínu',
          'Zaměstnanec nepotvrdil týdenní výkaz za období ' +
          closure.week_start +
          ' až ' +
          closure.week_end +
          ' do stanoveného termínu.'
        );


        update.overdue_notified_at =
          now_();

      }


      if (
        status !==
        current
      ) {

        update.status =
          status;

      }


      if (
        Object.keys(
          update
        ).length
      ) {

        update.updated_at =
          now_();


        updateBy_(
          WEEKLY_SHEETS.CLOSURES,
          'weekly_id',
          closure.weekly_id,
          update
        );

      }

    }
  );

}



/* ============================================================
   VÝPOČET DAT TÝDNE
============================================================ */

function buildWeeklyData_(
  employeeId,
  weekStart,
  weekEnd
) {

  const context =
    buildWeeklyRequestContext_();


  return buildWeeklyDataFromContext_(
    employeeId,
    weekStart,
    weekEnd,
    context
  );

}



/* ============================================================
   POSLEDNÍ PLÁNOVANÁ SMĚNA TÝDNE
============================================================ */

function getLastPlannedShiftEnd_(
  employeeId,
  weekStart,
  weekEnd
) {

  const context =
    buildWeeklyRequestContext_();


  return getLastPlannedShiftEndFromContext_(
    employeeId,
    weekStart,
    weekEnd,
    context
  );

}



/* ============================================================
   ZALOŽENÍ / OBNOVENÍ TÝDENNÍHO ZÁZNAMU
============================================================ */

function ensureWeeklyClosure_(
  employeeId,
  weekStart,
  weekEnd,
  data
) {

  const context =
    buildWeeklyRequestContext_();


  return ensureWeeklyClosureWithContext_(
    employeeId,
    weekStart,
    weekEnd,
    data,
    context
  );

}



/* ============================================================
   REQUEST CONTEXT – JEDNO ČTENÍ TABULEK NA REQUEST
============================================================ */

function buildWeeklyRequestContext_() {

  const employees =
    displayRows_(
      CFG.SHEETS.EMPLOYEES
    );


  const shifts =
    displayRows_(
      CFG.SHEETS.SHIFTS
    );


  const rates =
    displayRows_(
      CFG.SHEETS.PAY_RATES
    );


  const slots =
    displayRows_(
      'SHIFT_SLOTS'
    );


  const signups =
    displayRows_(
      'SHIFT_SIGNUPS'
    );


  const closures =
    displayRows_(
      WEEKLY_SHEETS.CLOSURES
    );


  const shiftsByEmployee =
    {};


  shifts.forEach(
    function(shift) {

      const employeeId =
        String(
          shift.employee_id ||
          ''
        );


      if (
        !employeeId
      ) {

        return;

      }


      if (
        !shiftsByEmployee[
          employeeId
        ]
      ) {

        shiftsByEmployee[
          employeeId
        ] =
          [];

      }


      shiftsByEmployee[
        employeeId
      ].push(
        shift
      );

    }
  );


  const ratesByEmployee =
    {};


  rates.forEach(
    function(rate) {

      const employeeId =
        String(
          rate.employee_id ||
          ''
        );


      if (
        !employeeId
      ) {

        return;

      }


      if (
        !ratesByEmployee[
          employeeId
        ]
      ) {

        ratesByEmployee[
          employeeId
        ] =
          [];

      }


      ratesByEmployee[
        employeeId
      ].push(
        rate
      );

    }
  );


  const slotMap =
    {};


  slots.forEach(
    function(slot) {

      slotMap[
        String(
          slot.slot_id
        )
      ] =
        slot;

    }
  );


  const plannedSlotsByEmployee =
    {};


  signups.forEach(
    function(signup) {

      if (
        String(
          signup.status
        ) !==
        'APPROVED'
      ) {

        return;

      }


      const employeeId =
        String(
          signup.employee_id ||
          ''
        );


      const slot =
        slotMap[
          String(
            signup.slot_id
          )
        ];


      if (
        !employeeId ||
        !slot
      ) {

        return;

      }


      if (
        !plannedSlotsByEmployee[
          employeeId
        ]
      ) {

        plannedSlotsByEmployee[
          employeeId
        ] =
          [];

      }


      plannedSlotsByEmployee[
        employeeId
      ].push(
        slot
      );

    }
  );


  const closuresByKey =
    {};


  closures.forEach(
    function(closure) {

      closuresByKey[
        weeklyClosureKey_(
          closure.employee_id,
          closure.week_start
        )
      ] =
        closure;

    }
  );


  return {

    employees:
      employees,

    shiftsByEmployee:
      shiftsByEmployee,

    ratesByEmployee:
      ratesByEmployee,

    plannedSlotsByEmployee:
      plannedSlotsByEmployee,

    closuresByKey:
      closuresByKey

  };

}



/* ============================================================
   VÝPOČET TÝDNE Z PŘEDNAČTENÝCH DAT
============================================================ */

function buildWeeklyDataFromContext_(
  employeeId,
  weekStart,
  weekEnd,
  context
) {

  const employeeKey =
    String(
      employeeId
    );


  const shifts =
    context.shiftsByEmployee[
      employeeKey
    ] ||
    [];


  const rates =
    context.ratesByEmployee[
      employeeKey
    ] ||
    [];


  let workedMinutes =
    0;


  let baseAmount =
    0;


  let shiftCount =
    0;


  let latestActualEnd =
    null;


  shifts.forEach(
    function(shift) {

      const workDate =
        normalizeDateOnly_(
          shift.work_date
        );


      if (
        !workDate ||
        workDate <
          weekStart ||
        workDate >
          weekEnd
      ) {

        return;

      }


      if (
        String(
          shift.status
        ) ===
        'OPEN'
      ) {

        return;

      }


      const minutes =
        weeklyNumber_(
          shift.worked_minutes
        );


      workedMinutes +=
        minutes;


      if (
        minutes >
        0
      ) {

        shiftCount++;

      }


      const clockIn =
        parseEmployeeDate_(
          shift.clock_in
        );


      const rate =
        employeeRateAt_(
          rates,
          employeeId,
          clockIn ||
          new Date(
            workDate
          )
        );


      baseAmount +=
        (
          minutes /
          60
        ) *
        rate;


      const clockOut =
        parseEmployeeDate_(
          shift.clock_out
        );


      if (
        clockOut &&
        (
          !latestActualEnd ||
          clockOut >
          latestActualEnd
        )
      ) {

        latestActualEnd =
          clockOut;

      }

    }
  );


  const plannedEnd =
    getLastPlannedShiftEndFromContext_(
      employeeId,
      weekStart,
      weekEnd,
      context
    );


  let lastShiftEnd =
    plannedEnd ||
    latestActualEnd;


  if (
    plannedEnd &&
    latestActualEnd &&
    latestActualEnd >
    plannedEnd
  ) {

    lastShiftEnd =
      latestActualEnd;

  }


  const deadline =
    lastShiftEnd
      ? new Date(
          lastShiftEnd.getTime() +
          12 *
          60 *
          60 *
          1000
        )
      : null;


  return {

    has_activity:
      (
        shiftCount >
        0 ||
        !!plannedEnd
      ),

    worked_minutes:
      Math.round(
        workedMinutes
      ),

    shift_count:
      shiftCount,

    base_amount:
      Math.round(
        baseAmount
      ),

    last_shift_end:
      lastShiftEnd
        ? formatWeeklyDateTime_(
            lastShiftEnd
          )
        : '',

    deadline_at:
      deadline
        ? formatWeeklyDateTime_(
            deadline
          )
        : ''

  };

}



/* ============================================================
   POSLEDNÍ PLÁNOVANÁ SMĚNA Z PŘEDNAČTENÝCH DAT
============================================================ */

function getLastPlannedShiftEndFromContext_(
  employeeId,
  weekStart,
  weekEnd,
  context
) {

  const slots =
    context.plannedSlotsByEmployee[
      String(
        employeeId
      )
    ] ||
    [];


  let latest =
    null;


  slots.forEach(
    function(slot) {

      const date =
        normalizeDateOnly_(
          slot.date
        );


      if (
        !date ||
        date <
          weekStart ||
        date >
          weekEnd
      ) {

        return;

      }


      const end =
        weeklySlotEnd_(
          slot
        );


      if (
        end &&
        (
          !latest ||
          end >
          latest
        )
      ) {

        latest =
          end;

      }

    }
  );


  return latest;

}



/* ============================================================
   UZÁVĚRKA Z REQUEST CONTEXTU

   Důležité:
   - tabulku WEEKLY_CLOSURES nečteme znovu pro každého člověka
   - update děláme pouze pokud se skutečně změnila data
============================================================ */

function ensureWeeklyClosureWithContext_(
  employeeId,
  weekStart,
  weekEnd,
  data,
  context
) {

  const key =
    weeklyClosureKey_(
      employeeId,
      weekStart
    );


  let closure =
    context.closuresByKey[
      key
    ];


  if (
    !closure
  ) {

    const weeklyId =
      uuid_(
        'WEEK'
      );


    closure =
      {

        weekly_id:
          weeklyId,

        employee_id:
          employeeId,

        week_start:
          weekStart,

        week_end:
          weekEnd,

        last_shift_end:
          data.last_shift_end,

        deadline_at:
          data.deadline_at,

        worked_minutes:
          data.worked_minutes,

        base_amount:
          data.base_amount,

        tips_amount:
          0,

        bonus_amount:
          0,

        sales_bonus_amount:
          getSalesBonusForEmployeeWeek_(
            employeeId,
            weekStart,
            weekEnd
          ),

        other_amount:
          0,

        final_amount:
          data.base_amount,

        employee_confirmed_at:
          '',

        status:
          initialWeeklyStatus_(
            data
          ),

        paid_at:
          '',

        paid_by:
          '',

        updated_at:
          now_()

      };


    append_(
      WEEKLY_SHEETS.CLOSURES,
      closure
    );


    context.closuresByKey[
      key
    ] =
      closure;


    return closure;

  }


  if (
    [
      'EMPLOYEE_CONFIRMED',
      'READY_TO_PAY',
      'PAID'
    ].includes(
      String(
        closure.status
      )
    )
  ) {

    return closure;

  }


  const finalAmount =
    data.base_amount +
    weeklyNumber_(
      closure.tips_amount
    ) +
    weeklyNumber_(
      closure.bonus_amount
    ) +
    getSalesBonusForEmployeeWeek_(
      employeeId,
      weekStart,
      weekEnd
    ) +
    weeklyNumber_(
      closure.other_amount
    );


  const changed =
    String(
      closure.week_end ||
      ''
    ) !==
      String(
        weekEnd
      ) ||

    String(
      closure.last_shift_end ||
      ''
    ) !==
      String(
        data.last_shift_end ||
        ''
      ) ||

    String(
      closure.deadline_at ||
      ''
    ) !==
      String(
        data.deadline_at ||
        ''
      ) ||

    Number(
      closure.worked_minutes ||
      0
    ) !==
      Number(
        data.worked_minutes ||
        0
      ) ||

    Number(
      closure.base_amount ||
      0
    ) !==
      Number(
        data.base_amount ||
        0
      ) ||

    Number(
      closure.final_amount ||
      0
    ) !==
      Number(
        finalAmount ||
        0
      );


  if (
    changed
  ) {

    const update =
      {

        week_end:
          weekEnd,

        last_shift_end:
          data.last_shift_end,

        deadline_at:
          data.deadline_at,

        worked_minutes:
          data.worked_minutes,

        base_amount:
          data.base_amount,

        final_amount:
          finalAmount,

        updated_at:
          now_()

      };


    updateBy_(
      WEEKLY_SHEETS.CLOSURES,
      'weekly_id',
      closure.weekly_id,
      update
    );


    Object.keys(
      update
    ).forEach(
      function(field) {

        closure[
          field
        ] =
          update[
            field
          ];

      }
    );

  }


  return closure;

}



/* ============================================================
   KLÍČ TÝDENNÍ UZÁVĚRKY
============================================================ */

function weeklyClosureKey_(
  employeeId,
  weekStart
) {

  return (
    String(
      employeeId ||
      ''
    ) +
    '|' +
    String(
      weekStart ||
      ''
    )
  );

}


/* ============================================================
   STAVY
============================================================ */

function initialWeeklyStatus_(
  data
) {

  const now =
    new Date();


  const lastShiftEnd =
    parseWeeklyDateTime_(
      data.last_shift_end
    );


  const deadline =
    parseWeeklyDateTime_(
      data.deadline_at
    );


  if (
    lastShiftEnd &&
    now <
    lastShiftEnd
  ) {

    return 'IN_PROGRESS';

  }


  if (
    deadline &&
    now >
    deadline
  ) {

    return 'OVERDUE';

  }


  return 'WAITING_EMPLOYEE';

}


function computeWeeklyStatus_(
  closure,
  now
) {

  const current =
    String(
      closure.status ||
      ''
    );


  if (
    [
      'EMPLOYEE_CONFIRMED',
      'READY_TO_PAY',
      'PAID'
    ].includes(
      current
    )
  ) {

    return current;

  }


  const lastShiftEnd =
    parseWeeklyDateTime_(
      closure.last_shift_end
    );


  const deadline =
    parseWeeklyDateTime_(
      closure.deadline_at
    );


  if (
    lastShiftEnd &&
    now <
    lastShiftEnd
  ) {

    return 'IN_PROGRESS';

  }


  if (
    deadline &&
    now >
    deadline
  ) {

    return 'OVERDUE';

  }


  return 'WAITING_EMPLOYEE';

}



/* ============================================================
   TÝDEN PO–NE
============================================================ */

function getWeekRange_(
  dateValue
) {

  let date;


  if (
    dateValue &&
    /^\d{4}-\d{2}-\d{2}$/.test(
      String(
        dateValue
      )
    )
  ) {

    const parts =
      String(
        dateValue
      ).split(
        '-'
      );


    date =
      new Date(
        Number(
          parts[0]
        ),
        Number(
          parts[1]
        ) - 1,
        Number(
          parts[2]
        ),
        12,
        0,
        0
      );

  } else {

    date =
      new Date();

  }


  const day =
    date.getDay();


  const mondayOffset =
    day === 0
      ? -6
      : 1 - day;


  const monday =
    new Date(
      date
    );


  monday.setDate(
    monday.getDate() +
    mondayOffset
  );


  const sunday =
    new Date(
      monday
    );


  sunday.setDate(
    sunday.getDate() +
    6
  );


  return {

    start:
      Utilities.formatDate(
        monday,
        CFG.TZ,
        'yyyy-MM-dd'
      ),

    end:
      Utilities.formatDate(
        sunday,
        CFG.TZ,
        'yyyy-MM-dd'
      )

  };

}



/* ============================================================
   HELPERS
============================================================ */

function findWeeklyClosureById_(
  weeklyId
) {

  return displayRows_(
    WEEKLY_SHEETS.CLOSURES
  )
    .find(
      function(row) {

        return (
          String(
            row.weekly_id
          ) ===
          String(
            weeklyId
          )
        );

      }
    ) ||
    null;

}


function normalizeDateOnly_(
  value
) {

  if (
    !value
  ) {

    return '';

  }


  if (
    value instanceof Date
  ) {

    return Utilities.formatDate(
      value,
      CFG.TZ,
      'yyyy-MM-dd'
    );

  }


  const text =
    String(
      value
    );


  const iso =
    text.match(
      /^(\d{4}-\d{2}-\d{2})/
    );


  if (
    iso
  ) {

    return iso[1];

  }


  const date =
    new Date(
      text
    );


  if (
    isNaN(
      date.getTime()
    )
  ) {

    return '';

  }


  return Utilities.formatDate(
    date,
    CFG.TZ,
    'yyyy-MM-dd'
  );

}


function weeklySlotEnd_(
  slot
) {

  const date =
    normalizeDateOnly_(
      slot.date
    );


  if (
    !date
  ) {

    return null;

  }


  const dateParts =
    date.split(
      '-'
    );


  const startParts =
    String(
      slot.start_time ||
      '00:00'
    ).split(
      ':'
    );


  const endParts =
    String(
      slot.end_time ||
      '00:00'
    ).split(
      ':'
    );


  const start =
    new Date(
      Number(
        dateParts[0]
      ),
      Number(
        dateParts[1]
      ) - 1,
      Number(
        dateParts[2]
      ),
      Number(
        startParts[0]
      ),
      Number(
        startParts[1] ||
        0
      ),
      0
    );


  const end =
    new Date(
      Number(
        dateParts[0]
      ),
      Number(
        dateParts[1]
      ) - 1,
      Number(
        dateParts[2]
      ),
      Number(
        endParts[0]
      ),
      Number(
        endParts[1] ||
        0
      ),
      0
    );


  if (
    end <=
    start
  ) {

    end.setDate(
      end.getDate() +
      1
    );

  }


  return end;

}


function formatWeeklyDateTime_(
  date
) {

  return Utilities.formatDate(
    date,
    CFG.TZ,
    'yyyy-MM-dd HH:mm:ss'
  );

}


function parseWeeklyDateTime_(
  value
) {

  if (
    !value
  ) {

    return null;

  }


  if (
    value instanceof Date
  ) {

    return value;

  }


  const text =
    String(
      value
    ).trim();


  const match =
    text.match(
      /^(\d{4})-(\d{2})-(\d{2})[ T](\d{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
      )
    );

  }


  const date =
    new Date(
      text
    );


  return isNaN(
    date.getTime()
  )
    ? null
    : date;

}


function weeklyNumber_(
  value
) {

  const number =
    Number(
      String(
        value == null
          ? 0
          : value
      )
        .replace(
          ',',
          '.'
        )
    );


  return isFinite(
    number
  )
    ? number
    : 0;

}


/* ============================================================
   VYTVOŘENÍ LISTU
============================================================ */

function ensureWeeklySheet_(
  sheetName,
  headers
) {

  const employeeSheet =
    sh_(
      CFG.SHEETS.EMPLOYEES
    );


  const spreadsheet =
    employeeSheet
      .getParent();


  let sheet =
    spreadsheet
      .getSheetByName(
        sheetName
      );


  if (
    !sheet
  ) {

    sheet =
      spreadsheet
        .insertSheet(
          sheetName
        );

  }


  if (
    sheet.getLastRow() ===
    0
  ) {

    sheet
      .getRange(
        1,
        1,
        1,
        headers.length
      )
      .setValues(
        [
          headers
        ]
      );


    sheet.setFrozenRows(
      1
    );

  }


  return sheet;

}


/* ============================================================
   DIAGNOSTIKA
============================================================ */

function testWeeklyPayroll() {

  Logger.log(
    'WEEKLY_CLOSURES: ' +
    JSON.stringify(
      displayRows_(
        WEEKLY_SHEETS.CLOSURES
      )
    )
  );


  Logger.log(
    'WeeklyPayrollService OK'
  );

}
