Files
Frontend-Iasis/src/pages/entregaveis/components/EntregavelNotesTab.tsx

245 lines
7.8 KiB
TypeScript

import { useState } from 'react';
import {
MessageSquare,
Plus,
Pencil,
Eye,
AlertTriangle,
OctagonX,
Scale,
Star,
Clock,
} from 'lucide-react';
import { Button } from '../../../components/ui/Button';
import { Badge } from '../../../components/ui/Badge';
import { Select } from '../../../components/ui/Select';
import { useToast } from '../../../components/ui/Toast';
import { useNotes, useUpdateNote } from '../../../hooks/useNotes';
import { NoteType } from '../../../types/deliverable.types';
import type { NoteItem } from '../../../types/deliverable.types';
import { NoteModal } from './NoteModal';
interface EntregavelNotesTabProps {
serviceOrderId: string;
readOnly?: boolean;
}
const NOTE_TYPE_OPTIONS = [
{ value: '', label: 'Todas' },
{ value: NoteType.OBSERVACAO, label: 'Observação' },
{ value: NoteType.RISCO, label: 'Risco' },
{ value: NoteType.IMPEDIMENTO, label: 'Impedimento' },
{ value: NoteType.DECISAO, label: 'Decisão' },
];
const NOTE_TYPE_CONFIG: Record<
NoteType,
{ icon: typeof Eye; label: string; variant: 'info' | 'warning' | 'danger' | 'purple' }
> = {
[NoteType.OBSERVACAO]: { icon: Eye, label: 'Observação', variant: 'info' },
[NoteType.RISCO]: { icon: AlertTriangle, label: 'Risco', variant: 'warning' },
[NoteType.IMPEDIMENTO]: { icon: OctagonX, label: 'Impedimento', variant: 'danger' },
[NoteType.DECISAO]: { icon: Scale, label: 'Decisão', variant: 'purple' },
};
function formatDateTime(dateStr: string): string {
return new Date(dateStr).toLocaleString('pt-BR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
export function EntregavelNotesTab({ serviceOrderId, readOnly = false }: EntregavelNotesTabProps) {
const [typeFilter, setTypeFilter] = useState<NoteType | undefined>();
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingNote, setEditingNote] = useState<NoteItem | undefined>();
const { data: notes = [], isLoading } = useNotes(serviceOrderId, {
type: typeFilter,
});
const updateNote = useUpdateNote();
const { showToast } = useToast();
function handleAdd() {
setEditingNote(undefined);
setIsModalOpen(true);
}
function handleEdit(note: NoteItem) {
setEditingNote(note);
setIsModalOpen(true);
}
function handleCloseModal() {
setIsModalOpen(false);
setEditingNote(undefined);
}
function handleToggleRelevance(note: NoteItem) {
updateNote.mutate(
{
osId: serviceOrderId,
noteId: note.id,
data: { isRelevant: !note.isRelevant },
},
{
onSuccess: () => {
showToast(note.isRelevant ? 'Relevância removida' : 'Marcada como relevante', 'success');
},
onError: () => {
showToast('Erro ao atualizar relevância', 'error');
},
},
);
}
function handleTypeChange(value: string) {
setTypeFilter(value ? (value as NoteType) : undefined);
}
if (isLoading) {
return (
<div className="flex items-center justify-center py-16">
<span className="h-8 w-8 animate-spin rounded-full border-4 border-isis-blue border-t-transparent" />
</div>
);
}
if (notes.length === 0 && !typeFilter) {
return (
<>
<div className="flex flex-col items-center justify-center gap-3 rounded-lg border border-border bg-card py-16 text-text-secondary">
<MessageSquare className="h-10 w-10" />
<p className="text-lg font-medium">Nenhuma anotação registrada</p>
{!readOnly && (
<Button
variant="primary"
size="sm"
icon={<Plus className="h-4 w-4" />}
onClick={handleAdd}
>
Adicionar Anotação
</Button>
)}
</div>
<NoteModal
isOpen={isModalOpen}
onClose={handleCloseModal}
serviceOrderId={serviceOrderId}
/>
</>
);
}
return (
<>
{/* Header com filtro e botão */}
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<Select
options={NOTE_TYPE_OPTIONS}
value={typeFilter ?? ''}
onChange={(e) => handleTypeChange(e.target.value)}
className="w-48"
/>
</div>
{!readOnly && (
<Button
variant="primary"
size="sm"
icon={<Plus className="h-4 w-4" />}
onClick={handleAdd}
>
Adicionar Anotação
</Button>
)}
</div>
{/* Lista de anotações */}
{notes.length === 0 ? (
<div className="flex flex-col items-center justify-center gap-3 rounded-lg border border-border bg-card py-16 text-text-secondary">
<MessageSquare className="h-10 w-10" />
<p className="text-body font-medium">Nenhuma anotação deste tipo</p>
</div>
) : (
<div className="divide-y divide-border rounded-lg border border-border bg-card">
{notes.map((note) => {
const config = NOTE_TYPE_CONFIG[note.type];
const Icon = config.icon;
return (
<div key={note.id} className="px-4 py-3">
<div className="flex items-start gap-3">
{/* Ícone do tipo */}
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-surface-subtle text-muted">
<Icon className="h-4 w-4" />
</div>
{/* Conteúdo */}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="font-medium text-primary">{note.title}</span>
<Badge variant={config.variant} size="sm">
{config.label}
</Badge>
</div>
{note.description && (
<p className="mt-0.5 line-clamp-2 text-small text-text-secondary">
{note.description}
</p>
)}
<div className="mt-1 flex items-center gap-3 text-[11px] text-text-muted">
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{formatDateTime(note.createdAt)}
</span>
{note.createdBy && <span>por {note.createdBy}</span>}
</div>
</div>
{/* Ações */}
{!readOnly && (
<div className="flex flex-shrink-0 items-center gap-1">
<button
type="button"
onClick={() => handleToggleRelevance(note)}
className="rounded p-1.5 hover:bg-bg"
title={note.isRelevant ? 'Remover relevância' : 'Marcar como relevante'}
>
<Star
className={`h-4 w-4 ${note.isRelevant ? 'fill-warning text-warning' : 'text-text-muted'}`}
/>
</button>
<Button
variant="ghost"
size="icon"
onClick={() => handleEdit(note)}
title="Editar"
>
<Pencil className="h-4 w-4" />
</Button>
</div>
)}
</div>
</div>
);
})}
</div>
)}
<NoteModal
isOpen={isModalOpen}
onClose={handleCloseModal}
serviceOrderId={serviceOrderId}
note={editingNote}
/>
</>
);
}