import React, { useState, useEffect, useMemo } from "react";
import {
Plus,
Search,
Trash2,
ArrowLeft,
Printer,
Clock,
CheckCircle2,
FileText,
Loader2,
AlertCircle,
Mail,
MessageCircle,
Tag,
Copy,
} from "lucide-react";
// ---------- styling (custom tokens layered under Tailwind utilities) ----------
const STYLES = `
.wo-root {
--paper: #EFE6D2;
--paper-deep: #DBCBA2;
--ink: #2A241C;
--ink-soft: #6B6152;
--rust: #C1502B;
--rust-deep: #8F3A1E;
--bench: #1B232C;
--bench-panel: #232D38;
--bench-line: #344150;
--bench-text: #E8E3D8;
--bench-label: #93A4B2;
--amber: #E8A33D;
--ok: #5B8C3E;
--ok-soft: #DCEAD2;
--cream: #F7F2E6;
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
color: var(--ink);
background: var(--cream);
min-height: 100vh;
}
.wo-mono {
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
}
.wo-header { background: var(--ink); color: var(--cream); border-bottom: 4px solid var(--rust); }
.wo-card {
background: var(--paper);
border: 1px solid var(--paper-deep);
border-left: 6px solid var(--rust);
}
.wo-card:hover { border-left-color: var(--rust-deep); }
.wo-panel-paper {
background: var(--paper);
border: 1px solid var(--paper-deep);
}
.wo-panel-paper input, .wo-panel-paper textarea {
background: var(--cream);
border: 1px solid var(--paper-deep);
color: var(--ink);
}
.wo-panel-paper input::placeholder, .wo-panel-paper textarea::placeholder { color: #B5A98A; }
.wo-panel-paper label { color: var(--ink-soft); }
.wo-panel-paper input:focus, .wo-panel-paper textarea:focus {
outline: none; border-color: var(--rust); box-shadow: 0 0 0 3px rgba(193,80,43,0.15);
}
.wo-panel-bench { background: var(--bench); color: var(--bench-text); }
.wo-panel-bench input, .wo-panel-bench textarea {
background: var(--bench-panel);
border: 1px solid var(--bench-line);
color: var(--bench-text);
}
.wo-panel-bench input::placeholder, .wo-panel-bench textarea::placeholder { color: #5C6A78; }
.wo-panel-bench label { color: var(--bench-label); }
.wo-panel-bench input:focus, .wo-panel-bench textarea:focus {
outline: none; border-color: var(--amber); box-shadow: 0 0 0 3px rgba(232,163,61,0.18);
}
.wo-divider { position: relative; height: 0; border-top: 3px dashed var(--paper-deep); margin: 0 1.5rem; }
.wo-divider::before, .wo-divider::after {
content: ""; position: absolute; width: 16px; height: 16px;
background: var(--cream); border-radius: 50%; top: -9px;
}
.wo-divider::before { left: -1.5rem; }
.wo-divider::after { right: -1.5rem; }
.wo-badge { font-size: 0.65rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; padding: 0.2rem 0.55rem; border-radius: 999px; display: inline-flex; align-items: center; gap: 0.3rem; }
.wo-badge-new { background: var(--paper-deep); color: var(--ink); }
.wo-badge-progress { background: #F4D8AE; color: #7A4A12; }
.wo-badge-done { background: var(--ok-soft); color: var(--ok); }
.wo-btn-primary { background: var(--rust); color: var(--cream); }
.wo-btn-primary:hover { background: var(--rust-deep); }
.wo-fade-in { animation: woFadeIn 0.25s ease-out; }
@keyframes woFadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
.wo-label { display: none; }
@media print {
.no-print { display: none !important; }
.wo-panel-bench { background: white !important; color: black !important; border: 1px solid #999 !important; }
.wo-panel-bench input, .wo-panel-bench textarea, .wo-panel-bench label { background: white !important; color: black !important; border-color: #999 !important; }
.wo-root { background: white !important; }
}
`;
const STORAGE_KEY = "workorders-data";
function blankOrder(number) {
const year = new Date().getFullYear();
return {
id: `WO-${year}-${String(number).padStart(4, "0")}`,
number,
createdAt: new Date().toISOString(),
contactPerson: "",
address: "",
email: "",
phone: "",
serial: "",
osVersion: "",
familyNumber: "",
specs: "",
model: "",
password: "",
itemsSent: "",
problemType: "",
otherNotes: "",
signature: "",
dateSigned: "",
dateReceived: "",
dateCompleted: "",
techNotes: "",
problemsSolution: "",
partsOrdered: "",
calledContact: "",
poNumber: "",
dateTime: "",
costs: "",
};
}
function getStatus(order) {
if (order.dateCompleted) return "done";
if (order.dateReceived) return "progress";
return "new";
}
function statusLabel(status) {
if (status === "done") return "Completed";
if (status === "progress") return "In repair";
return "New";
}
function buildMessage(order, kind) {
const name = order.contactPerson || "there";
const device = order.model || "computer";
if (kind === "approval") {
const parts = order.partsOrdered ? ` (${order.partsOrdered})` : "";
const cost = order.costs ? `, estimated cost ${order.costs}` : "";
return `Hi ${name}, this is Jaks Systems. We need your approval before ordering parts for ticket ${order.id}${parts}${cost}. Please confirm and we'll go ahead.`;
}
if (kind === "ready") {
return `Hi ${name}, good news — your ${device} (ticket ${order.id}) is repaired and ready for pickup at Jaks Systems, Christ Church.`;
}
return `Hi ${name}, this is Jaks Systems. We've received your ${device} for repair (ticket ${order.id}). We'll be in touch with updates. Thanks!`;
}
function defaultTemplateKind(order) {
if (getStatus(order) === "done") return "ready";
if (order.partsOrdered) return "approval";
return "received";
}
function formatWhatsAppNumber(phone) {
const digits = (phone || "").replace(/\D/g, "");
if (!digits) return "";
if (digits.length === 10) return `1${digits}`;
return digits;
}
function escapeHtml(value) {
return String(value ?? "")
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function printableShell(title, bodyHtml, pageStyle) {
return `
${escapeHtml(title)}
${bodyHtml}
`;
}
function buildFullPrintDoc(order) {
const field = (label, value, full) => `
${escapeHtml(label)}
${escapeHtml(value) || " "}
`;
const style = `
h1 { font-size: 19px; margin: 0 0 2px; letter-spacing: 0.02em; }
.sub { font-size: 11px; color: #666; margin-bottom: 18px; }
.section-title { font-size: 12px; font-weight: bold; text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid #999; margin: 18px 0 10px; padding-bottom: 4px; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 18px; }
.po-field { font-size: 12px; padding: 3px 0; border-bottom: 1px dotted #ccc; }
.po-field.full { grid-column: 1 / -1; }
.po-label { font-size: 9px; text-transform: uppercase; letter-spacing: 0.04em; color: #777; }
.po-value { margin-top: 1px; min-height: 14px; }
.note { font-size: 10px; color: #777; font-style: italic; margin-bottom: 10px; }
@page { margin: 0.5in; }
`;
const body = `
Jaks Systems
Christ Church · 246-257-3181 · Ticket ${escapeHtml(order.id)}
Customer intake
No parts will be ordered without contact person's permission.
${field("Contact person", order.contactPerson)}
${field("Phone / cell", order.phone)}
${field("Address", order.address)}
${field("E-mail", order.email)}
Computer information
${field("Serial number", order.serial)}
${field("OS version", order.osVersion)}
${field("Family number", order.familyNumber)}
${field("Processor / RAM / harddrive", order.specs)}
${field("Model", order.model)}
${field("Password", order.password)}
${field("Items sent with computer", order.itemsSent, true)}
${field("Type of problem", order.problemType, true)}
${field("Other notes", order.otherNotes, true)}
${field("Signature", order.signature)}
${field("Date", order.dateSigned)}
Repair center
${field("Date received", order.dateReceived)}
${field("Date completed and sent", order.dateCompleted)}
${field("Tech notes", order.techNotes, true)}
${field("Problems / solution", order.problemsSolution, true)}
${field("Parts ordered", order.partsOrdered)}
${field("Called contact person", order.calledContact)}
${field("PO #", order.poNumber)}
${field("Date / time", order.dateTime)}
${field("Costs", order.costs)}
`;
return printableShell(`Work order ${order.id}`, body, style);
}
function buildLabelPrintDoc(order) {
const style = `
body { padding: 0.15in; width: 3in; }
.brand { font-size: 9px; font-weight: bold; text-transform: uppercase; letter-spacing: 0.04em; border-bottom: 1px solid #000; padding-bottom: 4px; margin-bottom: 6px; }
.id { font-family: "Courier New", monospace; font-size: 18px; font-weight: 800; margin-bottom: 6px; }
.row { font-size: 11px; line-height: 1.35; margin-bottom: 2px; }
.date { color: #555; font-size: 9px; margin-top: 4px; }
@page { size: 4in 2in; margin: 0; }
`;
const device = [order.model, order.serial ? `SN ${order.serial}` : ""].filter(Boolean).join(" · ") || "Device not specified";
const dropped = order.dateSigned || order.createdAt;
const body = `
Jaks Systems · 246-257-3181
${escapeHtml(order.id)}
${escapeHtml(order.contactPerson) || "Unnamed customer"}
${escapeHtml(order.phone) || "No phone on file"}
${device}
${escapeHtml(order.problemType) || "No problem noted"}
Dropped off ${dropped ? escapeHtml(new Date(dropped).toLocaleDateString()) : ""}
`;
return printableShell(`Label ${order.id}`, body, style);
}
function StatusBadge({ status }) {
const cls =
status === "done" ? "wo-badge-done" : status === "progress" ? "wo-badge-progress" : "wo-badge-new";
const Icon = status === "done" ? CheckCircle2 : status === "progress" ? Clock : FileText;
return (
{statusLabel(status)}
);
}
function Field({ label, value, onChange, type = "text", mono = false, placeholder, full = false }) {
return (
{label}
onChange(e.target.value)}
placeholder={placeholder}
className={`rounded-md px-3 py-2 text-sm font-normal normal-case ${mono ? "wo-mono" : ""}`}
/>
);
}
function TextArea({ label, value, onChange, rows = 3, full = true }) {
return (
{label}
);
}
function SectionHeading({ children }) {
return (
{children}
);
}
export default function WorkOrderApp() {
const [orders, setOrders] = useState([]);
const [nextNumber, setNextNumber] = useState(1);
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState(false);
const [view, setView] = useState("list");
const [activeOrder, setActiveOrder] = useState(null);
const [search, setSearch] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [confirmDelete, setConfirmDelete] = useState(false);
const [saveNote, setSaveNote] = useState("");
const [messageText, setMessageText] = useState("");
const [emailSubject, setEmailSubject] = useState("");
const [printNotice, setPrintNotice] = useState("");
const [copyNote, setCopyNote] = useState("");
useEffect(() => {
(async () => {
try {
const res = await window.storage.get(STORAGE_KEY, true);
if (res && res.value) {
const parsed = JSON.parse(res.value);
setOrders(Array.isArray(parsed.orders) ? parsed.orders : []);
setNextNumber(parsed.nextNumber || 1);
}
} catch (e) {
// key not found yet is expected on first run; only flag real errors
}
setLoading(false);
})();
}, []);
async function persist(newOrders, newNextNumber) {
try {
const result = await window.storage.set(
STORAGE_KEY,
JSON.stringify({ orders: newOrders, nextNumber: newNextNumber }),
true
);
if (!result) setLoadError(true);
} catch (e) {
setLoadError(true);
}
}
function openNewTicket() {
const o = blankOrder(nextNumber);
setActiveOrder(o);
setMessageText(buildMessage(o, defaultTemplateKind(o)));
setEmailSubject(`Jaks Systems – Ticket ${o.id}`);
setConfirmDelete(false);
setView("form");
}
function openTicket(order) {
setActiveOrder({ ...order });
setMessageText(buildMessage(order, defaultTemplateKind(order)));
setEmailSubject(`Jaks Systems – Ticket ${order.id}`);
setConfirmDelete(false);
setView("form");
}
function updateField(key, value) {
setActiveOrder((prev) => ({ ...prev, [key]: value }));
}
function saveTicket() {
const isNew = !orders.some((o) => o.id === activeOrder.id);
const updated = isNew ? [...orders, activeOrder] : orders.map((o) => (o.id === activeOrder.id ? activeOrder : o));
const newNext = isNew ? nextNumber + 1 : nextNumber;
setOrders(updated);
setNextNumber(newNext);
persist(updated, newNext);
setSaveNote("Saved");
setTimeout(() => setSaveNote(""), 1600);
}
function deleteTicket() {
const updated = orders.filter((o) => o.id !== activeOrder.id);
setOrders(updated);
persist(updated, nextNumber);
setView("list");
setConfirmDelete(false);
}
function openPrintWindow(html) {
try {
const blob = new Blob([html], { type: "text/html" });
const url = URL.createObjectURL(blob);
const win = window.open(url, "_blank");
if (!win) {
setPrintNotice("Your browser blocked the print window — please allow pop-ups for this page and try again.");
setTimeout(() => setPrintNotice(""), 6000);
}
setTimeout(() => URL.revokeObjectURL(url), 60000);
} catch (e) {
setPrintNotice("Couldn't open the print preview. Please try again.");
setTimeout(() => setPrintNotice(""), 6000);
}
}
async function copyMessage() {
try {
await navigator.clipboard.writeText(messageText);
setCopyNote("Copied");
} catch (e) {
setCopyNote("Couldn't copy — select and copy the text manually");
}
setTimeout(() => setCopyNote(""), 2500);
}
const filtered = useMemo(() => {
return orders
.filter((o) => (statusFilter === "all" ? true : getStatus(o) === statusFilter))
.filter((o) => {
if (!search.trim()) return true;
const q = search.toLowerCase();
return (
(o.contactPerson || "").toLowerCase().includes(q) ||
(o.problemType || "").toLowerCase().includes(q) ||
(o.id || "").toLowerCase().includes(q)
);
})
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
}, [orders, search, statusFilter]);
if (loading) {
return (
);
}
return (
Jaks Systems
Christ Church · 246-257-3181
{view === "list" && (
New ticket
)}
{loadError && (
Couldn't sync with storage just now. Changes are kept on screen — try saving again in a moment.
)}
{view === "list" && (
setSearch(e.target.value)}
placeholder="Search by name, ticket #, or problem"
className="w-full rounded-md border pl-9 pr-3 py-2 text-sm"
style={{ borderColor: "var(--paper-deep)", background: "var(--paper)" }}
/>
{[
["all", "All"],
["new", "New"],
["progress", "In repair"],
["done", "Completed"],
].map(([key, label]) => (
setStatusFilter(key)}
className="rounded-full px-3 py-1.5 text-xs font-semibold whitespace-nowrap"
style={
statusFilter === key
? { background: "var(--ink)", color: "var(--cream)" }
: { background: "var(--paper)", color: "var(--ink-soft)", border: "1px solid var(--paper-deep)" }
}
>
{label}
))}
{filtered.length === 0 ? (
{orders.length === 0 ? "No tickets yet" : "No tickets match that search"}
{orders.length === 0
? "Create the first work order to get started."
: "Try a different name, ticket number, or filter."}
{orders.length === 0 && (
New ticket
)}
) : (
{filtered.map((o) => {
const status = getStatus(o);
return (
openTicket(o)}
className="wo-card w-full text-left rounded-md px-4 py-3 flex items-center justify-between gap-3"
>
{o.id}
{o.contactPerson || "Unnamed customer"}
{o.problemType || "No problem description yet"}
{new Date(o.createdAt).toLocaleDateString()}
);
})}
)}
)}
{view === "form" && activeOrder && (
setView("list")}
className="flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide opacity-70 hover:opacity-100"
>
Back to tickets
{activeOrder.id}
{/* Device label */}
Jaks Systems · 246-257-3181
{activeOrder.id}
{activeOrder.contactPerson || "Unnamed customer"}
{activeOrder.phone || "No phone on file"}
{[activeOrder.model, activeOrder.serial ? `SN ${activeOrder.serial}` : ""].filter(Boolean).join(" · ") || "Device not specified"}
{activeOrder.problemType || "No problem noted"}
Dropped off {new Date(activeOrder.dateSigned || activeOrder.createdAt).toLocaleDateString()}
{/* Customer intake */}
Customer intake
No parts will be ordered without contact person's permission.
updateField("contactPerson", v)} />
updateField("phone", v)} />
updateField("address", v)} full />
updateField("email", v)} full />
Computer information
updateField("serial", v)} />
updateField("osVersion", v)} />
updateField("familyNumber", v)} />
updateField("specs", v)} />
updateField("model", v)} />
updateField("password", v)} />
{/* Repair bench */}
Repair center
To be completed by repair center.
updateField("dateReceived", v)} />
updateField("dateCompleted", v)} />
updateField("techNotes", v)} rows={2} />
updateField("problemsSolution", v)} rows={2} />
updateField("partsOrdered", v)} />
updateField("calledContact", v)} placeholder="Date / initials" />
updateField("poNumber", v)} />
updateField("dateTime", v)} />
updateField("costs", v)} placeholder="$0.00" />
{/* Message customer */}
Message customer
{[
["received", "Ticket received"],
["approval", "Awaiting approval"],
["ready", "Ready for pickup"],
].map(([kind, label]) => (
setMessageText(buildMessage(activeOrder, kind))}
className="rounded-full px-3 py-1.5 text-xs font-semibold"
style={{ background: "var(--cream)", border: "1px solid var(--paper-deep)", color: "var(--ink-soft)" }}
>
{label}
))}
Email/WhatsApp opens your phone or computer's own app with the message pre-filled — nothing sends automatically. If a button doesn't open anything (some browsers block this), use "Copy message" and paste it in manually.
{!confirmDelete ? (
setConfirmDelete(true)}
className="flex items-center gap-1.5 rounded-md px-3 py-2 text-xs font-semibold text-red-700 hover:bg-red-50"
>
Delete
) : (
Delete this ticket?
Yes, delete
setConfirmDelete(false)} className="text-xs font-semibold opacity-60 px-2">
Cancel
)}
openPrintWindow(buildLabelPrintDoc(activeOrder))}
className="flex items-center gap-1.5 rounded-md px-3 py-2 text-xs font-semibold opacity-70 hover:opacity-100"
>
Print label
openPrintWindow(buildFullPrintDoc(activeOrder))}
className="flex items-center gap-1.5 rounded-md px-3 py-2 text-xs font-semibold opacity-70 hover:opacity-100"
>
Print
{saveNote && {saveNote} }
Save ticket
{printNotice && (
{printNotice}
)}
)}
);
}