Files
advx/src/components/atendimentos/AtendimentoForm.tsx
T
2025-04-10 23:07:21 -03:00

445 lines
15 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react';
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, FormControl, InputLabel, Select, MenuItem, Alert } from '@mui/material';
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Link from '@tiptap/extension-link';
import { TipoAtendimento } from '@prisma/client';
import api from '../../services/api'; // Adicionar esta importação
import { XMarkIcon, PaperClipIcon } from '@heroicons/react/24/outline'; // Alterado para v2
import { useDropzone } from 'react-dropzone';
import { uploadFile } from '../../services/upload';
// Implementar debounce manualmente ao invés de usar lodash
function debounce(fn: Function, ms = 300) {
let timeoutId: ReturnType<typeof setTimeout>;
return function (this: any, ...args: any[]) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), ms);
};
}
interface AtendimentoFormProps {
open: boolean;
onClose: () => void;
onSave: (data: any) => Promise<void>;
clienteId?: string;
processoId?: string;
initialData?: {
titulo?: string;
agendamentoId?: string;
tipo?: string;
atendimentoInicialId?: string;
};
}
interface AnexoTemp {
id?: string;
file: File;
nome: string;
progresso: number;
erro?: string;
}
export function AtendimentoForm({ open, onClose, onSave, clienteId, processoId, initialData }: AtendimentoFormProps) {
const [searchTerm, setSearchTerm] = useState('');
const [searchResults, setSearchResults] = useState<any[]>([]);
const [selectedClient, setSelectedClient] = useState<any>(null);
const [atendimentoInicial, setAtendimentoInicial] = useState<any>(null);
const [atendimentosIniciais, setAtendimentosIniciais] = useState<any[]>([]);
const [formData, setFormData] = useState({
titulo: '',
tipo: TipoAtendimento.INICIAL,
conteudo: '',
clienteId: clienteId || '',
processoId: processoId || '',
atendimentoInicialId: '',
});
const [anexos, setAnexos] = useState<AnexoTemp[]>([]);
const [uploading, setUploading] = useState(false);
// Resetar form ao abrir
useEffect(() => {
if (open) {
setFormData({
titulo: '',
tipo: TipoAtendimento.INICIAL,
conteudo: '',
clienteId: clienteId || '',
processoId: processoId || '',
atendimentoInicialId: '',
});
}
}, [open, clienteId, processoId]);
useEffect(() => {
if (open && initialData) {
setFormData(prev => ({
...prev,
titulo: initialData.titulo || prev.titulo,
clienteId: clienteId || prev.clienteId,
processoId: processoId || prev.processoId,
tipo: initialData.tipo || prev.tipo,
atendimentoInicialId: initialData.atendimentoInicialId || prev.atendimentoInicialId
}));
}
}, [open, initialData, clienteId, processoId]);
const [clientes, setClientes] = useState<any[]>([]);
const [error, setError] = useState('');
const [isSaving, setIsSaving] = useState(false); // Adicionar estado para prevenir múltiplos submits
// Buscar lista de clientes
useEffect(() => {
const fetchClientes = async () => {
try {
const response = await api.get('/clientes');
setClientes(response.data);
} catch (err) {
console.error('Erro ao carregar clientes:', err);
}
};
fetchClientes();
}, []);
// Busca de clientes
const searchClients = useCallback(
debounce(async (term: string) => {
if (term.length < 3) return;
try {
const response = await api.get(`/clientes/search?q=${term}`);
setSearchResults(response.data);
} catch (error) {
console.error('Erro ao buscar clientes:', error);
}
}, 300),
[]
);
// Carregar atendimentos iniciais quando selecionar cliente
useEffect(() => {
if (formData.clienteId && formData.tipo !== TipoAtendimento.INICIAL) {
api.get(`/atendimentos/iniciais/${formData.clienteId}`)
.then(response => setAtendimentosIniciais(response.data))
.catch(error => console.error('Erro ao carregar atendimentos:', error));
}
}, [formData.clienteId, formData.tipo]);
const editor = useEditor({
extensions: [
StarterKit,
Link.configure({
openOnClick: false,
}),
],
content: '',
onUpdate: ({ editor }) => {
setFormData(prev => ({ ...prev, conteudo: editor.getHTML() }));
},
});
const onDrop = useCallback((acceptedFiles: File[]) => {
const novosAnexos = acceptedFiles.map(file => ({
file,
nome: file.name,
progresso: 0
}));
setAnexos(prev => [...prev, ...novosAnexos]);
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
maxSize: 10485760 // 10MB
});
const handleRemoveAnexo = (index: number) => {
setAnexos(prev => prev.filter((_, i) => i !== index));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (isSaving) return;
try {
setIsSaving(true);
setError('');
if (!formData.clienteId) {
setError('Selecione um cliente');
setIsSaving(false);
return;
}
// Buscar dados do cliente caso não esteja selecionado
if (!selectedClient) {
try {
const response = await api.get(`/clientes/${formData.clienteId}`);
setSelectedClient(response.data);
} catch (error) {
console.error('Erro ao buscar cliente:', error);
setError('Erro ao buscar dados do cliente');
setIsSaving(false);
return;
}
}
// Criar atendimento primeiro para ter o protocolo
const response = await api.post('/atendimentos', {
...formData,
agendamentoId: initialData?.agendamentoId,
});
const atendimentoId = response.data.id;
const protocolo = response.data.protocolo;
// Upload dos anexos com protocolo
if (anexos.length > 0) {
const anexosPromises = anexos.map(async (anexo) => {
try {
const clienteNome = selectedClient?.nome || 'cliente';
return await uploadFile(
anexo.file,
clienteNome,
'atendimento',
protocolo
);
} catch (error) {
console.error('Erro no upload:', error);
return null;
}
});
const anexosUpload = await Promise.all(anexosPromises);
const anexosSalvos = anexosUpload.filter(Boolean);
if (anexosSalvos.length > 0) {
await api.patch(`/atendimentos/${atendimentoId}`, {
anexos: anexosSalvos
});
}
}
// Limpar form e fechar modal
setFormData({
titulo: '',
tipo: TipoAtendimento.INICIAL,
conteudo: '',
clienteId: '',
processoId: '',
atendimentoInicialId: '',
});
await onSave(response.data);
onClose();
} catch (err) {
console.error('Erro ao salvar atendimento:', err);
setError(err.response?.data?.error || 'Erro ao criar atendimento');
} finally {
setIsSaving(false);
}
};
return (
<div className={`fixed inset-0 bg-gray-500 bg-opacity-75 ${open ? 'flex' : 'hidden'} z-50`}>
<div className="relative bg-white rounded-lg max-w-4xl w-full m-auto p-6 overflow-y-auto max-h-[90vh]">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-gray-900">Novo Atendimento</h2>
<button onClick={onClose} className="text-gray-400 hover:text-gray-500">
<XMarkIcon className="h-6 w-6" /> {/* Alterado para XMarkIcon */}
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Busca de Cliente */}
<div className="relative">
<label className="block text-sm font-medium text-gray-700 mb-1">
Cliente
</label>
<input
type="text"
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
searchClients(e.target.value);
}}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Buscar cliente..."
/>
{searchResults.length > 0 && (
<div className="absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg">
{searchResults.map(client => (
<div
key={client.id}
className="px-4 py-2 hover:bg-gray-100 cursor-pointer"
onClick={() => {
setSelectedClient(client);
setFormData(prev => ({ ...prev, clienteId: client.id }));
setSearchTerm(client.nome);
setSearchResults([]);
}}
>
<div className="font-medium">{client.nome}</div>
<div className="text-sm text-gray-500">{client.cpfCnpj}</div>
</div>
))}
</div>
)}
</div>
{/* Tipo de Atendimento */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Tipo de Atendimento
</label>
<select
value={formData.tipo}
onChange={(e) => setFormData({ ...formData, tipo: e.target.value as TipoAtendimento })}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
{Object.values(TipoAtendimento).map((tipo) => (
<option key={tipo} value={tipo}>
{tipo.replace('_', ' ')}
</option>
))}
</select>
</div>
{/* Atendimento Inicial (apenas para tipos diferentes de INICIAL) */}
{formData.tipo !== TipoAtendimento.INICIAL && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Atendimento Inicial
</label>
<select
value={formData.atendimentoInicialId}
onChange={(e) => setFormData({ ...formData, atendimentoInicialId: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
required={formData.tipo !== TipoAtendimento.INICIAL}
>
<option value="">Selecione um atendimento inicial</option>
{atendimentosIniciais.map((atend) => (
<option key={atend.id} value={atend.id}>
{atend.protocolo} - {atend.titulo}
</option>
))}
</select>
</div>
)}
<TextField
fullWidth
label="Título do Atendimento"
value={formData.titulo}
onChange={(e) => setFormData({ ...formData, titulo: e.target.value })}
required
/>
{/* Editor de rich text */}
<div className="prose max-w-none">
<label className="block text-sm font-medium text-gray-700 mb-2">
Conteúdo do Atendimento
</label>
<div className="min-h-[200px] border rounded-lg">
<div className="border-b p-2 space-x-2">
<button
type="button"
onClick={() => editor?.chain().focus().toggleBold().run()}
className={`px-2 py-1 rounded ${editor?.isActive('bold') ? 'bg-gray-200' : 'hover:bg-gray-100'}`}
>
Negrito
</button>
<button
type="button"
onClick={() => editor?.chain().focus().toggleItalic().run()}
className={`px-2 py-1 rounded ${editor?.isActive('italic') ? 'bg-gray-200' : 'hover:bg-gray-100'}`}
>
Itálico
</button>
</div>
<div className="p-4">
<EditorContent editor={editor} />
</div>
</div>
</div>
{/* Upload de anexos */}
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Anexos
</label>
<div
{...getRootProps()}
className={`mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-dashed rounded-md ${
isDragActive ? 'border-blue-400 bg-blue-50' : 'border-gray-300'
}`}
>
<div className="space-y-1 text-center">
<PaperClipIcon className="mx-auto h-12 w-12 text-gray-400" />
<div className="flex text-sm text-gray-600">
<input {...getInputProps()} />
<p className="pl-1">Arraste arquivos aqui ou clique para selecionar</p>
</div>
<p className="text-xs text-gray-500">Máximo 10MB por arquivo</p>
</div>
</div>
</div>
{/* Lista de Anexos */}
{anexos.length > 0 && (
<div className="mt-4">
<h4 className="text-sm font-medium text-gray-700">Arquivos para upload</h4>
<ul className="mt-2 divide-y divide-gray-200">
{anexos.map((anexo, index) => (
<li key={index} className="py-3 flex justify-between items-center">
<div className="flex items-center">
<PaperClipIcon className="h-5 w-5 text-gray-400 mr-2" />
<span className="text-sm text-gray-900">{anexo.nome}</span>
</div>
<div className="flex items-center">
{anexo.erro && (
<span className="text-xs text-red-500 mr-2">{anexo.erro}</span>
)}
<button
type="button"
onClick={() => handleRemoveAnexo(index)}
className="text-red-500 hover:text-red-700"
>
<XMarkIcon className="h-5 w-5" />
</button>
</div>
</li>
))}
</ul>
</div>
)}
{error && (
<div className="p-4 bg-red-50 border-l-4 border-red-500 text-red-700">
{error}
</div>
)}
{/* Botões de ação dentro do form */}
<div className="flex justify-end space-x-4 mt-6 pt-4 border-t">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
>
Cancelar
</button>
<button
type="submit"
disabled={isSaving}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSaving ? 'Salvando...' : 'Salvar'}
</button>
</div>
</form>
</div>
</div>
);
}