diff --git a/src/components/incident-command/__tests__/incident-command-tab-panel.test.tsx b/src/components/incident-command/__tests__/incident-command-tab-panel.test.tsx index 111d9f3b..fe333337 100644 --- a/src/components/incident-command/__tests__/incident-command-tab-panel.test.tsx +++ b/src/components/incident-command/__tests__/incident-command-tab-panel.test.tsx @@ -26,6 +26,23 @@ jest.mock('lucide-react-native', () => ({ const { View } = require('react-native'); return ; }, + // Icons used by the incident chat section rendered inside the panel. + MessageCircle: () => { + const { View } = require('react-native'); + return ; + }, + MessagesSquare: () => { + const { View } = require('react-native'); + return ; + }, + ShieldCheck: () => { + const { View } = require('react-native'); + return ; + }, + Users: () => { + const { View } = require('react-native'); + return ; + }, })); jest.mock('@/lib/logging', () => ({ diff --git a/src/components/incident-command/incident-chat-section.tsx b/src/components/incident-command/incident-chat-section.tsx new file mode 100644 index 00000000..ee4e3ff5 --- /dev/null +++ b/src/components/incident-command/incident-chat-section.tsx @@ -0,0 +1,139 @@ +import { router } from 'expo-router'; +import { MessageCircle, MessagesSquare, Radio, ShieldCheck, Users } from 'lucide-react-native'; +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Box } from '@/components/ui/box'; +import { Heading } from '@/components/ui/heading'; +import { HStack } from '@/components/ui/hstack'; +import { Pressable } from '@/components/ui/pressable'; +import { Text } from '@/components/ui/text'; +import { VStack } from '@/components/ui/vstack'; +import { useDirectMessage } from '@/hooks/use-direct-message'; +import { type IncidentContactInfo, type ResourceIncidentView } from '@/models/v4/incidentCommand/resourceIncidentView'; + +import { getIncidentRoleName } from './incident-role-names'; + +interface IncidentChatSectionProps { + view: ResourceIncidentView; + testID?: string; +} + +/** One tappable channel row. Rendered only when the server handed us an id we are allowed to open. */ +const ChannelRow: React.FC<{ label: string; hint?: string | null; icon: React.ElementType; channelId?: string | null; testID: string }> = ({ label, hint, icon: IconComponent, channelId, testID }) => { + const onPress = useCallback(() => { + if (channelId) { + router.push(`/chat/${channelId}`); + } + }, [channelId]); + + if (!channelId) { + return null; + } + + return ( + + + + + {label} + {hint ? {hint} : null} + + + + ); +}; + +/** A person on the incident with a button to open a 1:1 with them. */ +const ContactRow: React.FC<{ label: string; contact?: IncidentContactInfo | null; onMessage: (userId?: string | null) => void; testID: string }> = ({ label, contact, onMessage, testID }) => { + const { t } = useTranslation(); + const handlePress = useCallback(() => onMessage(contact?.UserId), [onMessage, contact]); + + if (!contact) { + return null; + } + + return ( + + + {label} + {contact.Name} + + {/* External contacts have a name and phone but no Resgrid account, so no 1:1 to open. */} + {contact.UserId ? ( + + + + ) : null} + + ); +}; + +/** + * Incident chat on the Command tab: the channels this responder can open, and the people on the + * incident they can reach 1:1. + * + * Access is decided entirely by the server — a channel id only appears in the payload when the + * caller is actually allowed in, and it disappears again when they are (for example) taken off the + * lane. Nothing here infers access on its own. + */ +export const IncidentChatSection: React.FC = ({ view, testID = 'incident-command-chat' }) => { + const { t } = useTranslation(); + const { openDirectMessage } = useDirectMessage(); + + const chat = view.Chat; + const assignment = view.MyAssignment; + const roles = view.Roles ?? []; + + const handleMessage = useCallback((userId?: string | null) => void openDirectMessage(userId), [openDirectMessage]); + + const hasChannels = !!(chat?.IncidentChannelId || chat?.LaneChannelId || chat?.CommandChannelId || chat?.LeadsChannelId || chat?.DispatchChannelId); + // Only ICS role holders live here — the commander and this resource's lane leads already have + // contact cards higher up the panel, and listing them twice just made the screen noisy. + const hasContacts = roles.length > 0; + + if (!hasChannels && !hasContacts) { + return null; + } + + return ( + + + {t('incident_command.chat')} + + + {chat?.IsFrozen ? ( + + {t('incident_command.chat_frozen')} + + ) : null} + + {hasChannels ? ( + + + + + + + + ) : null} + + {hasContacts ? ( + + {t('incident_command.ics_positions')} + {roles.map((role) => ( + + ))} + + ) : null} + + ); +}; + +export default IncidentChatSection; diff --git a/src/components/incident-command/incident-command-tab-panel.tsx b/src/components/incident-command/incident-command-tab-panel.tsx index c1556110..cc48ed54 100644 --- a/src/components/incident-command/incident-command-tab-panel.tsx +++ b/src/components/incident-command/incident-command-tab-panel.tsx @@ -1,5 +1,5 @@ import { format } from 'date-fns'; -import { MailIcon, PhoneIcon } from 'lucide-react-native'; +import { MailIcon, MessageCircle, PhoneIcon } from 'lucide-react-native'; import { useColorScheme } from 'nativewind'; import React, { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; @@ -13,10 +13,13 @@ import { Pressable } from '@/components/ui/pressable'; import { Spinner } from '@/components/ui/spinner'; import { Text } from '@/components/ui/text'; import { VStack } from '@/components/ui/vstack'; +import { useDirectMessage } from '@/hooks/use-direct-message'; import { logger } from '@/lib/logging'; import { type IncidentContactInfo, IncidentNeedStatus, type TacticalObjective, TacticalObjectiveStatus } from '@/models/v4/incidentCommand/resourceIncidentView'; import { useIncidentCommandStore } from '@/stores/calls/incident-command-store'; +import { IncidentChatSection } from './incident-chat-section'; + interface IncidentCommandTabPanelProps { callId: string; } @@ -102,6 +105,9 @@ interface ContactRowProps { } const ContactRow: React.FC = ({ label, contact, testID }) => { + const { t } = useTranslation(); + const { openDirectMessage } = useDirectMessage(); + return ( {label} @@ -122,6 +128,15 @@ const ContactRow: React.FC = ({ label, contact, testID }) => { ) : null} + {/* External contacts carry a name and phone but no Resgrid account, so there is nobody to message. */} + {contact.UserId ? ( + void openDirectMessage(contact.UserId)} testID={`${testID}-message`}> + + + {t('incident_command.send_message')} + + + ) : null} ); }; @@ -264,6 +279,9 @@ export const IncidentCommandTabPanel: React.FC = ( + {/* Incident chat: the channels this responder may open, plus who they can reach 1:1. */} + + {/* Objectives */} {t('incident_command.objectives')} diff --git a/src/components/incident-command/incident-role-names.ts b/src/components/incident-command/incident-role-names.ts new file mode 100644 index 00000000..d3d547d8 --- /dev/null +++ b/src/components/incident-command/incident-role-names.ts @@ -0,0 +1,45 @@ +import { type TFunction } from 'i18next'; + +/** + * Standard NIMS/ICS position titles, keyed by the Core `IncidentRoleType` value. + * + * English titles rather than translation keys: these are the position names an IC is trained + * against, and the IC app treats them the same way. Translating them needs a subject-matter + * translator per locale, not a literal one. + */ +const ICS_ROLE_NAMES: Record = { + 0: 'Incident Commander', + 1: 'Deputy Incident Commander', + 2: 'Unified Command Member', + 3: 'Operations Section Chief', + 4: 'Planning Section Chief', + 5: 'Logistics Section Chief', + 6: 'Finance/Admin Section Chief', + 7: 'Safety Officer', + 8: 'Liaison Officer', + 9: 'Public Information Officer', + 10: 'Staging Area Manager', + 11: 'Resources Unit Leader', + 12: 'Situation Unit Leader', + 13: 'Documentation Unit Leader', + 14: 'Communications Unit Leader', + 15: 'Division/Group Supervisor', + 16: 'Branch Director', + 17: 'Strike Team/Task Force Leader', + 18: 'Medical Unit Leader', + 19: 'Rehab Officer', + 20: 'Medical Branch Director', + 21: 'Triage Officer', + 22: 'Treatment Officer', + 23: 'Transport Officer', + 24: 'HazMat Group Supervisor', + 25: 'Decon Officer', + 26: 'Entry Team Leader', + 27: 'Search Group Supervisor', + 28: 'Air Operations Branch Director', + 29: 'Shelter/Mass Care Coordinator', + 30: 'Damage Assessment Lead', +}; + +/** Display name for an ICS position; falls back to a generic label for a value we don't know yet. */ +export const getIncidentRoleName = (t: TFunction, roleType: number): string => ICS_ROLE_NAMES[roleType] ?? t('incident_command.role_generic', { role: roleType }); diff --git a/src/hooks/use-direct-message.ts b/src/hooks/use-direct-message.ts new file mode 100644 index 00000000..1fe2a46d --- /dev/null +++ b/src/hooks/use-direct-message.ts @@ -0,0 +1,48 @@ +import { router } from 'expo-router'; +import { useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { createDirectMessage } from '@/api/chat/chat'; +import { logger } from '@/lib/logging'; +import { useToastStore } from '@/stores/toast/store'; + +/** + * Opens a 1:1 conversation with someone and navigates to it. + * + * The server dedups on a normalized participant key, so calling this repeatedly for the same person + * reuses the existing conversation rather than starting a new one — which is what makes it safe to + * hang a "message" button off every contact on an incident. + */ +export const useDirectMessage = () => { + const { t } = useTranslation(); + const [isOpening, setIsOpening] = useState(false); + + const openDirectMessage = useCallback( + async (targetUserId?: string | null) => { + if (!targetUserId) { + // External lane leads have a name and phone but no Resgrid account to message. + useToastStore.getState().showToast('info', t('incident_command.dm_unavailable')); + return; + } + + setIsOpening(true); + try { + const channel = await createDirectMessage({ TargetUserId: targetUserId }); + const channelId = channel?.Data?.ChatChannelId; + if (!channelId) { + useToastStore.getState().showToast('error', t('incident_command.dm_failed')); + return; + } + router.push(`/chat/${channelId}`); + } catch (error) { + logger.error({ message: 'chat: failed to open direct message', context: { error, targetUserId } }); + useToastStore.getState().showToast('error', t('incident_command.dm_failed')); + } finally { + setIsOpening(false); + } + }, + [t] + ); + + return { openDirectMessage, isOpening }; +}; diff --git a/src/models/v4/chat/chatEnums.ts b/src/models/v4/chat/chatEnums.ts index 006fd1c9..11916f0c 100644 --- a/src/models/v4/chat/chatEnums.ts +++ b/src/models/v4/chat/chatEnums.ts @@ -13,6 +13,10 @@ export enum ChatChannelType { IncidentLane = 6, IncidentCommand = 7, Chatbot = 8, + /** IC plus every lane's primary/secondary lead — command talking to the people running the lanes. */ + IncidentLeads = 9, + /** The incident's line to the dispatch desk: everyone on the incident, plus every authorized dispatcher. */ + IncidentDispatch = 10, } /** Message type (ChatMessageResultData.MessageType). */ diff --git a/src/models/v4/incidentCommand/resourceIncidentView.ts b/src/models/v4/incidentCommand/resourceIncidentView.ts index 5ea46fe4..c650472a 100644 --- a/src/models/v4/incidentCommand/resourceIncidentView.ts +++ b/src/models/v4/incidentCommand/resourceIncidentView.ts @@ -121,6 +121,33 @@ export class ResourceLaneAssignmentView { public LinkedNeed?: IncidentNeed | null = null; } +/** Who holds an ICS position on the incident, with the contact details to reach them. */ +export class IncidentRoleContactInfo { + /** Maps to IncidentRoleType. */ + public RoleType: number = 0; + public Contact?: IncidentContactInfo | null = null; +} + +/** + * The incident's chat channels, already filtered by the server to the ones this caller may open. + * A null id means "not available to you" — not command staff, not a lane lead, or not provisioned. + * Never infer access from anything else: if the id isn't here, the channel will reject you. + */ +export class IncidentChatChannels { + /** Call-wide incident channel (everyone on the call). */ + public IncidentChannelId?: string | null = null; + /** Private command channel — command staff (IC or an ICS role holder) only. */ + public CommandChannelId?: string | null = null; + /** "All Leads" channel — the IC and lane primary/secondary leads only. */ + public LeadsChannelId?: string | null = null; + /** The caller's own lane channel, when they are assigned to a lane. */ + public LaneChannelId?: string | null = null; + /** The incident's line to the dispatch desk — open to everyone on the incident. */ + public DispatchChannelId?: string | null = null; + /** True once the incident is closed: readable, but frozen as a point-in-time record. */ + public IsFrozen: boolean = false; +} + export class ResourceIncidentView { public IncidentCommandId: string = ''; public CallId: number = 0; @@ -136,4 +163,8 @@ export class ResourceIncidentView { public Notes: IncidentNote[] = []; public Attachments: IncidentAttachment[] = []; public MyAssignment?: ResourceLaneAssignmentView | null = null; + /** ICS positions filled on this incident, so a crew can reach the right person directly. */ + public Roles?: IncidentRoleContactInfo[] = []; + /** Chat channels this caller may open. */ + public Chat?: IncidentChatChannels | null = null; } diff --git a/src/translations/ar.json b/src/translations/ar.json index ee4901f4..f7ed5dd6 100644 --- a/src/translations/ar.json +++ b/src/translations/ar.json @@ -565,13 +565,27 @@ "action_plan": "خطة عمل الحادث", "assigned_since": "تم التعيين {{time}}", "attachments": "المرفقات", + "chat": "Chat", + "chat_frozen": "This incident is closed. Conversations are kept as a point-in-time record — no new messages.", + "command_channel": "Command chat", + "command_channel_hint": "Command staff only", "commander": "القائد", + "dispatch_channel": "Dispatch", + "dispatch_channel_hint": "Reach the dispatch desk", + "dm_failed": "Couldn't open that conversation.", + "dm_unavailable": "That contact has no Resgrid account to message.", "error": "فشل في تحميل معلومات قيادة الحادث", "established": "تم التأسيس", "estimated_end": "النهاية المتوقعة", + "ics_positions": "ICS positions", "important_information": "معلومات مهمة", + "incident_channel": "Incident chat", "incident_info": "معلومات الحادث", + "lane_channel": "Lane chat", + "leads_channel": "All Leads", + "leads_channel_hint": "IC and lane leads", "linked_need": "احتياج مرتبط", + "message_person": "Message {{name}}", "my_assignment": "تعيين الوحدة", "need_category": { "equipment": "معدات", @@ -609,8 +623,11 @@ "primary_objective": "الهدف الرئيسي", "progress": "اكتمل {{percent}}%", "quantity_fulfilled": "تم تلبية {{fulfilled}} من {{requested}}", + "reach_directly": "Reach directly", + "role_generic": "ICS role {{role}}", "secondary_lead": "المسؤول الثانوي", "secondary_objective": "الهدف الثانوي", + "send_message": "Message", "tab_title": "القيادة" }, "livekit": { diff --git a/src/translations/de.json b/src/translations/de.json index c2b0f0e8..6679c7a1 100644 --- a/src/translations/de.json +++ b/src/translations/de.json @@ -565,13 +565,27 @@ "action_plan": "Einsatzplan", "assigned_since": "Zugewiesen {{time}}", "attachments": "Anhänge", + "chat": "Chat", + "chat_frozen": "This incident is closed. Conversations are kept as a point-in-time record — no new messages.", + "command_channel": "Command chat", + "command_channel_hint": "Command staff only", "commander": "Einsatzleiter", + "dispatch_channel": "Dispatch", + "dispatch_channel_hint": "Reach the dispatch desk", + "dm_failed": "Couldn't open that conversation.", + "dm_unavailable": "That contact has no Resgrid account to message.", "error": "Informationen zur Einsatzleitung konnten nicht geladen werden", "established": "Eingerichtet", "estimated_end": "Voraussichtliches Ende", + "ics_positions": "ICS positions", "important_information": "Wichtige Informationen", + "incident_channel": "Incident chat", "incident_info": "Vorfallinformationen", + "lane_channel": "Lane chat", + "leads_channel": "All Leads", + "leads_channel_hint": "IC and lane leads", "linked_need": "Verknüpfter Bedarf", + "message_person": "Message {{name}}", "my_assignment": "Einheitszuweisung", "need_category": { "equipment": "Ausrüstung", @@ -609,8 +623,11 @@ "primary_objective": "Primäres Ziel", "progress": "{{percent}} % abgeschlossen", "quantity_fulfilled": "{{fulfilled}} von {{requested}} erfüllt", + "reach_directly": "Reach directly", + "role_generic": "ICS role {{role}}", "secondary_lead": "Sekundäre Leitung", "secondary_objective": "Sekundäres Ziel", + "send_message": "Message", "tab_title": "Führung" }, "livekit": { diff --git a/src/translations/en.json b/src/translations/en.json index ae9c5dd3..81bddae1 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -565,13 +565,27 @@ "action_plan": "Incident Action Plan", "assigned_since": "Assigned {{time}}", "attachments": "Attachments", + "chat": "Chat", + "chat_frozen": "This incident is closed. Conversations are kept as a point-in-time record — no new messages.", + "command_channel": "Command chat", + "command_channel_hint": "Command staff only", "commander": "Commander", + "dispatch_channel": "Dispatch", + "dispatch_channel_hint": "Reach the dispatch desk", + "dm_failed": "Couldn't open that conversation.", + "dm_unavailable": "That contact has no Resgrid account to message.", "error": "Failed to load incident command information", "established": "Established", "estimated_end": "Estimated End", + "ics_positions": "ICS positions", "important_information": "Important Information", + "incident_channel": "Incident chat", "incident_info": "Incident Information", + "lane_channel": "Lane chat", + "leads_channel": "All Leads", + "leads_channel_hint": "IC and lane leads", "linked_need": "Linked Need", + "message_person": "Message {{name}}", "my_assignment": "Unit Assignment", "need_category": { "equipment": "Equipment", @@ -609,8 +623,11 @@ "primary_objective": "Primary Objective", "progress": "{{percent}}% complete", "quantity_fulfilled": "{{fulfilled}} of {{requested}} fulfilled", + "reach_directly": "Reach directly", + "role_generic": "ICS role {{role}}", "secondary_lead": "Secondary Lead", "secondary_objective": "Secondary Objective", + "send_message": "Message", "tab_title": "Command" }, "livekit": { diff --git a/src/translations/es.json b/src/translations/es.json index 1463f127..db3eb4fb 100644 --- a/src/translations/es.json +++ b/src/translations/es.json @@ -565,13 +565,27 @@ "action_plan": "Plan de acción del incidente", "assigned_since": "Asignado {{time}}", "attachments": "Adjuntos", + "chat": "Chat", + "chat_frozen": "This incident is closed. Conversations are kept as a point-in-time record — no new messages.", + "command_channel": "Command chat", + "command_channel_hint": "Command staff only", "commander": "Comandante", + "dispatch_channel": "Dispatch", + "dispatch_channel_hint": "Reach the dispatch desk", + "dm_failed": "Couldn't open that conversation.", + "dm_unavailable": "That contact has no Resgrid account to message.", "error": "Error al cargar la información del comando del incidente", "established": "Establecido", "estimated_end": "Fin estimado", + "ics_positions": "ICS positions", "important_information": "Información importante", + "incident_channel": "Incident chat", "incident_info": "Información del incidente", + "lane_channel": "Lane chat", + "leads_channel": "All Leads", + "leads_channel_hint": "IC and lane leads", "linked_need": "Necesidad vinculada", + "message_person": "Message {{name}}", "my_assignment": "Asignación de la unidad", "need_category": { "equipment": "Equipamiento", @@ -609,8 +623,11 @@ "primary_objective": "Objetivo principal", "progress": "{{percent}}% completado", "quantity_fulfilled": "{{fulfilled}} de {{requested}} completados", + "reach_directly": "Reach directly", + "role_generic": "ICS role {{role}}", "secondary_lead": "Líder secundario", "secondary_objective": "Objetivo secundario", + "send_message": "Message", "tab_title": "Comando" }, "livekit": { diff --git a/src/translations/fr.json b/src/translations/fr.json index 55d36ef3..3ffda709 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -565,13 +565,27 @@ "action_plan": "Plan d'action de l'incident", "assigned_since": "Affecté {{time}}", "attachments": "Pièces jointes", + "chat": "Chat", + "chat_frozen": "This incident is closed. Conversations are kept as a point-in-time record — no new messages.", + "command_channel": "Command chat", + "command_channel_hint": "Command staff only", "commander": "Commandant", + "dispatch_channel": "Dispatch", + "dispatch_channel_hint": "Reach the dispatch desk", + "dm_failed": "Couldn't open that conversation.", + "dm_unavailable": "That contact has no Resgrid account to message.", "error": "Échec du chargement des informations du commandement de l'incident", "established": "Établi", "estimated_end": "Fin estimée", + "ics_positions": "ICS positions", "important_information": "Informations importantes", + "incident_channel": "Incident chat", "incident_info": "Informations sur l'incident", + "lane_channel": "Lane chat", + "leads_channel": "All Leads", + "leads_channel_hint": "IC and lane leads", "linked_need": "Besoin lié", + "message_person": "Message {{name}}", "my_assignment": "Affectation de l'unité", "need_category": { "equipment": "Équipement", @@ -609,8 +623,11 @@ "primary_objective": "Objectif principal", "progress": "{{percent}} % terminé", "quantity_fulfilled": "{{fulfilled}} sur {{requested}} satisfaits", + "reach_directly": "Reach directly", + "role_generic": "ICS role {{role}}", "secondary_lead": "Responsable secondaire", "secondary_objective": "Objectif secondaire", + "send_message": "Message", "tab_title": "Commandement" }, "livekit": { diff --git a/src/translations/it.json b/src/translations/it.json index 8f94ceae..498c2ed8 100644 --- a/src/translations/it.json +++ b/src/translations/it.json @@ -565,13 +565,27 @@ "action_plan": "Piano d'azione dell'incidente", "assigned_since": "Assegnata {{time}}", "attachments": "Allegati", + "chat": "Chat", + "chat_frozen": "This incident is closed. Conversations are kept as a point-in-time record — no new messages.", + "command_channel": "Command chat", + "command_channel_hint": "Command staff only", "commander": "Comandante", + "dispatch_channel": "Dispatch", + "dispatch_channel_hint": "Reach the dispatch desk", + "dm_failed": "Couldn't open that conversation.", + "dm_unavailable": "That contact has no Resgrid account to message.", "error": "Impossibile caricare le informazioni del comando incidente", "established": "Istituito", "estimated_end": "Fine stimata", + "ics_positions": "ICS positions", "important_information": "Informazioni importanti", + "incident_channel": "Incident chat", "incident_info": "Informazioni sull'incidente", + "lane_channel": "Lane chat", + "leads_channel": "All Leads", + "leads_channel_hint": "IC and lane leads", "linked_need": "Necessità collegata", + "message_person": "Message {{name}}", "my_assignment": "Assegnazione unità", "need_category": { "equipment": "Attrezzatura", @@ -609,8 +623,11 @@ "primary_objective": "Obiettivo principale", "progress": "{{percent}}% completato", "quantity_fulfilled": "{{fulfilled}} di {{requested}} soddisfatti", + "reach_directly": "Reach directly", + "role_generic": "ICS role {{role}}", "secondary_lead": "Responsabile secondario", "secondary_objective": "Obiettivo secondario", + "send_message": "Message", "tab_title": "Comando" }, "livekit": { diff --git a/src/translations/pl.json b/src/translations/pl.json index 015b79bd..300ebdad 100644 --- a/src/translations/pl.json +++ b/src/translations/pl.json @@ -565,13 +565,27 @@ "action_plan": "Plan działań incydentu", "assigned_since": "Przydzielono {{time}}", "attachments": "Załączniki", + "chat": "Chat", + "chat_frozen": "This incident is closed. Conversations are kept as a point-in-time record — no new messages.", + "command_channel": "Command chat", + "command_channel_hint": "Command staff only", "commander": "Dowódca", + "dispatch_channel": "Dispatch", + "dispatch_channel_hint": "Reach the dispatch desk", + "dm_failed": "Couldn't open that conversation.", + "dm_unavailable": "That contact has no Resgrid account to message.", "error": "Nie udało się załadować informacji o dowodzeniu incydentem", "established": "Ustanowiono", "estimated_end": "Szacowane zakończenie", + "ics_positions": "ICS positions", "important_information": "Ważne informacje", + "incident_channel": "Incident chat", "incident_info": "Informacje o incydencie", + "lane_channel": "Lane chat", + "leads_channel": "All Leads", + "leads_channel_hint": "IC and lane leads", "linked_need": "Powiązana potrzeba", + "message_person": "Message {{name}}", "my_assignment": "Przydział jednostki", "need_category": { "equipment": "Sprzęt", @@ -609,8 +623,11 @@ "primary_objective": "Cel główny", "progress": "Ukończono {{percent}}%", "quantity_fulfilled": "Zaspokojono {{fulfilled}} z {{requested}}", + "reach_directly": "Reach directly", + "role_generic": "ICS role {{role}}", "secondary_lead": "Pomocniczy kierujący", "secondary_objective": "Cel drugorzędny", + "send_message": "Message", "tab_title": "Dowodzenie" }, "livekit": { diff --git a/src/translations/sv.json b/src/translations/sv.json index 726d2f37..2925e4f2 100644 --- a/src/translations/sv.json +++ b/src/translations/sv.json @@ -565,13 +565,27 @@ "action_plan": "Insatsplan", "assigned_since": "Tilldelad {{time}}", "attachments": "Bilagor", + "chat": "Chat", + "chat_frozen": "This incident is closed. Conversations are kept as a point-in-time record — no new messages.", + "command_channel": "Command chat", + "command_channel_hint": "Command staff only", "commander": "Insatsledare", + "dispatch_channel": "Dispatch", + "dispatch_channel_hint": "Reach the dispatch desk", + "dm_failed": "Couldn't open that conversation.", + "dm_unavailable": "That contact has no Resgrid account to message.", "error": "Det gick inte att ladda information om insatsledningen", "established": "Upprättad", "estimated_end": "Beräknat slut", + "ics_positions": "ICS positions", "important_information": "Viktig information", + "incident_channel": "Incident chat", "incident_info": "Incidentinformation", + "lane_channel": "Lane chat", + "leads_channel": "All Leads", + "leads_channel_hint": "IC and lane leads", "linked_need": "Kopplat behov", + "message_person": "Message {{name}}", "my_assignment": "Enhetstilldelning", "need_category": { "equipment": "Utrustning", @@ -609,8 +623,11 @@ "primary_objective": "Primärt mål", "progress": "{{percent}} % slutfört", "quantity_fulfilled": "{{fulfilled}} av {{requested}} uppfyllda", + "reach_directly": "Reach directly", + "role_generic": "ICS role {{role}}", "secondary_lead": "Biträdande ansvarig", "secondary_objective": "Sekundärt mål", + "send_message": "Message", "tab_title": "Ledning" }, "livekit": { diff --git a/src/translations/uk.json b/src/translations/uk.json index f8548ca5..76a46afb 100644 --- a/src/translations/uk.json +++ b/src/translations/uk.json @@ -565,13 +565,27 @@ "action_plan": "План дій щодо інциденту", "assigned_since": "Призначено {{time}}", "attachments": "Вкладення", + "chat": "Chat", + "chat_frozen": "This incident is closed. Conversations are kept as a point-in-time record — no new messages.", + "command_channel": "Command chat", + "command_channel_hint": "Command staff only", "commander": "Керівник", + "dispatch_channel": "Dispatch", + "dispatch_channel_hint": "Reach the dispatch desk", + "dm_failed": "Couldn't open that conversation.", + "dm_unavailable": "That contact has no Resgrid account to message.", "error": "Не вдалося завантажити інформацію про керування інцидентом", "established": "Встановлено", "estimated_end": "Орієнтовне завершення", + "ics_positions": "ICS positions", "important_information": "Важлива інформація", + "incident_channel": "Incident chat", "incident_info": "Інформація про інцидент", + "lane_channel": "Lane chat", + "leads_channel": "All Leads", + "leads_channel_hint": "IC and lane leads", "linked_need": "Пов'язана потреба", + "message_person": "Message {{name}}", "my_assignment": "Призначення підрозділу", "need_category": { "equipment": "Обладнання", @@ -609,8 +623,11 @@ "primary_objective": "Основна ціль", "progress": "Виконано {{percent}}%", "quantity_fulfilled": "Виконано {{fulfilled}} з {{requested}}", + "reach_directly": "Reach directly", + "role_generic": "ICS role {{role}}", "secondary_lead": "Додатковий відповідальний", "secondary_objective": "Другорядна ціль", + "send_message": "Message", "tab_title": "Керування" }, "livekit": {