@jean.anjos/coresuit-gen-crud 1.1.0

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 ADDED
@@ -0,0 +1,26 @@
1
+
2
+ # como instalar no projeto
3
+
4
+ ### Baixa o projeto git clone
5
+
6
+ ## no seu package.json colocar
7
+
8
+ ```
9
+ "scripts": {
10
+ "generate": "coresuit-react-gen"
11
+ },
12
+ ```
13
+
14
+ ## instala com o seguinte comando
15
+ npm i -D C:\dev\core-suit\generate-crud-react-js
16
+
17
+ # (opcional) filtrar por tags:
18
+
19
+ ```
20
+ npm run generate -- "--link=http://127.0.0.1:8000/docs?api-docs.json" --only=Categoria,Cliente
21
+
22
+ ```
23
+
24
+ ```
25
+ npm run generate -- --name=categoria --fields="id:int:true,user_id:int:true,nome:string:true,deleted_at:string:false,created_at:string:false,updated_at:string:false"
26
+ ```
package/bin/cli.js ADDED
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ // ---------- utils de nome ----------
7
+ function toPascalCase(s) {
8
+ return String(s)
9
+ .replace(/[_\-\s]+/g, " ")
10
+ .replace(/(?:^|\s)(\w)/g, (_, c) => c.toUpperCase())
11
+ .replace(/\s+/g, "");
12
+ }
13
+ function toCamelCase(s) {
14
+ const p = toPascalCase(s);
15
+ return p.charAt(0).toLowerCase() + p.slice(1);
16
+ }
17
+ function toKebab(s) {
18
+ return String(s)
19
+ .trim()
20
+ .replace(/[_\s]+/g, "-")
21
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
22
+ .toLowerCase();
23
+ }
24
+
25
+ // nome de campo no MODEL (response) => camelCase simples
26
+ function toModelFieldName(dtoFieldName) {
27
+ const n = String(dtoFieldName || "").trim();
28
+ if (!n) return n;
29
+ return n.charAt(0).toLowerCase() + n.slice(1);
30
+ }
31
+
32
+ // ---------- tipos ----------
33
+ function tsTypeFor(t) {
34
+ t = String(t || "").toLowerCase();
35
+ if (["int", "bigint", "integer", "smallint", "tinyint", "float", "double", "decimal", "number"].includes(t))
36
+ return "number";
37
+ if (["bool", "boolean"].includes(t)) return "boolean";
38
+ if (["json", "array", "object", "any"].includes(t)) return "any";
39
+ if (["date", "datetime", "date-time"].includes(t)) return "string";
40
+ return "string";
41
+ }
42
+
43
+ function isNumberType(t) {
44
+ return tsTypeFor(t) === "number";
45
+ }
46
+ function isBooleanType(t) {
47
+ return tsTypeFor(t) === "boolean";
48
+ }
49
+ function isDateType(t) {
50
+ return ["date", "datetime", "date-time"].includes(String(t).toLowerCase()) || /date/i.test(String(t));
51
+ }
52
+
53
+ // ---------- args ----------
54
+ function getArgMap(argv) {
55
+ const map = new Map();
56
+ const args = argv.slice(2);
57
+
58
+ for (let i = 0; i < args.length; i++) {
59
+ const a = args[i];
60
+ if (!a.startsWith("--")) continue;
61
+
62
+ const eq = a.indexOf("=");
63
+ if (eq > 2) {
64
+ map.set(a.slice(2, eq), a.slice(eq + 1));
65
+ } else {
66
+ const key = a.slice(2);
67
+ const nx = args[i + 1];
68
+ if (nx && !nx.startsWith("--")) {
69
+ map.set(key, nx);
70
+ i++;
71
+ } else map.set(key, "true");
72
+ }
73
+ }
74
+
75
+ // fallback npm_config_*
76
+ for (const k of ["name", "fields", "force", "baseDir", "servicesDir", "typesDir"]) {
77
+ const ev = process.env[`npm_config_${k.replace(/-/g, "_")}`];
78
+ if (ev !== undefined && !map.has(k)) map.set(k, ev);
79
+ }
80
+
81
+ return Object.fromEntries(map);
82
+ }
83
+
84
+ // ---------- fs helpers ----------
85
+ function ensureDir(dir) {
86
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
87
+ }
88
+ function writeFile(dest, contents, force) {
89
+ ensureDir(path.dirname(dest));
90
+ if (fs.existsSync(dest) && !force) {
91
+ console.log(`• Já existe: ${dest} (use --force=true para sobrescrever)`);
92
+ return;
93
+ }
94
+ fs.writeFileSync(dest, contents);
95
+ console.log(`• Criado: ${dest}`);
96
+ }
97
+ function readTpl(name) {
98
+ const root = path.dirname(path.dirname(__filename));
99
+ return fs.readFileSync(path.join(root, "templates", name), "utf8");
100
+ }
101
+
102
+ // ---------- templates ----------
103
+ const TPL_SERVICE_INDEX = readTpl("service_index.ts.tpl");
104
+ const TPL_SERVICE_TYPES = readTpl("service_types.ts.tpl");
105
+ const TPL_ENTITY_IFACE = readTpl("types_interface.ts.tpl");
106
+
107
+ const TPL_PAGE_INDEX = readTpl("page__index.tsx.tpl");
108
+ const TPL_CONSTS = readTpl("page__consts__index.ts.tpl");
109
+ const TPL_FILTERS_COMPONENT = readTpl("page__components__filters.tsx.tpl");
110
+
111
+ const TPL_CREATE = readTpl("page__create__index.tsx.tpl");
112
+ const TPL_UPDATE = readTpl("page__update__index.tsx.tpl");
113
+ const TPL_DELETE = readTpl("page__delete__index.tsx.tpl");
114
+ const TPL_RESTORE = readTpl("page__restore__index.tsx.tpl");
115
+
116
+ const TPL_HOOK_INDEX = readTpl("page__hooks__use-index.ts.tpl");
117
+ const TPL_HOOK_CREATE = readTpl("page__hooks__use-create.ts.tpl");
118
+ const TPL_HOOK_UPDATE = readTpl("page__hooks__use-update.ts.tpl");
119
+ const TPL_HOOK_DELETE = readTpl("page__hooks__use-delete.ts.tpl");
120
+ const TPL_HOOK_RESTORE = readTpl("page__hooks__use-restore.ts.tpl");
121
+
122
+ function renderTemplate(tpl, ctx) {
123
+ return tpl
124
+ .replaceAll("{{PascalName}}", ctx.PascalName)
125
+ .replaceAll("{{camelName}}", ctx.camelName)
126
+ .replaceAll("{{kebabName}}", ctx.kebabName)
127
+ .replaceAll("{{route}}", ctx.route)
128
+ .replaceAll("{{FIELDS_INTERFACE}}", ctx.FIELDS_INTERFACE || "")
129
+ .replaceAll("{{FILTERS_INTERFACE}}", ctx.FILTERS_INTERFACE || "")
130
+ .replaceAll("{{CREATE_FIELDS}}", ctx.CREATE_FIELDS || "")
131
+ .replaceAll("{{UPDATE_FIELDS}}", ctx.UPDATE_FIELDS || "")
132
+ .replaceAll("{{TABLE_HEAD}}", ctx.TABLE_HEAD || "")
133
+ .replaceAll("{{TABLE_ROW_CELLS}}", ctx.TABLE_ROW_CELLS || "")
134
+ .replaceAll("{{FILTERS_INITIAL_OBJECT}}", ctx.FILTERS_INITIAL_OBJECT || "")
135
+ .replaceAll("{{FILTERS_FIELDS_JSX}}", ctx.FILTERS_FIELDS_JSX || "")
136
+ .replaceAll("{{FILTER_SPREAD_IN_QUERY}}", ctx.FILTER_SPREAD_IN_QUERY || "")
137
+ .replaceAll("{{FORM_FIELDS_CREATE_TSX}}", ctx.FORM_FIELDS_CREATE_TSX || "")
138
+ .replaceAll("{{FORM_FIELDS_UPDATE_TSX}}", ctx.FORM_FIELDS_UPDATE_TSX || "")
139
+ .replaceAll("{{YUP_CREATE_RULES}}", ctx.YUP_CREATE_RULES || "")
140
+ .replaceAll("{{YUP_UPDATE_RULES}}", ctx.YUP_UPDATE_RULES || "")
141
+ .replaceAll("{{FORM_DEFAULTS_CREATE}}", ctx.FORM_DEFAULTS_CREATE || "")
142
+ .replaceAll("{{FORM_DEFAULTS_UPDATE}}", ctx.FORM_DEFAULTS_UPDATE || "")
143
+ .replaceAll("{{FORM_VALUES_UPDATE}}", ctx.FORM_VALUES_UPDATE || "");
144
+ }
145
+
146
+ // ---------- parsing manual ----------
147
+ function parseFields(raw) {
148
+ if (!raw) return [];
149
+ return raw
150
+ .split(",")
151
+ .map((x) => x.trim())
152
+ .filter(Boolean)
153
+ .map((chunk) => {
154
+ const [name, type, required] = chunk.split(":");
155
+ return {
156
+ name: String(name).trim(), // DTO/Request => PascalCase
157
+ type: String(type || "string").trim(),
158
+ required: String(required || "false").toLowerCase() === "true",
159
+ modelName: toModelFieldName(String(name).trim()), // Model/Response => camelCase
160
+ };
161
+ });
162
+ }
163
+
164
+ // ---------- geração (novo padrão) ----------
165
+ function buildCtxFromFields(resourceName, fields) {
166
+ const PascalName = toPascalCase(resourceName); // Bank
167
+ const camelName = toCamelCase(resourceName); // bank
168
+ const kebabName = toKebab(resourceName); // bank
169
+
170
+ // endpoint => PascalName literal
171
+ const route = PascalName;
172
+
173
+ // 1) Interface de entidade (MODEL/Response) camelCase + campos padrão
174
+ const entityFields = [
175
+ { name: "id", ts: "number" },
176
+ ...fields.map((f) => ({ name: f.modelName, ts: tsTypeFor(f.type) })),
177
+ { name: "deletedAt", ts: "string" },
178
+ { name: "createdAt", ts: "string" },
179
+ { name: "updatedAt", ts: "string" },
180
+ ];
181
+
182
+ const FIELDS_INTERFACE = entityFields
183
+ .map((f, i, arr) => ` ${f.name}: ${f.ts}${i < arr.length - 1 ? "," : ""}`)
184
+ .join("\n");
185
+
186
+ // 2) Table head (Padrão fixo)
187
+ const TABLE_HEAD = [
188
+ ` { id: 'Id', label: 'Id' },`,
189
+ ...fields.map((f) => ` { id: '${f.name}', label: '${f.name}' },`),
190
+ ` { id: 'DeletedAt', label: 'Ativo?' },`,
191
+ ` { id: 'CreatedAt', label: 'Cadastro' },`,
192
+ ` { id: 'UpdatedAt', label: 'Atualizado' },`,
193
+ ].join("\n");
194
+
195
+ // 3) Table row cells (MODEL)
196
+ const TABLE_ROW_CELLS = [
197
+ ` <TableCell>{d.id}</TableCell>`,
198
+ ...fields.map((f) => ` <TableCell>{d.${f.modelName}}</TableCell>`),
199
+ ` <TableCell>
200
+ <Label color={d.deletedAt ? 'error' : 'success'}>
201
+ {d.deletedAt ? 'Não' : 'Sim'}
202
+ </Label>
203
+ </TableCell>`,
204
+ ` <TableCell>{d.createdAt ? dayjs(d.createdAt).format('lll') : ''}</TableCell>`,
205
+ ` <TableCell>{d.updatedAt ? dayjs(d.updatedAt).format('lll') : ''}</TableCell>`,
206
+ ].join("\n");
207
+
208
+ // 4) Filters (DTO)
209
+ const FILTERS_INITIAL_OBJECT = fields.map((f) => ` ${f.name}: ""`).join(",\n");
210
+
211
+ const FILTERS_FIELDS_JSX = fields
212
+ .map(
213
+ (f) => `
214
+ <TextField
215
+ value={filter.${f.name}}
216
+ onChange={(e) => setFilter(prev => ({ ...prev, ${f.name}: e.target.value }))}
217
+ label="${f.name}"
218
+ autoComplete="off"
219
+ sx={{ mb: 2 }}
220
+ />`.trim()
221
+ )
222
+ .join("\n\n ");
223
+
224
+ const FILTER_SPREAD_IN_QUERY = fields.map((f) => ` ${f.name}: filter.${f.name},`).join("\n");
225
+
226
+ // 5) Services types (DTO)
227
+ const FILTERS_INTERFACE = fields.map((f) => ` ${f.name}: string;`).join("\n");
228
+
229
+ const CREATE_FIELDS = fields
230
+ .map((f) => ` ${f.name}${f.required ? "" : "?"}: ${tsTypeFor(f.type)};`)
231
+ .join("\n");
232
+
233
+ const UPDATE_FIELDS = fields
234
+ .map((f) => ` ${f.name}${f.required ? "" : "?"}: ${tsTypeFor(f.type)};`)
235
+ .join("\n");
236
+
237
+ // 6) Form fields (DTO) - Field.Text
238
+ function fieldToTsx(f) {
239
+ const isNum = isNumberType(f.type);
240
+ return ` <Grid item md={12} xs={12}>
241
+ <Field.Text fullWidth name="${f.name}" label="${f.name}"${isNum ? ' type="number"' : ""} />
242
+ </Grid>`;
243
+ }
244
+
245
+ const FORM_FIELDS_CREATE_TSX = fields.map(fieldToTsx).join("\n");
246
+ const FORM_FIELDS_UPDATE_TSX = fields.map(fieldToTsx).join("\n");
247
+
248
+ // 7) Yup + defaults
249
+ function yupBaseByType(f) {
250
+ if (isNumberType(f.type)) return `yup.number().typeError("Número inválido")`;
251
+ if (isBooleanType(f.type)) return `yup.boolean()`;
252
+ if (isDateType(f.type)) return `yup.string()`;
253
+ return `yup.string()`;
254
+ }
255
+
256
+ function yupRule(f, isReq) {
257
+ const base = yupBaseByType(f);
258
+ return ` ${f.name}: ${base}${isReq ? '.required("Campo obrigatório")' : ".optional()"},`;
259
+ }
260
+
261
+ const YUP_CREATE_RULES = fields.map((f) => yupRule(f, !!f.required)).join("\n");
262
+ const YUP_UPDATE_RULES = fields.map((f) => yupRule(f, !!f.required)).join("\n");
263
+
264
+ const FORM_DEFAULTS_CREATE = fields
265
+ .map((f) => ` ${f.name}: ${isNumberType(f.type) ? "undefined" : '""'},`)
266
+ .join("\n");
267
+
268
+ const FORM_DEFAULTS_UPDATE = fields
269
+ .map((f) => ` ${f.name}: undefined,`)
270
+ .join("\n");
271
+
272
+ const FORM_VALUES_UPDATE = fields
273
+ .map((f) => {
274
+ if (isNumberType(f.type)) return ` ${f.name}: selectClickItem?.${f.modelName} ?? undefined,`;
275
+ return ` ${f.name}: selectClickItem?.${f.modelName} ?? "",`;
276
+ })
277
+ .join("\n");
278
+
279
+ return {
280
+ PascalName,
281
+ camelName,
282
+ kebabName,
283
+ route,
284
+ FIELDS_INTERFACE,
285
+ FILTERS_INTERFACE,
286
+ CREATE_FIELDS,
287
+ UPDATE_FIELDS,
288
+ TABLE_HEAD,
289
+ TABLE_ROW_CELLS,
290
+ FILTERS_INITIAL_OBJECT,
291
+ FILTERS_FIELDS_JSX,
292
+ FILTER_SPREAD_IN_QUERY,
293
+ FORM_FIELDS_CREATE_TSX,
294
+ FORM_FIELDS_UPDATE_TSX,
295
+ YUP_CREATE_RULES,
296
+ YUP_UPDATE_RULES,
297
+ FORM_DEFAULTS_CREATE,
298
+ FORM_DEFAULTS_UPDATE,
299
+ FORM_VALUES_UPDATE,
300
+ };
301
+ }
302
+
303
+ function writeOutputs(cwd, baseDir, servicesDir, typesDir, ctx) {
304
+ const force = writeOutputs._force;
305
+
306
+ // services + types
307
+ const outServiceIndex = path.join(cwd, baseDir, servicesDir, ctx.kebabName, "index.ts");
308
+ const outServiceTypes = path.join(cwd, baseDir, servicesDir, ctx.kebabName, "types.ts");
309
+ const outEntityType = path.join(cwd, baseDir, typesDir, `i-${ctx.kebabName}.ts`);
310
+
311
+ writeFile(outServiceIndex, renderTemplate(TPL_SERVICE_INDEX, ctx), force);
312
+ writeFile(outServiceTypes, renderTemplate(TPL_SERVICE_TYPES, ctx), force);
313
+ writeFile(outEntityType, renderTemplate(TPL_ENTITY_IFACE, ctx), force);
314
+
315
+ // pages
316
+ const pageBase = path.join(cwd, baseDir, "pages", ctx.kebabName);
317
+
318
+ writeFile(path.join(pageBase, "index.tsx"), renderTemplate(TPL_PAGE_INDEX, ctx), force);
319
+ writeFile(path.join(pageBase, "consts", "index.ts"), renderTemplate(TPL_CONSTS, ctx), force);
320
+ writeFile(path.join(pageBase, "components", `${ctx.kebabName}-filters-component.tsx`), renderTemplate(TPL_FILTERS_COMPONENT, ctx), force);
321
+
322
+ writeFile(path.join(pageBase, "create", "index.tsx"), renderTemplate(TPL_CREATE, ctx), force);
323
+ writeFile(path.join(pageBase, "update", "index.tsx"), renderTemplate(TPL_UPDATE, ctx), force);
324
+ writeFile(path.join(pageBase, "delete", "index.tsx"), renderTemplate(TPL_DELETE, ctx), force);
325
+ writeFile(path.join(pageBase, "restore", "index.tsx"), renderTemplate(TPL_RESTORE, ctx), force);
326
+
327
+ writeFile(path.join(pageBase, "hooks", `use-${ctx.kebabName}-index.ts`), renderTemplate(TPL_HOOK_INDEX, ctx), force);
328
+ writeFile(path.join(pageBase, "hooks", `use-${ctx.kebabName}-create.ts`), renderTemplate(TPL_HOOK_CREATE, ctx), force);
329
+ writeFile(path.join(pageBase, "hooks", `use-${ctx.kebabName}-update.ts`), renderTemplate(TPL_HOOK_UPDATE, ctx), force);
330
+ writeFile(path.join(pageBase, "hooks", `use-${ctx.kebabName}-delete.ts`), renderTemplate(TPL_HOOK_DELETE, ctx), force);
331
+ writeFile(path.join(pageBase, "hooks", `use-${ctx.kebabName}-restore.ts`), renderTemplate(TPL_HOOK_RESTORE, ctx), force);
332
+
333
+ console.log(` → ${path.relative(cwd, outServiceIndex)}`);
334
+ console.log(` → ${path.relative(cwd, outServiceTypes)}`);
335
+ console.log(` → ${path.relative(cwd, outEntityType)}`);
336
+ console.log(` → ${path.relative(cwd, path.join(pageBase, "index.tsx"))}`);
337
+ }
338
+
339
+ // ---------- main ----------
340
+ async function main() {
341
+ const argMap = getArgMap(process.argv);
342
+
343
+ const defaults = {
344
+ baseDir: "src",
345
+ servicesDir: "services/system",
346
+ typesDir: "types/system",
347
+ };
348
+
349
+ const force = String(argMap.force || "false") === "true";
350
+ writeOutputs._force = force;
351
+
352
+ const baseDir = argMap.baseDir || defaults.baseDir;
353
+ const servicesDir = argMap.servicesDir || defaults.servicesDir;
354
+ const typesDir = argMap.typesDir || defaults.typesDir;
355
+
356
+ const cwd = process.cwd();
357
+
358
+ const name = argMap.name || "";
359
+ const fieldsRaw = argMap.fields || "";
360
+
361
+ if (!name || !fieldsRaw) {
362
+ console.error(
363
+ "Uso:\n" +
364
+ ' npm run generate -- --name=Bank --fields="Code:string:true,Name:string:true" [--force=true]\n'
365
+ );
366
+ process.exit(1);
367
+ }
368
+
369
+ const fields = parseFields(fieldsRaw);
370
+
371
+ const ctx = buildCtxFromFields(name, fields);
372
+ writeOutputs(cwd, baseDir, servicesDir, typesDir, ctx);
373
+
374
+ console.log("\n✅ Gerado com sucesso (novo padrão).");
375
+ }
376
+
377
+ main();
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@jean.anjos/coresuit-gen-crud",
3
+ "version": "1.1.0",
4
+ "description": "Gerador de services e types (React/TS) no padrão CoreSuit",
5
+ "license": "MIT",
6
+ "bin": {
7
+ "coresuit-react-gen": "bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "templates",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=16"
16
+ }
17
+ }
@@ -0,0 +1,47 @@
1
+ import { Button, TextField, Typography } from "@mui/material";
2
+
3
+ import DrawerFiltersComponent from "src/components/drawer-filters";
4
+
5
+ import { InitialFilter{{PascalName}} } from "../consts";
6
+
7
+ import type use{{PascalName}}Index from "../hooks/use-{{kebabName}}-index";
8
+
9
+ interface IProps {
10
+ hook{{PascalName}}Index: ReturnType<typeof use{{PascalName}}Index>;
11
+ }
12
+
13
+ export default function {{PascalName}}FiltersComponent({ hook{{PascalName}}Index }: IProps) {
14
+ const { isFilterApplied, openDrawerFilters, queryClient, setFilter, filter } = hook{{PascalName}}Index;
15
+
16
+ return (
17
+ <DrawerFiltersComponent
18
+ canReset={!isFilterApplied}
19
+ open={openDrawerFilters.value}
20
+ onClose={openDrawerFilters.onFalse}
21
+ onOpen={openDrawerFilters.onTrue}
22
+ resetFilters={() => {
23
+ setFilter(InitialFilter{{PascalName}});
24
+ setTimeout(() => {
25
+ queryClient.invalidateQueries({ queryKey: ["data{{PascalName}}"] });
26
+ }, 0);
27
+ }}
28
+ >
29
+ <>
30
+ <Typography variant="subtitle2" sx={{ mb: 1 }}>
31
+ Filtros de Pesquisas
32
+ </Typography>
33
+
34
+ {{FILTERS_FIELDS_JSX}}
35
+
36
+ <Button
37
+ variant="outlined"
38
+ onClick={() => {
39
+ queryClient.invalidateQueries({ queryKey: ["data{{PascalName}}"] });
40
+ }}
41
+ >
42
+ Aplicar Filtros
43
+ </Button>
44
+ </>
45
+ </DrawerFiltersComponent>
46
+ );
47
+ }
@@ -0,0 +1,12 @@
1
+ import type { I{{PascalName}}Get } from "src/services/system/{{kebabName}}/types";
2
+
3
+ const tableHead{{PascalName}} = [
4
+ {{TABLE_HEAD}}
5
+ { id: "", label: "Ações" },
6
+ ];
7
+
8
+ const InitialFilter{{PascalName}}: I{{PascalName}}Get = {
9
+ {{FILTERS_INITIAL_OBJECT}}
10
+ };
11
+
12
+ export { tableHead{{PascalName}}, InitialFilter{{PascalName}} };
@@ -0,0 +1,39 @@
1
+ import { LoadingButton } from "@mui/lab";
2
+ import { Grid, Stack, Button, Typography } from "@mui/material";
3
+
4
+ import { Form, Field } from "src/components/hook-form";
5
+ import DrawerFiltersComponent from "src/components/drawer-filters";
6
+
7
+ import type use{{PascalName}}Create from "../hooks/use-{{kebabName}}-create";
8
+
9
+ interface IProps {
10
+ hook{{PascalName}}Create: ReturnType<typeof use{{PascalName}}Create>;
11
+ }
12
+
13
+ export default function {{PascalName}}Create({ hook{{PascalName}}Create }: IProps) {
14
+ const { handleCreate, methods, openCreate, status } = hook{{PascalName}}Create;
15
+
16
+ return (
17
+ <DrawerFiltersComponent title="Cadastrar {{PascalName}}" open={openCreate.value} onClose={openCreate.onFalse} width={500}>
18
+ <>
19
+ <Typography variant="subtitle2">Preencha as informações</Typography>
20
+
21
+ <Form methods={methods} onSubmit={handleCreate}>
22
+ <Grid spacing={2} sx={{ mt: 2 }} container>
23
+ {{FORM_FIELDS_CREATE_TSX}}
24
+ </Grid>
25
+ <Stack spacing={2} alignItems="end" sx={{ mt: 3 }}>
26
+ <Stack spacing={2} flexDirection="row">
27
+ <Button variant="outlined" onClick={openCreate.onFalse}>
28
+ Cancelar
29
+ </Button>
30
+ <LoadingButton variant="outlined" type="submit" loading={status === "pending"} color="success">
31
+ Salvar
32
+ </LoadingButton>
33
+ </Stack>
34
+ </Stack>
35
+ </Form>
36
+ </>
37
+ </DrawerFiltersComponent>
38
+ );
39
+ }
@@ -0,0 +1,31 @@
1
+ import { LoadingButton } from "@mui/lab";
2
+ import { Button, Dialog, Typography, DialogTitle, DialogActions, DialogContent } from "@mui/material";
3
+
4
+ import type use{{PascalName}}Delete from "../hooks/use-{{kebabName}}-delete";
5
+
6
+ interface IProps {
7
+ hook{{PascalName}}Delete: ReturnType<typeof use{{PascalName}}Delete>;
8
+ }
9
+
10
+ export default function {{PascalName}}Delete({ hook{{PascalName}}Delete }: IProps) {
11
+ const { openDelete, selectClickItem, handleDelete, status } = hook{{PascalName}}Delete;
12
+
13
+ return (
14
+ <Dialog open={openDelete.value} onClose={openDelete.onFalse} maxWidth="xs" fullWidth>
15
+ <DialogTitle>Excluir {{PascalName}}</DialogTitle>
16
+ <DialogContent dividers>
17
+ <Typography>
18
+ Você realmente deseja <strong>desativar</strong> <strong>#{selectClickItem?.id}</strong>?
19
+ </Typography>
20
+ </DialogContent>
21
+ <DialogActions>
22
+ <Button onClick={openDelete.onFalse} variant="outlined">
23
+ Cancelar
24
+ </Button>
25
+ <LoadingButton variant="contained" color="error" loading={status === "pending"} onClick={handleDelete}>
26
+ Confirmar
27
+ </LoadingButton>
28
+ </DialogActions>
29
+ </Dialog>
30
+ );
31
+ }
@@ -0,0 +1,56 @@
1
+ import type { I{{PascalName}}Create } from "src/services/system/{{kebabName}}/types";
2
+
3
+ import * as yup from "yup";
4
+ import { useForm } from "react-hook-form";
5
+ import { enqueueSnackbar } from "notistack";
6
+ import { yupResolver } from "@hookform/resolvers/yup";
7
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
8
+
9
+ import { useBoolean } from "src/hooks/use-boolean";
10
+
11
+ import handleApiDataError from "src/utils/handle-api-data-error";
12
+
13
+ import { {{camelName}}Service } from "src/services/system/{{kebabName}}";
14
+
15
+ export default function use{{PascalName}}Create() {
16
+ const api = {{camelName}}Service();
17
+
18
+ const validateForm = yup.object({
19
+ {{YUP_CREATE_RULES}}
20
+ });
21
+
22
+ const openCreate = useBoolean();
23
+ const queryClient = useQueryClient();
24
+
25
+ const methods = useForm<I{{PascalName}}Create>({
26
+ resolver: yupResolver(validateForm),
27
+ defaultValues: {
28
+ {{FORM_DEFAULTS_CREATE}}
29
+ },
30
+ });
31
+
32
+ const { mutate, status } = useMutation({
33
+ mutationFn: api.{{camelName}}Create,
34
+ onSuccess: () => {
35
+ enqueueSnackbar("Cadastrado com sucesso");
36
+ queryClient.invalidateQueries({ queryKey: ["data{{PascalName}}"] });
37
+ openCreate.onFalse();
38
+ methods.reset();
39
+ },
40
+ onError: (err) => {
41
+ handleApiDataError(err, enqueueSnackbar);
42
+ },
43
+ });
44
+
45
+ const handleCreate = methods.handleSubmit((data) => {
46
+ const params: I{{PascalName}}Create = { ...data };
47
+ mutate(params);
48
+ });
49
+
50
+ return {
51
+ status,
52
+ methods,
53
+ handleCreate,
54
+ openCreate,
55
+ };
56
+ }
@@ -0,0 +1,46 @@
1
+ import type { I{{PascalName}} } from "src/types/system/i-{{kebabName}}";
2
+ import type { I{{PascalName}}Delete } from "src/services/system/{{kebabName}}/types";
3
+
4
+ import { useState } from "react";
5
+ import { enqueueSnackbar } from "notistack";
6
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
7
+
8
+ import { useBoolean } from "src/hooks/use-boolean";
9
+
10
+ import handleApiDataError from "src/utils/handle-api-data-error";
11
+
12
+ import { {{camelName}}Service } from "src/services/system/{{kebabName}}";
13
+
14
+ export default function use{{PascalName}}Delete() {
15
+ const api = {{camelName}}Service();
16
+
17
+ const [selectClickItem, setSelectClickItem] = useState<I{{PascalName}}>();
18
+
19
+ const openDelete = useBoolean();
20
+ const queryClient = useQueryClient();
21
+
22
+ const { mutate, status } = useMutation({
23
+ mutationFn: api.{{camelName}}Delete,
24
+ onSuccess: () => {
25
+ enqueueSnackbar("Deletado com sucesso");
26
+ queryClient.invalidateQueries({ queryKey: ["data{{PascalName}}"] });
27
+ openDelete.onFalse();
28
+ },
29
+ onError: (err) => {
30
+ handleApiDataError(err, enqueueSnackbar);
31
+ },
32
+ });
33
+
34
+ const handleDelete = () => {
35
+ const params: I{{PascalName}}Delete = { id: selectClickItem?.id.toString()! };
36
+ mutate(params);
37
+ };
38
+
39
+ return {
40
+ status,
41
+ handleDelete,
42
+ openDelete,
43
+ selectClickItem,
44
+ setSelectClickItem,
45
+ };
46
+ }
@@ -0,0 +1,58 @@
1
+ import type { I{{PascalName}}Get } from "src/services/system/{{kebabName}}/types";
2
+
3
+ import { useState } from "react";
4
+ import { isEqual } from "lodash";
5
+ import { enqueueSnackbar } from "notistack";
6
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
7
+
8
+ import useTable from "src/hooks/use-table";
9
+ import { useBoolean } from "src/hooks/use-boolean";
10
+
11
+ import handleApiDataError from "src/utils/handle-api-data-error";
12
+
13
+ import { {{camelName}}Service } from "src/services/system/{{kebabName}}";
14
+
15
+ import { InitialFilter{{PascalName}} } from "../consts";
16
+
17
+ export default function use{{PascalName}}Index() {
18
+ const api = {{camelName}}Service();
19
+
20
+ const [filter, setFilter] = useState<I{{PascalName}}Get>(InitialFilter{{PascalName}});
21
+
22
+ const table = useTable();
23
+ const openDrawerFilters = useBoolean();
24
+ const queryClient = useQueryClient();
25
+
26
+ const { data: data{{PascalName}}, status: status{{PascalName}} } = useQuery({
27
+ queryKey: ["data{{PascalName}}", table, filter],
28
+ queryFn: async () => {
29
+ try {
30
+ const params: I{{PascalName}}Get = {
31
+ Direction: table.order,
32
+ PageNumber: table.page,
33
+ PageSize: table.pageSize,
34
+ SortBy: table.orderBy,
35
+ {{FILTER_SPREAD_IN_QUERY}}
36
+ };
37
+ const response = await api.{{camelName}}Get(params);
38
+ return response;
39
+ } catch (error) {
40
+ handleApiDataError(error, enqueueSnackbar);
41
+ throw error;
42
+ }
43
+ },
44
+ });
45
+
46
+ const isFilterApplied = !!isEqual(filter, InitialFilter{{PascalName}});
47
+
48
+ return {
49
+ table,
50
+ queryClient,
51
+ data{{PascalName}},
52
+ status{{PascalName}},
53
+ filter,
54
+ openDrawerFilters,
55
+ setFilter,
56
+ isFilterApplied,
57
+ };
58
+ }
@@ -0,0 +1,46 @@
1
+ import type { I{{PascalName}} } from "src/types/system/i-{{kebabName}}";
2
+ import type { I{{PascalName}}Restore } from "src/services/system/{{kebabName}}/types";
3
+
4
+ import { useState } from "react";
5
+ import { enqueueSnackbar } from "notistack";
6
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
7
+
8
+ import { useBoolean } from "src/hooks/use-boolean";
9
+
10
+ import handleApiDataError from "src/utils/handle-api-data-error";
11
+
12
+ import { {{camelName}}Service } from "src/services/system/{{kebabName}}";
13
+
14
+ export default function use{{PascalName}}Restore() {
15
+ const api = {{camelName}}Service();
16
+
17
+ const [selectClickItem, setSelectClickItem] = useState<I{{PascalName}}>();
18
+
19
+ const openRestore = useBoolean();
20
+ const queryClient = useQueryClient();
21
+
22
+ const { mutate, status } = useMutation({
23
+ mutationFn: api.{{camelName}}Restore,
24
+ onSuccess: () => {
25
+ enqueueSnackbar("Restaurado com sucesso");
26
+ queryClient.invalidateQueries({ queryKey: ["data{{PascalName}}"] });
27
+ openRestore.onFalse();
28
+ },
29
+ onError: (err) => {
30
+ handleApiDataError(err, enqueueSnackbar);
31
+ },
32
+ });
33
+
34
+ const handleRestore = () => {
35
+ const params: I{{PascalName}}Restore = { id: selectClickItem?.id.toString()! };
36
+ mutate(params);
37
+ };
38
+
39
+ return {
40
+ status,
41
+ handleRestore,
42
+ openRestore,
43
+ selectClickItem,
44
+ setSelectClickItem,
45
+ };
46
+ }
@@ -0,0 +1,68 @@
1
+ import type { I{{PascalName}} } from "src/types/system/i-{{kebabName}}";
2
+ import type { I{{PascalName}}Update } from "src/services/system/{{kebabName}}/types";
3
+
4
+ import * as yup from "yup";
5
+ import { useState } from "react";
6
+ import { useForm } from "react-hook-form";
7
+ import { enqueueSnackbar } from "notistack";
8
+ import { yupResolver } from "@hookform/resolvers/yup";
9
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
10
+
11
+ import { useBoolean } from "src/hooks/use-boolean";
12
+
13
+ import handleApiDataError from "src/utils/handle-api-data-error";
14
+
15
+ import { {{camelName}}Service } from "src/services/system/{{kebabName}}";
16
+
17
+ export default function use{{PascalName}}Update() {
18
+ const api = {{camelName}}Service();
19
+
20
+ const [selectClickItem, setSelectClickItem] = useState<I{{PascalName}}>();
21
+
22
+ const validateForm = yup.object({
23
+ {{YUP_UPDATE_RULES}}
24
+ });
25
+
26
+ const openUpdate = useBoolean();
27
+ const queryClient = useQueryClient();
28
+
29
+ const methods = useForm<Omit<I{{PascalName}}Update, "id">>({
30
+ resolver: yupResolver(validateForm),
31
+ defaultValues: {
32
+ {{FORM_DEFAULTS_UPDATE}}
33
+ },
34
+ values: {
35
+ {{FORM_VALUES_UPDATE}}
36
+ },
37
+ });
38
+
39
+ const { mutate, status } = useMutation({
40
+ mutationFn: api.{{camelName}}Update,
41
+ onSuccess: () => {
42
+ enqueueSnackbar("Editado com sucesso");
43
+ queryClient.invalidateQueries({ queryKey: ["data{{PascalName}}"] });
44
+ openUpdate.onFalse();
45
+ methods.reset();
46
+ },
47
+ onError: (err) => {
48
+ handleApiDataError(err, enqueueSnackbar);
49
+ },
50
+ });
51
+
52
+ const handleUpdate = methods.handleSubmit((data) => {
53
+ const params: I{{PascalName}}Update = {
54
+ id: selectClickItem?.id.toString()!,
55
+ ...data,
56
+ };
57
+ mutate(params);
58
+ });
59
+
60
+ return {
61
+ status,
62
+ methods,
63
+ handleUpdate,
64
+ openUpdate,
65
+ selectClickItem,
66
+ setSelectClickItem,
67
+ };
68
+ }
@@ -0,0 +1,117 @@
1
+ import dayjs from "dayjs";
2
+
3
+ import { Box, Card, Stack, Button, Tooltip, TableRow, TableCell, ButtonGroup } from "@mui/material";
4
+
5
+ import { Label } from "src/components/label";
6
+ import PageComponent from "src/components/page";
7
+ import { Iconify } from "src/components/iconify";
8
+ import TableComponent from "src/components/table-basic";
9
+ import { CustomBreadcrumbs } from "src/components/custom-breadcrumbs";
10
+
11
+ import {{PascalName}}Create from "./create";
12
+ import {{PascalName}}Delete from "./delete";
13
+ import {{PascalName}}Update from "./update";
14
+ import {{PascalName}}Restore from "./restore";
15
+ import { tableHead{{PascalName}} } from "./consts";
16
+ import use{{PascalName}}Index from "./hooks/use-{{kebabName}}-index";
17
+ import use{{PascalName}}Create from "./hooks/use-{{kebabName}}-create";
18
+ import use{{PascalName}}Delete from "./hooks/use-{{kebabName}}-delete";
19
+ import use{{PascalName}}Update from "./hooks/use-{{kebabName}}-update";
20
+ import use{{PascalName}}Restore from "./hooks/use-{{kebabName}}-restore";
21
+ import {{PascalName}}FiltersComponent from "./components/{{kebabName}}-filters-component";
22
+
23
+ export default function {{PascalName}}IndexPage() {
24
+ const hook{{PascalName}}Index = use{{PascalName}}Index();
25
+ const hook{{PascalName}}Create = use{{PascalName}}Create();
26
+ const hook{{PascalName}}Delete = use{{PascalName}}Delete();
27
+ const hook{{PascalName}}Restore = use{{PascalName}}Restore();
28
+ const hook{{PascalName}}Update = use{{PascalName}}Update();
29
+
30
+ const { table, status{{PascalName}}, data{{PascalName}} } = hook{{PascalName}}Index;
31
+
32
+ return (
33
+ <PageComponent>
34
+ <>
35
+ <CustomBreadcrumbs
36
+ heading="{{PascalName}}"
37
+ links={[{ name: "{{PascalName}}" }, { name: "lista" }]}
38
+ action={
39
+ <Button
40
+ onClick={hook{{PascalName}}Create.openCreate.onTrue}
41
+ startIcon={<Iconify icon="material-symbols:add" />}
42
+ variant="outlined"
43
+ >
44
+ Novo (a)
45
+ </Button>
46
+ }
47
+ sx={{ mb: { xs: 3, md: 5 } }}
48
+ />
49
+
50
+ <Card>
51
+ <TableComponent
52
+ headLabel={tableHead{{PascalName}}}
53
+ table={table}
54
+ loading={status{{PascalName}} === "pending"}
55
+ totalCount={data{{PascalName}}?.totalCount ?? 0}
56
+ order={table.order}
57
+ orderBy={table.orderBy}
58
+ onSort={table.onSort}
59
+ action={
60
+ <{{PascalName}}FiltersComponent hook{{PascalName}}Index={hook{{PascalName}}Index} />
61
+ }
62
+ >
63
+ <>
64
+ {data{{PascalName}}?.data.map((d, k) => (
65
+ <TableRow hover key={k}>
66
+ {{TABLE_ROW_CELLS}}
67
+ <TableCell>
68
+ <ButtonGroup color="inherit" size="small">
69
+ <Tooltip title="Editar dados">
70
+ <Button
71
+ onClick={() => {
72
+ hook{{PascalName}}Update.openUpdate.onTrue();
73
+ hook{{PascalName}}Update.setSelectClickItem(d);
74
+ }}
75
+ >
76
+ <Iconify icon="material-symbols:edit-document-outline" />
77
+ </Button>
78
+ </Tooltip>
79
+ <Tooltip title={d.deletedAt ? "Ativar" : "Desativar"}>
80
+ <Button
81
+ onClick={() => {
82
+ if (d.deletedAt) {
83
+ hook{{PascalName}}Restore.setSelectClickItem(d);
84
+ hook{{PascalName}}Restore.openRestore.onTrue();
85
+ } else {
86
+ hook{{PascalName}}Delete.setSelectClickItem(d);
87
+ hook{{PascalName}}Delete.openDelete.onTrue();
88
+ }
89
+ }}
90
+ >
91
+ <Iconify
92
+ icon={
93
+ d.deletedAt
94
+ ? "material-symbols:upload"
95
+ : "material-symbols:delete-outline-sharp"
96
+ }
97
+ />
98
+ </Button>
99
+ </Tooltip>
100
+ </ButtonGroup>
101
+ </TableCell>
102
+ </TableRow>
103
+ ))}
104
+ </>
105
+ </TableComponent>
106
+ </Card>
107
+
108
+ <Box sx={{ mb: 10 }} />
109
+
110
+ <{{PascalName}}Create hook{{PascalName}}Create={hook{{PascalName}}Create} />
111
+ <{{PascalName}}Delete hook{{PascalName}}Delete={hook{{PascalName}}Delete} />
112
+ <{{PascalName}}Restore hook{{PascalName}}Restore={hook{{PascalName}}Restore} />
113
+ <{{PascalName}}Update hook{{PascalName}}Update={hook{{PascalName}}Update} />
114
+ </>
115
+ </PageComponent>
116
+ );
117
+ }
@@ -0,0 +1,31 @@
1
+ import { LoadingButton } from "@mui/lab";
2
+ import { Dialog, Button, Typography, DialogTitle, DialogContent, DialogActions } from "@mui/material";
3
+
4
+ import type use{{PascalName}}Restore from "../hooks/use-{{kebabName}}-restore";
5
+
6
+ interface IProps {
7
+ hook{{PascalName}}Restore: ReturnType<typeof use{{PascalName}}Restore>;
8
+ }
9
+
10
+ export default function {{PascalName}}Restore({ hook{{PascalName}}Restore }: IProps) {
11
+ const { openRestore, selectClickItem, status, handleRestore } = hook{{PascalName}}Restore;
12
+
13
+ return (
14
+ <Dialog open={openRestore.value} onClose={openRestore.onFalse} maxWidth="xs" fullWidth>
15
+ <DialogTitle>Restaurar {{PascalName}}</DialogTitle>
16
+ <DialogContent dividers>
17
+ <Typography>
18
+ Você realmente deseja <strong>restaurar</strong> <strong>#{selectClickItem?.id}</strong>?
19
+ </Typography>
20
+ </DialogContent>
21
+ <DialogActions>
22
+ <Button onClick={openRestore.onFalse} variant="outlined">
23
+ Cancelar
24
+ </Button>
25
+ <LoadingButton variant="contained" color="success" loading={status === "pending"} onClick={handleRestore}>
26
+ Confirmar
27
+ </LoadingButton>
28
+ </DialogActions>
29
+ </Dialog>
30
+ );
31
+ }
@@ -0,0 +1,39 @@
1
+ import { LoadingButton } from "@mui/lab";
2
+ import { Grid, Stack, Button, Typography } from "@mui/material";
3
+
4
+ import { Form, Field } from "src/components/hook-form";
5
+ import DrawerFiltersComponent from "src/components/drawer-filters";
6
+
7
+ import type use{{PascalName}}Update from "../hooks/use-{{kebabName}}-update";
8
+
9
+ interface IProps {
10
+ hook{{PascalName}}Update: ReturnType<typeof use{{PascalName}}Update>;
11
+ }
12
+
13
+ export default function {{PascalName}}Update({ hook{{PascalName}}Update }: IProps) {
14
+ const { handleUpdate, methods, openUpdate, status } = hook{{PascalName}}Update;
15
+
16
+ return (
17
+ <DrawerFiltersComponent title="Editar {{PascalName}}" open={openUpdate.value} onClose={openUpdate.onFalse} width={500}>
18
+ <>
19
+ <Typography variant="subtitle2">Atualize as informações</Typography>
20
+
21
+ <Form methods={methods} onSubmit={handleUpdate}>
22
+ <Grid spacing={2} sx={{ mt: 2 }} container>
23
+ {{FORM_FIELDS_UPDATE_TSX}}
24
+ </Grid>
25
+ <Stack spacing={2} alignItems="end" sx={{ mt: 3 }}>
26
+ <Stack spacing={2} flexDirection="row">
27
+ <Button variant="outlined" onClick={openUpdate.onFalse}>
28
+ Cancelar
29
+ </Button>
30
+ <LoadingButton variant="outlined" type="submit" loading={status === "pending"} color="success">
31
+ Salvar
32
+ </LoadingButton>
33
+ </Stack>
34
+ </Stack>
35
+ </Form>
36
+ </>
37
+ </DrawerFiltersComponent>
38
+ );
39
+ }
@@ -0,0 +1,33 @@
1
+ import type { I{{PascalName}} } from "src/types/system/i-{{kebabName}}";
2
+ import type { IPagination } from "src/types/default/i-pagination";
3
+
4
+ import { API_BASE } from "src/services/api";
5
+
6
+ import type { I{{PascalName}}Get, I{{PascalName}}Create, I{{PascalName}}Update, I{{PascalName}}Delete, I{{PascalName}}GetById, I{{PascalName}}Restore } from "./types";
7
+
8
+ export const {{camelName}}Service = () => ({
9
+ {{camelName}}Get: async (params: I{{PascalName}}Get): Promise<IPagination<I{{PascalName}}>> => {
10
+ const response = await API_BASE.get(`{{route}}`, { params });
11
+ return response.data;
12
+ },
13
+ {{camelName}}Create: async (params: I{{PascalName}}Create): Promise<void> => {
14
+ const response = await API_BASE.post(`{{route}}`, params);
15
+ return response.data;
16
+ },
17
+ {{camelName}}GetById: async (params: I{{PascalName}}GetById): Promise<I{{PascalName}}> => {
18
+ const response = await API_BASE.get(`{{route}}/${params.id}`);
19
+ return response.data;
20
+ },
21
+ {{camelName}}Update: async (params: I{{PascalName}}Update): Promise<void> => {
22
+ const response = await API_BASE.put(`{{route}}/${params.id}`);
23
+ return response.data;
24
+ },
25
+ {{camelName}}Delete: async (params: I{{PascalName}}Delete): Promise<void> => {
26
+ const response = await API_BASE.delete(`{{route}}/${params.id}`);
27
+ return response.data;
28
+ },
29
+ {{camelName}}Restore: async (params: I{{PascalName}}Restore): Promise<void> => {
30
+ const response = await API_BASE.put(`{{route}}/${params.id}`);
31
+ return response.data;
32
+ },
33
+ });
@@ -0,0 +1,35 @@
1
+ import type { IPaginationRequest } from "src/types/default/i-pagination";
2
+
3
+ interface I{{PascalName}}Get extends IPaginationRequest {
4
+ {{FILTERS_INTERFACE}}
5
+ }
6
+
7
+ interface I{{PascalName}}Create {
8
+ {{CREATE_FIELDS}}
9
+ }
10
+
11
+ interface I{{PascalName}}GetById {
12
+ id: string;
13
+ }
14
+
15
+ interface I{{PascalName}}Update {
16
+ id: string;
17
+ {{UPDATE_FIELDS}}
18
+ }
19
+
20
+ interface I{{PascalName}}Delete {
21
+ id: string;
22
+ }
23
+
24
+ interface I{{PascalName}}Restore {
25
+ id: string;
26
+ }
27
+
28
+ export type {
29
+ I{{PascalName}}Get,
30
+ I{{PascalName}}Create,
31
+ I{{PascalName}}Update,
32
+ I{{PascalName}}Delete,
33
+ I{{PascalName}}GetById,
34
+ I{{PascalName}}Restore,
35
+ }
@@ -0,0 +1,7 @@
1
+ interface I{{PascalName}} {
2
+ {{FIELDS_INTERFACE}}
3
+ }
4
+
5
+ export type {
6
+ I{{PascalName}}
7
+ }