// orders-page.jsx — Кадрові накази: журнал реєстрації + майстер створення.
// Нумерація НК-0N-2026. Друк через window.OrderPrintDoc.
function OrdStatusTag({ status }) {
const s = window.ORDER_STATUS[status] || window.ORDER_STATUS.draft;
return {s.label} ;
}
// ——— дрібні поля форми ———
function OWField({ label, children, wide }) {
return (
{label}
{children}
);
}
function OrderWizard({ onClose }) {
const team = window.DATA.TEAM;
const staff = team.filter(p => p.employment !== "contractor");
const SYS = window.SYS_DATE || "2026-05-29";
const [step, setStep] = React.useState(1);
const [type, setType] = React.useState(null);
const firstPid = staff[0] ? staff[0].id : "";
const [pid, setPid] = React.useState(firstPid);
const [basis, setBasis] = React.useState("");
const [f, setF] = React.useState({});
const set = (k, v) => setF(prev => ({ ...prev, [k]: v }));
// дефолти полів під тип + обрану людину
const seedFields = (tp, personId) => {
const p = window.getTeam(personId) || {};
if (tp === "hire") return { name: "", position: "", dept: "Проєктний відділ", salary: 30000, partRate: 1, from: SYS, probation: 3, contract: "Безстроковий трудовий договір" };
if (tp === "dismiss") return { date: SYS, ground: "art38", compDays: p.vacationBalance || 0 };
if (tp === "vacation") return { kind: "щорічну основну", days: 10, from: SYS, to: SYS, period: "за робочий рік 2025–2026" };
if (tp === "bonus") return { mode: "amount", amount: 10000, percent: 20, reason: "за сумлінне виконання посадових обов'язків" };
if (tp === "transfer") return { posOld: p.positionOfficial || p.role || "", posNew: "", salaryNew: (p.baseSalary || 0), from: SYS };
return {};
};
const pickType = (tp) => { setType(tp); setF(seedFields(tp, pid)); setBasis(defaultBasis(tp)); setStep(2); };
const defaultBasis = (tp) => ({
hire: "Заява про прийняття на роботу, трудовий договір",
dismiss: "Заява про звільнення за власним бажанням",
vacation: "Заява, графік відпусток на 2026 р.",
bonus: "Подання директора",
transfer: "Заява / службова записка",
}[tp] || "");
// при зміні людини — перерахувати залежні дефолти
const onPid = (id) => { setPid(id); if (type) setF(prev => ({ ...prev, ...((type === "transfer" || type === "dismiss") ? seedFields(type, id) : {}) })); };
const valid = (() => {
if (!type) return false;
if (type === "hire") return f.name && f.name.trim() && f.position && f.position.trim();
return !!pid;
})();
const submit = () => {
const order = {
id: "ord-" + Date.now(),
no: window.nextOrderNo(),
date: SYS,
type,
personId: type === "hire" ? null : pid,
status: "draft",
basis,
f: { ...f },
};
if (type === "bonus") { if (f.mode === "amount") delete order.f.percent; else delete order.f.amount; }
window.addOrder(order);
onClose(order);
};
const num = window.formatMoney;
return (
onClose(null)}>
Новий кадровий наказ
№ {window.nextOrderNo()} · {window.formatDate(SYS)}
onClose(null)}>
{step === 1 && (
Оберіть тип наказу
{window.ORDER_TYPE_ORDER.map(tp => {
const T = window.ORDER_TYPES[tp];
return (
pickType(tp)}>
{T.label}
);
})}
)}
{step === 2 && type && (
setStep(1)}>← інший тип
{window.ORDER_TYPES[type].label}
{/* Працівник */}
{type === "hire" ? (
set("name", e.target.value)} placeholder="Ім'я Прізвище" />
set("position", e.target.value)} placeholder="напр. Інженер-проєктувальник" />
set("dept", e.target.value)} />
set("salary", +e.target.value)} />
set("partRate", +e.target.value)}>
Повна (1,0)
Половина (0,5)
Чверть (0,25)
set("from", e.target.value)} />
set("probation", +e.target.value)}>
Без випробування
1 місяць
2 місяці
3 місяці
) : (
onPid(e.target.value)}>
{staff.map(p => {p.name} — {p.positionOfficial || p.role} )}
)}
{/* Поля під тип */}
{type === "dismiss" && (
set("date", e.target.value)} />
set("compDays", +e.target.value)} />
set("ground", e.target.value)}>
{window.DISMISS_GROUNDS.map(g => {g.label} )}
)}
{type === "vacation" && (
set("kind", e.target.value)}>
Щорічна основна
Додаткова
Без збереження ЗП
set("days", +e.target.value)} />
set("from", e.target.value)} />
set("to", e.target.value)} />
{(() => { const p = window.getTeam(pid); return p && p.vacationBalance != null
? Залишок відпустки: {p.vacationBalance} к. дн.
: null; })()}
)}
{type === "bonus" && (
set("mode", e.target.value)}>
Фіксована сума
% окладу
{f.mode === "percent"
? set("percent", +e.target.value)} />
: set("amount", +e.target.value)} /> }
set("reason", e.target.value)} />
)}
{type === "transfer" && (
set("posOld", e.target.value)} />
set("posNew", e.target.value)} placeholder="напр. Провідний інженер-проєктувальник" />
set("salaryNew", +e.target.value)} />
set("from", e.target.value)} />
)}
setBasis(e.target.value)} />
Створиться чернетка — текст можна відредагувати у бланку перед друком.
onClose(null)}>Скасувати
Сформувати наказ
)}
);
}
function OrdersPage({ role }) {
const st = window.useOrders();
const [wizard, setWizard] = React.useState(false);
const [printOrder, setPrintOrder] = React.useState(null);
const [filter, setFilter] = React.useState("all");
const canEdit = role === "director" || role === "accountant";
const list = [...st.list].sort((a, b) => (a.no < b.no ? 1 : -1));
const shown = filter === "all" ? list : list.filter(o => o.type === filter);
const byType = {};
window.ORDER_TYPE_ORDER.forEach(t => byType[t] = list.filter(o => o.type === t).length);
const drafts = list.filter(o => o.status === "draft").length;
const sign = (o) => window.updateOrder(o.id, { status: "signed", ackDate: o.ackDate || window.SYS_DATE });
return (
Кадрові накази
журнал реєстрації по особовому складу
нумерація НК-0N-2026
{list.length} наказів за рік{drafts > 0 ? ` · ${drafts} чернеток` : ""}
{/* Зведення по типах */}
{window.ORDER_TYPE_ORDER.map(t => {
const T = window.ORDER_TYPES[t];
return (
setFilter(filter === t ? "all" : t)}>
{byType[t]}
{T.label}
);
})}
{/* Тулбар */}
setFilter("all")}>Усі типи
{filter !== "all" && {window.ORDER_TYPES[filter].label} }
{canEdit &&
setWizard(true)}> Новий наказ }
{/* Журнал */}
№ наказу
Дата
Тип · працівник
Зміст
Підстава
Статус
{shown.map(o => {
const T = window.ORDER_TYPES[o.type] || {};
const p = window.getTeam(o.personId);
const who = p ? p.name : (o.f && o.f.name) || "—";
return (
{o.no}
{window.formatDate(o.date)}
{T.label}
{who}
{window.orderSummary(o)}
{o.basis}
setPrintOrder(o)}> Бланк
{canEdit && o.status === "draft" && sign(o)}>Підписати }
);
})}
Журнал реєстрації наказів з особового складу ведеться наскрізно (НК-0N-2026). Бланк формується автоматично з картки працівника
(ПІБ, посада, оклад), текст редагується перед друком. Підпис директора, печатка (М.П.) і блок ознайомлення — на бланку.
КЕП та інтеграція з кадровим архівом — на бекенді.
{wizard &&
{ setWizard(false); if (o) setPrintOrder(o); }} />}
{printOrder && setPrintOrder(null)} />}
);
}
window.OrdersPage = OrdersPage;