disabled logic for event_queue
This commit is contained in:
parent
d0f146b99e
commit
fecac3428d
15 changed files with 119 additions and 37 deletions
6
apps/db/migrations/55_nulla_fatto_flag.up.sql
Normal file
6
apps/db/migrations/55_nulla_fatto_flag.up.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
-- 55_nulla_fatto_flag.up.sql
|
||||
ALTER TABLE IF EXISTS public.servizio
|
||||
ADD COLUMN IF NOT EXISTS "isDisabled" boolean NOT NULL DEFAULT false;
|
||||
|
||||
ALTER TABLE IF EXISTS public.event_queue
|
||||
ADD COLUMN IF NOT EXISTS "isActive" boolean NOT NULL DEFAULT true;
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
-- nulla_fatto_flag.up.sql
|
||||
ALTER TABLE IF EXISTS public.servizio
|
||||
ADD COLUMN IF NOT EXISTS "isDisabled" boolean NOT NULL DEFAULT false;
|
||||
|
|
@ -5,8 +5,8 @@ import Base from "./base";
|
|||
|
||||
export const GenericMailTag = "generic";
|
||||
|
||||
const props_base = z.object({
|
||||
title: z.string(),
|
||||
export const GenericPropsSchema = z.object({
|
||||
title: z.string({error:"Il campo non può essere vuoto"}).nonempty("Il campo non può essere vuoto"),
|
||||
noreply: z.boolean().optional(),
|
||||
link: z
|
||||
.object({
|
||||
|
|
@ -14,21 +14,10 @@ const props_base = z.object({
|
|||
label: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
corpo: z.custom<ReactNode>().optional(),
|
||||
testo: z.string({error:"Il campo non può essere vuoto"}).nonempty("Il campo non può essere vuoto"),
|
||||
});
|
||||
|
||||
const props_content = z.union([
|
||||
z.object({
|
||||
corpo: z.custom<ReactNode>(),
|
||||
testo: z.undefined().optional(),
|
||||
}),
|
||||
z.object({
|
||||
testo: z.string(),
|
||||
corpo: z.undefined().optional(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const GenericPropsSchema = props_base.and(props_content);
|
||||
|
||||
const Generic = ({
|
||||
title,
|
||||
noreply = false,
|
||||
|
|
@ -41,9 +30,7 @@ const Generic = ({
|
|||
<>
|
||||
<Heading className="text-center text-3xl leading-8">{title}</Heading>
|
||||
<Section className="px-5 text-left text-base md:px-12">
|
||||
{corpo ? (
|
||||
corpo
|
||||
) : (
|
||||
{corpo || (
|
||||
<Row>
|
||||
<Text>{testo}</Text>
|
||||
</Row>
|
||||
|
|
|
|||
|
|
@ -176,6 +176,7 @@ export const AdminComunicazioni = () => {
|
|||
minute: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
{!email.isActive && <div className="text-sm">(Disattivata)</div>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import ReactSelect, {
|
|||
type MenuListProps,
|
||||
} from "react-select";
|
||||
import type {} from "react-select/base";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { cn, passwordManagerIgnore } from "~/lib/utils";
|
||||
|
||||
declare module "react-select/base" {
|
||||
export interface Props<
|
||||
|
|
@ -37,7 +37,7 @@ type BaseProps = {
|
|||
disabled?: boolean;
|
||||
menuPlacement?: "auto" | "bottom" | "top";
|
||||
defaultValue?: ListOption | ListOption[];
|
||||
autoComplete?: HTMLInputAutoCompleteAttribute;
|
||||
//autoComplete?: HTMLInputAutoCompleteAttribute;
|
||||
};
|
||||
|
||||
type MultiProps = {
|
||||
|
|
@ -119,8 +119,8 @@ const CustomInput = (
|
|||
<components.Input
|
||||
{...props}
|
||||
// This pulls the value from the main ReactSelect wrapper
|
||||
|
||||
autoComplete={props.selectProps.autoComplete}
|
||||
{...passwordManagerIgnore}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -154,13 +154,13 @@ export const MultiSelect = ({
|
|||
placeholder,
|
||||
disabled,
|
||||
menuPlacement,
|
||||
autoComplete,
|
||||
//autoComplete,
|
||||
}: MultiSelectProps) => {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<ReactSelect
|
||||
autoComplete={autoComplete}
|
||||
//autoComplete={autoComplete}
|
||||
classNamePrefix="select"
|
||||
classNames={{
|
||||
control: () =>
|
||||
|
|
@ -197,6 +197,7 @@ export const MultiSelect = ({
|
|||
MenuList: VirtualizedMenuList,
|
||||
Input: CustomInput,
|
||||
}}
|
||||
data-bitwarden-ignore
|
||||
defaultValue={defaultValue}
|
||||
inputId={elementId}
|
||||
instanceId={elementId}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
import { DateInput } from "~/components/custom_ui/inputDate";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import Input from "~/components/ui/input";
|
||||
import { Switch } from "~/components/ui/switch";
|
||||
import { Textarea } from "~/components/ui/textarea";
|
||||
import { useZodForm } from "~/lib/zodForm";
|
||||
import { useTranslation } from "~/providers/I18nProvider";
|
||||
|
|
@ -22,6 +23,7 @@ const Schema = z.object({
|
|||
lock_date: z.date(),
|
||||
lock_time: z.string(),
|
||||
data: z.string(),
|
||||
isActive: z.boolean(),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof Schema>;
|
||||
|
|
@ -43,6 +45,7 @@ export const FormEditQueueMail = ({
|
|||
lock_date: new Date(data.lock_expires),
|
||||
lock_time: format(new Date(data.lock_expires), "HH:mm:ss"),
|
||||
data: JSON.stringify(data.data, null, 2),
|
||||
isActive: data.isActive,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -54,6 +57,7 @@ export const FormEditQueueMail = ({
|
|||
"T" +
|
||||
fields.lock_time,
|
||||
),
|
||||
isActive: fields.isActive,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -119,6 +123,26 @@ export const FormEditQueueMail = ({
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isActive"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex flex-wrap items-center gap-x-2">
|
||||
<FormLabel htmlFor="attivo">Attivo</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
className="data-[state=checked]:bg-neutral-700"
|
||||
defaultChecked={field.value}
|
||||
id="attivo"
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex w-full justify-end gap-2">
|
||||
<Button type="submit">Salva</Button>
|
||||
<Confirm
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { DateInput } from "~/components/custom_ui/inputDate";
|
|||
import { MultiSelect } from "~/components/custom_ui/multiselect";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import Input from "~/components/ui/input";
|
||||
import { Switch } from "~/components/ui/switch";
|
||||
import { useZodForm } from "~/lib/zodForm";
|
||||
import {
|
||||
type MailsTemplates,
|
||||
|
|
@ -27,6 +28,7 @@ type FormProps = {
|
|||
lock_expires: Date;
|
||||
template: MailsTemplates;
|
||||
subject: string;
|
||||
isActive: boolean;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
|
|
@ -45,7 +47,8 @@ const Schema = z
|
|||
expires_time: z.string(),
|
||||
mailType: z.enum(TemplateKeys),
|
||||
mailProps: z.record(z.string(), z.unknown()).optional(),
|
||||
subject: z.string(),
|
||||
subject: z.string().nonempty("Il campo non può essere vuoto"),
|
||||
isActive: z.boolean(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
const propsSchema = Templates[data.mailType as TemplateType].props;
|
||||
|
|
@ -55,7 +58,7 @@ const Schema = z
|
|||
for (const issue of result.error.issues) {
|
||||
ctx.addIssue({
|
||||
...issue,
|
||||
path: ["mailProps", ...(issue.path ?? [])],
|
||||
path: ["mailProps", ...issue.path],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -71,6 +74,7 @@ export const FormAddCodaEmail = ({ submitMutation }: FormProps) => {
|
|||
mailType: TemplateKeys[0],
|
||||
mailProps: undefined,
|
||||
subject: "",
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -84,12 +88,14 @@ export const FormAddCodaEmail = ({ submitMutation }: FormProps) => {
|
|||
lock_expires: Date;
|
||||
template: MailsTemplates;
|
||||
subject: string;
|
||||
isActive: boolean;
|
||||
} = {
|
||||
lock_expires: new Date(
|
||||
`${fields.expires_date.toISOString().split("T")[0]}T${fields.expires_time}`,
|
||||
),
|
||||
template,
|
||||
subject: fields.subject,
|
||||
isActive: fields.isActive,
|
||||
};
|
||||
submitMutation(values);
|
||||
}
|
||||
|
|
@ -103,10 +109,11 @@ export const FormAddCodaEmail = ({ submitMutation }: FormProps) => {
|
|||
useWatch({
|
||||
name: "mailProps",
|
||||
control: form.control,
|
||||
compute: (props) => {
|
||||
compute: async (props) => {
|
||||
if (!props) return;
|
||||
if ("title" in props) {
|
||||
form.setValue("subject", props.title as string);
|
||||
await form.trigger("subject");
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
|
@ -216,6 +223,26 @@ export const FormAddCodaEmail = ({ submitMutation }: FormProps) => {
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isActive"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex flex-wrap items-center gap-x-2">
|
||||
<FormLabel htmlFor="attivo">Attivo</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
className="data-[state=checked]:bg-neutral-700"
|
||||
defaultChecked={field.value}
|
||||
id="attivo"
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit">Salva</Button>
|
||||
</form>
|
||||
|
|
@ -332,12 +359,11 @@ const FieldRenderer = ({
|
|||
return (
|
||||
<FormField
|
||||
control={control}
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic field name
|
||||
name={name as any}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex flex-wrap items-center gap-x-2">
|
||||
<FormLabel>
|
||||
<FormLabel htmlFor={`${name}-input`}>
|
||||
{label}
|
||||
{optional && (
|
||||
<span className="ml-1 text-muted-foreground text-xs">
|
||||
|
|
@ -352,11 +378,13 @@ const FieldRenderer = ({
|
|||
<input
|
||||
checked={!!(field.value as boolean)}
|
||||
className="h-4 w-4"
|
||||
id={`${name}-input`}
|
||||
onChange={(e) => field.onChange(e.target.checked)}
|
||||
type="checkbox"
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
id={`${name}-input`}
|
||||
onChange={
|
||||
fieldType === "number"
|
||||
? (e) => field.onChange(Number(e.target.value))
|
||||
|
|
|
|||
|
|
@ -69,3 +69,11 @@ export const debounce = <T extends unknown[]>(
|
|||
}, delay);
|
||||
};
|
||||
};
|
||||
|
||||
export const passwordManagerIgnore = {
|
||||
autocomplete: "off",
|
||||
"data-1p-ignore": true, // 1Password
|
||||
"data-lpignore": "true", // LastPass
|
||||
"data-protonpass-ignore": "true", // ProtonPass
|
||||
"data-bwignore": "1", // Bitwarden
|
||||
};
|
||||
|
|
|
|||
|
|
@ -279,6 +279,7 @@ const Coda = ({ userId }: { userId: UsersId }) => {
|
|||
minute: "2-digit",
|
||||
})}
|
||||
</div>
|
||||
{!email.isActive && <div className="text-sm">(Disattivata)</div>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ export default interface EventQueueTable {
|
|||
lock_expires: ColumnType<Date, Date | string, Date | string>;
|
||||
|
||||
data: ColumnType<unknown | null, unknown | null, unknown | null>;
|
||||
|
||||
isActive: ColumnType<boolean, boolean | undefined, boolean>;
|
||||
}
|
||||
|
||||
export type EventQueue = Selectable<EventQueueTable>;
|
||||
|
|
@ -25,16 +27,19 @@ export const EventQueueSchema = z.object({
|
|||
event_id: eventQueueEventId,
|
||||
lock_expires: z.date(),
|
||||
data: z.unknown().nullable(),
|
||||
isActive: z.boolean(),
|
||||
});
|
||||
|
||||
export const NewEventQueueSchema = z.object({
|
||||
event_id: eventQueueEventId.optional(),
|
||||
lock_expires: z.union([z.date(), z.string()]).pipe(z.coerce.date()),
|
||||
data: z.unknown().optional().nullable(),
|
||||
isActive: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const EventQueueUpdateSchema = z.object({
|
||||
event_id: eventQueueEventId.optional(),
|
||||
lock_expires: z.union([z.date(), z.string()]).pipe(z.coerce.date()).optional(),
|
||||
data: z.unknown().optional().nullable(),
|
||||
isActive: z.boolean().optional(),
|
||||
});
|
||||
|
|
@ -236,6 +236,7 @@ export const comunicazioniRouter = createTRPCRouter({
|
|||
userId: usersId,
|
||||
template: z.custom<MailsTemplates>(),
|
||||
subject: z.string(),
|
||||
isActive: z.boolean(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
|
|
@ -260,6 +261,7 @@ export const comunicazioniRouter = createTRPCRouter({
|
|||
},
|
||||
},
|
||||
lock_expires: input.lock_expires,
|
||||
isActive: input.isActive,
|
||||
});
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
|
|
@ -298,6 +300,7 @@ export const comunicazioniRouter = createTRPCRouter({
|
|||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
console.log(input.data);
|
||||
return await updateEvent(input.eventId, input.data);
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ const getEventsFromQueue = async () => {
|
|||
const events = await db
|
||||
.selectFrom("event_queue")
|
||||
.selectAll()
|
||||
.where("event_queue.isActive", "=", true)
|
||||
.where("event_queue.lock_expires", "<=", new Date())
|
||||
.execute();
|
||||
return events;
|
||||
|
|
@ -213,6 +214,7 @@ export const getEmailQueue = async () => {
|
|||
.as("username"),
|
||||
])
|
||||
.where(sql`data->>'tipo'`, "=", "email")
|
||||
|
||||
.execute();
|
||||
return events;
|
||||
} catch (e) {
|
||||
|
|
@ -286,6 +288,23 @@ export const clearUserEmailQueue = async (userId: UsersId) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const disableUserEmailQueue = async (userId: UsersId) => {
|
||||
try {
|
||||
await db
|
||||
.updateTable("event_queue")
|
||||
.set({ isActive: false })
|
||||
.where(sql`data->>'tipo'`, "=", "email")
|
||||
.where(sql`data->'data'->>'userId'`, "=", userId)
|
||||
.execute();
|
||||
} catch (e) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: `Error while disabling user events from queue, ${(e as Error).message}`,
|
||||
cause: e,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const updateEvent = async (
|
||||
eventId: EventQueueEventId,
|
||||
data: EventQueueUpdate,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
|
|||
import {
|
||||
addEventToQueue,
|
||||
canSendNotification,
|
||||
clearUserEmailQueue,
|
||||
disableUserEmailQueue,
|
||||
nextTimeForNotification,
|
||||
} from "~/server/controllers/event_queue.controller";
|
||||
import {
|
||||
|
|
@ -927,8 +927,8 @@ export const interruzioneServizio = async (
|
|||
},
|
||||
});
|
||||
|
||||
// cleanup coda notifiche: rimuove notifiche future relative a questo servizio
|
||||
await clearUserEmailQueue(utente.id);
|
||||
// disabilita coda notifiche: rimuove notifiche future relative a questo servizio
|
||||
await disableUserEmailQueue(utente.id);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -7,7 +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 { disableUserEmailQueue } from "~/server/controllers/event_queue.controller";
|
||||
import { db } from "../db";
|
||||
|
||||
export const addServizio = async (data: NewServizio) => {
|
||||
|
|
@ -97,7 +97,7 @@ export const updateServizio = async ({
|
|||
);
|
||||
if (!isDisabledPrev.isDisabled && updated.isDisabled) {
|
||||
// Only the on isDisabled false->true change
|
||||
await clearUserEmailQueue(updated.user_id);
|
||||
await disableUserEmailQueue(updated.user_id);
|
||||
}
|
||||
return updated;
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
"moduleResolution": "Bundler",
|
||||
"noEmit": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"allowUnreachableCode": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"rootDir": ".",
|
||||
"paths": {
|
||||
"~/*": ["./src/*"],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue