@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.
@@ -29,9 +29,15 @@ const handleSubmit = async (data: ServiceRequestCreateInput) => {
29
29
  </script>
30
30
 
31
31
  <template>
32
- <div class="container mx-auto py-8 max-w-2xl">
33
- <h1 class="text-3xl font-bold mb-6">{{ t('features.serviceRequests.create') }}</h1>
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
- <CustomerRequestForm :loading="loading" @submit="handleSubmit" @cancel="navigateTo('/requests')" />
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>
@@ -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
  }
@@ -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.0",
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.0",
45
- "@nuxt-customer-portal/clients": "^0.3.0"
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"
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  id: 'service-requests',
3
- version: '0.3.0',
3
+ version: '0.3.2',
4
4
  source: '@nuxt-customer-portal/service-requests',
5
5
  clientModuleId: 'service-requests',
6
6
  dependsOn: ['core', 'clients'],
@@ -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: 20
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: 20,
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: 30
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: 10
51
+ order: 50
75
52
  }
76
53
  ],
77
54
  clientIntegration: { moduleId: 'service-requests', labelKey: 'features.serviceRequests.navigation.myRequests' },
@@ -56,6 +56,7 @@ export interface ServiceRequestPagination {
56
56
  export interface ServiceRequestListResponse {
57
57
  items: ServiceRequestDto[]
58
58
  pagination: ServiceRequestPagination
59
+ categories?: string[]
59
60
  }
60
61
 
61
62
  export interface ServiceRequestDashboardDto {