// timesheets-data.jsx — управління таймшітом для проєктної діяльності ГО // Для кожного проєкту та працівника можна вести облік робочих днів/годин. // На основі таймшіту генеруються рахунки за проєктом. (function () { const SYS = window.SYS_DATE || "2026-06-12"; // Кешувати ID користувача при першому завантаженні const getCurrentUserId = async () => { // Спробуємо з кешу let cached = localStorage.getItem("erp_current_user_id"); if (cached) return cached; // Отримати з API try { if (window.API && window.API.mode === "live") { const res = await window.API.get("/auth/me"); const userId = res.data?.id || res.id; if (userId) { localStorage.setItem("erp_current_user_id", userId); return userId; } } } catch (e) { console.error("[ERP] Failed to get user ID:", e); } return null; }; window.getCurrentUserId = getCurrentUserId; // Завантажити список працівників з Довідника (API) window.loadEmployees = async function () { try { // Якщо вже завантажені й мають ID, повернути їх if (window.DATA?.EMPLOYEES && window.DATA.EMPLOYEES.length > 0 && window.DATA.EMPLOYEES[0].id) { console.log("[ERP] Employees already loaded:", window.DATA.EMPLOYEES.length); return window.DATA.EMPLOYEES; } // Live режим: завантажити з API if (window.API && window.API.mode === "live") { console.log("[ERP] Загружаємо працівників з Довідника..."); const res = await window.API.get("/employees"); const employees = Array.isArray(res.data) ? res.data : (res.data?.data || []); if (employees.length === 0) { console.warn("[ERP] No employees found in response:", res); return []; } // Зберегти в window.DATA if (!window.DATA) window.DATA = {}; window.DATA.EMPLOYEES = employees; console.log("[ERP] Завантажено працівників:", employees.length, "ID перших:", employees.slice(0, 3).map(e => e.id)); return employees; } // Demo режим: повернути пусто або дані з window.DATA if (!window.DATA) window.DATA = {}; console.log("[ERP] Demo режим - дані не доступні"); return window.DATA.EMPLOYEES || []; } catch (e) { console.error("[ERP] Помилка завантаження працівників:", e); return (window.DATA?.EMPLOYEES || []); } }; // Завантажити таймшіти для проєкту window.loadProjectTimesheets = async function (projectId) { try { if (!window.API || window.API.mode !== "live") { return JSON.parse(localStorage.getItem(`ubp.timesheets.${projectId}`) || "[]"); } // Фільтруємо за проєктом const res = await window.API.get("/timesheet?filters[project]=" + projectId); console.log("[ERP] Loaded timesheets:", res); return Array.isArray(res.data) ? res.data : (res.data?.data || []); } catch (e) { console.error("[ERP] Failed to load timesheets:", e); return []; } }; // Зберегти або створити запис таймшіту window.saveProjectTimesheet = async function (data) { try { // Валідація if (!data.date) throw new Error("Дата обов'язкова"); if (!data.project) throw new Error("Проєкт обов'язковий"); const hours = parseFloat(data.dailyHours); if (isNaN(hours) || hours <= 0) throw new Error("Години повинні бути > 0"); const rate = data.hourlyRate; if (isNaN(rate) || rate <= 0) throw new Error("Ставка повинна бути > 0"); // Отримати поточного користувача let employeeId = data.employee || await getCurrentUserId(); if (!employeeId) throw new Error("Не вдалось отримати користувача. Спробуйте перезавантажити сторінку."); const payload = { date: data.date, project: data.project, employee: employeeId, dailyHours: hours, hourlyRate: Math.round(rate), // у копійках note: data.note || "", }; if (!window.API || window.API.mode !== "live") { // Demo mode: зберегти в localStorage const list = JSON.parse(localStorage.getItem(`ubp.timesheets.${data.project}`) || "[]"); const existing = list.find(t => t.date === payload.date && t.employee === payload.employee); if (existing) { Object.assign(existing, payload); } else { payload.id = `ts_${data.project}_${data.employee}_${data.date.replace(/-/g, "")}`; list.push(payload); } localStorage.setItem(`ubp.timesheets.${data.project}`, JSON.stringify(list)); console.log("[ERP] Timesheet saved (demo):", payload); return payload; } // Live mode: надіслати на сервер let res; if (data.id) { console.log("[ERP] Updating timesheet:", data.id, payload); res = await window.API.put(`/timesheet/${data.id}`, payload); } else { // Генеруємо ID на основі проєкту, працівника та дати (без дефісів) const dateStr = payload.date ? payload.date.replace(/-/g, "") : new Date().toISOString().split("T")[0].replace(/-/g, ""); const tsId = `ts_${data.project}_${data.employee}_${dateStr}`; console.log("[ERP] Creating timesheet:", tsId, payload); res = await window.API.post("/timesheet", { id: tsId, ...payload }); } console.log("[ERP] API response:", res); return res.data || payload; } catch (e) { console.error("[ERP] Failed to save timesheet:", e); throw e; } }; // Вилучити запис таймшіту window.deleteProjectTimesheet = async function (id, projectId) { try { if (!window.API || window.API.mode !== "live") { const list = JSON.parse(localStorage.getItem(`ubp.timesheets.${projectId}`) || "[]"); const filtered = list.filter(t => t.id !== id); localStorage.setItem(`ubp.timesheets.${projectId}`, JSON.stringify(filtered)); return; } await window.API.delete(`/timesheet/${id}`); } catch (e) { console.error("[ERP] Failed to delete timesheet:", e); throw e; } }; // Розрахувати суму на основі таймшітів window.calculateTimesheetTotal = function (timesheets) { return timesheets.reduce((sum, ts) => { const hours = ts.dailyHours || 0; const rate = ts.hourlyRate || 0; return sum + (hours * rate); }, 0); }; // Генерувати рахунок на основі таймшітів за період window.generateInvoiceFromTimesheets = async function (projectId, startDate, endDate, tranchInfo) { try { const timesheets = await window.loadProjectTimesheets(projectId); const filtered = timesheets.filter(ts => ts.date >= startDate && ts.date <= endDate); if (filtered.length === 0) { throw new Error("Нема записів таймшіту за обраний період"); } const total = window.calculateTimesheetTotal(filtered); if (!window.API || window.API.mode !== "live") { // Demo: повернути модель рахунку без збереження return { number: tranchInfo?.stage || "Рахунок за таймшітом", issueDate: SYS, dueDate: tranchInfo?.dueDate || SYS, amount: total, timesheetData: filtered, }; } // Live: надіслати запит на генерування рахунку const res = await window.API.post("/projects/generate-invoice", { projectId, startDate, endDate, tranchId: tranchInfo?.id, amount: total, }); return res.data; } catch (e) { console.error("[ERP] Failed to generate invoice:", e); throw e; } }; // Статистика по таймшіту window.timesheetStats = function (timesheets) { const stats = { totalHours: 0, totalAmount: 0, byEmployee: {}, byDate: {}, }; timesheets.forEach(ts => { const hours = ts.dailyHours || 0; const amount = hours * (ts.hourlyRate || 0); stats.totalHours += hours; stats.totalAmount += amount; // За працівником if (!stats.byEmployee[ts.employee]) { stats.byEmployee[ts.employee] = { hours: 0, amount: 0 }; } stats.byEmployee[ts.employee].hours += hours; stats.byEmployee[ts.employee].amount += amount; // За датою if (!stats.byDate[ts.date]) { stats.byDate[ts.date] = { hours: 0, amount: 0 }; } stats.byDate[ts.date].hours += hours; stats.byDate[ts.date].amount += amount; }); return stats; }; })();