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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,7 @@ Gemfile
expo-env.d.ts
# @end expo-cli
.dual-graph/
/.dual-graph-pro
.mcp.json
/.codex
/.claude
20 changes: 20 additions & 0 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,26 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
'assets/audio/unitstatusupdated.wav',
'assets/audio/upcomingshift.wav',
'assets/audio/upcomingtraining.wav',
'assets/audio/modernnotification.wav',
'assets/audio/moderncallclosed.wav',
'assets/audio/moderncallupdated.wav',
'assets/audio/moderncallemergency.wav',
'assets/audio/moderncallhigh.wav',
'assets/audio/moderncalllow.wav',
'assets/audio/moderncallmedium.wav',
'assets/audio/modernchat.wav',
'assets/audio/modernmessage.wav',
'assets/audio/modernshift.wav',
'assets/audio/moderntraining.wav',
'assets/audio/modernpersonnelstatus.wav',
'assets/audio/modernstaffing.wav',
'assets/audio/moderntroublealert.wav',
'assets/audio/modernunitnotice.wav',
'assets/audio/modernunitstatus.wav',
'assets/audio/modernavailabilityalert.wav',
'assets/audio/moderncalendar.wav',
'assets/audio/modernresourceorder.wav',
'assets/audio/modernweatheralert.wav',
'assets/audio/custom/c1.wav',
'assets/audio/custom/c2.wav',
'assets/audio/custom/c3.wav',
Expand Down
Binary file added assets/audio/modernavailabilityalert.wav
Binary file not shown.
Binary file added assets/audio/moderncalendar.wav
Binary file not shown.
Binary file added assets/audio/moderncallclosed.wav
Binary file not shown.
Binary file added assets/audio/moderncallemergency.wav
Binary file not shown.
Binary file added assets/audio/moderncallhigh.wav
Binary file not shown.
Binary file added assets/audio/moderncalllow.wav
Binary file not shown.
Binary file added assets/audio/moderncallmedium.wav
Binary file not shown.
Binary file added assets/audio/moderncallupdated.wav
Binary file not shown.
Binary file added assets/audio/modernchat.wav
Binary file not shown.
Binary file added assets/audio/modernmessage.wav
Binary file not shown.
Binary file added assets/audio/modernnotification.wav
Binary file not shown.
Binary file added assets/audio/modernpersonnelstatus.wav
Binary file not shown.
Binary file added assets/audio/modernresourceorder.wav
Binary file not shown.
Binary file added assets/audio/modernshift.wav
Binary file not shown.
Binary file added assets/audio/modernstaffing.wav
Binary file not shown.
Binary file added assets/audio/moderntraining.wav
Binary file not shown.
Binary file added assets/audio/moderntroublealert.wav
Binary file not shown.
Binary file added assets/audio/modernunitnotice.wav
Binary file not shown.
Binary file added assets/audio/modernunitstatus.wav
Binary file not shown.
Binary file added assets/audio/modernweatheralert.wav
Binary file not shown.
33 changes: 33 additions & 0 deletions src/api/calls/calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const getCallExtraDataApi = createApiEndpoint('/Calls/GetCallExtraData');
const createCallApi = createApiEndpoint('/Calls/SaveCall');
const updateCallApi = createApiEndpoint('/Calls/EditCall');
const closeCallApi = createApiEndpoint('/Calls/CloseCall');
const deleteCallApi = createApiEndpoint('/Calls/DeleteCall');
const updateScheduledDispatchTimeApi = createApiEndpoint('/Calls/UpdateScheduledDispatchTime');

export const getCalls = async () => {
// Add timestamp to prevent any caching
Expand Down Expand Up @@ -89,6 +91,10 @@ export interface UpdateCallRequest {
externalId?: string;
referenceId?: string;
destinationPoiId?: number | null;
/** When true, re-sends the dispatch to all currently dispatched entities. */
rebroadcastCall?: boolean;
/** When true, notifies entities that were removed from the dispatch list on this edit. */
notifyCancelledEntities?: boolean;
}

export interface CloseCallRequest {
Expand Down Expand Up @@ -189,6 +195,8 @@ export const updateCall = async (callData: UpdateCallRequest) => {
IncidentId: callData.linkedCallId || '',
ExternalId: callData.externalId || '',
ReferenceId: callData.referenceId || '',
RebroadcastCall: callData.rebroadcastCall ?? false,
NotifyCancelledEntities: callData.notifyCancelledEntities ?? false,
};

const response = await updateCallApi.put<SaveCallResult>(data);
Expand All @@ -205,3 +213,28 @@ export const closeCall = async (callData: CloseCallRequest) => {
const response = await closeCallApi.put<SaveCallResult>(data);
return response.data;
};

/** Soft-deletes a call. The server only allows deleting a call that has not yet been dispatched. */
export const deleteCall = async (callId: string) => {
const response = await deleteCallApi.delete<SaveCallResult>({
callId: encodeURIComponent(callId),
});
return response.data;
};

export interface UpdateScheduledDispatchTimeRequest {
callId: string;
/** Department-local dispatch date/time (ISO string). */
date: string;
}

/** Reschedules the dispatch time of a not-yet-dispatched scheduled call. */
export const updateScheduledDispatchTime = async (request: UpdateScheduledDispatchTimeRequest) => {
const data = {
Id: request.callId,
Date: request.date,
};

const response = await updateScheduledDispatchTimeApi.put<SaveCallResult>(data);
return response.data;
};
8 changes: 8 additions & 0 deletions src/api/checkIn/checkInTimers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { type CallPersonnelCheckInStatusResult } from '@/models/v4/checkIn/callPersonnelCheckInStatusResult';
import { type CheckInRecordResult } from '@/models/v4/checkIn/checkInRecordResult';
import { type CheckInTimerStatusResult } from '@/models/v4/checkIn/checkInTimerStatusResult';
import { type PerformCheckInResult } from '@/models/v4/checkIn/performCheckInResult';
Expand All @@ -10,6 +11,7 @@ const getTimersForCallApi = createApiEndpoint('/CheckInTimers/GetTimersForCall')
const performCheckInApi = createApiEndpoint('/CheckInTimers/PerformCheckIn');
const getCheckInHistoryApi = createApiEndpoint('/CheckInTimers/GetCheckInHistory');
const toggleCallTimersApi = createApiEndpoint('/CheckInTimers/ToggleCallTimers');
const getCallPersonnelCheckInStatusesApi = createApiEndpoint('/CheckInTimers/GetCallPersonnelCheckInStatuses');

export const getTimerStatuses = async (callId: number) => {
const response = await getTimerStatusesApi.get<CheckInTimerStatusResult>({ callId });
Expand Down Expand Up @@ -77,3 +79,9 @@ export const toggleCallTimers = async (callId: number, enabled: boolean) => {
const response = await toggleCallTimersApi.put<PerformCheckInResult>({ callId, enabled });
return response.data;
};

/** Per-personnel accountability (PAR) roster for a call. */
export const getCallPersonnelCheckInStatuses = async (callId: number) => {
const response = await getCallPersonnelCheckInStatusesApi.get<CallPersonnelCheckInStatusResult>({ callId });
return response.data;
};
31 changes: 31 additions & 0 deletions src/api/incidentCommand/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createApiEndpoint } from '@/api/common/client';
import { type CommandResult, type CommandsResult, type SaveCommandInput } from '@/models/v4/incidentCommand/commandDefinition';

const seg = (value: string | number) => encodeURIComponent(String(value));

/** All command definitions (templates) for the department. */
export const getAllCommands = async () => {
const response = await createApiEndpoint('/Commands/GetAllCommands').get<CommandsResult>();
return response.data;
};

export const getCommand = async (commandDefinitionId: number) => {
const response = await createApiEndpoint(`/Commands/GetCommand/${seg(commandDefinitionId)}`).get<CommandResult>();
return response.data;
};

/** Resolve the template for a call type (pass 0 for "Any Call Type"; the server falls back to it). */
export const getCommandForCallType = async (callTypeId: number) => {
const response = await createApiEndpoint(`/Commands/GetCommandForCallType/${seg(callTypeId)}`).get<CommandResult>();
return response.data;
};

export const saveCommand = async (input: SaveCommandInput) => {
const response = await createApiEndpoint('/Commands/SaveCommand').post<CommandResult>({ ...input });
return response.data;
};

export const deleteCommand = async (commandDefinitionId: number) => {
const response = await createApiEndpoint(`/Commands/DeleteCommand/${seg(commandDefinitionId)}`).delete<CommandResult>();
return response.data;
};
132 changes: 132 additions & 0 deletions src/api/incidentCommand/incidentCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { createApiEndpoint } from '@/api/common/client';
import { type CommandAccountabilityResult, type EvaluateAccountabilityResult } from '@/models/v4/incidentCommand/accountability';
import { type CommandLogEntry, type CommandTimelineResult } from '@/models/v4/incidentCommand/commandLog';
import { type CommandNodeResult, type CommandStructureNode } from '@/models/v4/incidentCommand/commandStructureNode';
import {
type CommandTransferResult,
type EstablishCommandInput,
type IncidentCommandActionResult,
type IncidentCommandResult,
type TransferCommandInput,
type UpdateActionPlanInput,
} from '@/models/v4/incidentCommand/incidentCommand';
import { type IncidentCommandBoardResult } from '@/models/v4/incidentCommand/incidentCommandBoard';
import { type IncidentMapAnnotation, type IncidentMapAnnotationResult } from '@/models/v4/incidentCommand/incidentMapAnnotation';
import { type IncidentTimer, type IncidentTimerResult } from '@/models/v4/incidentCommand/incidentTimer';
import { type MoveResourceInput, type ResourceAssignment, type ResourceAssignmentResult } from '@/models/v4/incidentCommand/resourceAssignment';
import { type TacticalObjective, type TacticalObjectiveResult } from '@/models/v4/incidentCommand/tacticalObjective';

const seg = (value: string | number) => encodeURIComponent(String(value));

// --- Command lifecycle -----------------------------------------------------

export const establishCommand = async (input: EstablishCommandInput) => {
const response = await createApiEndpoint('/IncidentCommand/EstablishCommand').post<IncidentCommandResult>({ ...input });
return response.data;
};

export const getCommandBoard = async (callId: string) => {
const response = await createApiEndpoint(`/IncidentCommand/GetCommandBoard/${seg(callId)}`).get<IncidentCommandBoardResult>();
return response.data;
};

export const transferCommand = async (input: TransferCommandInput) => {
const response = await createApiEndpoint('/IncidentCommand/TransferCommand').post<CommandTransferResult>({ ...input });
return response.data;
};

export const closeCommand = async (incidentCommandId: string) => {
const response = await createApiEndpoint(`/IncidentCommand/CloseCommand/${seg(incidentCommandId)}`).put<IncidentCommandResult>({});
return response.data;
};

export const updateActionPlan = async (input: UpdateActionPlanInput) => {
const response = await createApiEndpoint('/IncidentCommand/UpdateActionPlan').put<IncidentCommandResult>({ ...input });
return response.data;
};

// --- Accountability (PAR) --------------------------------------------------

export const getAccountability = async (callId: string) => {
const response = await createApiEndpoint(`/IncidentCommand/GetAccountability/${seg(callId)}`).get<CommandAccountabilityResult>();
return response.data;
};

export const evaluateAccountability = async (callId: string) => {
const response = await createApiEndpoint(`/IncidentCommand/EvaluateAccountability/${seg(callId)}`).post<EvaluateAccountabilityResult>({});
return response.data;
};

// --- Command structure (org chart lanes) -----------------------------------

export const saveNode = async (node: Partial<CommandStructureNode>) => {
const response = await createApiEndpoint('/IncidentCommand/SaveNode').post<CommandNodeResult>({ ...node });
return response.data;
};

export const deleteNode = async (commandStructureNodeId: string) => {
const response = await createApiEndpoint(`/IncidentCommand/DeleteNode/${seg(commandStructureNodeId)}`).delete<IncidentCommandActionResult>();
return response.data;
};

// --- Resource assignments ---------------------------------------------------

export const assignResource = async (assignment: Partial<ResourceAssignment>) => {
const response = await createApiEndpoint('/IncidentCommand/AssignResource').post<ResourceAssignmentResult>({ ...assignment });
return response.data;
};

export const moveResource = async (input: MoveResourceInput) => {
const response = await createApiEndpoint('/IncidentCommand/MoveResource').post<ResourceAssignmentResult>({ ...input });
return response.data;
};

export const releaseResource = async (resourceAssignmentId: string) => {
const response = await createApiEndpoint(`/IncidentCommand/ReleaseResource/${seg(resourceAssignmentId)}`).post<IncidentCommandActionResult>({});
return response.data;
};

// --- Tactical objectives ----------------------------------------------------

export const saveObjective = async (objective: Partial<TacticalObjective>) => {
const response = await createApiEndpoint('/IncidentCommand/SaveObjective').post<TacticalObjectiveResult>({ ...objective });
return response.data;
};

export const completeObjective = async (tacticalObjectiveId: string) => {
const response = await createApiEndpoint(`/IncidentCommand/CompleteObjective/${seg(tacticalObjectiveId)}`).post<TacticalObjectiveResult>({});
return response.data;
};

// --- Timers -----------------------------------------------------------------

export const startTimer = async (timer: Partial<IncidentTimer>) => {
const response = await createApiEndpoint('/IncidentCommand/StartTimer').post<IncidentTimerResult>({ ...timer });
return response.data;
};

export const acknowledgeTimer = async (incidentTimerId: string) => {
const response = await createApiEndpoint(`/IncidentCommand/AcknowledgeTimer/${seg(incidentTimerId)}`).post<IncidentTimerResult>({});
return response.data;
};

// --- Map annotations --------------------------------------------------------

export const saveAnnotation = async (annotation: Partial<IncidentMapAnnotation>) => {
const response = await createApiEndpoint('/IncidentCommand/SaveAnnotation').post<IncidentMapAnnotationResult>({ ...annotation });
return response.data;
};

export const deleteAnnotation = async (incidentMapAnnotationId: string) => {
const response = await createApiEndpoint(`/IncidentCommand/DeleteAnnotation/${seg(incidentMapAnnotationId)}`).delete<IncidentCommandActionResult>();
return response.data;
};

// --- Timeline ---------------------------------------------------------------

export const getTimeline = async (callId: string): Promise<CommandTimelineResult> => {
const response = await createApiEndpoint(`/IncidentCommand/GetTimeline/${seg(callId)}`).get<CommandTimelineResult>();
return response.data;
};

export type { CommandLogEntry };
14 changes: 14 additions & 0 deletions src/api/incidentCommand/incidentReporting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createApiEndpoint } from '@/api/common/client';
import { type IncidentAfterActionReportResult, type IncidentReportSummaryResult } from '@/models/v4/incidentCommand/incidentReport';

const seg = (value: string | number) => encodeURIComponent(String(value));

export const getIncidentSummary = async (callId: string) => {
const response = await createApiEndpoint(`/IncidentReporting/GetIncidentSummary/${seg(callId)}`).get<IncidentReportSummaryResult>();
return response.data;
};

export const getAfterActionReport = async (callId: string) => {
const response = await createApiEndpoint(`/IncidentReporting/GetAfterActionReport/${seg(callId)}`).get<IncidentAfterActionReportResult>();
return response.data;
};
54 changes: 54 additions & 0 deletions src/api/incidentCommand/incidentResources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { createApiEndpoint } from '@/api/common/client';
import {
type AdHocPersonnelListResult,
type AdHocPersonnelResult,
type AdHocUnitResult,
type AdHocUnitsResult,
type AssignPersonnelToUnitInput,
type FormUnitInput,
type IncidentAdHocPersonnel,
type IncidentAdHocUnit,
} from '@/models/v4/incidentCommand/incidentAdHocResources';
import { type IncidentCommandActionResult } from '@/models/v4/incidentCommand/incidentCommand';

const seg = (value: string | number) => encodeURIComponent(String(value));

export const createAdHocUnit = async (unit: Partial<IncidentAdHocUnit>) => {
const response = await createApiEndpoint('/IncidentResources/CreateAdHocUnit').post<AdHocUnitResult>({ ...unit });
return response.data;
};

export const getAdHocUnits = async (callId: string) => {
const response = await createApiEndpoint(`/IncidentResources/GetAdHocUnits/${seg(callId)}`).get<AdHocUnitsResult>();
return response.data;
};

export const releaseAdHocUnit = async (incidentAdHocUnitId: string) => {
const response = await createApiEndpoint(`/IncidentResources/ReleaseAdHocUnit/${seg(incidentAdHocUnitId)}`).post<IncidentCommandActionResult>({});
return response.data;
};

export const createAdHocPersonnel = async (person: Partial<IncidentAdHocPersonnel>) => {
const response = await createApiEndpoint('/IncidentResources/CreateAdHocPersonnel').post<AdHocPersonnelResult>({ ...person });
return response.data;
};

export const getAdHocPersonnel = async (callId: string) => {
const response = await createApiEndpoint(`/IncidentResources/GetAdHocPersonnel/${seg(callId)}`).get<AdHocPersonnelListResult>();
return response.data;
};

export const releaseAdHocPersonnel = async (incidentAdHocPersonnelId: string) => {
const response = await createApiEndpoint(`/IncidentResources/ReleaseAdHocPersonnel/${seg(incidentAdHocPersonnelId)}`).post<IncidentCommandActionResult>({});
return response.data;
};

export const assignPersonnelToUnit = async (input: AssignPersonnelToUnitInput) => {
const response = await createApiEndpoint('/IncidentResources/AssignPersonnelToUnit').post<AdHocPersonnelResult>({ ...input });
return response.data;
};

export const formUnit = async (input: FormUnitInput) => {
const response = await createApiEndpoint('/IncidentResources/FormUnit').post<AdHocUnitResult>({ ...input });
return response.data;
};
26 changes: 26 additions & 0 deletions src/api/incidentCommand/incidentRoles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createApiEndpoint } from '@/api/common/client';
import { type IncidentCommandActionResult } from '@/models/v4/incidentCommand/incidentCommand';
import { type IncidentCapabilitiesResult, type IncidentRoleAssignment, type IncidentRoleResult, type IncidentRolesResult } from '@/models/v4/incidentCommand/incidentRole';

const seg = (value: string | number) => encodeURIComponent(String(value));

export const assignRole = async (assignment: Partial<IncidentRoleAssignment>) => {
const response = await createApiEndpoint('/IncidentRoles/AssignRole').post<IncidentRoleResult>({ ...assignment });
return response.data;
};

export const removeRole = async (incidentRoleAssignmentId: string) => {
const response = await createApiEndpoint(`/IncidentRoles/RemoveRole/${seg(incidentRoleAssignmentId)}`).post<IncidentCommandActionResult>({});
return response.data;
};

export const getRoles = async (callId: string) => {
const response = await createApiEndpoint(`/IncidentRoles/GetRoles/${seg(callId)}`).get<IncidentRolesResult>();
return response.data;
};

/** Caller's effective capabilities for the incident (drives the app's view gating). */
export const getMyCapabilities = async (callId: string) => {
const response = await createApiEndpoint(`/IncidentRoles/GetMyCapabilities/${seg(callId)}`).get<IncidentCapabilitiesResult>();
return response.data;
};
20 changes: 20 additions & 0 deletions src/api/incidentCommand/incidentVoice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createApiEndpoint } from '@/api/common/client';
import { type IncidentCommandActionResult } from '@/models/v4/incidentCommand/incidentCommand';
import { type CreateIncidentChannelInput, type IncidentVoiceChannelResult, type IncidentVoiceChannelsResult } from '@/models/v4/incidentCommand/incidentVoiceChannel';

const seg = (value: string | number) => encodeURIComponent(String(value));

export const createIncidentChannel = async (input: CreateIncidentChannelInput) => {
const response = await createApiEndpoint('/IncidentVoice/CreateIncidentChannel').post<IncidentVoiceChannelResult>({ ...input });
return response.data;
};

export const getChannelsForCall = async (callId: string) => {
const response = await createApiEndpoint(`/IncidentVoice/GetChannelsForCall/${seg(callId)}`).get<IncidentVoiceChannelsResult>();
return response.data;
};

export const closeIncidentChannels = async (callId: string) => {
const response = await createApiEndpoint(`/IncidentVoice/CloseIncidentChannels/${seg(callId)}`).post<IncidentCommandActionResult>({});
return response.data;
};
Loading
Loading