@@ -176,7 +176,9 @@ def search(
176176 name : str | None = None ,
177177 rank : str | None = None ,
178178 unit : list [str ] | None = None ,
179+ unit_uid : str | None = None ,
179180 agency : list [str ] | None = None ,
181+ agency_uid : str | None = None ,
180182 badge_number : list [str ] | None = None ,
181183 ethnicity : list [str ] | None = None ,
182184 active_after : date | None = None ,
@@ -215,10 +217,10 @@ def search(
215217
216218 match_clauses .append ("MATCH (o:Officer)" )
217219
218- if unit or active_after or active_before or badge_number or agency :
220+ if unit or unit_uid or active_after or active_before or badge_number or agency :
219221 match_clauses .append ("MATCH (o)-[]-(e:Employment)-[]-(u:Unit)" )
220222
221- if agency :
223+ if agency or agency_uid :
222224 match_clauses .append ("MATCH (u)-[:ESTABLISHED_BY]->(a:Agency)" )
223225
224226 # Build WHERE clauses and params
@@ -237,6 +239,10 @@ def search(
237239 where_clauses .append ("ANY(n IN $agency WHERE a.name CONTAINS n)" )
238240 params ["agency" ] = agency
239241
242+ if agency_uid :
243+ where_clauses .append ("a.uid = $agency_uid" )
244+ params ["agency_uid" ] = agency_uid
245+
240246 if badge_number :
241247 where_clauses .append (
242248 "ANY(n IN $badge_number WHERE e.badge_number CONTAINS n)" )
@@ -249,6 +255,10 @@ def search(
249255 if unit :
250256 where_clauses .append ("ANY(n IN $unit WHERE u.name CONTAINS n)" )
251257 params ["unit" ] = unit
258+
259+ if unit_uid :
260+ where_clauses .append ("u.uid = $unit_uid" )
261+ params ["unit_uid" ] = unit_uid
252262
253263 # Combine query
254264 match_str = "\n " .join (match_clauses )
@@ -275,3 +285,68 @@ def search(
275285 rows , _ = db .cypher_query (cypher_query , params ,
276286 resolve_objects = inflate )
277287 return [row [0 ] for row in rows ]
288+
289+ @classmethod
290+ def include_employment (
291+ cls ,
292+ uids : list [str ],
293+ unit_uid : str | None = None ,
294+ agency_uid : str | None = None ,
295+ ):
296+ """
297+ Include employment details for a list of officers.
298+
299+ Coalesces employment at the agency level:
300+ - earliest_date / latest_date come from all matching employments
301+ for the officer at that agency
302+ - other values come from the most recent matching employment
303+ """
304+
305+ cy = """
306+ UNWIND $uids AS officer_uid
307+ MATCH (o:Officer {uid: officer_uid})-[:HELD_BY]-(e:Employment)-[:IN_UNIT]-(u:Unit)-[:ESTABLISHED_BY]-(a:Agency)
308+ WHERE ($unit_uid IS NULL OR u.uid = $unit_uid)
309+ AND ($agency_uid IS NULL OR a.uid = $agency_uid)
310+
311+ WITH o, a, u, e
312+ ORDER BY coalesce(e.latest_date, e.earliest_date) DESC
313+
314+ WITH
315+ o,
316+ a,
317+ head(collect({e: e, u: u})) AS rep,
318+ min(e.earliest_date) AS earliest_date,
319+ max(e.latest_date) AS latest_date
320+
321+ RETURN
322+ o.uid AS officer_uid,
323+ {
324+ agency_uid: a.uid,
325+ agency_name: a.name,
326+ state: a.hq_state,
327+ unit_uid: rep.u.uid,
328+ unit_name: rep.u.name,
329+ badge_number: rep.e.badge_number,
330+ highest_rank: rep.e.highest_rank,
331+ salary: rep.e.salary,
332+ earliest_date: earliest_date,
333+ latest_date: latest_date
334+ } AS item
335+ """
336+ logging .warning ("Cypher query for employment:\n %s" , cy )
337+
338+ params = {
339+ "uids" : uids ,
340+ "unit_uid" : unit_uid ,
341+ "agency_uid" : agency_uid ,
342+ }
343+
344+ results , _ = db .cypher_query (cy , params )
345+
346+ employment_map : dict [str , list [dict ]] = {}
347+ for row in results :
348+ officer_uid = row [0 ]
349+ item = row [1 ]
350+ employment_map .setdefault (officer_uid , []).append (item )
351+
352+ return employment_map
0 commit comments