@consilioweb/payload-support 0.10.1 → 0.12.0
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/TicketConversation/locales/en.json +25 -1
- package/dist/components/TicketConversation/locales/fr.json +25 -1
- package/dist/index.cjs +799 -4
- package/dist/index.d.cts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +800 -6
- package/dist/styles/TicketDetail.module.scss +899 -353
- package/dist/views/TicketDetailView/client.cjs +504 -282
- package/dist/views/TicketDetailView/client.js +505 -283
- package/package.json +1 -1
- package/src/collections/ClientSummaries.ts +16 -0
- package/src/collections/TicketCollaborators.ts +93 -0
- package/src/collections/TicketFeedback.ts +116 -0
- package/src/collections/TicketMessages.ts +9 -0
- package/src/collections/Tickets.ts +31 -1
- package/src/collections/index.ts +2 -0
- package/src/components/TicketConversation/locales/en.json +25 -1
- package/src/components/TicketConversation/locales/fr.json +25 -1
- package/src/endpoints/escalate.ts +44 -0
- package/src/endpoints/index.ts +15 -0
- package/src/endpoints/invite-collaborator.ts +215 -0
- package/src/endpoints/kb-search.ts +156 -0
- package/src/endpoints/ticket-feedback.ts +104 -0
- package/src/endpoints/transfer-ticket.ts +248 -0
- package/src/index.ts +1 -0
- package/src/plugin.ts +4 -0
- package/src/portal/auth/ChatWidget.tsx +11 -0
- package/src/portal/auth/dashboard/DashboardClient.tsx +85 -99
- package/src/portal/auth/dashboard/page.tsx +11 -2
- package/src/portal/auth/tickets/detail/TransferAndInviteActions.tsx +402 -0
- package/src/portal/auth/tickets/detail/page.tsx +122 -23
- package/src/portal/auth/tickets/new/KbDeflection.tsx +128 -0
- package/src/portal/auth/tickets/new/page.tsx +203 -100
- package/src/portal/locales/en.json +5 -0
- package/src/portal/locales/fr.json +5 -0
- package/src/styles/TicketDetail.module.scss +899 -353
- package/src/types.ts +19 -0
- package/src/utils/slugs.ts +2 -0
- package/src/views/TicketDetailView/client.tsx +404 -121
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
interface KbResult {
|
|
6
|
+
id: number | string
|
|
7
|
+
title: string
|
|
8
|
+
slug: string
|
|
9
|
+
excerpt: string
|
|
10
|
+
category: string
|
|
11
|
+
score: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface KbDeflectionProps {
|
|
15
|
+
query: string
|
|
16
|
+
/** Max number of suggestions to show (default 3). */
|
|
17
|
+
limit?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Live KB suggestions under the "Subject" field of the new-ticket form.
|
|
22
|
+
*
|
|
23
|
+
* - Debounces the query (400ms) to avoid hitting the endpoint on each keystroke.
|
|
24
|
+
* - Hides itself when the query is too short (< 3 chars) or returns no results.
|
|
25
|
+
* - Renders nothing while loading the first time to avoid flicker.
|
|
26
|
+
*
|
|
27
|
+
* The goal is deflection: let the client find an answer before submitting a
|
|
28
|
+
* ticket. Opens articles in a new tab so the form state is preserved.
|
|
29
|
+
*/
|
|
30
|
+
export default function KbDeflection({ query, limit = 3 }: KbDeflectionProps) {
|
|
31
|
+
const [results, setResults] = useState<KbResult[]>([])
|
|
32
|
+
const [dismissed, setDismissed] = useState(false)
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const trimmed = (query || '').trim()
|
|
36
|
+
if (trimmed.length < 3) {
|
|
37
|
+
setResults([])
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ctrl = new AbortController()
|
|
42
|
+
const timer = window.setTimeout(async () => {
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch(
|
|
45
|
+
`/api/support/kb/search?q=${encodeURIComponent(trimmed)}&limit=${limit}`,
|
|
46
|
+
{ signal: ctrl.signal, credentials: 'include' },
|
|
47
|
+
)
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
setResults([])
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
const data = (await res.json()) as { results?: KbResult[] }
|
|
53
|
+
setResults(Array.isArray(data.results) ? data.results : [])
|
|
54
|
+
} catch (err) {
|
|
55
|
+
if ((err as Error).name === 'AbortError') return
|
|
56
|
+
setResults([])
|
|
57
|
+
}
|
|
58
|
+
}, 400)
|
|
59
|
+
|
|
60
|
+
return () => {
|
|
61
|
+
ctrl.abort()
|
|
62
|
+
window.clearTimeout(timer)
|
|
63
|
+
}
|
|
64
|
+
}, [query, limit])
|
|
65
|
+
|
|
66
|
+
// Reset dismissal whenever the user types a new query.
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
setDismissed(false)
|
|
69
|
+
}, [query])
|
|
70
|
+
|
|
71
|
+
if (dismissed) return null
|
|
72
|
+
if (!results || results.length === 0) return null
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div
|
|
76
|
+
role="region"
|
|
77
|
+
aria-label="Articles suggérés"
|
|
78
|
+
className="animate-in slide-in-from-top-2 fade-in mt-3 rounded-xl border border-blue-100 bg-blue-50/60 p-4 duration-300 dark:border-blue-900/40 dark:bg-blue-950/30"
|
|
79
|
+
>
|
|
80
|
+
<div className="mb-3 flex items-start justify-between gap-3">
|
|
81
|
+
<p className="text-sm font-semibold text-blue-700 dark:text-blue-300">
|
|
82
|
+
💡 Ces articles peuvent répondre à votre question
|
|
83
|
+
</p>
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
onClick={() => setDismissed(true)}
|
|
87
|
+
className="text-[11px] font-medium text-blue-600/70 hover:text-blue-700 dark:text-blue-300/70 dark:hover:text-blue-200"
|
|
88
|
+
>
|
|
89
|
+
Ignorer
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<ul className="space-y-2">
|
|
94
|
+
{results.map((r) => (
|
|
95
|
+
<li
|
|
96
|
+
key={r.id}
|
|
97
|
+
className="rounded-lg border border-blue-100 bg-white p-3 transition-colors hover:border-blue-300 hover:bg-blue-50/40 dark:border-blue-900/50 dark:bg-slate-900/40 dark:hover:border-blue-700"
|
|
98
|
+
>
|
|
99
|
+
<div className="flex items-start justify-between gap-3">
|
|
100
|
+
<div className="min-w-0 flex-1">
|
|
101
|
+
<h4 className="truncate text-sm font-semibold text-slate-800 dark:text-slate-100">
|
|
102
|
+
{r.title}
|
|
103
|
+
</h4>
|
|
104
|
+
{r.excerpt && (
|
|
105
|
+
<p className="mt-1 line-clamp-2 text-xs leading-relaxed text-slate-500 dark:text-slate-400">
|
|
106
|
+
{r.excerpt}
|
|
107
|
+
</p>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
<a
|
|
111
|
+
href={`/support/kb/${r.slug}`}
|
|
112
|
+
target="_blank"
|
|
113
|
+
rel="noopener noreferrer"
|
|
114
|
+
className="flex-shrink-0 rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-medium text-white shadow-sm transition hover:bg-blue-700"
|
|
115
|
+
>
|
|
116
|
+
Lire l'article
|
|
117
|
+
</a>
|
|
118
|
+
</div>
|
|
119
|
+
</li>
|
|
120
|
+
))}
|
|
121
|
+
</ul>
|
|
122
|
+
|
|
123
|
+
<p className="mt-3 text-[11px] text-slate-500 dark:text-slate-400">
|
|
124
|
+
Mon problème n'est pas résolu — continuez à remplir le formulaire pour créer un ticket.
|
|
125
|
+
</p>
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
|
4
4
|
import { useRouter } from 'next/navigation'
|
|
5
|
+
import KbDeflection from './KbDeflection'
|
|
5
6
|
|
|
6
7
|
const categories = [
|
|
7
|
-
{ label: 'Bug / Dysfonctionnement', value: 'bug', icon: '🐛' },
|
|
8
|
-
{ label: 'Modification de contenu', value: 'content', icon: '📝' },
|
|
9
|
-
{ label: 'Nouvelle fonctionnalité', value: 'feature', icon: '✨' },
|
|
10
|
-
{ label: 'Question / Aide', value: 'question', icon: '💬' },
|
|
11
|
-
{ label: 'Hébergement / Domaine', value: 'hosting', icon: '🌐' },
|
|
8
|
+
{ label: 'Bug / Dysfonctionnement', shortLabel: 'Bug', desc: 'Quelque chose ne fonctionne pas', value: 'bug', icon: '🐛' },
|
|
9
|
+
{ label: 'Modification de contenu', shortLabel: 'Contenu', desc: 'Texte, image, page', value: 'content', icon: '📝' },
|
|
10
|
+
{ label: 'Nouvelle fonctionnalité', shortLabel: 'Feature', desc: 'Idee, evolution', value: 'feature', icon: '✨' },
|
|
11
|
+
{ label: 'Question / Aide', shortLabel: 'Question', desc: 'Information, conseil', value: 'question', icon: '💬' },
|
|
12
|
+
{ label: 'Hébergement / Domaine', shortLabel: 'Hebergement', desc: 'DNS, mail, performance', value: 'hosting', icon: '🌐' },
|
|
12
13
|
]
|
|
13
14
|
|
|
14
15
|
const templates: Record<string, { subject: string; message: string }> = {
|
|
@@ -30,13 +31,49 @@ const templates: Record<string, { subject: string; message: string }> = {
|
|
|
30
31
|
},
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
// SLA fallback values (hours) used when no sla-policies record matches.
|
|
35
|
+
// Aligned with the brief: urgent=2h, high=4h, normal=24h, low=48h.
|
|
36
|
+
const SLA_FALLBACK_HOURS: Record<string, number> = {
|
|
37
|
+
urgent: 2,
|
|
38
|
+
high: 4,
|
|
39
|
+
normal: 24,
|
|
40
|
+
low: 48,
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
const priorities = [
|
|
34
|
-
{
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
{
|
|
45
|
+
label: 'Pas urgent',
|
|
46
|
+
value: 'low',
|
|
47
|
+
desc: 'Peut attendre quelques jours',
|
|
48
|
+
color: 'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
label: 'Normal',
|
|
52
|
+
value: 'normal',
|
|
53
|
+
desc: 'Demande standard',
|
|
54
|
+
color: 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
label: 'Important',
|
|
58
|
+
value: 'high',
|
|
59
|
+
desc: 'Genere une gene quotidienne',
|
|
60
|
+
color: 'bg-amber-50 text-amber-600 dark:bg-amber-900/30 dark:text-amber-400',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
label: 'Urgent',
|
|
64
|
+
value: 'urgent',
|
|
65
|
+
desc: 'Bloque mon activite',
|
|
66
|
+
color: 'bg-red-50 text-red-600 dark:bg-red-900/30 dark:text-red-400',
|
|
67
|
+
},
|
|
38
68
|
]
|
|
39
69
|
|
|
70
|
+
function formatSla(hours: number): string {
|
|
71
|
+
if (hours < 1) return 'moins d\'1h'
|
|
72
|
+
if (hours < 24) return `${hours}h`
|
|
73
|
+
const days = Math.round(hours / 24)
|
|
74
|
+
return days === 1 ? '1 jour' : `${days} jours`
|
|
75
|
+
}
|
|
76
|
+
|
|
40
77
|
interface Project {
|
|
41
78
|
id: number | string
|
|
42
79
|
name: string
|
|
@@ -58,7 +95,8 @@ export default function NewTicketPage() {
|
|
|
58
95
|
const [error, setError] = useState('')
|
|
59
96
|
const [loading, setLoading] = useState(false)
|
|
60
97
|
const [isDragging, setIsDragging] = useState(false)
|
|
61
|
-
|
|
98
|
+
// Map of priority → SLA in hours, loaded from sla-policies when available.
|
|
99
|
+
const [slaByPriority, setSlaByPriority] = useState<Record<string, number>>(SLA_FALLBACK_HOURS)
|
|
62
100
|
|
|
63
101
|
useEffect(() => {
|
|
64
102
|
const fetchProjects = async () => {
|
|
@@ -75,6 +113,32 @@ export default function NewTicketPage() {
|
|
|
75
113
|
fetchProjects()
|
|
76
114
|
}, [])
|
|
77
115
|
|
|
116
|
+
// Load SLA policies — try to read first-response targets and override fallback per priority.
|
|
117
|
+
// Endpoint may not be public; on failure we just keep the fallback.
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
const fetchSla = async () => {
|
|
120
|
+
try {
|
|
121
|
+
const res = await fetch('/api/sla-policies?limit=10&depth=0', { credentials: 'include' })
|
|
122
|
+
if (!res.ok) return
|
|
123
|
+
const data = await res.json()
|
|
124
|
+
const next: Record<string, number> = { ...SLA_FALLBACK_HOURS }
|
|
125
|
+
for (const policy of data.docs || []) {
|
|
126
|
+
// SLA policy shape may vary — try common field names defensively.
|
|
127
|
+
const prio = (policy.priority || policy.level || '').toLowerCase()
|
|
128
|
+
const responseHours: number | undefined =
|
|
129
|
+
policy.firstResponseHours ??
|
|
130
|
+
policy.responseTimeHours ??
|
|
131
|
+
(typeof policy.firstResponseMinutes === 'number' ? policy.firstResponseMinutes / 60 : undefined)
|
|
132
|
+
if (prio && typeof responseHours === 'number' && responseHours > 0) {
|
|
133
|
+
next[prio] = responseHours
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
setSlaByPriority(next)
|
|
137
|
+
} catch { /* keep fallback */ }
|
|
138
|
+
}
|
|
139
|
+
fetchSla()
|
|
140
|
+
}, [])
|
|
141
|
+
|
|
78
142
|
const addFiles = useCallback((newFiles: File[]) => {
|
|
79
143
|
const tooLarge = newFiles.filter((f) => f.size > MAX_FILE_SIZE)
|
|
80
144
|
if (tooLarge.length > 0) {
|
|
@@ -273,6 +337,8 @@ export default function NewTicketPage() {
|
|
|
273
337
|
placeholder="Ex: Erreur 404 sur la page contact"
|
|
274
338
|
className="w-full rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 px-4 py-3 text-sm text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 outline-none transition-all duration-200 focus:border-blue-500 focus:bg-white dark:focus:bg-slate-900 focus:ring-4 focus:ring-blue-500/10"
|
|
275
339
|
/>
|
|
340
|
+
{/* KB deflection — suggest existing articles before ticket creation */}
|
|
341
|
+
<KbDeflection query={subject} limit={3} />
|
|
276
342
|
</div>
|
|
277
343
|
|
|
278
344
|
{/* Description */}
|
|
@@ -380,102 +446,139 @@ export default function NewTicketPage() {
|
|
|
380
446
|
)}
|
|
381
447
|
</div>
|
|
382
448
|
|
|
383
|
-
{/*
|
|
384
|
-
<div className="
|
|
385
|
-
<
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
449
|
+
{/* Category — radio cards in a responsive grid (no longer hidden behind a toggle) */}
|
|
450
|
+
<div className="mb-6">
|
|
451
|
+
<label className="mb-2 block text-sm font-semibold text-slate-700 dark:text-slate-200">
|
|
452
|
+
Categorie
|
|
453
|
+
</label>
|
|
454
|
+
<div
|
|
455
|
+
role="radiogroup"
|
|
456
|
+
aria-label="Categorie du ticket"
|
|
457
|
+
className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-5"
|
|
389
458
|
>
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
className={`h-4 w-4 transition-transform duration-200 ${showAdvanced ? 'rotate-180' : ''}`}
|
|
401
|
-
>
|
|
402
|
-
<path fillRule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clipRule="evenodd" />
|
|
403
|
-
</svg>
|
|
404
|
-
</button>
|
|
405
|
-
|
|
406
|
-
{showAdvanced && (
|
|
407
|
-
<div className="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-3 rounded-xl border border-slate-100 dark:border-slate-700/50 bg-slate-50/50 dark:bg-slate-900/30 p-5">
|
|
408
|
-
{/* Category */}
|
|
409
|
-
<div>
|
|
410
|
-
<label htmlFor="category" className="mb-2 block text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
|
|
411
|
-
Categorie
|
|
412
|
-
</label>
|
|
413
|
-
<select
|
|
414
|
-
id="category"
|
|
415
|
-
value={category}
|
|
416
|
-
onChange={(e) => {
|
|
417
|
-
const val = e.target.value
|
|
418
|
-
setCategory(val)
|
|
419
|
-
const tmpl = templates[val]
|
|
420
|
-
if (tmpl) {
|
|
421
|
-
if (!message.trim()) setMessage(tmpl.message)
|
|
422
|
-
if (!subject.trim() && tmpl.subject) setSubject(tmpl.subject)
|
|
423
|
-
}
|
|
424
|
-
}}
|
|
425
|
-
className="w-full rounded-lg border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2.5 text-sm text-slate-900 dark:text-white outline-none transition-all focus:border-blue-500 focus:ring-4 focus:ring-blue-500/10"
|
|
459
|
+
{categories.map((cat) => {
|
|
460
|
+
const isActive = category === cat.value
|
|
461
|
+
return (
|
|
462
|
+
<label
|
|
463
|
+
key={cat.value}
|
|
464
|
+
className={`group relative flex cursor-pointer flex-col items-start gap-1 rounded-xl border p-3 text-left transition-all ${
|
|
465
|
+
isActive
|
|
466
|
+
? 'border-blue-500 bg-blue-50 ring-2 ring-blue-500/20 dark:border-blue-400 dark:bg-blue-950/40'
|
|
467
|
+
: 'border-slate-200 bg-white hover:border-slate-300 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800/50 dark:hover:border-slate-600 dark:hover:bg-slate-800'
|
|
468
|
+
}`}
|
|
426
469
|
>
|
|
427
|
-
<
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
470
|
+
<input
|
|
471
|
+
type="radio"
|
|
472
|
+
name="category"
|
|
473
|
+
value={cat.value}
|
|
474
|
+
checked={isActive}
|
|
475
|
+
onChange={() => {
|
|
476
|
+
setCategory(cat.value)
|
|
477
|
+
const tmpl = templates[cat.value]
|
|
478
|
+
if (tmpl) {
|
|
479
|
+
if (!message.trim()) setMessage(tmpl.message)
|
|
480
|
+
if (!subject.trim() && tmpl.subject) setSubject(tmpl.subject)
|
|
481
|
+
}
|
|
482
|
+
}}
|
|
483
|
+
className="sr-only"
|
|
484
|
+
/>
|
|
485
|
+
<span className="text-xl" aria-hidden>{cat.icon}</span>
|
|
486
|
+
<span className={`text-sm font-semibold ${
|
|
487
|
+
isActive ? 'text-blue-700 dark:text-blue-300' : 'text-slate-800 dark:text-slate-200'
|
|
488
|
+
}`}>
|
|
489
|
+
{cat.shortLabel}
|
|
490
|
+
</span>
|
|
491
|
+
<span className="text-xs text-slate-500 dark:text-slate-400">
|
|
492
|
+
{cat.desc}
|
|
493
|
+
</span>
|
|
440
494
|
</label>
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
495
|
+
)
|
|
496
|
+
})}
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
|
|
500
|
+
{/* Priority — radio cards with dynamic SLA per level */}
|
|
501
|
+
<div className="mb-6">
|
|
502
|
+
<label className="mb-2 block text-sm font-semibold text-slate-700 dark:text-slate-200">
|
|
503
|
+
Niveau d'urgence
|
|
504
|
+
</label>
|
|
505
|
+
<div role="radiogroup" aria-label="Niveau d'urgence" className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
506
|
+
{priorities.map((p) => {
|
|
507
|
+
const isActive = priority === p.value
|
|
508
|
+
const slaHours = slaByPriority[p.value] ?? SLA_FALLBACK_HOURS[p.value]
|
|
509
|
+
return (
|
|
510
|
+
<label
|
|
511
|
+
key={p.value}
|
|
512
|
+
className={`flex cursor-pointer items-start gap-3 rounded-xl border p-3 transition-all ${
|
|
513
|
+
isActive
|
|
514
|
+
? 'border-blue-500 bg-blue-50 ring-2 ring-blue-500/20 dark:border-blue-400 dark:bg-blue-950/40'
|
|
515
|
+
: 'border-slate-200 bg-white hover:border-slate-300 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800/50 dark:hover:border-slate-600 dark:hover:bg-slate-800'
|
|
516
|
+
}`}
|
|
446
517
|
>
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
id="project"
|
|
463
|
-
value={project}
|
|
464
|
-
onChange={(e) => setProject(e.target.value)}
|
|
465
|
-
className="w-full rounded-lg border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2.5 text-sm text-slate-900 dark:text-white outline-none transition-all focus:border-blue-500 focus:ring-4 focus:ring-blue-500/10"
|
|
518
|
+
<input
|
|
519
|
+
type="radio"
|
|
520
|
+
name="priority"
|
|
521
|
+
value={p.value}
|
|
522
|
+
checked={isActive}
|
|
523
|
+
onChange={() => setPriority(p.value)}
|
|
524
|
+
className="sr-only"
|
|
525
|
+
/>
|
|
526
|
+
<span
|
|
527
|
+
aria-hidden
|
|
528
|
+
className={`mt-0.5 flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full border-2 ${
|
|
529
|
+
isActive
|
|
530
|
+
? 'border-blue-500 dark:border-blue-400'
|
|
531
|
+
: 'border-slate-300 dark:border-slate-600'
|
|
532
|
+
}`}
|
|
466
533
|
>
|
|
467
|
-
<
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
534
|
+
{isActive && <span className="h-2 w-2 rounded-full bg-blue-500 dark:bg-blue-400" />}
|
|
535
|
+
</span>
|
|
536
|
+
<span className="min-w-0 flex-1">
|
|
537
|
+
<span className="flex flex-wrap items-baseline justify-between gap-2">
|
|
538
|
+
<span className={`text-sm font-semibold ${
|
|
539
|
+
isActive ? 'text-blue-700 dark:text-blue-300' : 'text-slate-800 dark:text-slate-200'
|
|
540
|
+
}`}>
|
|
541
|
+
{p.label}
|
|
542
|
+
</span>
|
|
543
|
+
<span className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-medium ${p.color}`}>
|
|
544
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-3 w-3" aria-hidden>
|
|
545
|
+
<circle cx="12" cy="12" r="10" />
|
|
546
|
+
<polyline points="12 6 12 12 16 14" />
|
|
547
|
+
</svg>
|
|
548
|
+
Reponse sous {formatSla(slaHours)}
|
|
549
|
+
</span>
|
|
550
|
+
</span>
|
|
551
|
+
<span className="mt-0.5 block text-xs text-slate-500 dark:text-slate-400">
|
|
552
|
+
{p.desc}
|
|
553
|
+
</span>
|
|
554
|
+
</span>
|
|
555
|
+
</label>
|
|
556
|
+
)
|
|
557
|
+
})}
|
|
558
|
+
</div>
|
|
478
559
|
</div>
|
|
560
|
+
|
|
561
|
+
{/* Project selector (only shown when host app has projects) */}
|
|
562
|
+
{projects.length > 0 && (
|
|
563
|
+
<div className="mb-2">
|
|
564
|
+
<label htmlFor="project" className="mb-2 block text-sm font-semibold text-slate-700 dark:text-slate-200">
|
|
565
|
+
Projet
|
|
566
|
+
</label>
|
|
567
|
+
<select
|
|
568
|
+
id="project"
|
|
569
|
+
value={project}
|
|
570
|
+
onChange={(e) => setProject(e.target.value)}
|
|
571
|
+
className="w-full rounded-xl border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2.5 text-sm text-slate-900 dark:text-white outline-none transition-all focus:border-blue-500 focus:ring-4 focus:ring-blue-500/10"
|
|
572
|
+
>
|
|
573
|
+
<option value="">Aucun projet specifique</option>
|
|
574
|
+
{projects.map((p) => (
|
|
575
|
+
<option key={p.id} value={p.id}>
|
|
576
|
+
{p.name}
|
|
577
|
+
</option>
|
|
578
|
+
))}
|
|
579
|
+
</select>
|
|
580
|
+
</div>
|
|
581
|
+
)}
|
|
479
582
|
</div>
|
|
480
583
|
|
|
481
584
|
{/* Submit footer */}
|
|
@@ -309,6 +309,11 @@
|
|
|
309
309
|
"normal": "Normal",
|
|
310
310
|
"high": "High",
|
|
311
311
|
"urgent": "Urgent"
|
|
312
|
+
},
|
|
313
|
+
"deflection": {
|
|
314
|
+
"title": "These articles may answer your question",
|
|
315
|
+
"readMore": "Read article",
|
|
316
|
+
"stillCreateTicket": "My issue isn't resolved — create a ticket anyway"
|
|
312
317
|
}
|
|
313
318
|
},
|
|
314
319
|
"profile": {
|
|
@@ -309,6 +309,11 @@
|
|
|
309
309
|
"normal": "Normale",
|
|
310
310
|
"high": "Haute",
|
|
311
311
|
"urgent": "Urgente"
|
|
312
|
+
},
|
|
313
|
+
"deflection": {
|
|
314
|
+
"title": "Ces articles peuvent répondre à votre question",
|
|
315
|
+
"readMore": "Lire l'article",
|
|
316
|
+
"stillCreateTicket": "Mon problème n'est pas résolu — créer un ticket quand même"
|
|
312
317
|
}
|
|
313
318
|
},
|
|
314
319
|
"profile": {
|