// Калькулятор ЗП — 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 ( {open && (

Калькулятор работает с двумя простыми табличками, которые вы заполняете за 5 минут из своей базы данных. Связывает их по ФИО, считает зарплату и формирует расчётные листы.

→ Справочник сотрудников
Постоянный · один раз настроили — пользуетесь
ФИОДолжностьСтавка/день% с продаж% за стажСтатус
Иванова А.Старший флорист1 800 ₽8%5%активный
Петрова М.Флорист1 500 ₽6%0%активный
Скачать шаблон Excel
→ Табель за месяц
Обновляете в конце каждого месяца
ФИООтраб. днейВыручка с продажОтзывы ЯндексАвансШтрафы
Иванова А.14320 000 ₽715 000 ₽0 ₽
Петрова М.12180 000 ₽310 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 (
{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 (
{label}
{value}
); } /* ============================================================ ControlsBar — search, filters, sort, export ============================================================ */ function ControlsBar({ search, onSearch, filterDol, onFilterDol, filterStatus, onFilterStatus, sort, onSort, dolzhnosti, onExportExcel, onExportPdf }) { return (
); } /* ============================================================ 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 (
{items.map(it => (
{it.l}
{it.v}
))}
); } /* ============================================================ 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-калькулятора Марии Андреевой
)}
); } 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, });