forlogic-core 1.7.4 → 1.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/README.md CHANGED
@@ -1634,6 +1634,577 @@ function ProcessLayout() {
1634
1634
 
1635
1635
  ---
1636
1636
 
1637
+ ### **5️⃣ Ações Customizadas na Action Bar**
1638
+
1639
+ Adicione botões customizados ao lado do botão "Novo" na barra de ações.
1640
+
1641
+ #### **Casos de Uso Comuns:**
1642
+ - 📥 Importar dados
1643
+ - 📤 Exportar relatório
1644
+ - 🔄 Sincronizar com API externa
1645
+ - 📊 Abrir dashboard/visualizações
1646
+
1647
+ #### **Exemplo Básico:**
1648
+
1649
+ ```typescript
1650
+ import { Download, Upload } from 'lucide-react';
1651
+
1652
+ const config = useMemo(() => ({
1653
+ entityName: "Processo",
1654
+ entityNamePlural: "Processos",
1655
+ columns: processColumns,
1656
+ formSections,
1657
+
1658
+ // ✅ Ações customizadas
1659
+ customActions: [
1660
+ {
1661
+ label: 'Importar',
1662
+ icon: Upload,
1663
+ variant: 'outline',
1664
+ action: () => {
1665
+ // Abrir dialog de importação
1666
+ setImportDialogOpen(true);
1667
+ }
1668
+ },
1669
+ {
1670
+ label: 'Exportar',
1671
+ icon: Download,
1672
+ variant: 'outline',
1673
+ action: async () => {
1674
+ // Exportar dados filtrados/selecionados
1675
+ const data = manager.entities;
1676
+ await exportToCSV(data);
1677
+ }
1678
+ }
1679
+ ]
1680
+ }), [manager.entities]);
1681
+ ```
1682
+
1683
+ #### **Propriedades:**
1684
+
1685
+ | Propriedade | Tipo | Obrigatório | Descrição |
1686
+ |------------|------|-------------|-----------|
1687
+ | `label` | `string` | ✅ | Texto do botão |
1688
+ | `icon` | `ComponentType` | ❌ | Ícone do lucide-react |
1689
+ | `variant` | `'default' \| 'destructive' \| 'outline' \| 'ghost'` | ❌ | Estilo do botão (default: `'outline'`) |
1690
+ | `action` | `() => void` | ✅ | Função executada ao clicar |
1691
+
1692
+ #### **Exemplo com Estado:**
1693
+
1694
+ ```typescript
1695
+ export default function ProcessesPage() {
1696
+ const manager = useProcesses();
1697
+ const [importDialogOpen, setImportDialogOpen] = useState(false);
1698
+ const [syncLoading, setSyncLoading] = useState(false);
1699
+
1700
+ const handleSyncWithAPI = async () => {
1701
+ setSyncLoading(true);
1702
+ try {
1703
+ await syncProcessesWithExternalAPI();
1704
+ manager.refetch(); // Recarregar dados
1705
+ toast({ title: 'Sincronização concluída' });
1706
+ } catch (error) {
1707
+ toast({ title: 'Erro na sincronização', variant: 'destructive' });
1708
+ } finally {
1709
+ setSyncLoading(false);
1710
+ }
1711
+ };
1712
+
1713
+ const config = useMemo(() => ({
1714
+ entityName: "Processo",
1715
+ entityNamePlural: "Processos",
1716
+ columns: processColumns,
1717
+ formSections,
1718
+ customActions: [
1719
+ {
1720
+ label: syncLoading ? 'Sincronizando...' : 'Sincronizar',
1721
+ icon: RefreshCw,
1722
+ variant: 'outline',
1723
+ action: handleSyncWithAPI
1724
+ }
1725
+ ]
1726
+ }), [syncLoading, handleSyncWithAPI]);
1727
+
1728
+ return (
1729
+ <>
1730
+ <CrudPage />
1731
+ <ImportDialog open={importDialogOpen} onOpenChange={setImportDialogOpen} />
1732
+ </>
1733
+ );
1734
+ }
1735
+ ```
1736
+
1737
+ #### **Integração com Bulk Actions:**
1738
+
1739
+ ```typescript
1740
+ customActions: [
1741
+ {
1742
+ label: 'Exportar Selecionados',
1743
+ icon: Download,
1744
+ variant: 'outline',
1745
+ action: () => {
1746
+ if (manager.selectedIds.length === 0) {
1747
+ toast({ title: 'Selecione pelo menos um item', variant: 'destructive' });
1748
+ return;
1749
+ }
1750
+
1751
+ const selectedItems = manager.entities.filter(
1752
+ item => manager.selectedIds.includes(item.id)
1753
+ );
1754
+ exportToCSV(selectedItems);
1755
+ }
1756
+ }
1757
+ ]
1758
+ ```
1759
+
1760
+ **Boas Práticas:**
1761
+ - ✅ Use `useMemo` para evitar recriação em cada render
1762
+ - ✅ Use `variant="outline"` para ações secundárias
1763
+ - ✅ Use ícones do `lucide-react` para consistência visual
1764
+ - ✅ Mostre feedback (loading, toasts) para ações assíncronas
1765
+ - ❌ Evite muitas ações (máximo 3-4 para não poluir a UI)
1766
+
1767
+ ---
1768
+
1769
+ ### **6️⃣ Views Customizadas (substituir Tabela/Cards)**
1770
+
1771
+ Substitua completamente a tabela padrão por qualquer componente customizado.
1772
+
1773
+ #### **Casos de Uso Comuns:**
1774
+ - 📅 Calendário de eventos
1775
+ - 🗺️ Mapa de localizações
1776
+ - 🎨 Galeria de imagens
1777
+ - 📊 Dashboard com widgets
1778
+ - 🌳 Árvore hierárquica (processos/subprocessos)
1779
+ - 📋 Kanban board
1780
+
1781
+ #### **Exemplo: Kanban Board**
1782
+
1783
+ ```typescript
1784
+ import { useMemo } from 'react';
1785
+ import { DragDropContext, Droppable, Draggable } from '@dnd-kit/core';
1786
+
1787
+ const KanbanColumn = ({ title, tasks, status }) => (
1788
+ <div className="flex flex-col gap-2 p-4 bg-muted rounded-lg min-w-[300px]">
1789
+ <h3 className="font-semibold text-sm text-muted-foreground uppercase">{title}</h3>
1790
+ <Droppable droppableId={status}>
1791
+ {tasks.map((task, index) => (
1792
+ <Draggable key={task.id} draggableId={task.id} index={index}>
1793
+ <div className="p-3 bg-background border rounded shadow-sm cursor-move">
1794
+ <p className="font-medium">{task.title}</p>
1795
+ <p className="text-xs text-muted-foreground mt-1">{task.description}</p>
1796
+ </div>
1797
+ </Draggable>
1798
+ ))}
1799
+ </Droppable>
1800
+ </div>
1801
+ );
1802
+
1803
+ export default function TasksPage() {
1804
+ const manager = useTasks();
1805
+
1806
+ const config = useMemo(() => ({
1807
+ entityName: "Tarefa",
1808
+ entityNamePlural: "Tarefas",
1809
+ columns: taskColumns,
1810
+ formSections,
1811
+
1812
+ // ✅ View customizada (Kanban)
1813
+ customListView: (tasks: Task[], manager: CrudManager<Task>) => {
1814
+ const columns = {
1815
+ todo: tasks.filter(t => t.status === 'todo'),
1816
+ in_progress: tasks.filter(t => t.status === 'in_progress'),
1817
+ done: tasks.filter(t => t.status === 'done')
1818
+ };
1819
+
1820
+ return (
1821
+ <DragDropContext onDragEnd={handleDragEnd}>
1822
+ <div className="flex gap-4 overflow-x-auto">
1823
+ <KanbanColumn title="A Fazer" tasks={columns.todo} status="todo" />
1824
+ <KanbanColumn title="Em Progresso" tasks={columns.in_progress} status="in_progress" />
1825
+ <KanbanColumn title="Concluído" tasks={columns.done} status="done" />
1826
+ </div>
1827
+ </DragDropContext>
1828
+ );
1829
+ }
1830
+ }), [manager.entities]);
1831
+
1832
+ return <CrudPage />;
1833
+ }
1834
+ ```
1835
+
1836
+ #### **Exemplo: Calendário de Eventos**
1837
+
1838
+ ```typescript
1839
+ import { Calendar } from '@/components/ui/calendar';
1840
+ import { Badge } from 'forlogic-core';
1841
+ import { format } from 'date-fns';
1842
+ import { ptBR } from 'date-fns/locale';
1843
+
1844
+ const config = useMemo(() => ({
1845
+ entityName: "Evento",
1846
+ entityNamePlural: "Eventos",
1847
+ columns: eventColumns,
1848
+ formSections,
1849
+
1850
+ customListView: (events: Event[]) => {
1851
+ const [selectedDate, setSelectedDate] = useState<Date>(new Date());
1852
+
1853
+ // Agrupar eventos por data
1854
+ const eventsByDate = events.reduce((acc, event) => {
1855
+ const dateKey = format(new Date(event.date), 'yyyy-MM-dd');
1856
+ if (!acc[dateKey]) acc[dateKey] = [];
1857
+ acc[dateKey].push(event);
1858
+ return acc;
1859
+ }, {} as Record<string, Event[]>);
1860
+
1861
+ const selectedDateKey = format(selectedDate, 'yyyy-MM-dd');
1862
+ const eventsForSelectedDate = eventsByDate[selectedDateKey] || [];
1863
+
1864
+ return (
1865
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
1866
+ {/* Calendário */}
1867
+ <div>
1868
+ <Calendar
1869
+ mode="single"
1870
+ selected={selectedDate}
1871
+ onSelect={(date) => date && setSelectedDate(date)}
1872
+ locale={ptBR}
1873
+ modifiers={{
1874
+ hasEvents: Object.keys(eventsByDate).map(d => new Date(d))
1875
+ }}
1876
+ modifiersStyles={{
1877
+ hasEvents: { fontWeight: 'bold', textDecoration: 'underline' }
1878
+ }}
1879
+ />
1880
+ </div>
1881
+
1882
+ {/* Lista de eventos do dia selecionado */}
1883
+ <div>
1884
+ <h3 className="font-semibold mb-4">
1885
+ Eventos de {format(selectedDate, "dd 'de' MMMM", { locale: ptBR })}
1886
+ </h3>
1887
+
1888
+ {eventsForSelectedDate.length === 0 ? (
1889
+ <p className="text-muted-foreground">Nenhum evento neste dia</p>
1890
+ ) : (
1891
+ <div className="space-y-3">
1892
+ {eventsForSelectedDate.map(event => (
1893
+ <div key={event.id} className="p-4 border rounded-lg">
1894
+ <div className="flex items-start justify-between">
1895
+ <div>
1896
+ <p className="font-medium">{event.title}</p>
1897
+ <p className="text-sm text-muted-foreground">{event.time}</p>
1898
+ </div>
1899
+ <Badge>{event.type}</Badge>
1900
+ </div>
1901
+ <p className="text-sm mt-2">{event.description}</p>
1902
+ </div>
1903
+ ))}
1904
+ </div>
1905
+ )}
1906
+ </div>
1907
+ </div>
1908
+ );
1909
+ }
1910
+ }), [manager.entities]);
1911
+ ```
1912
+
1913
+ #### **Exemplo: Galeria de Imagens**
1914
+
1915
+ ```typescript
1916
+ import { Dialog, DialogContent, DialogTrigger } from 'forlogic-core';
1917
+
1918
+ const config = useMemo(() => ({
1919
+ entityName: "Foto",
1920
+ entityNamePlural: "Fotos",
1921
+ columns: photoColumns,
1922
+ formSections,
1923
+
1924
+ customListView: (photos: Photo[]) => (
1925
+ <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
1926
+ {photos.map(photo => (
1927
+ <Dialog key={photo.id}>
1928
+ <DialogTrigger asChild>
1929
+ <div className="aspect-square relative rounded-lg overflow-hidden cursor-pointer group">
1930
+ <img
1931
+ src={photo.thumbnail_url}
1932
+ alt={photo.title}
1933
+ className="w-full h-full object-cover group-hover:scale-105 transition-transform"
1934
+ />
1935
+ <div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
1936
+ <p className="text-white font-medium">{photo.title}</p>
1937
+ </div>
1938
+ </div>
1939
+ </DialogTrigger>
1940
+
1941
+ <DialogContent className="max-w-4xl">
1942
+ <img src={photo.full_url} alt={photo.title} className="w-full" />
1943
+ <div className="mt-4">
1944
+ <h3 className="font-semibold text-lg">{photo.title}</h3>
1945
+ <p className="text-muted-foreground">{photo.description}</p>
1946
+ </div>
1947
+ </DialogContent>
1948
+ </Dialog>
1949
+ ))}
1950
+ </div>
1951
+ )
1952
+ }), [manager.entities]);
1953
+ ```
1954
+
1955
+ #### **Propriedades Recebidas:**
1956
+
1957
+ ```typescript
1958
+ customListView: (items: T[], manager?: CrudManager<T>) => React.ReactNode
1959
+ ```
1960
+
1961
+ | Parâmetro | Tipo | Descrição |
1962
+ |-----------|------|-----------|
1963
+ | `items` | `T[]` | Entidades filtradas/paginadas do manager |
1964
+ | `manager` | `CrudManager<T>` | Manager completo com métodos de CRUD |
1965
+
1966
+ **Acesso ao Manager:**
1967
+ ```typescript
1968
+ customListView: (items, manager) => (
1969
+ <div>
1970
+ {/* Acessar estado de loading */}
1971
+ {manager.isLoading && <Spinner />}
1972
+
1973
+ {/* Acessar busca atual */}
1974
+ <p>Busca: {manager.searchTerm}</p>
1975
+
1976
+ {/* Refetch manual */}
1977
+ <Button onClick={() => manager.refetch()}>Recarregar</Button>
1978
+
1979
+ {/* Criar nova entidade */}
1980
+ <Button onClick={() => manager.createEntity(newData)}>Criar</Button>
1981
+ </div>
1982
+ )
1983
+ ```
1984
+
1985
+ #### **Comportamento:**
1986
+
1987
+ ✅ **Mantém todas as funcionalidades:**
1988
+ - Paginação (footer fixo)
1989
+ - Busca global
1990
+ - Filtros customizados
1991
+ - Bulk actions (se habilitado)
1992
+ - Botão "Novo" + Custom Actions
1993
+
1994
+ ❌ **Remove automaticamente:**
1995
+ - Tabela/Cards padrão
1996
+ - Ações de linha (editar/deletar inline)
1997
+ - Seleção de itens (checkbox)
1998
+
1999
+ **💡 Dica:** Se precisar de ações inline, implemente no seu componente customizado:
2000
+
2001
+ ```typescript
2002
+ customListView: (items, manager) => (
2003
+ <div className="space-y-2">
2004
+ {items.map(item => (
2005
+ <div key={item.id} className="flex justify-between items-center p-4 border rounded">
2006
+ <span>{item.title}</span>
2007
+ <div className="flex gap-2">
2008
+ <Button size="sm" onClick={() => handleEdit(item)}>Editar</Button>
2009
+ <Button size="sm" variant="destructive" onClick={() => manager.deleteEntity(item.id)}>
2010
+ Deletar
2011
+ </Button>
2012
+ </div>
2013
+ </div>
2014
+ ))}
2015
+ </div>
2016
+ )
2017
+ ```
2018
+
2019
+ **Boas Práticas:**
2020
+ - ✅ Use `useMemo` para evitar recriação em cada render
2021
+ - ✅ Mantenha responsividade (grid com breakpoints)
2022
+ - ✅ Implemente loading states se necessário
2023
+ - ✅ Use componentes da lib (`Dialog`, `Badge`, etc) para consistência
2024
+ - ❌ Evite lógica pesada no render (mova para useMemo/useCallback)
2025
+
2026
+ ---
2027
+
2028
+ ### **7️⃣ Customizar Comportamento de Edição (`onEdit`)**
2029
+
2030
+ Sobrescreva o comportamento padrão ao clicar em "Editar" nas linhas.
2031
+
2032
+ #### **Casos de Uso Comuns:**
2033
+ - 🔀 Navegação para página dedicada de edição
2034
+ - 📋 Abrir modal customizado (diferente do BaseForm)
2035
+ - 🔍 Abrir modal somente-leitura (resumo)
2036
+ - 🔄 Transformar dados antes de abrir o formulário
2037
+
2038
+ #### **Comportamento Padrão (sem `onEdit`):**
2039
+
2040
+ ```typescript
2041
+ // Ao clicar em "Editar":
2042
+ // 1. Abre modal com BaseForm
2043
+ // 2. Preenche campos com dados da entidade
2044
+ // 3. Chama handleSave ao submeter
2045
+ ```
2046
+
2047
+ #### **Exemplo 1: Navegação para Página Dedicada**
2048
+
2049
+ ```typescript
2050
+ import { useNavigate } from 'react-router-dom';
2051
+
2052
+ export default function ProcessesPage() {
2053
+ const navigate = useNavigate();
2054
+ const manager = useProcesses();
2055
+
2056
+ const config = useMemo(() => ({
2057
+ entityName: "Processo",
2058
+ entityNamePlural: "Processos",
2059
+ columns: processColumns,
2060
+ formSections,
2061
+
2062
+ // ✅ Navegar ao invés de abrir modal
2063
+ onEdit: (process: Process) => {
2064
+ navigate(`/processos/${process.id}/editar`);
2065
+ // Retornar void = não abre modal
2066
+ }
2067
+ }), [navigate]);
2068
+
2069
+ return <CrudPage />;
2070
+ }
2071
+ ```
2072
+
2073
+ #### **Exemplo 2: Modal Somente-Leitura para Status Específico**
2074
+
2075
+ ```typescript
2076
+ // TrainingRequestsPage.tsx
2077
+ export default function TrainingRequestsPage() {
2078
+ const manager = useTrainingRequests();
2079
+ const [summaryModal, setSummaryModal] = useState<{
2080
+ isOpen: boolean;
2081
+ request: TrainingRequest | null;
2082
+ }>({ isOpen: false, request: null });
2083
+
2084
+ const config = useMemo(() => ({
2085
+ entityName: "Solicitação",
2086
+ entityNamePlural: "Solicitações",
2087
+ columns,
2088
+ formSections,
2089
+
2090
+ // ✅ Modal de resumo se aprovada, editor padrão se não
2091
+ onEdit: (request: TrainingRequest) => {
2092
+ if (request.status === 'approved') {
2093
+ // Abrir modal customizado
2094
+ setSummaryModal({ isOpen: true, request });
2095
+ return; // void = não abre BaseForm
2096
+ }
2097
+
2098
+ // Retornar undefined = usar comportamento padrão (abrir BaseForm)
2099
+ // Nenhum return explícito necessário
2100
+ }
2101
+ }), []);
2102
+
2103
+ return (
2104
+ <>
2105
+ <CrudPage />
2106
+
2107
+ {/* Modal customizado */}
2108
+ <TrainingRequestSummaryModal
2109
+ request={summaryModal.request}
2110
+ isOpen={summaryModal.isOpen}
2111
+ onClose={() => setSummaryModal({ isOpen: false, request: null })}
2112
+ />
2113
+ </>
2114
+ );
2115
+ }
2116
+ ```
2117
+
2118
+ #### **Exemplo 3: Transformar Dados Antes de Editar**
2119
+
2120
+ ```typescript
2121
+ const config = useMemo(() => ({
2122
+ entityName: "Produto",
2123
+ entityNamePlural: "Produtos",
2124
+ columns,
2125
+ formSections,
2126
+
2127
+ // ✅ Modificar dados antes de popular o formulário
2128
+ onEdit: (product: Product) => {
2129
+ // Transformar preço de centavos para reais
2130
+ const transformedProduct = {
2131
+ ...product,
2132
+ price: product.price / 100, // 1999 → 19.99
2133
+ categories: product.category_ids?.join(',') // [1,2] → "1,2"
2134
+ };
2135
+
2136
+ // Retornar entidade transformada = abre BaseForm com dados modificados
2137
+ return transformedProduct;
2138
+ }
2139
+ }), []);
2140
+ ```
2141
+
2142
+ #### **Assinatura:**
2143
+
2144
+ ```typescript
2145
+ onEdit?: (entity: T) => T | void
2146
+ ```
2147
+
2148
+ | Retorno | Comportamento |
2149
+ |---------|---------------|
2150
+ | `T` (entidade transformada) | Abre `BaseForm` com dados transformados |
2151
+ | `void` / `undefined` | Não abre `BaseForm` (comportamento totalmente customizado) |
2152
+ | Sem `onEdit` | Comportamento padrão (abre `BaseForm` com dados originais) |
2153
+
2154
+ #### **Exemplo 4: Condicionalmente Bloquear Edição**
2155
+
2156
+ ```typescript
2157
+ import { toast } from 'forlogic-core';
2158
+
2159
+ const config = useMemo(() => ({
2160
+ entityName: "Documento",
2161
+ entityNamePlural: "Documentos",
2162
+ columns,
2163
+ formSections,
2164
+
2165
+ onEdit: (document: Document) => {
2166
+ // Bloquear edição se documento arquivado
2167
+ if (document.status === 'archived') {
2168
+ toast({
2169
+ title: 'Documento arquivado',
2170
+ description: 'Não é possível editar documentos arquivados',
2171
+ variant: 'destructive'
2172
+ });
2173
+ return; // void = não abre modal
2174
+ }
2175
+
2176
+ // Permitir edição normalmente (sem return = undefined = padrão)
2177
+ }
2178
+ }), []);
2179
+ ```
2180
+
2181
+ **Boas Práticas:**
2182
+ - ✅ Use `onEdit` para casos específicos (não generalize demais)
2183
+ - ✅ Retorne `void` quando **não quer** abrir o `BaseForm`
2184
+ - ✅ Retorne entidade transformada quando **quer** abrir o `BaseForm` com dados modificados
2185
+ - ✅ Não retorne nada (undefined) para usar comportamento padrão
2186
+ - ❌ Evite lógica pesada no `onEdit` (mova para funções separadas)
2187
+
2188
+ **Integração com `useCustomRouting`:**
2189
+
2190
+ ```typescript
2191
+ const config = useMemo(() => ({
2192
+ entityName: "Artigo",
2193
+ entityNamePlural: "Artigos",
2194
+ columns,
2195
+ formSections,
2196
+ useCustomRouting: true, // ✅ Não renderiza BaseForm
2197
+ onEdit: (article) => {
2198
+ navigate(`/artigos/${article.id}`);
2199
+ },
2200
+ onNew: () => {
2201
+ navigate('/artigos/novo');
2202
+ }
2203
+ }), [navigate]);
2204
+ ```
2205
+
2206
+ ---
2207
+
1637
2208
  ## 🔄 GUIA DE MIGRAÇÃO - v2.0
1638
2209
 
1639
2210
  Se você tem projetos usando versões antigas do `forlogic-core`, siga este guia para atualizar.