diff --git a/apps/infoalloggi/kanel.config.js b/apps/infoalloggi/kanel.config.js index f8da207..bff5b0f 100644 --- a/apps/infoalloggi/kanel.config.js +++ b/apps/infoalloggi/kanel.config.js @@ -1,5 +1,6 @@ const { makeKyselyHook } = require("kanel-kysely"); const { makePgTsGenerator } = require("kanel"); +const { generateZodSchemas, makeGenerateZodSchemas } = require("kanel-zod"); const ignoredTables = ["schema_migrations"]; /** @@ -42,6 +43,62 @@ const specificTypes = (mappings) => { }; } +const kanelZodCastRegex = /as unknown as z.Schema<(.*?)(Mutator|Initializer)>/; +const zodItemInitializer = /^export const \w+ = z\.object/ + +/** + * @type {import("kanel").PostRenderHook} + * + * Renames the type of the `as unknown as z.Schema` casts from `kanel-zod` to + * to be compatible with `kanel-kysely`, turning + * 1. `as unknown as z.Schema` into `as unknown as z.Schema` + * 2. `as unknown as z.Schema` into `as unknown as z.Schema` + */ +function kanelKyselyZodCompatibilityHook(path, lines) { + + return lines.map((line) => { + if (zodItemInitializer.test(line)) { + const parts = line.split(" "); + if (parts[2].includes("Initializer")) { + const nome = parts[2].replace("Initializer", ""); + parts[2] = "New" + nome[0].toUpperCase() + nome.slice(1) + "Schema"; + return parts.join(" "); + } + if (parts[2].includes("Mutator")) { + const nome = parts[2].replace("Mutator", ""); + parts[2] = nome[0].toUpperCase() + nome.slice(1) + "UpdateSchema"; + return parts.join(" "); + } + const nome = parts[2]; + parts[2] = nome[0].toUpperCase() + nome.slice(1) + "Schema"; + return parts.join(" "); + } + + if (line.includes("as unknown as z.Schema")) { + const replacedLine = line.replace( + kanelZodCastRegex, + (_, typeName, mutatorOrInitializer) => { + + if (!mutatorOrInitializer) { + return `as unknown as z.Schema<${typeName}>`; + } + + if (mutatorOrInitializer === "Mutator") { + return `as unknown as z.Schema<${typeName}Update>`; + } + + return `as unknown as z.Schema`; + } + ); + return replacedLine; + } + + + + return line; + }); +} + /** @type {import('kanel').Config} */ module.exports = { connection: { @@ -53,27 +110,32 @@ module.exports = { }, filter: (pgTable) => !ignoredTables.includes(pgTable.name), outputPath: "./src/schemas", - generators: [makePgTsGenerator({ - customTypeMap: { - "pg_catalog.tsvector": "string", - "pg_catalog.bpchar": "string", - 'pg_catalog.numeric': 'number', - //"pg_catalog.json": "string", + generators: [ + makePgTsGenerator({ + customTypeMap: { + "pg_catalog.tsvector": "string", + "pg_catalog.bpchar": "string", + 'pg_catalog.numeric': 'number', + //"pg_catalog.json": "string", - }, - preRenderHooks: [ + }, + preRenderHooks: [ - specificTypes({ - "Annunci.caratteristiche": { - name: "Caratteristiche", - typeImports: [{ name: "Caratteristiche", path: "src/utils/kanel-types.ts", importAsType: true, isAbsolute: false, isDefault: false }], - }, - }), - makeKyselyHook(), - ], + specificTypes({ + "Annunci.caratteristiche": { + name: "Caratteristiche", + typeImports: [{ name: "Caratteristiche", path: "src/utils/kanel-types.ts", importAsType: true, isAbsolute: false, isDefault: false }], + }, + }), + makeKyselyHook(), + makeGenerateZodSchemas({ + castToSchema: false + }), + ], - }), + }), ], + postRenderHooks: [kanelKyselyZodCompatibilityHook], preDeleteOutputFolder: true, diff --git a/apps/infoalloggi/package-lock.json b/apps/infoalloggi/package-lock.json index 6a2c3a0..e03ee2f 100644 --- a/apps/infoalloggi/package-lock.json +++ b/apps/infoalloggi/package-lock.json @@ -116,6 +116,7 @@ "jsdom": "^29.0.2", "kanel": "^4.0.1", "kanel-kysely": "^4.0.0", + "kanel-zod": "^4.0.0", "knip": "^6.4.1", "npm-run-all": "^4.1.5", "react-email": "^5.2.10", @@ -9089,6 +9090,16 @@ "@kristiandupont/recase": "^1.2.1" } }, + "node_modules/kanel-zod": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kanel-zod/-/kanel-zod-4.0.0.tgz", + "integrity": "sha512-F2rLVeNDuoccjRtR5xW3iMJSwjdhyxVcmmoc7H5AXF8QB0uT+syMIrsrrKeN/x4+BLS39myYF0nRpUvOiahs9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kristiandupont/recase": "^1.2.1" + } + }, "node_modules/kleur": { "version": "3.0.3", "dev": true, diff --git a/apps/infoalloggi/package.json b/apps/infoalloggi/package.json index e754805..726ae92 100644 --- a/apps/infoalloggi/package.json +++ b/apps/infoalloggi/package.json @@ -131,6 +131,7 @@ "jsdom": "^29.0.2", "kanel": "^4.0.1", "kanel-kysely": "^4.0.0", + "kanel-zod": "^4.0.0", "knip": "^6.4.1", "npm-run-all": "^4.1.5", "react-email": "^5.2.10", diff --git a/apps/infoalloggi/src/i18n/en.ts b/apps/infoalloggi/src/i18n/en.ts index 3b5f545..465a8d2 100644 --- a/apps/infoalloggi/src/i18n/en.ts +++ b/apps/infoalloggi/src/i18n/en.ts @@ -675,6 +675,7 @@ export const en: LangDict = { items: [ { href: "/area-riservata/admin/etichette", title: "Labels" }, { href: "/area-riservata/admin/flags", title: "Flags" }, + { href: "/area-riservata/admin/catasto", title: "Catasto" }, { href: "/area-riservata/admin/banners", title: "Banners" }, { href: "/area-riservata/admin/testi-stringhe", diff --git a/apps/infoalloggi/src/i18n/it.ts b/apps/infoalloggi/src/i18n/it.ts index c409c5c..1223cca 100644 --- a/apps/infoalloggi/src/i18n/it.ts +++ b/apps/infoalloggi/src/i18n/it.ts @@ -680,6 +680,7 @@ export const it: LangDict = { items: [ { href: "/area-riservata/admin/etichette", title: "Etichette" }, { href: "/area-riservata/admin/flags", title: "Flags" }, + { href: "/area-riservata/admin/catasto", title: "Catasto" }, { href: "/area-riservata/admin/banners", title: "Banners" }, { href: "/area-riservata/admin/testi-stringhe", diff --git a/apps/infoalloggi/src/pages/area-riservata/admin/catasto.tsx b/apps/infoalloggi/src/pages/area-riservata/admin/catasto.tsx new file mode 100644 index 0000000..116dca8 --- /dev/null +++ b/apps/infoalloggi/src/pages/area-riservata/admin/catasto.tsx @@ -0,0 +1,1022 @@ +import type { ColumnDef } from "@tanstack/react-table"; +import type { GetServerSideProps } from "next"; +import Head from "next/head"; +import { useState } from "react"; +import toast from "react-hot-toast"; +import z from "zod"; +import { Confirm } from "~/components/confirm"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "~/components/custom_ui/form"; +import { AreaRiservataLayout } from "~/components/Layout"; +import { LoadingPage } from "~/components/loading"; +import { Button } from "~/components/ui/button"; +import { DataTable } from "~/components/ui/data-table"; +import { DataTableColumnHeader } from "~/components/ui/dataTable-header"; +import type { SearchFiltro } from "~/components/ui/dataTable-toolbar"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "~/components/ui/dialog"; +import Input from "~/components/ui/input"; +import { useZodForm } from "~/lib/zodForm"; +import type { NextPageWithLayout } from "~/pages/_app"; +import { + type Comuni, + type ComuniId, + ComuniSchema, +} from "~/schemas/public/Comuni"; +import { + type Nazioni, + type NazioniId, + NazioniSchema, +} from "~/schemas/public/Nazioni"; +import { + type Provincie, + type ProvincieId, + ProvincieSchema, +} from "~/schemas/public/Provincie"; + +import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper"; +import { api } from "~/utils/api"; + +/** + * Pagina di gestione catasto per admin: /area-riservata/admin/catasto + */ +const AdminCatasto: NextPageWithLayout = () => { + return ( + <> + + Catasto - Infoalloggi.it + +
+
+
+
+

+ Catasto +

+
+
+
+ + + +
+
+
+ + ); +}; +export default AdminCatasto; + +export const getServerSideProps = (async (context) => { + const access_token = context.req.cookies.access_token; + + const helpers = await TrpcAuthedFetchingIstance({ access_token }); + if (helpers) { + await helpers.trpc.catasto.getComuni.prefetch(); + return { + props: { + trpcState: helpers.trpc.dehydrate(), + }, + }; + } + return { + props: {}, + }; +}) satisfies GetServerSideProps; + +AdminCatasto.getLayout = function getLayout(page) { + return {page}; +}; + +const SezioneComuni = () => { + const utils = api.useUtils(); + const { data, isLoading } = api.catasto.getComuni.useQuery(); + const [newOpened, setNewOpened] = useState(false); + const { mutate: createComune } = api.catasto.createComune.useMutation({ + onSuccess: async () => { + toast.success("Comune creato con successo"); + await utils.catasto.getComuni.invalidate(); + setNewOpened(false); + }, + onError: async (error) => { + toast.error( + error instanceof Error ? error.message : "Errore sconosciuto", + ); + }, + }); + const [editOpened, setEditOpened] = useState(false); + const [selectedComune, setSelectedComune] = useState(null); + const { mutate: editComune } = api.catasto.editComune.useMutation({ + onSuccess: async () => { + toast.success("Comune modificato con successo"); + await utils.catasto.getComuni.invalidate(); + }, + onError: async (error) => { + toast.error( + error instanceof Error ? error.message : "Errore sconosciuto", + ); + }, + }); + const { mutate: deleteComune } = api.catasto.deleteComune.useMutation({ + onSuccess: async () => { + toast.success("Comune eliminato con successo"); + setEditOpened(false); + setSelectedComune(null); + await utils.catasto.getComuni.invalidate(); + }, + onError: async (error) => { + toast.error( + error instanceof Error ? error.message : "Errore sconosciuto", + ); + }, + }); + + if (isLoading) return ; + if (!data) return
Errore nel caricamento dei comuni
; + const columns_titles = { + id: "Codice", + nome: "Nome", + sigla: "Provincia", + catasto: "Catasto", + cap: "CAP", + action: "", + }; + type Column = (typeof data)[number]; + + const columns: ColumnDef[] = [ + { + accessorKey: "id", + enableHiding: false, + header: ({ column }) => ( + + ), + }, + { + accessorKey: "nome", + enableHiding: false, + header: ({ column }) => ( + + ), + }, + { + accessorKey: "sigla", + enableHiding: false, + header: ({ column }) => ( + + ), + }, + { + accessorKey: "catasto", + enableHiding: false, + header: ({ column }) => ( + + ), + }, + { + accessorKey: "cap", + enableHiding: false, + header: ({ column }) => ( + + ), + }, + { + id: "action", + cell: ({ row }) => ( + + ), + header: ({ column }) => ( + + ), + enableSorting: false, + enableHiding: false, + }, + ]; + + const searchFiltro: SearchFiltro = { + columnName: "nome", + placeholder: "Cerca ...", + }; + return ( +
+
+

Comuni

+ + + + + + + + Nuovo Comune + desc + + { + createComune(values); + }} + /> + + +
+
+ +
+ {selectedComune && ( + { + setEditOpened(v); + if (!v) setTimeout(() => setSelectedComune(null), 500); + }} + open={editOpened} + > + + + Modifica Comune + desc + + + deleteComune({ + id: selectedComune.id, + }) + } + initialValues={selectedComune} + submitMutation={(values) => { + editComune({ + id: values.id, + data: values, + }); + }} + /> + + + )} +
+ ); +}; + +const FormComuni = ({ + initialValues, + submitMutation, + handleRemove, +}: { + initialValues: Comuni | null; + submitMutation: (values: Comuni) => void; + handleRemove?: (id: ComuniId) => void; +}) => { + const Schema = ComuniSchema; + type FormValues = z.infer; + + let defaultValues: FormValues; + // This can come from your database or API. + if (!initialValues) { + defaultValues = { + id: 0 as ComuniId, + nome: "", + cap: null, + sigla: "", + catasto: "", + }; + } else { + defaultValues = initialValues; + } + + z.config(z.locales.it()); + + const form = useZodForm(Schema, { + defaultValues: defaultValues, + }); + + function onSubmit(fields: FormValues) { + submitMutation({ ...fields, cap: fields.cap === "" ? null : fields.cap }); + } + + return ( + <> +
+ + ( + +
+ Codice + +
+ + {initialValues ? ( + + ) : ( + Nuovo Comune + )} + +
+ )} + /> + ( + +
+ Nome + +
+ + + +
+ )} + /> + ( + +
+ Provincia + +
+ + + +
+ )} + /> + ( + +
+ Catasto + +
+ + + +
+ )} + /> + ( + +
+ CAP + +
+ + + +
+ )} + /> +
+ + {initialValues?.id && handleRemove && ( + handleRemove(initialValues.id)} + title="Sei sicuro di voler eliminare questo comune?" + > + + + )} +
+ + + + ); +}; + +const SezioneProvincie = () => { + const utils = api.useUtils(); + const { data, isLoading } = api.catasto.getProvincie.useQuery(); + const [newOpened, setNewOpened] = useState(false); + const { mutate: createProvincia } = api.catasto.createProvincia.useMutation({ + onSuccess: async () => { + toast.success("Provincia creata con successo"); + await utils.catasto.getProvincie.invalidate(); + setNewOpened(false); + }, + onError: async (error) => { + toast.error( + error instanceof Error ? error.message : "Errore sconosciuto", + ); + }, + }); + const [editOpened, setEditOpened] = useState(false); + const [selectedProvincia, setSelectedProvincia] = useState( + null, + ); + const { mutate: editProvincia } = api.catasto.editProvincia.useMutation({ + onSuccess: async () => { + toast.success("Provincia modificata con successo"); + await utils.catasto.getProvincie.invalidate(); + }, + onError: async (error) => { + toast.error( + error instanceof Error ? error.message : "Errore sconosciuto", + ); + }, + }); + const { mutate: deleteProvincia } = api.catasto.deleteProvincia.useMutation({ + onSuccess: async () => { + toast.success("Provincia eliminata con successo"); + setEditOpened(false); + setSelectedProvincia(null); + await utils.catasto.getProvincie.invalidate(); + }, + onError: async (error) => { + toast.error( + error instanceof Error ? error.message : "Errore sconosciuto", + ); + }, + }); + if (isLoading) return ; + if (!data) return
Errore nel caricamento delle provincie
; + const columns_titles = { + id: "Codice", + nome: "Nome", + sigla: "Provincia", + action: "", + }; + type Column = (typeof data)[number]; + + const columns: ColumnDef[] = [ + { + accessorKey: "id", + enableHiding: false, + header: ({ column }) => ( + + ), + }, + { + accessorKey: "nome", + enableHiding: false, + header: ({ column }) => ( + + ), + }, + { + accessorKey: "sigla", + enableHiding: false, + header: ({ column }) => ( + + ), + }, + { + id: "action", + cell: ({ row }) => ( + + ), + header: ({ column }) => ( + + ), + enableSorting: false, + enableHiding: false, + }, + ]; + + const searchFiltro: SearchFiltro = { + columnName: "nome", + placeholder: "Cerca ...", + }; + return ( +
+
+

Provincie

+ + + + + + + + Nuova Provincia + desc + + { + createProvincia(values); + }} + /> + + +
+
+ +
+ {selectedProvincia && ( + { + setEditOpened(v); + if (!v) setTimeout(() => setSelectedProvincia(null), 500); + }} + open={editOpened} + > + + + Modifica Provincia + desc + + + deleteProvincia({ + id: selectedProvincia.id, + }) + } + initialValues={selectedProvincia} + submitMutation={(values) => { + editProvincia({ + id: values.id, + data: values, + }); + }} + /> + + + )} +
+ ); +}; + +const FormProvincie = ({ + initialValues, + submitMutation, + handleRemove, +}: { + initialValues: Provincie | null; + submitMutation: (values: Provincie) => void; + handleRemove?: (id: ProvincieId) => void; +}) => { + const Schema = ProvincieSchema; + type FormValues = z.infer; + + let defaultValues: FormValues; + // This can come from your database or API. + if (!initialValues) { + defaultValues = { + id: 0 as ProvincieId, + nome: "", + sigla: "", + }; + } else { + defaultValues = initialValues; + } + + z.config(z.locales.it()); + + const form = useZodForm(Schema, { + defaultValues: defaultValues, + }); + + function onSubmit(fields: FormValues) { + submitMutation(fields); + } + + return ( + <> +
+ + ( + +
+ Codice + +
+ + {initialValues ? ( + + ) : ( + Nuova Provincia + )} + +
+ )} + /> + ( + +
+ Nome + +
+ + + +
+ )} + /> + ( + +
+ Provincia + +
+ + + +
+ )} + /> + +
+ + {initialValues?.id && handleRemove && ( + handleRemove(initialValues.id)} + title="Sei sicuro di voler eliminare questa provincia?" + > + + + )} +
+ + + + ); +}; + +const SezioneNazioni = () => { + const utils = api.useUtils(); + const { data, isLoading } = api.catasto.getNazioni.useQuery(); + const [newOpened, setNewOpened] = useState(false); + const { mutate: createNazione } = api.catasto.createNazione.useMutation({ + onSuccess: async () => { + toast.success("Nazione creata con successo"); + await utils.catasto.getNazioni.invalidate(); + setNewOpened(false); + }, + onError: async (error) => { + toast.error( + error instanceof Error ? error.message : "Errore sconosciuto", + ); + }, + }); + const [editOpened, setEditOpened] = useState(false); + const [selectedNazione, setSelectedNazione] = useState(null); + const { mutate: editNazione } = api.catasto.editNazione.useMutation({ + onSuccess: async () => { + toast.success("Nazione modificata con successo"); + await utils.catasto.getNazioni.invalidate(); + }, + onError: async (error) => { + toast.error( + error instanceof Error ? error.message : "Errore sconosciuto", + ); + }, + }); + const { mutate: deleteNazione } = api.catasto.deleteNazione.useMutation({ + onSuccess: async () => { + toast.success("Nazione eliminata con successo"); + setEditOpened(false); + setSelectedNazione(null); + await utils.catasto.getNazioni.invalidate(); + }, + onError: async (error) => { + toast.error( + error instanceof Error ? error.message : "Errore sconosciuto", + ); + }, + }); + if (isLoading) return ; + if (!data) return
Errore nel caricamento delle nazioni
; + const columns_titles = { + id: "Codice", + nome: "Nome", + catasto: "Catasto", + iso: "ISO", + action: "", + }; + type Column = (typeof data)[number]; + + const columns: ColumnDef[] = [ + { + accessorKey: "id", + enableHiding: false, + header: ({ column }) => ( + + ), + }, + { + accessorKey: "nome", + enableHiding: false, + header: ({ column }) => ( + + ), + }, + { + accessorKey: "catasto", + enableHiding: false, + header: ({ column }) => ( + + ), + }, + { + accessorKey: "iso", + enableHiding: false, + header: ({ column }) => ( + + ), + }, + { + id: "action", + cell: ({ row }) => ( + + ), + header: ({ column }) => ( + + ), + enableSorting: false, + enableHiding: false, + }, + ]; + + const searchFiltro: SearchFiltro = { + columnName: "nome", + placeholder: "Cerca ...", + }; + return ( +
+
+

Nazioni

+ + + + + + + + Nuova Nazione + desc + + { + createNazione(values); + }} + /> + + +
+
+ +
+ {selectedNazione && ( + { + setEditOpened(v); + if (!v) setTimeout(() => setSelectedNazione(null), 500); + }} + open={editOpened} + > + + + Modifica Nazione + desc + + + deleteNazione({ + id: selectedNazione.id, + }) + } + initialValues={selectedNazione} + submitMutation={(values) => { + editNazione({ + id: values.id, + data: values, + }); + }} + /> + + + )} +
+ ); +}; + +const FormNazioni = ({ + initialValues, + submitMutation, + handleRemove, +}: { + initialValues: Nazioni | null; + submitMutation: (values: Nazioni) => void; + handleRemove?: (id: NazioniId) => void; +}) => { + const Schema = NazioniSchema; + type FormValues = z.infer; + + let defaultValues: FormValues; + // This can come from your database or API. + if (!initialValues) { + defaultValues = { + id: 0 as NazioniId, + nome: "", + catasto: "", + iso: null, + }; + } else { + defaultValues = initialValues; + } + + z.config(z.locales.it()); + + const form = useZodForm(Schema, { + defaultValues: defaultValues, + }); + + function onSubmit(fields: FormValues) { + submitMutation({ ...fields, iso: fields.iso !== "" ? fields.iso : null }); + } + + return ( + <> +
+ + ( + +
+ Codice + +
+ + {initialValues ? ( + + ) : ( + Nuova Provincia + )} + +
+ )} + /> + ( + +
+ Nome + +
+ + + +
+ )} + /> + ( + +
+ Catasto + +
+ + + +
+ )} + /> + ( + +
+ ISO + +
+ + + +
+ )} + /> + +
+ + {initialValues?.id && handleRemove && ( + handleRemove(initialValues.id)} + title="Sei sicuro di voler eliminare questa nazione?" + > + + + )} +
+ + + + ); +}; diff --git a/apps/infoalloggi/src/pages/area-riservata/admin/flags.tsx b/apps/infoalloggi/src/pages/area-riservata/admin/flags.tsx index 0f37cb4..63dd234 100644 --- a/apps/infoalloggi/src/pages/area-riservata/admin/flags.tsx +++ b/apps/infoalloggi/src/pages/area-riservata/admin/flags.tsx @@ -50,7 +50,7 @@ const AdminFlags: NextPageWithLayout = () => {

- Impostazioni + Flags