// project-procurement-tab.jsx — вкладка «План закупівель» у воркспейсі проєкту ГО. // Рядки плану зберігаються у project.procurements (JSON) і персистяться через // ProjectsStore.save() → PATCH /projects/{id}. Редагування — інлайн у таблиці. // // Авто-підказка процедури за порогами політики Екоклуб/PIN (курс USD→€ = 1.15): // < 2 500 € → Проста закупівля // 2 500 – 19 999 € → Спрощена // 20 000 – (100k/300k) → Переговорна // ≥ 100 000 € (посл/мат) / ≥ 300 000 € (роботи) → Тендер (function () { const RATE = 1.15; // USD за 1 € (курс із затвердженого плану) // ── Тип закупівлі ── const TYPES = [ { key: "Services", uk: "Послуги" }, { key: "Works", uk: "Роботи" }, { key: "Goods", uk: "Товари / Матеріали" }, ]; const typeUk = (t) => (TYPES.find(x => x.key === t) || {}).uk || t || "—"; const isWorks = (t) => /work|роб/i.test(t || ""); // ── Процедури (канонічні ключі + двомовні підписи) ── const PROC = { simple: { en: "Simple purchase", uk: "Проста закупівля", tone: "neutral" }, simplified: { en: "Simplified procedure", uk: "Спрощена", tone: "blue" }, negotiated: { en: "Negotiated procedure", uk: "Переговорна", tone: "amber" }, tender: { en: "Open International Tender", uk: "Тендер", tone: "violet" }, }; const PROC_ORDER = ["simple", "simplified", "negotiated", "tender"]; function procKey(s) { s = (s || "").toLowerCase(); if (s.includes("tender") || s.includes("тендер")) return "tender"; if (s.includes("negoti") || s.includes("перегов")) return "negotiated"; if (s.includes("simplif") || s.includes("спрощ")) return "simplified"; if (s.includes("simple") || s.includes("прост")) return "simple"; return null; } // Рекомендована процедура за вартістю USD і типом function recommend(type, usd) { if (usd === "" || usd == null) return null; const v = Number(usd); if (isNaN(v)) return null; const eur = v / RATE; const hi = isWorks(type) ? 300000 : 100000; if (eur < 2500) return "simple"; if (eur < 20000) return "simplified"; if (eur < hi) return "negotiated"; return "tender"; } // ── Статуси (відомі — кольоровий чип; решта — звичайний текст) ── const STAT_TONE = { "Заплановано": "neutral", "В процесі": "amber", "Оновлення дат": "amber", "Виконано": "live", "Завершено": "live", "Попередньо скасовано": "neutral", "Скасовано": "late", }; const STAT_OPTS = ["Заплановано", "В процесі", "Оновлення дат", "Виконано", "Попередньо скасовано", "Скасовано"]; const fmtUsd = (v) => (v === "" || v == null || isNaN(Number(v))) ? "—" : "$" + Number(v).toLocaleString("uk-UA", { maximumFractionDigits: 0 }); // textarea, що автоматично росте під увесь текст (щоб назва не обрізалась) function AutoTextarea(props) { const ref = React.useRef(null); const fit = (el) => { if (el) { el.style.height = "auto"; el.style.height = el.scrollHeight + "px"; } }; React.useLayoutEffect(() => { fit(ref.current); }); return