@djangocfg/ext-support 1.0.24 → 1.0.26
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/dist/config.cjs +1 -1
- package/dist/config.js +1 -1
- package/dist/hooks.cjs +398 -334
- package/dist/hooks.d.cts +6 -2
- package/dist/hooks.d.ts +6 -2
- package/dist/hooks.js +356 -291
- package/dist/i18n.cjs +72 -72
- package/dist/i18n.js +73 -73
- package/dist/index-D-xo66K9.d.cts +862 -0
- package/dist/index-D-xo66K9.d.ts +863 -0
- package/dist/index-Dov7pn8Z.d.ts +2 -1
- package/dist/index-rR_XqXq1.d.cts +838 -0
- package/dist/index-rR_XqXq1.d.ts +838 -0
- package/dist/index.cjs +398 -334
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +356 -291
- package/package.json +8 -8
- package/src/adapters/api.ts +4 -5
- package/src/adapters/demo.ts +2 -1
- package/src/api/generated/ext_support/_utils/fetchers/ext_support__support.ts +4 -4
- package/src/api/generated/ext_support/_utils/hooks/ext_support__support.ts +2 -2
- package/src/api/generated/ext_support/_utils/schemas/Ticket.schema.ts +1 -1
- package/src/api/generated/ext_support/_utils/schemas/TicketRequest.schema.ts +1 -1
- package/src/api/generated/ext_support/enums.ts +0 -30
- package/src/api/generated/ext_support/ext_support__support/client.ts +6 -6
- package/src/api/generated/ext_support/ext_support__support/models.ts +2 -2
- package/src/api/generated/ext_support/schema.json +36 -0
- package/src/components/CreateTicketSheet.tsx +7 -12
- package/src/components/SupportHero.tsx +35 -47
- package/src/components/TicketItem.tsx +28 -74
- package/src/components/TicketList.tsx +30 -23
- package/src/components/TicketSheet.tsx +246 -162
- package/src/contexts/SupportExtensionProvider.tsx +4 -4
- package/src/contexts/types.ts +2 -2
- package/src/i18n/useSupportT.ts +3 -3
- package/src/utils/status.ts +44 -0
- package/src/utils/time.ts +88 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ext-support",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.26",
|
|
4
4
|
"description": "Support ticket system extension for DjangoCFG",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"django",
|
|
@@ -65,10 +65,10 @@
|
|
|
65
65
|
"check": "tsc --noEmit"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
|
-
"@djangocfg/api": "^2.1.
|
|
68
|
+
"@djangocfg/api": "^2.1.142",
|
|
69
69
|
"@djangocfg/ext-base": "^1.0.19",
|
|
70
|
-
"@djangocfg/i18n": "^2.1.
|
|
71
|
-
"@djangocfg/ui-core": "^2.1.
|
|
70
|
+
"@djangocfg/i18n": "^2.1.142",
|
|
71
|
+
"@djangocfg/ui-core": "^2.1.142",
|
|
72
72
|
"@hookform/resolvers": "^5.2.2",
|
|
73
73
|
"consola": "^3.4.2",
|
|
74
74
|
"lucide-react": "^0.545.0",
|
|
@@ -82,11 +82,11 @@
|
|
|
82
82
|
"zod": "^4.3.4"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
|
-
"@djangocfg/api": "^2.1.
|
|
85
|
+
"@djangocfg/api": "^2.1.142",
|
|
86
86
|
"@djangocfg/ext-base": "^1.0.19",
|
|
87
|
-
"@djangocfg/i18n": "^2.1.
|
|
88
|
-
"@djangocfg/ui-core": "^2.1.
|
|
89
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
87
|
+
"@djangocfg/i18n": "^2.1.142",
|
|
88
|
+
"@djangocfg/ui-core": "^2.1.142",
|
|
89
|
+
"@djangocfg/typescript-config": "^2.1.142",
|
|
90
90
|
"@types/node": "^24.7.2",
|
|
91
91
|
"@types/react": "^19.0.0",
|
|
92
92
|
"consola": "^3.4.2",
|
package/src/adapters/api.ts
CHANGED
|
@@ -7,18 +7,17 @@
|
|
|
7
7
|
|
|
8
8
|
'use client';
|
|
9
9
|
|
|
10
|
+
// Initialize API
|
|
11
|
+
import '../api';
|
|
12
|
+
|
|
10
13
|
import { useCallback, useMemo } from 'react';
|
|
11
14
|
import useSWRInfinite from 'swr/infinite';
|
|
12
15
|
|
|
13
16
|
import { useAuth } from '@djangocfg/api/auth';
|
|
14
17
|
|
|
15
|
-
// Initialize API
|
|
16
|
-
import '../api';
|
|
17
|
-
|
|
18
18
|
import * as Fetchers from '../api/generated/ext_support/_utils/fetchers';
|
|
19
19
|
import {
|
|
20
|
-
|
|
21
|
-
useCreateSupportTicketsMessagesCreate,
|
|
20
|
+
useCreateSupportTicketsCreate, useCreateSupportTicketsMessagesCreate
|
|
22
21
|
} from '../api/generated/ext_support/_utils/hooks';
|
|
23
22
|
|
|
24
23
|
import type {
|
package/src/adapters/demo.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import { useCallback, useMemo, useState } from 'react';
|
|
11
11
|
|
|
12
|
-
import { type
|
|
12
|
+
import { type Message, type Ticket, TicketStatus } from '../contexts/types';
|
|
13
|
+
|
|
13
14
|
import type {
|
|
14
15
|
SupportAdapter,
|
|
15
16
|
CreateTicketInput,
|
|
@@ -49,10 +49,10 @@ import { getAPIInstance } from '../../api-instance'
|
|
|
49
49
|
* @method GET
|
|
50
50
|
* @path /cfg/support/tickets/
|
|
51
51
|
*/
|
|
52
|
-
export async function getSupportTicketsList( params?: { page?: number; page_size?: number }, client?: any
|
|
52
|
+
export async function getSupportTicketsList( params?: { ordering?: string; page?: number; page_size?: number; search?: string }, client?: any
|
|
53
53
|
): Promise<PaginatedTicketList> {
|
|
54
54
|
const api = client || getAPIInstance()
|
|
55
|
-
const response = await api.ext_support_support.ticketsList(params?.page, params?.page_size)
|
|
55
|
+
const response = await api.ext_support_support.ticketsList(params?.ordering, params?.page, params?.page_size, params?.search)
|
|
56
56
|
try {
|
|
57
57
|
return PaginatedTicketListSchema.parse(response)
|
|
58
58
|
} catch (error) {
|
|
@@ -163,10 +163,10 @@ export async function createSupportTicketsCreate( data: TicketRequest, client?
|
|
|
163
163
|
* @method GET
|
|
164
164
|
* @path /cfg/support/tickets/{ticket_uuid}/messages/
|
|
165
165
|
*/
|
|
166
|
-
export async function getSupportTicketsMessagesList( ticket_uuid: string, params?: { page?: number; page_size?: number }, client?: any
|
|
166
|
+
export async function getSupportTicketsMessagesList( ticket_uuid: string, params?: { ordering?: string; page?: number; page_size?: number; search?: string }, client?: any
|
|
167
167
|
): Promise<PaginatedMessageList> {
|
|
168
168
|
const api = client || getAPIInstance()
|
|
169
|
-
const response = await api.ext_support_support.ticketsMessagesList(ticket_uuid, params?.page, params?.page_size)
|
|
169
|
+
const response = await api.ext_support_support.ticketsMessagesList(ticket_uuid, params?.ordering, params?.page, params?.page_size, params?.search)
|
|
170
170
|
try {
|
|
171
171
|
return PaginatedMessageListSchema.parse(response)
|
|
172
172
|
} catch (error) {
|
|
@@ -38,7 +38,7 @@ import type { TicketRequest } from '../schemas/TicketRequest.schema'
|
|
|
38
38
|
* @method GET
|
|
39
39
|
* @path /cfg/support/tickets/
|
|
40
40
|
*/
|
|
41
|
-
export function useSupportTicketsList(params?: { page?: number; page_size?: number }, client?: API): ReturnType<typeof useSWR<PaginatedTicketList>> {
|
|
41
|
+
export function useSupportTicketsList(params?: { ordering?: string; page?: number; page_size?: number; search?: string }, client?: API): ReturnType<typeof useSWR<PaginatedTicketList>> {
|
|
42
42
|
return useSWR<PaginatedTicketList>(
|
|
43
43
|
params ? ['cfg-support-tickets', params] : 'cfg-support-tickets',
|
|
44
44
|
() => Fetchers.getSupportTicketsList(params, client)
|
|
@@ -70,7 +70,7 @@ export function useCreateSupportTicketsCreate() {
|
|
|
70
70
|
* @method GET
|
|
71
71
|
* @path /cfg/support/tickets/{ticket_uuid}/messages/
|
|
72
72
|
*/
|
|
73
|
-
export function useSupportTicketsMessagesList(ticket_uuid: string, params?: { page?: number; page_size?: number }, client?: API): ReturnType<typeof useSWR<PaginatedMessageList>> {
|
|
73
|
+
export function useSupportTicketsMessagesList(ticket_uuid: string, params?: { ordering?: string; page?: number; page_size?: number; search?: string }, client?: API): ReturnType<typeof useSWR<PaginatedMessageList>> {
|
|
74
74
|
return useSWR<PaginatedMessageList>(
|
|
75
75
|
['cfg-support-tickets-messages', ticket_uuid],
|
|
76
76
|
() => Fetchers.getSupportTicketsMessagesList(ticket_uuid, params, client)
|
|
@@ -10,7 +10,7 @@ export const TicketSchema = z.object({
|
|
|
10
10
|
uuid: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i),
|
|
11
11
|
user: z.int(),
|
|
12
12
|
subject: z.string().max(255),
|
|
13
|
-
status: z.nativeEnum(Enums.
|
|
13
|
+
status: z.nativeEnum(Enums.PatchedTicketRequestStatus).optional(),
|
|
14
14
|
created_at: z.string().datetime({ offset: true }),
|
|
15
15
|
unanswered_messages_count: z.int(),
|
|
16
16
|
})
|
|
@@ -9,7 +9,7 @@ import * as Enums from '../../enums'
|
|
|
9
9
|
export const TicketRequestSchema = z.object({
|
|
10
10
|
user: z.int(),
|
|
11
11
|
subject: z.string().min(1).max(255),
|
|
12
|
-
status: z.nativeEnum(Enums.
|
|
12
|
+
status: z.nativeEnum(Enums.PatchedTicketRequestStatus).optional(),
|
|
13
13
|
})
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -14,33 +14,3 @@ export enum PatchedTicketRequestStatus {
|
|
|
14
14
|
CLOSED = "closed",
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* * `open` - Open
|
|
19
|
-
* * `waiting_for_user` - Waiting for User
|
|
20
|
-
* * `waiting_for_admin` - Waiting for Admin
|
|
21
|
-
* * `resolved` - Resolved
|
|
22
|
-
* * `closed` - Closed
|
|
23
|
-
*/
|
|
24
|
-
export enum TicketStatus {
|
|
25
|
-
OPEN = "open",
|
|
26
|
-
WAITING_FOR_USER = "waiting_for_user",
|
|
27
|
-
WAITING_FOR_ADMIN = "waiting_for_admin",
|
|
28
|
-
RESOLVED = "resolved",
|
|
29
|
-
CLOSED = "closed",
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* * `open` - Open
|
|
34
|
-
* * `waiting_for_user` - Waiting for User
|
|
35
|
-
* * `waiting_for_admin` - Waiting for Admin
|
|
36
|
-
* * `resolved` - Resolved
|
|
37
|
-
* * `closed` - Closed
|
|
38
|
-
*/
|
|
39
|
-
export enum TicketRequestStatus {
|
|
40
|
-
OPEN = "open",
|
|
41
|
-
WAITING_FOR_USER = "waiting_for_user",
|
|
42
|
-
WAITING_FOR_ADMIN = "waiting_for_admin",
|
|
43
|
-
RESOLVED = "resolved",
|
|
44
|
-
CLOSED = "closed",
|
|
45
|
-
}
|
|
46
|
-
|
|
@@ -11,8 +11,8 @@ export class ExtSupportSupport {
|
|
|
11
11
|
this.client = client;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
async ticketsList(page?: number, page_size?: number): Promise<Models.PaginatedTicketList>;
|
|
15
|
-
async ticketsList(params?: { page?: number; page_size?: number }): Promise<Models.PaginatedTicketList>;
|
|
14
|
+
async ticketsList(ordering?: string, page?: number, page_size?: number, search?: string): Promise<Models.PaginatedTicketList>;
|
|
15
|
+
async ticketsList(params?: { ordering?: string; page?: number; page_size?: number; search?: string }): Promise<Models.PaginatedTicketList>;
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* ViewSet for managing support tickets. Requires authenticated user (JWT
|
|
@@ -26,7 +26,7 @@ export class ExtSupportSupport {
|
|
|
26
26
|
if (isParamsObject) {
|
|
27
27
|
params = args[0];
|
|
28
28
|
} else {
|
|
29
|
-
params = {
|
|
29
|
+
params = { ordering: args[0], page: args[1], page_size: args[2], search: args[3] };
|
|
30
30
|
}
|
|
31
31
|
const response = await this.client.request('GET', "/cfg/support/tickets/", { params });
|
|
32
32
|
return response;
|
|
@@ -42,8 +42,8 @@ export class ExtSupportSupport {
|
|
|
42
42
|
return response;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
async ticketsMessagesList(ticket_uuid: string, page?: number, page_size?: number): Promise<Models.PaginatedMessageList>;
|
|
46
|
-
async ticketsMessagesList(ticket_uuid: string, params?: { page?: number; page_size?: number }): Promise<Models.PaginatedMessageList>;
|
|
45
|
+
async ticketsMessagesList(ticket_uuid: string, ordering?: string, page?: number, page_size?: number, search?: string): Promise<Models.PaginatedMessageList>;
|
|
46
|
+
async ticketsMessagesList(ticket_uuid: string, params?: { ordering?: string; page?: number; page_size?: number; search?: string }): Promise<Models.PaginatedMessageList>;
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* ViewSet for managing support messages. Requires authenticated user (JWT
|
|
@@ -57,7 +57,7 @@ export class ExtSupportSupport {
|
|
|
57
57
|
if (isParamsObject) {
|
|
58
58
|
params = args[1];
|
|
59
59
|
} else {
|
|
60
|
-
params = {
|
|
60
|
+
params = { ordering: args[1], page: args[2], page_size: args[3], search: args[4] };
|
|
61
61
|
}
|
|
62
62
|
const response = await this.client.request('GET', `/cfg/support/tickets/${ticket_uuid}/messages/`, { params });
|
|
63
63
|
return response;
|
|
@@ -38,7 +38,7 @@ export interface TicketRequest {
|
|
|
38
38
|
* `waiting_for_admin` - Waiting for Admin
|
|
39
39
|
* `resolved` - Resolved
|
|
40
40
|
* `closed` - Closed */
|
|
41
|
-
status?: Enums.
|
|
41
|
+
status?: Enums.PatchedTicketRequestStatus;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
@@ -54,7 +54,7 @@ export interface Ticket {
|
|
|
54
54
|
* `waiting_for_admin` - Waiting for Admin
|
|
55
55
|
* `resolved` - Resolved
|
|
56
56
|
* `closed` - Closed */
|
|
57
|
-
status?: Enums.
|
|
57
|
+
status?: Enums.PatchedTicketRequestStatus;
|
|
58
58
|
created_at: string;
|
|
59
59
|
/** Get count of unanswered messages for this specific ticket. */
|
|
60
60
|
unanswered_messages_count: number;
|
|
@@ -19,6 +19,15 @@
|
|
|
19
19
|
"operationId": "cfg_support_tickets_list",
|
|
20
20
|
"description": "ViewSet for managing support tickets.\n\nRequires authenticated user (JWT or Session).\nStaff users can see all tickets, regular users see only their own.",
|
|
21
21
|
"parameters": [
|
|
22
|
+
{
|
|
23
|
+
"name": "ordering",
|
|
24
|
+
"required": false,
|
|
25
|
+
"in": "query",
|
|
26
|
+
"description": "Which field to use when ordering the results.",
|
|
27
|
+
"schema": {
|
|
28
|
+
"type": "string"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
22
31
|
{
|
|
23
32
|
"name": "page",
|
|
24
33
|
"required": false,
|
|
@@ -36,6 +45,15 @@
|
|
|
36
45
|
"schema": {
|
|
37
46
|
"type": "integer"
|
|
38
47
|
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "search",
|
|
51
|
+
"required": false,
|
|
52
|
+
"in": "query",
|
|
53
|
+
"description": "A search term.",
|
|
54
|
+
"schema": {
|
|
55
|
+
"type": "string"
|
|
56
|
+
}
|
|
39
57
|
}
|
|
40
58
|
],
|
|
41
59
|
"tags": [
|
|
@@ -117,6 +135,15 @@
|
|
|
117
135
|
"operationId": "cfg_support_tickets_messages_list",
|
|
118
136
|
"description": "ViewSet for managing support messages.\n\nRequires authenticated user (JWT or Session).\nUsers can only access messages for their own tickets.",
|
|
119
137
|
"parameters": [
|
|
138
|
+
{
|
|
139
|
+
"name": "ordering",
|
|
140
|
+
"required": false,
|
|
141
|
+
"in": "query",
|
|
142
|
+
"description": "Which field to use when ordering the results.",
|
|
143
|
+
"schema": {
|
|
144
|
+
"type": "string"
|
|
145
|
+
}
|
|
146
|
+
},
|
|
120
147
|
{
|
|
121
148
|
"name": "page",
|
|
122
149
|
"required": false,
|
|
@@ -135,6 +162,15 @@
|
|
|
135
162
|
"type": "integer"
|
|
136
163
|
}
|
|
137
164
|
},
|
|
165
|
+
{
|
|
166
|
+
"name": "search",
|
|
167
|
+
"required": false,
|
|
168
|
+
"in": "query",
|
|
169
|
+
"description": "A search term.",
|
|
170
|
+
"schema": {
|
|
171
|
+
"type": "string"
|
|
172
|
+
}
|
|
173
|
+
},
|
|
138
174
|
{
|
|
139
175
|
"in": "path",
|
|
140
176
|
"name": "ticket_uuid",
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Create Ticket Sheet
|
|
2
|
+
* Create Ticket Sheet (Apple HIG)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Uses ResponsiveSheet (drawer on mobile, dialog on desktop)
|
|
4
|
+
* Clean form without Plus icons
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
'use client';
|
|
9
8
|
|
|
10
|
-
import { Loader2
|
|
9
|
+
import { Loader2 } from 'lucide-react';
|
|
11
10
|
import React, { useMemo, useState } from 'react';
|
|
12
11
|
import { useForm } from 'react-hook-form';
|
|
13
12
|
import { z } from 'zod';
|
|
@@ -109,8 +108,7 @@ export function CreateTicketSheet({ open, onOpenChange, onSuccess }: CreateTicke
|
|
|
109
108
|
<ResponsiveSheet open={open} onOpenChange={handleClose}>
|
|
110
109
|
<ResponsiveSheetContent className="sm:max-w-lg">
|
|
111
110
|
<ResponsiveSheetHeader>
|
|
112
|
-
<ResponsiveSheetTitle
|
|
113
|
-
<Plus className="h-5 w-5" />
|
|
111
|
+
<ResponsiveSheetTitle>
|
|
114
112
|
{labels.title}
|
|
115
113
|
</ResponsiveSheetTitle>
|
|
116
114
|
<ResponsiveSheetDescription>
|
|
@@ -119,7 +117,7 @@ export function CreateTicketSheet({ open, onOpenChange, onSuccess }: CreateTicke
|
|
|
119
117
|
</ResponsiveSheetHeader>
|
|
120
118
|
|
|
121
119
|
<Form {...form}>
|
|
122
|
-
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-
|
|
120
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-5 mt-4">
|
|
123
121
|
<FormField
|
|
124
122
|
control={form.control}
|
|
125
123
|
name="subject"
|
|
@@ -157,7 +155,7 @@ export function CreateTicketSheet({ open, onOpenChange, onSuccess }: CreateTicke
|
|
|
157
155
|
)}
|
|
158
156
|
/>
|
|
159
157
|
|
|
160
|
-
<div className="flex justify-end gap-3 pt-
|
|
158
|
+
<div className="flex justify-end gap-3 pt-2">
|
|
161
159
|
<Button
|
|
162
160
|
type="button"
|
|
163
161
|
variant="outline"
|
|
@@ -173,10 +171,7 @@ export function CreateTicketSheet({ open, onOpenChange, onSuccess }: CreateTicke
|
|
|
173
171
|
{labels.creating}
|
|
174
172
|
</>
|
|
175
173
|
) : (
|
|
176
|
-
|
|
177
|
-
<Plus className="h-4 w-4 mr-2" />
|
|
178
|
-
{labels.create}
|
|
179
|
-
</>
|
|
174
|
+
labels.create
|
|
180
175
|
)}
|
|
181
176
|
</Button>
|
|
182
177
|
</div>
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Support Hero (Apple
|
|
2
|
+
* Support Hero (Apple HIG)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Pattern follows ext-payments BalanceHero
|
|
4
|
+
* Clean header with stats pills and create action
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
'use client';
|
|
9
8
|
|
|
10
9
|
import { useMemo } from 'react';
|
|
11
|
-
import {
|
|
10
|
+
import { RefreshCw } from 'lucide-react';
|
|
12
11
|
|
|
13
12
|
import { useSupportT } from '../i18n';
|
|
14
13
|
import { Button, Skeleton } from '@djangocfg/ui-core';
|
|
@@ -36,36 +35,31 @@ export function SupportHero({ onCreateTicket, className }: SupportHeroProps) {
|
|
|
36
35
|
const openTicketsCount = tickets.filter(t => t.status !== 'closed' && t.status !== 'resolved').length;
|
|
37
36
|
|
|
38
37
|
return (
|
|
39
|
-
<div className={cn('flex flex-col items-center py-
|
|
40
|
-
{/*
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<p className="text-muted-foreground mt-1">{labels.subtitle}</p>
|
|
57
|
-
</>
|
|
58
|
-
)}
|
|
59
|
-
</div>
|
|
38
|
+
<div className={cn('flex flex-col items-center py-12 px-4', className)}>
|
|
39
|
+
{/* Title */}
|
|
40
|
+
{isLoadingTickets ? (
|
|
41
|
+
<div className="text-center">
|
|
42
|
+
<Skeleton className="h-10 w-48 mx-auto mb-2" />
|
|
43
|
+
<Skeleton className="h-5 w-64 mx-auto" />
|
|
44
|
+
</div>
|
|
45
|
+
) : (
|
|
46
|
+
<div className="text-center">
|
|
47
|
+
<h1 className="text-4xl font-bold tracking-tight">
|
|
48
|
+
{labels.title}
|
|
49
|
+
</h1>
|
|
50
|
+
<p className="text-lg text-muted-foreground mt-2">
|
|
51
|
+
{labels.subtitle}
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
60
55
|
|
|
61
|
-
{/* Action
|
|
62
|
-
<div className="flex items-center gap-3">
|
|
56
|
+
{/* Action */}
|
|
57
|
+
<div className="flex items-center gap-3 mt-6">
|
|
63
58
|
<Button
|
|
64
59
|
size="lg"
|
|
65
60
|
onClick={onCreateTicket}
|
|
66
|
-
className="rounded-full px-
|
|
61
|
+
className="rounded-full px-8 h-12 text-base"
|
|
67
62
|
>
|
|
68
|
-
<Plus className="h-5 w-5 mr-2" />
|
|
69
63
|
{labels.newTicket}
|
|
70
64
|
</Button>
|
|
71
65
|
|
|
@@ -73,31 +67,25 @@ export function SupportHero({ onCreateTicket, className }: SupportHeroProps) {
|
|
|
73
67
|
size="icon"
|
|
74
68
|
variant="ghost"
|
|
75
69
|
onClick={() => refreshTickets()}
|
|
76
|
-
className="rounded-full"
|
|
70
|
+
className="rounded-full h-10 w-10"
|
|
77
71
|
>
|
|
78
72
|
<RefreshCw className="h-4 w-4" />
|
|
79
73
|
</Button>
|
|
80
74
|
</div>
|
|
81
75
|
|
|
82
|
-
{/* Stats (
|
|
83
|
-
{!isLoadingTickets && (
|
|
84
|
-
<div className="flex items-center gap-
|
|
85
|
-
|
|
86
|
-
<span className="
|
|
87
|
-
{openTicketsCount}
|
|
76
|
+
{/* Stats as pills (only if has data) */}
|
|
77
|
+
{!isLoadingTickets && (openTicketsCount > 0 || unreadCount > 0) && (
|
|
78
|
+
<div className="flex items-center gap-2 mt-4">
|
|
79
|
+
{openTicketsCount > 0 && (
|
|
80
|
+
<span className="text-sm text-muted-foreground bg-muted px-3 py-1 rounded-full">
|
|
81
|
+
{openTicketsCount} {labels.openTickets.toLowerCase()}
|
|
88
82
|
</span>
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
<span className={cn(
|
|
94
|
-
'block font-medium text-xl tabular-nums',
|
|
95
|
-
unreadCount > 0 ? 'text-destructive' : 'text-foreground'
|
|
96
|
-
)}>
|
|
97
|
-
{unreadCount}
|
|
83
|
+
)}
|
|
84
|
+
{unreadCount > 0 && (
|
|
85
|
+
<span className="text-sm text-destructive bg-destructive/10 px-3 py-1 rounded-full">
|
|
86
|
+
{unreadCount} {labels.unreadMessages.toLowerCase()}
|
|
98
87
|
</span>
|
|
99
|
-
|
|
100
|
-
</div>
|
|
88
|
+
)}
|
|
101
89
|
</div>
|
|
102
90
|
)}
|
|
103
91
|
</div>
|
|
@@ -1,46 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Ticket Item Component (Apple
|
|
2
|
+
* Ticket Item Component (Apple HIG)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Pattern follows ext-payments ActivityItem
|
|
4
|
+
* Clean ticket row with typography-based hierarchy
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
'use client';
|
|
9
8
|
|
|
10
|
-
import { Clock, ChevronRight } from 'lucide-react';
|
|
11
|
-
import moment from 'moment';
|
|
12
9
|
import React, { useMemo, useCallback } from 'react';
|
|
13
10
|
|
|
14
11
|
import { useSupportT } from '../i18n';
|
|
15
|
-
import { Badge } from '@djangocfg/ui-core';
|
|
16
12
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
17
13
|
|
|
18
14
|
import type { Ticket } from '../contexts/SupportContext';
|
|
15
|
+
import { getStatusConfig } from '../utils/status';
|
|
16
|
+
import { formatRelativeTime } from '../utils/time';
|
|
19
17
|
|
|
20
18
|
interface TicketItemProps {
|
|
21
19
|
ticket: Ticket;
|
|
22
20
|
onClick?: () => void;
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
const getStatusBadgeVariant = (
|
|
26
|
-
status: string
|
|
27
|
-
): 'default' | 'secondary' | 'destructive' | 'outline' => {
|
|
28
|
-
switch (status) {
|
|
29
|
-
case 'open':
|
|
30
|
-
return 'default';
|
|
31
|
-
case 'waiting_for_user':
|
|
32
|
-
return 'secondary';
|
|
33
|
-
case 'waiting_for_admin':
|
|
34
|
-
return 'outline';
|
|
35
|
-
case 'resolved':
|
|
36
|
-
return 'outline';
|
|
37
|
-
case 'closed':
|
|
38
|
-
return 'secondary';
|
|
39
|
-
default:
|
|
40
|
-
return 'default';
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
|
|
44
23
|
export function TicketItem({ ticket, onClick }: TicketItemProps) {
|
|
45
24
|
const st = useSupportT();
|
|
46
25
|
|
|
@@ -60,71 +39,46 @@ export function TicketItem({ ticket, onClick }: TicketItemProps) {
|
|
|
60
39
|
},
|
|
61
40
|
}), [st]);
|
|
62
41
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const m = moment.utc(date).local();
|
|
67
|
-
const now = moment();
|
|
68
|
-
const diffInSeconds = now.diff(m, 'seconds');
|
|
69
|
-
|
|
70
|
-
if (diffInSeconds < 60) return labels.time.justNow;
|
|
71
|
-
if (diffInSeconds < 3600) return labels.time.minutesAgo.replace('{count}', String(Math.floor(diffInSeconds / 60)));
|
|
72
|
-
if (diffInSeconds < 86400) return labels.time.hoursAgo.replace('{count}', String(Math.floor(diffInSeconds / 3600)));
|
|
73
|
-
if (diffInSeconds < 604800) return labels.time.daysAgo.replace('{count}', String(Math.floor(diffInSeconds / 86400)));
|
|
74
|
-
|
|
75
|
-
return m.format('MMM D, YYYY');
|
|
42
|
+
const getRelativeTime = useCallback((date: string | null | undefined): string => {
|
|
43
|
+
return formatRelativeTime(date, labels.time);
|
|
76
44
|
}, [labels.time]);
|
|
77
45
|
|
|
78
46
|
const statusLabel = labels.status[ticket.status as keyof typeof labels.status] || ticket.status || labels.status.open;
|
|
47
|
+
const statusConfig = getStatusConfig(ticket.status || 'open');
|
|
79
48
|
const hasUnread = (ticket.unanswered_messages_count || 0) > 0;
|
|
80
49
|
|
|
81
50
|
return (
|
|
82
|
-
<
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
83
53
|
className={cn(
|
|
84
|
-
'
|
|
85
|
-
'
|
|
86
|
-
'hover:bg-accent/50'
|
|
87
|
-
'active:scale-[0.98]',
|
|
88
|
-
hasUnread && 'bg-primary/5'
|
|
54
|
+
'w-full flex items-start gap-3 p-4 text-left',
|
|
55
|
+
'transition-colors duration-150',
|
|
56
|
+
'hover:bg-accent/50 active:bg-accent'
|
|
89
57
|
)}
|
|
90
58
|
onClick={onClick}
|
|
91
59
|
>
|
|
92
|
-
{/* Unread
|
|
93
|
-
<div className="w-2
|
|
94
|
-
{hasUnread &&
|
|
95
|
-
<div className="w-2 h-2 rounded-full bg-primary animate-pulse" />
|
|
96
|
-
)}
|
|
60
|
+
{/* Unread dot - static, no animation */}
|
|
61
|
+
<div className="w-2 pt-1.5 shrink-0">
|
|
62
|
+
{hasUnread && <div className="w-2 h-2 rounded-full bg-blue-500" />}
|
|
97
63
|
</div>
|
|
98
64
|
|
|
99
65
|
{/* Content */}
|
|
100
66
|
<div className="flex-1 min-w-0">
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
{
|
|
109
|
-
<Badge variant="destructive" className="shrink-0 text-xs">
|
|
110
|
-
{ticket.unanswered_messages_count}
|
|
111
|
-
</Badge>
|
|
112
|
-
)}
|
|
113
|
-
</div>
|
|
114
|
-
|
|
115
|
-
<div className="flex items-center gap-3 mt-1 text-xs text-muted-foreground">
|
|
116
|
-
<Badge variant={getStatusBadgeVariant(ticket.status || 'open')} className="text-xs py-0">
|
|
67
|
+
<h3 className={cn(
|
|
68
|
+
'text-[15px] line-clamp-1',
|
|
69
|
+
hasUnread ? 'font-semibold' : 'font-medium'
|
|
70
|
+
)}>
|
|
71
|
+
{ticket.subject}
|
|
72
|
+
</h3>
|
|
73
|
+
<div className="flex items-center gap-2 mt-0.5">
|
|
74
|
+
<span className={cn('text-[13px]', statusConfig.color)}>
|
|
117
75
|
{statusLabel}
|
|
118
|
-
</
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
</div>
|
|
76
|
+
</span>
|
|
77
|
+
<span className="text-[13px] text-muted-foreground">
|
|
78
|
+
· {getRelativeTime(ticket.created_at)}
|
|
79
|
+
</span>
|
|
123
80
|
</div>
|
|
124
81
|
</div>
|
|
125
|
-
|
|
126
|
-
{/* Arrow */}
|
|
127
|
-
<ChevronRight className="h-5 w-5 text-muted-foreground/50 group-hover:text-muted-foreground transition-colors flex-shrink-0" />
|
|
128
|
-
</div>
|
|
82
|
+
</button>
|
|
129
83
|
);
|
|
130
84
|
}
|