@@ -49,6 +49,7 @@ function StatusBadge({ status }) {
const styles = {
open : { bg : '#dcfce7' , color : '#16a34a' , label : 'Ouvert' } ,
resolved : { bg : '#f1f5f9' , color : '#64748b' , label : 'Résolu' } ,
pending : { bg : '#fef9c3' , color : '#a16207' , label : 'En attente' } ,
} ;
const s = styles [ status ] ? ? styles . open ;
return (
@@ -510,6 +511,10 @@ export default function Communication() {
const { user , isAdmin } = useAuth ( ) ;
const [ searchParams , setSearchParams ] = useSearchParams ( ) ;
const navigate = useNavigate ( ) ;
const [ notifMenuPos , setNotifMenuPos ] = useState ( null ) ; // { x, y }
const [ supportMenuPos , setSupportMenuPos ] = useState ( null ) ; // { x, y }
const [ showAssignModal , setShowAssignModal ] = useState ( false ) ;
const [ assignSearch , setAssignSearch ] = useState ( '' ) ;
const [ tab , setTab ] = useState ( 'support' ) ; // 'support' | 'notifications'
const [ showBroadcastForm , setShowBroadcastForm ] = useState ( false ) ;
@@ -524,7 +529,7 @@ export default function Communication() {
} , [ ] ) ;
const [ tickets , setTickets ] = useState ( [ ] ) ;
const [ ticketSearch , setTicketSearch ] = useState ( '' ) ;
const [ ticketFilter , setTicketFilter ] = useState ( 'all' ) ; // 'all'|'open'|'resolved'
const [ ticketFilter , setTicketFilter ] = useState ( new Set ( [ 'open' , 'pending' ] ) ) ; // multi-sélection
const [ notifFilter , setNotifFilter ] = useState ( 'all' ) ; // 'all'|'unread'
const [ selectedTicket , setSelectedTicket ] = useState ( null ) ;
const [ thread , setThread ] = useState ( null ) ; // { ticket, messages }
@@ -595,7 +600,7 @@ export default function Communication() {
if ( ! isAdmin ) return ;
try {
const data = await api . get ( '/admin/users' ) ;
setUsers ( data . users ? ? data ? ? [ ] ) ;
setUsers ( ( data . users ? ? data ? ? [ ] ) . filter ( u => u . role === 'admin' ) ) ;
} catch { /* silencieux */ }
} , [ isAdmin ] ) ;
@@ -695,6 +700,25 @@ export default function Communication() {
} catch { /* silencieux */ }
} ;
// ── Assigner un ticket ───────────────────────────────────────────────
const assignTicket = async ( adminId ) => {
if ( ! thread ) return ;
try {
await api . patch ( ` /tickets/ ${ thread . ticket . id } /assign ` , { assigned _to : adminId } ) ;
await fetchThread ( thread . ticket . id ) ;
} catch { /* silencieux */ }
} ;
// ── Mettre en attente ────────────────────────────────────────────────
const setTicketPending = async ( ) => {
if ( ! thread ) return ;
try {
await api . patch ( ` /tickets/ ${ thread . ticket . id } /status ` , { status : 'pending' } ) ;
await fetchThread ( thread . ticket . id ) ;
await fetchTickets ( ) ;
} catch { /* silencieux */ }
} ;
// ── Broadcast ────────────────────────────────────────────────────────
const submitBroadcast = async ( e ) => {
e . preventDefault ( ) ;
@@ -718,6 +742,26 @@ export default function Communication() {
setBcSending ( false ) ;
} ;
// ── Supprimer toutes les notifications ────────────────────────────────────
const deleteAllNotifs = async ( ) => {
try {
await Promise . all ( notifs . map ( n => api . del ( ` /notifications/ ${ n . id } ` ) ) ) ;
setNotifs ( [ ] ) ;
setSelectedNotif ( null ) ;
window . dispatchEvent ( new CustomEvent ( 'notif:refresh' ) ) ;
} catch { /* silencieux */ }
} ;
// ── Supprimer une notification ────────────────────────────────────────────
const deleteNotif = async ( id ) => {
try {
await api . del ( ` /notifications/ ${ id } ` ) ;
setNotifs ( prev => prev . filter ( n => n . id !== id ) ) ;
setSelectedNotif ( null ) ;
window . dispatchEvent ( new CustomEvent ( 'notif:refresh' ) ) ;
} catch { /* silencieux */ }
} ;
// ── Marquer toutes notifs lues ───────────────────────────────────────
const markAllRead = async ( ) => {
try {
@@ -777,55 +821,45 @@ export default function Communication() {
< / div >
{ /* Colonne 2 — Dossier + filtres */ }
< div className = "comm-topbar-2" >
< span className = "comm-topbar-folder" > { tab === 'support' ? 'Support' : 'Notifications' } < / span >
< div className = "comm-topbar-filters" >
{ tab === 'support' && (
< >
{ [ [ 'all' , 'Tous' ] , [ 'open' , 'Ouverts' ] , [ 'resolved' , 'Résolus' ] ] . map ( ( [ v , l ] ) => (
< button key = { v } className = { ` comm-topbar-filter-btn ${ ticketFilter === v ? ' active' : '' } ` } onClick = { ( ) => setTicketFilter ( v ) } > { l } < / button >
) ) }
< / >
) }
{ tab === 'notifications' && (
< >
{ [ [ 'all' , 'Tous' ] , [ 'unread' , 'Non lus' ] ] . map ( ( [ v , l ] ) => (
< button key = { v } className = { ` comm-topbar-filter-btn ${ notifFilter === v ? ' active' : '' } ` } onClick = { ( ) => { setNotifFilter ( v ) ; setNotifPage ( 0 ) ; } } > { l } < / button >
) ) }
{ unreadCount > 0 && (
< button className = "comm-topbar-action-btn" onClick = { markAllRead } > Tout marquer lu < / button >
) }
< / >
) }
< / div >
< span className = "comm-topbar-folder" style = { { fontSize : 15 , fontWeight : 700 } } > { tab === 'support' ? 'Support' : 'Notifications' } < / span >
{ tab === 'notifications' && (
< button
className = "btn-icon-sm"
style = { { marginLeft : 'auto' , flexShrink : 0 } }
onClick = { e => {
const r = e . currentTarget . getBoundingClientRect ( ) ;
setNotifMenuPos ( { x : r . right , y : r . bottom + 4 } ) ;
} }
>
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" > < circle cx = "12" cy = "5" r = "1" / > < circle cx = "12" cy = "12" r = "1" / > < circle cx = "12" cy = "19" r = "1" / > < / svg >
< / button >
) }
< / div >
{ /* Colonne 3 — Toolbar contextuelle */ }
< div className = "comm-topbar-3" >
{ tab === 'support' && thread && (
< >
< span className = "comm-topbar-ticket-ref" > { thread . ticket . ticket _number } — { thread . ticket . subject } < / span >
< div style = { { display : 'flex' , gap : 6 , marginLeft : 'auto' , flexShrink : 0 } } >
{ isAdmin && (
< button className = "btn bt n-sm" onClick = { toggleStatus } >
{ thread . ticket . status === 'open' ? 'Résoudre' : 'Rouvrir' }
< / button >
) }
< div style = { { display : 'flex' , gap : 6 , marginLeft : 'auto' , flexShrink : 0 , alignItems : 'center' } } >
< button
className = "btn-ico n-sm"
onClick = { e => {
const r = e . currentTarget . getBoundingClientRect ( ) ;
setSupportMenuPos ( { x : r . right , y : r . bottom + 4 } ) ;
} }
>
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" > < circle cx = "12" cy = "5" r = "1" / > < circle cx = "12" cy = "12" r = "1" / > < circle cx = "12" cy = "19" r = "1" / > < / svg >
< / button >
< / div >
< / >
) }
{ tab === 'notifications' && selectedNotif && (
{ tab === 'notifications' && selectedNotif && ! selectedNotif . read && (
< div style = { { display : 'flex' , gap : 6 , marginLeft : 'auto' } } >
{ ! selectedNotif . read && (
< button className = "btn btn-sm btn-ghost" onClick = { async ( ) => {
await api . patch ( ` /notifications/ ${ selectedNotif . id } /read ` ) ;
setNotifs ( prev => prev . map ( n => n . id === selectedNotif . id ? { ... n , read : 1 } : n ) ) ;
setSelectedNotif ( prev => ( { ... prev , read : 1 } ) ) ;
} } > Marquer lu < / button >
) }
{ selectedNotif . link && (
< button className = "btn btn-sm btn-primary" onClick = { ( ) => navigate ( selectedNotif . link ) } >
Voir le détail →
< / button >
) }
< button className = "btn btn-sm btn-ghost" onClick = { async ( ) => {
await api . patch ( ` /notifications/ ${ selectedNotif . id } /read ` ) ;
setNotifs ( prev => prev . map ( n => n . id === selectedNotif . id ? { ... n , read : 1 } : n ) ) ;
setSelectedNotif ( prev => ( { ... prev , read : 1 } ) ) ;
} } > Marquer lu < / button >
< / div >
) }
< / div >
@@ -895,11 +929,15 @@ export default function Communication() {
< div className = "comm-list" >
{ tab === 'support' && (
< >
< div className = "comm-list-header" >
< div className = "comm-list-header" style = { { flexDirection : 'column' , alignItems : 'stretch' , gap : 8 } } >
< div className = "comm-topbar-filters" >
{ [ [ 'all' , 'Tous' ] , [ 'open' , 'Ouverts' ] , [ 'pending' , 'En attente' ] , [ 'resolved' , 'Résolus' ] ] . map ( ( [ v , l ] ) => (
< button key = { v } className = { ` comm-topbar-filter-btn ${ ticketFilter === v ? ' active' : '' } ` } onClick = { ( ) => setTicketFilter ( v ) } > { l } < / button >
) ) }
< / div >
< input
className = "comm-search"
placeholder = "Rechercher un ticket…"
style = { { flex : 1 } }
value = { ticketSearch }
onChange = { e => setTicketSearch ( e . target . value ) }
/ >
@@ -914,6 +952,7 @@ export default function Communication() {
< / div >
) }
{ tickets
. filter ( t => ticketFilter === 'all' || t . status === ticketFilter )
. filter ( t => {
if ( ! ticketSearch . trim ( ) ) return true ;
const q = ticketSearch . toLowerCase ( ) ;
@@ -962,13 +1001,20 @@ export default function Communication() {
return (
< >
< div className = "comm-list-header" >
< span style = { { fontWeight : 600 , fontSize : 14 } } > Notifications < / span >
< div className = "comm-topbar-filters" >
{ [ [ 'all' , 'Tous' ] , [ 'unread' , 'Non lus' ] ] . map ( ( [ v , l ] ) => (
< button key = { v } className = { ` comm-topbar-filter-btn ${ notifFilter === v ? ' active' : '' } ` } onClick = { ( ) => { setNotifFilter ( v ) ; setNotifPage ( 0 ) ; } } > { l } < / button >
) ) }
{ unreadCount > 0 && (
< button className = "comm-topbar-action-btn" style = { { marginLeft : 'auto' } } onClick = { markAllRead } > Tout marquer lu < / button >
) }
< / div >
< / div >
< div className = "comm-list-scroll" >
{ notifs . length === 0 && (
< div className = "comm-empty" >
< div style = { { fontSize : 32 , marginBottom : 8 } } > 🔔 < / div >
< div style = { { fontWeight : 600 } } > Aucune notification < / div >
< svg width = "48" height = "48" viewBox = "0 0 24 24" fill = "none" stroke = "var(--border)" strokeWidth = "1.5" > < path d = "M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" / > < path d = "M13.73 21a2 2 0 0 1-3.46 0" / > < / svg >
< div style = { { marginTop : 12 , color : 'var(--text-muted)' , fontSize : 14 } } > Aucune notification < / div >
< / div >
) }
{ pagedNotifs . map ( n => (
@@ -981,6 +1027,8 @@ export default function Communication() {
< div className = "comm-list-row-body" >
< div className = "comm-list-row-top" >
< span className = "comm-list-row-name" > { n . title } < / span >
< / div >
< div >
< span style = { {
fontSize : 11 , padding : '2px 7px' , borderRadius : 99 , fontWeight : 600 ,
background : ( TYPE _META [ n . type ] ? ? TYPE _META . info ) . bg ,
@@ -1200,17 +1248,43 @@ export default function Communication() {
< / div >
< / div >
{ selectedNotif . body && (
< div style = { { padding : '20px 24px' , color : 'var(--text)' , lineHeight : 1.6 } } >
< div style = { {
background : '#fff' ,
borderTop : '1px solid var(--border)' ,
borderBottom : '1px solid var(--border)' ,
padding : '16px 24px' ,
height : 250 ,
overflowY : 'auto' ,
color : 'var(--text)' ,
lineHeight : 1.6 ,
fontSize : 14 ,
} } >
{ selectedNotif . body }
< / div >
) }
{ selectedNotif . link && (
< div style = { { padding : '0 24px 20px' } } >
< div style = { {
display : 'flex' ,
justifyContent : 'flex-end' ,
alignItems : 'center' ,
gap : 8 ,
padding : '10px 16px' ,
background : '#fff' ,
borderBottom : '1px solid var(--border)' ,
} } >
< button
className = "btn btn-sm btn-ghost"
style = { { color : 'var(--danger)' } }
onClick = { ( ) => deleteNotif ( selectedNotif . id ) }
>
< svg width = "13" height = "13" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" style = { { marginRight : 5 } } > < polyline points = "3 6 5 6 21 6" / > < path d = "M19 6l-1 14H6L5 6" / > < path d = "M10 11v6" / > < path d = "M14 11v6" / > < path d = "M9 6V4h6v2" / > < / svg >
Supprimer
< / button >
{ selectedNotif . link && (
< button className = "btn btn-primary btn-sm" onClick = { ( ) => navigate ( selectedNotif . link ) } >
Voir le détail →
< / button >
< / div >
) }
) }
< / div >
< / div >
) }
@@ -1268,13 +1342,33 @@ export default function Communication() {
< / div >
) }
{ /* Placeholder vide */ }
{ tab === 'support' && ! thread && (
< div className = "comm-detail-empty" >
< svg width = "48" height = "48" viewBox = "0 0 24 24" fill = "none" stroke = "var(--border)" strokeWidth = "1.5" > < path d = "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" / > < / svg >
< div style = { { marginTop : 12 , color : 'var(--text-muted) ' , fontSize : 14 } } > Sélectionnez un ticket < / div >
< / div >
) }
{ /* KPIs support */ }
{ tab === 'support' && ! thread && ( ( ) => {
const kpis = [
{ label : 'Total Tickets' , count : tickets . length , bg : '#f3f4f6' , color : '#111827' } ,
{ label : 'En attente' , count : tickets . filter ( t => t . status === 'pending' ) . length , bg : '#fefce8 ' , color : '#a16207' } ,
{ label : 'Ouverts' , count : tickets . filter ( t => t . status === 'open' ) . length , bg : '#ecfdf5' , color : '#0d9488' } ,
{ label : 'Clôturés' , count : tickets . filter ( t => t . status === 'resolved' ) . length , bg : '#fff1f2' , color : '#e11d48' } ,
] ;
return (
< div style = { { background : '#fff' , height : '100%' , display : 'flex' , flexDirection : 'column' } } >
< div style = { { padding : '28px 28px 20px' } } >
< div style = { { display : 'grid' , gridTemplateColumns : 'repeat(4, 1fr)' , gap : 16 } } >
{ kpis . map ( k => (
< div key = { k . label } style = { { background : k . bg , borderRadius : 12 , padding : '24px 16px' , textAlign : 'center' } } >
< div style = { { fontSize : 32 , fontWeight : 700 , color : k . color , lineHeight : 1 } } > { k . count } < / div >
< div style = { { marginTop : 8 , fontSize : 13 , fontWeight : 600 , color : k . color } } > { k . label } < / div >
< / div >
) ) }
< / div >
< / div >
< div style = { { flex : 1 , display : 'flex' , flexDirection : 'column' , alignItems : 'center' , justifyContent : 'center' , gap : 12 } } >
< svg width = "48" height = "48" viewBox = "0 0 24 24" fill = "none" stroke = "var(--border)" strokeWidth = "1.5" > < path d = "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" / > < / svg >
< div style = { { color : 'var(--text-muted)' , fontSize : 14 } } > Sélectionnez un ticket < / div >
< / div >
< / div >
) ;
} ) ( ) }
{ tab === 'notifications' && ! selectedNotif && ! showBroadcastForm && (
< div className = "comm-detail-empty" >
< svg width = "48" height = "48" viewBox = "0 0 24 24" fill = "none" stroke = "var(--border)" strokeWidth = "1.5" > < path d = "M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" / > < path d = "M13.73 21a2 2 0 0 1-3.46 0" / > < / svg >
@@ -1286,6 +1380,183 @@ export default function Communication() {
< / div > { /* end comm-wrap */ }
< / div > { /* end comm-page */ }
{ /* ── Menu ⋮ notifications ── */ }
{ notifMenuPos && (
< >
< div style = { { position : 'fixed' , inset : 0 , zIndex : 299 } } onClick = { ( ) => setNotifMenuPos ( null ) } / >
< div style = { {
position : 'fixed' , left : notifMenuPos . x , top : notifMenuPos . y ,
transform : 'translateX(-100%) translateY(4px)' ,
zIndex : 300 ,
background : 'var(--surface)' , border : '1px solid var(--border)' ,
borderRadius : 8 , boxShadow : '0 4px 20px rgba(0,0,0,0.15)' ,
padding : '4px 0' , minWidth : 180 ,
} } >
{ unreadCount > 0 && (
< button
style = { { display : 'flex' , alignItems : 'center' , gap : 8 , width : '100%' , padding : '8px 14px' , background : 'none' , border : 'none' , cursor : 'pointer' , fontSize : 'var(--fs-sm)' , color : 'var(--text)' , textAlign : 'left' } }
onMouseEnter = { e => e . currentTarget . style . background = 'var(--surface-2)' }
onMouseLeave = { e => e . currentTarget . style . background = 'none' }
onClick = { ( ) => { markAllRead ( ) ; setNotifMenuPos ( null ) ; } }
>
< svg width = "13" height = "13" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2.5" > < polyline points = "20 6 9 17 4 12" / > < / svg >
Tout marquer lu
< / button >
) }
< button
style = { { display : 'flex' , alignItems : 'center' , gap : 8 , width : '100%' , padding : '8px 14px' , background : 'none' , border : 'none' , cursor : 'pointer' , fontSize : 'var(--fs-sm)' , color : 'var(--danger)' , textAlign : 'left' } }
onMouseEnter = { e => e . currentTarget . style . background = 'var(--surface-2)' }
onMouseLeave = { e => e . currentTarget . style . background = 'none' }
onClick = { ( ) => { deleteAllNotifs ( ) ; setNotifMenuPos ( null ) ; } }
>
< svg width = "13" height = "13" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" strokeLinecap = "round" strokeLinejoin = "round" > < polyline points = "3 6 5 6 21 6" / > < path d = "M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" / > < path d = "M10 11v6" / > < path d = "M14 11v6" / > < path d = "M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" / > < / svg >
Tout supprimer
< / button >
< / div >
< / >
) }
{ /* ── Menu ⋮ support ── */ }
{ supportMenuPos && (
< >
< div style = { { position : 'fixed' , inset : 0 , zIndex : 299 } } onClick = { ( ) => setSupportMenuPos ( null ) } / >
< div style = { {
position : 'fixed' , left : supportMenuPos . x , top : supportMenuPos . y ,
transform : 'translateX(-100%) translateY(4px)' ,
zIndex : 300 ,
background : 'var(--surface)' , border : '1px solid var(--border)' ,
borderRadius : 8 , boxShadow : '0 4px 20px rgba(0,0,0,0.15)' ,
padding : '4px 0' , minWidth : 180 ,
} } >
< button
style = { { display : 'flex' , alignItems : 'center' , gap : 8 , width : '100%' , padding : '8px 14px' , background : 'none' , border : 'none' , cursor : 'pointer' , fontSize : 'var(--fs-sm)' , color : 'var(--text)' , textAlign : 'left' } }
onMouseEnter = { e => e . currentTarget . style . background = 'var(--surface-2)' }
onMouseLeave = { e => e . currentTarget . style . background = 'none' }
onClick = { ( ) => { setShowCompose ( true ) ; setSupportMenuPos ( null ) ; } }
>
< svg width = "13" height = "13" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" > < path d = "M12 5v14" / > < path d = "M5 12h14" / > < / svg >
Nouveau ticket
< / button >
{ thread && isAdmin && (
< >
< button
style = { { display : 'flex' , alignItems : 'center' , gap : 8 , width : '100%' , padding : '8px 14px' , background : 'none' , border : 'none' , cursor : 'pointer' , fontSize : 'var(--fs-sm)' , color : 'var(--text)' , textAlign : 'left' } }
onMouseEnter = { e => e . currentTarget . style . background = 'var(--surface-2)' }
onMouseLeave = { e => e . currentTarget . style . background = 'none' }
onClick = { ( ) => { setAssignSearch ( '' ) ; setShowAssignModal ( true ) ; setSupportMenuPos ( null ) ; } }
>
< svg width = "13" height = "13" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" > < path d = "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" / > < circle cx = "12" cy = "7" r = "4" / > < / svg >
Assigner à
< / button >
{ thread . ticket . status !== 'pending' ? (
< button
style = { { display : 'flex' , alignItems : 'center' , gap : 8 , width : '100%' , padding : '8px 14px' , background : 'none' , border : 'none' , cursor : 'pointer' , fontSize : 'var(--fs-sm)' , color : 'var(--text)' , textAlign : 'left' } }
onMouseEnter = { e => e . currentTarget . style . background = 'var(--surface-2)' }
onMouseLeave = { e => e . currentTarget . style . background = 'none' }
onClick = { ( ) => { setTicketPending ( ) ; setSupportMenuPos ( null ) ; } }
>
< svg width = "13" height = "13" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" > < circle cx = "12" cy = "12" r = "10" / > < polyline points = "12 6 12 12 16 14" / > < / svg >
Mettre en attente
< / button >
) : (
< button
style = { { display : 'flex' , alignItems : 'center' , gap : 8 , width : '100%' , padding : '8px 14px' , background : 'none' , border : 'none' , cursor : 'pointer' , fontSize : 'var(--fs-sm)' , color : 'var(--text)' , textAlign : 'left' } }
onMouseEnter = { e => e . currentTarget . style . background = 'var(--surface-2)' }
onMouseLeave = { e => e . currentTarget . style . background = 'none' }
onClick = { ( ) => { toggleStatus ( ) ; setSupportMenuPos ( null ) ; } }
>
< svg width = "13" height = "13" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" > < polyline points = "1 4 1 10 7 10" / > < path d = "M3.51 15a9 9 0 1 0 .49-3.5" / > < / svg >
Réouvrir le ticket
< / button >
) }
< button
style = { { display : 'flex' , alignItems : 'center' , gap : 8 , width : '100%' , padding : '8px 14px' , background : 'none' , border : 'none' , cursor : 'pointer' , fontSize : 'var(--fs-sm)' , color : 'var(--text)' , textAlign : 'left' } }
onMouseEnter = { e => e . currentTarget . style . background = 'var(--surface-2)' }
onMouseLeave = { e => e . currentTarget . style . background = 'none' }
onClick = { ( ) => { toggleStatus ( ) ; setSupportMenuPos ( null ) ; } }
>
{ thread . ticket . status === 'resolved'
? < > < svg width = "13" height = "13" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" > < polyline points = "1 4 1 10 7 10" / > < path d = "M3.51 15a9 9 0 1 0 .49-3.5" / > < / svg > Rouvrir < / >
: < > < svg width = "13" height = "13" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" > < polyline points = "20 6 9 17 4 12" / > < / svg > Clôturer le ticket < / >
}
< / button >
< div style = { { borderTop : '1px solid var(--border)' , margin : '4px 0' } } / >
< button
style = { { display : 'flex' , alignItems : 'center' , gap : 8 , width : '100%' , padding : '8px 14px' , background : 'none' , border : 'none' , cursor : 'pointer' , fontSize : 'var(--fs-sm)' , color : 'var(--text-muted)' , textAlign : 'left' } }
onMouseEnter = { e => e . currentTarget . style . background = 'var(--surface-2)' }
onMouseLeave = { e => e . currentTarget . style . background = 'none' }
onClick = { ( ) => { setThread ( null ) ; setSelectedTicket ( null ) ; setSearchParams ( { } , { replace : true } ) ; setSupportMenuPos ( null ) ; } }
>
< svg width = "13" height = "13" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" > < line x1 = "18" y1 = "6" x2 = "6" y2 = "18" / > < line x1 = "6" y1 = "6" x2 = "18" y2 = "18" / > < / svg >
Fermer
< / button >
< / >
) }
< / div >
< / >
) }
{ /* ── Modale Assigner à ── */ }
{ showAssignModal && (
< div className = "comm-modal-backdrop" onClick = { ( ) => setShowAssignModal ( false ) } >
< div className = "comm-modal" style = { { maxWidth : 420 } } onClick = { e => e . stopPropagation ( ) } >
< div className = "comm-modal-header" >
< span style = { { fontWeight : 700 , fontSize : 15 } } > Assigner le ticket < / span >
< button className = "comm-modal-close" onClick = { ( ) => setShowAssignModal ( false ) } > × < / button >
< / div >
< div style = { { padding : '12px 20px 16px' } } >
< input
className = "comm-search"
placeholder = "Rechercher un administrateur…"
value = { assignSearch }
onChange = { e => setAssignSearch ( e . target . value ) }
autoFocus
style = { { width : '100%' , marginBottom : 10 } }
/ >
{ /* Désassigner */ }
{ thread ? . ticket . assigned _to && (
< button
style = { { display : 'flex' , alignItems : 'center' , gap : 10 , width : '100%' , padding : '8px 12px' , background : 'none' , border : 'none' , cursor : 'pointer' , fontSize : 13 , color : 'var(--danger)' , borderRadius : 6 , marginBottom : 4 } }
onMouseEnter = { e => e . currentTarget . style . background = 'var(--surface-2)' }
onMouseLeave = { e => e . currentTarget . style . background = 'none' }
onClick = { async ( ) => { await assignTicket ( null ) ; setShowAssignModal ( false ) ; } }
>
< svg width = "13" height = "13" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" > < line x1 = "18" y1 = "6" x2 = "6" y2 = "18" / > < line x1 = "6" y1 = "6" x2 = "18" y2 = "18" / > < / svg >
Retirer l 'assignation
</button>
)}
{/* Liste admins */}
<div style={{ maxHeight: 260, overflowY: ' auto ' }}>
{users
.filter(u => !assignSearch.trim() || (u.display_name ?? u.email ?? ' ').toLowerCase().includes(assignSearch.toLowerCase()))
.map(u => {
const isAssigned = thread?.ticket.assigned_to === u.id;
return (
<button
key={u.id}
style={{ display: ' flex ', alignItems: ' center ', gap: 10, width: ' 100 % ', padding: ' 8 px 12 px ', background: isAssigned ? ' var ( -- surface - 2 ) ' : ' none ', border: ' none ', cursor: ' pointer ', fontSize: 13, color: ' var ( -- text ) ', borderRadius: 6, textAlign: ' left ' }}
onMouseEnter={e => e.currentTarget.style.background = ' var ( -- surface - 2 ) '}
onMouseLeave={e => e.currentTarget.style.background = isAssigned ? ' var ( -- surface - 2 ) ' : ' none '}
onClick={async () => { await assignTicket(u.id); setShowAssignModal(false); }}
>
<UserAvatar name={u.display_name ?? u.email} isAdmin size={30} />
<div>
<div style={{ fontWeight: 600 }}>{u.display_name ?? u.email}</div>
{u.display_name && <div style={{ fontSize: 11, color: ' var ( -- text - muted ) ' }}>{u.email}</div>}
</div>
{isAssigned && (
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--primary)" strokeWidth="2.5" style={{ marginLeft: ' auto ' }}><polyline points="20 6 9 17 4 12"/></svg>
)}
</button>
);
})
}
</div>
</div>
</div>
</div>
)}
{/* ── Lightbox image ── */}
{lightbox && (
<div className="comm-lightbox" onClick={() => setLightbox(null)}>