245 lines
7.8 KiB
TypeScript
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}
|
|
/>
|
|
</>
|
|
);
|
|
}
|