feat: replace department text input with preloaded select dropdown
This commit is contained in:
1 parent
43e8b87da9
commit
9864de0f8a
1 file changed
+1
-189
@@ -1,189 +1 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
aW1wb3J0IFJlYWN0LCB7IHVzZVN0YXRlLCB1c2VFZmZlY3QgfSBmcm9tICdyZWFjdCc7CmltcG9ydCBheGlvcyBmcm9tICdheGlvcyc7CmltcG9ydCB7IERFUEFSVE1FTlRTIH0gZnJvbSAnLi4vZGF0YS9kZXBhcnRtZW50cyc7Cgpjb25zdCBzID0gewogIG92ZXJsYXk6IHsKICAgIHBvc2l0aW9uOiAnZml4ZWQnLCBpbnNldDogMCwgYmFja2dyb3VuZDogJ3JnYmEoMCwwLDAsMC44KScsCiAgICB6SW5kZXg6IDIwMDAsIGRpc3BsYXk6ICdmbGV4JywgYWxpZ25JdGVtczogJ2NlbnRlcicsIGp1c3RpZnlDb250ZW50OiAnY2VudGVyJywKICB9LAogIG1vZGFsOiB7CiAgICBiYWNrZ3JvdW5kOiAnIzExMTIxNycsIGNvbG9yOiAnI2Y4ZjlmYScsIHdpZHRoOiAnNDgwcHgnLCBtYXhXaWR0aDogJzk1dncnLAogICAgYm9yZGVyUmFkaXVzOiAnMTBweCcsIGJveFNoYWRvdzogJzAgOHB4IDQwcHggcmdiYSgwLDAsMCwwLjgpJywKICAgIGJvcmRlcjogJzFweCBzb2xpZCAjMjIyJywgb3ZlcmZsb3c6ICdoaWRkZW4nLAogIH0sCiAgaGVhZGVyOiB7CiAgICBiYWNrZ3JvdW5kOiAnbGluZWFyLWdyYWRpZW50KDEzNWRlZywgIzAwMDAwMCwgIzE1MTYyMiknLCBjb2xvcjogJ3doaXRlJywKICAgIHBhZGRpbmc6ICcxOHB4IDIycHgnLCBkaXNwbGF5OiAnZmxleCcsIGFsaWduSXRlbXM6ICdjZW50ZXInLCBqdXN0aWZ5Q29udGVudDogJ3NwYWNlLWJldHdlZW4nLAogICAgYm9yZGVyQm90dG9tOiAnMXB4IHNvbGlkICMyMjInLAogIH0sCiAgdGl0bGU6IHsgZm9udFNpemU6ICcxNXB4JywgZm9udFdlaWdodDogNzAwIH0sCiAgY2xvc2VCdG46IHsKICAgIGJhY2tncm91bmQ6ICdub25lJywgYm9yZGVyOiAnbm9uZScsIGNvbG9yOiAnd2hpdGUnLCBmb250U2l6ZTogJzIwcHgnLAogICAgY3Vyc29yOiAncG9pbnRlcicsIGxpbmVIZWlnaHQ6IDEsCiAgfSwKICBib2R5OiB7IHBhZGRpbmc6ICcyMnB4JyB9LAogIHRhYnM6IHsgZGlzcGxheTogJ2ZsZXgnLCBnYXA6ICc0cHgnLCBtYXJnaW5Cb3R0b206ICcyMHB4JyB9LAogIHRhYjogKGFjdGl2ZSkgPT4gKHsKICAgIGZsZXg6IDEsIHBhZGRpbmc6ICc4cHgnLCBib3JkZXJSYWRpdXM6ICc2cHgnLCBjdXJzb3I6ICdwb2ludGVyJywgZm9udFNpemU6ICcxMnB4JywKICAgIGZvbnRXZWlnaHQ6IDcwMCwgdGV4dEFsaWduOiAnY2VudGVyJywgYm9yZGVyOiAnMXB4IHNvbGlkJywKICAgIGJhY2tncm91bmQ6IGFjdGl2ZSA/ICcjMWExYzJlJyA6ICdub25lJywKICAgIGJvcmRlckNvbG9yOiBhY3RpdmUgPyAnIzY2N2VlYScgOiAnIzJhMmIzYScsCiAgICBjb2xvcjogYWN0aXZlID8gJyM2NjdlZWEnIDogJyM3NzcnLAogIH0pLAogIGxhYmVsOiB7IGZvbnRTaXplOiAnMTFweCcsIGNvbG9yOiAnIzljYTBiOCcsIHRleHRUcmFuc2Zvcm06ICd1cHBlcmNhc2UnLCBsZXR0ZXJTcGFjaW5nOiAnMC41cHgnLCBtYXJnaW5Cb3R0b206ICc1cHgnIH0sCiAgaW5wdXQ6IHsKICAgIHdpZHRoOiAnMTAwJScsIGJhY2tncm91bmQ6ICcjMGQwZTE0JywgYm9yZGVyOiAnMXB4IHNvbGlkICMyYTJiM2EnLCBib3JkZXJSYWRpdXM6ICc2cHgnLAogICAgY29sb3I6ICcjZjhmOWZhJywgcGFkZGluZzogJzlweCAxMnB4JywgZm9udFNpemU6ICcxM3B4JywgbWFyZ2luQm90dG9tOiAnMTRweCcsCiAgICBvdXRsaW5lOiAnbm9uZScsIGJveFNpemluZzogJ2JvcmRlci1ib3gnLAogIH0sCiAgc2VsZWN0OiB7CiAgICB3aWR0aDogJzEwMCUnLCBiYWNrZ3JvdW5kOiAnIzBkMGUxNCcsIGJvcmRlcjogJzFweCBzb2xpZCAjMmEyYjNhJywgYm9yZGVyUmFkaXVzOiAnNnB4JywKICAgIGNvbG9yOiAnI2Y4ZjlmYScsIHBhZGRpbmc6ICc5cHggMTJweCcsIGZvbnRTaXplOiAnMTNweCcsIG1hcmdpbkJvdHRvbTogJzE0cHgnLAogICAgb3V0bGluZTogJ25vbmUnLCBib3hTaXppbmc6ICdib3JkZXItYm94JywKICB9LAogIHJvdzogeyBkaXNwbGF5OiAnZmxleCcsIGdhcDogJzEwcHgnLCBqdXN0aWZ5Q29udGVudDogJ2ZsZXgtZW5kJywgbWFyZ2luVG9wOiAnNnB4JyB9LAogIGJ0bjogKGNvbG9yLCBiZykgPT4gKHsKICAgIHBhZGRpbmc6ICc4cHggMThweCcsIGJvcmRlclJhZGl1czogJzZweCcsIGZvbnRXZWlnaHQ6IDcwMCwgZm9udFNpemU6ICcxM3B4JywKICAgIGN1cnNvcjogJ3BvaW50ZXInLCBib3JkZXI6IGAxcHggc29saWQgJHtjb2xvcn1gLCBjb2xvciwgYmFja2dyb3VuZDogYmcgfHwgJ25vbmUnLAogIH0pLAogIGVycm9yOiB7CiAgICBiYWNrZ3JvdW5kOiAnIzNjMTExNCcsIGJvcmRlcjogJzFweCBzb2xpZCAjZjVjNmNiJywgYm9yZGVyUmFkaXVzOiAnNnB4JywKICAgIHBhZGRpbmc6ICcxMHB4IDEycHgnLCBmb250U2l6ZTogJzEycHgnLCBjb2xvcjogJyNmZmIzYjgnLCBtYXJnaW5Cb3R0b206ICcxNHB4JywKICB9LAogIHN1Y2Nlc3M6IHsKICAgIGJhY2tncm91bmQ6ICcjMGEyZTFmJywgYm9yZGVyOiAnMXB4IHNvbGlkICMwZjUxMzInLCBib3JkZXJSYWRpdXM6ICc2cHgnLAogICAgcGFkZGluZzogJzEwcHggMTJweCcsIGZvbnRTaXplOiAnMTJweCcsIGNvbG9yOiAnIzllZjdjMScsIG1hcmdpbkJvdHRvbTogJzE0cHgnLAogIH0sCiAgbWVyZ2VXYXJuaW5nOiB7CiAgICBiYWNrZ3JvdW5kOiAnIzJhMWYwMCcsIGJvcmRlcjogJzFweCBzb2xpZCAjN2E1MDAwJywgYm9yZGVyUmFkaXVzOiAnNnB4JywKICAgIHBhZGRpbmc6ICcxMnB4JywgZm9udFNpemU6ICcxMnB4JywgY29sb3I6ICcjZmZjMTA3JywgbWFyZ2luQm90dG9tOiAnMTRweCcsIGxpbmVIZWlnaHQ6IDEuNSwKICB9LAp9OwoKZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24gRWRpdEVtcGxveWVlTW9kYWwoeyBlbXBsb3llZSwgb25DbG9zZSwgb25TYXZlZCB9KSB7CiAgY29uc3QgW3RhYiwgc2V0VGFiXSA9IHVzZVN0YXRlKCdlZGl0Jyk7CgogIC8vIEVkaXQgc3RhdGUKICBjb25zdCBbbmFtZSwgc2V0TmFtZV0gICAgICAgICAgICAgICAgICAgPSB1c2VTdGF0ZShlbXBsb3llZS5uYW1lKTsKICBjb25zdCBbZGVwYXJ0bWVudCwgc2V0RGVwYXJ0bWVudF0gICAgICAgPSB1c2VTdGF0ZShlbXBsb3llZS5kZXBhcnRtZW50IHx8ICcnKTsKICBjb25zdCBbc3VwZXJ2aXNvciwgc2V0U3VwZXJ2aXNvcl0gICAgICAgPSB1c2VTdGF0ZShlbXBsb3llZS5zdXBlcnZpc29yIHx8ICcnKTsKICBjb25zdCBbZWRpdEVycm9yLCBzZXRFZGl0RXJyb3JdICAgICAgICAgPSB1c2VTdGF0ZSgnJyk7CiAgY29uc3QgW2VkaXRTYXZpbmcsIHNldEVkaXRTYXZpbmddICAgICAgID0gdXNlU3RhdGUoZmFsc2UpOwoKICAvLyBNZXJnZSBzdGF0ZQogIGNvbnN0IFthbGxFbXBsb3llZXMsIHNldEFsbEVtcGxveWVlc10gICA9IHVzZVN0YXRlKFtdKTsKICBjb25zdCBbc291cmNlSWQsIHNldFNvdXJjZUlkXSAgICAgICAgICAgPSB1c2VTdGF0ZSgnJyk7CiAgY29uc3QgW21lcmdlRXJyb3IsIHNldE1lcmdlRXJyb3JdICAgICAgID0gdXNlU3RhdGUoJycpOwogIGNvbnN0IFttZXJnZVJlc3VsdCwgc2V0TWVyZ2VSZXN1bHRdICAgICA9IHVzZVN0YXRlKG51bGwpOwogIGNvbnN0IFttZXJnaW5nLCBzZXRNZXJnaW5nXSAgICAgICAgICAgICA9IHVzZVN0YXRlKGZhbHNlKTsKCiAgdXNlRWZmZWN0KCgpID0+IHsKICAgIGlmICh0YWIgPT09ICdtZXJnZScpIHsKICAgICAgYXhpb3MuZ2V0KCcvYXBpL2VtcGxveWVlcycpLnRoZW4ociA9PiBzZXRBbGxFbXBsb3llZXMoci5kYXRhKSk7CiAgICB9CiAgfSwgW3RhYl0pOwoKICBjb25zdCBoYW5kbGVFZGl0ID0gYXN5bmMgKCkgPT4gewogICAgc2V0RWRLine truncated
|
||||
Reference in new issue
Block a user