From a95015d447a379093158616fd1ac050d0fa86df0 Mon Sep 17 00:00:00 2001 From: Walisson Pires Date: Tue, 17 Sep 2024 14:56:47 -0300 Subject: [PATCH] Allow edit contact notification trigger --- .../api/notifications/triggers/[id]/route.ts | 75 +++++++++-- .../[triggerId]/page.tsx | 33 +++++ src/common/routes/index.tsx | 1 + .../ContactNotificationTriggersView/index.tsx | 10 +- .../Form/DaysOfMonthSelect/index.tsx | 18 +-- src/components/Form/MonthsSelect/index.tsx | 18 +-- .../NotificationTriggerCard/index.tsx | 8 +- .../NotificationView/hooks/index.ts | 72 ++++++++-- src/components/NotificationView/index.tsx | 51 +++++++- .../notification-triggers/client-api.ts | 27 ++++ .../use-cases/get-notification-trigger.ts | 48 +++++++ .../update-notification-trigger-types.ts | 11 ++ .../use-cases/update-notification-trigger.ts | 123 ++++++++++++++++++ 13 files changed, 451 insertions(+), 44 deletions(-) create mode 100644 src/app/contacts/[contactId]/notification-triggers/[triggerId]/page.tsx create mode 100644 src/domains/notification-triggers/use-cases/get-notification-trigger.ts create mode 100644 src/domains/notification-triggers/use-cases/update-notification-trigger-types.ts create mode 100644 src/domains/notification-triggers/use-cases/update-notification-trigger.ts diff --git a/src/app/api/notifications/triggers/[id]/route.ts b/src/app/api/notifications/triggers/[id]/route.ts index 027d428..eeb582a 100644 --- a/src/app/api/notifications/triggers/[id]/route.ts +++ b/src/app/api/notifications/triggers/[id]/route.ts @@ -2,22 +2,77 @@ import { NextRequest, NextResponse } from "next/server"; import { DeleteNotificationTrigger } from "@/domains/notification-triggers/use-cases/delete-notification-trigger"; import { PrismaClientFactory } from "@/common/database/prisma-factory"; import { UserSessionManager } from "@/domains/auth/services/user-session-maganer"; +import { GetNotificationTrigger } from "@/domains/notification-triggers/use-cases/get-notification-trigger"; +import { ApiErrorHandler } from "@/common/error/api-error-handler"; +import { UpdateNotificationTriggerInput } from "@/domains/notification-triggers/use-cases/update-notification-trigger-types"; +import { UpdateNotificationTrigger } from "@/domains/notification-triggers/use-cases/update-notification-trigger"; +import { GenerateNotificationsByTriggers } from "@/domains/notifications/use-cases/generate-notification-by-triggers"; +export async function GET(request: NextRequest, { params }: { params: RequestParams }) { -export async function DELETE(request: NextRequest, { params }: { params: DeleteParams }) { + try { + if (!params.id) + return NextResponse.json(null); - const useCase = new DeleteNotificationTrigger({ - userLogged: await new UserSessionManager().getUserOrThrow(), - prismaClient: PrismaClientFactory.create() - }); + const useCase = new GetNotificationTrigger({ + userLogged: await new UserSessionManager().getUserOrThrow(), + prismaClient: PrismaClientFactory.create() + }); - await useCase.execute({ - id: params.id - }); + const result = await useCase.execute({ + triggerId: params.id + }); - return new Response(null, { status: 204 }); + return NextResponse.json(result); + } + catch(error) { + return ApiErrorHandler.handler(error); + } } -interface DeleteParams { +export async function PUT(request: NextRequest) { + + try { + const input: UpdateNotificationTriggerInput = await request.json(); + + const useCase = new UpdateNotificationTrigger({ + userLogged: await new UserSessionManager().getUserOrThrow(), + prismaClient: PrismaClientFactory.create() + }); + + const trigger = await useCase.execute(input); + + const generateNotificaion = new GenerateNotificationsByTriggers(); + await generateNotificaion.execute({ + triggerId: trigger.id + }); + + return NextResponse.json(trigger); + } + catch(error) { + return ApiErrorHandler.handler(error); + } +} + +export async function DELETE(request: NextRequest, { params }: { params: RequestParams }) { + + try { + const useCase = new DeleteNotificationTrigger({ + userLogged: await new UserSessionManager().getUserOrThrow(), + prismaClient: PrismaClientFactory.create() + }); + + await useCase.execute({ + id: params.id + }); + + return new Response(null, { status: 204 }); + } + catch(error) { + return ApiErrorHandler.handler(error); + } +} + +interface RequestParams { id: string; } \ No newline at end of file diff --git a/src/app/contacts/[contactId]/notification-triggers/[triggerId]/page.tsx b/src/app/contacts/[contactId]/notification-triggers/[triggerId]/page.tsx new file mode 100644 index 0000000..dfeb2b6 --- /dev/null +++ b/src/app/contacts/[contactId]/notification-triggers/[triggerId]/page.tsx @@ -0,0 +1,33 @@ +import { AppLayout } from "@/components/AppLayout"; +import { AppLayoutBackWithContactTitle } from "@/components/AppLayout/BackWithTitle/WithContact"; +import { AppLayoutHeader } from "@/components/AppLayout/Header"; +import { AppLayoutBody } from "@/components/AppLayout/Body"; +import { AppLayoutPreLoading } from "@/components/AppLayout/PreLoading"; +import { AppNavMenuDefault, AppNavMenuItens } from "@/components/AppLayout/NavMenu"; +import { ValidationInit } from "@/components/ValidationInit"; +import { NotificationView } from "@/components/NotificationView"; + +export default function ViewContactNotification({ params }: PageProps) { + + return ( + + + + + + + + + + + + + ) +} + +interface PageProps { + params: { + contactId: string; + triggerId: string; + } +} \ No newline at end of file diff --git a/src/common/routes/index.tsx b/src/common/routes/index.tsx index ec18b1d..aa964b2 100644 --- a/src/common/routes/index.tsx +++ b/src/common/routes/index.tsx @@ -6,6 +6,7 @@ export abstract class AppRoutes { public static importContacts() { return '/contacts/import'; } public static viewContact(contactId: string) { return '/contacts/' + contactId; } public static newContactNotification(contactId: string) { return `/contacts/${contactId}/notification-triggers/new`; } + public static viewContactNotification(contactId: string, triggerId: string) { return `/contacts/${contactId}/notification-triggers/${triggerId}`; } public static contactNotificationTriggers(contactId: string) { return `/contacts/${contactId}/notification-triggers`; } public static contactNotifications(contactId: string) { return `/contacts/${contactId}/notifications`; } public static home() { return '/'; } diff --git a/src/components/ContactNotificationTriggersView/index.tsx b/src/components/ContactNotificationTriggersView/index.tsx index c9b9d19..554b475 100644 --- a/src/components/ContactNotificationTriggersView/index.tsx +++ b/src/components/ContactNotificationTriggersView/index.tsx @@ -1,7 +1,9 @@ 'use client' +import { useRouter } from "next/navigation"; import { ArrowDownTrayIcon } from "@heroicons/react/24/outline"; import { AppError } from "@/common/error"; +import { AppRoutes } from "@/common/routes"; import { AppToast } from "@/common/ui/toast"; import { NotificationTriggersApi } from "@/domains/notification-triggers/client-api"; import { Button } from "../Form"; @@ -13,6 +15,7 @@ export default function ContactNotificationTriggersView({ contactId }: ContactNo const { data, isLoading: isLoadingTriggers, error, hasMore, loadNextPage, removeItem } = useNotificationTriggers({ contactId }); const { setLoading } = useLoading(); + const router = useRouter(); const isEmpty = data.length == 0 && !isLoadingTriggers && !error; const isFirstLoading = isLoadingTriggers && data.length === 0; @@ -39,13 +42,18 @@ export default function ContactNotificationTriggersView({ contactId }: ContactNo } }; + const handleEditTrigger = (triggerId: string) => async () => { + + router.push(AppRoutes.viewContactNotification(contactId, triggerId)); + }; + return (
    {data.map(item =>
  • - +
  • )} {isEmpty &&

    Nenhuma notificação encontrada

    } {isFirstLoading && } diff --git a/src/components/Form/DaysOfMonthSelect/index.tsx b/src/components/Form/DaysOfMonthSelect/index.tsx index fccf955..ed35245 100644 --- a/src/components/Form/DaysOfMonthSelect/index.tsx +++ b/src/components/Form/DaysOfMonthSelect/index.tsx @@ -6,15 +6,15 @@ export const DaysOfMonthSelect = forwardRef(func return ( - - - - - - - - - + + + + + + + + + diff --git a/src/components/NotificationTriggerCard/index.tsx b/src/components/NotificationTriggerCard/index.tsx index 6d62249..3cd746a 100644 --- a/src/components/NotificationTriggerCard/index.tsx +++ b/src/components/NotificationTriggerCard/index.tsx @@ -3,12 +3,12 @@ import { Info } from "luxon"; import { useMemo } from "react"; import Skeleton from "react-loading-skeleton"; -import { EllipsisVerticalIcon, XMarkIcon } from "@heroicons/react/24/outline"; +import { EllipsisVerticalIcon, PencilSquareIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { Trigger1, TriggerType, TriggerTypeDisplay } from "@/domains/notification-triggers/entities"; import { DropdownMenu, DropdownMenuItem, DropdownMenuToggle } from "../Form"; import { useDrodownMenu } from "../Form/DropdownMenu/hooks"; -export default function NotificationTriggerCard({ trigger, onDeleteClick }: NotificationTriggerCardProps) { +export default function NotificationTriggerCard({ trigger, onEditClick, onDeleteClick }: NotificationTriggerCardProps) { const { templateMessage, type, day, month } = trigger; @@ -40,6 +40,9 @@ export default function NotificationTriggerCard({ trigger, onDeleteClick }: Noti setVisible(!visible)}>}> + + Editar + Excluir @@ -69,5 +72,6 @@ NotificationTriggerCard.Skeleton = function NotificationTriggerCardSkeleton() { export interface NotificationTriggerCardProps { trigger: Trigger1; + onEditClick: () => void; onDeleteClick: () => void; } diff --git a/src/components/NotificationView/hooks/index.ts b/src/components/NotificationView/hooks/index.ts index 308d90e..63ea35c 100644 --- a/src/components/NotificationView/hooks/index.ts +++ b/src/components/NotificationView/hooks/index.ts @@ -18,15 +18,17 @@ import { useMessageTemplates } from "./message-templates.hook"; export interface UseNotificationViewProps { contactId: string; + triggerId?: string; } -export function useNotificationView({ contactId }: UseNotificationViewProps) { +export function useNotificationView({ contactId, triggerId }: UseNotificationViewProps) { + const [ isLoaded, setIsLoaded ] = useState(false); const [ isSaving, setIsLoading ] = useState(false); const { messageTemplates, isLoading: isLoadingMessageTemplates } = useMessageTemplates(); const router = useRouter(); - const { register, handleSubmit, watch, setValue, formState: { errors }, control } = useForm({ + const { register, handleSubmit, watch, setValue, reset, formState: { errors }, control } = useForm({ resolver: zodResolver(validationSchema), defaultValues: { triggerType: TriggerType.Monthy, @@ -48,16 +50,33 @@ export function useNotificationView({ contactId }: UseNotificationViewProps) { const api = new NotificationTriggersApi(); setIsLoading(true); - await api.register({ - contactId: contactId, - templateMessageId: data.templateMessageId, - day: (data.triggerType === TriggerType.Monthy || data.triggerType === TriggerType.Yearly) && data.day ? data.day : null, - month: (data.triggerType === TriggerType.Yearly) && data.month ? data.month : null, - type: data.triggerType, - paramsValue: data.messageTemplateParams - }); + if (triggerId) { + + await api.update({ + triggerId: triggerId, + contactId: contactId, + templateMessageId: data.templateMessageId, + day: (data.triggerType === TriggerType.Monthy || data.triggerType === TriggerType.Yearly) && data.day ? data.day : null, + month: (data.triggerType === TriggerType.Yearly) && data.month ? data.month : null, + type: data.triggerType, + paramsValue: data.messageTemplateParams + }); + + AppToast.success('Notificação atualizada'); + + } else { + + await api.register({ + contactId: contactId, + templateMessageId: data.templateMessageId, + day: (data.triggerType === TriggerType.Monthy || data.triggerType === TriggerType.Yearly) && data.day ? data.day : null, + month: (data.triggerType === TriggerType.Yearly) && data.month ? data.month : null, + type: data.triggerType, + paramsValue: data.messageTemplateParams + }); - AppToast.success('Notificação agendada'); + AppToast.success('Notificação agendada'); + } router.back(); } @@ -71,6 +90,36 @@ export function useNotificationView({ contactId }: UseNotificationViewProps) { } }; + useEffect(() => { + + if (triggerId) return; + + setIsLoaded(true); + }, []); + + useEffect(() => { + + if (!triggerId || messageTemplates.length === 0) return; + + (async() => { + + const api = new NotificationTriggersApi(); + const trigger = await api.getById({ id: triggerId }); + + reset({ + day: trigger.day ?? undefined, + month: trigger.month ?? undefined, + triggerType: trigger.type as any, + templateMessageId: trigger.templateMessage?.id, + messageTemplateParams: trigger.paramsValue + }); + + setIsLoaded(true); + + })(); + + }, [ triggerId, messageTemplates ]); + useEffect(() => { if (!messageTemplatedSelected) return; @@ -112,6 +161,7 @@ export function useNotificationView({ contactId }: UseNotificationViewProps) { register, isLoadingMessageTemplates, isSaving, + isLoaded, values, errors, control, diff --git a/src/components/NotificationView/index.tsx b/src/components/NotificationView/index.tsx index f6c6f33..a7412a9 100644 --- a/src/components/NotificationView/index.tsx +++ b/src/components/NotificationView/index.tsx @@ -1,13 +1,14 @@ 'use client' import { Controller } from "react-hook-form"; +import Skeleton from "react-loading-skeleton"; import { CheckIcon } from "@heroicons/react/24/outline"; import { TriggerType } from "@/domains/notification-triggers/entities"; import { FormRow, FormColumn, Input, Select, ButtonsGroup, ButtonItem, Button, DaysOfMonthSelect, MonthsSelect, ColSize } from "../Form"; import { useNotificationView, DefaultParamValue } from "./hooks"; -export function NotificationView({ contactId }: NotificationViewProps) { +export function NotificationView({ contactId, triggerId }: NotificationViewProps) { const { values, @@ -18,9 +19,13 @@ export function NotificationView({ contactId }: NotificationViewProps) { messageTemplates, messageTemplatedSelected, isSaving, + isLoaded, register, handleSubmit - } = useNotificationView({ contactId }); + } = useNotificationView({ contactId, triggerId }); + + if (!isLoaded) + return ; return (
    @@ -96,7 +101,49 @@ export function NotificationView({ contactId }: NotificationViewProps) { ); } +NotificationView.Skeleton = function NotificationViewSkeleton() { + + return ( +
    +
    +
    +

    Criar notificação

    +
    +
    + + + + + + + + + +
    + +
    +
    +
    + + + + + + + + +
    + +
    +
    +
    +
    +
    +
    + ); +} export interface NotificationViewProps { contactId: string; + triggerId?: string; } diff --git a/src/domains/notification-triggers/client-api.ts b/src/domains/notification-triggers/client-api.ts index 83ac955..bf985be 100644 --- a/src/domains/notification-triggers/client-api.ts +++ b/src/domains/notification-triggers/client-api.ts @@ -3,6 +3,7 @@ import { HttpClient } from "@/common/http/client"; import { PagedInput, PagedResult } from "@/common/http/pagination"; import { UrlFormatter } from "@/common/http/url/url-formatter"; import { RegisterNotificationTriggerInput } from "./use-cases/register-notification-trigger-types"; +import { UpdateNotificationTriggerInput } from "./use-cases/update-notification-trigger-types"; import { Trigger1, TriggerProps } from "./entities"; export class NotificationTriggersApi { @@ -24,6 +25,28 @@ export class NotificationTriggersApi { return result; } + public async update(args: UpdateNotificationTriggerInput): Promise { + + const url = UrlFormatter.format('{id}', { id: args.triggerId }); + const result = await this._client.put(url, args); + + if (!result) + throw new Error('Server did not return results'); + + return result; + } + + public async getById(args: GetByIdArgs): Promise { + + const url = UrlFormatter.format('{id}', args); + const result = await this._client.get(url); + + if (!result) + throw new Error('Server did not return results'); + + return result; + } + public async getAll(args: GetAllArgs): Promise> { const url = UrlFormatter.format('', args); @@ -42,6 +65,10 @@ export class NotificationTriggersApi { } } +export interface GetByIdArgs { + id: string; +} + export interface GetAllArgs extends PagedInput { contactId?: string; } \ No newline at end of file diff --git a/src/domains/notification-triggers/use-cases/get-notification-trigger.ts b/src/domains/notification-triggers/use-cases/get-notification-trigger.ts new file mode 100644 index 0000000..2a16695 --- /dev/null +++ b/src/domains/notification-triggers/use-cases/get-notification-trigger.ts @@ -0,0 +1,48 @@ +import { PrismaClient } from "@prisma/client"; +import { UseCase } from "@/common/use-cases"; +import { UserLogged } from "@/common/auth/user"; +import { Trigger1 } from "../entities"; +import { TriggerMapper } from "../mapper"; + + +export class GetNotificationTrigger implements UseCase { + + private _user: UserLogged; + private _db: PrismaClient; + + constructor({ userLogged, prismaClient }: { userLogged: UserLogged, prismaClient: PrismaClient }) { + + this._user = userLogged; + this._db = prismaClient; + } + + public async execute(input: GetNotificationTriggerInput): Promise { + + const trigger = await this._db.notificationTrigger.findFirst({ + where: { + id: input.triggerId, + contact: { + accountId: this._user.accountId + } + }, + include: { + templateMessage: { + select: { + id: true, + name: true, + notifyDaysBefore: true + } + } + } + }); + + const mapper = new TriggerMapper(); + const result = trigger ? mapper.mapToView1(trigger) : null; + return result; + } + +} + +export interface GetNotificationTriggerInput { + triggerId: string; +} diff --git a/src/domains/notification-triggers/use-cases/update-notification-trigger-types.ts b/src/domains/notification-triggers/use-cases/update-notification-trigger-types.ts new file mode 100644 index 0000000..a46ce3d --- /dev/null +++ b/src/domains/notification-triggers/use-cases/update-notification-trigger-types.ts @@ -0,0 +1,11 @@ +import { TriggerParamValue, TriggerType } from "../entities"; + +export interface UpdateNotificationTriggerInput { + triggerId: string; + templateMessageId: string; + contactId: string; + type: TriggerType; + day: number | null; + month: number | null; + paramsValue: TriggerParamValue[]; +} \ No newline at end of file diff --git a/src/domains/notification-triggers/use-cases/update-notification-trigger.ts b/src/domains/notification-triggers/use-cases/update-notification-trigger.ts new file mode 100644 index 0000000..b55eeb6 --- /dev/null +++ b/src/domains/notification-triggers/use-cases/update-notification-trigger.ts @@ -0,0 +1,123 @@ +import z from "zod"; +import { AppError } from "@/common/error"; +import { UseCase } from "@/common/use-cases"; +import { messages } from "@/common/validation/messages"; +import { UserLogged } from "@/common/auth/user"; +import { PrismaClient } from "@prisma/client"; +import { Trigger, TriggerType } from "../entities"; +import { TriggerMapper } from "../mapper"; +import { UpdateNotificationTriggerInput } from "./update-notification-trigger-types"; + +export class UpdateNotificationTrigger implements UseCase { + + private _user: UserLogged; + private _db: PrismaClient; + + constructor({ userLogged, prismaClient }: { userLogged: UserLogged, prismaClient: PrismaClient }) { + + this._user = userLogged; + this._db = prismaClient; + } + + public async execute(input: UpdateNotificationTriggerInput): Promise { + + this.validate(input); + + const triggerMapper = new TriggerMapper(); + + const triggerExists = await this._db.notificationTrigger.findFirst({ + where: { + contactId: input.contactId, + templateMessageId: input.templateMessageId, + type: triggerMapper.mapTriggerType2(input.type), + day: input.day, + month: input.month, + id: { + not: input.triggerId + } + }, + select: { + id: true + } + }); + + if (triggerExists) { + throw new AppError('Existe outra notificação para o dia informado'); + } + + await this._db.$transaction(async transaction => { + + await this._db.notificationTrigger.update({ + where: { id: input.triggerId }, + data: { + templateMessageId: input.templateMessageId, + contactId: input.contactId, + type: triggerMapper.mapTriggerType2(input.type), + day: input.day, + month: input.month, + paramsValue: JSON.stringify(input.paramsValue ?? []) + } + }); + + await transaction.notification.deleteMany({ + where: { + accountId: this._user.accountId, + triggerId: input.triggerId, + sendedAt: null, + } + }); + + }); + + const triggerDb = await this._db.notificationTrigger.findFirst({ + where: { + id: input.triggerId + }, + include: { + templateMessage: true + } + }); + + return triggerMapper.map(triggerDb!); + } + + private validate(input: UpdateNotificationTriggerInput) { + + const baseValidationSchema = z.object({ + triggerId: z.string().min(1, { message: messages.required }), + templateMessageId: z.string().min(1, { message: messages.required }), + contactId: z.string().min(1, { message: messages.required }), + type: z.nativeEnum(TriggerType), + paramsValue: z.array(z.object({ + name: z.string().min(1, { message: messages.required }), + value: z.string().min(1, { message: messages.required }) + })), + day: z.coerce.number().optional(), + month: z.coerce.number().optional(), + }); + + const dailyValidationSchema = z.object({ + type: z.literal(TriggerType.Daily) + }); + + const monthlyValidationSchema = z.object({ + type: z.literal(TriggerType.Monthy), + day: z.coerce.number().min(1).max(31) + }); + + const yearlyValidationSchema = z.object({ + type: z.literal(TriggerType.Yearly), + day: z.coerce.number().min(1).max(31), + month: z.coerce.number().min(1).max(12), + }); + + const validationSchema = z.intersection(baseValidationSchema, z.discriminatedUnion('type', [ dailyValidationSchema, monthlyValidationSchema, yearlyValidationSchema ])); + + const result = validationSchema.safeParse(input); + + if (result.success) + return; + + throw AppError.fromZodError(result.error); + } +} \ No newline at end of file