function getMyMonthClosure(token, period) {

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


  if (
    !user.employee_id
  ) {

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

  }


  period =
    String(
      period ||
      Utilities.formatDate(
        new Date(),
        CFG.TZ,
        'yyyy-MM'
      )
    ).trim();


  if (
    !/^\d{4}-\d{2}$/.test(
      period
    )
  ) {

    throw new Error(
      'Neplatné období.'
    );

  }


  /*
   * Načteme uzávěrky jako TEXT.
   * Žádné Date objekty posílané do browseru.
   */

  let closures =
    displayRows_(
      CFG.SHEETS.MONTH_CLOSURES
    );


  let closure =
    closures.find(
      function(row) {

        return (
          String(
            row.employee_id
          ) ===
          String(
            user.employee_id
          ) &&
          String(
            row.period
          ) ===
          period
        );

      }
    );


  /*
   * Pokud ještě uzávěrka neexistuje,
   * automaticky ji založíme.
   */

  if (
    !closure
  ) {

    const closureId =
      uuid_(
        'CLOSE'
      );


    append_(
      CFG.SHEETS.MONTH_CLOSURES,
      {

        closure_id:
          closureId,

        employee_id:
          user.employee_id,

        period:
          period,

        employee_confirmed_at:
          '',

        manager_approved_at:
          '',

        manager_approved_by:
          '',

        locked_at:
          '',

        status:
          'WAITING_EMPLOYEE'

      }
    );


    /*
     * Znovu načteme už jako text.
     */

    closures =
      displayRows_(
        CFG.SHEETS.MONTH_CLOSURES
      );


    closure =
      closures.find(
        function(row) {

          return (
            String(
              row.closure_id
            ) ===
            String(
              closureId
            )
          );

        }
      );

  }


  if (
    !closure
  ) {

    throw new Error(
      'Nepodařilo se vytvořit měsíční uzávěrku.'
    );

  }


  const summary =
    getClosureSummary_(
      user.employee_id,
      period
    );


  /*
   * Do browseru pouze primitivní typy.
   */

  return {

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

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

    period:
      String(
        closure.period ||
        period
      ),

    status:
      String(
        closure.status ||
        'WAITING_EMPLOYEE'
      ),

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

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

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

    summary: {

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

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

      earned_estimate:
        Number(
          summary.earned_estimate ||
          0
        ),

      night_minutes:
        Number(
          summary.night_minutes ||
          0
        ),

      weekend_minutes:
        Number(
          summary.weekend_minutes ||
          0
        )

    }

  };

}



/* ============================================================
   POTVRZENÍ DOCHÁZKY ZAMĚSTNANCEM
============================================================ */

function confirmMyMonthClosure(
  token,
  period
) {

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


  if (
    !user.employee_id
  ) {

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

  }


  period =
    String(
      period || ''
    ).trim();


  if (
    !/^\d{4}-\d{2}$/.test(
      period
    )
  ) {

    throw new Error(
      'Neplatné období.'
    );

  }


  const lock =
    LockService
      .getScriptLock();


  lock.waitLock(
    10000
  );


  try {

    /*
     * Nejdřív si uzávěrku vyžádáme.
     * Tím zajistíme, že existuje.
     */

    const closureData =
      getMyMonthClosure(
        token,
        period
      );


    const closureId =
      closureData.closure_id;


    const currentStatus =
      String(
        closureData.status ||
        ''
      );


    if (
      currentStatus ===
      'LOCKED'
    ) {

      throw new Error(
        'Docházka je už uzamčena.'
      );

    }


    if (
      currentStatus ===
      'MANAGER_APPROVED'
    ) {

      throw new Error(
        'Docházka už byla schválena vedoucím.'
      );

    }


    if (
      currentStatus ===
      'EMPLOYEE_CONFIRMED'
    ) {

      return {
        ok:true
      };

    }


    /*
     * Kontrola otevřené směny v daném měsíci.
     */

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


    const openShift =
      shifts.find(
        function(shift) {

          if (
            String(
              shift.employee_id
            ) !==
            String(
              user.employee_id
            )
          ) {

            return false;

          }


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

            return false;

          }


          const date =
            parseEmployeeDate_(
              shift.clock_in
            );


          if (
            !date
          ) {

            return false;

          }


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

        }
      );


    if (
      openShift
    ) {

      throw new Error(
        'V tomto měsíci máš stále otevřenou směnu.'
      );

    }


    /*
     * Nevyřešené žádosti o opravu.
     */

    const requests =
      displayRows_(
        CFG.SHEETS.SHIFT_CHANGE_REQUESTS
      );


    const pendingRequest =
      requests.find(
        function(request) {

          return (
            String(
              request.employee_id
            ) ===
            String(
              user.employee_id
            ) &&
            String(
              request.status
            ) ===
            'PENDING'
          );

        }
      );


    if (
      pendingRequest
    ) {

      throw new Error(
        'Nejdřív je potřeba vyřešit žádost o opravu docházky.'
      );

    }


    updateBy_(
      CFG.SHEETS.MONTH_CLOSURES,
      'closure_id',
      closureId,
      {

        employee_confirmed_at:
          now_(),

        status:
          'EMPLOYEE_CONFIRMED'

      }
    );


    setMonthShiftStatus_(
      user.employee_id,
      period,
      'EMPLOYEE_CONFIRMED'
    );


    audit_(
      user.user_id,
      'MONTH_CONFIRMED',
      'MONTH_CLOSURE',
      closureId,
      currentStatus,
      'EMPLOYEE_CONFIRMED'
    );


    notifyManagers_(
      user.employee_id,
      'Docházka čeká na schválení',
      'Zaměstnanec potvrdil docházku za ' +
      period +
      '.'
    );


    return {

      ok:true

    };

  }

  finally {

    lock.releaseLock();

  }

}



/* ============================================================
   SOUHRN MĚSÍCE
============================================================ */

function getClosureSummary_(
  employeeId,
  period
) {

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


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


  let workedMinutes =
    0;


  let shiftCount =
    0;


  let earnedEstimate =
    0;


  let nightMinutes =
    0;


  let weekendMinutes =
    0;


  shifts.forEach(
    function(shift) {

      if (
        String(
          shift.employee_id
        ) !==
        String(
          employeeId
        )
      ) {

        return;

      }


      const date =
        parseEmployeeDate_(
          shift.clock_in
        );


      if (
        !date
      ) {

        return;

      }


      const shiftPeriod =
        Utilities.formatDate(
          date,
          CFG.TZ,
          'yyyy-MM'
        );


      if (
        shiftPeriod !==
        period
      ) {

        return;

      }


      /*
       * Otevřenou směnu nepočítáme.
       */

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

        return;

      }


      const minutes =
        numberFromSheet_(
          shift.worked_minutes
        );


      const night =
        numberFromSheet_(
          shift.night_minutes
        );


      const weekend =
        numberFromSheet_(
          shift.weekend_minutes
        );


      workedMinutes +=
        minutes;


      nightMinutes +=
        night;


      weekendMinutes +=
        weekend;


      if (
        minutes >
        0
      ) {

        shiftCount++;

      }


      const hourlyRate =
        employeeRateAt_(
          rates,
          employeeId,
          date
        );


      earnedEstimate +=
        (
          minutes /
          60
        ) *
        hourlyRate;

    }
  );


  return {

    worked_minutes:
      Math.round(
        workedMinutes
      ),

    shift_count:
      shiftCount,

    earned_estimate:
      Math.round(
        earnedEstimate
      ),

    night_minutes:
      Math.round(
        nightMinutes
      ),

    weekend_minutes:
      Math.round(
        weekendMinutes
      )

  };

}



/* ============================================================
   ŽÁDOST O OPRAVU SMĚNY
============================================================ */

function createAttendanceCorrectionRequest(
  token,
  shiftId,
  requestedClockIn,
  requestedClockOut,
  reason
) {

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


  if (
    !user.employee_id
  ) {

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

  }


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


  const shift =
    shifts.find(
      function(row) {

        return (
          String(
            row.shift_id
          ) ===
          String(
            shiftId
          ) &&
          String(
            row.employee_id
          ) ===
          String(
            user.employee_id
          )
        );

      }
    );


  if (
    !shift
  ) {

    throw new Error(
      'Směna nebyla nalezena.'
    );

  }


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

    throw new Error(
      'Uzamčenou směnu už nelze upravit.'
    );

  }


  reason =
    String(
      reason ||
      ''
    ).trim();


  if (
    reason.length <
    3
  ) {

    throw new Error(
      'Doplň důvod opravy.'
    );

  }


  const requestId =
    uuid_(
      'FIX'
    );


  append_(
    CFG.SHEETS.SHIFT_CHANGE_REQUESTS,
    {

      request_id:
        requestId,

      shift_id:
        shift.shift_id,

      employee_id:
        user.employee_id,

      field:
        'SHIFT_TIME',

      old_value:
        JSON.stringify({

          clock_in:
            shift.clock_in,

          clock_out:
            shift.clock_out

        }),

      requested_value:
        JSON.stringify({

          clock_in:
            requestedClockIn ||
            '',

          clock_out:
            requestedClockOut ||
            ''

        }),

      reason:
        reason,

      status:
        'PENDING',

      created_at:
        now_(),

      resolved_at:
        '',

      resolved_by:
        ''

    }
  );


  audit_(
    user.user_id,
    'ATTENDANCE_CORRECTION_REQUESTED',
    'SHIFT',
    shift.shift_id,
    '',
    requestId
  );


  notifyManagers_(
    user.employee_id,
    'Nová žádost o opravu docházky',
    'Zaměstnanec požádal o opravu směny.'
  );


  return {

    ok:true

  };

}



/* ============================================================
   ADMIN – ČEKAJÍCÍ UZÁVĚRKY
============================================================ */

function getPendingClosures(token) {

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


  const closures =
    displayRows_(
      CFG.SHEETS.MONTH_CLOSURES
    );


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


  const employeeMap =
    {};


  employees.forEach(
    function(employee) {

      employeeMap[
        String(
          employee.employee_id
        )
      ] =
        employee;

    }
  );


  return closures
    .filter(
      function(closure) {

        return [
          'EMPLOYEE_CONFIRMED',
          'MANAGER_APPROVED'
        ].includes(
          String(
            closure.status
          )
        );

      }
    )
    .map(
      function(closure) {

        const employee =
          employeeMap[
            String(
              closure.employee_id
            )
          ];


        const summary =
          getClosureSummary_(
            closure.employee_id,
            closure.period
          );


        return {

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

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

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

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

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

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

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

          earned_estimate:
            Number(
              summary.earned_estimate ||
              0
            )

        };

      }
    );

}



/* ============================================================
   ADMIN / MANAGER – SCHVÁLENÍ
============================================================ */

function managerApproveClosure(
  token,
  closureId
) {

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


  const closures =
    displayRows_(
      CFG.SHEETS.MONTH_CLOSURES
    );


  const closure =
    closures.find(
      function(row) {

        return (
          String(
            row.closure_id
          ) ===
          String(
            closureId
          )
        );

      }
    );


  if (
    !closure
  ) {

    throw new Error(
      'Uzávěrka nebyla nalezena.'
    );

  }


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

    throw new Error(
      'Docházku zatím nepotvrdil zaměstnanec.'
    );

  }


  const requests =
    displayRows_(
      CFG.SHEETS.SHIFT_CHANGE_REQUESTS
    );


  const pending =
    requests.find(
      function(request) {

        return (
          String(
            request.employee_id
          ) ===
          String(
            closure.employee_id
          ) &&
          String(
            request.status
          ) ===
          'PENDING'
        );

      }
    );


  if (
    pending
  ) {

    throw new Error(
      'Zaměstnanec má nevyřešenou žádost o opravu docházky.'
    );

  }


  updateBy_(
    CFG.SHEETS.MONTH_CLOSURES,
    'closure_id',
    closureId,
    {

      manager_approved_at:
        now_(),

      manager_approved_by:
        user.user_id,

      status:
        'MANAGER_APPROVED'

    }
  );


  setMonthShiftStatus_(
    closure.employee_id,
    closure.period,
    'MANAGER_APPROVED'
  );


  audit_(
    user.user_id,
    'MONTH_APPROVED',
    'MONTH_CLOSURE',
    closureId,
    'EMPLOYEE_CONFIRMED',
    'MANAGER_APPROVED'
  );


  notifyEmployee_(
    closure.employee_id,
    'Docházka schválena',
    'Docházka za ' +
    closure.period +
    ' byla schválena.'
  );


  return {

    ok:true

  };

}



/* ============================================================
   ADMIN – UZAMČENÍ
============================================================ */

function lockMonthClosure(
  token,
  closureId
) {

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


  const closures =
    displayRows_(
      CFG.SHEETS.MONTH_CLOSURES
    );


  const closure =
    closures.find(
      function(row) {

        return (
          String(
            row.closure_id
          ) ===
          String(
            closureId
          )
        );

      }
    );


  if (
    !closure
  ) {

    throw new Error(
      'Uzávěrka nebyla nalezena.'
    );

  }


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

    throw new Error(
      'Uzávěrku lze zamknout až po schválení.'
    );

  }


  updateBy_(
    CFG.SHEETS.MONTH_CLOSURES,
    'closure_id',
    closureId,
    {

      locked_at:
        now_(),

      status:
        'LOCKED'

    }
  );


  setMonthShiftStatus_(
    closure.employee_id,
    closure.period,
    'LOCKED'
  );


  createPayrollDraft_(
    closure.employee_id,
    closure.period
  );


  audit_(
    user.user_id,
    'MONTH_LOCKED',
    'MONTH_CLOSURE',
    closureId,
    'MANAGER_APPROVED',
    'LOCKED'
  );


  notifyEmployee_(
    closure.employee_id,
    'Docházka uzavřena',
    'Docházka za ' +
    closure.period +
    ' byla uzavřena a předána ke zpracování.'
  );


  return {

    ok:true

  };

}



/* ============================================================
   PŘEPIS STAVU SMĚN V MĚSÍCI
============================================================ */

function setMonthShiftStatus_(
  employeeId,
  period,
  newStatus
) {

  const sheet =
    sh_(
      CFG.SHEETS.SHIFTS
    );


  const range =
    sheet.getDataRange();


  const values =
    range.getValues();


  if (
    values.length <
    2
  ) {

    return;

  }


  const headers =
    values[0]
      .map(
        function(value) {

          return String(
            value
          );

        }
      );


  const employeeIndex =
    headers.indexOf(
      'employee_id'
    );


  const clockInIndex =
    headers.indexOf(
      'clock_in'
    );


  const statusIndex =
    headers.indexOf(
      'status'
    );


  const updatedIndex =
    headers.indexOf(
      'updated_at'
    );


  if (
    employeeIndex <
      0 ||
    clockInIndex <
      0 ||
    statusIndex <
      0
  ) {

    throw new Error(
      'Tabulka SHIFTS nemá potřebné sloupce.'
    );

  }


  for (
    let i = 1;
    i < values.length;
    i++
  ) {

    const row =
      values[i];


    if (
      String(
        row[
          employeeIndex
        ]
      ) !==
      String(
        employeeId
      )
    ) {

      continue;

    }


    const date =
      parseEmployeeDate_(
        row[
          clockInIndex
        ]
      );


    if (
      !date
    ) {

      continue;

    }


    const rowPeriod =
      Utilities.formatDate(
        date,
        CFG.TZ,
        'yyyy-MM'
      );


    if (
      rowPeriod !==
      period
    ) {

      continue;

    }


    /*
     * Otevřenou směnu nesmíme
     * přepsat na potvrzenou.
     */

    if (
      String(
        row[
          statusIndex
        ]
      ) ===
      'OPEN'
    ) {

      continue;

    }


    sheet
      .getRange(
        i + 1,
        statusIndex + 1
      )
      .setValue(
        newStatus
      );


    if (
      updatedIndex >=
      0
    ) {

      sheet
        .getRange(
          i + 1,
          updatedIndex + 1
        )
        .setValue(
          now_()
        );

    }

  }

}



/* ============================================================
   PAYROLL DRAFT
============================================================ */

function createPayrollDraft_(
  employeeId,
  period
) {

  const payrolls =
    displayRows_(
      CFG.SHEETS.PAYROLL
    );


  const existing =
    payrolls.find(
      function(row) {

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

      }
    );


  const summary =
    getClosureSummary_(
      employeeId,
      period
    );


  if (
    existing
  ) {

    updateBy_(
      CFG.SHEETS.PAYROLL,
      'payroll_id',
      existing.payroll_id,
      {

        approved_minutes:
          summary.worked_minutes,

        base_amount:
          summary.earned_estimate,

        final_amount:
          summary.earned_estimate,

        status:
          'READY_FOR_ACCOUNTANT',

        updated_at:
          now_()

      }
    );


    return;

  }


  append_(
    CFG.SHEETS.PAYROLL,
    {

      payroll_id:
        uuid_(
          'PAY'
        ),

      employee_id:
        employeeId,

      period:
        period,

      approved_minutes:
        summary.worked_minutes,

      base_amount:
        summary.earned_estimate,

      bonus_amount:
        0,

      tips_amount:
        0,

      other_amount:
        0,

      final_amount:
        summary.earned_estimate,

      status:
        'READY_FOR_ACCOUNTANT',

      payment_date:
        '',

      updated_at:
        now_()

    }
  );

}



/* ============================================================
   NOTIFIKACE ZAMĚSTNANCI
============================================================ */

function notifyEmployee_(
  employeeId,
  title,
  message
) {

  const users =
    displayRows_(
      CFG.SHEETS.USERS
    );


  const user =
    users.find(
      function(row) {

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

      }
    );


  if (
    !user
  ) {

    return;

  }


  append_(
    CFG.SHEETS.NOTIFICATIONS,
    {

      notification_id:
        uuid_(
          'NOT'
        ),

      user_id:
        user.user_id,

      type:
        'ATTENDANCE',

      title:
        title,

      message:
        message,

      url:
        '',

      read_at:
        '',

      created_at:
        now_(),

      email_sent_at:
        ''

    }
  );


  if (
    user.email
  ) {

    try {

      MailApp.sendEmail({

        to:
          user.email,

        subject:
          CFG.APP_NAME +
          ' – ' +
          title,

        body:
          message,

        name:
          CFG.APP_NAME

      });

    }

    catch(error) {

      /*
       * E-mail nesmí shodit
       * samotnou uzávěrku.
       */

      Logger.log(
        'EMAIL ERROR: ' +
        error
      );

    }

  }

}



/* ============================================================
   NOTIFIKACE ADMINŮM / MANAGERŮM
============================================================ */

function notifyManagers_(
  employeeId,
  title,
  message
) {

  const users =
    displayRows_(
      CFG.SHEETS.USERS
    );


  users
    .filter(
      function(user) {

        return [
          'ADMIN',
          'MANAGER'
        ].includes(
          String(
            user.role
          )
        );

      }
    )
    .forEach(
      function(user) {

        append_(
          CFG.SHEETS.NOTIFICATIONS,
          {

            notification_id:
              uuid_(
                'NOT'
              ),

            user_id:
              user.user_id,

            type:
              'ATTENDANCE',

            title:
              title,

            message:
              message,

            url:
              '',

            read_at:
              '',

            created_at:
              now_(),

            email_sent_at:
              ''

          }
        );

      }
    );

}



/* ============================================================
   DIAGNOSTICKÝ TEST

   Tuhle funkci můžeš ručně spustit.
============================================================ */

function testClosureDatabase() {

  Logger.log(
    'MONTH_CLOSURES: ' +
    JSON.stringify(
      displayRows_(
        CFG.SHEETS.MONTH_CLOSURES
      )
    )
  );


  Logger.log(
    'SHIFTS: ' +
    JSON.stringify(
      displayRows_(
        CFG.SHEETS.SHIFTS
      )
    )
  );


  Logger.log(
    'PAY_RATES: ' +
    JSON.stringify(
      displayRows_(
        CFG.SHEETS.PAY_RATES
      )
    )
  );


  Logger.log(
    'ClosureService OK'
  );

}
function testCreateClosureForEmployee() {

  const employeeId =
    'EMP_6C233E6102D94CE4';

  const period =
    '2026-08';


  let closures =
    displayRows_(
      CFG.SHEETS.MONTH_CLOSURES
    );


  let closure =
    closures.find(
      function(row) {

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

      }
    );


  if (
    !closure
  ) {

    append_(
      CFG.SHEETS.MONTH_CLOSURES,
      {

        closure_id:
          uuid_(
            'CLOSE'
          ),

        employee_id:
          employeeId,

        period:
          period,

        employee_confirmed_at:
          '',

        manager_approved_at:
          '',

        manager_approved_by:
          '',

        locked_at:
          '',

        status:
          'WAITING_EMPLOYEE'

      }
    );

  }


  Logger.log(
    JSON.stringify(
      displayRows_(
        CFG.SHEETS.MONTH_CLOSURES
      )
    )
  );

}