@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.
- package/dist/components/RichTextEditor/index.cjs +233 -0
- package/dist/components/RichTextEditor/index.js +232 -0
- package/dist/components/TicketConversation/components/CodeBlock.cjs +24 -7
- package/dist/components/TicketConversation/components/CodeBlock.js +23 -9
- package/dist/index.cjs +19 -1
- package/dist/index.js +19 -1
- package/dist/views/BillingView/client.cjs +260 -103
- package/dist/views/BillingView/client.js +259 -103
- package/dist/views/ChatView/client.cjs +184 -137
- package/dist/views/ChatView/client.js +180 -137
- package/dist/views/CrmView/client.cjs +270 -122
- package/dist/views/CrmView/client.js +266 -122
- package/dist/views/EmailTrackingView/client.cjs +80 -69
- package/dist/views/EmailTrackingView/client.js +80 -70
- package/dist/views/ImportConversationView/client.cjs +127 -94
- package/dist/views/ImportConversationView/client.js +123 -94
- package/dist/views/LogsView/client.cjs +56 -58
- package/dist/views/LogsView/client.js +52 -58
- package/dist/views/NewTicketView/client.cjs +39 -55
- package/dist/views/NewTicketView/client.js +38 -55
- package/dist/views/PendingEmailsView/client.cjs +399 -102
- package/dist/views/PendingEmailsView/client.js +396 -103
- package/dist/views/SupportDashboardView/client.cjs +276 -137
- package/dist/views/SupportDashboardView/client.js +275 -137
- package/dist/views/TicketDetailView/client.cjs +487 -204
- package/dist/views/TicketDetailView/client.js +486 -204
- package/dist/views/TicketInboxView/client.cjs +62 -65
- package/dist/views/TicketInboxView/client.js +62 -66
- package/dist/views/TicketingSettingsView/client.cjs +10 -8
- package/dist/views/TicketingSettingsView/client.js +10 -8
- package/dist/views/TimeDashboardView/client.cjs +70 -59
- package/dist/views/TimeDashboardView/client.js +69 -59
- package/package.json +7 -3
- package/src/components/RichTextEditor/index.tsx +261 -0
- package/src/components/TicketConversation/components/CodeBlock.tsx +53 -14
- package/src/plugin.ts +2 -0
- package/src/utils/emailTemplate.ts +37 -0
- package/src/views/BillingView/client.tsx +362 -69
- package/src/views/ChatView/client.tsx +225 -140
- package/src/views/CrmView/client.tsx +447 -189
- package/src/views/EmailTrackingView/client.tsx +111 -71
- package/src/views/ImportConversationView/client.tsx +255 -70
- package/src/views/LogsView/client.tsx +85 -50
- package/src/views/NewTicketView/client.tsx +37 -53
- package/src/views/PendingEmailsView/client.tsx +512 -92
- package/src/views/SupportDashboardView/client.tsx +294 -134
- package/src/views/TicketDetailView/client.tsx +486 -213
- package/src/views/TicketInboxView/client.tsx +52 -61
- package/src/views/TicketingSettingsView/client.tsx +10 -9
- 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 ? '
|
|
65
|
-
if (!window.confirm(
|
|
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) {
|
|
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
|
|
89
|
-
<div
|
|
90
|
-
<h1
|
|
91
|
-
<div
|
|
92
|
-
<button
|
|
93
|
-
<button
|
|
94
|
-
<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
|
-
|
|
99
|
-
|
|
100
|
-
<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
|
|
101
|
+
{purgeResult && <div className={s.purgeResult}>{purgeResult}</div>}
|
|
104
102
|
|
|
105
103
|
{loading ? (
|
|
106
|
-
<div
|
|
104
|
+
<div className={s.loading}>{t('common.loading')}</div>
|
|
107
105
|
) : logs.length === 0 ? (
|
|
108
|
-
<div
|
|
106
|
+
<div className={s.empty}>{t('logs.noLogs')}</div>
|
|
109
107
|
) : logType === 'email' ? (
|
|
110
|
-
<table
|
|
111
|
-
<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
|
|
116
|
-
<td
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
127
|
-
<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
|
|
132
|
-
<td
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
144
|
-
<button
|
|
145
|
-
<span
|
|
146
|
-
<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
|
|
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
|
|
139
|
-
<div
|
|
140
|
-
<Link href="/admin/support/inbox"
|
|
141
|
-
<h1
|
|
142
|
-
<p
|
|
121
|
+
<div className={s.page}>
|
|
122
|
+
<div className={s.header}>
|
|
123
|
+
<Link href="/admin/support/inbox" className={s.backLink}>← {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
|
|
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
|
|
150
|
-
<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}
|
|
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 }}>×</button>
|
|
158
141
|
</div>
|
|
159
142
|
) : (
|
|
160
|
-
<div
|
|
143
|
+
<div className={s.searchWrap}>
|
|
161
144
|
<input
|
|
162
145
|
type="text"
|
|
163
|
-
|
|
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
|
|
153
|
+
<div className={s.searchResults}>
|
|
170
154
|
{clientResults.map((c) => (
|
|
171
|
-
<div key={c.id}
|
|
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>
|
|
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
|
|
188
|
-
<label
|
|
189
|
-
<input type="text"
|
|
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
|
|
194
|
-
<div
|
|
195
|
-
<label
|
|
196
|
-
<select
|
|
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
|
|
201
|
-
<label
|
|
202
|
-
<select
|
|
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
|
|
207
|
-
<label
|
|
208
|
-
<select
|
|
209
|
-
<option value=""
|
|
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
|
|
217
|
-
<label
|
|
218
|
-
<textarea
|
|
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"
|
|
205
|
+
<button type="submit" className={s.submitBtn} disabled={submitting}>
|
|
222
206
|
{submitting ? t('newTicket.submitting') : t('newTicket.submitButton')}
|
|
223
207
|
</button>
|
|
224
208
|
</form>
|