"use strict"; // ---------- Графики работы: конструктор ---------- const PRESETS = [ { name: "3 дня · отсыпной · 3 ночи · отсыпной · 3 выходных", steps: [ { type: "work", days: 3, start_time: "08:00", end_time: "20:00", label: "день" }, { type: "off", days: 1, label: "отсыпной" }, { type: "work", days: 3, start_time: "20:00", end_time: "08:00", label: "ночь" }, { type: "off", days: 1, label: "отсыпной" }, { type: "off", days: 3, label: "выходные" }] }, { name: "Сутки через трое", steps: [ { type: "work", days: 1, start_time: "08:00", end_time: "08:00", label: "сутки" }, { type: "off", days: 3, label: "выходные" }] }, { name: "2 дня / 2 ночи / 4 выходных", steps: [ { type: "work", days: 2, start_time: "08:00", end_time: "20:00", label: "день" }, { type: "work", days: 2, start_time: "20:00", end_time: "08:00", label: "ночь" }, { type: "off", days: 4, label: "выходные" }] }, ]; const hm = t => String(t || "").slice(0, 5); const dayWord = n => n === 1 ? "день" : (n >= 2 && n <= 4 ? "дня" : "дней"); function describeSchedule(r) { if (r.kind === "pattern") { const steps = (r.pattern || []).map(x => x.type === "work" ? `${x.days}×${x.label || "смена"} ${hm(x.start_time)}–${hm(x.end_time)}` : `${x.days}×${x.label || "вых."}`); const total = (r.pattern || []).reduce((a, x) => a + x.days, 0); return `Цикл ${total} дн.: ` + steps.join(" → "); } if (r.kind === "cycle") return `${r.work_days} через ${r.off_days}, ${hm(r.start_time)}–${hm(r.end_time)}`; return `${(r.weekdays || []).map(i => DAYS[i]).join(" ")}, ${hm(r.start_time)}–${hm(r.end_time)}`; } async function schedulesPage() { const box = h("div"), formBox = h("div"); $app.replaceChildren(h("div", { class: "row" }, h("h1", { style: "flex:1" }, "Графики работы"), h("button", { onclick: () => edit(null) }, "+ Добавить")), formBox, box); async function load() { const [rows, pos] = await Promise.all([api("GET", "/schedules"), api("GET", "/positions")]); box.replaceChildren(rows.length ? h("div", { class: "card tablewrap" }, h("table", {}, h("thead", {}, h("tr", {}, ...["Название", "Расписание", "Должности"].map(t => h("th", {}, t)), h("th"))), h("tbody", {}, ...rows.map(r => h("tr", {}, h("td", {}, r.name), h("td", { class: "small" }, describeSchedule(r)), h("td", { class: "small muted" }, pos.filter(p => p.schedule_id === r.id).map(p => p.name).join(", ") || "—"), h("td", { class: "row end" }, h("button", { class: "sec sm", onclick: () => edit(r) }, "Изменить"), h("button", { class: "sec sm", onclick: guard(async () => { if (confirm("Удалить график?")) { await api("DELETE", "/schedules/" + r.id); load(); } }) }, "✕"))))))) : h("div", { class: "card muted" }, "Пока пусто")); } function edit(row) { const st = row ? JSON.parse(JSON.stringify(row)) : { name: "", kind: "pattern", weekdays: [0, 1, 2, 3, 4], work_days: 2, off_days: 2, start_time: "08:00", end_time: "20:00", pattern: JSON.parse(JSON.stringify(PRESETS[0].steps)), cycle_start: new Date().toLocaleDateString("sv") }; st.pattern = st.pattern || []; st.cycle_start = st.cycle_start || new Date().toLocaleDateString("sv"); for (const x of st.pattern) { x.start_time = hm(x.start_time); x.end_time = hm(x.end_time); } st.start_time = hm(st.start_time); st.end_time = hm(st.end_time); const body = h("div"), preview = h("div"); const payload = () => ({ name: st.name, kind: st.kind, weekdays: st.weekdays, work_days: st.work_days, off_days: st.off_days, cycle_start: st.kind === "weekly" ? null : st.cycle_start || null, start_time: st.start_time, end_time: st.end_time, pattern: st.kind === "pattern" ? st.pattern.map(x => ({ type: x.type, days: +x.days || 1, start_time: x.start_time || "08:00", end_time: x.end_time || "20:00", label: x.label || "" })) : null, }); let timer; const refresh = () => { clearTimeout(timer); timer = setTimeout(guard(drawPreview), 250); }; async function drawPreview() { if (st.kind === "pattern" && !st.pattern.some(x => x.type === "work")) { preview.replaceChildren(h("p", { class: "muted small" }, "Добавьте хотя бы одну рабочую смену")); return; } const from = st.kind === "weekly" ? new Date().toLocaleDateString("sv") : st.cycle_start; let days; try { days = await api("POST", "/schedules/preview", { schedule: payload(), from_date: from, days: 42 }); } catch (e) { preview.replaceChildren(h("p", { class: "warnline" }, e.message)); return; } const first = (new Date(days[0].date + "T00:00").getDay() + 6) % 7; // сдвиг до понедельника const cells = [...Array(first).fill(null), ...days]; const grid = h("div", { class: "cal" }, ...DAYS.map(d => h("div", { class: "calh" }, d)), ...cells.map(d => { if (!d) return h("div"); const night = d.work && d.end.slice(0, 10) !== d.start.slice(0, 10); return h("div", { class: "calc " + (d.work ? (night ? "night" : "work") : "off") }, h("b", {}, new Date(d.date + "T00:00").getDate()), d.work ? h("span", {}, d.start.slice(11, 16) + "–" + d.end.slice(11, 16)) : h("span", {}, "—"), d.label ? h("i", {}, d.label) : null); })); preview.replaceChildren(h("h2", {}, "Как это выглядит (6 недель с " + new Date(from + "T00:00").toLocaleDateString("ru-RU") + ")"), grid, h("p", { class: "small muted" }, "Ночные смены показаны в день начала и заканчиваются утром следующего дня.")); } function stepRow(x, n) { const mv = d => () => { const j = n + d; [st.pattern[j], st.pattern[n]] = [st.pattern[n], st.pattern[j]]; draw(); }; return h("div", { class: "step " + x.type }, h("select", { onchange: e => { x.type = e.target.value; draw(); } }, h("option", { value: "work", selected: x.type === "work" }, "Смена"), h("option", { value: "off", selected: x.type === "off" }, "Выходной")), h("input", { type: "number", min: 1, max: 60, value: x.days, title: "Сколько дней подряд", onchange: e => { x.days = +e.target.value || 1; draw(); } }), h("span", { class: "small muted" }, dayWord(x.days)), x.type === "work" ? [h("input", { type: "time", value: x.start_time, onchange: e => { x.start_time = e.target.value; draw(); } }), "–", h("input", { type: "time", value: x.end_time, onchange: e => { x.end_time = e.target.value; draw(); } })] : h("span", { class: "spacer" }), h("input", { placeholder: x.type === "work" ? "метка: день/ночь" : "метка: отсыпной", value: x.label || "", onchange: e => { x.label = e.target.value; draw(); } }), h("button", { class: "sec sm", disabled: n === 0, onclick: mv(-1) }, "↑"), h("button", { class: "sec sm", disabled: n === st.pattern.length - 1, onclick: mv(1) }, "↓"), h("button", { class: "sec sm", onclick: () => { st.pattern.splice(n, 1); draw(); } }, "✕")); } function draw() { const name = h("input", { value: st.name, onchange: e => st.name = e.target.value }); const kind = h("select", { onchange: e => { st.kind = e.target.value; draw(); } }, ...[["pattern", "Цикл со своими шагами (конструктор)"], ["cycle", "Простой цикл N через M"], ["weekly", "По дням недели"]].map(([k, t]) => h("option", { value: k, selected: st.kind === k }, t))); const inp = (key, type, label) => h("div", {}, h("label", {}, label), h("input", { type, value: st[key] ?? "", onchange: e => { st[key] = type === "number" ? +e.target.value : e.target.value; draw(); } })); const parts = [h("div", { class: "grid" }, h("div", {}, h("label", {}, "Название *"), name), h("div", {}, h("label", {}, "Тип графика"), kind))]; if (st.kind === "pattern") { const total = st.pattern.reduce((a, x) => a + (+x.days || 1), 0); parts.push(h("h2", {}, "Шаги цикла"), h("p", { class: "small muted" }, "Шаги идут по кругу. Для ночной смены поставьте конец раньше начала (20:00–08:00), для суток — одинаковое время (08:00–08:00). «Отсыпной» — это просто выходной шаг с меткой."), h("div", { class: "row", style: "margin-bottom:8px" }, h("span", { class: "small muted" }, "Шаблоны:"), ...PRESETS.map(p => h("button", { class: "sec sm", onclick: () => { st.pattern = JSON.parse(JSON.stringify(p.steps)); draw(); } }, p.name))), ...st.pattern.map(stepRow), h("div", { class: "row", style: "margin-top:8px" }, h("button", { class: "sec", onclick: () => { st.pattern.push({ type: "work", days: 1, start_time: "08:00", end_time: "20:00", label: "" }); draw(); } }, "+ Смена"), h("button", { class: "sec", onclick: () => { st.pattern.push({ type: "off", days: 1, label: "" }); draw(); } }, "+ Выходной"), h("span", { class: "small muted" }, `Длина цикла: ${total} ${dayWord(total)}`)), h("div", { class: "grid", style: "margin-top:12px;max-width:320px" }, h("div", {}, h("label", {}, "Первый день цикла (с какой даты начинается шаг №1)"), h("input", { type: "date", value: st.cycle_start, onchange: e => { st.cycle_start = e.target.value; draw(); } })))); } else if (st.kind === "cycle") { parts.push(h("div", { class: "grid", style: "margin-top:12px" }, inp("work_days", "number", "Рабочих дней подряд"), inp("off_days", "number", "Выходных подряд"), inp("cycle_start", "date", "Первый рабочий день цикла"), inp("start_time", "time", "Начало смены"), inp("end_time", "time", "Конец смены"))); } else { parts.push(h("div", { style: "margin-top:12px" }, h("label", {}, "Дни недели"), h("div", { class: "days" }, ...DAYS.map((d, i) => h("label", {}, h("input", { type: "checkbox", checked: st.weekdays.includes(i), onchange: e => { st.weekdays = DAYS.map((_, j) => j).filter(j => j === i ? e.target.checked : st.weekdays.includes(j)); draw(); } }), d)))), h("div", { class: "grid", style: "margin-top:12px;max-width:440px" }, inp("start_time", "time", "Начало смены"), inp("end_time", "time", "Конец смены"))); } body.replaceChildren(...parts); refresh(); } draw(); formBox.replaceChildren(h("div", { class: "card" }, h("h2", { style: "margin-top:0" }, row ? "Изменение графика" : "Новый график"), body, preview, h("div", { class: "row", style: "margin-top:12px" }, h("button", { onclick: guard(async () => { await api(row ? "PATCH" : "POST", "/schedules" + (row ? "/" + row.id : ""), payload()); for (const k in cache) delete cache[k]; formBox.replaceChildren(); toast("Сохранено"); load(); }) }, "Сохранить"), h("button", { class: "sec", onclick: () => formBox.replaceChildren() }, "Отмена")))); formBox.scrollIntoView({ behavior: "smooth" }); } await load(); }