Commit inicial - upload de todos os arquivos da pasta
This commit is contained in:
244
src/pages/entregaveis/components/EntregavelNotesTab.tsx
Normal file
244
src/pages/entregaveis/components/EntregavelNotesTab.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user