// field-visits-data.jsx — Виїзди на об'єкт // Виїзд = задача спеціального типу (task.kind === "visit") з payload task.visit. // Живе всередині Задач (канбан / дерево / Сьогодні), як і будь-яка задача. // // Чотири складові виїзду: спорядження (комплект) · хто їде · транспорт · погодження часу. // Облікові прилади комплекту тягнуться зі Складу (видача → повернення). (function () { // ============ КАТАЛОГ КОМПЛЕКТУ ============ // kind: "instrument" — обліковий прилад зі Складу (має assetId, видача/повернення) // "consumable" — витратка/дрібниця (у Складі не ведеться, проста галочка) // future: true — ще не використовується (напр. iPad з Revit) const KIT_CATALOG = [ { id: "k-tape", name: "Звичайна рулетка", icon: "ruler", kind: "consumable" }, { id: "k-laser", name: "Лазерна рулетка", icon: "ruler", kind: "instrument", assetId: "as-19" }, { id: "k-pencil", name: "Олівець", icon: "pencil", kind: "consumable" }, { id: "k-eraser", name: "Гумова резинка", icon: "minus", kind: "consumable" }, { id: "k-clipboard", name: "Планшетка", icon: "clipboard", kind: "consumable" }, { id: "k-a3", name: "Аркуші А3", icon: "file", kind: "consumable" }, { id: "k-prints", name: "Роздруківки (техпаспорт / креслення)", icon: "files", kind: "consumable" }, { id: "k-ipad", name: "iPad з Revit", icon: "monitor", kind: "instrument", future: true }, // прилади під конкретні типи { id: "k-level", name: "Нівелір зі штативом", icon: "ruler", kind: "instrument", assetId: "as-20" }, { id: "k-total", name: "Тахеометр", icon: "target", kind: "instrument", assetId: "as-21" }, { id: "k-thermal", name: "Тепловізор", icon: "flame", kind: "instrument", assetId: "as-22" }, { id: "k-journal", name: "Журнал авторського нагляду", icon: "notebook", kind: "consumable" }, { id: "k-camera", name: "Фотоапарат / камера", icon: "camera", kind: "consumable" }, ]; const KIT_BY_ID = Object.fromEntries(KIT_CATALOG.map(k => [k.id, k])); window.VISIT_KIT_CATALOG = KIT_CATALOG; window.getKitItem = (id) => KIT_BY_ID[id]; // Базовий комплект — на будь-який виїзд const BASE_KIT = ["k-tape", "k-laser", "k-pencil", "k-eraser", "k-clipboard", "k-a3", "k-prints"]; // ============ ТИПИ ВИЇЗДІВ ============ // defaultKit — базовий + специфічні прилади (редагований під конкретний виїзд) // section — розділ задачі; closeout — типові пункти «що зробили на місці» const VISIT_TYPES = [ { id: "measure", label: "Обмірні роботи", short: "Обмір", icon: "ruler", section: "ВД", color: "var(--c-blue)", note: "Заміри існуючої будівлі під проєкт.", defaultKit: [...BASE_KIT], closeout: ["Обміряти геометрію приміщень", "Зафіксувати позначки рівнів", "Прив'язати отвори та вузли"], resultFiles: ["Обмірні креслення / ескізи", "Фотофіксація"] }, { id: "supervision", label: "Авторський нагляд", short: "Авт. нагляд", icon: "hardhat", section: "АН", color: "var(--c-green-deep)", note: "Контроль на майданчику під час будівництва.", defaultKit: [...BASE_KIT, "k-journal", "k-camera"], closeout: ["Перевірити критичні вузли", "Запис у журнал авт. нагляду", "Фотофіксація виконаних робіт"], resultFiles: ["Запис у журналі АН", "Фотофіксація"] }, { id: "inspection", label: "Технічне обстеження / дефектування", short: "Обстеження", icon: "search", section: "ТО", color: "var(--c-blue)", note: "Огляд конструкцій, фіксація дефектів і обсягів.", defaultKit: [...BASE_KIT, "k-thermal", "k-camera"], closeout: ["Візуальний та інструментальний огляд", "Розкриття вузлів за потреби", "Скласти відомість дефектів"], resultFiles: ["Відомість дефектів", "Фотофіксація", "Термограми"] }, { id: "approval", label: "Погоджувальний виїзд", short: "Погодження", icon: "users", section: "ГІП", color: "var(--c-blue)", note: "Зустріч із замовником / зацікавленими сторонами на місці.", defaultKit: ["k-clipboard", "k-prints", "k-pencil"], closeout: ["Обговорити рішення на місці", "Зафіксувати домовленості у протоколі"], resultFiles: ["Протокол зустрічі", "Фотофіксація"] }, { id: "geodesy", label: "Геодезичні роботи", short: "Геодезія", icon: "target", section: "ВД", color: "var(--c-green-deep)", note: "Нівелювання, тахеометрична зйомка майданчика.", defaultKit: [...BASE_KIT, "k-level", "k-total"], closeout: ["Прив'язка до реперів", "Тахеометрична зйомка", "Камеральна обробка"], resultFiles: ["Топогеодезичний план", "Каталог координат"] }, ]; const VT_BY_ID = Object.fromEntries(VISIT_TYPES.map(v => [v.id, v])); window.VISIT_TYPES = VISIT_TYPES; window.getVisitType = (id) => VT_BY_ID[id] || VISIT_TYPES[0]; // ============ ТРАНСПОРТ ============ // Поки таксі; своє авто → пальне (службове RAV4, as-05). Бувають потяг / автобус. const TRANSPORT_MODES = [ { id: "taxi", label: "Таксі", icon: "car", cashFromRegister: true, note: "Готівка з каси / корпоративний рахунок Bolt." }, { id: "car", label: "Службове авто", icon: "fuel", assetId: "as-05", note: "Toyota RAV4 — пальне за авансовим звітом." }, { id: "train", label: "Потяг", icon: "arrowRight", tickets: true, note: "Квитки Укрзалізниці — додати до витрат." }, { id: "bus", label: "Автобус", icon: "arrowRight", tickets: true, note: "Квитки автоперевізника." }, ]; const TM_BY_ID = Object.fromEntries(TRANSPORT_MODES.map(m => [m.id, m])); window.TRANSPORT_MODES = TRANSPORT_MODES; window.getTransportMode = (id) => TM_BY_ID[id] || TRANSPORT_MODES[0]; const TRANSPORT_STATUS = { todo: { label: "Не вирішено", tone: "neutral" }, booked: { label: "Замовлено", tone: "amber" }, ready: { label: "Готово", tone: "live" }, }; window.TRANSPORT_STATUS = TRANSPORT_STATUS; // ============ ПОГОДЖЕННЯ ЧАСУ ============ const COORD_STATUS = { draft: { label: "Не узгоджено", tone: "neutral" }, proposed: { label: "Запропоновано", tone: "amber" }, confirmed: { label: "Підтверджено", tone: "live" }, }; window.COORD_STATUS = COORD_STATUS; // ============ ФАЗИ ВИЇЗДУ ============ // Наскрізний флоу: планування/збори → готовий до виїзду → закрито. const VISIT_PHASES = [ { id: "plan", label: "Планування", short: "Збори", icon: "clipboard" }, { id: "ready", label: "Готовий до виїзду", short: "Готово", icon: "checkCircle" }, { id: "done", label: "Закрито", short: "Закрито", icon: "check" }, ]; window.VISIT_PHASES = VISIT_PHASES; window.visitPhaseIndex = (id) => Math.max(0, VISIT_PHASES.findIndex(p => p.id === id)); // ============ ГОТОВНІСТЬ ДО ВИЇЗДУ ============ // 4 умови = 4 складові виїзду. Повертає кроки + відсоток + ready. window.visitReadiness = function (visit) { if (!visit) return { steps: [], done: 0, total: 0, pct: 0, ready: false }; const kit = visit.kit || []; const required = kit.filter(k => !k.optional); const packed = required.filter(k => k.packed).length; const tr = visit.transport || {}; const co = visit.coordination || {}; const team = visit.team || []; const steps = [ { key: "team", label: "Склад визначено", done: team.length > 0, detail: team.length ? `${team.length} учасн.` : "нікого" }, { key: "kit", label: "Комплект зібрано", done: required.length > 0 && packed === required.length, detail: `${packed}/${required.length}` }, { key: "transport", label: "Транспорт організовано", done: tr.status === "ready" || tr.status === "booked", detail: window.getTransportMode(tr.mode).label }, { key: "coord", label: "Час погоджено", done: co.status === "confirmed", detail: window.COORD_STATUS[co.status || "draft"].label }, ]; const done = steps.filter(s => s.done).length; const total = steps.length; return { steps, done, total, pct: Math.round(done / total * 100), ready: done === total }; }; // Зібрати об'єкт kit зі списку id шаблону window.buildKitFromIds = function (ids) { return (ids || []).map(id => { const c = window.getKitItem(id) || { id, name: id, icon: "dot", kind: "consumable" }; return { id: c.id, name: c.name, icon: c.icon, kind: c.kind, assetId: c.assetId || null, future: !!c.future, packed: false, checkedOut: false, optional: !!c.future, }; }); }; // ============ ПРИКЛАДИ ВИЇЗДІВ (вшиваємо в window.DATA.TASKS) ============ function makeVisitTask(spec) { const vt = window.getVisitType(spec.typeId); const t = { id: spec.id, kind: "visit", title: spec.title, obj: spec.obj || null, stage: spec.stage || "project", section: vt.section, assignee: spec.team[0], assignees: spec.team, day: spec.day, time: spec.time || null, est: spec.est || null, status: spec.status || "todo", note: spec.note || null, subtasks: [], checklists: [], visit: { typeId: spec.typeId, phase: spec.phase || "plan", team: spec.team, kit: spec.kit, transport: spec.transport, coordination: spec.coordination, closeout: spec.closeout || { didItems: [], journalAN: false, photos: 0, drawings: false, expensesActual: null, files: [] }, }, }; return window.normalizeTask ? window.normalizeTask(t) : t; } function withPacked(ids, packedIds, checkedOutIds) { const kit = window.buildKitFromIds(ids); kit.forEach(k => { if (packedIds && packedIds.includes(k.id)) k.packed = true; if (checkedOutIds && checkedOutIds.includes(k.id)) { k.checkedOut = true; k.packed = true; } }); return kit; } function installVisits() { if (!window.DATA || !Array.isArray(window.DATA.TASKS)) return; // 1) Перетворюємо наявну t6 «Виїзд на майданчик — авт. нагляд» на повноцінний виїзд const t6 = window.DATA.TASKS.find(t => t.id === "t6"); if (t6 && !t6.kind) { t6.kind = "visit"; t6.assignees = ["ok", "ap"]; t6.visit = { typeId: "supervision", phase: "ready", team: ["ok", "ap"], kit: withPacked( ["k-clipboard", "k-pencil", "k-laser", "k-journal", "k-camera", "k-prints"], ["k-clipboard", "k-pencil", "k-journal", "k-camera", "k-prints"], ["k-laser"] ), transport: { mode: "car", amount: 600, status: "ready", note: "Службове авто, ~45 км в одну сторону" }, coordination: { status: "confirmed", datetime: "26 трав, 15:00", contactName: "Сергій Бондар", contactRole: "Виконроб ГК «Моноліт»", contactPhone: "+380 67 990 11 22", notifiedAt: "вчора о 17:20", }, closeout: { didItems: [], journalAN: false, photos: 0, drawings: false, expensesActual: null, files: [] }, }; } // 2) Обмірні роботи — реконструкція адмінбудівлі (УКП-89) const vMeasure = makeVisitTask({ id: "v-meas-89", typeId: "measure", title: "Обмір санвузлів і сходових клітин (дообмір)", obj: "ukp-89", stage: "supervision", team: ["ap", "tm"], day: "28", time: "10:00 — 13:00", est: "3 год", status: "todo", phase: "plan", note: "Уточнити обміри для коригування КЖ. Узгодити доступ із комендантом будівлі.", kit: withPacked([...VISIT_TYPES[0].defaultKit], ["k-pencil", "k-a3"]), transport: { mode: "taxi", amount: 350, status: "todo", note: "Bolt туди-назад, центр" }, coordination: { status: "proposed", datetime: "28 трав, 10:00", contactName: "Ірина Лагода", contactRole: "Комендант (Мін'юст)", contactPhone: "+380 44 271 06 14", notifiedAt: null, }, }); // 3) Погоджувальний виїзд — ЖК «Подільський» (УКП-47) const vApproval = makeVisitTask({ id: "v-appr-47", typeId: "approval", title: "Виїзд на ділянку із замовником — посадка секцій", obj: "ukp-47", stage: "project", team: ["ok", "tm"], day: "29", time: "12:30 — 14:00", est: "1.5 год", status: "todo", phase: "plan", note: "Узгодити на місці остаточну посадку 2-ї та 3-ї секцій і відмітку входів.", kit: withPacked(["k-clipboard", "k-prints", "k-pencil"], ["k-prints"]), transport: { mode: "taxi", amount: 280, status: "booked", note: "Уклинд — Поділ" }, coordination: { status: "draft", datetime: "29 трав, 12:30", contactName: "Олександр Гриценко", contactRole: "Директор з розвитку, Подільський Девелопмент", contactPhone: "+380 67 412 38 90", notifiedAt: null, }, }); // вставляємо нові виїзди поряд (на початок — щоб одразу видно на дошці) const exists = (id) => window.DATA.TASKS.some(t => t.id === id); const toAdd = [vMeasure, vApproval].filter(t => !exists(t.id)); if (toAdd.length) window.DATA.TASKS = [...toAdd, ...window.DATA.TASKS]; } installVisits(); window.installVisits = installVisits; })();