| 1 | <template> |
| 2 | <div class="case-tasks-list flex flex-col gap-4"> |
| 3 | <CaseTasksToolbar :case-id :customer-code :can-edit :tasks :linked-alerts @updated="fetchTasks" /> |
| 4 | |
| 5 | <n-spin :show="loading"> |
| 6 | <div v-if="tasks.length" class="flex flex-col gap-4"> |
| 7 | <!-- |
| 8 | One group per originating alert plus a "Case-wide" bucket for |
| 9 | orphaned / never-attached tasks (alert_id IS NULL). Group order: |
| 10 | alerts in their linked order, case-wide last. |
| 11 | --> |
| 12 | <CardEntity |
| 13 | v-for="group in groups" |
| 14 | :key="group.key" |
| 15 | embedded |
| 16 | size="small" |
| 17 | header-box-class="flex flex-wrap items-center gap-2" |
| 18 | > |
| 19 | <template #header> |
| 20 | <div v-if="group.alert" class="flex flex-wrap items-center gap-2"> |
| 21 | <span class="text-default text-base font-semibold">{{ group.alert.alert_name }}</span> |
| 22 | <n-tag size="small" :bordered="false">{{ group.alert.source }}</n-tag> |
| 23 | <n-tag size="small" :bordered="false">alert #{{ group.alert.id }}</n-tag> |
| 24 | <n-tag size="small" :bordered="false">{{ group.tasks.length }} task(s)</n-tag> |
| 25 | </div> |
| 26 | <div v-else class="flex flex-wrap items-center gap-2"> |
| 27 | <span class="text-default text-base font-semibold">Case-wide / general</span> |
| 28 | <n-tag size="small" :bordered="false" type="info">not attached to any alert</n-tag> |
| 29 | <n-tag size="small" :bordered="false">{{ group.tasks.length }} task(s)</n-tag> |
| 30 | </div> |
| 31 | </template> |
| 32 | <template #mainExtra> |
| 33 | <div class="flex flex-col gap-3"> |
| 34 | <CaseTaskItem |
| 35 | v-for="task in group.tasks" |
| 36 | :key="task.id" |
| 37 | :task |
| 38 | :case-id |
| 39 | :can-edit |
| 40 | @deleted="fetchTasks" |
| 41 | @updated="handleTaskUpdated" |
| 42 | /> |
| 43 | </div> |
| 44 | </template> |
| 45 | </CardEntity> |
| 46 | </div> |
| 47 | <n-empty v-else-if="!loading" description="No tasks on this case" class="h-32 justify-center" /> |
| 48 | </n-spin> |
| 49 | </div> |
| 50 | </template> |
| 51 | |
| 52 | <script setup lang="ts"> |
| 53 | import type { Alert } from "@/types/incidentManagement/alerts.d" |
| 54 | import type { CaseTask } from "@/types/incidentManagement/caseTemplates.d" |
| 55 | import { NEmpty, NSpin, NTag, useMessage } from "naive-ui" |
| 56 | import { computed, onBeforeMount, ref } from "vue" |
| 57 | import Api from "@/api" |
| 58 | import CardEntity from "@/components/common/cards/CardEntity.vue" |
| 59 | import CaseTaskItem from "./CaseTaskItem.vue" |
| 60 | import CaseTasksToolbar from "./CaseTasksToolbar.vue" |
| 61 | |
| 62 | const props = defineProps<{ |
| 63 | caseId: number |
| 64 | customerCode?: string | null |
| 65 | canEdit: boolean |
| 66 | linkedAlerts?: Alert[] |
| 67 | }>() |
| 68 | |
| 69 | const message = useMessage() |
| 70 | const tasks = ref<CaseTask[]>([]) |
| 71 | const loading = ref(false) |
| 72 | |
| 73 | interface TaskGroup { |
| 74 | key: string |
| 75 | alert: Alert | null |
| 76 | tasks: CaseTask[] |
| 77 | } |
| 78 | |
| 79 | const groups = computed<TaskGroup[]>(() => { |
| 80 | // Index tasks by alert_id so we can attach them to their alert group; tasks |
| 81 | // whose alert_id no longer matches a linked alert (orphans) fall through |
| 82 | // to the case-wide bucket alongside genuine alert_id=null tasks. |
| 83 | const linkedAlerts = props.linkedAlerts || [] |
| 84 | const linkedIds = new Set(linkedAlerts.map(a => a.id)) |
| 85 | const byAlert: Record<number, CaseTask[]> = {} |
| 86 | const caseWide: CaseTask[] = [] |
| 87 | |
| 88 | for (const t of tasks.value) { |
| 89 | if (t.alert_id != null && linkedIds.has(t.alert_id)) { |
| 90 | if (!byAlert[t.alert_id]) byAlert[t.alert_id] = [] |
| 91 | byAlert[t.alert_id].push(t) |
| 92 | } else { |
| 93 | caseWide.push(t) |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | const out: TaskGroup[] = [] |
| 98 | for (const a of linkedAlerts) { |
| 99 | const groupTasks = byAlert[a.id] || [] |
| 100 | if (!groupTasks.length) continue |
| 101 | out.push({ key: `alert-${a.id}`, alert: a, tasks: groupTasks }) |
| 102 | } |
| 103 | if (caseWide.length) { |
| 104 | out.push({ key: "case-wide", alert: null, tasks: caseWide }) |
| 105 | } |
| 106 | return out |
| 107 | }) |
| 108 | |
| 109 | function fetchTasks() { |
| 110 | loading.value = true |
| 111 | Api.incidentManagement.caseTemplates |
| 112 | .listCaseTasks(props.caseId) |
| 113 | .then(res => { |
| 114 | if (res.data.success) { |
| 115 | tasks.value = res.data.tasks |
| 116 | } else { |
| 117 | message.warning(res.data.message) |
| 118 | } |
| 119 | }) |
| 120 | .catch(err => { |
| 121 | message.error(err.response?.data?.message || "Failed to load case tasks") |
| 122 | }) |
| 123 | .finally(() => { |
| 124 | loading.value = false |
| 125 | }) |
| 126 | } |
| 127 | |
| 128 | function handleTaskUpdated(task: CaseTask) { |
| 129 | tasks.value = tasks.value.map(t => (t.id === task.id ? task : t)) |
| 130 | } |
| 131 | |
| 132 | onBeforeMount(fetchTasks) |
| 133 | </script> |