roadmap #23
189
client/src/components/EditEmployeeModal.jsx
Normal file
189
client/src/components/EditEmployeeModal.jsx
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const s = {
|
||||||
|
overlay: {
|
||||||
|
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.8)',
|
||||||
|
zIndex: 2000, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
},
|
||||||
|
modal: {
|
||||||
|
background: '#111217', color: '#f8f9fa', width: '480px', maxWidth: '95vw',
|
||||||
|
borderRadius: '10px', boxShadow: '0 8px 40px rgba(0,0,0,0.8)',
|
||||||
|
border: '1px solid #222', overflow: 'hidden',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
background: 'linear-gradient(135deg, #000000, #151622)', color: 'white',
|
||||||
|
padding: '18px 22px', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||||
|
borderBottom: '1px solid #222',
|
||||||
|
},
|
||||||
|
title: { fontSize: '15px', fontWeight: 700 },
|
||||||
|
closeBtn: {
|
||||||
|
background: 'none', border: 'none', color: 'white', fontSize: '20px',
|
||||||
|
cursor: 'pointer', lineHeight: 1,
|
||||||
|
},
|
||||||
|
body: { padding: '22px' },
|
||||||
|
tabs: { display: 'flex', gap: '4px', marginBottom: '20px' },
|
||||||
|
tab: (active) => ({
|
||||||
|
flex: 1, padding: '8px', borderRadius: '6px', cursor: 'pointer', fontSize: '12px',
|
||||||
|
fontWeight: 700, textAlign: 'center', border: '1px solid',
|
||||||
|
background: active ? '#1a1c2e' : 'none',
|
||||||
|
borderColor: active ? '#667eea' : '#2a2b3a',
|
||||||
|
color: active ? '#667eea' : '#777',
|
||||||
|
}),
|
||||||
|
label: { fontSize: '11px', color: '#9ca0b8', textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: '5px' },
|
||||||
|
input: {
|
||||||
|
width: '100%', background: '#0d0e14', border: '1px solid #2a2b3a', borderRadius: '6px',
|
||||||
|
color: '#f8f9fa', padding: '9px 12px', fontSize: '13px', marginBottom: '14px',
|
||||||
|
outline: 'none', boxSizing: 'border-box',
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
width: '100%', background: '#0d0e14', border: '1px solid #2a2b3a', borderRadius: '6px',
|
||||||
|
color: '#f8f9fa', padding: '9px 12px', fontSize: '13px', marginBottom: '14px',
|
||||||
|
outline: 'none', boxSizing: 'border-box',
|
||||||
|
},
|
||||||
|
row: { display: 'flex', gap: '10px', justifyContent: 'flex-end', marginTop: '6px' },
|
||||||
|
btn: (color, bg) => ({
|
||||||
|
padding: '8px 18px', borderRadius: '6px', fontWeight: 700, fontSize: '13px',
|
||||||
|
cursor: 'pointer', border: `1px solid ${color}`, color, background: bg || 'none',
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
background: '#3c1114', border: '1px solid #f5c6cb', borderRadius: '6px',
|
||||||
|
padding: '10px 12px', fontSize: '12px', color: '#ffb3b8', marginBottom: '14px',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
background: '#0a2e1f', border: '1px solid #0f5132', borderRadius: '6px',
|
||||||
|
padding: '10px 12px', fontSize: '12px', color: '#9ef7c1', marginBottom: '14px',
|
||||||
|
},
|
||||||
|
mergeWarning: {
|
||||||
|
background: '#2a1f00', border: '1px solid #7a5000', borderRadius: '6px',
|
||||||
|
padding: '12px', fontSize: '12px', color: '#ffc107', marginBottom: '14px', lineHeight: 1.5,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function EditEmployeeModal({ employee, onClose, onSaved }) {
|
||||||
|
const [tab, setTab] = useState('edit');
|
||||||
|
|
||||||
|
// Edit state
|
||||||
|
const [name, setName] = useState(employee.name);
|
||||||
|
const [department, setDepartment] = useState(employee.department || '');
|
||||||
|
const [supervisor, setSupervisor] = useState(employee.supervisor || '');
|
||||||
|
const [editError, setEditError] = useState('');
|
||||||
|
const [editSaving, setEditSaving] = useState(false);
|
||||||
|
|
||||||
|
// Merge state
|
||||||
|
const [allEmployees, setAllEmployees] = useState([]);
|
||||||
|
const [sourceId, setSourceId] = useState('');
|
||||||
|
const [mergeError, setMergeError] = useState('');
|
||||||
|
const [mergeResult, setMergeResult] = useState(null);
|
||||||
|
const [merging, setMerging] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tab === 'merge') {
|
||||||
|
axios.get('/api/employees').then(r => setAllEmployees(r.data));
|
||||||
|
}
|
||||||
|
}, [tab]);
|
||||||
|
|
||||||
|
const handleEdit = async () => {
|
||||||
|
setEditError('');
|
||||||
|
setEditSaving(true);
|
||||||
|
try {
|
||||||
|
await axios.patch(`/api/employees/${employee.id}`, { name, department, supervisor });
|
||||||
|
onSaved();
|
||||||
|
onClose();
|
||||||
|
} catch (e) {
|
||||||
|
setEditError(e.response?.data?.error || 'Failed to save changes');
|
||||||
|
} finally {
|
||||||
|
setEditSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMerge = async () => {
|
||||||
|
if (!sourceId) return setMergeError('Select an employee to merge in');
|
||||||
|
setMergeError('');
|
||||||
|
setMerging(true);
|
||||||
|
try {
|
||||||
|
const r = await axios.post(`/api/employees/${employee.id}/merge`, { source_id: parseInt(sourceId) });
|
||||||
|
setMergeResult(r.data);
|
||||||
|
onSaved(); // refresh dashboard / parent list
|
||||||
|
} catch (e) {
|
||||||
|
setMergeError(e.response?.data?.error || 'Merge failed');
|
||||||
|
} finally {
|
||||||
|
setMerging(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const otherEmployees = allEmployees.filter(e => e.id !== employee.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={s.overlay} onClick={e => e.target === e.currentTarget && onClose()}>
|
||||||
|
<div style={s.modal}>
|
||||||
|
<div style={s.header}>
|
||||||
|
<div style={s.title}>Edit Employee</div>
|
||||||
|
<button style={s.closeBtn} onClick={onClose}>✕</button>
|
||||||
|
</div>
|
||||||
|
<div style={s.body}>
|
||||||
|
<div style={s.tabs}>
|
||||||
|
<button style={s.tab(tab === 'edit')} onClick={() => setTab('edit')}>Edit Details</button>
|
||||||
|
<button style={s.tab(tab === 'merge')} onClick={() => setTab('merge')}>Merge Duplicate</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{tab === 'edit' && (
|
||||||
|
<>
|
||||||
|
{editError && <div style={s.error}>{editError}</div>}
|
||||||
|
<div style={s.label}>Full Name</div>
|
||||||
|
<input style={s.input} value={name} onChange={e => setName(e.target.value)} />
|
||||||
|
<div style={s.label}>Department</div>
|
||||||
|
<input style={s.input} value={department} onChange={e => setDepartment(e.target.value)} placeholder="Optional" />
|
||||||
|
<div style={s.label}>Supervisor</div>
|
||||||
|
<input style={s.input} value={supervisor} onChange={e => setSupervisor(e.target.value)} placeholder="Optional" />
|
||||||
|
<div style={s.row}>
|
||||||
|
<button style={s.btn('#888')} onClick={onClose}>Cancel</button>
|
||||||
|
<button style={s.btn('#fff', '#667eea')} onClick={handleEdit} disabled={editSaving}>
|
||||||
|
{editSaving ? 'Saving…' : 'Save Changes'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tab === 'merge' && (
|
||||||
|
<>
|
||||||
|
{mergeResult ? (
|
||||||
|
<div style={s.success}>
|
||||||
|
✓ Merge complete — {mergeResult.violations_reassigned} violation{mergeResult.violations_reassigned !== 1 ? 's' : ''} reassigned
|
||||||
|
to <strong>{employee.name}</strong>. The duplicate record has been removed.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div style={s.mergeWarning}>
|
||||||
|
⚠ This will reassign <strong>all violations</strong> from the selected employee into{' '}
|
||||||
|
<strong>{employee.name}</strong>, then permanently delete the duplicate record.
|
||||||
|
This cannot be undone.
|
||||||
|
</div>
|
||||||
|
{mergeError && <div style={s.error}>{mergeError}</div>}
|
||||||
|
<div style={s.label}>Duplicate to merge into {employee.name}</div>
|
||||||
|
<select style={s.select} value={sourceId} onChange={e => setSourceId(e.target.value)}>
|
||||||
|
<option value="">— select employee —</option>
|
||||||
|
{otherEmployees.map(e => (
|
||||||
|
<option key={e.id} value={e.id}>{e.name}{e.department ? ` (${e.department})` : ''}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div style={s.row}>
|
||||||
|
<button style={s.btn('#888')} onClick={onClose}>Cancel</button>
|
||||||
|
<button style={s.btn('#fff', '#c0392b')} onClick={handleMerge} disabled={merging || !sourceId}>
|
||||||
|
{merging ? 'Merging…' : 'Merge & Delete Duplicate'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{mergeResult && (
|
||||||
|
<div style={s.row}>
|
||||||
|
<button style={s.btn('#fff', '#667eea')} onClick={onClose}>Done</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user