fix(erp): enable erp pages and menu items
This commit is contained in:
@@ -1,15 +1,251 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { SolutionGuard } from '@/components/auth/SolutionGuard';
|
||||
import { PageHeader, DataTable, Card, Badge } from '@/components/ui';
|
||||
import {
|
||||
PlusIcon,
|
||||
MagnifyingGlassIcon,
|
||||
DocumentTextIcon,
|
||||
PencilSquareIcon,
|
||||
TrashIcon,
|
||||
ArrowPathIcon,
|
||||
EyeIcon,
|
||||
ClockIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { docApi, Document } from '@/lib/api-docs';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import DocumentEditor from '@/components/documentos/DocumentEditor';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
|
||||
export default function DocumentosPage() {
|
||||
const [documents, setDocuments] = useState<Document[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [currentDoc, setCurrentDoc] = useState<Partial<Document> | null>(null);
|
||||
|
||||
// Pagination
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const itemsPerPage = 8;
|
||||
|
||||
useEffect(() => {
|
||||
fetchDocuments();
|
||||
}, []);
|
||||
|
||||
const fetchDocuments = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await docApi.getDocuments();
|
||||
setDocuments(data || []);
|
||||
} catch (error) {
|
||||
toast.error('Erro ao carregar documentos');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
const newDoc = await docApi.createDocument({
|
||||
title: 'Novo Documento',
|
||||
content: '{"type":"doc","content":[{"type":"paragraph"}]}',
|
||||
status: 'published',
|
||||
parent_id: null
|
||||
});
|
||||
setCurrentDoc(newDoc);
|
||||
setIsEditing(true);
|
||||
fetchDocuments();
|
||||
} catch (error) {
|
||||
toast.error('Erro ao iniciar novo documento');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (doc: Document) => {
|
||||
setCurrentDoc(doc);
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const handleSave = async (docData: Partial<Document>) => {
|
||||
try {
|
||||
if (docData.id) {
|
||||
await docApi.updateDocument(docData.id, docData);
|
||||
// toast.success('Documento atualizado!'); // Auto-save já acontece
|
||||
} else {
|
||||
await docApi.createDocument(docData);
|
||||
toast.success('Documento criado!');
|
||||
}
|
||||
setIsEditing(false);
|
||||
fetchDocuments();
|
||||
} catch (error) {
|
||||
toast.error('Erro ao salvar documento');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('Tem certeza que deseja excluir este documento e todas as suas subpáginas?')) return;
|
||||
try {
|
||||
await docApi.deleteDocument(id);
|
||||
toast.success('Documento excluído!');
|
||||
fetchDocuments();
|
||||
} catch (error) {
|
||||
toast.error('Erro ao excluir documento');
|
||||
}
|
||||
};
|
||||
|
||||
const filteredDocuments = useMemo(() => {
|
||||
return documents.filter(doc =>
|
||||
(doc.title || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(doc.content || '').toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
}, [documents, searchTerm]);
|
||||
|
||||
const paginatedDocuments = useMemo(() => {
|
||||
const start = (currentPage - 1) * itemsPerPage;
|
||||
return filteredDocuments.slice(start, start + itemsPerPage);
|
||||
}, [filteredDocuments, currentPage]);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: 'Documento',
|
||||
accessor: (doc: Document) => (
|
||||
<div className="flex items-center gap-3 py-1">
|
||||
<div className="p-2.5 bg-zinc-50 dark:bg-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-700 shadow-sm">
|
||||
<DocumentTextIcon className="w-5 h-5 text-zinc-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-zinc-900 dark:text-white group-hover:text-brand-500 transition-colors uppercase tracking-tight text-sm">
|
||||
{doc.title || 'Sem título'}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<Badge variant="info" className="text-[8px] px-1.5 font-black">v{doc.version || 1}</Badge>
|
||||
<span className="text-[10px] text-zinc-400 font-medium">#{doc.id.substring(0, 8)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Última Modificação',
|
||||
accessor: (doc: Document) => (
|
||||
<div className="flex items-center gap-3">
|
||||
<ClockIcon className="w-4 h-4 text-zinc-300" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold text-zinc-600 dark:text-zinc-400">
|
||||
{format(parseISO(doc.updated_at), "dd 'de' MMM", { locale: ptBR })}
|
||||
</span>
|
||||
<span className="text-[9px] text-zinc-400 uppercase font-black tracking-tighter">
|
||||
às {format(parseISO(doc.updated_at), "HH:mm")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Ações',
|
||||
align: 'right' as const,
|
||||
accessor: (doc: Document) => (
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<button
|
||||
onClick={() => handleEdit(doc)}
|
||||
className="flex items-center gap-2 px-4 py-2 text-xs font-black uppercase tracking-widest text-zinc-600 dark:text-zinc-400 hover:text-brand-500 hover:bg-brand-50 dark:hover:bg-brand-500/10 rounded-xl transition-all"
|
||||
>
|
||||
<PencilSquareIcon className="w-4 h-4" />
|
||||
Abrir
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(doc.id)}
|
||||
className="p-2 text-zinc-300 hover:text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-500/10 rounded-xl transition-all"
|
||||
title="Excluir"
|
||||
>
|
||||
<TrashIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<SolutionGuard requiredSolution="documentos">
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">Documentos</h1>
|
||||
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 p-8 text-center">
|
||||
<p className="text-gray-500">Gestão Eletrônica de Documentos (GED) em breve</p>
|
||||
<div className="p-6 max-w-[1600px] mx-auto space-y-8 animate-in fade-in duration-700">
|
||||
<PageHeader
|
||||
title="Wiki & Base de Conhecimento"
|
||||
description="Organize processos, manuais e documentação técnica da agência."
|
||||
primaryAction={{
|
||||
label: "Criar Novo",
|
||||
icon: <PlusIcon className="w-5 h-5" />,
|
||||
onClick: handleCreate
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-4 items-center justify-between bg-white dark:bg-zinc-900/50 p-4 rounded-[28px] border border-zinc-200 dark:border-zinc-800 shadow-sm">
|
||||
<div className="w-full md:w-96 relative">
|
||||
<MagnifyingGlassIcon className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-zinc-300" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Pesquisar wiki..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-100 dark:border-zinc-800 rounded-2xl pl-12 pr-4 py-3 text-sm outline-none focus:ring-2 ring-brand-500/20 transition-all font-semibold placeholder:text-zinc-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={fetchDocuments}
|
||||
className="p-3 text-zinc-400 hover:text-zinc-900 dark:hover:text-white transition-colors"
|
||||
>
|
||||
<ArrowPathIcon className={`w-5 h-5 ${loading ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
<div className="h-6 w-px bg-zinc-200 dark:border-zinc-800" />
|
||||
<div className="flex items-center gap-2 px-5 py-2.5 bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 rounded-2xl text-[10px] font-black uppercase tracking-widest shadow-lg shadow-zinc-200 dark:shadow-none">
|
||||
{filteredDocuments.length} Documentos
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card noPadding allowOverflow className="border-none shadow-2xl shadow-black/5 overflow-hidden rounded-[32px]">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={paginatedDocuments}
|
||||
isLoading={loading}
|
||||
/>
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="p-6 border-t border-zinc-50 dark:border-zinc-800 flex items-center justify-between bg-zinc-50/50 dark:bg-zinc-900/50">
|
||||
<p className="text-[10px] font-black text-zinc-400 uppercase tracking-widest">
|
||||
{filteredDocuments.length} itens no total
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
disabled={currentPage === 1}
|
||||
onClick={() => setCurrentPage(p => p - 1)}
|
||||
className="px-6 py-2.5 text-[10px] font-black uppercase tracking-widest bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl disabled:opacity-30 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-all shadow-sm"
|
||||
>
|
||||
Anterior
|
||||
</button>
|
||||
<button
|
||||
disabled={currentPage * itemsPerPage >= filteredDocuments.length}
|
||||
onClick={() => setCurrentPage(p => p + 1)}
|
||||
className="px-6 py-2.5 text-[10px] font-black uppercase tracking-widest bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 rounded-xl disabled:opacity-30 hover:bg-black dark:hover:bg-white transition-all shadow-lg active:scale-95"
|
||||
>
|
||||
Próximo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{isEditing && (
|
||||
<DocumentEditor
|
||||
initialDocument={currentDoc}
|
||||
onSave={handleSave}
|
||||
onCancel={() => {
|
||||
setIsEditing(false);
|
||||
fetchDocuments();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</SolutionGuard>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user