// project-timesheet-tab.jsx — таймшіт для ГО проектів function ProjectTimesheetTab({ project, role }) { const [timesheets, setTimesheets] = React.useState([]); const [loading, setLoading] = React.useState(true); const [editing, setEditing] = React.useState(null); const [filterDate, setFilterDate] = React.useState("2026-06"); const m = window.formatMoney; React.useEffect(() => { loadTimesheets(); }, [project.id]); const loadTimesheets = async () => { setLoading(true); try { const data = await window.loadProjectTimesheets(project.id); setTimesheets(data || []); } catch (e) { console.error("[ERP] Load error:", e); } finally { setLoading(false); } }; const handleSave = async (data) => { try { await window.saveProjectTimesheet({ ...data, project: project.id, dailyHours: parseFloat(data.dailyHours), hourlyRate: Math.round(parseFloat(data.hourlyRate) * 100) }); await loadTimesheets(); setEditing(null); } catch (e) { alert("Помилка: " + e.message); } }; const handleDelete = async (id) => { if (!confirm("Видалити запис?")) return; try { await window.deleteProjectTimesheet(id, project.id); await loadTimesheets(); } catch (e) { alert("Помилка: " + e.message); } }; const canEdit = ["director", "accountant", "hr_team"].includes(role); const filtered = timesheets.filter(ts => !filterDate || ts.date.startsWith(filterDate)); const totalHours = filtered.reduce((s, ts) => s + (ts.dailyHours || 0), 0); const totalAmount = filtered.reduce((s, ts) => s + ((ts.dailyHours || 0) * (ts.hourlyRate || 0) / 100), 0); return (
Днів роботи
{filtered.length}
Всього годин
{totalHours.toFixed(1)}
До сплати
{m(Math.round(totalAmount * 100))} ₴
setFilterDate(e.target.value)} className="cp-input" style={{ flex: "0 0 160px" }} /> {canEdit && ( )}
{loading ? (
Завантаження...
) : filtered.length === 0 ? (
Немає записів за цей період
) : (
{canEdit && } {filtered.map(ts => ( setEditing(ts)} onSave={handleSave} onDelete={() => handleDelete(ts.id)} canEdit={canEdit} m={m} /> ))}
Дата Години Ставка/год Сума Примітка СкріншотДії
)} {editing && ( setEditing(null)} /> )}
); } function TimesheetRow({ ts, isEditing, onEdit, onSave, onDelete, canEdit, m }) { const hours = ts.dailyHours || 0; const rate = ts.hourlyRate || 0; const amount = hours * rate / 100; if (isEditing) { return ( ); } return ( {ts.date} {hours.toFixed(1)} {m(rate)} ₴ {m(Math.round(amount * 100))} ₴ {ts.note || "—"} {ts.screenshot ? "✓" : "—"} {canEdit && ( )} ); } function TimesheetModal({ ts, onSave, onClose }) { const getSysDate = () => new Date().toISOString().split("T")[0]; const [form, setForm] = React.useState({ date: ts.date || getSysDate(), employee: ts.employee || "", dailyHours: ts.dailyHours || "", note: ts.note || "" }); const [hourlyRate, setHourlyRate] = React.useState(ts.hourlyRate ? ts.hourlyRate / 100 : ""); const [screenshotFile, setScreenshotFile] = React.useState(null); React.useEffect(() => { // Завантажити ID користувача при відкритті const loadUser = async () => { if (!form.employee && window.getCurrentUserId) { try { const userId = await window.getCurrentUserId(); if (userId) { setForm(prev => ({ ...prev, employee: userId })); } } catch (e) { console.error("[ERP] Failed to get user:", e); } } }; loadUser(); }, []); const handleSave = () => { if (!form.date) { alert("Виберіть дату"); return; } if (!form.dailyHours || parseFloat(form.dailyHours) <= 0) { alert("Введіть години > 0"); return; } if (!hourlyRate || parseFloat(hourlyRate) <= 0) { alert("Введіть ставку > 0"); return; } if (!form.employee) { alert("Завантажується користувач..."); return; } if (!screenshotFile && !form.screenshot) { alert("Завантажте скріншот"); return; } onSave({ ...form, hourlyRate: Math.round(parseFloat(hourlyRate) * 100), screenshot: screenshotFile || form.screenshot }); }; return (
Запис таймшіту
setForm({ ...form, date: e.target.value })} />
setForm({ ...form, dailyHours: e.target.value })} step="0.5" min="0" placeholder="напр. 8" />
setHourlyRate(e.target.value)} step="0.01" min="0" placeholder="напр. 300" />