@nuxt-customer-portal/service-requests 0.3.0

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +21 -0
  3. package/README.md +16 -0
  4. package/app/components/Admin/RequestDashboard.vue +64 -0
  5. package/app/components/Admin/RequestTable.vue +158 -0
  6. package/app/components/CustomerRequestDetail.vue +76 -0
  7. package/app/components/CustomerRequestForm.vue +143 -0
  8. package/app/components/ServiceRequestsDashboardAttention.vue +55 -0
  9. package/app/components/ServiceRequestsDashboardOverview.vue +55 -0
  10. package/app/components/StatusBadge.vue +16 -0
  11. package/app/composables/useAdminServiceRequests.ts +60 -0
  12. package/app/composables/usePaginatedServiceRequests.ts +21 -0
  13. package/app/composables/useServiceRequestAccess.ts +18 -0
  14. package/app/composables/useServiceRequestMenu.ts +34 -0
  15. package/app/composables/useServiceRequests.ts +134 -0
  16. package/app/composables/useServiceRequestsDashboard.ts +13 -0
  17. package/app/pages/admin/requests/[id].vue +140 -0
  18. package/app/pages/admin/requests/index.vue +55 -0
  19. package/app/pages/requests/[id].vue +161 -0
  20. package/app/pages/requests/index.vue +873 -0
  21. package/app/pages/requests/new.vue +37 -0
  22. package/app/plugins/service-requests-feature.ts +18 -0
  23. package/i18n/locales/en.json +97 -0
  24. package/i18n/locales/nl.json +97 -0
  25. package/migrations/0000_baseline.sql +30 -0
  26. package/migrations/0001_required_client.sql +6 -0
  27. package/migrations/meta/0000_snapshot.json +245 -0
  28. package/migrations/meta/_journal.json +13 -0
  29. package/nuxt.config.ts +29 -0
  30. package/package.json +53 -0
  31. package/portal.manifest.mjs +9 -0
  32. package/server/api/service-requests/[id].delete.ts +42 -0
  33. package/server/api/service-requests/[id].get.ts +39 -0
  34. package/server/api/service-requests/[id].patch.ts +44 -0
  35. package/server/api/service-requests/admin/[id].patch.ts +43 -0
  36. package/server/api/service-requests/admin/index.get.ts +19 -0
  37. package/server/api/service-requests/dashboard.get.ts +11 -0
  38. package/server/api/service-requests/index.get.ts +22 -0
  39. package/server/api/service-requests/index.post.ts +39 -0
  40. package/server/db/schema/service-requests.ts +78 -0
  41. package/server/plugins/openapi-contracts.ts +22 -0
  42. package/server/utils/service-request-repository.ts +185 -0
  43. package/server/utils/service-request-scope.ts +41 -0
  44. package/server/utils/service-request-validation.ts +54 -0
  45. package/shared/feature.ts +90 -0
  46. package/shared/types/service-request.ts +111 -0
@@ -0,0 +1,60 @@
1
+ import type {
2
+ AdminServiceRequestUpdateInput,
3
+ ServiceRequest,
4
+ ServiceRequestFilters,
5
+ ServiceRequestListResponse
6
+ } from '@nuxt-customer-portal/service-requests/shared/types/service-request'
7
+
8
+ export const useAdminServiceRequests = () => {
9
+ const requests = ref<ServiceRequest[]>([])
10
+ const loading = ref(false)
11
+ const error = ref<string | null>(null)
12
+ const pagination = ref({ total: 0, page: 1, pageSize: 20, pageCount: 0 })
13
+ const stats = computed(() =>
14
+ requests.value.reduce<Record<string, number>>((result, request) => {
15
+ result[request.status] = (result[request.status] ?? 0) + 1
16
+ return result
17
+ }, {})
18
+ )
19
+
20
+ const fetchAllRequests = async (filters: ServiceRequestFilters = {}) => {
21
+ loading.value = true
22
+ error.value = null
23
+ try {
24
+ const response = await $fetch<ServiceRequestListResponse>('/api/service-requests/admin', { query: filters })
25
+ requests.value = response.items
26
+ pagination.value = response.pagination
27
+ } catch (cause) {
28
+ error.value = cause instanceof Error ? cause.message : 'Failed to fetch service requests'
29
+ throw cause
30
+ } finally {
31
+ loading.value = false
32
+ }
33
+ }
34
+
35
+ const adminUpdateRequest = async (id: string, data: AdminServiceRequestUpdateInput) => {
36
+ const updated = await $fetch<ServiceRequest>(`/api/service-requests/admin/${id}`, {
37
+ method: 'PATCH',
38
+ body: data
39
+ })
40
+ const index = requests.value.findIndex((request) => request.id === id)
41
+ if (index >= 0) {
42
+ requests.value[index] = updated
43
+ }
44
+ return updated
45
+ }
46
+
47
+ return {
48
+ requests: readonly(requests),
49
+ loading: readonly(loading),
50
+ error: readonly(error),
51
+ pagination: readonly(pagination),
52
+ stats,
53
+ fetchAllRequests,
54
+ adminUpdateRequest,
55
+ assignRequest: (id: string, userId: string) => adminUpdateRequest(id, { assignedToId: userId }),
56
+ resolveRequest: (id: string) => adminUpdateRequest(id, { status: 'RESOLVED' }),
57
+ closeRequest: (id: string) => adminUpdateRequest(id, { status: 'CLOSED' }),
58
+ reopenRequest: (id: string) => adminUpdateRequest(id, { status: 'OPEN' })
59
+ }
60
+ }
@@ -0,0 +1,21 @@
1
+ import type {
2
+ ServiceRequest,
3
+ ServiceRequestFilters,
4
+ ServiceRequestListResponse
5
+ } from '@nuxt-customer-portal/service-requests/shared/types/service-request'
6
+ import { usePaginatedResource } from '@nuxt-customer-portal/core/app/composables/usePaginatedResource'
7
+
8
+ export const usePaginatedServiceRequests = (pageSize = 20) =>
9
+ usePaginatedResource<ServiceRequest, ServiceRequestFilters>({
10
+ pageSize,
11
+ getKey: (request) => request.id,
12
+ fetchPage: ({ filters, page, pageSize, signal }) =>
13
+ $fetch<ServiceRequestListResponse>('/api/service-requests', {
14
+ query: {
15
+ ...filters,
16
+ page,
17
+ pageSize
18
+ },
19
+ signal
20
+ })
21
+ })
@@ -0,0 +1,18 @@
1
+ import type { ServiceRequestAction } from '@nuxt-customer-portal/service-requests/shared/feature'
2
+ import { serviceRequestFeature } from '@nuxt-customer-portal/service-requests/shared/feature'
3
+ import { isPortalActionAllowed } from '@nuxt-customer-portal/core/shared/feature-registry'
4
+ import type { PortalOrganizationRole } from '@nuxt-customer-portal/core/shared/types/feature'
5
+
6
+ export const useServiceRequestAccess = () => {
7
+ const { activeOrganizationRole, activeOrganizationType } = usePortalSession()
8
+
9
+ const can = (action: ServiceRequestAction) =>
10
+ isPortalActionAllowed(
11
+ serviceRequestFeature.policy,
12
+ activeOrganizationRole.value as PortalOrganizationRole | null,
13
+ action,
14
+ activeOrganizationType.value ?? 'PROVIDER'
15
+ )
16
+
17
+ return { can }
18
+ }
@@ -0,0 +1,34 @@
1
+ export const useServiceRequestMenu = () => {
2
+ const { activeOrganizationRole } = usePortalSession()
3
+ const { t } = useI18n()
4
+
5
+ const isOrganizationAdmin = computed(() => {
6
+ const role = activeOrganizationRole.value
7
+ return role === 'owner' || role === 'admin'
8
+ })
9
+
10
+ const menuItems = computed(() => {
11
+ const items = [
12
+ {
13
+ label: t('features.serviceRequests.navigation.myRequests'),
14
+ to: '/requests',
15
+ icon: 'i-lucide-ticket'
16
+ }
17
+ ]
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
+ return items
28
+ })
29
+
30
+ return {
31
+ menuItems: readonly(menuItems),
32
+ isOrganizationAdmin: readonly(isOrganizationAdmin)
33
+ }
34
+ }
@@ -0,0 +1,134 @@
1
+ import type {
2
+ ServiceRequest,
3
+ ServiceRequestCreateInput,
4
+ ServiceRequestFilters,
5
+ ServiceRequestListResponse,
6
+ ServiceRequestPriority,
7
+ ServiceRequestStatus,
8
+ ServiceRequestUpdateInput
9
+ } from '@nuxt-customer-portal/service-requests/shared/types/service-request'
10
+
11
+ export const useServiceRequests = () => {
12
+ const { t } = useI18n()
13
+ const requests = ref<ServiceRequest[]>([])
14
+ const loading = ref(false)
15
+ const error = ref<string | null>(null)
16
+ const pagination = ref({ total: 0, page: 1, pageSize: 20, pageCount: 0 })
17
+
18
+ const fetchRequests = async (filters: ServiceRequestFilters = {}) => {
19
+ loading.value = true
20
+ error.value = null
21
+ try {
22
+ const response = await $fetch<ServiceRequestListResponse>('/api/service-requests', { query: filters })
23
+ requests.value = response.items
24
+ pagination.value = response.pagination
25
+ return response
26
+ } catch (cause) {
27
+ error.value = cause instanceof Error ? cause.message : t('features.serviceRequests.messages.fetchError')
28
+ throw cause
29
+ } finally {
30
+ loading.value = false
31
+ }
32
+ }
33
+
34
+ const createRequest = async (data: ServiceRequestCreateInput) => {
35
+ loading.value = true
36
+ try {
37
+ const created = await $fetch<ServiceRequest>('/api/service-requests', { method: 'POST', body: data })
38
+ requests.value.unshift(created)
39
+ return created
40
+ } finally {
41
+ loading.value = false
42
+ }
43
+ }
44
+
45
+ const updateRequest = async (id: string, data: ServiceRequestUpdateInput) => {
46
+ loading.value = true
47
+ try {
48
+ const updated = await $fetch<ServiceRequest>(`/api/service-requests/${id}`, { method: 'PATCH', body: data })
49
+ const index = requests.value.findIndex((request) => request.id === id)
50
+ if (index >= 0) {
51
+ requests.value[index] = updated
52
+ }
53
+ return updated
54
+ } finally {
55
+ loading.value = false
56
+ }
57
+ }
58
+
59
+ const deleteRequest = async (id: string) => {
60
+ await $fetch(`/api/service-requests/${id}`, { method: 'DELETE' as never })
61
+ requests.value = requests.value.filter((request) => request.id !== id)
62
+ }
63
+
64
+ const getRequest = (id: string) => $fetch<ServiceRequest>(`/api/service-requests/${id}`)
65
+ const getStatusBadgeText = (status: ServiceRequestStatus) =>
66
+ t(`features.serviceRequests.status.${status.toLowerCase()}`)
67
+ const getPriorityBadgeText = (priority: ServiceRequestPriority) =>
68
+ t(`features.serviceRequests.priority.${priority.toLowerCase()}`)
69
+ const getPriorityColor = (priority: ServiceRequestPriority) =>
70
+ (
71
+ ({
72
+ LOW: 'success',
73
+ MEDIUM: 'info',
74
+ HIGH: 'warning',
75
+ URGENT: 'error'
76
+ }) as const
77
+ )[priority]
78
+ const getStatusColor = (status: ServiceRequestStatus) =>
79
+ (
80
+ ({
81
+ OPEN: 'primary',
82
+ IN_PROGRESS: 'warning',
83
+ RESOLVED: 'success',
84
+ CLOSED: 'neutral'
85
+ }) as const
86
+ )[status]
87
+
88
+ const statusOptions = computed(() => [
89
+ {
90
+ label: t('features.serviceRequests.filters.allStatuses'),
91
+ value: undefined,
92
+ badgeText: '',
93
+ badgeColor: 'neutral' as const
94
+ },
95
+ ...(['OPEN', 'IN_PROGRESS', 'RESOLVED', 'CLOSED'] as const).map((value) => ({
96
+ label: getStatusBadgeText(value),
97
+ value,
98
+ badgeText: getStatusBadgeText(value),
99
+ badgeColor: getStatusColor(value)
100
+ }))
101
+ ])
102
+ const priorityOptions = computed(() => [
103
+ {
104
+ label: t('features.serviceRequests.filters.allPriorities'),
105
+ value: undefined,
106
+ badgeText: '',
107
+ badgeColor: 'neutral' as const
108
+ },
109
+ ...(['LOW', 'MEDIUM', 'HIGH', 'URGENT'] as const).map((value) => ({
110
+ label: getPriorityBadgeText(value),
111
+ value,
112
+ badgeText: getPriorityBadgeText(value),
113
+ badgeColor: getPriorityColor(value)
114
+ }))
115
+ ])
116
+
117
+ return {
118
+ requests: readonly(requests),
119
+ loading: readonly(loading),
120
+ error: readonly(error),
121
+ pagination: readonly(pagination),
122
+ fetchRequests,
123
+ createRequest,
124
+ updateRequest,
125
+ deleteRequest,
126
+ getRequest,
127
+ getStatusColor,
128
+ getPriorityBadgeText,
129
+ getPriorityColor,
130
+ getStatusBadgeText,
131
+ statusOptions,
132
+ priorityOptions
133
+ }
134
+ }
@@ -0,0 +1,13 @@
1
+ import type { ServiceRequestDashboardDto } from '@nuxt-customer-portal/service-requests/shared/types/service-request'
2
+
3
+ export const useServiceRequestsDashboard = () => {
4
+ const { activeOrganizationId } = usePortalSession()
5
+ return useAsyncData(
6
+ 'service-requests-dashboard',
7
+ () => $fetch<ServiceRequestDashboardDto>('/api/service-requests/dashboard'),
8
+ {
9
+ watch: [activeOrganizationId],
10
+ dedupe: 'defer'
11
+ }
12
+ )
13
+ }
@@ -0,0 +1,140 @@
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
+ const route = useRoute()
8
+ const { t } = useI18n()
9
+ const requestId = route.params.id as string
10
+
11
+ const { adminUpdateRequest } = useAdminServiceRequests()
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
+ }
91
+ </script>
92
+
93
+ <template>
94
+ <div class="container mx-auto py-8 max-w-4xl">
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>
140
+ </template>
@@ -0,0 +1,55 @@
1
+ <script setup lang="ts">
2
+ import type {
3
+ AdminServiceRequestUpdateInput,
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
+ }
39
+ </script>
40
+
41
+ <template>
42
+ <div class="container mx-auto py-8">
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>
55
+ </template>
@@ -0,0 +1,161 @@
1
+ <script setup lang="ts">
2
+ import type {
3
+ ServiceRequestUpdateInput,
4
+ ServiceRequestWithRelations
5
+ } from '@nuxt-customer-portal/service-requests/shared/types/service-request'
6
+
7
+ const route = useRoute()
8
+ const { t } = useI18n()
9
+ const requestId = route.params.id as string
10
+
11
+ const { getRequest, updateRequest, deleteRequest } = useServiceRequests()
12
+ const { currentUser } = usePortalSession()
13
+ const toast = useToast()
14
+
15
+ const request = ref<ServiceRequestWithRelations | null>(null)
16
+ const loading = ref(true)
17
+ const updating = ref(false)
18
+ const showEditModal = ref(false)
19
+
20
+ useSeoMeta({
21
+ title: () => request.value?.title || t('features.serviceRequests.title')
22
+ })
23
+
24
+ const canEdit = computed(() => {
25
+ return request.value?.createdById === currentUser.value?.id
26
+ })
27
+
28
+ const canDelete = computed(() => {
29
+ return request.value?.createdById === currentUser.value?.id
30
+ })
31
+
32
+ const backRoute = computed(() => {
33
+ if (route.query.from !== 'list') {
34
+ return '/requests'
35
+ }
36
+ const query: Record<string, string> = {}
37
+ if (route.query.search != null && route.query.search !== '') {
38
+ query.search = String(route.query.search)
39
+ }
40
+ if (route.query.status != null && route.query.status !== '') {
41
+ query.status = String(route.query.status)
42
+ }
43
+ if (route.query.priority != null && route.query.priority !== '') {
44
+ query.priority = String(route.query.priority)
45
+ }
46
+ if (route.query.category != null && route.query.category !== '') {
47
+ query.category = String(route.query.category)
48
+ }
49
+ if (route.query.sortBy != null && route.query.sortBy !== '') {
50
+ query.sortBy = String(route.query.sortBy)
51
+ }
52
+ if (route.query.sortDir != null && route.query.sortDir !== '') {
53
+ query.sortDir = String(route.query.sortDir)
54
+ }
55
+ if (route.query.page != null && route.query.page !== '') {
56
+ query.page = String(route.query.page)
57
+ }
58
+ return { path: '/requests', query }
59
+ })
60
+
61
+ onMounted(async () => {
62
+ try {
63
+ request.value = await getRequest(requestId)
64
+ } catch {
65
+ toast.add({
66
+ title: t('common.error'),
67
+ description: t('features.serviceRequests.messages.fetchError'),
68
+ color: 'error'
69
+ })
70
+ } finally {
71
+ loading.value = false
72
+ }
73
+ })
74
+
75
+ const handleUpdate = async (data: ServiceRequestUpdateInput) => {
76
+ updating.value = true
77
+ try {
78
+ request.value = await updateRequest(requestId, data)
79
+ showEditModal.value = false
80
+ toast.add({
81
+ title: t('common.success'),
82
+ description: t('features.serviceRequests.messages.updateSuccess')
83
+ })
84
+ } catch {
85
+ toast.add({
86
+ title: t('common.error'),
87
+ description: t('features.serviceRequests.messages.updateError'),
88
+ color: 'error'
89
+ })
90
+ } finally {
91
+ updating.value = false
92
+ }
93
+ }
94
+
95
+ const handleDelete = async () => {
96
+ if (!confirm('Are you sure you want to delete this request?')) {
97
+ return
98
+ }
99
+
100
+ try {
101
+ await deleteRequest(requestId)
102
+ toast.add({
103
+ title: t('common.success'),
104
+ description: t('features.serviceRequests.messages.deleteSuccess')
105
+ })
106
+ navigateTo(backRoute.value)
107
+ } catch {
108
+ toast.add({
109
+ title: t('common.error'),
110
+ description: t('features.serviceRequests.messages.deleteError'),
111
+ color: 'error'
112
+ })
113
+ }
114
+ }
115
+ </script>
116
+
117
+ <template>
118
+ <div class="container mx-auto py-8 max-w-4xl">
119
+ <div class="mb-4">
120
+ <UButton icon="i-lucide-arrow-left" variant="ghost" size="sm" :to="backRoute">
121
+ {{ t('features.serviceRequests.actions.back') }}
122
+ </UButton>
123
+ </div>
124
+
125
+ <div v-if="loading">
126
+ <USkeleton class="h-32 w-full mb-4" />
127
+ <USkeleton class="h-64 w-full" />
128
+ </div>
129
+
130
+ <div v-else-if="!request" class="text-center py-8">
131
+ <p>{{ t('features.serviceRequests.messages.notFound') }}</p>
132
+ <UButton :to="backRoute">{{ t('features.serviceRequests.actions.back') }}</UButton>
133
+ </div>
134
+
135
+ <div v-else>
136
+ <CustomerRequestDetail
137
+ :request-id="request.id"
138
+ :can-edit="canEdit"
139
+ :can-delete="canDelete"
140
+ @edit="showEditModal = true"
141
+ @delete="handleDelete"
142
+ />
143
+
144
+ <!-- Edit Modal -->
145
+ <UModal v-model="showEditModal">
146
+ <UCard>
147
+ <template #header>
148
+ <h2 class="text-xl font-bold">{{ t('features.serviceRequests.edit') }}</h2>
149
+ </template>
150
+
151
+ <CustomerRequestForm
152
+ :initial-data="request"
153
+ :loading="updating"
154
+ @submit="handleUpdate"
155
+ @cancel="showEditModal = false"
156
+ />
157
+ </UCard>
158
+ </UModal>
159
+ </div>
160
+ </div>
161
+ </template>