feat: add toast notifications to EmployeeModal for all actions

- Toast success/error on PDF download, negate, restore, hard delete
- Toast success on employee edit and violation amendment via modal callbacks
- Error details from API responses included in error toasts
This commit is contained in:
2026-03-07 21:40:36 -06:00
parent 114dbb1166
commit ecd3810050

View File

@@ -6,6 +6,7 @@ import EditEmployeeModal from './EditEmployeeModal';
import AmendViolationModal from './AmendViolationModal'; import AmendViolationModal from './AmendViolationModal';
import ExpirationTimeline from './ExpirationTimeline'; import ExpirationTimeline from './ExpirationTimeline';
import EmployeeNotes from './EmployeeNotes'; import EmployeeNotes from './EmployeeNotes';
import { useToast } from './ToastProvider';
const s = { const s = {
overlay: { overlay: {
@@ -97,6 +98,8 @@ export default function EmployeeModal({ employeeId, onClose }) {
const [editingEmp, setEditingEmp] = useState(false); const [editingEmp, setEditingEmp] = useState(false);
const [amending, setAmending] = useState(null); // violation object const [amending, setAmending] = useState(null); // violation object
const toast = useToast();
const load = useCallback(() => { const load = useCallback(() => {
setLoading(true); setLoading(true);
Promise.all([ Promise.all([
@@ -116,34 +119,54 @@ export default function EmployeeModal({ employeeId, onClose }) {
useEffect(() => { load(); }, [load]); useEffect(() => { load(); }, [load]);
const handleDownloadPdf = async (violId, empName, date) => { const handleDownloadPdf = async (violId, empName, date) => {
const response = await axios.get(`/api/violations/${violId}/pdf`, { responseType: 'blob' }); try {
const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/pdf' })); const response = await axios.get(`/api/violations/${violId}/pdf`, { responseType: 'blob' });
const link = document.createElement('a'); const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/pdf' }));
link.href = url; const link = document.createElement('a');
link.download = `CPAS_${(empName || '').replace(/[^a-z0-9]/gi, '_')}_${date}.pdf`; link.href = url;
document.body.appendChild(link); link.download = `CPAS_${(empName || '').replace(/[^a-z0-9]/gi, '_')}_${date}.pdf`;
link.click(); document.body.appendChild(link);
link.remove(); link.click();
window.URL.revokeObjectURL(url); link.remove();
window.URL.revokeObjectURL(url);
toast.success('PDF downloaded.');
} catch (err) {
toast.error('PDF generation failed: ' + (err.response?.data?.error || err.message));
}
}; };
const handleHardDelete = async (id) => { const handleHardDelete = async (id) => {
await axios.delete(`/api/violations/${id}`); try {
setConfirmDel(null); await axios.delete(`/api/violations/${id}`);
load(); toast.success('Violation permanently deleted.');
setConfirmDel(null);
load();
} catch (err) {
toast.error('Delete failed: ' + (err.response?.data?.error || err.message));
}
}; };
const handleRestore = async (id) => { const handleRestore = async (id) => {
await axios.patch(`/api/violations/${id}/restore`); try {
setConfirmDel(null); await axios.patch(`/api/violations/${id}/restore`);
load(); toast.success('Violation restored to active.');
setConfirmDel(null);
load();
} catch (err) {
toast.error('Restore failed: ' + (err.response?.data?.error || err.message));
}
}; };
const handleNegate = async ({ resolution_type, details, resolved_by }) => { const handleNegate = async ({ resolution_type, details, resolved_by }) => {
await axios.patch(`/api/violations/${negating.id}/negate`, { resolution_type, details, resolved_by }); try {
setNegating(null); await axios.patch(`/api/violations/${negating.id}/negate`, { resolution_type, details, resolved_by });
setConfirmDel(null); toast.success('Violation negated.');
load(); setNegating(null);
setConfirmDel(null);
load();
} catch (err) {
toast.error('Negate failed: ' + (err.response?.data?.error || err.message));
}
}; };
const tier = score ? getTier(score.active_points) : null; const tier = score ? getTier(score.active_points) : null;
@@ -203,7 +226,7 @@ export default function EmployeeModal({ employeeId, onClose }) {
</div> </div>
<div style={{ ...s.scoreCard, minWidth: '140px' }}> <div style={{ ...s.scoreCard, minWidth: '140px' }}>
<div style={{ fontSize: '13px', fontWeight: 700, color: tier?.color || '#f8f9fa' }}> <div style={{ fontSize: '13px', fontWeight: 700, color: tier?.color || '#f8f9fa' }}>
{tier ? tier.label : ''} {tier ? tier.label : ''}
</div> </div>
<div style={s.scoreLbl}>Current Tier</div> <div style={s.scoreLbl}>Current Tier</div>
</div> </div>
@@ -405,14 +428,14 @@ export default function EmployeeModal({ employeeId, onClose }) {
<EditEmployeeModal <EditEmployeeModal
employee={employee} employee={employee}
onClose={() => setEditingEmp(false)} onClose={() => setEditingEmp(false)}
onSaved={load} onSaved={() => { toast.success('Employee updated.'); load(); }}
/> />
)} )}
{amending && ( {amending && (
<AmendViolationModal <AmendViolationModal
violation={amending} violation={amending}
onClose={() => setAmending(null)} onClose={() => setAmending(null)}
onSaved={load} onSaved={() => { toast.success('Violation amended.'); load(); }}
/> />
)} )}
</div> </div>