forlogic-core 1.5.0 → 1.5.2
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/README.md +21 -1116
- package/dist/README.md +248 -0
- package/dist/docs/AI_RULES.md +213 -0
- package/dist/docs/README.md +102 -0
- package/dist/docs/TROUBLESHOOTING.md +473 -0
- package/dist/docs/architecture/TOKENS_ARCHITECTURE.md +712 -0
- package/dist/docs/templates/app-layout.tsx +192 -0
- package/dist/docs/templates/basic-crud-page.tsx +97 -0
- package/dist/docs/templates/basic-crud-working.tsx +182 -0
- package/dist/docs/templates/complete-crud-example.tsx +307 -0
- package/dist/docs/templates/custom-form.tsx +99 -0
- package/dist/docs/templates/custom-service.tsx +194 -0
- package/dist/docs/templates/permission-check-hook.tsx +275 -0
- package/dist/docs/templates/qualiex-config.ts +137 -0
- package/dist/docs/templates/qualiex-integration-example.tsx +430 -0
- package/dist/docs/templates/quick-start-example.tsx +96 -0
- package/dist/docs/templates/rls-policies.sql +80 -0
- package/dist/docs/templates/sidebar-config.tsx +145 -0
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -1
- package/dist/assets/index-C_ZLBeXY.css +0 -1
- package/dist/assets/index-wEAMQwsw.js +0 -7898
- package/dist/index.html +0 -19
|
@@ -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
|
+
*/
|