/* global React */ // ===== Bug reports store ===== const BUGS_KEY = "mayster-bug-reports-v1"; const _ico = (d, extra) => ; const BUG_CATEGORIES = [ // Zła odpowiedź AI: mowa dymek { id: "ai", label: "Zła odpowiedź AI", icon: }, // Błąd w diagramie: kwadrat z przekątnymi (schemat) { id: "diagram", label: "Błąd w diagramie", icon: }, // Błędne dane części: klucz nasadowy { id: "part", label: "Błędne dane części", icon: }, // Problem z interfejsem: monitor { id: "ui", label: "Problem z interfejsem", icon: }, // Wydajność: stoper { id: "performance", label: "Wydajność / wolne działanie", icon: }, // Sugestia / żarówka { id: "feature", label: "Sugestia / brakująca funkcja", icon: }, // Inne: trzy kropki { id: "other", label: "Inne", icon: }, ]; const BUG_PRIORITIES = [ { id: "low", label: "Niski — drobiazg" }, { id: "normal", label: "Normalny" }, { id: "high", label: "Wysoki — blokuje pracę" }, ]; function loadBugs() { try { const stored = JSON.parse(localStorage.getItem(BUGS_KEY) || "null"); if (stored) return stored; return SEED_BUGS; } catch (e) { return SEED_BUGS; } } function saveBugs(list) { try { localStorage.setItem(BUGS_KEY, JSON.stringify(list)); } catch (e) {} window.dispatchEvent(new CustomEvent("mayster-bugs-changed")); } // Seed so user sees history immediately const SEED_BUGS = [ { id: "BUG-0142", title: "Czasem brakuje numeru diagramu przy częściach z D-35", category: "part", priority: "normal", status: "in_progress", description: "Dla niektórych części z diagramu D-35 (instalacja elektryczna) w wynikach chatu nie pojawia się etykieta diagramu — trzeba ręcznie wyszukiwać. Widziałem to przy akumulatorze i prądnicy.", context: { tab: "chat", machine: "Komatsu PC210LC-8K" }, createdAt: Date.now() - 86400000 * 3 - 3600000 * 2, updatedAt: Date.now() - 86400000 * 1, responses: [ { from: "support", author: "Support Mayster", text: "Dzięki za zgłoszenie — potwierdzamy, dotyczy też diagramu D-32. Pracujemy nad poprawką, planowany termin: ten tydzień.", time: Date.now() - 86400000 * 1 } ], }, { id: "BUG-0137", title: "Sugestia: wyszukiwanie po numerze OEM konkurenta", category: "feature", priority: "low", status: "backlog", description: "Fajnie byłoby wpisać numer Volvo i dostać polecone odpowiedniki Komatsu. Klienci czasem podają numer z innej maszyny.", context: { tab: "chat", machine: "Komatsu PC210LC-8K" }, createdAt: Date.now() - 86400000 * 8, updatedAt: Date.now() - 86400000 * 6, responses: [ { from: "support", author: "Support Mayster", text: "Zgadzamy się, ciekawy pomysł. Dodaliśmy do backlogu — ocenimy w następnym sprincie.", time: Date.now() - 86400000 * 6 } ], }, { id: "BUG-0121", title: "Dark mode — słaby kontrast w karcie części", category: "ui", priority: "low", status: "resolved", description: "W trybie ciemnym numer katalogowy jest trudny do odczytania — za ciemny.", context: { tab: "chat", machine: "Komatsu PC210LC-8K" }, createdAt: Date.now() - 86400000 * 14, updatedAt: Date.now() - 86400000 * 10, responses: [ { from: "support", author: "Support Mayster", text: "Poprawione w wersji z 12.03 — daj znać czy teraz OK.", time: Date.now() - 86400000 * 11 }, { from: "user", author: "Jakub K.", text: "Teraz super, dzięki.", time: Date.now() - 86400000 * 10 } ], }, ]; const STATUS_META = { new: { label: "Nowe", cls: "status-new" }, in_progress: { label: "W toku", cls: "status-progress" }, backlog: { label: "W backlogu", cls: "status-backlog" }, resolved: { label: "Rozwiązane", cls: "status-resolved" }, closed: { label: "Zamknięte", cls: "status-closed" }, }; // ===== Bug Report Modal ===== const BUG_DRAFT_KEY = "glimat-bug-report-draft"; function loadBugDraft() { try { return JSON.parse(localStorage.getItem(BUG_DRAFT_KEY) || "null"); } catch { return null; } } function saveBugDraft(d) { try { localStorage.setItem(BUG_DRAFT_KEY, JSON.stringify(d)); } catch {} } function clearBugDraft() { try { localStorage.removeItem(BUG_DRAFT_KEY); } catch {} } function BugReportModal({ initial, onClose }) { const draft = !initial?.aiMessage ? loadBugDraft() : null; const [category, setCategory] = React.useState(initial?.category || draft?.category || "ui"); const [priority, setPriority] = React.useState(initial?.priority || draft?.priority || "normal"); const [title, setTitle] = React.useState(initial?.title || draft?.title || ""); const [description, setDescription] = React.useState(initial?.description || draft?.description || ""); const [submitted, setSubmitted] = React.useState(false); const [bugId, setBugId] = React.useState(null); const [attachments, setAttachments] = React.useState(initial?.attachments || draft?.attachments || []); const fileInputRef = React.useRef(null); const onPickFiles = (e) => { const files = Array.from(e.target.files || []); if (!files.length) return; files.forEach((f) => { if (!f.type.startsWith("image/")) return; if (f.size > 5 * 1024 * 1024) return; // 5MB cap const reader = new FileReader(); reader.onload = (ev) => { setAttachments((prev) => [...prev, { name: f.name, size: f.size, type: f.type, dataUrl: ev.target.result }]); }; reader.readAsDataURL(f); }); e.target.value = ""; }; const removeAttachment = (i) => setAttachments((prev) => prev.filter((_, idx) => idx !== i)); const onPaste = (e) => { const items = Array.from(e.clipboardData?.items || []); const imgs = items.filter((it) => it.type.startsWith("image/")); if (!imgs.length) return; imgs.forEach((it) => { const f = it.getAsFile(); if (!f) return; const reader = new FileReader(); reader.onload = (ev) => { setAttachments((prev) => [...prev, { name: f.name || "zrzut.png", size: f.size, type: f.type, dataUrl: ev.target.result }]); }; reader.readAsDataURL(f); }); }; // Persist draft on every change (only for generic reports, not AI-message-tied ones) React.useEffect(() => { if (initial?.aiMessage) return; if (submitted) return; if (!description.trim() && !title.trim() && category === "ui" && priority === "normal") { clearBugDraft(); return; } saveBugDraft({ category, priority, title, description, attachments }); }, [category, priority, title, description, attachments, submitted, initial]); // Context attached to the ticket. Read DYNAMICALLY from localStorage // (machine label + active tab) — the previous version hard-coded // "Komatsu PC210LC-8K" so every handlowiec submission claimed to be about // that machine, even when they were on Profile / Browser or on a different // machine in chat. Now: read whatever the user actually has selected; if // there's nothing concrete, render no context chip at all. const onAppSurface = typeof window !== "undefined" && window.location.pathname.startsWith("/app"); const currentContext = (() => { if (initial?.context) return initial.context; if (!onAppSurface) return null; const tab = (typeof window !== "undefined" && (localStorage.getItem("mayster-active-tab") || localStorage.getItem("glimat-tab"))) || null; const machine = (typeof window !== "undefined" && localStorage.getItem("mayster-machine-label")) || null; if (!tab && !machine) return null; return { tab, machine }; })(); // Map widget category ids → backend ticket enum const CATEGORY_TO_API = { ai: "bad_ai_answer", diagram: "diagram_error", part: "wrong_part_data", ui: "ui_problem", performance: "performance", feature: "suggestion", other: "other", }; const submit = () => { if (!description.trim()) return; // Auto-derive a title from the first line of description (first 80 chars) const autoTitle = description.trim().split(/\n/)[0].slice(0, 80).trim() || "Bez tytułu"; const list = loadBugs(); const nextNum = Math.max(142, ...list.map((b) => parseInt(b.id.replace("BUG-", ""), 10) || 0)) + 1; const finalTitle = (initial?.title && initial.title.trim()) || autoTitle; const newBug = { id: "BUG-" + String(nextNum).padStart(4, "0"), title: finalTitle, category, priority, status: "new", description: description.trim(), context: currentContext, aiMessage: initial?.aiMessage, attachments, createdAt: Date.now(), updatedAt: Date.now(), responses: [], }; const next = [newBug, ...list]; saveBugs(next); clearBugDraft(); // Post to backend so the ticket lands in tenant_admin / superadmin Zgłoszenia. // localStorage entry stays as user's personal cache regardless of API result. // Screenshots <= 5 sent inline as data URLs (cap mirrors backend zod // schema; if a user attaches more, only the first 5 go to the server — // local cache keeps everything for the user's own profile view). const payloadShots = (attachments || []).slice(0, 5).map((a) => ({ name: a.name || "zrzut.png", type: a.type || "image/png", dataUrl: a.dataUrl, })); const techContext = { url: typeof window !== "undefined" ? window.location.pathname : undefined, userAgent: typeof navigator !== "undefined" ? navigator.userAgent.slice(0, 480) : undefined, viewport: typeof window !== "undefined" ? `${window.innerWidth}×${window.innerHeight}` : undefined, machine: currentContext?.machine, }; // Link the active conversation (chat tab) so admin's TicketDetailView // can inline-render the rozmowa-poprzedzająca-zgłoszenie + jump from // ticket → conversation. Skip when there's no active conv or when the // reporter is on admin (no chat session in flight). const activeConv = typeof localStorage !== "undefined" ? localStorage.getItem("mayster-active-conv") : null; fetch("/api/tickets", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ category: CATEGORY_TO_API[category] || "other", title: finalTitle, description: description.trim(), priority, screenshots: payloadShots, techContext, ...(activeConv ? { conversationId: activeConv } : {}), }), }).catch(() => { /* offline / unauth — local copy is enough */ }); setBugId(newBug.id); setSubmitted(true); }; const cancel = () => { clearBugDraft(); onClose(); }; if (submitted) { return (
Twoje zgłoszenie {bugId} trafiło do zespołu Mayster. Zobaczysz je w sekcji Moje zgłoszenia w profilu — tam dostaniesz też odpowiedź.
Kliknij na liście po lewej, żeby zobaczyć szczegóły, odpowiedzi Supportu i historię.