// Калькулятор ЗП — major screen sections
// UploadZone · KpiBlock · ControlsBar · EmployeesTable · SummaryBlock · SheetModal
const { useState: useS, useMemo: useM } = React;
/* ============================================================
UploadZone — two file tiles + demo data button
============================================================ */
function HowItWorks() {
const [open, setOpen] = useS(false);
return (
setOpen(o => !o)}>
Как работает калькулятор
{open && (
Калькулятор работает с двумя простыми табличками , которые вы заполняете за 5 минут
из своей базы данных. Связывает их по ФИО, считает зарплату и формирует расчётные листы.
→ Справочник сотрудников
Постоянный · один раз настроили — пользуетесь
ФИО Должность Ставка/день % с продаж % за стаж Статус
Иванова А. Старший флорист 1 800 ₽ 8% 5% активный
Петрова М. Флорист 1 500 ₽ 6% 0% активный
Скачать шаблон Excel
→ Табель за месяц
Обновляете в конце каждого месяца
ФИО Отраб. дней Выручка с продаж Отзывы Яндекс Аванс Штрафы
Иванова А. 14 320 000 ₽ 7 15 000 ₽ 0 ₽
Петрова М. 12 180 000 ₽ 3 10 000 ₽ 500 ₽
Скачать шаблон Excel
Калькулятор связывает таблички по ФИО .
Неактивные сотрудники в расчёт не идут.
Если сотрудник есть в справочнике, но нет в табеле — статус «нет данных за месяц» .
)}
);
}
function UploadZone({ loaded, mode, sotrudnikiInfo, tabelInfo, onUploadSotrudniki, onUploadTabel, onLoadDemo, onReset }) {
const sotRef = React.useRef(null);
const tabRef = React.useRef(null);
const handleFile = (e, fn) => {
const f = e.target.files && e.target.files[0];
if (f) fn(f);
e.target.value = '';
};
return (
Справочник сотрудников
{sotrudnikiInfo || 'sotrudniki.xlsx — нажмите, чтобы выбрать'}
handleFile(e, onUploadSotrudniki)} />
Табель с выручкой
{tabelInfo || 'tabel-s-vyruchkoi.xlsx — нажмите, чтобы выбрать'}
handleFile(e, onUploadTabel)} />
{loaded ? (mode === 'real' ? 'Загружено' : 'Демо') : 'Без файлов'}
{loaded ? (
Сбросить
) : (
Загрузить демо
)}
);
}
/* ============================================================
KpiBlock — period inputs + 8 KPI cards
============================================================ */
function KpiBlock({ kpi, onKpiChange, computed }) {
return (
onKpiChange({ ...kpi, period: e.target.value })}
placeholder="апрель 2026"
/>
onKpiChange({ ...kpi, revPlan: v })} placeholder="0 ₽" />
onKpiChange({ ...kpi, revFact: v })} placeholder="0 ₽" />
onKpiChange({ ...kpi, writeoff: v })} placeholder="0 ₽" />
);
}
function Kpi({ label, value, tone, valueSm }) {
const cls = `kpi${tone ? ' ' + tone : ''}`;
return (
);
}
/* ============================================================
ControlsBar — search, filters, sort, export
============================================================ */
function ControlsBar({ search, onSearch, filterDol, onFilterDol, filterStatus, onFilterStatus, sort, onSort, dolzhnosti, onExportExcel, onExportPdf }) {
return (
onFilterDol(e.target.value)}>
Все должности
{dolzhnosti.map(d => {d} )}
onFilterStatus(e.target.value)}>
Все статусы
Активные
Неактивные
onSort(e.target.value)}>
Сортировка: по умолчанию
По ФИО
По должности
По выручке ↓
По начислению ↓
К выдаче ↓
Скачать Excel
Скачать PDF
);
}
/* ============================================================
EmployeesTable
============================================================ */
function EmployeesTable({ rows, onOpenSheet }) {
if (!rows.length) {
return (
Никто не найден по этим фильтрам.
);
}
return (
Сотрудник
Статус
Отраб. дней
Оплата за дни
Σ %
Бонусы
Удержания
Начислено
К выдаче
{rows.map((r, i) => (
onOpenSheet(i)} />
))}
);
}
function EmployeeRow({ row, onOpenSheet }) {
if (row._nodata) {
return (
{shortName(row.fio)}
{row.dolzhnost}
Нет данных
Нет записей в табеле за этот период
Лист →
);
}
const bonuses = (row.oplOtzyvy || 0) + (row.nadStazh || 0) + (row.bonus || 0);
const withholds = (row.avans || 0) + (row.uderzhaniya || 0);
return (
{shortName(row.fio)}
{row.dolzhnost}
{row._active
? Активен
: Неактивен }
{row.vyhody}
{fmtMoney(row.oplVyhody)}
{fmtMoney(row.sumPct)}
{fmtMoney(bonuses)}
−{fmtMoney(withholds).replace('−','')}
{fmtMoney(row.nachisleno)}
{fmtMoney(row.vydacha)}
Лист →
);
}
function shortName(fio) {
// "Иванова Анна Сергеевна" → "Иванова А.С."
const parts = fio.trim().split(/\s+/);
if (parts.length < 2) return fio;
const last = parts[0];
const initials = parts.slice(1).map(p => p.charAt(0) + '.').join('\u00A0');
return `${last} ${initials}`;
}
/* ============================================================
SummaryBlock
============================================================ */
function SummaryBlock({ totals }) {
const items = [
{ l: 'Общий ФОТ', v: fmtMoney(totals.fot) },
{ l: 'Личная выручка (Σ)', v: fmtMoney(totals.vyruchka) },
{ l: 'Средняя выплата', v: fmtMoney(totals.avgPay) },
{ l: 'Сумма авансов', v: fmtMoney(totals.avans) },
{ l: 'Сумма удержаний', v: fmtMoney(totals.ud) },
{ l: 'К выдаче (всего)', v: fmtMoney(totals.vydacha) },
{ l: 'Сотрудников', v: `${totals.activeCount} / ${totals.count}` },
{ l: 'С бонусом по списанию', v: String(totals.withBonus) },
];
return (
);
}
/* ============================================================
SheetModal — расчётный лист per employee
============================================================ */
function SheetModal({ row, onClose, onExportExcel, onExportPdf }) {
if (!row) return null;
const isOpen = !!row;
return (
{
if (e.target.classList.contains('modal-overlay')) onClose();
}}>
{row.fio}
{row.dolzhnost} · {row.period || '—'}
{row._nodata ? (
Нет данных для расчёта за этот период.
) : (
<>
К выдаче
{fmtMoney(row.vydacha)}
Расчёт произведён с помощью AI-калькулятора Марии Андреевой
>
)}
window.print()}>Распечатать
Скачать Excel
Скачать PDF
Закрыть
);
}
function SheetSection({ title, children }) {
return (
{title}
{children}
);
}
function SheetRow({ l, f, amt, total }) {
return (
{l}
{f ? {f} : null}
{amt}
);
}
Object.assign(window, {
HowItWorks, UploadZone, KpiBlock, ControlsBar, EmployeesTable, SummaryBlock, SheetModal,
});