← back to Omega Watches 2

src/app/(dashboard)/condition-mappings/page.tsx

248 lines

"use client";

import { useState, useEffect } from "react";
import { formatDateTime } from "@/lib/utils";

interface ConditionMapping {
  id: number;
  source_name: string;
  source_category: string;
  normalized_grade: string;
  score_0_100: number;
  version: number;
  created_at: string;
}

const GRADES = [
  "new_unworn", "excellent", "very_good", "good", "fair", "poor", "parts_only", "unknown",
];

const gradeColor: Record<string, string> = {
  new_unworn: "bg-green-900/30 text-green-300",
  excellent: "bg-blue-900/30 text-blue-300",
  very_good: "bg-cyan-900/30 text-cyan-300",
  good: "bg-yellow-900/30 text-yellow-300",
  fair: "bg-orange-900/30 text-orange-300",
  poor: "bg-red-900/30 text-red-300",
  parts_only: "bg-gray-800 text-gray-400",
  unknown: "bg-navy-800 text-gray-500",
};

export default function ConditionMappingsPage() {
  const [mappings, setMappings] = useState<ConditionMapping[]>([]);
  const [bySource, setBySource] = useState<Record<string, ConditionMapping[]>>({});
  const [stats, setStats] = useState({ total_mappings: 0, sources: 0, grades: [] as string[] });
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState("");
  const [expanded, setExpanded] = useState<Set<string>>(new Set());
  const [submitting, setSubmitting] = useState(false);
  const [successMsg, setSuccessMsg] = useState("");

  const [sourceName, setSourceName] = useState("");
  const [sourceCategory, setSourceCategory] = useState("");
  const [grade, setGrade] = useState("unknown");
  const [score, setScore] = useState(50);

  function getHeaders() {
    const token = typeof window !== "undefined" ? localStorage.getItem("omega_token") : null;
    const h: Record<string, string> = { "Content-Type": "application/json" };
    if (token) h["Authorization"] = `Bearer ${token}`;
    return h;
  }

  async function loadMappings() {
    try {
      const res = await fetch("/api/condition-mappings", { headers: getHeaders() });
      const json = await res.json();
      if (json.success) {
        setMappings(json.data.mappings);
        setBySource(json.data.by_source);
        setStats(json.data.stats);
        if (expanded.size === 0) {
          setExpanded(new Set(Object.keys(json.data.by_source)));
        }
      } else {
        setError(json.error?.message || "Failed to load");
      }
    } catch (e: any) {
      setError(e.message);
    } finally {
      setLoading(false);
    }
  }

  useEffect(() => { loadMappings(); }, []);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    if (!sourceName || !sourceCategory) return;
    setSubmitting(true);
    try {
      const res = await fetch("/api/condition-mappings", {
        method: "POST",
        headers: getHeaders(),
        body: JSON.stringify({
          source_name: sourceName.trim().toLowerCase(),
          source_category: sourceCategory.trim(),
          normalized_grade: grade,
          score_0_100: score,
        }),
      });
      const json = await res.json();
      if (json.success) {
        setSourceName("");
        setSourceCategory("");
        setGrade("unknown");
        setScore(50);
        setSuccessMsg("Mapping saved");
        setTimeout(() => setSuccessMsg(""), 3000);
        loadMappings();
      } else {
        setError(json.error?.message || "Failed to save");
      }
    } catch (e: any) {
      setError(e.message);
    } finally {
      setSubmitting(false);
    }
  }

  function toggleSource(src: string) {
    const next = new Set(expanded);
    next.has(src) ? next.delete(src) : next.add(src);
    setExpanded(next);
  }

  if (loading) {
    return <div className="flex h-full items-center justify-center"><div className="text-gray-400">Loading mappings...</div></div>;
  }

  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <h1 className="text-xl font-bold text-gray-100">Condition Mappings</h1>
        <div className="flex gap-2 text-xs">
          <button onClick={() => setExpanded(new Set(Object.keys(bySource)))}
            className="text-omega-400 hover:text-omega-300">Expand All</button>
          <span className="text-gray-600">|</span>
          <button onClick={() => setExpanded(new Set())}
            className="text-omega-400 hover:text-omega-300">Collapse All</button>
        </div>
      </div>

      {error && (
        <div className="rounded bg-red-900/30 p-3 text-red-300">
          {error}
          <button onClick={() => setError("")} className="ml-2 text-xs underline">Dismiss</button>
        </div>
      )}

      {/* Stats */}
      <div className="grid gap-4 md:grid-cols-3">
        <div className="card border-navy-700 text-center">
          <div className="text-3xl font-bold text-omega-400">{stats.total_mappings}</div>
          <div className="text-xs text-gray-500">Total Mappings</div>
        </div>
        <div className="card border-navy-700 text-center">
          <div className="text-3xl font-bold text-omega-400">{stats.sources}</div>
          <div className="text-xs text-gray-500">Sources</div>
        </div>
        <div className="card border-navy-700">
          <div className="text-xs text-gray-500 mb-2">Grades Used</div>
          <div className="flex flex-wrap gap-1">
            {stats.grades.map((g) => (
              <span key={g} className={`rounded px-2 py-0.5 text-xs ${gradeColor[g] || "bg-navy-800 text-gray-500"}`}>
                {g}
              </span>
            ))}
          </div>
        </div>
      </div>

      {/* Add Form */}
      <div className="card border-navy-700">
        <h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-400">Add / Update Mapping</h2>
        <form onSubmit={handleSubmit}>
          <div className="grid gap-4 md:grid-cols-2">
            <div>
              <label className="mb-1 block text-xs text-gray-400">Source Name</label>
              <input type="text" className="input" placeholder="e.g., chrono24"
                value={sourceName} onChange={(e) => setSourceName(e.target.value)} required />
            </div>
            <div>
              <label className="mb-1 block text-xs text-gray-400">Source Category</label>
              <input type="text" className="input" placeholder="e.g., Very good"
                value={sourceCategory} onChange={(e) => setSourceCategory(e.target.value)} required />
            </div>
            <div>
              <label className="mb-1 block text-xs text-gray-400">Normalized Grade</label>
              <select className="input" value={grade} onChange={(e) => setGrade(e.target.value)}>
                {GRADES.map((g) => <option key={g} value={g}>{g}</option>)}
              </select>
            </div>
            <div>
              <label className="mb-1 block text-xs text-gray-400">Score (0-100)</label>
              <input type="number" min={0} max={100} className="input"
                value={score} onChange={(e) => setScore(parseInt(e.target.value) || 0)} required />
            </div>
          </div>
          <div className="mt-4 flex items-center justify-between">
            <button type="submit" disabled={submitting} className="btn-primary disabled:opacity-50">
              {submitting ? "Saving..." : "Save Mapping"}
            </button>
            {successMsg && <span className="text-sm text-green-400">{successMsg}</span>}
          </div>
        </form>
      </div>

      {/* Grouped Mappings */}
      <div className="space-y-3">
        {Object.entries(bySource).sort(([a], [b]) => a.localeCompare(b)).map(([src, items]) => (
          <div key={src} className="card border-navy-700 overflow-hidden p-0">
            <button onClick={() => toggleSource(src)}
              className="flex w-full items-center justify-between px-4 py-3 hover:bg-navy-800 transition-colors">
              <div className="flex items-center gap-3">
                <span className={`text-gray-400 transition-transform ${expanded.has(src) ? "rotate-90" : ""}`}>
                  &#9654;
                </span>
                <span className="text-sm font-semibold text-gray-100">{src}</span>
                <span className="text-xs text-gray-500">({items.length} mappings)</span>
              </div>
            </button>
            {expanded.has(src) && (
              <div className="border-t border-navy-700">
                <table className="w-full text-sm">
                  <thead>
                    <tr className="border-b border-navy-700 text-left text-xs uppercase text-gray-500">
                      <th className="px-4 py-2">Source Category</th>
                      <th className="px-4 py-2">Normalized Grade</th>
                      <th className="px-4 py-2">Score</th>
                      <th className="px-4 py-2">Version</th>
                      <th className="px-4 py-2">Created</th>
                    </tr>
                  </thead>
                  <tbody>
                    {(items as ConditionMapping[]).map((m) => (
                      <tr key={m.id} className="border-b border-navy-800 hover:bg-navy-800/50">
                        <td className="px-4 py-2 text-gray-200">{m.source_category}</td>
                        <td className="px-4 py-2">
                          <span className={`rounded px-2 py-0.5 text-xs ${gradeColor[m.normalized_grade] || "bg-navy-800 text-gray-500"}`}>
                            {m.normalized_grade}
                          </span>
                        </td>
                        <td className="px-4 py-2 text-gray-300">{m.score_0_100}</td>
                        <td className="px-4 py-2 text-gray-400">v{m.version}</td>
                        <td className="px-4 py-2 text-gray-500 text-xs">{formatDateTime(m.created_at)}</td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
            )}
          </div>
        ))}
      </div>
    </div>
  );
}