diff --git a/Surge365.MassEmailReact.Infrastructure/Repositories/BouncedEmailRepository.cs b/Surge365.MassEmailReact.Infrastructure/Repositories/BouncedEmailRepository.cs index 1f15e6e..17374fe 100644 --- a/Surge365.MassEmailReact.Infrastructure/Repositories/BouncedEmailRepository.cs +++ b/Surge365.MassEmailReact.Infrastructure/Repositories/BouncedEmailRepository.cs @@ -67,6 +67,7 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories parameters.Add("@unsubscribe", bouncedEmail.Unsubscribe, DbType.Boolean); parameters.Add("@entered_by_admin", bouncedEmail.EnteredByAdmin, DbType.Boolean); parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output); + parameters.Add("@bounced_email_key", dbType: DbType.Int32, direction: ParameterDirection.Output); await conn.ExecuteAsync("mem_save_bounced_email", parameters, commandType: CommandType.StoredProcedure); diff --git a/Surge365.MassEmailReact.Web/src/components/modals/BouncedEmailEdit.tsx b/Surge365.MassEmailReact.Web/src/components/modals/BouncedEmailEdit.tsx index 8e8314c..f102d8c 100644 --- a/Surge365.MassEmailReact.Web/src/components/modals/BouncedEmailEdit.tsx +++ b/Surge365.MassEmailReact.Web/src/components/modals/BouncedEmailEdit.tsx @@ -95,7 +95,7 @@ const BouncedEmailEdit = ({ open, bouncedEmail, onClose, onSave }: BouncedEmailE return ( - {isNew ? "Add Bounced Email" : "Edit Bounced Email"} + {isNew ? "Add Blocked Email" : "Edit Blocked Email"} t.id === value); - }), - targetId: yup.number().typeError("Target is required").required("Target is required").test("valid-target", "Invalid target", function (value) { - const setupData = this.options.context?.setupData as SetupData; - return setupData.targets.some(t => t.id === value); - }), - statusCode: yup.string().default("ED"), - scheduleDate: yup.string() - .nullable() - .when("$scheduleForLater", (scheduleForLater, schema) => { - const isScheduledForLater = scheduleForLater[0] ?? false; - return isScheduledForLater - ? schema - .required("Schedule date is required when scheduled for later") - .matches( - /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/, - "Schedule date must be a valid UTC ISO string (e.g., 'YYYY-MM-DDTHH:mm:ssZ')" - ) - .test("is-future", "Schedule date must be in the future", (value) => { - if (!value) return true; // Nullable when not required - return dayjs(value).isAfter(dayjs()); - }) - : schema.nullable(); - }), - - //scheduleDate: yup.date().nullable(), - - // .when("statusCode", { - // is: (value: string) => value === "SC" || value === "SD", // String comparison - // then: (schema) => schema.required("Schedule date is required for scheduled or sending status"), - // otherwise: (schema) => schema.nullable(), - //}), - sentDate: yup.date().nullable().default(null), - sessionActivityId: yup.string().nullable(), - recurringTypeCode: yup - .string() - .nullable() - .when("$recurring", (recurring, schema) => { // Use context variable - const isRecurring = recurring[0] ?? false; - return isRecurring - ? schema.oneOf(recurringTypeOptions.map((r) => r.code), "Invalid recurring type") - : schema.nullable(); - }), - - recurringStartDate: yup.string() - .nullable() - .when("$recurring", (recurring, schema) => { - const isRecurring = recurring[0] ?? false; - return isRecurring - ? schema - .required("Recurring start date is required when recurring") - .matches( - /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/, - "Recurring start date must be a valid UTC ISO string (e.g., 'YYYY-MM-DDTHH:mm:ssZ')" - ) - .test("is-future", "Recurring start date must be in the future", (value) => { - if (!value) return true; // Nullable when not required - return dayjs(value).isAfter(dayjs()); - }) - : schema.nullable(); - }), - template: yup.object().shape({ - id: yup.number().nullable().default(0), - mailingId: yup.number().default(0), - name: yup.string().default(""), - domainId: yup - .number() - .typeError("Domain is required") - .required("Domain is required") - .test("valid-domain", "Invalid domain", function (value) { - const setupData = this.options.context?.setupData as SetupData; - return setupData.emailDomains.some((d) => d.id === value); - }), - description: yup.string().default(""), - htmlBody: yup.string().default(""), - subject: yup.string().required("Subject is required").default(""), - toName: yup.string().default(""), - fromName: yup.string().required("From Name is required").default(""), - fromEmail: yup.string().default(""), - replyToEmail: yup.string().default(""), - clickTracking: yup.boolean().default(false), - openTracking: yup.boolean().default(false), - categoryXml: yup.string().default(""), - }), - target: yup.object().shape({ - id: yup.number().nullable().default(0), - mailingId: yup.number().default(0), - serverId: yup.number().default(0), - name: yup.string().default(""), - databaseName: yup.string().default(""), - viewName: yup.string().default(""), - filterQuery: yup.string().default(""), - allowWriteBack: yup.boolean().default(false), - }).nullable(), -}); - -const nameIsAvailable = async (id: number, name: string) => { - const response = await customFetch(`/api/mailings/available?${id > 0 ? "id=" + id + "&" : ""}name=${name}`); - const data = await response.json(); - return data.available; -}; -const getNextAvailableName = async (id: number, name: string) => { - const response = await customFetch(`/api/mailings/nextavailablename?${id > 0 ? "id=" + id + "&" : ""}name=${name}`); - const data = await response.json(); - return data.name; -}; - -const defaultMailing: Mailing = { - id: 0, - name: "", - description: "", - templateId: 0, - targetId: 0, - statusCode: "ED", - scheduleDate: null, - sentDate: null, - sessionActivityId: null, - recurringTypeCode: null, - recurringStartDate: null, - template: { - id: 0, - mailingId: 0, - name: "", - domainId: 0, - description: "", - htmlBody: "", - subject: "", - toName: "", - fromName: "", - fromEmail: "", - replyToEmail: "", - clickTracking: false, - openTracking: false, - categoryXml: "" - }, - target: { - id: 0, - mailingId: 0, - serverId: 0, - name: "", - databaseName: "", - viewName: "", - filterQuery: "", - allowWriteBack: false, - } -, -}; - const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => { const customFetch = useCustomFetch(); const isNew = !mailing || mailing.id === 0; @@ -242,6 +83,166 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => { const [targetSample, setTargetSample] = useState(null); const [targetSampleLoading, setTargetSampleLoading] = useState(false); + const defaultMailing: Mailing = { + id: 0, + name: "", + description: "", + templateId: 0, + targetId: 0, + statusCode: "ED", + scheduleDate: null, + sentDate: null, + sessionActivityId: null, + recurringTypeCode: null, + recurringStartDate: null, + template: { + id: 0, + mailingId: 0, + name: "", + domainId: 0, + description: "", + htmlBody: "", + subject: "", + toName: "", + fromName: "", + fromEmail: "", + replyToEmail: "", + clickTracking: false, + openTracking: false, + categoryXml: "" + }, + target: { + id: 0, + mailingId: 0, + serverId: 0, + name: "", + databaseName: "", + viewName: "", + filterQuery: "", + allowWriteBack: false, + } + , + }; + + + const nameIsAvailable = async (id: number, name: string) => { + const response = await customFetch(`/api/mailings/available?${id > 0 ? "id=" + id + "&" : ""}name=${name}`); + const data = await response.json(); + return data.available; + }; + const getNextAvailableName = async (id: number, name: string) => { + const response = await customFetch(`/api/mailings/nextavailablename?${id > 0 ? "id=" + id + "&" : ""}name=${name}`); + const data = await response.json(); + return data.name; + }; + + const schema = yup.object().shape({ + id: yup.number().nullable(), + name: yup.string().required("Name is required") + .test("unique-name", "Name must be unique", async function (value) { + if (value.length === 0) + return true; + return await nameIsAvailable(this.parent.id, value); + }), + description: yup.string().default(""), + templateId: yup.number().typeError("Template is required").required("Template is required").test("valid-template", "Invalid template", function (value) { + const setupData = this.options.context?.setupData as SetupData; + return setupData.templates.some(t => t.id === value); + }), + targetId: yup.number().typeError("Target is required").required("Target is required").test("valid-target", "Invalid target", function (value) { + const setupData = this.options.context?.setupData as SetupData; + return setupData.targets.some(t => t.id === value); + }), + statusCode: yup.string().default("ED"), + scheduleDate: yup.string() + .nullable() + .when("$scheduleForLater", (scheduleForLater, schema) => { + const isScheduledForLater = scheduleForLater[0] ?? false; + return isScheduledForLater + ? schema + .required("Schedule date is required when scheduled for later") + .matches( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/, + "Schedule date must be a valid UTC ISO string (e.g., 'YYYY-MM-DDTHH:mm:ssZ')" + ) + .test("is-future", "Schedule date must be in the future", (value) => { + if (!value) return true; // Nullable when not required + return dayjs(value).isAfter(dayjs()); + }) + : schema.nullable(); + }), + + //scheduleDate: yup.date().nullable(), + + // .when("statusCode", { + // is: (value: string) => value === "SC" || value === "SD", // String comparison + // then: (schema) => schema.required("Schedule date is required for scheduled or sending status"), + // otherwise: (schema) => schema.nullable(), + //}), + sentDate: yup.date().nullable().default(null), + sessionActivityId: yup.string().nullable(), + recurringTypeCode: yup + .string() + .nullable() + .when("$recurring", (recurring, schema) => { // Use context variable + const isRecurring = recurring[0] ?? false; + return isRecurring + ? schema.oneOf(recurringTypeOptions.map((r) => r.code), "Invalid recurring type") + : schema.nullable(); + }), + + recurringStartDate: yup.string() + .nullable() + .when("$recurring", (recurring, schema) => { + const isRecurring = recurring[0] ?? false; + return isRecurring + ? schema + .required("Recurring start date is required when recurring") + .matches( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/, + "Recurring start date must be a valid UTC ISO string (e.g., 'YYYY-MM-DDTHH:mm:ssZ')" + ) + .test("is-future", "Recurring start date must be in the future", (value) => { + if (!value) return true; // Nullable when not required + return dayjs(value).isAfter(dayjs()); + }) + : schema.nullable(); + }), + template: yup.object().shape({ + id: yup.number().nullable().default(0), + mailingId: yup.number().default(0), + name: yup.string().default(""), + domainId: yup + .number() + .typeError("Domain is required") + .required("Domain is required") + .test("valid-domain", "Invalid domain", function (value) { + const setupData = this.options.context?.setupData as SetupData; + return setupData.emailDomains.some((d) => d.id === value); + }), + description: yup.string().default(""), + htmlBody: yup.string().default(""), + subject: yup.string().required("Subject is required").default(""), + toName: yup.string().default(""), + fromName: yup.string().required("From Name is required").default(""), + fromEmail: yup.string().default(""), + replyToEmail: yup.string().default(""), + clickTracking: yup.boolean().default(false), + openTracking: yup.boolean().default(false), + categoryXml: yup.string().default(""), + }), + target: yup.object().shape({ + id: yup.number().nullable().default(0), + mailingId: yup.number().default(0), + serverId: yup.number().default(0), + name: yup.string().default(""), + databaseName: yup.string().default(""), + viewName: yup.string().default(""), + filterQuery: yup.string().default(""), + allowWriteBack: yup.boolean().default(false), + }).nullable(), + }); + const { register, trigger, control, handleSubmit, reset, setValue, formState: { errors } } = useForm({ mode: "onBlur", defaultValues: { @@ -253,6 +254,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => { const [loading, setLoading] = useState(false); + useEffect(() => { const initializeMailingEdit = async () => { if (open) { diff --git a/Surge365.MassEmailReact.Web/src/components/pages/ActiveMailings.tsx b/Surge365.MassEmailReact.Web/src/components/pages/ActiveMailings.tsx index cd04dbf..ed9b60f 100644 --- a/Surge365.MassEmailReact.Web/src/components/pages/ActiveMailings.tsx +++ b/Surge365.MassEmailReact.Web/src/components/pages/ActiveMailings.tsx @@ -1,4 +1,5 @@ import { useState, useRef, useEffect } from 'react'; +import { useSetupData, SetupData } from "@/context/SetupDataContext"; import RefreshIcon from '@mui/icons-material/Refresh'; import Switch from '@mui/material/Switch'; import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton, FormControlLabel } from '@mui/material'; @@ -9,6 +10,7 @@ import { useCustomFetch } from "@/utils/customFetch"; function ActiveMailings() { const customFetch = useCustomFetch(); const theme = useTheme(); + const setupData: SetupData = useSetupData(); const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const gridContainerRef = useRef(null); @@ -40,6 +42,7 @@ function ActiveMailings() { isFetchingRef.current = true; setMailingsLoading(true); + setupData.reloadSetupData(); try { const response = await customFetch("/api/mailings/status/SD/stats"); const statsData = await response.json(); diff --git a/Surge365.MassEmailReact.Web/src/components/pages/CancelledMailings.tsx b/Surge365.MassEmailReact.Web/src/components/pages/CancelledMailings.tsx index 670aa87..ea42822 100644 --- a/Surge365.MassEmailReact.Web/src/components/pages/CancelledMailings.tsx +++ b/Surge365.MassEmailReact.Web/src/components/pages/CancelledMailings.tsx @@ -53,6 +53,7 @@ function CancelledMailings() { const reloadMailings = async () => { setMailingsLoading(true); + setupData.reloadSetupData(); const mailingsResponse = await customFetch("/api/mailings/status/c"); const mailingsData = await mailingsResponse.json(); if (mailingsData) { diff --git a/Surge365.MassEmailReact.Web/src/components/pages/NewMailings.tsx b/Surge365.MassEmailReact.Web/src/components/pages/NewMailings.tsx index 1a8ea9f..2d17633 100644 --- a/Surge365.MassEmailReact.Web/src/components/pages/NewMailings.tsx +++ b/Surge365.MassEmailReact.Web/src/components/pages/NewMailings.tsx @@ -57,6 +57,7 @@ function NewMailings() { const reloadMailings = async () => { setMailingsLoading(true); + setupData.reloadSetupData(); const mailingsResponse = await customFetch("/api/mailings/status/ed"); const mailingsData = await mailingsResponse.json(); diff --git a/Surge365.MassEmailReact.Web/src/components/pages/ScheduledMailings.tsx b/Surge365.MassEmailReact.Web/src/components/pages/ScheduledMailings.tsx index b9e5533..6669c38 100644 --- a/Surge365.MassEmailReact.Web/src/components/pages/ScheduledMailings.tsx +++ b/Surge365.MassEmailReact.Web/src/components/pages/ScheduledMailings.tsx @@ -1,4 +1,5 @@ import { useState, useRef, useEffect } from 'react'; +import { useSetupData, SetupData } from "@/context/SetupDataContext"; import VisibilityIcon from '@mui/icons-material/Visibility'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import CancelIcon from '@mui/icons-material/Cancel'; @@ -14,6 +15,7 @@ import { useCustomFetch } from "@/utils/customFetch"; function ScheduleMailings() { const customFetch = useCustomFetch(); const theme = useTheme(); + const setupData: SetupData = useSetupData(); const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const gridContainerRef = useRef(null); @@ -81,6 +83,7 @@ function ScheduleMailings() { const reloadMailings = async () => { setMailingsLoading(true); + setupData.reloadSetupData(); const mailingsResponse = await customFetch("/api/mailings/status/sc"); // Adjust endpoint as needed const mailingsData = await mailingsResponse.json(); if (mailingsData) {