Frontend redesign chunk 3: type layer honesty and the duplicate fix
Declare the fields the backend already writes and the UI was discarding: CallRecord gains location_coords, units, vehicles, cleared_units, duplicate_of, srcaddr (intelligence.py ~315-327); IncidentRecord gains units_active, units_cleared, location_mentions, last_thin_at (incident_correlator.py _attach, ~1270-1300). Filter duplicate_of client-side in useCalls.ts's three hooks (useCalls, useCallsByIncident, useActiveCalls) so a call flagged as a second node's recording of the same transmission no longer renders twice. Client-side rather than a where() clause to avoid a new composite index. Removes the two now-resolved DEFERRED.md entries (dedup/useCalls, lib/types.ts field gaps). Per UI_REDESIGN.md chunk 3.
This commit is contained in:
@@ -109,6 +109,17 @@ export interface CallRecord {
|
|||||||
/** Legacy field — present on calls recorded before the multi-scene migration. */
|
/** Legacy field — present on calls recorded before the multi-scene migration. */
|
||||||
incident_id?: string | null;
|
incident_id?: string | null;
|
||||||
location: string | null;
|
location: string | null;
|
||||||
|
/** Per-call geocode, written by intelligence.py. Powers the incident-path polyline on the map. */
|
||||||
|
location_coords?: { lat: number; lng: number } | null;
|
||||||
|
/** Unit callsigns mentioned in this specific transmission (e.g. "15-9", "E-41"). */
|
||||||
|
units?: string[];
|
||||||
|
vehicles?: string[];
|
||||||
|
/** Units this call reported as clearing/back in service. */
|
||||||
|
cleared_units?: string[];
|
||||||
|
/** Set when dedup.py identifies this as a second node's recording of the same transmission — the canonical copy has this null. */
|
||||||
|
duplicate_of?: string | null;
|
||||||
|
/** Radio source address, when the system exposes it. */
|
||||||
|
srcaddr?: string | null;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
status: "active" | "ended";
|
status: "active" | "ended";
|
||||||
/** Four-level ladder: routine | minor | moderate | major. Legacy docs may still carry "unknown". */
|
/** Four-level ladder: routine | minor | moderate | major. Legacy docs may still carry "unknown". */
|
||||||
@@ -134,6 +145,14 @@ export interface IncidentRecord {
|
|||||||
talkgroup_ids: string[];
|
talkgroup_ids: string[];
|
||||||
units: string[];
|
units: string[];
|
||||||
vehicles: string[];
|
vehicles: string[];
|
||||||
|
/** Units currently believed on scene — maintained by incident_correlator.py `_attach`. */
|
||||||
|
units_active?: string[];
|
||||||
|
/** Units that reported clearing/back in service on this incident. */
|
||||||
|
units_cleared?: string[];
|
||||||
|
/** Free-text location mentions accumulated across the incident's calls, beyond the primary `location`. */
|
||||||
|
location_mentions?: string[];
|
||||||
|
/** ISO timestamp of the last thin/status-only call attached (doesn't refresh `updated_at`). */
|
||||||
|
last_thin_at?: string | null;
|
||||||
severity: string | null;
|
severity: string | null;
|
||||||
started_at: string;
|
started_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
|||||||
@@ -52,10 +52,17 @@ export function useCalls(limitCount = 50, dateFrom?: Date, dateTo?: Date) {
|
|||||||
const toISO = (v: any): string | null =>
|
const toISO = (v: any): string | null =>
|
||||||
v?.toDate?.()?.toISOString?.() ?? (typeof v === "string" ? v : null);
|
v?.toDate?.()?.toISOString?.() ?? (typeof v === "string" ? v : null);
|
||||||
unsubFirestore = onSnapshot(q, (snap) => {
|
unsubFirestore = onSnapshot(q, (snap) => {
|
||||||
setCalls(snap.docs.map((d) => {
|
const docs = snap.docs
|
||||||
const data = d.data();
|
.map((d) => {
|
||||||
return { ...data, started_at: toISO(data.started_at) ?? "", ended_at: toISO(data.ended_at) } as CallRecord;
|
const data = d.data();
|
||||||
}));
|
return { ...data, started_at: toISO(data.started_at) ?? "", ended_at: toISO(data.ended_at) } as CallRecord;
|
||||||
|
})
|
||||||
|
// dedup.py flags a second node's recording of the same transmission
|
||||||
|
// with duplicate_of set to the canonical call_id — filtered client-
|
||||||
|
// side (not a where() clause) so this doesn't need a new composite
|
||||||
|
// index alongside the existing org_id/started_at query.
|
||||||
|
.filter((c) => !c.duplicate_of);
|
||||||
|
setCalls(docs);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, (err: FirestoreError) => { console.error("useCalls:", err); setError(err.message); setLoading(false); });
|
}, (err: FirestoreError) => { console.error("useCalls:", err); setError(err.message); setLoading(false); });
|
||||||
});
|
});
|
||||||
@@ -92,10 +99,12 @@ export function useCallsByIncident(incidentId: string | null) {
|
|||||||
where("incident_ids", "array-contains", incidentId)
|
where("incident_ids", "array-contains", incidentId)
|
||||||
);
|
);
|
||||||
unsubFirestore = onSnapshot(q, (snap) => {
|
unsubFirestore = onSnapshot(q, (snap) => {
|
||||||
const docs = snap.docs.map((d) => {
|
const docs = snap.docs
|
||||||
const data = d.data();
|
.map((d) => {
|
||||||
return { ...data, started_at: toISO(data.started_at) ?? "", ended_at: toISO(data.ended_at) } as CallRecord;
|
const data = d.data();
|
||||||
});
|
return { ...data, started_at: toISO(data.started_at) ?? "", ended_at: toISO(data.ended_at) } as CallRecord;
|
||||||
|
})
|
||||||
|
.filter((c) => !c.duplicate_of);
|
||||||
docs.sort((a, b) => a.started_at.localeCompare(b.started_at));
|
docs.sort((a, b) => a.started_at.localeCompare(b.started_at));
|
||||||
setCalls(docs);
|
setCalls(docs);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -131,10 +140,14 @@ export function useActiveCalls() {
|
|||||||
const toISO = (v: any): string | null =>
|
const toISO = (v: any): string | null =>
|
||||||
v?.toDate?.()?.toISOString?.() ?? (typeof v === "string" ? v : null);
|
v?.toDate?.()?.toISOString?.() ?? (typeof v === "string" ? v : null);
|
||||||
unsubFirestore = onSnapshot(q, (snap) => {
|
unsubFirestore = onSnapshot(q, (snap) => {
|
||||||
setCalls(snap.docs.map((d) => {
|
setCalls(
|
||||||
const data = d.data();
|
snap.docs
|
||||||
return { ...data, started_at: toISO(data.started_at) ?? "", ended_at: toISO(data.ended_at) } as CallRecord;
|
.map((d) => {
|
||||||
}));
|
const data = d.data();
|
||||||
|
return { ...data, started_at: toISO(data.started_at) ?? "", ended_at: toISO(data.ended_at) } as CallRecord;
|
||||||
|
})
|
||||||
|
.filter((c) => !c.duplicate_of)
|
||||||
|
);
|
||||||
}, (err: FirestoreError) => { console.error("useActiveCalls:", err); });
|
}, (err: FirestoreError) => { console.error("useActiveCalls:", err); });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user