forlogic-core 1.4.15 → 1.5.1

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.
@@ -0,0 +1,307 @@
1
+ /**
2
+ * TEMPLATE: CRUD Completo
3
+ * ⚠️ REGRAS: Consulte docs/AI_REFERENCE.md
4
+ */
5
+
6
+ // ============= 1. DEFINIR INTERFACE DA ENTIDADE =============
7
+ // src/your-entity/your-entity.ts
8
+
9
+ export interface YourEntity {
10
+ id: string;
11
+ // Campos obrigatórios
12
+ name: string;
13
+ email: string;
14
+ status: 'active' | 'inactive' | 'pending';
15
+
16
+ // Campos opcionais
17
+ phone?: string | null;
18
+ description?: string | null;
19
+ category?: string | null;
20
+
21
+ // Timestamps
22
+ created_at?: string;
23
+ updated_at?: string;
24
+ }
25
+
26
+ // ============= 2. CRIAR SERVIÇO =============
27
+ // src/your-entity/yourEntityService.ts
28
+
29
+ import { createSimpleService } from 'forlogic-core';
30
+ import { YourEntity } from './your-entity';
31
+
32
+ export const yourEntityService = createSimpleService<YourEntity>({
33
+ tableName: 'your_entities',
34
+ entityName: 'YourEntity',
35
+ schemaName: 'YOUR_SCHEMA', // Use o schema do seu projeto (ex: 'central', 'trainings', 'public')
36
+ searchFields: ['name', 'email'], // Campos que serão buscados
37
+ });
38
+
39
+ // ============= 3. FORM SECTIONS WITH VALIDATION =============
40
+ // src/your-entity/YourEntityPage.tsx (seções do formulário)
41
+
42
+ import type { FormSection } from 'forlogic-core';
43
+
44
+ // Define as seções do formulário com validações customizadas
45
+ const formSections: FormSection[] = [
46
+ {
47
+ id: 'basic-info',
48
+ title: 'Informações Básicas',
49
+ fields: [
50
+ {
51
+ name: 'name',
52
+ label: 'Nome',
53
+ type: 'text',
54
+ required: true,
55
+ placeholder: 'Digite o nome',
56
+ },
57
+ {
58
+ name: 'email',
59
+ label: 'Email',
60
+ type: 'email',
61
+ required: true,
62
+ placeholder: 'email@exemplo.com',
63
+ validation: (value: string) => {
64
+ if (!value) return 'Email é obrigatório';
65
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
66
+ if (!emailRegex.test(value)) return 'Email inválido';
67
+ return undefined;
68
+ },
69
+ },
70
+ {
71
+ name: 'status',
72
+ label: 'Status',
73
+ type: 'select',
74
+ required: true,
75
+ options: [
76
+ { value: 'active', label: 'Ativo' },
77
+ { value: 'inactive', label: 'Inativo' },
78
+ { value: 'pending', label: 'Pendente' },
79
+ ],
80
+ defaultValue: 'active',
81
+ },
82
+ ],
83
+ },
84
+ {
85
+ id: 'additional-info',
86
+ title: 'Informações Adicionais',
87
+ fields: [
88
+ {
89
+ name: 'phone',
90
+ label: 'Telefone',
91
+ type: 'tel',
92
+ placeholder: '(00) 00000-0000',
93
+ },
94
+ {
95
+ name: 'category',
96
+ label: 'Categoria',
97
+ type: 'select',
98
+ options: [
99
+ { value: 'type1', label: 'Tipo 1' },
100
+ { value: 'type2', label: 'Tipo 2' },
101
+ ],
102
+ },
103
+ {
104
+ name: 'description',
105
+ label: 'Descrição',
106
+ type: 'textarea',
107
+ placeholder: 'Digite uma descrição...',
108
+ },
109
+ ],
110
+ },
111
+ ];
112
+
113
+ // ============= 4. CRIAR PÁGINA CRUD =============
114
+ // src/your-entity/YourEntityPage.tsx
115
+
116
+ import { createCrudPage, createSimpleSaveHandler, useCrud, type CrudColumn } from 'forlogic-core';
117
+ import { Folder, Mail, CheckCircle, XCircle, AlertCircle } from 'lucide-react';
118
+ import { YourEntity } from './your-entity';
119
+ import { yourEntityService } from './yourEntityService';
120
+
121
+ // Função helper para renderizar status
122
+ const renderStatus = (status: string) => {
123
+ const statusConfig = {
124
+ active: { icon: CheckCircle, label: 'Ativo', color: 'text-green-500' },
125
+ inactive: { icon: XCircle, label: 'Inativo', color: 'text-red-500' },
126
+ pending: { icon: AlertCircle, label: 'Pendente', color: 'text-yellow-500' },
127
+ };
128
+
129
+ const config = statusConfig[status as keyof typeof statusConfig];
130
+ const Icon = config.icon;
131
+
132
+ return (
133
+ <div className="flex items-center gap-2">
134
+ <Icon className={`h-4 w-4 ${config.color}`} />
135
+ <span className={config.color}>{config.label}</span>
136
+ </div>
137
+ );
138
+ };
139
+
140
+ // Configuração das colunas com renderização customizada
141
+ const columns: CrudColumn<YourEntity>[] = [
142
+ {
143
+ key: 'name',
144
+ header: 'Nome',
145
+ sortable: true,
146
+ render: (item: YourEntity) => <span className="font-medium">{item.name}</span>
147
+ },
148
+ {
149
+ key: 'status',
150
+ header: 'Status',
151
+ sortable: true,
152
+ render: (item: YourEntity) => renderStatus(item.status)
153
+ },
154
+ {
155
+ key: 'email',
156
+ header: 'Email',
157
+ sortable: true,
158
+ render: (item: YourEntity) => (
159
+ <a
160
+ href={`mailto:${item.email}`}
161
+ className="flex items-center gap-2 text-primary hover:underline"
162
+ >
163
+ <Mail className="h-4 w-4" />
164
+ {item.email}
165
+ </a>
166
+ )
167
+ },
168
+ {
169
+ key: 'category',
170
+ header: 'Categoria',
171
+ sortable: true,
172
+ render: (item: YourEntity) => item.category || '-'
173
+ },
174
+ ];
175
+
176
+ // Campos para exibição em cards (mobile/tablet)
177
+ const cardFields = [
178
+ {
179
+ key: 'name',
180
+ label: 'Nome',
181
+ render: (item: YourEntity) => <span className="font-semibold text-lg">{item.name}</span>
182
+ },
183
+ {
184
+ key: 'status',
185
+ label: 'Status',
186
+ render: (item: YourEntity) => renderStatus(item.status)
187
+ },
188
+ {
189
+ key: 'email',
190
+ label: 'Email',
191
+ render: (item: YourEntity) => (
192
+ <a
193
+ href={`mailto:${item.email}`}
194
+ className="flex items-center gap-2 text-primary hover:underline"
195
+ >
196
+ <Mail className="h-4 w-4" />
197
+ {item.email}
198
+ </a>
199
+ )
200
+ },
201
+ ];
202
+
203
+ // Componente da página
204
+ export function YourEntityPage() {
205
+ const manager = useCrud(yourEntityService);
206
+
207
+ // Handler customizado para normalizar dados antes de salvar
208
+ const handleSave = createSimpleSaveHandler(
209
+ manager,
210
+ // Transform para CREATE
211
+ (data: any) => ({
212
+ ...data,
213
+ email: data.email?.toLowerCase().trim(),
214
+ phone: data.phone || null,
215
+ category: data.category || null,
216
+ description: data.description || null,
217
+ status: data.status || 'active',
218
+ }),
219
+ // Transform para UPDATE
220
+ (data: any) => ({
221
+ ...data,
222
+ email: data.email?.toLowerCase().trim(),
223
+ phone: data.phone || null,
224
+ category: data.category || null,
225
+ description: data.description || null,
226
+ })
227
+ );
228
+
229
+ // Criar página CRUD completa
230
+ const CrudPage = createCrudPage({
231
+ manager,
232
+ config: {
233
+ entityName: 'Entidade',
234
+ entityNamePlural: 'Entidades',
235
+ columns,
236
+ formSections, // Seções definidas anteriormente
237
+ cardFields
238
+ },
239
+ onSave: handleSave
240
+ });
241
+
242
+ return <CrudPage />;
243
+ }
244
+
245
+ // ============= 5. ADICIONAR NO APP.TSX =============
246
+ /*
247
+ import { YourEntityPage } from './your-entity/YourEntityPage';
248
+ import { Folder } from 'lucide-react';
249
+
250
+ // No sidebarConfig:
251
+ navigation: [
252
+ {
253
+ label: 'Suas Entidades',
254
+ path: '/your-entities',
255
+ icon: Folder,
256
+ complementaryText: 'Gerenciar suas entidades'
257
+ },
258
+ ]
259
+
260
+ // Nas Routes:
261
+ <Route
262
+ path="/your-entities"
263
+ element={
264
+ <ProtectedRoute>
265
+ <YourEntityPage />
266
+ </ProtectedRoute>
267
+ }
268
+ />
269
+ */
270
+
271
+ // ============= PONTOS-CHAVE PARA CRUD COMPLETO =============
272
+ /*
273
+ 1. **Arquitetura Simplificada**:
274
+ - Apenas 2 camadas: createCrudPage → BaseForm
275
+ - Sem componentes intermediários desnecessários
276
+ - Use formSections diretamente no createCrudPage
277
+
278
+ 2. **Renderização Customizada**:
279
+ - Use a propriedade `render` nas columns e cardFields
280
+ - Receba o item completo no render: (item: YourEntity) => JSX
281
+ - Adicione ícones e formatação visual
282
+ - Crie links clicáveis (mailto, tel)
283
+
284
+ 3. **handleSave Customizado**:
285
+ - Use createSimpleSaveHandler com manager
286
+ - Normalize dados (lowercase emails, trim strings)
287
+ - Transforme dados antes de salvar
288
+ - Trate campos opcionais (null vs undefined)
289
+
290
+ 4. **cardFields**:
291
+ - Define como os dados aparecem em mobile/tablet
292
+ - Mostra apenas campos mais importantes
293
+ - Mesma renderização customizada das columns
294
+
295
+ 5. **Validações**:
296
+ - Cliente: função `validation` nos fields das formSections
297
+ - Retorne undefined (não null) para validação com sucesso
298
+ - Servidor: RLS policies no Supabase
299
+ - Dupla camada de segurança
300
+
301
+ 6. **Boas Práticas**:
302
+ - Use tipos TypeScript consistentes
303
+ - Crie funções helper para renderizações reutilizáveis
304
+ - Defina status como union types ('active' | 'inactive')
305
+ - Sempre normalize dados (emails lowercase, trim)
306
+ - NUNCA crie FormComponent customizado - use formSections!
307
+ */
@@ -0,0 +1,99 @@
1
+ /**
2
+ * TEMPLATE: Formulário Customizado
3
+ * 📚 Detalhes: Consulte docs/FOR_AI.md
4
+ */
5
+ import { BaseForm } from 'forlogic-core'
6
+ import { z } from 'zod'
7
+
8
+ // Schema de validação (opcional)
9
+ const validationSchema = z.object({
10
+ FIELD_NAME: z.string().min(1, 'FIELD_LABEL é obrigatório'),
11
+ // Adicione mais validações conforme necessário
12
+ })
13
+
14
+ // Definir seções do formulário
15
+ const formSections = [{
16
+ title: 'SECTION_TITLE',
17
+ fields: [
18
+ {
19
+ key: 'FIELD_NAME',
20
+ label: 'FIELD_LABEL',
21
+ type: 'FIELD_TYPE' as const,
22
+ required: BOOLEAN,
23
+ placeholder: 'PLACEHOLDER_TEXT'
24
+ },
25
+ // Campos de seleção
26
+ {
27
+ key: 'SELECT_FIELD',
28
+ label: 'SELECT_LABEL',
29
+ type: 'select' as const,
30
+ options: [
31
+ { value: 'option1', label: 'Opção 1' },
32
+ { value: 'option2', label: 'Opção 2' }
33
+ ]
34
+ },
35
+ // Campo de checkbox
36
+ {
37
+ key: 'CHECKBOX_FIELD',
38
+ label: 'CHECKBOX_LABEL',
39
+ type: 'checkbox' as const
40
+ },
41
+ // Campo de texto longo
42
+ {
43
+ key: 'TEXTAREA_FIELD',
44
+ label: 'TEXTAREA_LABEL',
45
+ type: 'textarea' as const,
46
+ rows: 4
47
+ }
48
+ ]
49
+ }]
50
+
51
+ interface FORM_NAMEProps {
52
+ entityId?: string
53
+ onSave: (data: any) => void
54
+ onCancel: () => void
55
+ }
56
+
57
+ export function FORM_NAMEForm({ entityId, onSave, onCancel }: FORM_NAMEProps) {
58
+ return (
59
+ <BaseForm
60
+ entityId={entityId}
61
+ entity="TABLE_NAME"
62
+ entityLabel="ENTITY_LABEL"
63
+ formSections={formSections}
64
+ validationSchema={validationSchema}
65
+ onSave={onSave}
66
+ onCancel={onCancel}
67
+ submitText="Salvar ENTITY_LABEL"
68
+ cancelText="Cancelar"
69
+ />
70
+ )
71
+ }
72
+
73
+ /*
74
+ TIPOS DE CAMPO DISPONÍVEIS:
75
+
76
+ - 'text' - Texto simples
77
+ - 'email' - Email com validação
78
+ - 'password' - Senha oculta
79
+ - 'number' - Números
80
+ - 'textarea' - Texto longo
81
+ - 'select' - Lista de opções
82
+ - 'checkbox' - Verdadeiro/falso
83
+ - 'date' - Data
84
+ - 'rich-text' - Editor HTML
85
+ - 'qualiex-user' - Seleção de usuário Qualiex
86
+ - 'qualiex-responsible' - Seleção de responsável Qualiex
87
+
88
+ EXEMPLO DE VALIDAÇÃO COM ZOD:
89
+
90
+ const schema = z.object({
91
+ name: z.string().min(1, 'Nome é obrigatório'),
92
+ email: z.string().email('Email inválido'),
93
+ age: z.number().min(18, 'Deve ser maior de idade'),
94
+ active: z.boolean(),
95
+ category: z.enum(['option1', 'option2'], {
96
+ errorMap: () => ({ message: 'Categoria é obrigatória' })
97
+ })
98
+ })
99
+ */
@@ -0,0 +1,194 @@
1
+ /**
2
+ * TEMPLATE: Service Customizado
3
+ * 📚 Detalhes: Consulte docs/FOR_AI.md
4
+ */
5
+ import { BaseService } from 'forlogic-core'
6
+
7
+ // Interface da entidade
8
+ interface ENTITY_TYPE {
9
+ id?: string
10
+ FIELD_NAME: string
11
+ // Adicione mais campos conforme necessário
12
+ created_at?: string
13
+ updated_at?: string
14
+ }
15
+
16
+ class ENTITY_NAMEService extends BaseService<ENTITY_TYPE> {
17
+ constructor() {
18
+ super('TABLE_NAME')
19
+ }
20
+
21
+ // Método customizado - buscar por campo específico
22
+ async getByFIELD_NAME(fieldValue: string): Promise<ENTITY_TYPE[]> {
23
+ const { data, error } = await this.supabase
24
+ .from(this.tableName)
25
+ .select('*')
26
+ .eq('FIELD_NAME', fieldValue)
27
+
28
+ if (error) throw error
29
+ return data || []
30
+ }
31
+
32
+ // Método customizado - buscar ativos
33
+ async getActive(): Promise<ENTITY_TYPE[]> {
34
+ const { data, error } = await this.supabase
35
+ .from(this.tableName)
36
+ .select('*')
37
+ .eq('active', true)
38
+ .order('created_at', { ascending: false })
39
+
40
+ if (error) throw error
41
+ return data || []
42
+ }
43
+
44
+ // Override do método create com validação customizada
45
+ async create(item: Omit<ENTITY_TYPE, 'id'>): Promise<ENTITY_TYPE> {
46
+ // Validação customizada
47
+ if (!item.FIELD_NAME?.trim()) {
48
+ throw new Error('FIELD_LABEL é obrigatório')
49
+ }
50
+
51
+ // Verificar duplicatas
52
+ const existing = await this.getByFIELD_NAME(item.FIELD_NAME)
53
+ if (existing.length > 0) {
54
+ throw new Error('FIELD_LABEL já existe')
55
+ }
56
+
57
+ return super.create(item)
58
+ }
59
+
60
+ // Override do método update
61
+ async update(id: string, item: Partial<ENTITY_TYPE>): Promise<ENTITY_TYPE> {
62
+ // Validação antes de atualizar
63
+ if (item.FIELD_NAME && !item.FIELD_NAME.trim()) {
64
+ throw new Error('FIELD_LABEL não pode estar vazio')
65
+ }
66
+
67
+ return super.update(id, item)
68
+ }
69
+
70
+ // Método customizado - busca com paginação
71
+ async getPaginated(page: number = 1, pageSize: number = 10) {
72
+ const from = (page - 1) * pageSize
73
+ const to = from + pageSize - 1
74
+
75
+ const { data, error, count } = await this.supabase
76
+ .from(this.tableName)
77
+ .select('*', { count: 'exact' })
78
+ .range(from, to)
79
+ .order('created_at', { ascending: false })
80
+
81
+ if (error) throw error
82
+
83
+ return {
84
+ data: data || [],
85
+ count: count || 0,
86
+ page,
87
+ pageSize,
88
+ totalPages: Math.ceil((count || 0) / pageSize)
89
+ }
90
+ }
91
+
92
+ // Método customizado - busca com filtros
93
+ async search(filters: {
94
+ FIELD_NAME?: string
95
+ active?: boolean
96
+ dateFrom?: string
97
+ dateTo?: string
98
+ }) {
99
+ let query = this.supabase.from(this.tableName).select('*')
100
+
101
+ if (filters.FIELD_NAME) {
102
+ query = query.ilike('FIELD_NAME', `%${filters.FIELD_NAME}%`)
103
+ }
104
+
105
+ if (filters.active !== undefined) {
106
+ query = query.eq('active', filters.active)
107
+ }
108
+
109
+ if (filters.dateFrom) {
110
+ query = query.gte('created_at', filters.dateFrom)
111
+ }
112
+
113
+ if (filters.dateTo) {
114
+ query = query.lte('created_at', filters.dateTo)
115
+ }
116
+
117
+ const { data, error } = await query.order('created_at', { ascending: false })
118
+
119
+ if (error) throw error
120
+ return data || []
121
+ }
122
+
123
+ // Método customizado - estatísticas
124
+ async getStats() {
125
+ const { data, error } = await this.supabase
126
+ .from(this.tableName)
127
+ .select('active')
128
+
129
+ if (error) throw error
130
+
131
+ const total = data?.length || 0
132
+ const active = data?.filter(item => item.active).length || 0
133
+ const inactive = total - active
134
+
135
+ return {
136
+ total,
137
+ active,
138
+ inactive,
139
+ activePercentage: total > 0 ? Math.round((active / total) * 100) : 0
140
+ }
141
+ }
142
+ }
143
+
144
+ // Exportar instância singleton
145
+ export const ENTITY_NAMEService_instance = new ENTITY_NAMEService()
146
+
147
+ /*
148
+ EXEMPLO DE USO NO COMPONENTE:
149
+
150
+ import { ENTITY_NAMEService_instance } from '../services/ENTITY_NAMEService'
151
+
152
+ function MyComponent() {
153
+ const [items, setItems] = useState([])
154
+ const [loading, setLoading] = useState(true)
155
+
156
+ useEffect(() => {
157
+ async function loadData() {
158
+ try {
159
+ const data = await ENTITY_NAMEService_instance.getActive()
160
+ setItems(data)
161
+ } catch (error) {
162
+ console.error('Erro ao carregar dados:', error)
163
+ } finally {
164
+ setLoading(false)
165
+ }
166
+ }
167
+
168
+ loadData()
169
+ }, [])
170
+
171
+ const handleCreate = async (newItem) => {
172
+ try {
173
+ await ENTITY_NAMEService_instance.create(newItem)
174
+ // Recarregar dados
175
+ } catch (error) {
176
+ console.error('Erro ao criar:', error)
177
+ }
178
+ }
179
+
180
+ return (
181
+ <div>
182
+ {/* Sua UI aqui */}
183
+ </div>
184
+ )
185
+ }
186
+
187
+ MÉTODOS HERDADOS DO BaseService:
188
+
189
+ - getAll(): Promise<T[]>
190
+ - getById(id: string): Promise<T | null>
191
+ - create(item: Omit<T, 'id'>): Promise<T>
192
+ - update(id: string, item: Partial<T>): Promise<T>
193
+ - delete(id: string): Promise<void>
194
+ */