@nuxt-customer-portal/service-requests 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +8 -0
- package/app/components/CustomerRequestDetail.vue +52 -64
- package/app/components/CustomerRequestForm.vue +29 -9
- package/app/components/ServiceRequestManagement.vue +109 -0
- package/app/components/ServiceRequestsList.vue +168 -0
- package/app/components/ServiceRequestsPaginatedList.vue +115 -0
- package/app/composables/useAdminServiceRequests.ts +2 -0
- package/app/composables/useServiceRequestList.ts +184 -0
- package/app/composables/useServiceRequestMenu.ts +0 -8
- package/app/pages/admin/requests/[id].vue +5 -134
- package/app/pages/admin/requests/index.vue +3 -50
- package/app/pages/requests/[id].vue +20 -18
- package/app/pages/requests/index.vue +1 -871
- package/app/pages/requests/new.vue +9 -3
- package/i18n/locales/en.json +22 -1
- package/i18n/locales/nl.json +22 -1
- package/package.json +3 -3
- package/portal.manifest.mjs +1 -1
- package/server/api/service-requests/admin/[id].patch.ts +8 -0
- package/server/api/service-requests/admin/assignees.get.ts +7 -0
- package/server/utils/service-request-repository.ts +25 -3
- package/server/utils/service-request-validation.ts +6 -6
- package/shared/feature.ts +4 -27
- package/shared/types/service-request.ts +1 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ServiceRequest,
|
|
3
|
+
ServiceRequestListResponse
|
|
4
|
+
} from '@nuxt-customer-portal/service-requests/shared/types/service-request'
|
|
5
|
+
import { usePaginatedResource } from '@nuxt-customer-portal/core/app/composables/usePaginatedResource'
|
|
6
|
+
|
|
7
|
+
export const useServiceRequestList = (options: {
|
|
8
|
+
endpoint: string
|
|
9
|
+
filterKeys?: string[]
|
|
10
|
+
defaultFilters?: Record<string, string>
|
|
11
|
+
defaultSort: string
|
|
12
|
+
defaultSortDir?: 'asc' | 'desc'
|
|
13
|
+
}) => {
|
|
14
|
+
const route = useRoute()
|
|
15
|
+
const router = useRouter()
|
|
16
|
+
const pageSize = 20
|
|
17
|
+
const defaultSortDir = options.defaultSortDir ?? 'asc'
|
|
18
|
+
const search = ref(String(route.query.search ?? ''))
|
|
19
|
+
const sortBy = ref(String(route.query.sortBy ?? options.defaultSort))
|
|
20
|
+
const sortDir = ref<'asc' | 'desc'>(
|
|
21
|
+
route.query.sortDir === 'asc' || route.query.sortDir === 'desc' ? route.query.sortDir : defaultSortDir
|
|
22
|
+
)
|
|
23
|
+
const currentPage = ref(Math.max(1, Number(route.query.page) || 1))
|
|
24
|
+
const filters = reactive<Record<string, string | undefined>>(
|
|
25
|
+
Object.fromEntries(
|
|
26
|
+
(options.filterKeys ?? []).map((key) => [
|
|
27
|
+
key,
|
|
28
|
+
route.query[key] ? String(route.query[key]) : options.defaultFilters?.[key]
|
|
29
|
+
])
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
const categories = ref<string[]>([])
|
|
33
|
+
const resource = usePaginatedResource<ServiceRequest, Record<string, string | undefined>>({
|
|
34
|
+
pageSize,
|
|
35
|
+
getKey: (item) => (item as { id: string }).id,
|
|
36
|
+
fetchPage: async ({ filters: query, page, pageSize, signal }) => {
|
|
37
|
+
const result = await $fetch<ServiceRequestListResponse>(options.endpoint, {
|
|
38
|
+
query: { ...query, page, pageSize },
|
|
39
|
+
signal
|
|
40
|
+
})
|
|
41
|
+
if (!signal.aborted) {
|
|
42
|
+
categories.value = result.categories ?? []
|
|
43
|
+
}
|
|
44
|
+
return result
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
const query = () => ({
|
|
48
|
+
...(search.value.trim() ? { search: search.value.trim() } : {}),
|
|
49
|
+
...Object.fromEntries(Object.entries(filters).filter(([, value]) => value)),
|
|
50
|
+
...(sortBy.value !== options.defaultSort ? { sortBy: sortBy.value } : {}),
|
|
51
|
+
...(sortDir.value !== defaultSortDir ? { sortDir: sortDir.value } : {})
|
|
52
|
+
})
|
|
53
|
+
const routeQuery = () => ({
|
|
54
|
+
...(search.value.trim() ? { search: search.value.trim() } : {}),
|
|
55
|
+
...Object.fromEntries(
|
|
56
|
+
Object.entries(filters).filter(([key, value]) => value && value !== options.defaultFilters?.[key])
|
|
57
|
+
),
|
|
58
|
+
...(sortBy.value !== options.defaultSort ? { sortBy: sortBy.value } : {}),
|
|
59
|
+
...(sortDir.value !== defaultSortDir ? { sortDir: sortDir.value } : {})
|
|
60
|
+
})
|
|
61
|
+
const syncRoute = (page = currentPage.value) =>
|
|
62
|
+
router.replace({ path: route.path, query: { ...routeQuery(), ...(page > 1 ? { page: String(page) } : {}) } })
|
|
63
|
+
const load = async (page = currentPage.value) => {
|
|
64
|
+
const result = await resource.loadPage(query(), { page, pageSize }).catch(() => undefined)
|
|
65
|
+
if (result) {
|
|
66
|
+
const lastPage = Math.max(1, result.pagination.pageCount)
|
|
67
|
+
if (page > lastPage) {
|
|
68
|
+
currentPage.value = lastPage
|
|
69
|
+
await syncRoute(lastPage)
|
|
70
|
+
return resource.loadPage(query(), { page: lastPage, pageSize }).catch(() => undefined)
|
|
71
|
+
}
|
|
72
|
+
currentPage.value = result.pagination.page
|
|
73
|
+
}
|
|
74
|
+
return result
|
|
75
|
+
}
|
|
76
|
+
const resetAndLoad = async () => {
|
|
77
|
+
currentPage.value = 1
|
|
78
|
+
await syncRoute(1)
|
|
79
|
+
await load(1)
|
|
80
|
+
}
|
|
81
|
+
const loadNext = async () => {
|
|
82
|
+
const result = await resource.loadNextPage(query())?.catch(() => undefined)
|
|
83
|
+
if (result) {
|
|
84
|
+
currentPage.value = result.pagination.page
|
|
85
|
+
await syncRoute(currentPage.value)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const loadPrevious = async () => {
|
|
89
|
+
const result = await resource.loadPreviousPage(query())?.catch(() => undefined)
|
|
90
|
+
if (result) {
|
|
91
|
+
currentPage.value = result.pagination.page
|
|
92
|
+
await syncRoute(currentPage.value)
|
|
93
|
+
}
|
|
94
|
+
return result
|
|
95
|
+
}
|
|
96
|
+
const goToPage = async (page: number) => {
|
|
97
|
+
currentPage.value = page
|
|
98
|
+
await syncRoute(page)
|
|
99
|
+
await load(page)
|
|
100
|
+
}
|
|
101
|
+
const setFilter = (key: string, value: string | undefined) => {
|
|
102
|
+
filters[key] = value
|
|
103
|
+
}
|
|
104
|
+
const toggleSortDir = () => {
|
|
105
|
+
sortDir.value = sortDir.value === 'asc' ? 'desc' : 'asc'
|
|
106
|
+
}
|
|
107
|
+
let searchTimer: ReturnType<typeof setTimeout> | undefined
|
|
108
|
+
let applyingRoute = false
|
|
109
|
+
watch(
|
|
110
|
+
search,
|
|
111
|
+
() => {
|
|
112
|
+
if (applyingRoute) {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
clearTimeout(searchTimer)
|
|
116
|
+
searchTimer = setTimeout(() => {
|
|
117
|
+
void resetAndLoad()
|
|
118
|
+
}, 300)
|
|
119
|
+
},
|
|
120
|
+
{ flush: 'sync' }
|
|
121
|
+
)
|
|
122
|
+
watch(
|
|
123
|
+
[sortBy, sortDir, ...Object.keys(filters).map((key) => () => filters[key])],
|
|
124
|
+
() => {
|
|
125
|
+
if (applyingRoute) {
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
clearTimeout(searchTimer)
|
|
129
|
+
void resetAndLoad()
|
|
130
|
+
},
|
|
131
|
+
{ flush: 'sync' }
|
|
132
|
+
)
|
|
133
|
+
watch(
|
|
134
|
+
() => route.query,
|
|
135
|
+
async (routeQuery) => {
|
|
136
|
+
const nextSearch = String(routeQuery.search ?? '')
|
|
137
|
+
const nextSort = String(routeQuery.sortBy ?? options.defaultSort)
|
|
138
|
+
const nextDir =
|
|
139
|
+
routeQuery.sortDir === 'asc' || routeQuery.sortDir === 'desc' ? routeQuery.sortDir : defaultSortDir
|
|
140
|
+
const nextPage = Math.max(1, Number(routeQuery.page) || 1)
|
|
141
|
+
const nextFilters = Object.fromEntries(
|
|
142
|
+
(options.filterKeys ?? []).map((key) => [
|
|
143
|
+
key,
|
|
144
|
+
routeQuery[key] ? String(routeQuery[key]) : options.defaultFilters?.[key]
|
|
145
|
+
])
|
|
146
|
+
)
|
|
147
|
+
if (
|
|
148
|
+
nextSearch === search.value.trim() &&
|
|
149
|
+
nextSort === sortBy.value &&
|
|
150
|
+
nextDir === sortDir.value &&
|
|
151
|
+
nextPage === currentPage.value &&
|
|
152
|
+
Object.keys(nextFilters).every((key) => nextFilters[key] === filters[key])
|
|
153
|
+
) {
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
clearTimeout(searchTimer)
|
|
157
|
+
applyingRoute = true
|
|
158
|
+
search.value = nextSearch
|
|
159
|
+
sortBy.value = nextSort
|
|
160
|
+
sortDir.value = nextDir
|
|
161
|
+
currentPage.value = nextPage
|
|
162
|
+
Object.assign(filters, nextFilters)
|
|
163
|
+
applyingRoute = false
|
|
164
|
+
await load(nextPage)
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
onScopeDispose(() => clearTimeout(searchTimer))
|
|
168
|
+
return {
|
|
169
|
+
...resource,
|
|
170
|
+
categories,
|
|
171
|
+
search,
|
|
172
|
+
filters,
|
|
173
|
+
sortBy,
|
|
174
|
+
sortDir,
|
|
175
|
+
currentPage,
|
|
176
|
+
load,
|
|
177
|
+
refresh: load,
|
|
178
|
+
loadNext,
|
|
179
|
+
loadPrevious,
|
|
180
|
+
goToPage,
|
|
181
|
+
setFilter,
|
|
182
|
+
toggleSortDir
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -16,14 +16,6 @@ export const useServiceRequestMenu = () => {
|
|
|
16
16
|
}
|
|
17
17
|
]
|
|
18
18
|
|
|
19
|
-
if (isOrganizationAdmin.value) {
|
|
20
|
-
items.push({
|
|
21
|
-
label: t('features.serviceRequests.navigation.manageRequests'),
|
|
22
|
-
to: '/admin/requests',
|
|
23
|
-
icon: 'i-lucide-settings'
|
|
24
|
-
})
|
|
25
|
-
}
|
|
26
|
-
|
|
27
19
|
return items
|
|
28
20
|
})
|
|
29
21
|
|
|
@@ -1,140 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type {
|
|
3
|
-
AdminServiceRequestUpdateInput,
|
|
4
|
-
ServiceRequestWithRelations
|
|
5
|
-
} from '@nuxt-customer-portal/service-requests/shared/types/service-request'
|
|
6
|
-
|
|
7
2
|
const route = useRoute()
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const toast = useToast()
|
|
13
|
-
|
|
14
|
-
const request = ref<ServiceRequestWithRelations | null>(null)
|
|
15
|
-
const loading = ref(true)
|
|
16
|
-
const updating = ref(false)
|
|
17
|
-
|
|
18
|
-
useSeoMeta({
|
|
19
|
-
title: () => request.value?.title || t('features.serviceRequests.navigation.manageRequests')
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
const adminUpdates = reactive<AdminServiceRequestUpdateInput>({
|
|
23
|
-
status: undefined,
|
|
24
|
-
priority: undefined,
|
|
25
|
-
assignedToId: '',
|
|
26
|
-
internalNotes: ''
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
const statusOptions = computed(() => [
|
|
30
|
-
{ label: t('features.serviceRequests.status.open'), value: 'OPEN' },
|
|
31
|
-
{ label: t('features.serviceRequests.status.in_progress'), value: 'IN_PROGRESS' },
|
|
32
|
-
{ label: t('features.serviceRequests.status.resolved'), value: 'RESOLVED' },
|
|
33
|
-
{ label: t('features.serviceRequests.status.closed'), value: 'CLOSED' }
|
|
34
|
-
])
|
|
35
|
-
|
|
36
|
-
const priorityOptions = computed(() => [
|
|
37
|
-
{ label: t('features.serviceRequests.priority.low'), value: 'LOW' },
|
|
38
|
-
{ label: t('features.serviceRequests.priority.medium'), value: 'MEDIUM' },
|
|
39
|
-
{ label: t('features.serviceRequests.priority.high'), value: 'HIGH' },
|
|
40
|
-
{ label: t('features.serviceRequests.priority.urgent'), value: 'URGENT' }
|
|
41
|
-
])
|
|
42
|
-
|
|
43
|
-
const userOptions = [
|
|
44
|
-
// This would be populated with users from the organization
|
|
45
|
-
{ label: t('features.serviceRequests.placeholders.selectUser'), value: '' }
|
|
46
|
-
]
|
|
47
|
-
|
|
48
|
-
onMounted(async () => {
|
|
49
|
-
try {
|
|
50
|
-
request.value = await $fetch(`/api/service-requests/${requestId}`)
|
|
51
|
-
if (!request.value) {
|
|
52
|
-
return
|
|
53
|
-
}
|
|
54
|
-
adminUpdates.status = request.value.status
|
|
55
|
-
adminUpdates.priority = request.value.priority
|
|
56
|
-
adminUpdates.assignedToId = request.value.assignedToId || ''
|
|
57
|
-
adminUpdates.internalNotes = request.value.internalNotes || ''
|
|
58
|
-
} catch {
|
|
59
|
-
toast.add({
|
|
60
|
-
title: t('common.error'),
|
|
61
|
-
description: t('features.serviceRequests.messages.fetchError'),
|
|
62
|
-
color: 'error'
|
|
63
|
-
})
|
|
64
|
-
} finally {
|
|
65
|
-
loading.value = false
|
|
66
|
-
}
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
const handleUpdate = async () => {
|
|
70
|
-
updating.value = true
|
|
71
|
-
try {
|
|
72
|
-
request.value = await adminUpdateRequest(requestId, adminUpdates)
|
|
73
|
-
toast.add({
|
|
74
|
-
title: t('common.success'),
|
|
75
|
-
description: t('features.serviceRequests.messages.updateSuccess')
|
|
76
|
-
})
|
|
77
|
-
} catch {
|
|
78
|
-
toast.add({
|
|
79
|
-
title: t('common.error'),
|
|
80
|
-
description: t('features.serviceRequests.messages.updateError'),
|
|
81
|
-
color: 'error'
|
|
82
|
-
})
|
|
83
|
-
} finally {
|
|
84
|
-
updating.value = false
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const handleQuickUpdate = () => {
|
|
89
|
-
handleUpdate()
|
|
90
|
-
}
|
|
3
|
+
await navigateTo(
|
|
4
|
+
{ path: `/requests/${encodeURIComponent(String(route.params.id))}`, query: route.query },
|
|
5
|
+
{ replace: true }
|
|
6
|
+
)
|
|
91
7
|
</script>
|
|
92
8
|
|
|
93
9
|
<template>
|
|
94
|
-
<div
|
|
95
|
-
<div v-if="loading">
|
|
96
|
-
<USkeleton class="h-32 w-full mb-4" />
|
|
97
|
-
<USkeleton class="h-64 w-full" />
|
|
98
|
-
</div>
|
|
99
|
-
|
|
100
|
-
<div v-else-if="request" class="space-y-6">
|
|
101
|
-
<!-- Full request details with admin controls -->
|
|
102
|
-
<CustomerRequestDetail :request-id="request.id" />
|
|
103
|
-
|
|
104
|
-
<USeparator />
|
|
105
|
-
|
|
106
|
-
<!-- Admin Actions -->
|
|
107
|
-
<UCard>
|
|
108
|
-
<template #header>
|
|
109
|
-
<h3 class="text-lg font-semibold">{{ t('features.serviceRequests.actions.adminActions') }}</h3>
|
|
110
|
-
</template>
|
|
111
|
-
|
|
112
|
-
<div class="space-y-4">
|
|
113
|
-
<UFormField :label="t('features.serviceRequests.fields.status')">
|
|
114
|
-
<USelect v-model="adminUpdates.status" :options="statusOptions" @change="handleQuickUpdate" />
|
|
115
|
-
</UFormField>
|
|
116
|
-
|
|
117
|
-
<UFormField :label="t('features.serviceRequests.fields.priority')">
|
|
118
|
-
<USelect v-model="adminUpdates.priority" :options="priorityOptions" @change="handleQuickUpdate" />
|
|
119
|
-
</UFormField>
|
|
120
|
-
|
|
121
|
-
<UFormField :label="t('features.serviceRequests.fields.assignedTo')">
|
|
122
|
-
<USelect v-model="adminUpdates.assignedToId" :options="userOptions" @change="handleQuickUpdate" />
|
|
123
|
-
</UFormField>
|
|
124
|
-
|
|
125
|
-
<UFormField :label="t('features.serviceRequests.fields.internalNotes')">
|
|
126
|
-
<UTextarea
|
|
127
|
-
v-model="adminUpdates.internalNotes"
|
|
128
|
-
:rows="4"
|
|
129
|
-
:placeholder="t('features.serviceRequests.placeholders.internalNotes')"
|
|
130
|
-
/>
|
|
131
|
-
</UFormField>
|
|
132
|
-
|
|
133
|
-
<UButton :loading="updating" @click="handleUpdate">
|
|
134
|
-
{{ t('features.serviceRequests.actions.saveChanges') }}
|
|
135
|
-
</UButton>
|
|
136
|
-
</div>
|
|
137
|
-
</UCard>
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
10
|
+
<div />
|
|
140
11
|
</template>
|
|
@@ -1,55 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
ServiceRequestFilters
|
|
5
|
-
} from '@nuxt-customer-portal/service-requests/shared/types/service-request'
|
|
6
|
-
|
|
7
|
-
const { requests, loading, pagination, stats, fetchAllRequests, adminUpdateRequest } = useAdminServiceRequests()
|
|
8
|
-
|
|
9
|
-
const toast = useToast()
|
|
10
|
-
const { t } = useI18n()
|
|
11
|
-
|
|
12
|
-
useSeoMeta({
|
|
13
|
-
title: () => t('features.serviceRequests.navigation.manageRequests')
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
onMounted(() => {
|
|
17
|
-
fetchAllRequests()
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
const handleFilter = (filters: ServiceRequestFilters) => {
|
|
21
|
-
fetchAllRequests(filters)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const handleUpdate = async ({ id, updates }: { id: string; updates: AdminServiceRequestUpdateInput }) => {
|
|
25
|
-
try {
|
|
26
|
-
await adminUpdateRequest(id, updates)
|
|
27
|
-
toast.add({
|
|
28
|
-
title: t('common.success'),
|
|
29
|
-
description: t('features.serviceRequests.messages.updateSuccess')
|
|
30
|
-
})
|
|
31
|
-
} catch {
|
|
32
|
-
toast.add({
|
|
33
|
-
title: t('common.error'),
|
|
34
|
-
description: t('features.serviceRequests.messages.updateError'),
|
|
35
|
-
color: 'error'
|
|
36
|
-
})
|
|
37
|
-
}
|
|
38
|
-
}
|
|
2
|
+
const route = useRoute()
|
|
3
|
+
await navigateTo({ path: '/requests', query: route.query }, { replace: true })
|
|
39
4
|
</script>
|
|
40
5
|
|
|
41
6
|
<template>
|
|
42
|
-
<div
|
|
43
|
-
<h1 class="text-3xl font-bold mb-6">{{ t('features.serviceRequests.navigation.manageRequests') }}</h1>
|
|
44
|
-
|
|
45
|
-
<AdminRequestDashboard
|
|
46
|
-
:requests="requests"
|
|
47
|
-
:loading="loading"
|
|
48
|
-
:pagination="pagination"
|
|
49
|
-
:stats="stats"
|
|
50
|
-
@select="navigateTo(`/admin/requests/${$event}`)"
|
|
51
|
-
@filter="handleFilter"
|
|
52
|
-
@update="handleUpdate"
|
|
53
|
-
/>
|
|
54
|
-
</div>
|
|
7
|
+
<div />
|
|
55
8
|
</template>
|
|
@@ -16,17 +16,19 @@ const request = ref<ServiceRequestWithRelations | null>(null)
|
|
|
16
16
|
const loading = ref(true)
|
|
17
17
|
const updating = ref(false)
|
|
18
18
|
const showEditModal = ref(false)
|
|
19
|
+
const showDeleteConfirm = ref(false)
|
|
20
|
+
const { can } = useServiceRequestAccess()
|
|
19
21
|
|
|
20
22
|
useSeoMeta({
|
|
21
23
|
title: () => request.value?.title || t('features.serviceRequests.title')
|
|
22
24
|
})
|
|
23
25
|
|
|
24
26
|
const canEdit = computed(() => {
|
|
25
|
-
return request.value?.createdById === currentUser.value?.id
|
|
27
|
+
return request.value?.createdById === currentUser.value?.id || can('update')
|
|
26
28
|
})
|
|
27
29
|
|
|
28
30
|
const canDelete = computed(() => {
|
|
29
|
-
return request.value?.createdById === currentUser.value?.id
|
|
31
|
+
return request.value?.createdById === currentUser.value?.id || can('delete')
|
|
30
32
|
})
|
|
31
33
|
|
|
32
34
|
const backRoute = computed(() => {
|
|
@@ -93,10 +95,6 @@ const handleUpdate = async (data: ServiceRequestUpdateInput) => {
|
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
const handleDelete = async () => {
|
|
96
|
-
if (!confirm('Are you sure you want to delete this request?')) {
|
|
97
|
-
return
|
|
98
|
-
}
|
|
99
|
-
|
|
100
98
|
try {
|
|
101
99
|
await deleteRequest(requestId)
|
|
102
100
|
toast.add({
|
|
@@ -115,7 +113,7 @@ const handleDelete = async () => {
|
|
|
115
113
|
</script>
|
|
116
114
|
|
|
117
115
|
<template>
|
|
118
|
-
<div class="
|
|
116
|
+
<div class="mx-auto h-full min-h-0 w-full max-w-5xl overflow-y-auto px-4 py-5 sm:px-6">
|
|
119
117
|
<div class="mb-4">
|
|
120
118
|
<UButton icon="i-lucide-arrow-left" variant="ghost" size="sm" :to="backRoute">
|
|
121
119
|
{{ t('features.serviceRequests.actions.back') }}
|
|
@@ -132,30 +130,34 @@ const handleDelete = async () => {
|
|
|
132
130
|
<UButton :to="backRoute">{{ t('features.serviceRequests.actions.back') }}</UButton>
|
|
133
131
|
</div>
|
|
134
132
|
|
|
135
|
-
<div v-else>
|
|
133
|
+
<div v-else class="space-y-5">
|
|
136
134
|
<CustomerRequestDetail
|
|
137
|
-
:request
|
|
135
|
+
:request="request"
|
|
138
136
|
:can-edit="canEdit"
|
|
139
137
|
:can-delete="canDelete"
|
|
140
138
|
@edit="showEditModal = true"
|
|
141
|
-
@delete="
|
|
139
|
+
@delete="showDeleteConfirm = true"
|
|
142
140
|
/>
|
|
143
141
|
|
|
144
|
-
|
|
145
|
-
<UModal v-model="showEditModal">
|
|
146
|
-
<
|
|
147
|
-
<template #header>
|
|
148
|
-
<h2 class="text-xl font-bold">{{ t('features.serviceRequests.edit') }}</h2>
|
|
149
|
-
</template>
|
|
150
|
-
|
|
142
|
+
<ServiceRequestManagement v-if="can('manage')" :request="request" @updated="request = $event" />
|
|
143
|
+
<UModal v-model:open="showEditModal" :title="t('features.serviceRequests.edit')">
|
|
144
|
+
<template #body>
|
|
151
145
|
<CustomerRequestForm
|
|
152
146
|
:initial-data="request"
|
|
153
147
|
:loading="updating"
|
|
154
148
|
@submit="handleUpdate"
|
|
155
149
|
@cancel="showEditModal = false"
|
|
156
150
|
/>
|
|
157
|
-
</
|
|
151
|
+
</template>
|
|
158
152
|
</UModal>
|
|
153
|
+
<ConfirmationModal
|
|
154
|
+
v-model:open="showDeleteConfirm"
|
|
155
|
+
title="features.serviceRequests.delete"
|
|
156
|
+
message="features.serviceRequests.confirmDelete"
|
|
157
|
+
confirm-color="error"
|
|
158
|
+
confirm-text="common.delete"
|
|
159
|
+
@confirm="handleDelete"
|
|
160
|
+
/>
|
|
159
161
|
</div>
|
|
160
162
|
</div>
|
|
161
163
|
</template>
|