diff --git a/apps/db/migrations/nulla_fatto_flag.up.sql b/apps/db/migrations/nulla_fatto_flag.up.sql
new file mode 100644
index 0000000..9325b17
--- /dev/null
+++ b/apps/db/migrations/nulla_fatto_flag.up.sql
@@ -0,0 +1,3 @@
+-- nulla_fatto_flag.up.sql
+ALTER TABLE IF EXISTS public.servizio
+ ADD COLUMN IF NOT EXISTS "isDisabled" boolean NOT NULL DEFAULT false;
\ No newline at end of file
diff --git a/apps/infoalloggi/Dockerfile b/apps/infoalloggi/Dockerfile
index fd4d48a..349826b 100644
--- a/apps/infoalloggi/Dockerfile
+++ b/apps/infoalloggi/Dockerfile
@@ -70,8 +70,6 @@ ARG EXP_API_USER
ENV EXP_API_USER=$EXP_API_USER
ARG EXP_API_PASS
ENV EXP_API_PASS=$EXP_API_PASS
-ARG TILES_URL
-ENV TILES_URL=$TILES_URL
ARG SKEBBY_USER
ENV SKEBBY_USER=$SKEBBY_USER
ARG SKEBBY_PASS
diff --git a/apps/infoalloggi/src/components/servizio/parametri_ricerca.tsx b/apps/infoalloggi/src/components/servizio/parametri_ricerca.tsx
index bb36a80..65a19d4 100644
--- a/apps/infoalloggi/src/components/servizio/parametri_ricerca.tsx
+++ b/apps/infoalloggi/src/components/servizio/parametri_ricerca.tsx
@@ -798,6 +798,7 @@ export const EditServizioModal = () => {
disabled={
!isAdmin &&
(servizio.isInterrotto ||
+ servizio.isDisabled ||
servizio.annunci.some((a) => a.status_conferma !== null))
}
variant="secondary"
diff --git a/apps/infoalloggi/src/components/servizio/servizio.tsx b/apps/infoalloggi/src/components/servizio/servizio.tsx
index 2ed3126..0ee7060 100644
--- a/apps/infoalloggi/src/components/servizio/servizio.tsx
+++ b/apps/infoalloggi/src/components/servizio/servizio.tsx
@@ -11,6 +11,7 @@ import {
ExternalLink,
type LucideIcon,
PackageCheck,
+ SearchSlash,
SearchX,
Timer,
Trash2,
@@ -161,6 +162,9 @@ const ServizioLinkCard = ({
if (servizio.isInterrotto) {
status = "interrotto";
StatusIcon = SearchX;
+ } else if (servizio.isDisabled) {
+ status = "disabled";
+ StatusIcon = SearchSlash;
} else if (!servizio.isOkAcconto) {
status = "in_attesa";
StatusIcon = ClockFading;
@@ -223,6 +227,7 @@ const ServizioLinkCard = ({
className={cn(
status === "attivo" && "border-green-500",
status === "interrotto" && "border-red-500",
+ status === "disabled" && "border-gray-500",
status === "scaduto" && "border-gray-500",
status === "completato" && "border-blue-500",
"transition-shadow",
@@ -243,6 +248,7 @@ const ServizioLinkCard = ({
"flex items-center gap-2",
status === "attivo" && "text-green-500",
status === "interrotto" && "text-red-500",
+ status === "disabled" && "text-gray-500",
status === "scaduto" && "text-gray-500",
status === "completato" && "text-blue-500",
)}
@@ -436,6 +442,61 @@ const Main = () => {
);
}
+ if (servizio.isDisabled) {
+ if (isAdmin) {
+ return (
+ <>
+
+
+
+ Servizio Disattivato
+
+
+
+ {servizio.annunci.map((data) => {
+ return (
+
+
+
+ );
+ })}
+ {servizio.annunci.length > 1 &&
+ servizio.annunci.length % 2 === 1 && (
+
+ )}
+
+ >
+ );
+ }
+ return (
+
+
+
+
+
+
+ {t.servizio.servizio_disattivato}
+
+
+ {t.servizio.servizio_disattivato_desc}
+
+
+
+
+
+
+ );
+ }
// mostra onboarding
if (!servizio.onboardOk) {
return (
diff --git a/apps/infoalloggi/src/components/servizio/servizio_actions.tsx b/apps/infoalloggi/src/components/servizio/servizio_actions.tsx
index fa7a246..05fc836 100644
--- a/apps/infoalloggi/src/components/servizio/servizio_actions.tsx
+++ b/apps/infoalloggi/src/components/servizio/servizio_actions.tsx
@@ -1,5 +1,4 @@
import {
- Bug,
Check,
Cog,
Euro,
@@ -7,7 +6,6 @@ import {
Logs,
Plus,
Search,
- Trash2,
} from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -27,11 +25,6 @@ import { LoadingPage } from "~/components/loading";
import { InterruzioneServizio } from "~/components/servizio/interruzione";
import { EditServizioModal } from "~/components/servizio/parametri_ricerca";
import { Button } from "~/components/ui/button";
-import {
- Collapsible,
- CollapsibleContent,
- CollapsibleTrigger,
-} from "~/components/ui/collapsible";
import {
Dialog,
DialogContent,
@@ -568,33 +561,11 @@ const EditServizioAdmin = () => {
-
-
-
-
-
-
-
- {(() => {
- const { annunci, ...rest } = servizio;
- return {JSON.stringify(rest, null, 2)};
- })()}
-
-
- {
- remove({ servizioId });
- }}
- title="Elimina servizio"
- >
-
-
+ remove({ servizioId })}
+ initialData={servizio}
+ onSubmit={onSubmit}
+ />
diff --git a/apps/infoalloggi/src/components/tables/richieste-table.tsx b/apps/infoalloggi/src/components/tables/richieste-table.tsx
index 816dba9..35c6341 100644
--- a/apps/infoalloggi/src/components/tables/richieste-table.tsx
+++ b/apps/infoalloggi/src/components/tables/richieste-table.tsx
@@ -17,6 +17,7 @@ import {
ExternalLink,
Logs,
ReceiptEuro,
+ SearchSlash,
SearchX,
UserCog,
} from "lucide-react";
@@ -75,6 +76,7 @@ type RichiesteTableProps = {
const StatusMapping = {
attesa_attivazione: "Attesa di attivazione",
interrotto: "Interrotto",
+ disabled: "Disattivato",
onboarding: "Onboarding",
scaduto: "Scaduto",
consulenza_da_pagare: "Consulenza da pagare",
@@ -93,6 +95,9 @@ function statusParser(data: RichiesteData): keyof typeof StatusMapping {
if (data.isInterrotto) {
return "interrotto";
}
+ if (data.isDisabled) {
+ return "disabled";
+ }
if (!data.onboardOk) {
return "onboarding";
}
@@ -380,6 +385,9 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => {
} else if (row.original.isInterrotto) {
decorrenzaColor = "text-destructive";
decorrenzaTooltip = "Servizio interrotto";
+ } else if (row.original.isDisabled) {
+ decorrenzaColor = "text-muted-foreground";
+ decorrenzaTooltip = "Servizio disattivato";
} else if (row.original.isOkAcconto && !row.original.isOkSaldo) {
decorrenzaColor = "text-warning";
decorrenzaTooltip = "Attesa di saldo";
@@ -452,6 +460,10 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => {
"border-destructive bg-destructive/10 text-destructive";
icon = ;
break;
+ case "disabled":
+ badgeClass = "border-gray-500 bg-muted/10 text-muted-foreground";
+ icon = ;
+ break;
case "onboarding":
badgeClass = "border-pink-500 bg-pink-500/10 text-pink-600";
icon = (
@@ -543,6 +555,7 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => {
accessorKey: "interruzione_expires_at",
cell: ({ row }) => {
if (row.original.isInterrotto) return "---";
+ if (row.original.isDisabled) return "---";
if (!row.original.decorrenza) return "---";
const expiresAt = add(row.original.decorrenza, {
days: row.original.interruzioneDays,
diff --git a/apps/infoalloggi/src/env.ts b/apps/infoalloggi/src/env.ts
index cda98e3..a9ceba3 100644
--- a/apps/infoalloggi/src/env.ts
+++ b/apps/infoalloggi/src/env.ts
@@ -22,7 +22,6 @@ export const env = createEnv({
POSTGRES_USER: z.string(),
STRIPE_SECRET_KEY: z.string(),
STRIPE_WEBHOOK_SECRET: z.string(),
- TILES_URL: z.string(),
SKEBBY_USER: z.string(),
SKEBBY_PASS: z.string(),
REVALIDATION_SECRET: z.string(),
diff --git a/apps/infoalloggi/src/forms/FormEditServizioAdmin.tsx b/apps/infoalloggi/src/forms/FormEditServizioAdmin.tsx
index 5a18c24..34df9f9 100644
--- a/apps/infoalloggi/src/forms/FormEditServizioAdmin.tsx
+++ b/apps/infoalloggi/src/forms/FormEditServizioAdmin.tsx
@@ -1,5 +1,6 @@
-import { X } from "lucide-react";
+import { Bug, Trash2, X } from "lucide-react";
import { z } from "zod/v4";
+import { Confirm } from "~/components/confirm";
import { Counter } from "~/components/counter";
import {
Form,
@@ -12,26 +13,33 @@ import {
} from "~/components/custom_ui/form";
import { DateInput } from "~/components/custom_ui/inputDate";
import { Button } from "~/components/ui/button";
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "~/components/ui/collapsible";
import { Label } from "~/components/ui/label";
import { Switch } from "~/components/ui/switch";
import { cn } from "~/lib/utils";
import { useZodForm } from "~/lib/zodForm";
import { useTranslation } from "~/providers/I18nProvider";
-import type { Servizio } from "~/schemas/public/Servizio";
+import { ServizioSchema } from "~/schemas/public/Servizio";
+import type { ServizioData } from "~/server/controllers/servizio.controller";
import { DEFAULT_SERVIZIO_INTERRUZIONE_DURATION_DAYS } from "~/utils/config";
-const Schema = z.object({
- decorrenza: z.date().nullable(),
- isInterrotto: z.boolean(),
- isOkAcconto: z.boolean(),
- isOkConsulenza: z.boolean(),
- isOkSaldo: z.boolean(),
- skipPayment: z.boolean(),
- skipControlloDoc: z.boolean(),
- skipDocMotivazione: z.boolean(),
- onboardOk: z.boolean(),
- interruzioneDays: z.number(),
- acceptedInterr: z.boolean(),
+const Schema = ServizioSchema.pick({
+ decorrenza: true,
+ isInterrotto: true,
+ isOkAcconto: true,
+ isOkConsulenza: true,
+ isOkSaldo: true,
+ skipPayment: true,
+ skipControlloDoc: true,
+ skipDocMotivazione: true,
+ onboardOk: true,
+ interruzioneDays: true,
+ acceptedInterr: true,
+ isDisabled: true,
});
export type FormValues = z.infer;
@@ -39,16 +47,29 @@ export type FormValues = z.infer;
export const FormEditServizio = ({
initialData,
onSubmit,
+ handleRemove,
}: {
- initialData: Servizio | undefined;
+ initialData: ServizioData | undefined;
onSubmit: (fields: FormValues) => void;
+ handleRemove: () => void;
}) => {
// This can come from your database or API.
let defaultValues: FormValues;
if (initialData) {
defaultValues = {
- ...initialData,
+ decorrenza: initialData.decorrenza,
+ isInterrotto: initialData.isInterrotto,
+ isOkAcconto: initialData.isOkAcconto,
+ isOkConsulenza: initialData.isOkConsulenza,
+ isOkSaldo: initialData.isOkSaldo,
+ skipPayment: initialData.skipPayment,
+ skipControlloDoc: initialData.skipControlloDoc,
+ skipDocMotivazione: initialData.skipDocMotivazione,
+ onboardOk: initialData.onboardOk,
+ interruzioneDays: initialData.interruzioneDays,
+ acceptedInterr: initialData.acceptedInterr,
+ isDisabled: initialData.isDisabled,
};
} else {
defaultValues = {
@@ -63,6 +84,7 @@ export const FormEditServizio = ({
onboardOk: false,
interruzioneDays: DEFAULT_SERVIZIO_INTERRUZIONE_DURATION_DAYS,
acceptedInterr: false,
+ isDisabled: false,
};
}
const { locale, t } = useTranslation();
@@ -147,6 +169,7 @@ export const FormEditServizio = ({
)}
/>
+
+
-
+
+ (
+
+
+ DISABILITATO
+
+
+
+
+
+
+
+ Disabilita il servizio, chiude la posizione senza eliminare.
+
+
+ )}
+ />
+
+
+
+
+
+
+
+
+ {
+ handleRemove();
+ }}
+ title="Elimina servizio"
+ >
+
+
+
+
+ {(() => {
+ if (!initialData) return null;
+ const { annunci, ...rest } = initialData;
+ return {JSON.stringify(rest, null, 2)};
+ })()}
+
+
);
diff --git a/apps/infoalloggi/src/i18n/en.ts b/apps/infoalloggi/src/i18n/en.ts
index 31753d1..d8fb8d7 100644
--- a/apps/infoalloggi/src/i18n/en.ts
+++ b/apps/infoalloggi/src/i18n/en.ts
@@ -1320,6 +1320,7 @@ The Stable Rent service, ideal for those with demonstrable work references and f
interrotto: "Interrupted",
scaduto: "Expired",
completato: "Completed",
+ disabled: "Disabled",
},
torna_alla_dashboard: "Dashboard",
dettaglio_servizio: "Service Details",
@@ -1330,7 +1331,9 @@ The Stable Rent service, ideal for those with demonstrable work references and f
durata_servizio: "Service Duration:",
servizio_interrotto: "Service Interrupted",
servizio_interrotto_desc:
- "This service cannot be reused as it has been interrupted.",
+ "This service cannot be used as it has been interrupted.",
+ servizio_disattivato: "Service Disabled",
+ servizio_disattivato_desc: "This service cannot be used.",
non_attivo: "Service not yet activated",
attiva_cta: "Activate the Service Now!",
per_te_1: "Already selected",
diff --git a/apps/infoalloggi/src/i18n/it.ts b/apps/infoalloggi/src/i18n/it.ts
index b0d9e01..45a9749 100644
--- a/apps/infoalloggi/src/i18n/it.ts
+++ b/apps/infoalloggi/src/i18n/it.ts
@@ -1319,6 +1319,7 @@ export const it: LangDict = {
in_attesa: "In Attesa di Attivazione",
scaduto: "Scaduto",
completato: "Completato",
+ disabled: "Disattivato",
},
torna_alla_dashboard: "Dashboard",
dettaglio_servizio: "Dettaglio Servizio",
@@ -1329,7 +1330,9 @@ export const it: LangDict = {
durata_servizio: "Durata servizio:",
servizio_interrotto: "Servizio interrotto",
servizio_interrotto_desc:
- "Non è possibile riutilizzare questo servizio, poichè è stato interrotto.",
+ "Non è possibile utilizzare questo servizio, poichè è stato interrotto.",
+ servizio_disattivato: "Servizio disattivato",
+ servizio_disattivato_desc: "Non è possibile utilizzare questo servizio.",
non_attivo: "Servizio non ancora attivato",
attiva_cta: "Attiva il Servizio Ora!",
per_te_1: "Sono già stati selezionati",
diff --git a/apps/infoalloggi/src/i18n/locales.ts b/apps/infoalloggi/src/i18n/locales.ts
index 584c308..d618fd4 100644
--- a/apps/infoalloggi/src/i18n/locales.ts
+++ b/apps/infoalloggi/src/i18n/locales.ts
@@ -779,6 +779,7 @@ export type LangDict = {
in_attesa: string;
scaduto: string;
completato: string;
+ disabled: string;
};
torna_alla_dashboard: string;
dettaglio_servizio: string;
@@ -789,6 +790,8 @@ export type LangDict = {
durata_servizio: string;
servizio_interrotto: string;
servizio_interrotto_desc: string;
+ servizio_disattivato: string;
+ servizio_disattivato_desc: string;
non_attivo: string;
attiva_cta: string;
per_te_1: string;
diff --git a/apps/infoalloggi/src/schemas/public/Servizio.ts b/apps/infoalloggi/src/schemas/public/Servizio.ts
index c5575cf..a595901 100644
--- a/apps/infoalloggi/src/schemas/public/Servizio.ts
+++ b/apps/infoalloggi/src/schemas/public/Servizio.ts
@@ -72,6 +72,8 @@ export default interface ServizioTable {
interruzioneDays: ColumnType;
acceptedInterr: ColumnType;
+
+ isDisabled: ColumnType;
}
export type Servizio = Selectable;
@@ -115,6 +117,7 @@ export const ServizioSchema = z.object({
onboardOk: z.boolean(),
interruzioneDays: z.number(),
acceptedInterr: z.boolean(),
+ isDisabled: z.boolean(),
});
export const NewServizioSchema = z.object({
@@ -150,6 +153,7 @@ export const NewServizioSchema = z.object({
onboardOk: z.boolean().optional(),
interruzioneDays: z.number().optional(),
acceptedInterr: z.boolean().optional(),
+ isDisabled: z.boolean().optional(),
});
export const ServizioUpdateSchema = z.object({
@@ -185,4 +189,5 @@ export const ServizioUpdateSchema = z.object({
onboardOk: z.boolean().optional(),
interruzioneDays: z.number().optional(),
acceptedInterr: z.boolean().optional(),
+ isDisabled: z.boolean().optional(),
});
\ No newline at end of file
diff --git a/apps/infoalloggi/src/server/api/routers/servizio.ts b/apps/infoalloggi/src/server/api/routers/servizio.ts
index 5c80b21..d9f8e91 100644
--- a/apps/infoalloggi/src/server/api/routers/servizio.ts
+++ b/apps/infoalloggi/src/server/api/routers/servizio.ts
@@ -198,6 +198,7 @@ export const servizioRouter = createTRPCRouter({
!servizio ||
servizio.isOkAcconto ||
servizio.isInterrotto ||
+ servizio.isDisabled ||
servizio.decorrenza !== null
) {
return { available: false, userId: null };
diff --git a/apps/infoalloggi/src/server/controllers/servizio.controller.ts b/apps/infoalloggi/src/server/controllers/servizio.controller.ts
index f1d3aba..f8ae8a4 100644
--- a/apps/infoalloggi/src/server/controllers/servizio.controller.ts
+++ b/apps/infoalloggi/src/server/controllers/servizio.controller.ts
@@ -1143,6 +1143,7 @@ export type RichiesteData = Pick<
| "servizio_id"
| "user_id"
| "isInterrotto"
+ | "isDisabled"
| "isOkAcconto"
| "isOkSaldo"
| "decorrenza"
@@ -1184,6 +1185,7 @@ export const getRichieste = async (): Promise => {
"servizio.created_at",
"servizio.user_id",
"servizio.isInterrotto",
+ "servizio.isDisabled",
"servizio.isOkAcconto",
"servizio.isOkSaldo",
"servizio.decorrenza",
diff --git a/apps/infoalloggi/src/server/services/servizio.service.ts b/apps/infoalloggi/src/server/services/servizio.service.ts
index 267fdb5..c4ce998 100644
--- a/apps/infoalloggi/src/server/services/servizio.service.ts
+++ b/apps/infoalloggi/src/server/services/servizio.service.ts
@@ -7,6 +7,7 @@ import type {
ServizioUpdate,
} from "~/schemas/public/Servizio";
import type { ServizioAnnunciUpdate } from "~/schemas/public/ServizioAnnunci";
+import { clearUserEmailQueue } from "~/server/controllers/event_queue.controller";
import { db } from "../db";
export const addServizio = async (data: NewServizio) => {
@@ -78,7 +79,14 @@ export const updateServizio = async ({
data: ServizioUpdate;
}) => {
try {
- return await db
+ const isDisabledPrev = await db
+ .selectFrom("servizio")
+ .select("isDisabled")
+ .where("servizio_id", "=", servizioId)
+ .executeTakeFirstOrThrow(
+ () => new Error(`Servizio not found - servizioId: ${servizioId}`),
+ );
+ const updated = await db
.updateTable("servizio")
.set(data)
.where("servizio_id", "=", servizioId)
@@ -87,6 +95,11 @@ export const updateServizio = async ({
() =>
new Error(`Failed to update servizio - servizioId: ${servizioId}`),
);
+ if (!isDisabledPrev.isDisabled && updated.isDisabled) {
+ // Only the on isDisabled false->true change
+ await clearUserEmailQueue(updated.user_id);
+ }
+ return updated;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",