feat: Implement On-Call policies feature with navigation, API integration, and UI components

This commit is contained in:
Nawaz Dhandala
2026-02-14 21:51:09 +00:00
parent f383bbba4d
commit 66b995c64a
9 changed files with 834 additions and 1 deletions

View File

@@ -0,0 +1,22 @@
import type { AxiosResponse } from "axios";
import apiClient from "./client";
import type { CurrentOnDutyEscalationPoliciesResponse } from "./types";
export async function fetchCurrentOnDutyEscalationPolicies(
projectId: string,
): Promise<CurrentOnDutyEscalationPoliciesResponse> {
const response: AxiosResponse = await apiClient.get(
"/api/on-call-duty-policy/current-on-duty-escalation-policies",
{
headers: {
tenantid: projectId,
},
},
);
return {
escalationRulesByUser: response.data?.escalationRulesByUser ?? [],
escalationRulesByTeam: response.data?.escalationRulesByTeam ?? [],
escalationRulesBySchedule: response.data?.escalationRulesBySchedule ?? [],
};
}

View File

@@ -12,7 +12,11 @@ import type IncidentSeverityModel from "Common/Models/DatabaseModels/IncidentSev
import type IncidentStateModelClass from "Common/Models/DatabaseModels/IncidentState.js";
import type IncidentStateTimelineModel from "Common/Models/DatabaseModels/IncidentStateTimeline.js";
import type MonitorModel from "Common/Models/DatabaseModels/Monitor.js";
import type OnCallDutyPolicyModel from "Common/Models/DatabaseModels/OnCallDutyPolicy.js";
import type OnCallDutyPolicyEscalationRuleModel from "Common/Models/DatabaseModels/OnCallDutyPolicyEscalationRule.js";
import type OnCallDutyPolicyScheduleModel from "Common/Models/DatabaseModels/OnCallDutyPolicySchedule.js";
import type ProjectModel from "Common/Models/DatabaseModels/Project.js";
import type TeamModel from "Common/Models/DatabaseModels/Team.js";
import type UserModel from "Common/Models/DatabaseModels/User.js";
type Alert = InstanceType<typeof AlertModel>;
@@ -31,7 +35,13 @@ type IncidentSeverity = InstanceType<typeof IncidentSeverityModel>;
type IncidentStateModel = InstanceType<typeof IncidentStateModelClass>;
type IncidentStateTimeline = InstanceType<typeof IncidentStateTimelineModel>;
type Monitor = InstanceType<typeof MonitorModel>;
type OnCallDutyPolicy = InstanceType<typeof OnCallDutyPolicyModel>;
type OnCallDutyPolicyEscalationRule = InstanceType<
typeof OnCallDutyPolicyEscalationRuleModel
>;
type OnCallDutyPolicySchedule = InstanceType<typeof OnCallDutyPolicyScheduleModel>;
type Project = InstanceType<typeof ProjectModel>;
type Team = InstanceType<typeof TeamModel>;
type User = InstanceType<typeof UserModel>;
type RequiredModelFields<T, K extends keyof T> = {
@@ -232,3 +242,67 @@ export type ProjectIncidentItem = WithProject<IncidentItem>;
export type ProjectAlertItem = WithProject<AlertItem>;
export type ProjectIncidentEpisodeItem = WithProject<IncidentEpisodeItem>;
export type ProjectAlertEpisodeItem = WithProject<AlertEpisodeItem>;
interface OnCallPolicyRef
extends RequiredModelFields<OnCallDutyPolicy, "name"> {
_id?: string;
id?: string;
}
interface OnCallEscalationRuleRef
extends RequiredModelFields<OnCallDutyPolicyEscalationRule, "name"> {
_id?: string;
id?: string;
}
interface TeamRef extends RequiredModelFields<Team, "name"> {
_id?: string;
id?: string;
}
interface OnCallScheduleRef
extends RequiredModelFields<OnCallDutyPolicySchedule, "name"> {
_id?: string;
id?: string;
}
export interface OnCallDutyEscalationRuleUserItem {
onCallDutyPolicy?: OnCallPolicyRef;
onCallDutyPolicyEscalationRule?: OnCallEscalationRuleRef;
}
export interface OnCallDutyEscalationRuleTeamItem {
onCallDutyPolicy?: OnCallPolicyRef;
onCallDutyPolicyEscalationRule?: OnCallEscalationRuleRef;
team?: TeamRef;
}
export interface OnCallDutyEscalationRuleScheduleItem {
onCallDutyPolicy?: OnCallPolicyRef;
onCallDutyPolicyEscalationRule?: OnCallEscalationRuleRef;
onCallDutyPolicySchedule?: OnCallScheduleRef;
}
export interface CurrentOnDutyEscalationPoliciesResponse {
escalationRulesByUser: OnCallDutyEscalationRuleUserItem[];
escalationRulesByTeam: OnCallDutyEscalationRuleTeamItem[];
escalationRulesBySchedule: OnCallDutyEscalationRuleScheduleItem[];
}
export type OnCallAssignmentType = "user" | "team" | "schedule";
export interface OnCallAssignmentItem {
projectId: string;
projectName: string;
policyId?: string;
policyName: string;
escalationRuleName: string;
assignmentType: OnCallAssignmentType;
assignmentDetail: string;
}
export interface ProjectOnCallAssignments {
projectId: string;
projectName: string;
assignments: OnCallAssignmentItem[];
}

View File

@@ -0,0 +1,142 @@
import { useMemo } from "react";
import { useQuery, UseQueryResult } from "@tanstack/react-query";
import { useProject } from "./useProject";
import { fetchCurrentOnDutyEscalationPolicies } from "../api/onCallPolicies";
import type {
CurrentOnDutyEscalationPoliciesResponse,
OnCallAssignmentItem,
ProjectItem,
ProjectOnCallAssignments,
} from "../api/types";
interface UseAllProjectOnCallPoliciesResult {
projects: ProjectOnCallAssignments[];
totalAssignments: number;
isLoading: boolean;
isError: boolean;
refetch: () => Promise<void>;
}
function getEntityId(entity?: { _id?: string; id?: string }): string | undefined {
return entity?._id ?? entity?.id;
}
function toAssignments(
project: ProjectItem,
response: CurrentOnDutyEscalationPoliciesResponse,
): OnCallAssignmentItem[] {
const assignments: OnCallAssignmentItem[] = [];
response.escalationRulesByUser.forEach((rule) => {
assignments.push({
projectId: project._id,
projectName: project.name,
policyId: getEntityId(rule.onCallDutyPolicy),
policyName: rule.onCallDutyPolicy?.name ?? "Unknown policy",
escalationRuleName: rule.onCallDutyPolicyEscalationRule?.name ?? "Unknown rule",
assignmentType: "user",
assignmentDetail: "You are directly assigned",
});
});
response.escalationRulesByTeam.forEach((rule) => {
assignments.push({
projectId: project._id,
projectName: project.name,
policyId: getEntityId(rule.onCallDutyPolicy),
policyName: rule.onCallDutyPolicy?.name ?? "Unknown policy",
escalationRuleName: rule.onCallDutyPolicyEscalationRule?.name ?? "Unknown rule",
assignmentType: "team",
assignmentDetail: `Via team: ${rule.team?.name ?? "Unknown"}`,
});
});
response.escalationRulesBySchedule.forEach((rule) => {
assignments.push({
projectId: project._id,
projectName: project.name,
policyId: getEntityId(rule.onCallDutyPolicy),
policyName: rule.onCallDutyPolicy?.name ?? "Unknown policy",
escalationRuleName: rule.onCallDutyPolicyEscalationRule?.name ?? "Unknown rule",
assignmentType: "schedule",
assignmentDetail: `Via schedule: ${rule.onCallDutyPolicySchedule?.name ?? "Unknown"}`,
});
});
return assignments;
}
export function useAllProjectOnCallPolicies(): UseAllProjectOnCallPoliciesResult {
const { projectList } = useProject();
const query: UseQueryResult<ProjectOnCallAssignments[], Error> = useQuery({
queryKey: [
"oncall",
"current-duty",
projectList
.map((project: ProjectItem) => {
return project._id;
})
.sort()
.join(","),
],
enabled: projectList.length > 0,
queryFn: async () => {
const results: PromiseSettledResult<ProjectOnCallAssignments | null>[] =
await Promise.allSettled(
projectList.map(async (project: ProjectItem) => {
const response: CurrentOnDutyEscalationPoliciesResponse =
await fetchCurrentOnDutyEscalationPolicies(project._id);
const assignments: OnCallAssignmentItem[] = toAssignments(
project,
response,
);
if (assignments.length === 0) {
return null;
}
return {
projectId: project._id,
projectName: project.name,
assignments,
};
}),
);
const projects: ProjectOnCallAssignments[] = [];
results.forEach((result: PromiseSettledResult<ProjectOnCallAssignments | null>) => {
if (result.status === "fulfilled" && result.value) {
projects.push(result.value);
}
});
return projects.sort((a: ProjectOnCallAssignments, b: ProjectOnCallAssignments) => {
return a.projectName.localeCompare(b.projectName);
});
},
});
const totalAssignments: number = useMemo(() => {
return (query.data ?? []).reduce(
(total: number, project: ProjectOnCallAssignments) => {
return total + project.assignments.length;
},
0,
);
}, [query.data]);
const refetch: () => Promise<void> = async (): Promise<void> => {
await query.refetch();
};
return {
projects: query.data ?? [],
totalAssignments,
isLoading: query.isPending,
isError: query.isError,
refetch,
};
}

View File

@@ -6,6 +6,7 @@ import { MainTabParamList } from "./types";
import HomeScreen from "../screens/HomeScreen";
import IncidentsStackNavigator from "./IncidentsStackNavigator";
import AlertsStackNavigator from "./AlertsStackNavigator";
import OnCallStackNavigator from "./OnCallStackNavigator";
import SettingsStackNavigator from "./SettingsStackNavigator";
import { useTheme } from "../theme";
@@ -166,6 +167,31 @@ export default function MainTabNavigator(): React.JSX.Element {
},
}}
/>
<Tab.Screen
name="OnCall"
component={OnCallStackNavigator}
options={{
headerShown: false,
title: "On-Call",
tabBarIcon: ({
color,
focused,
}: {
color: string;
focused: boolean;
}) => {
return (
<TabIcon
name="call-outline"
focusedName="call"
color={color}
focused={focused}
accentColor={theme.colors.actionPrimary}
/>
);
},
}}
/>
<Tab.Screen
name="Settings"
component={SettingsStackNavigator}

View File

@@ -0,0 +1,39 @@
import React from "react";
import { Platform } from "react-native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { useTheme } from "../theme";
import MyOnCallPoliciesScreen from "../screens/MyOnCallPoliciesScreen";
import type { OnCallStackParamList } from "./types";
const Stack: ReturnType<typeof createNativeStackNavigator<OnCallStackParamList>> =
createNativeStackNavigator<OnCallStackParamList>();
export default function OnCallStackNavigator(): React.JSX.Element {
const { theme } = useTheme();
return (
<Stack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: theme.colors.backgroundPrimary,
},
headerTintColor: theme.colors.textPrimary,
headerShadowVisible: false,
...(Platform.OS === "ios"
? {
headerLargeTitle: true,
headerLargeStyle: {
backgroundColor: theme.colors.backgroundPrimary,
},
}
: {}),
}}
>
<Stack.Screen
name="OnCallList"
component={MyOnCallPoliciesScreen}
options={{ title: "My On-Call Policies" }}
/>
</Stack.Navigator>
);
}

View File

@@ -35,6 +35,11 @@ const linking: React.ComponentProps<typeof NavigationContainer>["linking"] = {
AlertEpisodeDetail: "alert-episode/:projectId/:episodeId",
},
},
OnCall: {
screens: {
OnCallList: "on-call",
},
},
},
},
};

View File

@@ -7,6 +7,7 @@ export type MainTabParamList = {
Home: undefined;
Incidents: undefined;
Alerts: undefined;
OnCall: undefined;
Settings: undefined;
};
@@ -14,6 +15,10 @@ export type SettingsStackParamList = {
SettingsList: undefined;
};
export type OnCallStackParamList = {
OnCallList: undefined;
};
export type IncidentsStackParamList = {
IncidentsList: undefined;
IncidentDetail: { incidentId: string; projectId: string };

View File

@@ -18,6 +18,7 @@ import type { BottomTabNavigationProp } from "@react-navigation/bottom-tabs";
import type { MainTabParamList } from "../navigation/types";
import Logo from "../components/Logo";
import GradientButton from "../components/GradientButton";
import { useAllProjectOnCallPolicies } from "../hooks/useAllProjectOnCallPolicies";
type HomeNavProp = BottomTabNavigationProp<MainTabParamList, "Home">;
@@ -144,11 +145,18 @@ export default function HomeScreen(): React.JSX.Element {
refetch,
} = useAllProjectCounts();
const {
totalAssignments,
projects: onCallProjects,
isLoading: onCallLoading,
refetch: refetchOnCall,
} = useAllProjectOnCallPolicies();
const { lightImpact } = useHaptics();
const onRefresh: () => Promise<void> = async (): Promise<void> => {
lightImpact();
await Promise.all([refetch(), refreshProjects()]);
await Promise.all([refetch(), refreshProjects(), refetchOnCall()]);
};
if (!isLoadingProjects && projectList.length === 0) {
@@ -324,6 +332,105 @@ export default function HomeScreen(): React.JSX.Element {
</View>
<View className="gap-4 px-5">
<View>
<Text
className="text-[12px] font-semibold uppercase mb-2"
style={{
color: theme.colors.textSecondary,
letterSpacing: 1,
}}
>
On-Call
</Text>
<TouchableOpacity
activeOpacity={0.8}
onPress={() => {
lightImpact();
navigation.navigate("OnCall");
}}
className="rounded-3xl overflow-hidden p-4"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
shadowColor: theme.isDark ? "#000" : theme.colors.accentGradientMid,
shadowOpacity: theme.isDark ? 0.24 : 0.09,
shadowOffset: { width: 0, height: 8 },
shadowRadius: 14,
elevation: 5,
}}
accessibilityRole="button"
accessibilityLabel="View my on-call assignments"
>
<LinearGradient
colors={[
theme.colors.oncallActiveBg,
theme.colors.accentGradientEnd + "06",
]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={{
position: "absolute",
top: -20,
left: -10,
right: -10,
height: 120,
}}
/>
<View className="flex-row items-center justify-between">
<View className="flex-row items-center flex-1">
<View
className="w-10 h-10 rounded-2xl items-center justify-center mr-3"
style={{ backgroundColor: theme.colors.oncallActiveBg }}
>
<Ionicons
name="call-outline"
size={18}
color={theme.colors.oncallActive}
/>
</View>
<View className="flex-1">
<Text
className="text-[15px] font-bold"
style={{ color: theme.colors.textPrimary }}
>
My On-Call Policies
</Text>
<Text
className="text-[12px] mt-0.5"
style={{ color: theme.colors.textSecondary }}
>
{onCallLoading
? "Loading assignments..."
: totalAssignments > 0
? `${totalAssignments} active ${totalAssignments === 1 ? "assignment" : "assignments"} across ${onCallProjects.length} ${onCallProjects.length === 1 ? "project" : "projects"}`
: "You are not currently on-call"}
</Text>
</View>
</View>
<View className="items-end ml-3">
<Text
className="text-[28px] font-bold"
style={{
color: theme.colors.textPrimary,
fontVariant: ["tabular-nums"],
letterSpacing: -1,
}}
>
{onCallLoading ? "--" : totalAssignments}
</Text>
<Ionicons
name="chevron-forward"
size={14}
color={theme.colors.textTertiary}
/>
</View>
</View>
</TouchableOpacity>
</View>
<View>
<Text
className="text-[12px] font-semibold uppercase mb-2"

View File

@@ -0,0 +1,413 @@
import React, { useMemo } from "react";
import {
View,
Text,
ScrollView,
RefreshControl,
TouchableOpacity,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { LinearGradient } from "expo-linear-gradient";
import { useTheme } from "../theme";
import { useHaptics } from "../hooks/useHaptics";
import { useAllProjectOnCallPolicies } from "../hooks/useAllProjectOnCallPolicies";
import EmptyState from "../components/EmptyState";
import SkeletonCard from "../components/SkeletonCard";
import type {
OnCallAssignmentItem,
OnCallAssignmentType,
ProjectOnCallAssignments,
} from "../api/types";
interface AssignmentBadgeConfig {
icon: keyof typeof Ionicons.glyphMap;
label: string;
color: string;
background: string;
}
function getAssignmentBadge(
type: OnCallAssignmentType,
colors: {
success: string;
successBg: string;
info: string;
infoBg: string;
warning: string;
warningBg: string;
},
): AssignmentBadgeConfig {
switch (type) {
case "user":
return {
icon: "person-outline",
label: "Direct",
color: colors.success,
background: colors.successBg,
};
case "team":
return {
icon: "people-outline",
label: "Team",
color: colors.info,
background: colors.infoBg,
};
case "schedule":
return {
icon: "time-outline",
label: "Schedule",
color: colors.warning,
background: colors.warningBg,
};
}
}
export default function MyOnCallPoliciesScreen(): React.JSX.Element {
const { theme } = useTheme();
const { lightImpact } = useHaptics();
const { projects, totalAssignments, isLoading, isError, refetch } =
useAllProjectOnCallPolicies();
const projectCount: number = projects.length;
const summaryText: string = useMemo(() => {
const assignmentLabel: string =
totalAssignments === 1 ? "assignment" : "assignments";
const projectLabel: string = projectCount === 1 ? "project" : "projects";
return `You are currently on duty for ${totalAssignments} ${assignmentLabel} across ${projectCount} ${projectLabel}.`;
}, [projectCount, totalAssignments]);
const onRefresh: () => Promise<void> = async (): Promise<void> => {
lightImpact();
await refetch();
};
if (isLoading) {
return (
<View
className="flex-1"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
>
<ScrollView contentContainerStyle={{ padding: 16, paddingBottom: 44 }}>
<View
className="rounded-3xl overflow-hidden p-5 mb-4"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
}}
>
<SkeletonCard variant="compact" />
</View>
<SkeletonCard lines={3} />
<SkeletonCard lines={4} />
<SkeletonCard lines={3} />
</ScrollView>
</View>
);
}
if (isError) {
return (
<View
className="flex-1"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
>
<EmptyState
title="Could not load on-call assignments"
subtitle="Pull to refresh or try again."
icon="alerts"
actionLabel="Retry"
onAction={() => {
return refetch();
}}
/>
</View>
);
}
return (
<ScrollView
style={{ backgroundColor: theme.colors.backgroundPrimary }}
contentContainerStyle={{ padding: 16, paddingBottom: 56 }}
refreshControl={
<RefreshControl
refreshing={false}
onRefresh={onRefresh}
tintColor={theme.colors.actionPrimary}
/>
}
>
<View
className="rounded-3xl overflow-hidden p-5 mb-5"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
shadowColor: theme.isDark ? "#000" : theme.colors.accentGradientMid,
shadowOpacity: theme.isDark ? 0.32 : 0.1,
shadowOffset: { width: 0, height: 10 },
shadowRadius: 18,
elevation: 7,
}}
>
<LinearGradient
colors={[
theme.colors.oncallActiveBg,
theme.colors.accentGradientEnd + "08",
]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={{
position: "absolute",
top: -70,
left: -30,
right: -20,
height: 220,
}}
/>
<View className="flex-row items-center justify-between">
<View className="flex-row items-center flex-1">
<View
className="w-11 h-11 rounded-2xl items-center justify-center mr-3"
style={{
backgroundColor: theme.colors.oncallActiveBg,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
}}
>
<Ionicons
name="call-outline"
size={20}
color={theme.colors.oncallActive}
/>
</View>
<View className="flex-1">
<Text
className="text-[20px] font-bold"
style={{
color: theme.colors.textPrimary,
letterSpacing: -0.4,
}}
>
On-Call Now
</Text>
<Text
className="text-[12px] mt-0.5"
style={{ color: theme.colors.textSecondary }}
>
Live duty assignments
</Text>
</View>
</View>
<View
className="px-3 py-1.5 rounded-xl"
style={{
backgroundColor: theme.colors.backgroundTertiary,
borderWidth: 1,
borderColor: theme.colors.borderSubtle,
}}
>
<Text
className="text-[18px] font-bold"
style={{
color: theme.colors.textPrimary,
fontVariant: ["tabular-nums"],
}}
>
{totalAssignments}
</Text>
</View>
</View>
<Text
className="text-[13px] mt-4 leading-5"
style={{ color: theme.colors.textSecondary }}
>
{summaryText}
</Text>
</View>
{projects.length === 0 ? (
<View
className="rounded-3xl overflow-hidden"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
}}
>
<EmptyState
title="Not currently on-call"
subtitle="You are not on duty for any on-call policy right now."
icon="alerts"
/>
</View>
) : (
<View className="gap-4">
{projects.map((projectData: ProjectOnCallAssignments) => {
return (
<View
key={projectData.projectId}
className="rounded-3xl overflow-hidden"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
shadowColor: theme.isDark
? "#000"
: theme.colors.accentGradientMid,
shadowOpacity: theme.isDark ? 0.2 : 0.08,
shadowOffset: { width: 0, height: 6 },
shadowRadius: 14,
elevation: 4,
}}
>
<View
className="px-4 py-3.5 flex-row items-center justify-between"
style={{
borderBottomWidth: 1,
borderBottomColor: theme.colors.borderSubtle,
backgroundColor: theme.colors.backgroundSecondary,
}}
>
<View className="flex-row items-center flex-1">
<Ionicons
name="folder-open-outline"
size={16}
color={theme.colors.textSecondary}
/>
<Text
className="text-[14px] font-semibold ml-2 mr-2 flex-1"
style={{ color: theme.colors.textPrimary }}
numberOfLines={1}
>
{projectData.projectName}
</Text>
</View>
<View
className="px-2 py-1 rounded-lg"
style={{
backgroundColor: theme.colors.backgroundTertiary,
borderWidth: 1,
borderColor: theme.colors.borderSubtle,
}}
>
<Text
className="text-[11px] font-semibold"
style={{ color: theme.colors.textSecondary }}
>
{projectData.assignments.length} active
</Text>
</View>
</View>
<View>
{projectData.assignments.map(
(
assignment: OnCallAssignmentItem,
assignmentIndex: number,
): React.JSX.Element => {
const badge: AssignmentBadgeConfig = getAssignmentBadge(
assignment.assignmentType,
{
success: theme.colors.oncallActive,
successBg: theme.colors.oncallActiveBg,
info: theme.colors.severityInfo,
infoBg: theme.colors.severityInfoBg,
warning: theme.colors.severityWarning,
warningBg: theme.colors.severityWarningBg,
},
);
return (
<TouchableOpacity
key={`${assignment.projectId}-${assignment.policyId ?? assignmentIndex}`}
activeOpacity={0.82}
className="px-4 py-3.5"
style={
assignmentIndex !==
projectData.assignments.length - 1
? {
borderBottomWidth: 1,
borderBottomColor: theme.colors.borderSubtle,
}
: undefined
}
>
<View className="flex-row items-center justify-between">
<Text
className="text-[15px] font-semibold flex-1 mr-3"
style={{ color: theme.colors.textPrimary }}
numberOfLines={1}
>
{assignment.policyName}
</Text>
<View
className="px-2.5 py-1 rounded-full flex-row items-center"
style={{
backgroundColor: badge.background,
}}
>
<Ionicons
name={badge.icon}
size={12}
color={badge.color}
/>
<Text
className="text-[11px] font-semibold ml-1"
style={{ color: badge.color }}
>
{badge.label}
</Text>
</View>
</View>
<View className="mt-2">
<View className="flex-row items-center">
<Ionicons
name="git-branch-outline"
size={13}
color={theme.colors.textTertiary}
/>
<Text
className="text-[12px] ml-1.5"
style={{ color: theme.colors.textSecondary }}
numberOfLines={1}
>
Rule: {assignment.escalationRuleName}
</Text>
</View>
<View className="flex-row items-center mt-1">
<Ionicons
name="information-circle-outline"
size={13}
color={theme.colors.textTertiary}
/>
<Text
className="text-[12px] ml-1.5"
style={{ color: theme.colors.textSecondary }}
numberOfLines={1}
>
{assignment.assignmentDetail}
</Text>
</View>
</View>
</TouchableOpacity>
);
},
)}
</View>
</View>
);
})}
</View>
)}
</ScrollView>
);
}