@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
|
@@ -29,9 +29,15 @@ const handleSubmit = async (data: ServiceRequestCreateInput) => {
|
|
|
29
29
|
</script>
|
|
30
30
|
|
|
31
31
|
<template>
|
|
32
|
-
<div class="
|
|
33
|
-
<
|
|
32
|
+
<div class="mx-auto h-full min-h-0 w-full max-w-3xl overflow-y-auto px-4 py-5 sm:px-6">
|
|
33
|
+
<UButton to="/requests" icon="i-lucide-arrow-left" color="neutral" variant="ghost" class="mb-4">{{
|
|
34
|
+
t('features.serviceRequests.actions.back')
|
|
35
|
+
}}</UButton>
|
|
36
|
+
<h1 class="mb-2 flex items-center gap-3 text-xl font-semibold">
|
|
37
|
+
<UIcon name="i-lucide-ticket" class="size-5 text-primary" />{{ t('features.serviceRequests.create') }}
|
|
38
|
+
</h1>
|
|
34
39
|
|
|
35
|
-
<
|
|
40
|
+
<p class="mb-6 text-sm text-muted">{{ t('features.serviceRequests.list.createDescription') }}</p>
|
|
41
|
+
<UCard><CustomerRequestForm :loading="loading" @submit="handleSubmit" @cancel="navigateTo('/requests')" /></UCard>
|
|
36
42
|
</div>
|
|
37
43
|
</template>
|
package/i18n/locales/en.json
CHANGED
|
@@ -91,7 +91,28 @@
|
|
|
91
91
|
"empty": "No service requests found",
|
|
92
92
|
"endOfList": "You've reached the end"
|
|
93
93
|
},
|
|
94
|
-
"confirmDelete": "Are you sure you want to delete this service request? This action cannot be undone."
|
|
94
|
+
"confirmDelete": "Are you sure you want to delete this service request? This action cannot be undone.",
|
|
95
|
+
"list": {
|
|
96
|
+
"description": "Share requests, follow progress and keep track of the work for your organization.",
|
|
97
|
+
"adminDescription": "Triage requests, set priorities and coordinate the work across your clients.",
|
|
98
|
+
"search": "Search requests…",
|
|
99
|
+
"noMatches": "No matching requests",
|
|
100
|
+
"noMatchesDescription": "Try another search or adjust your filters.",
|
|
101
|
+
"emptyTitle": "No service requests yet",
|
|
102
|
+
"emptyDescription": "Create a request to share a question, report an issue or suggest an improvement.",
|
|
103
|
+
"createFirst": "Create your first request",
|
|
104
|
+
"createDescription": "Describe what you need and add enough context for the team to get started.",
|
|
105
|
+
"loadPrevious": "Load previous requests",
|
|
106
|
+
"unassigned": "Unassigned",
|
|
107
|
+
"previousAssignee": "Previously assigned team member"
|
|
108
|
+
},
|
|
109
|
+
"validation": {
|
|
110
|
+
"client": "Select a client.",
|
|
111
|
+
"title": "Enter a title between 3 and 200 characters.",
|
|
112
|
+
"description": "Enter a description between 10 and 5,000 characters.",
|
|
113
|
+
"category": "Use no more than 100 characters.",
|
|
114
|
+
"notes": "Use no more than 5,000 characters."
|
|
115
|
+
}
|
|
95
116
|
}
|
|
96
117
|
}
|
|
97
118
|
}
|
package/i18n/locales/nl.json
CHANGED
|
@@ -91,7 +91,28 @@
|
|
|
91
91
|
"empty": "Geen serviceaanvragen gevonden",
|
|
92
92
|
"endOfList": "Je hebt het einde bereikt"
|
|
93
93
|
},
|
|
94
|
-
"confirmDelete": "Weet je zeker dat je deze serviceaanvraag wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt."
|
|
94
|
+
"confirmDelete": "Weet je zeker dat je deze serviceaanvraag wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
|
|
95
|
+
"list": {
|
|
96
|
+
"description": "Deel verzoeken, volg de voortgang en houd overzicht over het werk voor uw organisatie.",
|
|
97
|
+
"adminDescription": "Beoordeel verzoeken, stel prioriteiten en coördineer het werk voor uw klanten.",
|
|
98
|
+
"search": "Zoek verzoeken…",
|
|
99
|
+
"noMatches": "Geen overeenkomende verzoeken",
|
|
100
|
+
"noMatchesDescription": "Probeer een andere zoekterm of pas uw filters aan.",
|
|
101
|
+
"emptyTitle": "Nog geen serviceverzoeken",
|
|
102
|
+
"emptyDescription": "Maak een verzoek aan om een vraag te stellen, een probleem te melden of een verbetering voor te stellen.",
|
|
103
|
+
"createFirst": "Maak uw eerste verzoek",
|
|
104
|
+
"createDescription": "Beschrijf wat u nodig heeft en geef het team voldoende informatie om te beginnen.",
|
|
105
|
+
"loadPrevious": "Eerdere verzoeken laden",
|
|
106
|
+
"unassigned": "Niet toegewezen",
|
|
107
|
+
"previousAssignee": "Eerder toegewezen teamlid"
|
|
108
|
+
},
|
|
109
|
+
"validation": {
|
|
110
|
+
"client": "Selecteer een klant.",
|
|
111
|
+
"title": "Voer een titel van 3 tot 200 tekens in.",
|
|
112
|
+
"description": "Voer een beschrijving van 10 tot 5.000 tekens in.",
|
|
113
|
+
"category": "Gebruik maximaal 100 tekens.",
|
|
114
|
+
"notes": "Gebruik maximaal 5.000 tekens."
|
|
115
|
+
}
|
|
95
116
|
}
|
|
96
117
|
}
|
|
97
118
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuxt-customer-portal/service-requests",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://nuxt-customer-portal.com",
|
|
6
6
|
"repository": {
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"nanoid": "^5.1.6",
|
|
42
42
|
"pg": "^8.22.0",
|
|
43
43
|
"zod": "^4.4.3",
|
|
44
|
-
"@nuxt-customer-portal/core": "^0.3.
|
|
45
|
-
"@nuxt-customer-portal/clients": "^0.3.
|
|
44
|
+
"@nuxt-customer-portal/core": "^0.3.2",
|
|
45
|
+
"@nuxt-customer-portal/clients": "^0.3.2"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"nuxt": "^4.5.1"
|
package/portal.manifest.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
findServiceRequest,
|
|
3
|
+
listServiceRequestAssignees,
|
|
3
4
|
toServiceRequestDto,
|
|
4
5
|
updateServiceRequest
|
|
5
6
|
} from '@nuxt-customer-portal/service-requests/server/utils/service-request-repository'
|
|
@@ -31,8 +32,15 @@ export default defineEventHandler(async (event) => {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
const data = adminUpdateServiceRequestSchema.parse(await readBody(event))
|
|
35
|
+
if (data.assignedToId && data.assignedToId !== existing.assignedToId) {
|
|
36
|
+
const assignees = await listServiceRequestAssignees(scope.providerOrganizationId)
|
|
37
|
+
if (!assignees.some((user) => user.id === data.assignedToId)) {
|
|
38
|
+
throw createError({ statusCode: 400, message: 'Assignee must belong to the provider organization' })
|
|
39
|
+
}
|
|
40
|
+
}
|
|
34
41
|
const row = await updateServiceRequest(id, {
|
|
35
42
|
...data,
|
|
43
|
+
...(data.assignedToId !== undefined ? { assignedToId: data.assignedToId || null } : {}),
|
|
36
44
|
resolvedAt: data.status === 'RESOLVED' ? new Date() : existing.resolvedAt,
|
|
37
45
|
closedAt: data.status === 'CLOSED' ? new Date() : existing.closedAt
|
|
38
46
|
})
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { requireServiceRequestScope } from '@nuxt-customer-portal/service-requests/server/utils/service-request-scope'
|
|
2
|
+
import { listServiceRequestAssignees } from '@nuxt-customer-portal/service-requests/server/utils/service-request-repository'
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const scope = await requireServiceRequestScope(event, 'manage')
|
|
6
|
+
return listServiceRequestAssignees(scope.providerOrganizationId)
|
|
7
|
+
})
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
type NewServiceRequestRecord,
|
|
6
6
|
type ServiceRequestRecord
|
|
7
7
|
} from '@nuxt-customer-portal/service-requests/server/db/schema/service-requests'
|
|
8
|
-
import { organization } from '@nuxt-customer-portal/core/schema'
|
|
8
|
+
import { organization, member, user } from '@nuxt-customer-portal/core/schema'
|
|
9
9
|
import { alias } from 'drizzle-orm/pg-core'
|
|
10
10
|
import type {
|
|
11
11
|
ServiceRequestDashboardDto,
|
|
@@ -97,7 +97,7 @@ export const listServiceRequests = async (
|
|
|
97
97
|
? priorityRank
|
|
98
98
|
: serviceRequest.createdAt
|
|
99
99
|
|
|
100
|
-
const [rows, total] = await Promise.all([
|
|
100
|
+
const [rows, total, categories] = await Promise.all([
|
|
101
101
|
db
|
|
102
102
|
.select({ request: serviceRequest, clientName: clientOrganization.name })
|
|
103
103
|
.from(serviceRequest)
|
|
@@ -106,11 +106,25 @@ export const listServiceRequests = async (
|
|
|
106
106
|
.orderBy(filters.sortDir === 'asc' ? asc(orderExpression) : desc(orderExpression), asc(serviceRequest.id))
|
|
107
107
|
.offset(filters.offset)
|
|
108
108
|
.limit(filters.pageSize),
|
|
109
|
-
db.$count(serviceRequest, where)
|
|
109
|
+
db.$count(serviceRequest, where),
|
|
110
|
+
db
|
|
111
|
+
.selectDistinct({ category: serviceRequest.category })
|
|
112
|
+
.from(serviceRequest)
|
|
113
|
+
.where(
|
|
114
|
+
and(
|
|
115
|
+
...filterConditions(
|
|
116
|
+
organizationId,
|
|
117
|
+
{ page: 1, pageSize: 20, offset: 0, sortBy: 'createdAt', sortDir: 'desc' },
|
|
118
|
+
scope
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
.orderBy(asc(serviceRequest.category))
|
|
110
123
|
])
|
|
111
124
|
|
|
112
125
|
return {
|
|
113
126
|
items: rows.map((row) => toServiceRequestDto(row.request, row.clientName)),
|
|
127
|
+
categories: categories.flatMap((row) => (row.category ? [row.category] : [])),
|
|
114
128
|
pagination: {
|
|
115
129
|
total,
|
|
116
130
|
page: filters.page,
|
|
@@ -183,3 +197,11 @@ export const updateServiceRequest = async (id: string, values: Partial<NewServic
|
|
|
183
197
|
}
|
|
184
198
|
|
|
185
199
|
export const deleteServiceRequest = (id: string) => db.delete(serviceRequest).where(eq(serviceRequest.id, id))
|
|
200
|
+
|
|
201
|
+
export const listServiceRequestAssignees = (organizationId: string) =>
|
|
202
|
+
db
|
|
203
|
+
.select({ id: user.id, name: user.name, image: user.image })
|
|
204
|
+
.from(member)
|
|
205
|
+
.innerJoin(user, eq(user.id, member.userId))
|
|
206
|
+
.where(eq(member.organizationId, organizationId))
|
|
207
|
+
.orderBy(asc(user.name), asc(user.id))
|
|
@@ -2,18 +2,18 @@ import { z } from 'zod'
|
|
|
2
2
|
|
|
3
3
|
export const createServiceRequestSchema = z.object({
|
|
4
4
|
clientOrganizationId: z.string().min(1).max(128).optional(),
|
|
5
|
-
title: z.string().min(3).max(200),
|
|
6
|
-
description: z.string().min(10).max(5000),
|
|
5
|
+
title: z.string().trim().min(3).max(200),
|
|
6
|
+
description: z.string().trim().min(10).max(5000),
|
|
7
7
|
priority: z.enum(['LOW', 'MEDIUM', 'HIGH', 'URGENT']).optional(),
|
|
8
|
-
category: z.string().max(100).optional()
|
|
8
|
+
category: z.string().trim().max(100).optional()
|
|
9
9
|
})
|
|
10
10
|
|
|
11
11
|
export const updateServiceRequestSchema = z.object({
|
|
12
|
-
title: z.string().min(3).max(200).optional(),
|
|
13
|
-
description: z.string().min(10).max(5000).optional(),
|
|
12
|
+
title: z.string().trim().min(3).max(200).optional(),
|
|
13
|
+
description: z.string().trim().min(10).max(5000).optional(),
|
|
14
14
|
status: z.enum(['OPEN', 'IN_PROGRESS', 'RESOLVED', 'CLOSED']).optional(),
|
|
15
15
|
priority: z.enum(['LOW', 'MEDIUM', 'HIGH', 'URGENT']).optional(),
|
|
16
|
-
category: z.string().max(100).optional()
|
|
16
|
+
category: z.string().trim().max(100).optional()
|
|
17
17
|
})
|
|
18
18
|
|
|
19
19
|
export const adminUpdateServiceRequestSchema = updateServiceRequestSchema.extend({
|
package/shared/feature.ts
CHANGED
|
@@ -12,16 +12,7 @@ export const serviceRequestFeature: PortalFeatureDefinition<ServiceRequestAction
|
|
|
12
12
|
icon: 'i-lucide-ticket',
|
|
13
13
|
to: '/requests',
|
|
14
14
|
audiences: ['authenticated'],
|
|
15
|
-
order:
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
id: 'service-requests-admin',
|
|
19
|
-
labelKey: 'features.serviceRequests.navigation.manageRequests',
|
|
20
|
-
icon: 'i-lucide-list-checks',
|
|
21
|
-
to: '/admin/requests',
|
|
22
|
-
audiences: ['providerAdmin'],
|
|
23
|
-
location: 'admin',
|
|
24
|
-
order: 120
|
|
15
|
+
order: 40
|
|
25
16
|
}
|
|
26
17
|
],
|
|
27
18
|
modules: [
|
|
@@ -32,7 +23,7 @@ export const serviceRequestFeature: PortalFeatureDefinition<ServiceRequestAction
|
|
|
32
23
|
to: '/requests',
|
|
33
24
|
routePrefixes: ['/requests', '/admin/requests'],
|
|
34
25
|
audiences: ['authenticated'],
|
|
35
|
-
order:
|
|
26
|
+
order: 40,
|
|
36
27
|
menuItems: [
|
|
37
28
|
{
|
|
38
29
|
id: 'requests',
|
|
@@ -40,20 +31,6 @@ export const serviceRequestFeature: PortalFeatureDefinition<ServiceRequestAction
|
|
|
40
31
|
icon: 'i-lucide-inbox',
|
|
41
32
|
to: '/requests',
|
|
42
33
|
audiences: ['authenticated']
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
id: 'new-request',
|
|
46
|
-
labelKey: 'features.serviceRequests.navigation.newRequest',
|
|
47
|
-
icon: 'i-lucide-plus',
|
|
48
|
-
to: '/requests/new',
|
|
49
|
-
audiences: ['authenticated']
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
id: 'manage-requests',
|
|
53
|
-
labelKey: 'features.serviceRequests.navigation.manageRequests',
|
|
54
|
-
icon: 'i-lucide-list-checks',
|
|
55
|
-
to: '/admin/requests',
|
|
56
|
-
audiences: ['providerAdmin']
|
|
57
34
|
}
|
|
58
35
|
]
|
|
59
36
|
}
|
|
@@ -64,14 +41,14 @@ export const serviceRequestFeature: PortalFeatureDefinition<ServiceRequestAction
|
|
|
64
41
|
component: 'ServiceRequestsDashboardAttention',
|
|
65
42
|
area: 'attention',
|
|
66
43
|
size: 'half',
|
|
67
|
-
order:
|
|
44
|
+
order: 60
|
|
68
45
|
},
|
|
69
46
|
{
|
|
70
47
|
id: 'service-requests-overview',
|
|
71
48
|
component: 'ServiceRequestsDashboardOverview',
|
|
72
49
|
area: 'aside',
|
|
73
50
|
size: 'full',
|
|
74
|
-
order:
|
|
51
|
+
order: 50
|
|
75
52
|
}
|
|
76
53
|
],
|
|
77
54
|
clientIntegration: { moduleId: 'service-requests', labelKey: 'features.serviceRequests.navigation.myRequests' },
|