// field-visits.jsx — UI виїздів // window.VisitPanel — рендериться у Drawer для задач kind === "visit" // window.NewVisitModal — майстер створення виїзду (повний флоу) // window.VisitTypeBadge — бейдж типу виїзду для списків / карток (function () { const { useState } = React; const Ico = window.Ico; const money = (n) => (window.formatMoney ? window.formatMoney(n) : n); // ───────────────────────── helpers ───────────────────────── function KitIcon({ item, size }) { return ; } function TransportPick({ mode, onPick }) { return (
{window.TRANSPORT_MODES.map(m => ( ))}
); } // ═══════════════════════ VISIT TYPE BADGE ═══════════════════════ function VisitTypeBadge({ typeId, small }) { const vt = window.getVisitType(typeId); return ( {vt.short} ); } window.VisitTypeBadge = VisitTypeBadge; // ═══════════════════════ READINESS STRIP ═══════════════════════ function VisitPhaseStrip({ visit, readiness, onSetPhase }) { const phases = window.VISIT_PHASES; const curIdx = window.visitPhaseIndex(visit.phase); return (
{phases.map((p, i) => ( {i > 0 &&
}
))}
); } // ═══════════════════════ VISIT PANEL (у Drawer) ═══════════════════════ function VisitPanel({ task, onTaskPatch }) { const visit = task.visit || {}; const vt = window.getVisitType(visit.typeId); const obj = window.getObject(task.obj); const readiness = window.visitReadiness(visit); const team = window.DATA.TEAM || []; const patchVisit = (fn) => onTaskPatch(task.id, (x) => ({ ...x, visit: fn(x.visit || {}) })); const setTeam = (ids) => onTaskPatch(task.id, (x) => ({ ...x, assignees: ids, assignee: ids[0] || null, visit: { ...(x.visit || {}), team: ids }, })); const setPhase = (id) => { patchVisit(v => ({ ...v, phase: id })); // синхронізуємо статус задачі з фазою const map = { plan: "todo", ready: "live", done: "done" }; onTaskPatch(task.id, (x) => ({ ...x, status: map[id] || x.status })); }; // — team — const teamIds = visit.team || []; const addPerson = (id) => { if (id && !teamIds.includes(id)) setTeam([...teamIds, id]); }; const removePerson = (id) => setTeam(teamIds.filter(x => x !== id)); const notInTeam = team.filter(p => !teamIds.includes(p.id)); // — kit — const kit = visit.kit || []; const patchKit = (fn) => patchVisit(v => ({ ...v, kit: fn(v.kit || []) })); const toggleProp = (id, prop) => patchKit(list => list.map(k => k.id === id ? { ...k, [prop]: !k[prop] } : k)); const removeKit = (id) => patchKit(list => list.filter(k => k.id !== id)); const addKit = (id) => { if (!id || kit.some(k => k.id === id)) return; patchKit(list => [...list, window.buildKitFromIds([id])[0]]); }; const notInKit = window.VISIT_KIT_CATALOG.filter(c => !kit.some(k => k.id === c.id)); const packedReq = kit.filter(k => !k.optional && k.packed).length; const totalReq = kit.filter(k => !k.optional).length; // — transport — const tr = visit.transport || { mode: "taxi", status: "todo" }; const patchTr = (p) => patchVisit(v => ({ ...v, transport: { ...(v.transport || {}), ...p } })); const trMode = window.getTransportMode(tr.mode); // — coordination — const co = visit.coordination || { status: "draft" }; const patchCo = (p) => patchVisit(v => ({ ...v, coordination: { ...(v.coordination || {}), ...p } })); const notify = () => patchCo({ notifiedAt: "щойно" }); // — closeout — const cl = visit.closeout || { didItems: [], journalAN: false, photos: 0, drawings: false, expensesActual: null, files: [] }; const patchCl = (p) => patchVisit(v => ({ ...v, closeout: { ...(v.closeout || {}), ...p } })); const [didInput, setDidInput] = useState(""); const fillDid = () => patchCl({ didItems: vt.closeout.map((t, i) => ({ id: `did-${i}`, text: t, done: false })) }); const addDid = () => { const t = didInput.trim(); if (!t) return; patchCl({ didItems: [...(cl.didItems || []), { id: "did-" + Math.random().toString(36).slice(2, 6), text: t, done: false }] }); setDidInput(""); }; const toggleDid = (id) => patchCl({ didItems: (cl.didItems || []).map(d => d.id === id ? { ...d, done: !d.done } : d) }); const removeDid = (id) => patchCl({ didItems: (cl.didItems || []).filter(d => d.id !== id) }); const showCloseout = visit.phase !== "plan"; return (
{/* Тип + фаза + готовність */}
{vt.label}
{vt.note}
{readiness.ready ? "Готовий до виїзду" : `Готовність ${readiness.pct}%`}
{readiness.steps.map(s => (
{s.label} {s.detail}
))}
{visit.phase === "plan" && ( )} {visit.phase === "ready" && ( )} {visit.phase === "done" && (
Виїзд закрито. Заповніть результат нижче.
)}
{/* Погодження часу */}
Погодження часу
patchCo({ datetime: e.target.value })} />
{Object.entries(window.COORD_STATUS).map(([k, v]) => ( ))}
patchCo({ contactName: e.target.value })} /> patchCo({ contactPhone: e.target.value })} />
patchCo({ contactRole: e.target.value })} />
{co.notifiedAt ? `Сповіщено ${co.notifiedAt}` : "Ще не сповіщали"}
{/* Хто їде */}
Хто їде {teamIds.length > 0 && {teamIds.length}}
{teamIds.length === 0 &&
Склад ще не визначено.
} {teamIds.map((id, i) => { const p = window.getTeam(id); if (!p) return null; return (
{p.initials}
{p.name}{i === 0 ? старший : null}
{p.role}
); })}
{notInTeam.length > 0 && (
)}
{/* Спорядження */}
Спорядження {packedReq}/{totalReq} зібрано
{kit.map(k => { const asset = k.assetId ? window.getAsset(k.assetId) : null; return (
{k.name}{k.future && скоро}
{k.kind === "instrument" && asset && (
{asset.invNo} {k.checkedOut ? видано зі складу : на складі}
)} {k.kind === "instrument" && !asset && !k.future && (
обліковий прилад
)}
{k.kind === "instrument" && !k.future && ( )}
); })}
{notInKit.length > 0 && (
)}
{/* Транспорт */}
Транспорт
patchTr({ mode: m })} />
patchTr({ amount: e.target.value ? Number(e.target.value) : null })} />
{Object.entries(window.TRANSPORT_STATUS).map(([k, v]) => ( ))}
patchTr({ note: e.target.value })} /> {trMode.cashFromRegister && (tr.amount > 0) && (
{money(tr.amount)} ₴ — готівка з каси компанії
)}
{/* Результат виїзду */} {showCloseout && (
Результат виїзду
Що зробили на місці {(!cl.didItems || cl.didItems.length === 0) && }
{(cl.didItems || []).map(d => (
{d.text}
))}
setDidInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") addDid(); }} />
{visit.typeId === "supervision" && ( )}
Фотофіксація {cl.photos || 0} фото
Витрати факт, ₴ patchCl({ expensesActual: e.target.value ? Number(e.target.value) : null })} /> план: {money(tr.amount || 0)} ₴
Файли результату
{(vt.resultFiles || []).map((f, i) => (
{f}
))}
)}
); } window.VisitPanel = VisitPanel; // ═══════════════════════ NEW VISIT MODAL ═══════════════════════ function NewVisitModal({ onClose, onCreate, preset }) { const objects = window.DATA.OBJECTS || []; const team = window.DATA.TEAM || []; const days = window.WEEK_DAYS || []; const today = (days.find(d => d.isToday) || days[0] || {}).day || (window.TODAY ? window.TODAY.day : ""); const [typeId, setTypeId] = useState((preset && preset.typeId) || "measure"); const vt = window.getVisitType(typeId); const [objId, setObjId] = useState((preset && preset.objId) || ""); const [title, setTitle] = useState(""); const [teamIds, setTeamIds] = useState([]); const [day, setDay] = useState(today); const [time, setTime] = useState(""); const [est, setEst] = useState(""); const [datetime, setDatetime] = useState(""); const [contactName, setContactName] = useState(""); const [contactPhone, setContactPhone] = useState(""); const [coordStatus, setCoordStatus] = useState("draft"); const [trMode, setTrMode] = useState("taxi"); const [trAmount, setTrAmount] = useState(""); const [note, setNote] = useState(""); const [kit, setKit] = useState(() => window.buildKitFromIds(window.getVisitType("measure").defaultKit)); // зміна типу → перезбираємо комплект із дефолту цього типу const changeType = (id) => { setTypeId(id); setKit(window.buildKitFromIds(window.getVisitType(id).defaultKit)); }; React.useEffect(() => { const onKey = (e) => e.key === "Escape" && onClose(); window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); const toggleP = (id) => setTeamIds(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]); const removeKit = (id) => setKit(prev => prev.filter(k => k.id !== id)); const addKit = (id) => { if (id && !kit.some(k => k.id === id)) setKit(prev => [...prev, window.buildKitFromIds([id])[0]]); }; const notInKit = window.VISIT_KIT_CATALOG.filter(c => !kit.some(k => k.id === c.id)); const canSubmit = title.trim().length > 0 && teamIds.length > 0; const submit = () => { if (!canSubmit) return; const selObj = objId ? objects.find(o => o.id === objId) : null; const task = { id: "v-" + Math.random().toString(36).slice(2, 8), kind: "visit", title: title.trim(), obj: objId || null, stage: selObj ? (selObj.activeStage || "project") : (objId ? "project" : "admin"), section: vt.section, assignee: teamIds[0], assignees: teamIds, day, time: time.trim() || null, est: est.trim() || null, status: "todo", note: note.trim() || null, subtasks: [], checklists: [], visit: { typeId, phase: "plan", team: teamIds, kit, transport: { mode: trMode, amount: trAmount ? Number(trAmount) : null, status: "todo", note: "" }, coordination: { status: coordStatus, datetime: datetime.trim(), contactName: contactName.trim(), contactPhone: contactPhone.trim(), contactRole: "", notifiedAt: null }, closeout: { didItems: [], journalAN: false, photos: 0, drawings: false, expensesActual: null, files: [] }, }, }; onCreate && onCreate(window.normalizeTask ? window.normalizeTask(task) : task); }; return ( <>
Новий виїзд
Спорядження · склад · транспорт · погодження
{/* Тип виїзду */}
{window.VISIT_TYPES.map(t => ( ))}
{vt.note} Комплект підставлено за типом — можна відредагувати нижче.
{/* Що / назва */}
setTitle(e.target.value)} />
{/* Об'єкт */}
{/* День */}
setTime(e.target.value)} />
setEst(e.target.value)} />
{/* Хто їде */}
{team.map(p => { const on = teamIds.includes(p.id); return ( ); })}
Перший обраний — старший на виїзді.
{/* Транспорт */}
setTrAmount(e.target.value)} />
{/* Погодження */}
setDatetime(e.target.value)} />
{Object.entries(window.COORD_STATUS).map(([k, v]) => ( ))}
setContactName(e.target.value)} /> setContactPhone(e.target.value)} />
{/* Комплект */}
{kit.map(k => (
{k.name}{k.future && скоро}
{k.kind === "instrument" && !k.future && прилад}
))}
{notInKit.length > 0 && (
)}