Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 128 additions & 7 deletions frontend/src/app/police/_components/PartyList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ import {
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import type { IncidentSeverity } from "@/lib/api/incident/incident.types";
import {
getCitationCount,
Expand All @@ -27,7 +36,9 @@ import { format } from "date-fns";
import { EllipsisVertical } from "lucide-react";
import Image from "next/image";
import type { MouseEvent } from "react";
import { useState } from "react";
import { useEffect, useState } from "react";

const PAGE_SIZE = 10;

interface PartyListProps {
parties?: PartyDto[];
Expand All @@ -48,6 +59,47 @@ const PartyList = ({ parties = [], onSelect, activeParty }: PartyListProps) => {
const [incidentType, setIncidentType] =
useState<IncidentSeverity>("in_person_warning");
const [selectedParty, setSelectedParty] = useState<PartyDto | null>(null);
const [currentPage, setCurrentPage] = useState(0);

// Reset to first page when the party list changes (filters/date range updated)
useEffect(() => {
setCurrentPage(0);
}, [parties]);

// Jump to the correct page when a map pin is selected
useEffect(() => {
if (!activeParty) return;
const idx = parties.findIndex((p) => p.id === activeParty.id);
if (idx === -1) return;
setCurrentPage(Math.floor(idx / PAGE_SIZE));
}, [activeParty, parties]);

// Scroll to the active party card after the page renders
useEffect(() => {
if (!activeParty) return;
const el = document.querySelector(`[data-party-id="${activeParty.id}"]`);
if (el) el.scrollIntoView({ behavior: "smooth", block: "nearest" });
}, [activeParty, currentPage]);

const totalPages = Math.ceil(parties.length / PAGE_SIZE);
const paginatedParties = parties.slice(
currentPage * PAGE_SIZE,
(currentPage + 1) * PAGE_SIZE
);

const maxVisiblePages = 3;
const pageStart = Math.max(
0,
Math.min(
currentPage - Math.floor(maxVisiblePages / 2),
totalPages - maxVisiblePages
)
);
const pageEnd = Math.min(pageStart + maxVisiblePages, totalPages);
const pageIndexes = Array.from(
{ length: Math.max(pageEnd - pageStart, 0) },
(_, i) => pageStart + i
);

if (parties.length === 0) {
return (
Expand All @@ -70,13 +122,13 @@ const PartyList = ({ parties = [], onSelect, activeParty }: PartyListProps) => {

return (
<>
<ul className="h-full w-full overflow-y-auto rounded-md border border-border bg-card card-shadow [scroll-behavior:smooth]">
{parties.map((party) =>
(() => {
<div className="flex flex-col min-h-0 flex-1 gap-3">
<ul className="flex-1 min-h-0 w-full overflow-y-auto rounded-md border border-border bg-card card-shadow [scroll-behavior:smooth]">
{paginatedParties.map((party) => {
const remoteWarningCount = getRemoteWarningCount(party.location);
const inPersonWarningCount = getInPersonWarningCount(
party.location
);
const remoteWarningCount = getRemoteWarningCount(party.location);
const citationCount = getCitationCount(party.location);

return (
Expand Down Expand Up @@ -229,9 +281,78 @@ const PartyList = ({ parties = [], onSelect, activeParty }: PartyListProps) => {
</article>
</li>
);
})()
})}
</ul>

{totalPages > 1 && (
<div className="flex flex-col items-center gap-2">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href="#"
onClick={(e) => {
e.preventDefault();
setCurrentPage((p) => Math.max(0, p - 1));
}}
className={cn(
currentPage === 0
? "pointer-events-none opacity-50"
: "cursor-pointer"
)}
/>
</PaginationItem>
{pageStart > 0 && (
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
)}
{pageIndexes.map((pageIndex) => (
<PaginationItem key={pageIndex}>
<PaginationLink
href="#"
onClick={(e) => {
e.preventDefault();
setCurrentPage(pageIndex);
}}
isActive={currentPage === pageIndex}
className="cursor-pointer"
>
{pageIndex + 1}
</PaginationLink>
</PaginationItem>
))}
{pageEnd < totalPages && (
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
)}
<PaginationItem>
<PaginationNext
href="#"
onClick={(e) => {
e.preventDefault();
setCurrentPage((p) => Math.min(totalPages - 1, p + 1));
}}
className={cn(
currentPage === totalPages - 1
? "pointer-events-none opacity-50"
: "cursor-pointer"
)}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
<p className="content text-muted-foreground">
Results {currentPage * PAGE_SIZE + 1}
{" - "}
{Math.min((currentPage + 1) * PAGE_SIZE, parties.length)} of{" "}
{parties.length}
</p>
</div>
)}
</ul>
</div>

<AddIncidentDialog
open={incidentDialogOpen}
onOpenChange={setIncidentDialogOpen}
Expand Down
8 changes: 1 addition & 7 deletions frontend/src/app/police/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
usePoliceParties,
} from "@/lib/api/party/police-party.queries";
import { startOfDay } from "date-fns";
import { useEffect, useMemo, useState } from "react";
import { useMemo, useState } from "react";
import AdvancedPartySearch, {
AdvancedPartyFilters,
} from "./_components/AdvancedPartySearch";
Expand Down Expand Up @@ -138,12 +138,6 @@ export default function PolicePage() {
setActiveParty(party ?? undefined);
}

useEffect(() => {
if (!activeParty) return;
const el = document.querySelector(`[data-party-id="${activeParty.id}"]`);
if (el) el.scrollIntoView({ behavior: "smooth", block: "nearest" });
}, [activeParty]);

return (
<main className="lg:h-[calc(100vh-var(--app-header-height))] lg:overflow-hidden overflow-y-auto bg-background px-4 py-4 md:px-6 md:py-6">
<div className="grid lg:h-full gap-6 lg:grid-cols-[minmax(22rem,34rem)_minmax(0,1fr)]">
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/lib/api/party/party.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
import apiClient from "@/lib/network/apiClient";
import { PaginatedResponse } from "@/lib/shared";
import { AxiosInstance } from "axios";
import { format } from "date-fns";
import {
AdminCreatePartyDto,
PartyDto,
Expand Down Expand Up @@ -68,8 +69,8 @@ export class PartyService {
{
params: {
place_id: placeId,
start_date: startDate.toISOString(),
end_date: endDate.toISOString(),
start_date: format(startDate, "yyyy-MM-dd"),
end_date: format(endDate, "yyyy-MM-dd"),
},
}
);
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/lib/api/party/police-party.queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ export function usePoliceParties(
queryFn: async () => {
const params: ServerTableParams = {
page_number: 1,
page_size: 200,
filters: {},
};
if (startDate)
params.filters.party_datetime_gte = startDate.toISOString();
if (endDate) params.filters.party_datetime_lte = endDate.toISOString();
if (endDate) {
const endOfDay = new Date(endDate);
endOfDay.setHours(23, 59, 59, 999);
params.filters.party_datetime_lte = endOfDay.toISOString();
}
const page = await partyService.listParties(params);
return page.items;
},
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/lib/api/shared/query-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export type ServerColumnMap = Record<string, ServerColumnConfig>;

export type ServerTableParams = {
page_number: number;
page_size: number;
page_size?: number;
sort_by?: string;
sort_order?: "asc" | "desc";
filters: Record<string, string>;
Expand Down Expand Up @@ -109,8 +109,8 @@ export function toAxiosParams(
): Record<string, string | number> {
const result: Record<string, string | number> = {
page_number: params.page_number,
page_size: params.page_size,
};
if (params.page_size !== undefined) result.page_size = params.page_size;
if (params.sort_by) result.sort_by = params.sort_by;
if (params.sort_order) result.sort_order = params.sort_order;
return { ...result, ...params.filters };
Expand Down
Loading