main
vue 293 lines 7.53 KB
Raw
1 <template>
2 <div class="flex flex-col gap-4">
3 <div class="flex w-full items-center justify-between gap-4">
4 <Filters v-model:value="filters" class="w-auto!" @loaded="handleFiltersLoaded" />
5 <CreateCaseButton secondary @success="handleCreated" />
6 </div>
7
8 <div class="flex flex-col gap-2">
9 <div ref="headerRef" class="flex items-center justify-between">
10 <Chip size="small" :value="loading ? 'Loading...' : pagination.total" label="items" />
11
12 <div class="flex items-center gap-2 whitespace-nowrap">
13 <n-pagination
14 v-model:page="pagination.page"
15 v-model:page-size="pagination.pageSize"
16 :page-slot
17 :show-size-picker
18 :page-sizes
19 :item-count="pagination.total"
20 :simple="simpleMode"
21 size="small"
22 />
23 </div>
24 </div>
25
26 <div class="grow overflow-hidden">
27 <n-data-table
28 bordered
29 :loading
30 size="small"
31 :data
32 :columns
33 :scroll-x="1400"
34 class="[&_.n-data-table-th\_\_title]:whitespace-nowrap"
35 >
36 <template #empty>
37 <n-empty description="No cases found">
38 <template #extra>try changing the filters</template>
39 </n-empty>
40 </template>
41 </n-data-table>
42 </div>
43
44 <div class="flex justify-end">
45 <n-pagination
46 v-if="pagination.total > pagination.pageSize"
47 v-model:page="pagination.page"
48 :page-size="pagination.pageSize"
49 :item-count="pagination.total"
50 :page-slot="6"
51 size="small"
52 :simple="simpleMode"
53 />
54 </div>
55 </div>
56 </div>
57 </template>
58
59 <script setup lang="tsx">
60 import type { AxiosResponse } from "axios"
61 import type { DataTableColumns } from "naive-ui"
62 import type { CaseAssignedUpdateSuccessPayload } from "@/components/cases/CaseAssignedSelect.vue"
63 import type { CaseStatusUpdateSuccessPayload } from "@/components/cases/CaseStatusSelect.vue"
64 import type { FiltersModel } from "@/components/cases/Filters.vue"
65 import type { Case, CasesListResponse, CaseStatus } from "@/types/cases"
66 import type { ApiError, CommonResponse, Pagination } from "@/types/common"
67 import { useDebounceFn, useElementSize } from "@vueuse/core"
68 import axios from "axios"
69 import { NDataTable, NEmpty, NPagination, NTag, useMessage } from "naive-ui"
70 import { computed, ref, useTemplateRef, watch } from "vue"
71 import Api from "@/api"
72 import CaseAssignedSelect from "@/components/cases/CaseAssignedSelect.vue"
73 import CaseDetailsButton from "@/components/cases/CaseDetailsButton.vue"
74 import CaseStatusSelect from "@/components/cases/CaseStatusSelect.vue"
75 import CreateCaseButton from "@/components/cases/CreateCaseButton.vue"
76 import Filters from "@/components/cases/Filters.vue"
77 import Chip from "@/components/common/Chip.vue"
78 import Icon from "@/components/common/Icon.vue"
79 import { useCustomerFilterStore } from "@/stores/customerFilter"
80 import { useSettingsStore } from "@/stores/settings"
81 import { getApiErrorMessage, getStatusColor } from "@/utils"
82 import { formatDate } from "@/utils/format"
83
84 const message = useMessage()
85 const data = ref<Case[]>([])
86 const loading = ref(false)
87 const dFormats = useSettingsStore().dateFormat
88 const customerFilterStore = useCustomerFilterStore()
89
90 const { width: headerWidthRef } = useElementSize(useTemplateRef("headerRef"))
91 const pageSizes = [10, 25, 50, 100]
92 const pageSlot = computed(() => (headerWidthRef.value < 800 ? 5 : 8))
93 const simpleMode = computed(() => headerWidthRef.value < 600)
94 const showSizePicker = ref(true)
95 const assignedAvailable = ref<string[]>([])
96
97 const pagination = ref({
98 page: 1,
99 pageSize: pageSizes[1],
100 total: 0
101 })
102
103 // Filters
104 const filters = ref<FiltersModel>({
105 key: null,
106 value: null
107 })
108
109 const columns = computed<DataTableColumns<Case>>(() => [
110 {
111 title: "Case Name",
112 key: "case_name",
113 fixed: simpleMode.value ? undefined : "left",
114 width: 380,
115 render: row => <div>{row.case_name}</div>
116 },
117 {
118 title: "Created",
119 key: "case_creation_time",
120 minWidth: 200,
121 render: row => <span class="font-mono text-sm">{formatDate(row.case_creation_time, dFormats.datetime)}</span>
122 },
123 {
124 title: "Assigned To",
125 key: "assigned_to",
126 width: 120,
127 render: row => {
128 return (
129 <CaseAssignedSelect
130 caseId={row.id}
131 assignedTo={row.assigned_to}
132 assignedAvailable={assignedAvailable.value}
133 onSuccess={handleAssignedToUpdateSuccess}
134 />
135 )
136 }
137 },
138 {
139 title: "Status",
140 key: "status",
141 width: 200,
142 render: row => {
143 return (
144 <div class="flex items-center gap-2">
145 <NTag
146 type={getStatusColor(row.case_status)}
147 round
148 class="p-1! [&_.n-tag\_\_icon]:m-0!"
149 v-slots={{
150 icon: () => <Icon name="carbon:circle-solid" />
151 }}
152 />
153 <CaseStatusSelect caseId={row.id} status={row.case_status} onSuccess={handleStatusUpdateSuccess} />
154 </div>
155 )
156 }
157 },
158 {
159 title: "Actions",
160 key: "actions",
161 width: 194,
162 render: row => {
163 return (
164 <CaseDetailsButton
165 caseId={row.id}
166 onStatusUpdated={handleStatusUpdateSuccess}
167 onAssignedToUpdated={handleAssignedToUpdateSuccess}
168 onDeleted={handleDeleted}
169 />
170 )
171 }
172 }
173 ])
174
175 let abortController = new AbortController()
176
177 const loadCases = useDebounceFn(async () => {
178 loading.value = true
179
180 abortController?.abort()
181 abortController = new AbortController()
182
183 try {
184 let response: AxiosResponse<CommonResponse<CasesListResponse>>
185
186 const paginationPayload: Pagination = {
187 page: pagination.value.page,
188 pageSize: pagination.value.pageSize,
189 order: "desc"
190 }
191
192 if (filters.value.key && filters.value.value) {
193 switch (filters.value.key) {
194 case "statuses":
195 response = await Api.cases.getCasesByStatus(
196 filters.value.value as CaseStatus,
197 paginationPayload,
198 abortController.signal,
199 customerFilterStore.queryCustomerCodes
200 )
201 break
202 case "assigned_to":
203 response = await Api.cases.getCasesByAssignedTo(
204 filters.value.value,
205 paginationPayload,
206 abortController.signal,
207 customerFilterStore.queryCustomerCodes
208 )
209 break
210 default:
211 response = await Api.cases.getCases(
212 paginationPayload,
213 abortController.signal,
214 customerFilterStore.queryCustomerCodes
215 )
216 break
217 }
218 } else {
219 response = await Api.cases.getCases(
220 paginationPayload,
221 abortController.signal,
222 customerFilterStore.queryCustomerCodes
223 )
224 }
225
226 data.value = response.data.cases
227 pagination.value.total = response.data.total
228 loading.value = false
229 } catch (err) {
230 if (!axios.isCancel(err)) {
231 message.error(getApiErrorMessage(err as ApiError))
232 loading.value = false
233 }
234 }
235 }, 400)
236
237 function resetPage() {
238 pagination.value.page = 1
239 loadCases()
240 }
241
242 function handleFiltersLoaded(value: Record<string, string[]>) {
243 assignedAvailable.value = value.assigned_to || []
244 }
245
246 function handleDeleted() {
247 loadCases()
248 }
249
250 function resetFilters() {
251 filters.value.key = null
252 filters.value.value = null
253 }
254
255 function resetPagination() {
256 pagination.value.page = 1
257 pagination.value.pageSize = pageSizes[1]
258 }
259
260 function handleCreated() {
261 resetFilters()
262 resetPagination()
263 loadCases()
264 }
265
266 function handleAssignedToUpdateSuccess(payload: CaseAssignedUpdateSuccessPayload) {
267 const case_ = data.value.find(c => c.id === payload.caseId)
268 if (case_) {
269 case_.assigned_to = payload.assignedTo
270 }
271 }
272
273 function handleStatusUpdateSuccess(payload: CaseStatusUpdateSuccessPayload) {
274 const case_ = data.value.find(c => c.id === payload.caseId)
275 if (case_) {
276 case_.case_status = payload.status
277 }
278 }
279
280 watch(
281 [() => pagination.value.pageSize, () => filters.value.value, () => customerFilterStore.selectedCustomerCodes],
282 resetPage,
283 {
284 deep: true,
285 immediate: true
286 }
287 )
288
289 watch(() => pagination.value.page, loadCases, {
290 deep: true,
291 immediate: true
292 })
293 </script>