Skip to content

Commit 3e05466

Browse files
authored
Merge pull request #54 from vitruv-tools/47-manage-vsum-member-page
add manage member in vsum
2 parents 4bb67aa + d8ed1be commit 3e05466

File tree

4 files changed

+589
-187
lines changed

4 files changed

+589
-187
lines changed
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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

Comments
 (0)