/* ============================================================
   MESTAFF – SHIFT PLANNING SERVICE
   Plánované směny jsou oddělené od skutečné docházky.

   Výchozí pravidla:
   NE–ČT  18:00–23:00, kapacita 1
   PÁ–SO  18:00–02:00, kapacita 1
          20:00–00:00, kapacita 1

   Automatické vytvoření dalšího měsíce:
   každý 10. den předchozího měsíce kolem 17:00
============================================================ */

const SHIFT_PLAN_SHEETS = {
  SLOTS: 'SHIFT_SLOTS',
  SIGNUPS: 'SHIFT_SIGNUPS'
};


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

function setupShiftPlanning() {

  ensureShiftPlanningSheet_(
    SHIFT_PLAN_SHEETS.SLOTS,
    [
      'slot_id',
      'date',
      'start_time',
      'end_time',
      'capacity',
      'location_id',
      'status',
      'note',
      'generated',
      'created_at',
      'updated_at'
    ]
  );

  ensureShiftPlanningSheet_(
    SHIFT_PLAN_SHEETS.SIGNUPS,
    [
      'signup_id',
      'slot_id',
      'employee_id',
      'status',
      'created_at',
      'cancelled_at',
      'cancelled_by'
    ]
  );

  createShiftPlanningTrigger_();

  Logger.log('Shift planning setup OK');

}


/* ============================================================
   AUTOMATICKÝ TRIGGER

   Apps Script měsíční trigger neumí garantovat přesně 17:00:00.
   atHour(17) znamená spuštění v hodinovém okně 17:00–18:00.
============================================================ */

function createShiftPlanningTrigger_() {

  const handler =
    'generateNextMonthShiftSlots';


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

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

      }
    )
    .forEach(
      function(trigger) {

        ScriptApp.deleteTrigger(
          trigger
        );

      }
    );


  ScriptApp
    .newTrigger(
      handler
    )
    .timeBased()
    .onMonthDay(
      10
    )
    .atHour(
      17
    )
    .create();

}


/* ============================================================
   GENERÁTOR DALŠÍHO MĚSÍCE
============================================================ */

function generateNextMonthShiftSlots() {

  const now =
    new Date();


  let year =
    now.getFullYear();


  let month =
    now.getMonth() + 1;


  if (
    month > 11
  ) {

    month =
      0;

    year++;

  }


  return generateShiftSlotsForMonth_(
    year,
    month
  );

}


/* ============================================================
   RUČNÝ GENERÁTOR PRO ADMINA

   monthNumber = 1–12
============================================================ */

function adminGenerateShiftMonth(
  token,
  year,
  monthNumber
) {

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


  year =
    Number(
      year
    );


  monthNumber =
    Number(
      monthNumber
    );


  if (
    !year ||
    monthNumber < 1 ||
    monthNumber > 12
  ) {

    throw new Error(
      'Neplatný měsíc.'
    );

  }


  return generateShiftSlotsForMonth_(
    year,
    monthNumber - 1
  );

}


/* ============================================================
   VLASTNÍ GENERÁTOR

   monthIndex = JS měsíc 0–11
============================================================ */

function generateShiftSlotsForMonth_(
  year,
  monthIndex
) {

  const slots =
    displayRows_(
      SHIFT_PLAN_SHEETS.SLOTS
    );


  const existingKeys =
    {};


  slots.forEach(
    function(slot) {

      existingKeys[
        shiftSlotKey_(
          slot.date,
          slot.start_time,
          slot.end_time,
          slot.location_id
        )
      ] =
        true;

    }
  );


  const defaultLocation =
    getDefaultPlanningLocation_();


  const days =
    new Date(
      year,
      monthIndex + 1,
      0
    ).getDate();


  let created =
    0;


  for (
    let day = 1;
    day <= days;
    day++
  ) {

    const date =
      new Date(
        year,
        monthIndex,
        day
      );


    const dow =
      date.getDay();


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


    let templates =
      [];


    /*
     * neděle = 0
     * pondělí = 1
     * ...
     * čtvrtek = 4
     */

    if (
      dow === 0 ||
      (
        dow >= 1 &&
        dow <= 4
      )
    ) {

      templates =
        [
          {
            start:'18:00',
            end:'23:00',
            capacity:1
          }
        ];

    }


    /*
     * pátek + sobota
     */

    if (
      dow === 5 ||
      dow === 6
    ) {

      templates =
        [
          {
            start:'18:00',
            end:'02:00',
            capacity:1
          },
          {
            start:'20:00',
            end:'00:00',
            capacity:1
          }
        ];

    }


    templates.forEach(
      function(template) {

        const key =
          shiftSlotKey_(
            dateText,
            template.start,
            template.end,
            defaultLocation
          );


        /*
         * idempotence:
         * při opakovaném spuštění nevznikne duplicita
         */

        if (
          existingKeys[
            key
          ]
        ) {

          return;

        }


        append_(
          SHIFT_PLAN_SHEETS.SLOTS,
          {

            slot_id:
              uuid_(
                'SLOT'
              ),

            date:
              dateText,

            start_time:
              template.start,

            end_time:
              template.end,

            capacity:
              template.capacity,

            location_id:
              defaultLocation,

            status:
              'OPEN_FOR_SIGNUP',

            note:
              '',

            generated:
              true,

            created_at:
              now_(),

            updated_at:
              now_()

          }
        );


        existingKeys[
          key
        ] =
          true;


        created++;

      }
    );

  }


  return {

    ok:true,

    year:
      year,

    month:
      monthIndex + 1,

    created:
      created

  };

}


/* ============================================================
   ADMIN – MĚSÍČNÍ PLÁN
============================================================ */

function getAdminShiftPlan(
  token,
  period
) {

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


  period =
    normalizePlanningPeriod_(
      period
    );


  const slots =
    displayRows_(
      SHIFT_PLAN_SHEETS.SLOTS
    );


  const signups =
    displayRows_(
      SHIFT_PLAN_SHEETS.SIGNUPS
    );


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


  const employeeMap =
    {};


  employees.forEach(
    function(employee) {

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

    }
  );


  const signupsBySlot =
    buildApprovedSignupsBySlot_(
      signups
    );


  return slots
    .filter(
      function(slot) {

        return (
          String(
            slot.date ||
            ''
          ).substring(
            0,
            7
          ) ===
          period
        );

      }
    )
    .map(
      function(slot) {

        const slotSignups =
          signupsBySlot[
            String(
              slot.slot_id
            )
          ] ||
          [];


        return {

          slot_id:
            String(
              slot.slot_id ||
              ''
            ),

          date:
            String(
              slot.date ||
              ''
            ),

          start_time:
            String(
              slot.start_time ||
              ''
            ),

          end_time:
            String(
              slot.end_time ||
              ''
            ),

          capacity:
            planningNumber_(
              slot.capacity
            ),

          location_id:
            String(
              slot.location_id ||
              ''
            ),

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

          note:
            String(
              slot.note ||
              ''
            ),

          occupied:
            slotSignups.length,

          employees:
            slotSignups.map(
              function(signup) {

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


                return {

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

                  name:
                    employee
                      ? (
                          String(
                            employee.first_name ||
                            ''
                          ) +
                          ' ' +
                          String(
                            employee.last_name ||
                            ''
                          )
                        ).trim()
                      : String(
                          signup.employee_id ||
                          ''
                        )

                };

              }
            )

        };

      }
    )
    .sort(
      shiftSlotSort_
    );

}


/* ============================================================
   ADMIN – ÚPRAVA KONKRÉTNÍ SMĚNY
============================================================ */

function updateShiftSlot(
  token,
  slotId,
  data
) {

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


  const slots =
    displayRows_(
      SHIFT_PLAN_SHEETS.SLOTS
    );


  const slot =
    slots.find(
      function(row) {

        return (
          String(
            row.slot_id
          ) ===
          String(
            slotId
          )
        );

      }
    );


  if (
    !slot
  ) {

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

  }


  const capacity =
    Number(
      data.capacity
    );


  if (
    !Number.isInteger(
      capacity
    ) ||
    capacity < 0
  ) {

    throw new Error(
      'Kapacita musí být celé číslo 0 nebo vyšší.'
    );

  }


  const signups =
    getApprovedSlotSignups_(
      slotId
    );


  if (
    capacity <
    signups.length
  ) {

    throw new Error(
      'Kapacitu nelze snížit pod počet již přihlášených zaměstnanců.'
    );

  }


  const status =
    capacity === 0
      ? 'CLOSED'
      : (
          signups.length >=
          capacity
            ? 'FULL'
            : 'OPEN_FOR_SIGNUP'
        );


  updateBy_(
    SHIFT_PLAN_SHEETS.SLOTS,
    'slot_id',
    slotId,
    {

      date:
        String(
          data.date ||
          slot.date
        ),

      start_time:
        normalizePlanningTime_(
          data.start_time
        ),

      end_time:
        normalizePlanningTime_(
          data.end_time
        ),

      capacity:
        capacity,

      status:
        status,

      note:
        String(
          data.note ||
          ''
        ),

      updated_at:
        now_()

    }
  );


  audit_(
    user.user_id,
    'SHIFT_SLOT_UPDATED',
    'SHIFT_SLOT',
    slotId,
    JSON.stringify(
      slot
    ),
    JSON.stringify(
      data
    )
  );


  return {

    ok:true

  };

}


/* ============================================================
   ADMIN – PŘIDÁNÍ MIMOŘÁDNÉ SMĚNY
============================================================ */

function createShiftSlot(
  token,
  data
) {

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


  const date =
    String(
      data.date ||
      ''
    );


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

    throw new Error(
      'Neplatné datum.'
    );

  }


  const startTime =
    normalizePlanningTime_(
      data.start_time
    );


  const endTime =
    normalizePlanningTime_(
      data.end_time
    );


  const capacity =
    Number(
      data.capacity
    );


  if (
    !Number.isInteger(
      capacity
    ) ||
    capacity < 1
  ) {

    throw new Error(
      'Kapacita musí být alespoň 1.'
    );

  }


  const locationId =
    String(
      data.location_id ||
      getDefaultPlanningLocation_()
    );


  const existing =
    displayRows_(
      SHIFT_PLAN_SHEETS.SLOTS
    )
      .find(
        function(slot) {

          return (
            shiftSlotKey_(
              slot.date,
              slot.start_time,
              slot.end_time,
              slot.location_id
            ) ===
            shiftSlotKey_(
              date,
              startTime,
              endTime,
              locationId
            )
          );

        }
      );


  if (
    existing
  ) {

    throw new Error(
      'Stejná směna už existuje.'
    );

  }


  const slotId =
    uuid_(
      'SLOT'
    );


  append_(
    SHIFT_PLAN_SHEETS.SLOTS,
    {

      slot_id:
        slotId,

      date:
        date,

      start_time:
        startTime,

      end_time:
        endTime,

      capacity:
        capacity,

      location_id:
        locationId,

      status:
        'OPEN_FOR_SIGNUP',

      note:
        String(
          data.note ||
          ''
        ),

      generated:
        false,

      created_at:
        now_(),

      updated_at:
        now_()

    }
  );


  audit_(
    user.user_id,
    'SHIFT_SLOT_CREATED',
    'SHIFT_SLOT',
    slotId,
    '',
    JSON.stringify(
      data
    )
  );


  return {

    ok:true,

    slot_id:
      slotId

  };

}


/* ============================================================
   ADMIN – RUČNÍ PŘIDÁNÍ ČLOVĚKA NA SMĚNU
============================================================ */

function adminAssignEmployeeToSlot(
  token,
  slotId,
  employeeId
) {

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


  return signupEmployeeToSlot_(
    slotId,
    employeeId,
    user.user_id
  );

}


/* ============================================================
   ADMIN – ODEBRÁNÍ ČLOVĚKA ZE SMĚNY
============================================================ */

function adminRemoveEmployeeFromSlot(
  token,
  slotId,
  employeeId
) {

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


  const signups =
    displayRows_(
      SHIFT_PLAN_SHEETS.SIGNUPS
    );


  const signup =
    signups.find(
      function(row) {

        return (
          String(
            row.slot_id
          ) ===
          String(
            slotId
          ) &&
          String(
            row.employee_id
          ) ===
          String(
            employeeId
          ) &&
          String(
            row.status
          ) ===
          'APPROVED'
        );

      }
    );


  if (
    !signup
  ) {

    throw new Error(
      'Přihlášení na směnu nebylo nalezeno.'
    );

  }


  updateBy_(
    SHIFT_PLAN_SHEETS.SIGNUPS,
    'signup_id',
    signup.signup_id,
    {

      status:
        'CANCELLED',

      cancelled_at:
        now_(),

      cancelled_by:
        user.user_id

    }
  );


  refreshSlotStatus_(
    slotId
  );


  audit_(
    user.user_id,
    'SHIFT_SIGNUP_REMOVED',
    'SHIFT_SLOT',
    slotId,
    employeeId,
    ''
  );


  return {

    ok:true

  };

}


/* ============================================================
   RYCHLÝ BUNDLE PRO ZAMĚSTNANECKÉ SMĚNY

   Jeden request + jedno čtení SHIFT_SLOTS + jedno čtení
   SHIFT_SIGNUPS.
============================================================ */

function getEmployeeShiftPlanningBundle(
  token,
  period
) {

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


  if (
    !user.employee_id
  ) {

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

  }


  period =
    normalizePlanningPeriod_(
      period
    );


  const slots =
    displayRows_(
      SHIFT_PLAN_SHEETS.SLOTS
    );


  const signups =
    displayRows_(
      SHIFT_PLAN_SHEETS.SIGNUPS
    );


  return {

    available:
      buildAvailableShiftSlotsFromRows_(
        user.employee_id,
        period,
        slots,
        signups
      ),

    mine:
      buildMyPlannedShiftsFromRows_(
        user.employee_id,
        period,
        slots,
        signups
      )

  };

}



/* ============================================================
   INDEX PŘIHLÁŠEK PODLE SLOTU
============================================================ */

function buildApprovedSignupsBySlot_(
  signups
) {

  const result =
    {};


  signups.forEach(
    function(signup) {

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

        return;

      }


      const slotId =
        String(
          signup.slot_id ||
          ''
        );


      if (
        !slotId
      ) {

        return;

      }


      if (
        !result[
          slotId
        ]
      ) {

        result[
          slotId
        ] =
          [];

      }


      result[
        slotId
      ].push(
        signup
      );

    }
  );


  return result;

}



/* ============================================================
   VOLNÉ SMĚNY Z JIŽ NAČTENÝCH ŘÁDKŮ
============================================================ */

function buildAvailableShiftSlotsFromRows_(
  employeeId,
  period,
  slots,
  signups
) {

  const employeeKey =
    String(
      employeeId
    );


  const signupsBySlot =
    buildApprovedSignupsBySlot_(
      signups
    );


  const mySlotMap =
    {};


  signups.forEach(
    function(signup) {

      if (
        String(
          signup.employee_id
        ) ===
          employeeKey &&
        String(
          signup.status
        ) ===
          'APPROVED'
      ) {

        mySlotMap[
          String(
            signup.slot_id
          )
        ] =
          true;

      }

    }
  );


  const result =
    [];


  slots.forEach(
    function(slot) {

      if (
        String(
          slot.date ||
          ''
        ).substring(
          0,
          7
        ) !==
        period
      ) {

        return;

      }


      if (
        String(
          slot.status
        ) !==
        'OPEN_FOR_SIGNUP'
      ) {

        return;

      }


      const slotId =
        String(
          slot.slot_id ||
          ''
        );


      if (
        mySlotMap[
          slotId
        ]
      ) {

        return;

      }


      const occupied =
        (
          signupsBySlot[
            slotId
          ] ||
          []
        ).length;


      const capacity =
        planningNumber_(
          slot.capacity
        );


      const freePlaces =
        Math.max(
          0,
          capacity -
          occupied
        );


      if (
        freePlaces <=
        0
      ) {

        return;

      }


      result.push(
        {

          slot_id:
            slotId,

          date:
            String(
              slot.date ||
              ''
            ),

          start_time:
            String(
              slot.start_time ||
              ''
            ),

          end_time:
            String(
              slot.end_time ||
              ''
            ),

          capacity:
            capacity,

          occupied:
            occupied,

          free_places:
            freePlaces,

          already_signed:
            false,

          note:
            String(
              slot.note ||
              ''
            )

        }
      );

    }
  );


  return result.sort(
    shiftSlotSort_
  );

}



/* ============================================================
   MOJE SMĚNY Z JIŽ NAČTENÝCH ŘÁDKŮ
============================================================ */

function buildMyPlannedShiftsFromRows_(
  employeeId,
  period,
  slots,
  signups
) {

  const slotMap =
    {};


  slots.forEach(
    function(slot) {

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

    }
  );


  const result =
    [];


  signups.forEach(
    function(signup) {

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

        return;

      }


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


      if (
        !slot
      ) {

        return;

      }


      if (
        String(
          slot.date ||
          ''
        ).substring(
          0,
          7
        ) !==
        period
      ) {

        return;

      }


      result.push(
        {

          signup_id:
            String(
              signup.signup_id ||
              ''
            ),

          slot_id:
            String(
              slot.slot_id ||
              ''
            ),

          date:
            String(
              slot.date ||
              ''
            ),

          start_time:
            String(
              slot.start_time ||
              ''
            ),

          end_time:
            String(
              slot.end_time ||
              ''
            ),

          note:
            String(
              slot.note ||
              ''
            )

        }
      );

    }
  );


  return result.sort(
    shiftSlotSort_
  );

}


/* ============================================================
   ZAMĚSTNANEC – VOLNÉ SMĚNY
============================================================ */

function getAvailableShiftSlots(
  token,
  period
) {

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


  if (
    !user.employee_id
  ) {

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

  }


  period =
    normalizePlanningPeriod_(
      period
    );


  const slots =
    displayRows_(
      SHIFT_PLAN_SHEETS.SLOTS
    );


  const signups =
    displayRows_(
      SHIFT_PLAN_SHEETS.SIGNUPS
    );


  return buildAvailableShiftSlotsFromRows_(
    user.employee_id,
    period,
    slots,
    signups
  );

}


/* ============================================================
   ZAMĚSTNANEC – PŘIHLÁŠENÍ NA SMĚNU
============================================================ */

function signupForShiftSlot(
  token,
  slotId
) {

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


  if (
    !user.employee_id
  ) {

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

  }


  return signupEmployeeToSlot_(
    slotId,
    user.employee_id,
    user.user_id
  );

}


/* ============================================================
   ZAMĚSTNANEC – MOJE SMĚNY
============================================================ */

function getMyPlannedShifts(
  token,
  period
) {

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


  if (
    !user.employee_id
  ) {

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

  }


  period =
    normalizePlanningPeriod_(
      period
    );


  const slots =
    displayRows_(
      SHIFT_PLAN_SHEETS.SLOTS
    );


  const signups =
    displayRows_(
      SHIFT_PLAN_SHEETS.SIGNUPS
    );


  return buildMyPlannedShiftsFromRows_(
    user.employee_id,
    period,
    slots,
    signups
  );

}


/* ============================================================
   ZAMĚSTNANEC – ZRUŠENÍ PŘIHLÁŠKY
============================================================ */

function cancelMyShiftSignup(
  token,
  slotId
) {

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


  if (
    !user.employee_id
  ) {

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

  }


  const signup =
    displayRows_(
      SHIFT_PLAN_SHEETS.SIGNUPS
    )
      .find(
        function(row) {

          return (
            String(
              row.slot_id
            ) ===
            String(
              slotId
            ) &&
            String(
              row.employee_id
            ) ===
            String(
              user.employee_id
            ) &&
            String(
              row.status
            ) ===
            'APPROVED'
          );

        }
      );


  if (
    !signup
  ) {

    throw new Error(
      'Na této směně nejsi přihlášen.'
    );

  }


  updateBy_(
    SHIFT_PLAN_SHEETS.SIGNUPS,
    'signup_id',
    signup.signup_id,
    {

      status:
        'CANCELLED',

      cancelled_at:
        now_(),

      cancelled_by:
        user.user_id

    }
  );


  refreshSlotStatus_(
    slotId
  );


  return {

    ok:true

  };

}


/* ============================================================
   INTERNÍ PŘIHLÁŠENÍ
============================================================ */

function signupEmployeeToSlot_(
  slotId,
  employeeId,
  actorUserId
) {

  const lock =
    LockService
      .getScriptLock();


  lock.waitLock(
    10000
  );


  try {

    const slots =
      displayRows_(
        SHIFT_PLAN_SHEETS.SLOTS
      );


    const slot =
      slots.find(
        function(row) {

          return (
            String(
              row.slot_id
            ) ===
            String(
              slotId
            )
          );

        }
      );


    if (
      !slot
    ) {

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

    }


    if (
      String(
        slot.status
      ) ===
      'CLOSED'
    ) {

      throw new Error(
        'Směna je uzavřená.'
      );

    }


    const existing =
      displayRows_(
        SHIFT_PLAN_SHEETS.SIGNUPS
      )
        .find(
          function(row) {

            return (
              String(
                row.slot_id
              ) ===
              String(
                slotId
              ) &&
              String(
                row.employee_id
              ) ===
              String(
                employeeId
              ) &&
              String(
                row.status
              ) ===
              'APPROVED'
            );

          }
        );


    if (
      existing
    ) {

      throw new Error(
        'Na této směně už jsi přihlášen.'
      );

    }


    assertNoPlanningCollision_(
      employeeId,
      slot
    );


    const approved =
      getApprovedSlotSignups_(
        slotId
      );


    const capacity =
      planningNumber_(
        slot.capacity
      );


    if (
      approved.length >=
      capacity
    ) {

      throw new Error(
        'Směna je už obsazená.'
      );

    }


    append_(
      SHIFT_PLAN_SHEETS.SIGNUPS,
      {

        signup_id:
          uuid_(
            'SIGN'
          ),

        slot_id:
          slotId,

        employee_id:
          employeeId,

        status:
          'APPROVED',

        created_at:
          now_(),

        cancelled_at:
          '',

        cancelled_by:
          ''

      }
    );


    refreshSlotStatus_(
      slotId
    );


    audit_(
      actorUserId,
      'SHIFT_SIGNUP_CREATED',
      'SHIFT_SLOT',
      slotId,
      '',
      employeeId
    );


    return {

      ok:true

    };

  }

  finally {

    lock.releaseLock();

  }

}


/* ============================================================
   KOLIZE SMĚN
============================================================ */

function assertNoPlanningCollision_(
  employeeId,
  targetSlot
) {

  const myShifts =
    getApprovedPlannedSlotsForEmployee_(
      employeeId
    );


  const targetStart =
    planningSlotStart_(
      targetSlot
    );


  const targetEnd =
    planningSlotEnd_(
      targetSlot
    );


  myShifts.forEach(
    function(slot) {

      const start =
        planningSlotStart_(
          slot
        );


      const end =
        planningSlotEnd_(
          slot
        );


      if (
        targetStart <
          end &&
        targetEnd >
          start
      ) {

        throw new Error(
          'Tato směna se překrývá s jinou směnou, na kterou už jsi přihlášen.'
        );

      }

    }
  );

}


/* ============================================================
   POMOCNÉ FUNKCE
============================================================ */

function getApprovedSlotSignups_(
  slotId
) {

  return displayRows_(
    SHIFT_PLAN_SHEETS.SIGNUPS
  )
    .filter(
      function(signup) {

        return (
          String(
            signup.slot_id
          ) ===
          String(
            slotId
          ) &&
          String(
            signup.status
          ) ===
          'APPROVED'
        );

      }
    );

}


function getApprovedPlannedSlotsForEmployee_(
  employeeId
) {

  const slots =
    displayRows_(
      SHIFT_PLAN_SHEETS.SLOTS
    );


  const slotMap =
    {};


  slots.forEach(
    function(slot) {

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

    }
  );


  return displayRows_(
    SHIFT_PLAN_SHEETS.SIGNUPS
  )
    .filter(
      function(signup) {

        return (
          String(
            signup.employee_id
          ) ===
          String(
            employeeId
          ) &&
          String(
            signup.status
          ) ===
          'APPROVED'
        );

      }
    )
    .map(
      function(signup) {

        return slotMap[
          String(
            signup.slot_id
          )
        ];

      }
    )
    .filter(
      Boolean
    );

}


function refreshSlotStatus_(
  slotId
) {

  const slots =
    displayRows_(
      SHIFT_PLAN_SHEETS.SLOTS
    );


  const slot =
    slots.find(
      function(row) {

        return (
          String(
            row.slot_id
          ) ===
          String(
            slotId
          )
        );

      }
    );


  if (
    !slot
  ) {

    return;

  }


  const capacity =
    planningNumber_(
      slot.capacity
    );


  const occupied =
    getApprovedSlotSignups_(
      slotId
    ).length;


  let status =
    'OPEN_FOR_SIGNUP';


  if (
    capacity <= 0
  ) {

    status =
      'CLOSED';

  } else if (
    occupied >=
    capacity
  ) {

    status =
      'FULL';

  }


  updateBy_(
    SHIFT_PLAN_SHEETS.SLOTS,
    'slot_id',
    slotId,
    {

      status:
        status,

      updated_at:
        now_()

    }
  );

}


function planningSlotStart_(
  slot
) {

  const parts =
    String(
      slot.date
    ).split(
      '-'
    );


  const time =
    String(
      slot.start_time
    ).split(
      ':'
    );


  return new Date(
    Number(
      parts[0]
    ),
    Number(
      parts[1]
    ) - 1,
    Number(
      parts[2]
    ),
    Number(
      time[0]
    ),
    Number(
      time[1] ||
      0
    )
  );

}


function planningSlotEnd_(
  slot
) {

  const start =
    planningSlotStart_(
      slot
    );


  const time =
    String(
      slot.end_time
    ).split(
      ':'
    );


  const end =
    new Date(
      start.getFullYear(),
      start.getMonth(),
      start.getDate(),
      Number(
        time[0]
      ),
      Number(
        time[1] ||
        0
      )
    );


  if (
    end <=
    start
  ) {

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

  }


  return end;

}


function shiftSlotKey_(
  date,
  start,
  end,
  locationId
) {

  return [
    String(
      date ||
      ''
    ).substring(
      0,
      10
    ),
    String(
      start ||
      ''
    ).substring(
      0,
      5
    ),
    String(
      end ||
      ''
    ).substring(
      0,
      5
    ),
    String(
      locationId ||
      ''
    )
  ].join(
    '|'
  );

}


function shiftSlotSort_(
  a,
  b
) {

  const aKey =
    String(
      a.date ||
      ''
    ) +
    ' ' +
    String(
      a.start_time ||
      ''
    );


  const bKey =
    String(
      b.date ||
      ''
    ) +
    ' ' +
    String(
      b.start_time ||
      ''
    );


  return aKey.localeCompare(
    bKey
  );

}


function normalizePlanningPeriod_(
  period
) {

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


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

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

  }


  return period;

}


function normalizePlanningTime_(
  value
) {

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


  if (
    !/^\d{1,2}:\d{2}$/.test(
      value
    )
  ) {

    throw new Error(
      'Neplatný čas.'
    );

  }


  const parts =
    value.split(
      ':'
    );


  const hour =
    Number(
      parts[0]
    );


  const minute =
    Number(
      parts[1]
    );


  if (
    hour < 0 ||
    hour > 23 ||
    minute < 0 ||
    minute > 59
  ) {

    throw new Error(
      'Neplatný čas.'
    );

  }


  return (
    String(
      hour
    ).padStart(
      2,
      '0'
    ) +
    ':' +
    String(
      minute
    ).padStart(
      2,
      '0'
    )
  );

}


function planningNumber_(
  value
) {

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


  return isFinite(
    number
  )
    ? number
    : 0;

}


/* ============================================================
   VÝCHOZÍ PROVOZOVNA
============================================================ */

function getDefaultPlanningLocation_() {

  const locations =
    displayRows_(
      CFG.SHEETS.LOCATIONS
    );


  if (
    locations.length
  ) {

    return String(
      locations[0]
        .location_id ||
      ''
    );

  }


  return 'LOCATION_MAIN';

}


/* ============================================================
   VYTVOŘENÍ LISTŮ

   Použije existující databázový spreadsheet.
============================================================ */

function ensureShiftPlanningSheet_(
  sheetName,
  headers
) {

  let sheet;


  try {

    sheet =
      sh_(
        sheetName
      );


    if (
      sheet
    ) {

      return sheet;

    }

  }

  catch(error) {

    /*
     * list zatím neexistuje
     */

  }


  const spreadsheet =
    getPlanningSpreadsheet_();


  sheet =
    spreadsheet
      .getSheetByName(
        sheetName
      );


  if (
    !sheet
  ) {

    sheet =
      spreadsheet
        .insertSheet(
          sheetName
        );

  }


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


  sheet
    .setFrozenRows(
      1
    );


  return sheet;

}


function getPlanningSpreadsheet_() {

  /*
   * Nejprve zkusíme aktivní spreadsheet.
   */

  const active =
    SpreadsheetApp
      .getActiveSpreadsheet();


  if (
    active
  ) {

    return active;

  }


  /*
   * Pokud je projekt standalone,
   * hledáme ID ve Script Properties.
   */

  const id =
    PropertiesService
      .getScriptProperties()
      .getProperty(
        'SPREADSHEET_ID'
      );


  if (
    id
  ) {

    return SpreadsheetApp
      .openById(
        id
      );

  }


  /*
   * Kompatibilita s případným CFG.SPREADSHEET_ID
   */

  if (
    typeof CFG !==
      'undefined' &&
    CFG.SPREADSHEET_ID
  ) {

    return SpreadsheetApp
      .openById(
        CFG.SPREADSHEET_ID
      );

  }


  throw new Error(
    'Nepodařilo se zjistit databázový spreadsheet.'
  );

}


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

function testShiftPlanning() {

  Logger.log(
    'SHIFT_SLOTS: ' +
    JSON.stringify(
      displayRows_(
        SHIFT_PLAN_SHEETS.SLOTS
      )
    )
  );


  Logger.log(
    'SHIFT_SIGNUPS: ' +
    JSON.stringify(
      displayRows_(
        SHIFT_PLAN_SHEETS.SIGNUPS
      )
    )
  );


  Logger.log(
    'ShiftPlanningService OK'
  );

}
