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
69 changes: 69 additions & 0 deletions src/api/calls/__tests__/saveCall.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { createCall, updateCall } from '../calls';

jest.mock('../../common/client', () => {
const post = jest.fn();
const put = jest.fn();
return {
createApiEndpoint: jest.fn(() => ({ get: jest.fn(), post, put, delete: jest.fn() })),
__mockPost: post,
__mockPut: put,
};
});

const { __mockPost: mockPost, __mockPut: mockPut } = jest.requireMock('../../common/client') as { __mockPost: jest.Mock; __mockPut: jest.Mock };

const baseCall = { name: 'Structure Fire', nature: 'Smoke showing', priority: 1 };

describe('call save payloads', () => {
beforeEach(() => {
jest.clearAllMocks();
mockPost.mockResolvedValue({ data: { Id: '42' } });
mockPut.mockResolvedValue({ data: { Id: '42' } });
});

describe('createCall (SaveCall)', () => {
it('sends the call identifiers', async () => {
await createCall({ ...baseCall, externalId: 'CAD-9', incidentId: 'INC-1', referenceId: 'REF-2' });

expect(mockPost).toHaveBeenCalledWith(expect.objectContaining({ ExternalId: 'CAD-9', IncidentId: 'INC-1', ReferenceId: 'REF-2' }));
});

it('sends blank identifiers when none were entered', async () => {
await createCall(baseCall);

expect(mockPost).toHaveBeenCalledWith(expect.objectContaining({ ExternalId: '', IncidentId: '', ReferenceId: '' }));
});

it('sends DispatchOnUtc only when a dispatch time is set', async () => {
await createCall({ ...baseCall, dispatchOnUtc: '2026-10-09T14:30:00.000Z' });
expect(mockPost).toHaveBeenLastCalledWith(expect.objectContaining({ DispatchOnUtc: '2026-10-09T14:30:00.000Z' }));

await createCall(baseCall);
expect(mockPost.mock.calls[1][0]).not.toHaveProperty('DispatchOnUtc');
});

it('never sends the picker-only properties this app has no input for', async () => {
await createCall({ ...baseCall, dispatchOnUtc: '2026-10-09T14:30:00.000Z' });

for (const property of ['ProtocolIds', 'LinkedCallId', 'IndoorMapZoneId', 'IndoorMapFloorId']) {
expect(mockPost.mock.calls[0][0]).not.toHaveProperty(property);
}
});
});

describe('updateCall (EditCall)', () => {
it('sends the call identifiers, blank meaning "keep what is stored"', async () => {
await updateCall({ ...baseCall, callId: '42', externalId: 'CAD-9', incidentId: '', referenceId: 'REF-2' });

expect(mockPut).toHaveBeenCalledWith(expect.objectContaining({ Id: '42', ExternalId: 'CAD-9', IncidentId: '', ReferenceId: 'REF-2' }));
});

it('sends DispatchOnUtc only when a dispatch time is set, leaving the stored schedule alone otherwise', async () => {
await updateCall({ ...baseCall, callId: '42', dispatchOnUtc: '2026-10-09T14:30:00.000Z' });
expect(mockPut).toHaveBeenLastCalledWith(expect.objectContaining({ DispatchOnUtc: '2026-10-09T14:30:00.000Z' }));

await updateCall({ ...baseCall, callId: '42' });
expect(mockPut.mock.calls[1][0]).not.toHaveProperty('DispatchOnUtc');
});
});
});
30 changes: 30 additions & 0 deletions src/api/calls/calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ export interface CreateCallRequest {
type?: string;
contactName?: string;
contactInfo?: string;
/** External (CAD) call id. On update a blank value keeps the stored one. */
externalId?: string;
/** Incident id. On update a blank value keeps the stored one. */
incidentId?: string;
/** Reference id. On update a blank value keeps the stored one. */
referenceId?: string;
/**
* Scheduled dispatch time, ISO 8601 UTC. Left out of the request when not set: on create the call goes out
* now, on update the stored schedule is kept (the API has no way to clear it).
*/
dispatchOnUtc?: string;
/** Primary Contact (premises/customer record) to link; the contact must belong to the department. */
contactId?: string | null;
/** Additional Contacts to link. On update, supplying either list replaces the existing links; omitting both leaves them alone. */
Expand All @@ -69,6 +80,17 @@ export interface UpdateCallRequest {
type?: string;
contactName?: string;
contactInfo?: string;
/** External (CAD) call id. On update a blank value keeps the stored one. */
externalId?: string;
/** Incident id. On update a blank value keeps the stored one. */
incidentId?: string;
/** Reference id. On update a blank value keeps the stored one. */
referenceId?: string;
/**
* Scheduled dispatch time, ISO 8601 UTC. Left out of the request when not set: on create the call goes out
* now, on update the stored schedule is kept (the API has no way to clear it).
*/
dispatchOnUtc?: string;
/** Primary Contact (premises/customer record) to link; the contact must belong to the department. */
contactId?: string | null;
/** Additional Contacts to link. On update, supplying either list replaces the existing links; omitting both leaves them alone. */
Expand Down Expand Up @@ -128,6 +150,10 @@ export const createCall = async (callData: CreateCallRequest) => {
Type: callData.type || '',
ContactName: callData.contactName || '',
ContactInfo: callData.contactInfo || '',
ExternalId: callData.externalId || '',
IncidentId: callData.incidentId || '',
ReferenceId: callData.referenceId || '',
...(callData.dispatchOnUtc ? { DispatchOnUtc: callData.dispatchOnUtc } : {}),
...(callData.contactId !== undefined ? { ContactId: callData.contactId || '' } : {}),
...(callData.additionalContactIds !== undefined ? { AdditionalContactIds: callData.additionalContactIds } : {}),
What3Words: callData.what3words || '',
Expand Down Expand Up @@ -175,6 +201,10 @@ export const updateCall = async (callData: UpdateCallRequest) => {
Type: callData.type || '',
ContactName: callData.contactName || '',
ContactInfo: callData.contactInfo || '',
ExternalId: callData.externalId || '',
IncidentId: callData.incidentId || '',
ReferenceId: callData.referenceId || '',
...(callData.dispatchOnUtc ? { DispatchOnUtc: callData.dispatchOnUtc } : {}),
...(callData.contactId !== undefined ? { ContactId: callData.contactId || '' } : {}),
...(callData.additionalContactIds !== undefined ? { AdditionalContactIds: callData.additionalContactIds } : {}),
What3Words: callData.what3words || '',
Expand Down
2 changes: 1 addition & 1 deletion src/app/(app)/work-orders/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export default function WorkOrderScreen() {
const attach = async (source: 'camera' | 'library') => {
setMessage(null);
try {
const photo = await capturePhoto(source, `work-order-${order?.Number ?? 'photo'}.jpg`);
const photo = await capturePhoto(source, `work-order-${(order?.Number ?? 'photo').replace(/[^A-Za-z0-9._-]+/g, '-')}.jpg`);
if (!photo) return;
const ok = await useWorkOrdersStore.getState().attach({ uri: photo.uri, name: photo.name, type: photo.contentType });
await discardPhoto(photo);
Expand Down
Loading
Loading