File size: 4,802 Bytes
de1e3fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9122959
de1e3fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import { useCallback, useEffect, useState } from "react";
import { api } from "../api";
import type { AdminCaseDetail } from "../types";
import MatchCard from "./MatchCard";

const TYPE_BADGE: Record<string, string> = {
  lost: "bg-red-100 text-red-700",
  found: "bg-accent-100 text-accent-700",
};

// Admin modal: a case with its subject dog (all photos) + persisted matches, and a run/re-run
// matching action. Shared by the All Cases table and the owner-detail view.
export default function CaseDetailModal({
  caseId,
  onClose,
  onChanged,
}: {
  caseId: number;
  onClose: () => void;
  onChanged?: () => void;
}) {
  const [data, setData] = useState<AdminCaseDetail | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [running, setRunning] = useState(false);

  const load = useCallback(() => {
    return api
      .getCaseDetail(caseId)
      .then(setData)
      .catch((e) => setError(e instanceof Error ? e.message : "Failed to load"));
  }, [caseId]);

  useEffect(() => {
    setData(null);
    setError(null);
    load();
  }, [load]);

  async function runMatch() {
    setRunning(true);
    setError(null);
    try {
      await api.runCaseMatch(caseId);
      await load();
      onChanged?.();
    } catch (e) {
      setError(e instanceof Error ? e.message : "Matching failed");
    } finally {
      setRunning(false);
    }
  }

  const c = data?.case;
  const dog = data?.dog;

  return (
    <div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-30" role="dialog" onClick={onClose}>
      <div className="card max-w-3xl w-full max-h-[85vh] overflow-y-auto" onClick={(e) => e.stopPropagation()}>
        {error && <p className="text-red-600 text-sm mb-2">{error}</p>}
        {!data ? (
          <p className="text-gray-500">Loading…</p>
        ) : (
          <>
            <div className="flex items-start justify-between mb-1">
              <div>
                <h3 className="font-bold text-lg">
                  <span className={`badge ${TYPE_BADGE[c!.type]} mr-2`}>{c!.type}</span>
                  {dog?.profile.name ?? "—"}
                </h3>
                <p className="text-xs text-gray-500">
                  {c!.type === "lost" ? "Reported by " : "Found by "}
                  {c!.person?.name || c!.finder_name || c!.finder_email || "—"}
                  {" · "}ZIP {c!.event_zip} · {c!.event_date} · {c!.status}
                </p>
              </div>
              <button className="btn-secondary" onClick={onClose}>
                Close
              </button>
            </div>

            {dog && (
              <div className="mt-3">
                <p className="text-sm font-medium text-gray-700 mb-1">
                  {c!.type === "lost" ? "Lost dog" : "Found dog"} · {dog.photos.length} photo(s)
                </p>
                <div className="grid grid-cols-3 sm:grid-cols-4 gap-2">
                  {dog.photos.map((p) => (
                    <img
                      key={p.id}
                      src={p.url || p.thumb_url || "/paw-placeholder.svg"}
                      alt={dog.profile.name}
                      className="w-full h-28 object-contain rounded-md bg-gray-100"
                    />
                  ))}
                  {dog.photos.length === 0 && (
                    <p className="text-gray-500 text-sm col-span-full">No photos.</p>
                  )}
                </div>
              </div>
            )}

            <div className="mt-4 flex items-center justify-between">
              <h4 className="font-semibold">
                Matches{" "}
                <span className="text-sm font-normal text-gray-500">
                  ({c!.type === "lost" ? "candidate found dogs" : "candidate lost dogs"})
                </span>
              </h4>
              <button className="btn-primary" disabled={running} onClick={runMatch}>
                {running ? "Running…" : data.matches.length ? "Re-run matching" : "Run matching"}
              </button>
            </div>

            {data.matches.length === 0 ? (
              <p className="text-gray-500 text-sm mt-2">
                No matches yet. Run matching to find candidates; if none appear, widen the search or the
                dog may not be in the system.
              </p>
            ) : (
              <div className="space-y-3 mt-3">
                {data.matches.map((m) => (
                  <MatchCard
                    key={m.id}
                    match={m}
                    queryPhotos={dog?.photos}
                    queryLabel={dog ? `${dog.profile.name} (this case)` : undefined}
                  />
                ))}
              </div>
            )}
          </>
        )}
      </div>
    </div>
  );
}