@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.
Files changed (38) hide show
  1. package/dist/config.cjs +1 -1
  2. package/dist/config.js +1 -1
  3. package/dist/hooks.cjs +398 -334
  4. package/dist/hooks.d.cts +6 -2
  5. package/dist/hooks.d.ts +6 -2
  6. package/dist/hooks.js +356 -291
  7. package/dist/i18n.cjs +72 -72
  8. package/dist/i18n.js +73 -73
  9. package/dist/index-D-xo66K9.d.cts +862 -0
  10. package/dist/index-D-xo66K9.d.ts +863 -0
  11. package/dist/index-Dov7pn8Z.d.ts +2 -1
  12. package/dist/index-rR_XqXq1.d.cts +838 -0
  13. package/dist/index-rR_XqXq1.d.ts +838 -0
  14. package/dist/index.cjs +398 -334
  15. package/dist/index.d.cts +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +356 -291
  18. package/package.json +8 -8
  19. package/src/adapters/api.ts +4 -5
  20. package/src/adapters/demo.ts +2 -1
  21. package/src/api/generated/ext_support/_utils/fetchers/ext_support__support.ts +4 -4
  22. package/src/api/generated/ext_support/_utils/hooks/ext_support__support.ts +2 -2
  23. package/src/api/generated/ext_support/_utils/schemas/Ticket.schema.ts +1 -1
  24. package/src/api/generated/ext_support/_utils/schemas/TicketRequest.schema.ts +1 -1
  25. package/src/api/generated/ext_support/enums.ts +0 -30
  26. package/src/api/generated/ext_support/ext_support__support/client.ts +6 -6
  27. package/src/api/generated/ext_support/ext_support__support/models.ts +2 -2
  28. package/src/api/generated/ext_support/schema.json +36 -0
  29. package/src/components/CreateTicketSheet.tsx +7 -12
  30. package/src/components/SupportHero.tsx +35 -47
  31. package/src/components/TicketItem.tsx +28 -74
  32. package/src/components/TicketList.tsx +30 -23
  33. package/src/components/TicketSheet.tsx +246 -162
  34. package/src/contexts/SupportExtensionProvider.tsx +4 -4
  35. package/src/contexts/types.ts +2 -2
  36. package/src/i18n/useSupportT.ts +3 -3
  37. package/src/utils/status.ts +44 -0
  38. 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.24",
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.125",
68
+ "@djangocfg/api": "^2.1.142",
69
69
  "@djangocfg/ext-base": "^1.0.19",
70
- "@djangocfg/i18n": "^2.1.125",
71
- "@djangocfg/ui-core": "^2.1.125",
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.125",
85
+ "@djangocfg/api": "^2.1.142",
86
86
  "@djangocfg/ext-base": "^1.0.19",
87
- "@djangocfg/i18n": "^2.1.125",
88
- "@djangocfg/ui-core": "^2.1.125",
89
- "@djangocfg/typescript-config": "^2.1.125",
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",
@@ -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
- useCreateSupportTicketsCreate,
21
- useCreateSupportTicketsMessagesCreate,
20
+ useCreateSupportTicketsCreate, useCreateSupportTicketsMessagesCreate
22
21
  } from '../api/generated/ext_support/_utils/hooks';
23
22
 
24
23
  import type {
@@ -9,7 +9,8 @@
9
9
 
10
10
  import { useCallback, useMemo, useState } from 'react';
11
11
 
12
- import { type Ticket, type Message, TicketStatus } from '../contexts/types';
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.TicketStatus).optional(),
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.TicketRequestStatus).optional(),
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 = { page: args[0], page_size: args[1] };
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 = { page: args[1], page_size: args[2] };
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.TicketRequestStatus;
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.TicketStatus;
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
- * Responsive sheet for creating new support tickets
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, Plus } from 'lucide-react';
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 className="flex items-center gap-2">
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-6 mt-6">
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-4">
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-style)
2
+ * Support Hero (Apple HIG)
3
3
  *
4
- * Header with ticket stats and create action
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 { Plus, LifeBuoy, RefreshCw } from 'lucide-react';
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-16 px-4', className)}>
40
- {/* Icon & Title */}
41
- <div className="text-center mb-6">
42
- {isLoadingTickets ? (
43
- <>
44
- <Skeleton className="h-12 w-12 mx-auto mb-4 rounded-full" />
45
- <Skeleton className="h-8 w-32 mx-auto mb-2" />
46
- <Skeleton className="h-4 w-48 mx-auto" />
47
- </>
48
- ) : (
49
- <>
50
- <div className="h-16 w-16 mx-auto mb-4 bg-primary/10 rounded-full flex items-center justify-center">
51
- <LifeBuoy className="h-8 w-8 text-primary" />
52
- </div>
53
- <h1 className="text-3xl font-bold tracking-tight">
54
- {labels.title}
55
- </h1>
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 Buttons */}
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-6"
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 (subtle) */}
83
- {!isLoadingTickets && (
84
- <div className="flex items-center gap-6 mt-6 text-sm text-muted-foreground">
85
- <div className="text-center">
86
- <span className="block font-medium text-foreground text-xl tabular-nums">
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
- <span>{labels.openTickets}</span>
90
- </div>
91
- <div className="h-8 w-px bg-border" />
92
- <div className="text-center">
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
- <span>{labels.unreadMessages}</span>
100
- </div>
88
+ )}
101
89
  </div>
102
90
  )}
103
91
  </div>
@@ -1,46 +1,25 @@
1
1
  /**
2
- * Ticket Item Component (Apple-style)
2
+ * Ticket Item Component (Apple HIG)
3
3
  *
4
- * Single ticket row in the list
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 formatRelativeTime = useCallback((date: string | null | undefined): string => {
64
- if (!date) return 'N/A';
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
- <div
51
+ <button
52
+ type="button"
83
53
  className={cn(
84
- 'group flex items-center gap-4 p-4 rounded-xl',
85
- 'cursor-pointer transition-all duration-200',
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 indicator */}
93
- <div className="w-2 flex-shrink-0">
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
- <div className="flex items-start justify-between gap-2">
102
- <h3 className={cn(
103
- 'font-medium text-sm line-clamp-1',
104
- hasUnread && 'font-semibold'
105
- )}>
106
- {ticket.subject}
107
- </h3>
108
- {hasUnread && (
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
- </Badge>
119
- <div className="flex items-center gap-1">
120
- <Clock className="h-3 w-3" />
121
- <span>{formatRelativeTime(ticket.created_at)}</span>
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
  }