1+ // src/components/ui/VsumDetailsModal.tsx
2+ import React , { useEffect , useState } from 'react' ;
3+ import { apiService } from '../../services/api' ;
4+ import { VsumDetails } from '../../types' ;
5+ import { VsumUsersTab } from './VsumUsersTab' ;
6+
7+ interface Props {
8+ isOpen : boolean ;
9+ vsumId : number | null ;
10+ onClose : ( ) => void ;
11+ onSaved ?: ( ) => void ;
12+ }
13+
14+ // ---- styles ----
15+ const overlay : React . CSSProperties = {
16+ position : 'fixed' , inset : 0 , background : 'rgba(0,0,0,0.35)' ,
17+ display : 'flex' , alignItems : 'center' , justifyContent : 'center' , zIndex : 9999 ,
18+ } ;
19+ const dialog : React . CSSProperties = {
20+ width : 900 , maxWidth : '95vw' , maxHeight : '90vh' ,
21+ background : '#fff' , borderRadius : 12 , boxShadow : '0 10px 30px rgba(0,0,0,0.25)' ,
22+ overflow : 'hidden' , display : 'flex' , flexDirection : 'column' , fontFamily : 'Georgia, serif' ,
23+ } ;
24+ const header : React . CSSProperties = {
25+ padding : '16px 20px' , borderBottom : '1px solid #e9ecef' ,
26+ display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' ,
27+ } ;
28+ const title : React . CSSProperties = { margin : 0 , fontSize : 18 , fontWeight : 700 , color : '#2c3e50' } ;
29+ const closeBtn : React . CSSProperties = { border : 'none' , background : 'transparent' , fontSize : 22 , cursor : 'pointer' , color : '#6c757d' } ;
30+ const body : React . CSSProperties = { padding : 20 , overflowY : 'auto' } ;
31+ const footer : React . CSSProperties = { padding : '12px 20px' , borderTop : '1px solid #e9ecef' , display : 'flex' , justifyContent : 'flex-end' , gap : 8 } ;
32+ // field styles
33+ const label : React . CSSProperties = { fontSize : 12 , fontWeight : 700 , color : '#495057' , marginTop : 12 , marginBottom : 6 } ;
34+ const textInput : React . CSSProperties = { width :'100%' , padding :'8px 10px' , border :'1px solid #dee2e6' , borderRadius :6 , fontSize :13 } ;
35+ // --------------
36+
37+ export const VsumDetailsModal : React . FC < Props > = ( { isOpen, vsumId, onClose, onSaved } ) => {
38+ const [ details , setDetails ] = useState < VsumDetails | null > ( null ) ;
39+ const [ name , setName ] = useState ( '' ) ;
40+ const [ error , setError ] = useState ( '' ) ;
41+ const [ saving , setSaving ] = useState ( false ) ;
42+ const [ activeTab , setActiveTab ] = useState < 'details' | 'users' > ( 'details' ) ;
43+ const [ loading , setLoading ] = useState ( false ) ;
44+
45+ // lock body scroll
46+ useEffect ( ( ) => {
47+ if ( ! isOpen ) return ;
48+ const orig = document . body . style . overflow ;
49+ document . body . style . overflow = 'hidden' ;
50+ return ( ) => { document . body . style . overflow = orig ; } ;
51+ } , [ isOpen ] ) ;
52+
53+ // load vsum details
54+ useEffect ( ( ) => {
55+ const load = async ( ) => {
56+ if ( ! isOpen || ! vsumId ) return ;
57+ setLoading ( true ) ;
58+ setError ( '' ) ;
59+ try {
60+ const res = await apiService . getVsumDetails ( vsumId ) ;
61+ const d = res . data ;
62+ setDetails ( d ) ;
63+ setName ( d . name ?? '' ) ;
64+ } catch ( e : any ) {
65+ setError ( e ?. message || 'Failed to load details' ) ;
66+ } finally {
67+ setLoading ( false ) ;
68+ }
69+ } ;
70+ load ( ) ;
71+ } , [ isOpen , vsumId ] ) ;
72+
73+ // save name (preserve existing meta model links)
74+ const save = async ( ) => {
75+ if ( ! vsumId || ! details ) return ;
76+ setSaving ( true ) ;
77+ setError ( '' ) ;
78+ try {
79+ const currentMetaModelIds = ( details . metaModels || [ ] ) . map ( m => m . id ) ;
80+ await apiService . updateVsum ( vsumId , { name : name . trim ( ) , metaModelIds : currentMetaModelIds } ) ;
81+ onSaved ?.( ) ;
82+ onClose ( ) ;
83+ } catch ( e : any ) {
84+ setError ( e ?. response ?. data ?. message || e ?. message || 'Save failed' ) ;
85+ } finally {
86+ setSaving ( false ) ;
87+ }
88+ } ;
89+
90+ if ( ! isOpen ) return null ;
91+
92+ // Helper: date only (no clock)
93+ const updatedDateOnly = details ?. updatedAt ? new Date ( details . updatedAt ) . toLocaleDateString ( ) : '' ;
94+
95+ return (
96+ < div style = { overlay } onClick = { onClose } role = "dialog" aria-modal = "true" >
97+ < div style = { dialog } onClick = { ( e ) => e . stopPropagation ( ) } >
98+ < div style = { header } >
99+ < h3 style = { title } > { details ?. name ?? 'vSUM Details' } </ h3 >
100+ < div style = { { display :'flex' , gap :8 } } >
101+ < button
102+ onClick = { ( ) => setActiveTab ( 'details' ) }
103+ style = { { border :'1px solid #dee2e6' , background : activeTab === 'details' ? '#e7f5ff' : '#fff' , borderRadius : 6 , padding : '6px 10px' , cursor : 'pointer' , fontWeight : 700 } }
104+ >
105+ Details
106+ </ button >
107+ < button
108+ onClick = { ( ) => setActiveTab ( 'users' ) }
109+ style = { { border :'1px solid #dee2e6' , background : activeTab === 'users' ? '#e7f5ff' : '#fff' , borderRadius : 6 , padding : '6px 10px' , cursor : 'pointer' , fontWeight : 700 } }
110+ >
111+ Manage Users
112+ </ button >
113+ < button aria-label = "Close" style = { closeBtn } onClick = { onClose } > ×</ button >
114+ </ div >
115+ </ div >
116+
117+ < div style = { body } >
118+ { error && (
119+ < div style = { { marginBottom :12 , padding :10 , border :'1px solid #f5c6cb' , background :'#f8d7da' , color :'#721c24' , borderRadius :6 , fontSize :12 } } >
120+ { error }
121+ </ div >
122+ ) }
123+
124+ { activeTab === 'details' ? (
125+ loading || ! details ? (
126+ < div style = { { fontStyle :'italic' , color :'#6c757d' } } > Loading…</ div >
127+ ) : (
128+ < >
129+ { /* 🔹 Removed ID; show date-only */ }
130+ < div style = { { fontSize : 12 , color : '#6c757d' , marginBottom : 10 } } >
131+ < strong > Updated:</ strong > { updatedDateOnly }
132+ </ div >
133+
134+ { /* Name (editable) */ }
135+ < div style = { label } > Name</ div >
136+ < input
137+ style = { textInput }
138+ value = { name }
139+ onChange = { ( e ) => setName ( e . target . value ) }
140+ />
141+
142+ { /* Meta Models — read-only list by name */ }
143+ < div style = { label } > Meta Models</ div >
144+ { ( details . metaModels && details . metaModels . length > 0 ) ? (
145+ < ul style = { { margin : 0 , paddingLeft : 18 } } >
146+ { details . metaModels . map ( mm => (
147+ < li key = { mm . id } style = { { marginBottom : 6 } } >
148+ < span style = { { fontWeight : 700 , color : '#2c3e50' } } > { mm . name } </ span >
149+ { /* optional extra info:
150+ <span style={{ color:'#6c757d', marginLeft: 6 }}>
151+ {mm.domain ? `• ${mm.domain}` : ''} {mm.keyword?.length ? `• ${mm.keyword.join(', ')}` : ''}
152+ </span>
153+ */ }
154+ </ li >
155+ ) ) }
156+ </ ul >
157+ ) : (
158+ < div style = { { fontSize : 12 , color : '#6c757d' , fontStyle : 'italic' } } >
159+ No meta models linked.
160+ </ div >
161+ ) }
162+ </ >
163+ )
164+ ) : (
165+ ! vsumId
166+ ? < div style = { { fontStyle :'italic' , color :'#6c757d' } } > No vSUM selected.</ div >
167+ : < VsumUsersTab vsumId = { vsumId } onChanged = { onSaved } />
168+ ) }
169+ </ div >
170+
171+ < div style = { footer } >
172+ < button
173+ style = { { padding : '8px 14px' , borderRadius : 6 , border : '1px solid #dee2e6' , background : '#fff' , color : '#495057' , fontWeight : 700 , cursor : 'pointer' } }
174+ onClick = { onClose }
175+ >
176+ Close
177+ </ button >
178+ { activeTab === 'details' && (
179+ < button
180+ style = { { padding : '8px 14px' , borderRadius : 6 , border : 'none' , background : '#3498db' , color : '#fff' , fontWeight : 700 , cursor : 'pointer' } }
181+ onClick = { save }
182+ disabled = { saving }
183+ >
184+ { saving ? 'Saving…' : 'Save' }
185+ </ button >
186+ ) }
187+ </ div >
188+ </ div >
189+ </ div >
190+ ) ;
191+ } ;
0 commit comments