@consilioweb/payload-support 0.11.0 → 0.13.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.
@@ -661,12 +661,26 @@ const [clientTyping, setClientTyping] = useState(false)
661
661
 
662
662
  return (
663
663
  <div className={s.page}>
664
- {/* TOP BAR */}
664
+ {/* TOP BAR — detail__header maquette : back-btn + (id+meta + title) + quick-actions */}
665
665
  <div className={s.topBar}>
666
666
  <Link href="/admin/support/inbox" className={s.backLink} aria-label="Retour à la boîte de réception">&larr;</Link>
667
667
  <div className={s.ticketMeta}>
668
- <span className={s.ticketNumber}>{ticket.ticketNumber as string}</span>
669
- <span className={s.ticketSubject}>{ticket.subject as string}</span>
668
+ {/* detail__id : ticketNumber · category · source email */}
669
+ <span className={s.ticketNumber}>
670
+ <span>{ticket.ticketNumber as string}</span>
671
+ {ticket.category ? (
672
+ <>
673
+ <span className={s.ticketNumberSep}>·</span>
674
+ <span className={s.ticketNumberCat}>{ticket.category as string}</span>
675
+ </>
676
+ ) : null}
677
+ <span className={s.ticketNumberSep}>·</span>
678
+ <span className={s.ticketNumberSource} aria-label={t('detail.source')}>
679
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden><rect x="3" y="5" width="18" height="14" rx="2" /><path d="m3 7 9 6 9-6" /></svg>
680
+ {(ticket.source as string) === 'email' ? 'Email' : (ticket.source as string) || t('ticket.source.portal')}
681
+ </span>
682
+ </span>
683
+ <h1 className={s.ticketSubject}>{ticket.subject as string}</h1>
670
684
  </div>
671
685
  <div className={s.topBarRight}>
672
686
  <select className={s.statusChip} style={{ background: st.bg, color: st.color }} value={(ticket.status as string) || 'open'} onChange={(e) => handleStatusChange(e.target.value)} disabled={statusUpdating} aria-label={t('ticket.status.label')}>
@@ -947,35 +961,38 @@ const [clientTyping, setClientTyping] = useState(false)
947
961
  </select>
948
962
  )}
949
963
  </div>
950
- <RichTextEditor
951
- ref={editorRef}
952
- onChange={(html, text) => { setReplyHtml(html); setReplyBody(text) }}
953
- placeholder={isInternal ? t('composer.placeholderInternal') : t('composer.placeholderReplyTo', { name: client?.firstName || 'client' })}
954
- minHeight={100}
955
- borderColor="transparent"
956
- onFileUpload={async (file) => {
957
- try {
958
- const formData = new FormData()
959
- formData.append('file', file)
960
- formData.append('_payload', JSON.stringify({ alt: file.name }))
961
- const ur = await fetch('/api/media', { method: 'POST', credentials: 'include', body: formData })
962
- if (!ur.ok) return null
963
- const ud = await ur.json()
964
- return ud.doc?.url || null
965
- } catch { return null }
966
- }}
967
- />
968
- {/* #5 — File upload preview */}
969
- {pendingFiles.length > 0 && (
970
- <div className={s.uploadPreview}>
971
- {pendingFiles.map((f, i) => (
972
- <div key={i} className={s.uploadPreviewItem}>
973
- <span>PJ {f.name}</span>
974
- <button className={s.uploadRemoveBtn} aria-label={`Retirer ${f.name}`} onClick={() => setPendingFiles((prev) => prev.filter((_, j) => j !== i))}>&times;</button>
975
- </div>
976
- ))}
977
- </div>
978
- )}
964
+ {/* composerBox (composer__box maquette) : wrap the Lexical RTE with maquette box style */}
965
+ <div className={s.composerBox}>
966
+ <RichTextEditor
967
+ ref={editorRef}
968
+ onChange={(html, text) => { setReplyHtml(html); setReplyBody(text) }}
969
+ placeholder={isInternal ? t('composer.placeholderInternal') : t('composer.placeholderReplyTo', { name: client?.firstName || 'client' })}
970
+ minHeight={100}
971
+ borderColor="transparent"
972
+ onFileUpload={async (file) => {
973
+ try {
974
+ const formData = new FormData()
975
+ formData.append('file', file)
976
+ formData.append('_payload', JSON.stringify({ alt: file.name }))
977
+ const ur = await fetch('/api/media', { method: 'POST', credentials: 'include', body: formData })
978
+ if (!ur.ok) return null
979
+ const ud = await ur.json()
980
+ return ud.doc?.url || null
981
+ } catch { return null }
982
+ }}
983
+ />
984
+ {/* #5 — File upload preview */}
985
+ {pendingFiles.length > 0 && (
986
+ <div className={s.uploadPreview}>
987
+ {pendingFiles.map((f, i) => (
988
+ <div key={i} className={s.uploadPreviewItem}>
989
+ <span>PJ {f.name}</span>
990
+ <button className={s.uploadRemoveBtn} aria-label={`Retirer ${f.name}`} onClick={() => setPendingFiles((prev) => prev.filter((_, j) => j !== i))}>&times;</button>
991
+ </div>
992
+ ))}
993
+ </div>
994
+ )}
995
+ </div>
979
996
  <div className={s.composerFooter}>
980
997
  <div className={s.composerOptions}>
981
998
  <select
@@ -1171,25 +1188,34 @@ const [clientTyping, setClientTyping] = useState(false)
1171
1188
 
1172
1189
  {features.timeTracking && (
1173
1190
  <div className={s.sideSection}>
1174
- <div className={s.sideSectionTitle}>{t('detail.time')} <span style={{ fontWeight: 700, fontSize: 13, color: '#d97706' }}>{totalMin > 0 ? `${Math.floor(totalMin / 60)}h${String(totalMin % 60).padStart(2, '0')} ${t('detail.total')}` : '0min'}</span></div>
1191
+ <div className={s.sideSectionTitle}>
1192
+ <span>{t('detail.time')}</span>
1193
+ <span style={{ color: 'var(--theme-text)', fontWeight: 600, textTransform: 'none', letterSpacing: 0 }}>
1194
+ {totalMin > 0 ? `${Math.floor(totalMin / 60)}h${String(totalMin % 60).padStart(2, '0')} ${t('detail.total')}` : '0min'}
1195
+ </span>
1196
+ </div>
1175
1197
  {/* Timer */}
1176
1198
  <div className={s.timer}>
1177
1199
  <span className={`${s.timerDisplay} ${timerRunning ? s.timerActive : ''}`}>
1178
1200
  {String(Math.floor(timerSeconds / 60)).padStart(2, '0')}:{String(timerSeconds % 60).padStart(2, '0')}
1179
1201
  </span>
1180
1202
  {!timerRunning ? (
1181
- <button className={s.timerBtn} onClick={() => setTimerRunning(true)} style={{ color: '#dc2626', borderColor: '#dc2626' }} aria-label="Démarrer le timer">{timerSeconds > 0 ? '▶' : '▶ Go'}</button>
1203
+ <button className={s.timerBtn} onClick={() => setTimerRunning(true)} aria-label="Démarrer le timer">{timerSeconds > 0 ? '▶' : '▶ Go'}</button>
1182
1204
  ) : (
1183
- <button className={s.timerBtn} onClick={() => setTimerRunning(false)} aria-label="Mettre en pause le timer">⏸</button>
1205
+ <button className={`${s.timerBtn} ${s.timerBtnRunning}`} onClick={() => setTimerRunning(false)} aria-label="Mettre en pause le timer">⏸ Pause</button>
1184
1206
  )}
1185
1207
  {timerSeconds >= 60 && !timerRunning && (
1186
- <button className={s.timerBtn} onClick={() => { handleTimerSave(); localStorage.removeItem(`timer-sec-${ticketId}`); localStorage.removeItem(`timer-run-${ticketId}`) }} style={{ color: '#16a34a', borderColor: '#16a34a' }} aria-label="Sauvegarder le temps">💾 {Math.round(timerSeconds / 60)}m</button>
1208
+ <button className={`${s.timerBtn} ${s.timerBtnGhost}`} onClick={() => { handleTimerSave(); localStorage.removeItem(`timer-sec-${ticketId}`); localStorage.removeItem(`timer-run-${ticketId}`) }} aria-label="Sauvegarder le temps">💾 {Math.round(timerSeconds / 60)}m</button>
1187
1209
  )}
1188
1210
  </div>
1211
+ {/* Progress bar (tt__bar maquette) — full = 120 min reference */}
1212
+ <div className={s.timerBar} aria-hidden>
1213
+ <div className={s.timerBarFill} style={{ width: `${Math.min(100, ((totalMin + Math.floor(timerSeconds / 60)) / 120) * 100)}%` }} />
1214
+ </div>
1189
1215
  {/* Manual time entry */}
1190
1216
  <div style={{ display: 'flex', gap: 6, marginTop: 8, alignItems: 'center' }}>
1191
1217
  <input type="number" min="1" placeholder="min" style={{ width: 60, padding: '4px 8px', borderRadius: 6, border: '1px solid var(--theme-elevation-200)', fontSize: 12, color: 'var(--theme-text)', background: 'var(--theme-elevation-0)' }} id="manual-time-input" />
1192
- <button className={s.timerBtn} onClick={async () => {
1218
+ <button className={`${s.timerBtn} ${s.timerBtnGhost}`} onClick={async () => {
1193
1219
  const input = document.getElementById('manual-time-input') as HTMLInputElement
1194
1220
  const mins = Number(input?.value)
1195
1221
  if (!mins || mins < 1 || !ticketId) return