feat: replace department text input with preloaded select dropdown

This commit is contained in:
jason committed 2026-03-07 22:41:20 -06:00
1 parent 43e8b87da9
commit 9864de0f8a
1 file changed
+1 -189
+1 -189
View File
@@ -1,189 +1 @@
import React, { useState, useEffect } from 'react'; aW1wb3J0IFJlYWN0LCB7IHVzZVN0YXRlLCB1c2VFZmZlY3QgfSBmcm9tICdyZWFjdCc7CmltcG9ydCBheGlvcyBmcm9tICdheGlvcyc7CmltcG9ydCB7IERFUEFSVE1FTlRTIH0gZnJvbSAnLi4vZGF0YS9kZXBhcnRtZW50cyc7Cgpjb25zdCBzID0gewogIG92ZXJsYXk6IHsKICAgIHBvc2l0aW9uOiAnZml4ZWQnLCBpbnNldDogMCwgYmFja2dyb3VuZDogJ3JnYmEoMCwwLDAsMC44KScsCiAgICB6SW5kZXg6IDIwMDAsIGRpc3BsYXk6ICdmbGV4JywgYWxpZ25JdGVtczogJ2NlbnRlcicsIGp1c3RpZnlDb250ZW50OiAnY2VudGVyJywKICB9LAogIG1vZGFsOiB7CiAgICBiYWNrZ3JvdW5kOiAnIzExMTIxNycsIGNvbG9yOiAnI2Y4ZjlmYScsIHdpZHRoOiAnNDgwcHgnLCBtYXhXaWR0aDogJzk1dncnLAogICAgYm9yZGVyUmFkaXVzOiAnMTBweCcsIGJveFNoYWRvdzogJzAgOHB4IDQwcHggcmdiYSgwLDAsMCwwLjgpJywKICAgIGJvcmRlcjogJzFweCBzb2xpZCAjMjIyJywgb3ZlcmZsb3c6ICdoaWRkZW4nLAogIH0sCiAgaGVhZGVyOiB7CiAgICBiYWNrZ3JvdW5kOiAnbGluZWFyLWdyYWRpZW50KDEzNWRlZywgIzAwMDAwMCwgIzE1MTYyMiknLCBjb2xvcjogJ3doaXRlJywKICAgIHBhZGRpbmc6ICcxOHB4IDIycHgnLCBkaXNwbGF5OiAnZmxleCcsIGFsaWduSXRlbXM6ICdjZW50ZXInLCBqdXN0aWZ5Q29udGVudDogJ3NwYWNlLWJldHdlZW4nLAogICAgYm9yZGVyQm90dG9tOiAnMXB4IHNvbGlkICMyMjInLAogIH0sCiAgdGl0bGU6IHsgZm9udFNpemU6ICcxNXB4JywgZm9udFdlaWdodDogNzAwIH0sCiAgY2xvc2VCdG46IHsKICAgIGJhY2tncm91bmQ6ICdub25lJywgYm9yZGVyOiAnbm9uZScsIGNvbG9yOiAnd2hpdGUnLCBmb250U2l6ZTogJzIwcHgnLAogICAgY3Vyc29yOiAncG9pbnRlcicsIGxpbmVIZWlnaHQ6IDEsCiAgfSwKICBib2R5OiB7IHBhZGRpbmc6ICcyMnB4JyB9LAogIHRhYnM6IHsgZGlzcGxheTogJ2ZsZXgnLCBnYXA6ICc0cHgnLCBtYXJnaW5Cb3R0b206ICcyMHB4JyB9LAogIHRhYjogKGFjdGl2ZSkgPT4gKHsKICAgIGZsZXg6IDEsIHBhZGRpbmc6ICc4cHgnLCBib3JkZXJSYWRpdXM6ICc2cHgnLCBjdXJzb3I6ICdwb2ludGVyJywgZm9udFNpemU6ICcxMnB4JywKICAgIGZvbnRXZWlnaHQ6IDcwMCwgdGV4dEFsaWduOiAnY2VudGVyJywgYm9yZGVyOiAnMXB4IHNvbGlkJywKICAgIGJhY2tncm91bmQ6IGFjdGl2ZSA/ICcjMWExYzJlJyA6ICdub25lJywKICAgIGJvcmRlckNvbG9yOiBhY3RpdmUgPyAnIzY2N2VlYScgOiAnIzJhMmIzYScsCiAgICBjb2xvcjogYWN0aXZlID8gJyM2NjdlZWEnIDogJyM3NzcnLAogIH0pLAogIGxhYmVsOiB7IGZvbnRTaXplOiAnMTFweCcsIGNvbG9yOiAnIzljYTBiOCcsIHRleHRUcmFuc2Zvcm06ICd1cHBlcmNhc2UnLCBsZXR0ZXJTcGFjaW5nOiAnMC41cHgnLCBtYXJnaW5Cb3R0b206ICc1cHgnIH0sCiAgaW5wdXQ6IHsKICAgIHdpZHRoOiAnMTAwJScsIGJhY2tncm91bmQ6ICcjMGQwZTE0JywgYm9yZGVyOiAnMXB4IHNvbGlkICMyYTJiM2EnLCBib3JkZXJSYWRpdXM6ICc2cHgnLAogICAgY29sb3I6ICcjZjhmOWZhJywgcGFkZGluZzogJzlweCAxMnB4JywgZm9udFNpemU6ICcxM3B4JywgbWFyZ2luQm90dG9tOiAnMTRweCcsCiAgICBvdXRsaW5lOiAnbm9uZScsIGJveFNpemluZzogJ2JvcmRlci1ib3gnLAogIH0sCiAgc2VsZWN0OiB7CiAgICB3aWR0aDogJzEwMCUnLCBiYWNrZ3JvdW5kOiAnIzBkMGUxNCcsIGJvcmRlcjogJzFweCBzb2xpZCAjMmEyYjNhJywgYm9yZGVyUmFkaXVzOiAnNnB4JywKICAgIGNvbG9yOiAnI2Y4ZjlmYScsIHBhZGRpbmc6ICc5cHggMTJweCcsIGZvbnRTaXplOiAnMTNweCcsIG1hcmdpbkJvdHRvbTogJzE0cHgnLAogICAgb3V0bGluZTogJ25vbmUnLCBib3hTaXppbmc6ICdib3JkZXItYm94JywKICB9LAogIHJvdzogeyBkaXNwbGF5OiAnZmxleCcsIGdhcDogJzEwcHgnLCBqdXN0aWZ5Q29udGVudDogJ2ZsZXgtZW5kJywgbWFyZ2luVG9wOiAnNnB4JyB9LAogIGJ0bjogKGNvbG9yLCBiZykgPT4gKHsKICAgIHBhZGRpbmc6ICc4cHggMThweCcsIGJvcmRlclJhZGl1czogJzZweCcsIGZvbnRXZWlnaHQ6IDcwMCwgZm9udFNpemU6ICcxM3B4JywKICAgIGN1cnNvcjogJ3BvaW50ZXInLCBib3JkZXI6IGAxcHggc29saWQgJHtjb2xvcn1gLCBjb2xvciwgYmFja2dyb3VuZDogYmcgfHwgJ25vbmUnLAogIH0pLAogIGVycm9yOiB7CiAgICBiYWNrZ3JvdW5kOiAnIzNjMTExNCcsIGJvcmRlcjogJzFweCBzb2xpZCAjZjVjNmNiJywgYm9yZGVyUmFkaXVzOiAnNnB4JywKICAgIHBhZGRpbmc6ICcxMHB4IDEycHgnLCBmb250U2l6ZTogJzEycHgnLCBjb2xvcjogJyNmZmIzYjgnLCBtYXJnaW5Cb3R0b206ICcxNHB4JywKICB9LAogIHN1Y2Nlc3M6IHsKICAgIGJhY2tncm91bmQ6ICcjMGEyZTFmJywgYm9yZGVyOiAnMXB4IHNvbGlkICMwZjUxMzInLCBib3JkZXJSYWRpdXM6ICc2cHgnLAogICAgcGFkZGluZzogJzEwcHggMTJweCcsIGZvbnRTaXplOiAnMTJweCcsIGNvbG9yOiAnIzllZjdjMScsIG1hcmdpbkJvdHRvbTogJzE0cHgnLAogIH0sCiAgbWVyZ2VXYXJuaW5nOiB7CiAgICBiYWNrZ3JvdW5kOiAnIzJhMWYwMCcsIGJvcmRlcjogJzFweCBzb2xpZCAjN2E1MDAwJywgYm9yZGVyUmFkaXVzOiAnNnB4JywKICAgIHBhZGRpbmc6ICcxMnB4JywgZm9udFNpemU6ICcxMnB4JywgY29sb3I6ICcjZmZjMTA3JywgbWFyZ2luQm90dG9tOiAnMTRweCcsIGxpbmVIZWlnaHQ6IDEuNSwKICB9LAp9OwoKZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24gRWRpdEVtcGxveWVlTW9kYWwoeyBlbXBsb3llZSwgb25DbG9zZSwgb25TYXZlZCB9KSB7CiAgY29uc3QgW3RhYiwgc2V0VGFiXSA9IHVzZVN0YXRlKCdlZGl0Jyk7CgogIC8vIEVkaXQgc3RhdGUKICBjb25zdCBbbmFtZSwgc2V0TmFtZV0gICAgICAgICAgICAgICAgICAgPSB1c2VTdGF0ZShlbXBsb3llZS5uYW1lKTsKICBjb25zdCBbZGVwYXJ0bWVudCwgc2V0RGVwYXJ0bWVudF0gICAgICAgPSB1c2VTdGF0ZShlbXBsb3llZS5kZXBhcnRtZW50IHx8ICcnKTsKICBjb25zdCBbc3VwZXJ2aXNvciwgc2V0U3VwZXJ2aXNvcl0gICAgICAgPSB1c2VTdGF0ZShlbXBsb3llZS5zdXBlcnZpc29yIHx8ICcnKTsKICBjb25zdCBbZWRpdEVycm9yLCBzZXRFZGl0RXJyb3JdICAgICAgICAgPSB1c2VTdGF0ZSgnJyk7CiAgY29uc3QgW2VkaXRTYXZpbmcsIHNldEVkaXRTYXZpbmddICAgICAgID0gdXNlU3RhdGUoZmFsc2UpOwoKICAvLyBNZXJnZSBzdGF0ZQogIGNvbnN0IFthbGxFbXBsb3llZXMsIHNldEFsbEVtcGxveWVlc10gICA9IHVzZVN0YXRlKFtdKTsKICBjb25zdCBbc291cmNlSWQsIHNldFNvdXJjZUlkXSAgICAgICAgICAgPSB1c2VTdGF0ZSgnJyk7CiAgY29uc3QgW21lcmdlRXJyb3IsIHNldE1lcmdlRXJyb3JdICAgICAgID0gdXNlU3RhdGUoJycpOwogIGNvbnN0IFttZXJnZVJlc3VsdCwgc2V0TWVyZ2VSZXN1bHRdICAgICA9IHVzZVN0YXRlKG51bGwpOwogIGNvbnN0IFttZXJnaW5nLCBzZXRNZXJnaW5nXSAgICAgICAgICAgICA9IHVzZVN0YXRlKGZhbHNlKTsKCiAgdXNlRWZmZWN0KCgpID0+IHsKICAgIGlmICh0YWIgPT09ICdtZXJnZScpIHsKICAgICAgYXhpb3MuZ2V0KCcvYXBpL2VtcGxveWVlcycpLnRoZW4ociA9PiBzZXRBbGxFbXBsb3llZXMoci5kYXRhKSk7CiAgICB9CiAgfSwgW3RhYl0pOwoKICBjb25zdCBoYW5kbGVFZGl0ID0gYXN5bmMgKCkgPT4gewogICAgc2V0RWRLine truncated
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>
);
}