update: Add Global Search on Header, Add Global Audit log for all actions.

This commit is contained in:
2026-04-19 15:41:29 +03:00
parent 4f35bef6e3
commit 6a958a8d7d
27 changed files with 2086 additions and 267 deletions
@@ -431,97 +431,7 @@ export default function ManageTab({ device, canEdit, deviceUsers: propUsers, use
</div>
</GlassCard>
{/* ── 2. DEVICE NOTES ───────────────────────────────────────────────── */}
<GlassCard>
<CardHeader
label="Device Notes"
count={notes.length || undefined}
action={canEdit && (
<Button variant="secondary" size="sm" onClick={() => setNoteModal({ open: true, note: null })}>
+ Add Note
</Button>
)}
/>
<div style={{ padding: notesLoading || notes.length === 0 ? 0 : 'var(--space-4) var(--space-5)' }}>
{notesLoading ? (
<div style={{ display: 'flex', justifyContent: 'center', padding: 'var(--space-8)' }}>
<Spinner size="sm" />
</div>
) : notes.length === 0 ? (
<EmptySlate
icon={
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>
}
message="No notes recorded for this device yet."
action={canEdit && (
<Button variant="secondary" size="sm" onClick={() => setNoteModal({ open: true, note: null })}>
Add first note
</Button>
)}
/>
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: 'var(--space-3)' }}>
{notes.map(note => (
<div
key={note.id}
className="manage-note-card"
style={{
display: 'flex', flexDirection: 'column', gap: 'var(--space-3)',
padding: 'var(--space-4)',
borderRadius: 'var(--radius-lg)',
background: GLASS_INNER,
border: '1px solid var(--color-border)',
boxShadow: 'var(--shadow-sm)',
}}
>
<p style={{ margin: 0, fontSize: 'var(--font-size-sm)', color: 'var(--color-text-primary)', lineHeight: 1.6, whiteSpace: 'pre-wrap', flex: 1 }}>
{note.content || '—'}
</p>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--space-2)' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, minWidth: 0 }}>
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)' }} title={fmtDateMedium(note.created_at)}>
{fmtRelative(note.created_at)}
</span>
{note.created_by && (
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{note.created_by}
</span>
)}
</div>
{canEdit && (
<div style={{ display: 'flex', gap: 'var(--space-1)', flexShrink: 0 }}>
<button onClick={() => setNoteModal({ open: true, note })} title="Edit note" aria-label="Edit note"
style={{ width: 26, height: 26, borderRadius: 'var(--radius-sm)', border: '1px solid transparent', background: 'none', color: 'var(--color-text-muted)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'color 0.12s, background 0.12s, border-color 0.12s' }}
onMouseEnter={e => { e.currentTarget.style.color = 'var(--color-primary)'; e.currentTarget.style.background = 'var(--color-primary-subtle)'; e.currentTarget.style.borderColor = 'rgba(192,193,255,0.25)' }}
onMouseLeave={e => { e.currentTarget.style.color = 'var(--color-text-muted)'; e.currentTarget.style.background = 'none'; e.currentTarget.style.borderColor = 'transparent' }}
>
<Icon name="edit" size={13} />
</button>
<button onClick={() => setConfirmDeleteNote(note)} title="Delete note" aria-label="Delete note"
style={{ width: 26, height: 26, borderRadius: 'var(--radius-sm)', border: '1px solid transparent', background: 'none', color: 'var(--color-text-muted)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'color 0.12s, background 0.12s, border-color 0.12s' }}
onMouseEnter={e => { e.currentTarget.style.color = 'var(--color-danger)'; e.currentTarget.style.background = 'var(--color-danger-bg)'; e.currentTarget.style.borderColor = 'var(--color-danger)' }}
onMouseLeave={e => { e.currentTarget.style.color = 'var(--color-text-muted)'; e.currentTarget.style.background = 'none'; e.currentTarget.style.borderColor = 'transparent' }}
>
<Icon name="delete" size={13} />
</button>
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
</GlassCard>
{/* ── 3. LINKED ISSUES ──────────────────────────────────────────────── */}
{/* ── 2. LINKED ISSUES ──────────────────────────────────────────────── */}
<GlassCard>
<CardHeader
label="Linked Issues"
@@ -605,6 +515,96 @@ export default function ManageTab({ device, canEdit, deviceUsers: propUsers, use
</div>
</GlassCard>
{/* ── 3. DEVICE NOTES ───────────────────────────────────────────────── */}
<GlassCard>
<CardHeader
label="Device Notes"
count={notes.length || undefined}
action={canEdit && (
<Button variant="secondary" size="sm" onClick={() => setNoteModal({ open: true, note: null })}>
+ Add Note
</Button>
)}
/>
<div style={{ padding: notesLoading || notes.length === 0 ? 0 : 'var(--space-4) var(--space-5)' }}>
{notesLoading ? (
<div style={{ display: 'flex', justifyContent: 'center', padding: 'var(--space-8)' }}>
<Spinner size="sm" />
</div>
) : notes.length === 0 ? (
<EmptySlate
icon={
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>
}
message="No notes recorded for this device yet."
action={canEdit && (
<Button variant="secondary" size="sm" onClick={() => setNoteModal({ open: true, note: null })}>
Add first note
</Button>
)}
/>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-3)' }}>
{notes.map(note => (
<div
key={note.id}
className="manage-note-card"
style={{
display: 'flex', flexDirection: 'column', gap: 'var(--space-3)',
padding: 'var(--space-4)',
borderRadius: 'var(--radius-lg)',
background: GLASS_INNER,
border: '1px solid var(--color-border)',
boxShadow: 'var(--shadow-sm)',
}}
>
<p style={{ margin: 0, fontSize: 'var(--font-size-sm)', color: 'var(--color-text-primary)', lineHeight: 1.6, whiteSpace: 'pre-wrap' }}>
{note.content || '—'}
</p>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--space-2)' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, minWidth: 0 }}>
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)' }} title={fmtDateMedium(note.created_at)}>
{fmtRelative(note.created_at)}
</span>
{note.created_by && (
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{note.created_by}
</span>
)}
</div>
{canEdit && (
<div style={{ display: 'flex', gap: 'var(--space-1)', flexShrink: 0 }}>
<button onClick={() => setNoteModal({ open: true, note })} title="Edit note" aria-label="Edit note"
style={{ width: 26, height: 26, borderRadius: 'var(--radius-sm)', border: '1px solid transparent', background: 'none', color: 'var(--color-text-muted)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'color 0.12s, background 0.12s, border-color 0.12s' }}
onMouseEnter={e => { e.currentTarget.style.color = 'var(--color-primary)'; e.currentTarget.style.background = 'var(--color-primary-subtle)'; e.currentTarget.style.borderColor = 'rgba(192,193,255,0.25)' }}
onMouseLeave={e => { e.currentTarget.style.color = 'var(--color-text-muted)'; e.currentTarget.style.background = 'none'; e.currentTarget.style.borderColor = 'transparent' }}
>
<Icon name="edit" size={13} />
</button>
<button onClick={() => setConfirmDeleteNote(note)} title="Delete note" aria-label="Delete note"
style={{ width: 26, height: 26, borderRadius: 'var(--radius-sm)', border: '1px solid transparent', background: 'none', color: 'var(--color-text-muted)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'color 0.12s, background 0.12s, border-color 0.12s' }}
onMouseEnter={e => { e.currentTarget.style.color = 'var(--color-danger)'; e.currentTarget.style.background = 'var(--color-danger-bg)'; e.currentTarget.style.borderColor = 'var(--color-danger)' }}
onMouseLeave={e => { e.currentTarget.style.color = 'var(--color-text-muted)'; e.currentTarget.style.background = 'none'; e.currentTarget.style.borderColor = 'transparent' }}
>
<Icon name="delete" size={13} />
</button>
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
</GlassCard>
</div>
{/* ── Modals ─────────────────────────────────────────────────────────── */}