@consilioweb/payload-support 0.8.2 → 0.9.5

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 (50) hide show
  1. package/dist/components/RichTextEditor/index.cjs +233 -0
  2. package/dist/components/RichTextEditor/index.js +232 -0
  3. package/dist/components/TicketConversation/components/CodeBlock.cjs +24 -7
  4. package/dist/components/TicketConversation/components/CodeBlock.js +23 -9
  5. package/dist/index.cjs +19 -1
  6. package/dist/index.js +19 -1
  7. package/dist/views/BillingView/client.cjs +260 -103
  8. package/dist/views/BillingView/client.js +259 -103
  9. package/dist/views/ChatView/client.cjs +184 -137
  10. package/dist/views/ChatView/client.js +180 -137
  11. package/dist/views/CrmView/client.cjs +270 -122
  12. package/dist/views/CrmView/client.js +266 -122
  13. package/dist/views/EmailTrackingView/client.cjs +80 -69
  14. package/dist/views/EmailTrackingView/client.js +80 -70
  15. package/dist/views/ImportConversationView/client.cjs +127 -94
  16. package/dist/views/ImportConversationView/client.js +123 -94
  17. package/dist/views/LogsView/client.cjs +56 -58
  18. package/dist/views/LogsView/client.js +52 -58
  19. package/dist/views/NewTicketView/client.cjs +39 -55
  20. package/dist/views/NewTicketView/client.js +38 -55
  21. package/dist/views/PendingEmailsView/client.cjs +399 -102
  22. package/dist/views/PendingEmailsView/client.js +396 -103
  23. package/dist/views/SupportDashboardView/client.cjs +276 -137
  24. package/dist/views/SupportDashboardView/client.js +275 -137
  25. package/dist/views/TicketDetailView/client.cjs +487 -204
  26. package/dist/views/TicketDetailView/client.js +486 -204
  27. package/dist/views/TicketInboxView/client.cjs +62 -65
  28. package/dist/views/TicketInboxView/client.js +62 -66
  29. package/dist/views/TicketingSettingsView/client.cjs +10 -8
  30. package/dist/views/TicketingSettingsView/client.js +10 -8
  31. package/dist/views/TimeDashboardView/client.cjs +70 -59
  32. package/dist/views/TimeDashboardView/client.js +69 -59
  33. package/package.json +7 -3
  34. package/src/components/RichTextEditor/index.tsx +261 -0
  35. package/src/components/TicketConversation/components/CodeBlock.tsx +53 -14
  36. package/src/plugin.ts +2 -0
  37. package/src/utils/emailTemplate.ts +37 -0
  38. package/src/views/BillingView/client.tsx +362 -69
  39. package/src/views/ChatView/client.tsx +225 -140
  40. package/src/views/CrmView/client.tsx +447 -189
  41. package/src/views/EmailTrackingView/client.tsx +111 -71
  42. package/src/views/ImportConversationView/client.tsx +255 -70
  43. package/src/views/LogsView/client.tsx +85 -50
  44. package/src/views/NewTicketView/client.tsx +37 -53
  45. package/src/views/PendingEmailsView/client.tsx +512 -92
  46. package/src/views/SupportDashboardView/client.tsx +294 -134
  47. package/src/views/TicketDetailView/client.tsx +486 -213
  48. package/src/views/TicketInboxView/client.tsx +52 -61
  49. package/src/views/TicketingSettingsView/client.tsx +10 -9
  50. package/src/views/TimeDashboardView/client.tsx +184 -69
@@ -17,6 +17,7 @@ interface LogEntry {
17
17
  errorMessage?: string
18
18
  httpStatus?: number
19
19
  processingTimeMs?: number
20
+ // Auth log fields
20
21
  email?: string
21
22
  success?: boolean
22
23
  ip?: string
@@ -61,78 +62,112 @@ export const LogsClient: React.FC = () => {
61
62
  useEffect(() => { fetchLogs() }, [fetchLogs])
62
63
 
63
64
  const handlePurge = async (days: number) => {
64
- const label = days === 0 ? 'TOUS les logs' : `les logs de plus de ${days} jours`
65
- if (!window.confirm(`Supprimer ${label} (${collection}) ? Cette action est irreversible.`)) return
65
+ const label = days === 0 ? t('logs.purgeAllLabel') : t('logs.purgeDaysLabel', { days: String(days) })
66
+ if (!window.confirm(t('logs.purgeConfirm', { label, collection }))) return
67
+
66
68
  try {
67
69
  const res = await fetch(`/api/support/purge-logs?collection=${collection}&days=${days}`, { method: 'DELETE', credentials: 'include' })
68
- if (res.ok) { const d = await res.json(); setPurgeResult(`${d.purged} log(s) supprime(s)`); setTimeout(() => setPurgeResult(null), 5000); fetchLogs() }
70
+ if (res.ok) {
71
+ const d = await res.json()
72
+ setPurgeResult(t('logs.purgeResult', { count: String(d.purged) }))
73
+ setTimeout(() => setPurgeResult(null), 5000)
74
+ fetchLogs()
75
+ }
69
76
  } catch { /* silent */ }
70
77
  }
71
78
 
72
- const S: Record<string, React.CSSProperties> = {
73
- page: { padding: '20px 30px', maxWidth: 1100, margin: '0 auto' },
74
- header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 },
75
- title: { fontSize: 22, fontWeight: 700, margin: 0, color: 'var(--theme-text)' },
76
- tabsRow: { display: 'flex', gap: 4, marginBottom: 12 },
77
- tab: { padding: '6px 12px', borderRadius: 6, border: 'none', background: 'none', cursor: 'pointer', fontSize: 13, color: 'var(--theme-elevation-500)', fontWeight: 500 },
78
- tabActive: { background: 'var(--theme-elevation-100)', color: 'var(--theme-text)', fontWeight: 700 },
79
- purgeBtn: { padding: '4px 10px', borderRadius: 6, border: '1px solid var(--theme-elevation-200)', fontSize: 11, background: 'var(--theme-elevation-0)', cursor: 'pointer', color: 'var(--theme-text)' },
80
- table: { width: '100%', borderCollapse: 'collapse' as const, fontSize: 13 },
81
- th: { textAlign: 'left' as const, padding: '8px', borderBottom: '1px solid var(--theme-elevation-200)', fontSize: 11, color: 'var(--theme-elevation-500)' },
82
- td: { padding: '8px', borderBottom: '1px solid var(--theme-elevation-100)' },
83
- badge: { padding: '2px 8px', borderRadius: 4, fontSize: 11, fontWeight: 600 },
84
- mono: { fontFamily: 'monospace', fontSize: 12 },
85
- }
86
-
87
79
  return (
88
- <div style={S.page}>
89
- <div style={S.header}>
90
- <h1 style={S.title}>{logType === 'email' ? t('logs.title') : t('logs.titleAuth')}</h1>
91
- <div style={{ display: 'flex', gap: 6 }}>
92
- <button style={S.purgeBtn} onClick={() => handlePurge(7)}>{t('logs.purge7')}</button>
93
- <button style={S.purgeBtn} onClick={() => handlePurge(30)}>{t('logs.purge30')}</button>
94
- <button style={{ ...S.purgeBtn, color: '#dc2626', borderColor: '#dc2626' }} onClick={() => handlePurge(0)}>{t('logs.purgeAll')}</button>
80
+ <div className={s.page}>
81
+ <div className={s.header}>
82
+ <h1 className={s.title}>{logType === 'email' ? t('logs.title') : t('logs.titleAuth')}</h1>
83
+ <div className={s.headerActions}>
84
+ <button className={s.purgeBtn} onClick={() => handlePurge(7)}>{t('logs.purge7')}</button>
85
+ <button className={s.purgeBtn} onClick={() => handlePurge(30)}>{t('logs.purge30')}</button>
86
+ <button className={s.purgeBtn} onClick={() => handlePurge(90)}>Purger +90j</button>
87
+ <button className={`${s.purgeBtn} ${s.purgeBtnDanger}`} onClick={() => handlePurge(0)}>{t('logs.purgeAll')}</button>
95
88
  </div>
96
89
  </div>
97
90
 
98
- <div style={S.tabsRow}>
99
- <button style={{ ...S.tab, ...(logType === 'email' ? S.tabActive : {}) }} onClick={() => setLogType('email')}>{t('logs.tabs.email')} ({logType === 'email' ? totalDocs : '...'})</button>
100
- <button style={{ ...S.tab, ...(logType === 'auth' ? S.tabActive : {}) }} onClick={() => setLogType('auth')}>{t('logs.tabs.auth')} ({logType === 'auth' ? totalDocs : '...'})</button>
91
+ {/* Tabs */}
92
+ <div className={s.tabs}>
93
+ <button className={`${s.tab} ${logType === 'email' ? s.tabActive : ''}`} onClick={() => setLogType('email')}>
94
+ {t('logs.tabs.email')} ({logType === 'email' ? totalDocs : '...'})
95
+ </button>
96
+ <button className={`${s.tab} ${logType === 'auth' ? s.tabActive : ''}`} onClick={() => setLogType('auth')}>
97
+ {t('logs.tabs.auth')} ({logType === 'auth' ? totalDocs : '...'})
98
+ </button>
101
99
  </div>
102
100
 
103
- {purgeResult && <div style={{ padding: '8px 14px', borderRadius: 6, background: '#dcfce7', color: '#166534', fontSize: 13, marginBottom: 12 }}>{purgeResult}</div>}
101
+ {purgeResult && <div className={s.purgeResult}>{purgeResult}</div>}
104
102
 
105
103
  {loading ? (
106
- <div style={{ padding: 40, textAlign: 'center', color: '#94a3b8' }}>{t('common.loading')}</div>
104
+ <div className={s.loading}>{t('common.loading')}</div>
107
105
  ) : logs.length === 0 ? (
108
- <div style={{ padding: 40, textAlign: 'center', color: '#94a3b8' }}>{t('logs.noLogs')}</div>
106
+ <div className={s.empty}>{t('logs.noLogs')}</div>
109
107
  ) : logType === 'email' ? (
110
- <table style={S.table}>
111
- <thead><tr><th style={S.th}>{t('logs.tableHeaders.date')}</th><th style={S.th}>{t('logs.tableHeaders.status')}</th><th style={S.th}>{t('logs.tableHeaders.recipient')}</th><th style={S.th}>{t('logs.tableHeaders.subject')}</th><th style={S.th}>{t('logs.tableHeaders.action')}</th><th style={{ ...S.th, textAlign: 'right' }}>{t('logs.tableHeaders.time')}</th></tr></thead>
108
+ <table className={s.table}>
109
+ <thead>
110
+ <tr>
111
+ <th>{t('logs.tableHeaders.date')}</th>
112
+ <th>{t('logs.tableHeaders.status')}</th>
113
+ <th>{t('logs.tableHeaders.recipient')}</th>
114
+ <th>{t('logs.tableHeaders.subject')}</th>
115
+ <th>{t('logs.tableHeaders.action')}</th>
116
+ <th style={{ textAlign: 'right' }}>{t('logs.tableHeaders.time')}</th>
117
+ </tr>
118
+ </thead>
112
119
  <tbody>
113
120
  {logs.map((log) => (
114
121
  <tr key={log.id}>
115
- <td style={{ ...S.td, ...S.mono }}>{fmtDate(log.createdAt)}</td>
116
- <td style={S.td}><span style={{ ...S.badge, background: log.status === 'success' ? '#dcfce7' : log.status === 'error' ? '#fef2f2' : '#f3f4f6', color: log.status === 'success' ? '#16a34a' : log.status === 'error' ? '#dc2626' : '#6b7280' }}>{log.status === 'success' ? t('logs.statusSuccess') : log.status === 'error' ? t('logs.statusError') : t('logs.statusIgnored')}</span></td>
117
- <td style={{ ...S.td, maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={log.recipientEmail}>{log.recipientEmail || '--'}</td>
118
- <td style={{ ...S.td, maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={log.subject}>{log.subject || '--'}</td>
119
- <td style={{ ...S.td, fontSize: 12, color: 'var(--theme-elevation-500)' }}>{log.action || '--'}</td>
120
- <td style={{ ...S.td, textAlign: 'right' }}>{log.processingTimeMs != null ? <span style={{ ...S.mono, color: log.processingTimeMs > 2000 ? '#dc2626' : '#16a34a' }}>{log.processingTimeMs}ms</span> : '--'}</td>
122
+ <td className={s.mono}>{fmtDate(log.createdAt)}</td>
123
+ <td>
124
+ <span className={s.badge} style={{
125
+ background: log.status === 'success' ? '#dcfce7' : log.status === 'error' ? '#fef2f2' : '#f3f4f6',
126
+ color: log.status === 'success' ? '#16a34a' : log.status === 'error' ? '#dc2626' : '#6b7280',
127
+ }}>
128
+ {log.status === 'success' ? t('logs.statusSuccess') : log.status === 'error' ? t('logs.statusError') : t('logs.statusIgnored')}
129
+ </span>
130
+ </td>
131
+ <td className={s.truncate} title={log.recipientEmail}>{log.recipientEmail || '—'}</td>
132
+ <td className={s.truncate} title={log.subject}>{log.subject || '—'}</td>
133
+ <td style={{ fontSize: 12, color: 'var(--theme-elevation-500)' }}>{log.action || '—'}</td>
134
+ <td style={{ textAlign: 'right' }}>
135
+ {log.processingTimeMs != null ? (
136
+ <span className={s.mono} style={{ color: log.processingTimeMs > 2000 ? '#dc2626' : log.processingTimeMs > 500 ? '#d97706' : '#16a34a' }}>
137
+ {log.processingTimeMs}ms
138
+ </span>
139
+ ) : '—'}
140
+ </td>
121
141
  </tr>
122
142
  ))}
123
143
  </tbody>
124
144
  </table>
125
145
  ) : (
126
- <table style={S.table}>
127
- <thead><tr><th style={S.th}>{t('logs.tableHeaders.date')}</th><th style={S.th}>{t('logs.tableHeaders.status')}</th><th style={S.th}>{t('logs.tableHeaders.email')}</th><th style={S.th}>{t('logs.tableHeaders.ip')}</th><th style={S.th}>{t('logs.tableHeaders.userAgent')}</th></tr></thead>
146
+ <table className={s.table}>
147
+ <thead>
148
+ <tr>
149
+ <th>{t('logs.tableHeaders.date')}</th>
150
+ <th>{t('logs.tableHeaders.status')}</th>
151
+ <th>{t('logs.tableHeaders.email')}</th>
152
+ <th>{t('logs.tableHeaders.ip')}</th>
153
+ <th>{t('logs.tableHeaders.userAgent')}</th>
154
+ </tr>
155
+ </thead>
128
156
  <tbody>
129
157
  {logs.map((log) => (
130
158
  <tr key={log.id}>
131
- <td style={{ ...S.td, ...S.mono }}>{fmtDate(log.createdAt)}</td>
132
- <td style={S.td}><span style={{ ...S.badge, background: log.success ? '#dcfce7' : '#fef2f2', color: log.success ? '#16a34a' : '#dc2626' }}>{log.success ? t('logs.statusSuccess') : t('logs.statusFailed')}</span></td>
133
- <td style={{ ...S.td, ...S.mono }}>{log.email || '--'}</td>
134
- <td style={{ ...S.td, ...S.mono }}>{log.ip || '--'}</td>
135
- <td style={{ ...S.td, fontSize: 11, maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{log.userAgent || '--'}</td>
159
+ <td className={s.mono}>{fmtDate(log.createdAt)}</td>
160
+ <td>
161
+ <span className={s.badge} style={{
162
+ background: log.success ? '#dcfce7' : '#fef2f2',
163
+ color: log.success ? '#16a34a' : '#dc2626',
164
+ }}>
165
+ {log.success ? t('logs.statusSuccess') : t('logs.statusFailed')}
166
+ </span>
167
+ </td>
168
+ <td className={s.mono}>{log.email || '—'}</td>
169
+ <td className={s.mono}>{log.ip || '—'}</td>
170
+ <td className={s.truncate} style={{ fontSize: 11 }}>{log.userAgent || '—'}</td>
136
171
  </tr>
137
172
  ))}
138
173
  </tbody>
@@ -140,10 +175,10 @@ export const LogsClient: React.FC = () => {
140
175
  )}
141
176
 
142
177
  {totalDocs > 0 && (
143
- <div style={{ display: 'flex', justifyContent: 'center', gap: 12, marginTop: 16, alignItems: 'center' }}>
144
- <button style={S.purgeBtn} onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page <= 1}>{t('common.previous')}</button>
145
- <span style={{ fontSize: 12, color: 'var(--theme-elevation-500)' }}>{t('common.page')} {page} -- {totalDocs} {t('common.results')}</span>
146
- <button style={S.purgeBtn} onClick={() => setPage((p) => p + 1)} disabled={!hasMore}>{t('common.next')}</button>
178
+ <div className={s.pagination}>
179
+ <button className={s.pageBtn} onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page <= 1}>{t('common.previous')}</button>
180
+ <span className={s.pageInfo}>{t('common.page')} {page} {totalDocs} {t('common.results')}</span>
181
+ <button className={s.pageBtn} onClick={() => setPage((p) => p + 1)} disabled={!hasMore}>{t('common.next')}</button>
147
182
  </div>
148
183
  )}
149
184
  </div>
@@ -48,7 +48,7 @@ export const NewTicketClient: React.FC = () => {
48
48
  try {
49
49
  const res = await fetch(`/api/support-clients?where[or][0][email][contains]=${encodeURIComponent(clientSearch)}&where[or][1][firstName][contains]=${encodeURIComponent(clientSearch)}&where[or][2][company][contains]=${encodeURIComponent(clientSearch)}&limit=8&depth=0`, { credentials: 'include' })
50
50
  if (res.ok) { const d = await res.json(); setClientResults(d.docs || []) }
51
- } catch (err) { console.warn('[support] client search error:', err) }
51
+ } catch {}
52
52
  }, 300)
53
53
  return () => clearTimeout(timer)
54
54
  }, [clientSearch])
@@ -70,6 +70,7 @@ export const NewTicketClient: React.FC = () => {
70
70
 
71
71
  setSubmitting(true)
72
72
  try {
73
+ // Create ticket
73
74
  const res = await fetch('/api/tickets', {
74
75
  method: 'POST',
75
76
  headers: { 'Content-Type': 'application/json' },
@@ -93,6 +94,7 @@ export const NewTicketClient: React.FC = () => {
93
94
 
94
95
  const ticket = await res.json()
95
96
 
97
+ // Create first message if description provided
96
98
  if (description.trim()) {
97
99
  await fetch('/api/ticket-messages', {
98
100
  method: 'POST',
@@ -115,66 +117,48 @@ export const NewTicketClient: React.FC = () => {
115
117
  }
116
118
  }
117
119
 
118
- const S: Record<string, React.CSSProperties> = {
119
- page: { padding: '20px 30px', maxWidth: 720, margin: '0 auto' },
120
- header: { marginBottom: 24 },
121
- backLink: { fontSize: 13, color: '#2563eb', textDecoration: 'none' },
122
- title: { fontSize: 24, fontWeight: 700, margin: '8px 0 4px', color: 'var(--theme-text)' },
123
- subtitle: { fontSize: 14, color: 'var(--theme-elevation-500)' },
124
- error: { padding: '10px 14px', borderRadius: 8, background: '#fef2f2', color: '#dc2626', fontSize: 13, marginBottom: 16, border: '1px solid #fecaca' },
125
- fieldGroup: { marginBottom: 16 },
126
- label: { display: 'block', fontSize: 13, fontWeight: 600, marginBottom: 6, color: 'var(--theme-text)' },
127
- required: { color: '#dc2626' },
128
- input: { width: '100%', padding: '8px 12px', borderRadius: 8, border: '1px solid var(--theme-elevation-200)', fontSize: 13, color: 'var(--theme-text)', background: 'var(--theme-elevation-0)' },
129
- select: { width: '100%', padding: '8px 12px', borderRadius: 8, border: '1px solid var(--theme-elevation-200)', fontSize: 13, color: 'var(--theme-text)', background: 'var(--theme-elevation-0)' },
130
- textarea: { width: '100%', padding: '8px 12px', borderRadius: 8, border: '1px solid var(--theme-elevation-200)', fontSize: 13, color: 'var(--theme-text)', background: 'var(--theme-elevation-0)', minHeight: 120, fontFamily: 'inherit', resize: 'vertical' as const },
131
- row3: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12, marginBottom: 16 },
132
- submitBtn: { padding: '10px 20px', borderRadius: 8, background: '#2563eb', color: '#fff', fontSize: 14, fontWeight: 600, border: 'none', cursor: 'pointer' },
133
- searchResults: { position: 'absolute' as const, top: '100%', left: 0, right: 0, background: 'var(--theme-elevation-0)', border: '1px solid var(--theme-elevation-200)', borderRadius: 8, boxShadow: '0 4px 12px rgba(0,0,0,0.1)', zIndex: 50, maxHeight: 200, overflowY: 'auto' as const },
134
- searchItem: { padding: '8px 12px', cursor: 'pointer', fontSize: 13, borderBottom: '1px solid var(--theme-elevation-100)' },
135
- }
136
-
137
120
  return (
138
- <div style={S.page}>
139
- <div style={S.header}>
140
- <Link href="/admin/support/inbox" style={S.backLink}>&larr; {t('newTicket.backToInbox')}</Link>
141
- <h1 style={S.title}>{t('newTicket.title')}</h1>
142
- <p style={S.subtitle}>{t('newTicket.subtitle')}</p>
121
+ <div className={s.page}>
122
+ <div className={s.header}>
123
+ <Link href="/admin/support/inbox" className={s.backLink}>&larr; {t('newTicket.backToInbox')}</Link>
124
+ <h1 className={s.title}>{t('newTicket.title')}</h1>
125
+ <p className={s.subtitle}>{t('newTicket.subtitle')}</p>
143
126
  </div>
144
127
 
145
- {error && <div style={S.error}>{error}</div>}
128
+ {error && <div className={s.error}>{error}</div>}
146
129
 
147
- <form onSubmit={handleSubmit}>
130
+ <form className={s.form} onSubmit={handleSubmit}>
148
131
  {/* Client search */}
149
- <div style={S.fieldGroup}>
150
- <label style={S.label}>{t('newTicket.clientLabel')} <span style={S.required}>*</span></label>
132
+ <div className={s.fieldGroup}>
133
+ <label className={s.label}>{t('newTicket.clientLabel')} <span className={s.required}>*</span></label>
151
134
  {selectedClient ? (
152
135
  <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 14px', borderRadius: 10, border: '1px solid var(--theme-elevation-200)', background: 'var(--theme-elevation-50)' }}>
153
136
  <span style={{ fontWeight: 600, fontSize: 13, color: 'var(--theme-text)' }}>
154
- {selectedClient.firstName} {selectedClient.lastName} -- {selectedClient.company}
137
+ {selectedClient.firstName} {selectedClient.lastName} {selectedClient.company}
155
138
  </span>
156
139
  <span style={{ fontSize: 12, color: 'var(--theme-elevation-500)' }}>{selectedClient.email}</span>
157
140
  <button type="button" onClick={() => { setSelectedClient(null); setClientId(null); setClientSearch('') }} style={{ marginLeft: 'auto', background: 'none', border: 'none', cursor: 'pointer', color: 'var(--theme-elevation-400)', fontSize: 16 }}>&times;</button>
158
141
  </div>
159
142
  ) : (
160
- <div style={{ position: 'relative' }}>
143
+ <div className={s.searchWrap}>
161
144
  <input
162
145
  type="text"
163
- style={S.input}
146
+ className={s.input}
164
147
  placeholder={t('newTicket.clientSearchPlaceholder')}
165
148
  value={clientSearch}
166
149
  onChange={(e) => setClientSearch(e.target.value)}
150
+ style={{ width: '100%' }}
167
151
  />
168
152
  {clientResults.length > 0 && (
169
- <div style={S.searchResults}>
153
+ <div className={s.searchResults}>
170
154
  {clientResults.map((c) => (
171
- <div key={c.id} style={S.searchItem} onClick={() => {
155
+ <div key={c.id} className={s.searchItem} onClick={() => {
172
156
  setSelectedClient(c)
173
157
  setClientId(c.id)
174
158
  setClientSearch('')
175
159
  setClientResults([])
176
160
  }}>
177
- <strong>{c.firstName} {c.lastName}</strong> -- {c.company} <span style={{ color: 'var(--theme-elevation-400)', fontSize: 12 }}>{c.email}</span>
161
+ <strong>{c.firstName} {c.lastName}</strong> {c.company} <span style={{ color: 'var(--theme-elevation-400)', fontSize: 12 }}>{c.email}</span>
178
162
  </div>
179
163
  ))}
180
164
  </div>
@@ -184,41 +168,41 @@ export const NewTicketClient: React.FC = () => {
184
168
  </div>
185
169
 
186
170
  {/* Subject */}
187
- <div style={S.fieldGroup}>
188
- <label style={S.label}>{t('newTicket.subjectLabel')} <span style={S.required}>*</span></label>
189
- <input type="text" style={S.input} placeholder={t('newTicket.subjectPlaceholder')} value={subject} onChange={(e) => setSubject(e.target.value)} />
171
+ <div className={s.fieldGroup}>
172
+ <label className={s.label}>{t('newTicket.subjectLabel')} <span className={s.required}>*</span></label>
173
+ <input type="text" className={s.input} placeholder={t('newTicket.subjectPlaceholder')} value={subject} onChange={(e) => setSubject(e.target.value)} />
190
174
  </div>
191
175
 
192
176
  {/* Category + Priority + Project */}
193
- <div style={S.row3}>
194
- <div style={S.fieldGroup}>
195
- <label style={S.label}>{t('newTicket.categoryLabel')}</label>
196
- <select style={S.select} value={category} onChange={(e) => setCategory(e.target.value)}>
177
+ <div className={s.row3}>
178
+ <div className={s.fieldGroup}>
179
+ <label className={s.label}>{t('newTicket.categoryLabel')}</label>
180
+ <select className={s.select} value={category} onChange={(e) => setCategory(e.target.value)}>
197
181
  {CATEGORY_KEYS.map((c) => <option key={c.value} value={c.value}>{t(c.key)}</option>)}
198
182
  </select>
199
183
  </div>
200
- <div style={S.fieldGroup}>
201
- <label style={S.label}>{t('newTicket.priorityLabel')}</label>
202
- <select style={S.select} value={priority} onChange={(e) => setPriority(e.target.value)}>
184
+ <div className={s.fieldGroup}>
185
+ <label className={s.label}>{t('newTicket.priorityLabel')}</label>
186
+ <select className={s.select} value={priority} onChange={(e) => setPriority(e.target.value)}>
203
187
  {PRIORITY_KEYS.map((p) => <option key={p.value} value={p.value}>{t(p.key)}</option>)}
204
188
  </select>
205
189
  </div>
206
- <div style={S.fieldGroup}>
207
- <label style={S.label}>{t('newTicket.projectLabel')}</label>
208
- <select style={S.select} value={projectId || ''} onChange={(e) => setProjectId(e.target.value ? Number(e.target.value) : null)}>
209
- <option value="">{t('ticket.noProject')}</option>
190
+ <div className={s.fieldGroup}>
191
+ <label className={s.label}>{t('newTicket.projectLabel')}</label>
192
+ <select className={s.select} value={projectId || ''} onChange={(e) => setProjectId(e.target.value ? Number(e.target.value) : null)}>
193
+ <option value="">— Aucun —</option>
210
194
  {projects.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
211
195
  </select>
212
196
  </div>
213
197
  </div>
214
198
 
215
199
  {/* Description */}
216
- <div style={S.fieldGroup}>
217
- <label style={S.label}>{t('newTicket.descriptionLabel')}</label>
218
- <textarea style={S.textarea} placeholder={t('newTicket.descriptionPlaceholder')} value={description} onChange={(e) => setDescription(e.target.value)} />
200
+ <div className={s.fieldGroup}>
201
+ <label className={s.label}>{t('newTicket.descriptionLabel')}</label>
202
+ <textarea className={s.textarea} placeholder={t('newTicket.descriptionPlaceholder')} value={description} onChange={(e) => setDescription(e.target.value)} />
219
203
  </div>
220
204
 
221
- <button type="submit" style={S.submitBtn} disabled={submitting}>
205
+ <button type="submit" className={s.submitBtn} disabled={submitting}>
222
206
  {submitting ? t('newTicket.submitting') : t('newTicket.submitButton')}
223
207
  </button>
224
208
  </form>