Skip to content
Open
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
28 changes: 27 additions & 1 deletion client/src/lesc/components/custody/Custody.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { isValidDeflection, isValidIncident } from '@/utils/validators';

import ChairAvailabilityCard from '../ChairAvailabilityCard';
import EmptyState from '../EmptyState';
import FacilityAddressLinkFromParts from '@/components/facilityAddressLink/FacilityAddressLinkFromParts';
import StatusAccordion from '@/components/StatusAccordion';
import CustodyCard from './CustodyCard';

Expand Down Expand Up @@ -143,6 +144,7 @@ function TransitCustodyCard ({ deflection, highlighted }) {
const detailsComplete = isValidDeflection(deflection) && isValidIncident(deflection?.incident);
const isArrived = deflection?.subjectStatus === 'ONSITE_AWAITING_TRANSFER';
const now = useNow(1000, !isArrived && !!deflection?.expiresAt && detailsComplete);
const incidentAddressDisplay = [deflection?.incident?.addressLine1, deflection?.incident?.addressLine2].filter(Boolean).join(', ');

return (
<Card
Expand All @@ -166,7 +168,15 @@ function TransitCustodyCard ({ deflection, highlighted }) {
<>
<Text size='md' c='gray.4'>•</Text>
<Text size='md' c='indigo.6' truncate>
{deflection.arrivedAt ? `Arrived at ${formatTime(deflection.arrivedAt)}` : 'Arrived'}
{deflection.arrivedAt ? `Arrived ${formatTime(deflection.arrivedAt)}` : 'Arrived'}
</Text>
</>
)}
{!isArrived && deflection.incident?.arrestedAt && (
<>
<Text size='md' c='gray.4'>•</Text>
<Text size='md' c='gray.6' truncate>
Detained {formatTime(deflection.incident.arrestedAt)}
</Text>
</>
)}
Expand All @@ -177,6 +187,22 @@ function TransitCustodyCard ({ deflection, highlighted }) {
<Text size='md'>{subjectDetails.join(', ')}</Text>
)}
</Stack>
{incidentAddressDisplay && (
<FacilityAddressLinkFromParts
addressLine1={deflection.incident?.addressLine1}
addressLine2={deflection.incident?.addressLine2}
city={deflection.incident?.city}
state={deflection.incident?.state}
postalCode={deflection.incident?.postalCode}
style={{
color: 'var(--mantine-color-gray-6)',
textDecoration: 'none',
fontSize: 'var(--mantine-font-size-md)',
}}
>
{incidentAddressDisplay}
</FacilityAddressLinkFromParts>
)}
</Stack>
{detailsComplete && (
<Group gap='md' wrap='nowrap' justify={isArrived ? 'flex-end' : 'space-between'}>
Expand Down
31 changes: 29 additions & 2 deletions client/src/lesc/components/custody/Custody.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -290,10 +290,10 @@ describe('Custody', () => {
renderCustody();

await screen.findByText('Onsite Person');
// Hold 8 has arrivedAt → "Arrived at HH:MM"; Hold 5 has no arrivedAt → plain "Arrived" (defensive fallback).
// Hold 8 has arrivedAt → "Arrived HH:MM AM/PM"; Hold 5 has no arrivedAt → plain "Arrived" (defensive fallback).
// Implementation gates the inline label and the expiry Title as mutually-exclusive,
// so label presence implies timer suppression on those cards.
expect(screen.getByText(/^Arrived at \d{1,2}:\d{2} (AM|PM)$/)).toBeInTheDocument();
expect(screen.getByText(/^Arrived \d{1,2}:\d{2} (AM|PM)$/)).toBeInTheDocument();
expect(screen.getByText('Arrived')).toBeInTheDocument();
});

Expand Down Expand Up @@ -327,4 +327,31 @@ describe('Custody', () => {
includeCurrentOfficer: true,
}));
});

it('renders the incident address as a tappable map link, omitting the city', async () => {
mockSessionStateValue.current = 'transit';

renderCustody();

await screen.findByText('Onsite Person');
// Hold 4's incident address (addressLine1 only, city omitted).
const addressLink = screen.getByRole('link', { name: '1 Main St' });
expect(addressLink.getAttribute('href')).toMatch(/^(https:\/\/www\.google\.com\/maps|https:\/\/maps\.apple\.com|geo:)/);
// Hold 8's incident address.
expect(screen.getByRole('link', { name: '2 Main St' })).toBeInTheDocument();
// Hold 5's incident is empty — no address link rendered for it.
expect(screen.queryByRole('link', { name: /Incomplete Person/i })).not.toBeInTheDocument();
});

it('shows a Detained inline label with arrest time on DETAINED holds', async () => {
mockSessionStateValue.current = 'transit';

renderCustody();

await screen.findByText('Complete Person');
// Hold 4 is DETAINED with an arrestedAt → "Detained HH:MM AM/PM" inline next to the Hold ID.
// Holds 5 and 8 are ONSITE_AWAITING_TRANSFER → render "Arrived ..." instead, not "Detained".
expect(screen.getByText(/^Detained \d{1,2}:\d{2} (AM|PM)$/)).toBeInTheDocument();
expect(screen.queryAllByText(/^Detained /)).toHaveLength(1);
});
});
Loading