feat: EmployeeNotes component — inline free-text notes with quick-add HR flag tags
This commit is contained in:
146
client/src/components/EmployeeNotes.jsx
Normal file
146
client/src/components/EmployeeNotes.jsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import React, { useState } from 'react';
|
||||
import axios from 'axios';
|
||||
|
||||
const s = {
|
||||
wrapper: { marginTop: '20px' },
|
||||
sectionHd: {
|
||||
fontSize: '13px', fontWeight: 700, color: '#f8f9fa', textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px', marginBottom: '8px',
|
||||
},
|
||||
display: {
|
||||
background: '#181924', border: '1px solid #2a2b3a', borderRadius: '6px',
|
||||
padding: '10px 12px', fontSize: '13px', color: '#f8f9fa', minHeight: '36px',
|
||||
cursor: 'pointer', position: 'relative',
|
||||
},
|
||||
displayEmpty: {
|
||||
color: '#555770', fontStyle: 'italic',
|
||||
},
|
||||
editHint: {
|
||||
position: 'absolute', right: '8px', top: '8px',
|
||||
fontSize: '10px', color: '#555770',
|
||||
},
|
||||
textarea: {
|
||||
width: '100%', background: '#0d1117', border: '1px solid #4d6fa8',
|
||||
borderRadius: '6px', color: '#f8f9fa', fontSize: '13px',
|
||||
padding: '10px 12px', resize: 'vertical', minHeight: '80px',
|
||||
boxSizing: 'border-box', fontFamily: 'inherit', outline: 'none',
|
||||
},
|
||||
actions: { display: 'flex', gap: '8px', marginTop: '8px' },
|
||||
saveBtn: {
|
||||
background: '#1a3a6b', border: '1px solid #4d6fa8', color: '#90caf9',
|
||||
borderRadius: '5px', padding: '5px 14px', fontSize: '12px',
|
||||
cursor: 'pointer', fontWeight: 600,
|
||||
},
|
||||
cancelBtn: {
|
||||
background: 'none', border: '1px solid #444', color: '#888',
|
||||
borderRadius: '5px', padding: '5px 14px', fontSize: '12px',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
saving: { fontSize: '12px', color: '#9ca0b8', alignSelf: 'center' },
|
||||
tagRow: { display: 'flex', flexWrap: 'wrap', gap: '6px', marginBottom: '8px' },
|
||||
tag: {
|
||||
display: 'inline-block', padding: '2px 8px', borderRadius: '10px',
|
||||
fontSize: '11px', fontWeight: 600, background: '#1a2a3a',
|
||||
color: '#90caf9', border: '1px solid #2a3a5a', cursor: 'default',
|
||||
},
|
||||
};
|
||||
|
||||
// Quick-add tags for common HR flags
|
||||
const QUICK_TAGS = ['On PIP', 'Union member', 'Probationary', 'Pending investigation', 'FMLA', 'ADA'];
|
||||
|
||||
export default function EmployeeNotes({ employeeId, initialNotes, onSaved }) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [draft, setDraft] = useState(initialNotes || '');
|
||||
const [saved, setSaved] = useState(initialNotes || '');
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await axios.patch(`/api/employees/${employeeId}/notes`, { notes: draft });
|
||||
setSaved(draft);
|
||||
setEditing(false);
|
||||
if (onSaved) onSaved(draft);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setDraft(saved);
|
||||
setEditing(false);
|
||||
};
|
||||
|
||||
const addTag = (tag) => {
|
||||
const current = draft.trim();
|
||||
// Don't add a tag that's already present
|
||||
if (current.includes(tag)) return;
|
||||
setDraft(current ? `${current}\n${tag}` : tag);
|
||||
};
|
||||
|
||||
// Parse saved notes into display lines
|
||||
const lines = saved ? saved.split('\n').filter(Boolean) : [];
|
||||
|
||||
return (
|
||||
<div style={s.wrapper}>
|
||||
<div style={s.sectionHd}>Notes & Flags</div>
|
||||
|
||||
{!editing ? (
|
||||
<div
|
||||
style={s.display}
|
||||
onClick={() => { setDraft(saved); setEditing(true); }}
|
||||
title="Click to edit"
|
||||
>
|
||||
<span style={s.editHint}>✎ edit</span>
|
||||
{lines.length === 0 ? (
|
||||
<span style={s.displayEmpty}>No notes — click to add</span>
|
||||
) : (
|
||||
<div style={s.tagRow}>
|
||||
{lines.map((line, i) => (
|
||||
<span key={i} style={s.tag}>{line}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{/* Quick-add tag buttons */}
|
||||
<div style={{ ...s.tagRow, marginBottom: '6px' }}>
|
||||
{QUICK_TAGS.map(tag => (
|
||||
<button
|
||||
key={tag}
|
||||
style={{
|
||||
...s.tag,
|
||||
cursor: 'pointer',
|
||||
background: draft.includes(tag) ? '#0e2a3a' : '#1a2a3a',
|
||||
opacity: draft.includes(tag) ? 0.5 : 1,
|
||||
}}
|
||||
onClick={() => addTag(tag)}
|
||||
title="Add tag"
|
||||
>
|
||||
+ {tag}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
style={s.textarea}
|
||||
value={draft}
|
||||
onChange={e => setDraft(e.target.value)}
|
||||
placeholder="Free-text notes — one per line or comma-separated. Does not affect CPAS scoring."
|
||||
autoFocus
|
||||
/>
|
||||
<div style={s.actions}>
|
||||
<button style={s.saveBtn} onClick={handleSave} disabled={saving}>
|
||||
{saving ? 'Saving…' : 'Save Notes'}
|
||||
</button>
|
||||
<button style={s.cancelBtn} onClick={handleCancel} disabled={saving}>
|
||||
Cancel
|
||||
</button>
|
||||
{saving && <span style={s.saving}>Saving…</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user