- Added new methods for creating mailings and testing targets. - Updated configuration files for JWT settings and connection strings. - Introduced new DTOs for target column updates and test targets. - Enhanced MailingStatistic with a new SentDate property. - Created new components for handling cancelled mailings and target samples. - Refactored authentication in Login.tsx to use fetch API. - Updated various services and repositories to support new functionalities.
584 lines
26 KiB
TypeScript
584 lines
26 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import {
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
TextField,
|
|
Autocomplete,
|
|
DialogActions,
|
|
Button,
|
|
Checkbox,
|
|
FormControl,
|
|
FormControlLabel,
|
|
Box,
|
|
MenuItem,
|
|
Select,
|
|
InputLabel
|
|
} from "@mui/material";
|
|
import VisibilityIcon from '@mui/icons-material/Visibility';
|
|
|
|
import Template from "@/types/template";
|
|
import Mailing from "@/types/mailing";
|
|
import Target from "@/types/target";
|
|
import EmailList from "@/components/forms/EmailList";
|
|
import TestEmailList from "@/types/testEmailList";
|
|
import TemplateViewer from "@/components/modals/TemplateViewer"
|
|
import TargetSampleModal from "@/components/modals/TargetSampleModal"
|
|
import { useSetupData, SetupData } from "@/context/SetupDataContext";
|
|
import { useForm, Controller, Resolver } from "react-hook-form";
|
|
import { yupResolver } from "@hookform/resolvers/yup";
|
|
import * as yup from "yup";
|
|
|
|
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
|
|
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
|
import dayjs, { Dayjs } from 'dayjs'; // Import Dayjs for date handling
|
|
import utc from 'dayjs/plugin/utc'; // Import the UTC plugin
|
|
import { toast } from "react-toastify";
|
|
|
|
dayjs.extend(utc);
|
|
|
|
type MailingEditProps = {
|
|
open: boolean;
|
|
mailing: Mailing | null;
|
|
onClose: (reason: 'backdropClick' | 'escapeKeyDown' | 'saved' | 'cancelled') => void;
|
|
onSave: (updatedMailing: Mailing) => void;
|
|
};
|
|
|
|
//const statusOptions = [
|
|
// { code: 'C', name: 'Cancelled' },
|
|
// { code: 'ED', name: 'Editing' },
|
|
// { code: 'ER', name: 'Error' },
|
|
// { code: 'QE', name: 'Queueing Error' },
|
|
// { code: 'S', name: 'Sent' },
|
|
// { code: 'SC', name: 'Scheduled' },
|
|
// { code: 'SD', name: 'Sending' },
|
|
//];
|
|
|
|
const recurringTypeOptions = [
|
|
{ code: 'D', name: 'Daily' },
|
|
{ code: 'M', name: 'Monthly' },
|
|
{ code: 'W', name: 'Weekly' },
|
|
];
|
|
|
|
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();
|
|
}),
|
|
|
|
//.when("recurringTypeCode", {
|
|
//is: (value: string) => value !== "" && value !== null, // String comparison for "None"
|
|
//then: (schema) => schema.required("Recurring start date is required when recurring type is set"),
|
|
//otherwise: (schema) => schema.nullable(),
|
|
//}),
|
|
});
|
|
|
|
const nameIsAvailable = async (id: number, name: string) => {
|
|
const response = await fetch(`/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 fetch(`/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,
|
|
};
|
|
|
|
const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
|
const isNew = !mailing || mailing.id === 0;
|
|
const setupData: SetupData = useSetupData();
|
|
const [approved, setApproved] = useState<boolean>(false);
|
|
const [recurring, setRecurring] = useState<boolean>(false);
|
|
const [scheduleForLater, setScheduleForLater] = useState<boolean>(false);
|
|
const [testEmailListId, setTestEmailListId] = useState<number | null>(null);
|
|
const [emails, setEmails] = useState<string[]>([]); // State for email array
|
|
const [templateViewerOpen, setTemplateViewerOpen] = useState<boolean>(false);
|
|
const [currentTemplate, setCurrentTemplate] = useState<Template | null>(null);
|
|
const [TargetSampleModalOpen, setTargetSampleModalOpen] = useState<boolean>(false);
|
|
const [currentTarget, setCurrentTarget] = useState<Target | null>(null);
|
|
|
|
const { register, trigger, control, handleSubmit, reset, formState: { errors } } = useForm<Mailing>({
|
|
mode: "onBlur",
|
|
defaultValues: {
|
|
...(mailing || defaultMailing),
|
|
},
|
|
resolver: yupResolver(schema) as Resolver<Mailing>,
|
|
context: { setupData, scheduleForLater, recurring },
|
|
});
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const initializeMailingEdit = async () => {
|
|
if (open) {
|
|
if (mailing) {
|
|
mailing.scheduleDate = null;
|
|
mailing.recurringTypeCode = null;
|
|
mailing.recurringStartDate = null;
|
|
if (mailing.id == 0) {
|
|
mailing.name = await getNextAvailableName(mailing.id, mailing.name);
|
|
}
|
|
}
|
|
setApproved(false);
|
|
setRecurring(false);
|
|
setScheduleForLater(false);
|
|
reset(mailing || defaultMailing, { keepDefaultValues: true });
|
|
if (setupData.testEmailLists.length > 0) {
|
|
setTestEmailListId(setupData.testEmailLists[0].id);
|
|
setEmails(setupData.testEmailLists[0].emails);
|
|
} else {
|
|
setTestEmailListId(null);
|
|
setEmails([]);
|
|
}
|
|
if (mailing?.templateId) {
|
|
const template = setupData.templates.find(t => t.id === mailing.templateId) || null;
|
|
setCurrentTemplate(template);
|
|
} else {
|
|
setCurrentTemplate(null);
|
|
}
|
|
if (mailing?.targetId) {
|
|
const target = setupData.targets.find(t => t.id === mailing.targetId) || null;
|
|
setCurrentTarget(target);
|
|
} else {
|
|
setCurrentTarget(null);
|
|
}
|
|
}
|
|
};
|
|
|
|
initializeMailingEdit();
|
|
}, [open, mailing, reset, setupData.testEmailLists, setupData.targets, setupData.templates]);
|
|
|
|
const handleSave = async (formData: Mailing) => {
|
|
const apiUrl = isNew ? "/api/mailings" : `/api/mailings/${formData.id}`;
|
|
const method = isNew ? "POST" : "PUT";
|
|
setLoading(true);
|
|
|
|
if (approved) {
|
|
formData.statusCode = "SC";
|
|
if (!scheduleForLater) {
|
|
formData.scheduleDate = null;
|
|
}
|
|
if (!recurring) {
|
|
formData.recurringTypeCode = null;
|
|
formData.recurringStartDate = null;
|
|
}
|
|
}
|
|
else {
|
|
formData.statusCode = "ED";
|
|
formData.scheduleDate = null;
|
|
formData.recurringTypeCode = null;
|
|
formData.recurringStartDate = null;
|
|
}
|
|
try {
|
|
const jsonPayload = JSON.stringify(formData);
|
|
const response = await fetch(apiUrl, {
|
|
method: method,
|
|
headers: { "Content-Type": "application/json" },
|
|
body: jsonPayload,
|
|
});
|
|
|
|
if (!response.ok) throw new Error(isNew ? "Failed to create" : "Failed to update");
|
|
|
|
const updatedMailing = await response.json();
|
|
onSave(updatedMailing);
|
|
onClose('saved');
|
|
} catch (error) {
|
|
console.error("Update error:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleEmailsChange = (newEmails: string[]) => {
|
|
setEmails(newEmails);
|
|
};
|
|
|
|
const handleTestMailing = async () => {
|
|
setLoading(true); // Show loading state
|
|
|
|
const isValid = await trigger();
|
|
if (!isValid) {
|
|
console.log("Form is invalid, cannot test mailing");
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Get the current form data
|
|
const formData = control._formValues as Mailing;
|
|
|
|
// Prepare the payload matching TestMailingDto
|
|
const testMailingPayload = {
|
|
mailing: formData,
|
|
emailAddresses: emails,
|
|
};
|
|
|
|
// Make the API call to /api/mailings/test
|
|
const response = await fetch("/api/mailings/test", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(testMailingPayload),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error("Failed to test mailing");
|
|
}
|
|
|
|
toast.success("Test mailing sent successfully");
|
|
console.log("Test mailing sent successfully");
|
|
} catch (error) {
|
|
console.error("Test mailing error:", error);
|
|
toast.error("Failed to send test mailing");
|
|
} finally {
|
|
setLoading(false); // Reset loading state
|
|
}
|
|
};
|
|
|
|
const handleTestEmailListChange = (list: TestEmailList | null) => {
|
|
if (list) {
|
|
setTestEmailListId(list.id);
|
|
setEmails(list.emails);
|
|
}
|
|
}
|
|
const handleApprovedChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
setApproved(event.target.checked);
|
|
};
|
|
const handleRecurringChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
setRecurring(event.target.checked);
|
|
};
|
|
const handleScheduleForLaterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
setScheduleForLater(event.target.checked);
|
|
};
|
|
const handleTemplateViewerOpen = () => {
|
|
setTemplateViewerOpen(!templateViewerOpen);
|
|
};
|
|
const handleTargetSampleModalOpen = () => {
|
|
setTargetSampleModalOpen(!TargetSampleModalOpen);
|
|
};
|
|
return (
|
|
<LocalizationProvider dateAdapter={AdapterDayjs}> {/* Wrap with LocalizationProvider */}
|
|
<Dialog open={open} onClose={(_, reason) => { onClose(reason); }} maxWidth="sm" fullWidth disableEscapeKeyDown >
|
|
<DialogTitle>{isNew ? "Add Mailing" : "Edit Mailing id=" + mailing?.id}</DialogTitle>
|
|
<DialogContent>
|
|
<TextField
|
|
{...register("name")}
|
|
label="Name"
|
|
fullWidth
|
|
margin="dense"
|
|
error={!!errors.name}
|
|
helperText={errors.name?.message}
|
|
/>
|
|
<TextField
|
|
{...register("description")}
|
|
label="Description"
|
|
fullWidth
|
|
margin="dense"
|
|
error={!!errors.description}
|
|
helperText={errors.description?.message}
|
|
/>
|
|
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<Controller
|
|
name="templateId"
|
|
control={control}
|
|
render={({ field }) => (
|
|
<Autocomplete
|
|
{...field}
|
|
options={setupData.templates}
|
|
getOptionLabel={(option) => option.name}
|
|
value={setupData.templates.find(t => t.id === field.value) || null}
|
|
onChange={(_, newValue) => {
|
|
field.onChange(newValue ? newValue.id : null);
|
|
trigger("templateId");
|
|
setCurrentTemplate(newValue);
|
|
}}
|
|
renderInput={(params) => (
|
|
<TextField
|
|
{...params}
|
|
label="Template"
|
|
fullWidth
|
|
margin="dense"
|
|
error={!!errors.templateId}
|
|
helperText={errors.templateId?.message}
|
|
/>
|
|
)}
|
|
sx={{ flexGrow: 1 }}
|
|
/>
|
|
)}
|
|
/>
|
|
{currentTemplate && (
|
|
<Button
|
|
onClick={handleTemplateViewerOpen}
|
|
variant="outlined"
|
|
startIcon={<VisibilityIcon />}
|
|
sx={{ height: '100%', alignSelf: 'center' }}
|
|
>
|
|
View
|
|
</Button>
|
|
)}
|
|
</Box>
|
|
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<Controller
|
|
name="targetId"
|
|
control={control}
|
|
render={({ field }) => (
|
|
<Autocomplete
|
|
{...field}
|
|
options={setupData.targets}
|
|
getOptionLabel={(option) => option.name}
|
|
value={setupData.targets.find(t => t.id === field.value) || null}
|
|
onChange={(_, newValue) => {
|
|
field.onChange(newValue ? newValue.id : null);
|
|
trigger("targetId");
|
|
setCurrentTarget(newValue);
|
|
}}
|
|
renderInput={(params) => (
|
|
<TextField
|
|
{...params}
|
|
label="Target"
|
|
fullWidth
|
|
margin="dense"
|
|
error={!!errors.targetId}
|
|
helperText={errors.targetId?.message}
|
|
/>
|
|
)}
|
|
sx={{ flexGrow: 1 }}
|
|
/>
|
|
)}
|
|
/>
|
|
{currentTarget && (
|
|
<Button
|
|
onClick={handleTargetSampleModalOpen}
|
|
variant="outlined"
|
|
startIcon={<VisibilityIcon />}
|
|
sx={{ height: '100%', alignSelf: 'center' }}
|
|
>
|
|
View
|
|
</Button>
|
|
)}
|
|
</Box>
|
|
<Autocomplete
|
|
options={setupData.testEmailLists}
|
|
getOptionLabel={(option) => option.name}
|
|
value={setupData.testEmailLists.find(t => t.id === testEmailListId) || null}
|
|
onChange={(_, newValue) => handleTestEmailListChange(newValue)}
|
|
renderInput={(params) => (
|
|
<TextField
|
|
{...params}
|
|
label="Test Email List"
|
|
fullWidth
|
|
margin="dense"
|
|
/>
|
|
)}
|
|
/>
|
|
<EmailList emails={emails} onEmailTextChange={handleEmailsChange} />
|
|
<Button onClick={handleTestMailing} disabled={loading}>Test Mailing</Button>
|
|
<Box>
|
|
<FormControlLabel control={<Checkbox checked={approved} onChange={handleApprovedChange} />} label="Approved" />
|
|
</Box>
|
|
{approved && (<>
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', ml: 3 }}>
|
|
<FormControlLabel control={<Checkbox checked={recurring} onChange={handleRecurringChange} />} label="Recurring Mailing" />
|
|
{recurring && (<>
|
|
<FormControl fullWidth margin="dense">
|
|
<InputLabel id="recurring-type-select-label">Recurring Type</InputLabel>
|
|
<Controller
|
|
name="recurringTypeCode"
|
|
control={control}
|
|
render={({ field }) => (
|
|
<Select
|
|
{...field}
|
|
labelId="recurring-type-select-label"
|
|
id="recurring-type-select"
|
|
label="Recurring Type"
|
|
value={field.value || ''}
|
|
onChange={(e) => {
|
|
field.onChange(e.target.value);
|
|
trigger("recurringTypeCode");
|
|
}}
|
|
error={!!errors.recurringTypeCode}
|
|
>
|
|
<MenuItem value="">
|
|
<em>None</em>
|
|
</MenuItem>
|
|
{recurringTypeOptions.map((option) => (
|
|
<MenuItem key={option.code} value={option.code}>
|
|
{option.name}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
)}
|
|
/>
|
|
{errors.recurringTypeCode && (
|
|
<span style={{ color: 'red', fontSize: '0.75rem' }}>
|
|
{errors.recurringTypeCode.message}
|
|
</span>
|
|
)}
|
|
</FormControl>
|
|
<Controller
|
|
name="recurringStartDate"
|
|
control={control}
|
|
render={({ field }) => (
|
|
<DateTimePicker
|
|
label="Recurring Start Date"
|
|
value={field.value ? dayjs(field.value) : null} // Convert to Dayjs
|
|
onChange={(newValue: Dayjs | null) => {
|
|
const utcString = newValue ? newValue.utc().format("YYYY-MM-DDTHH:mm:ss[Z]") : null;
|
|
field.onChange(utcString);
|
|
trigger("recurringStartDate");
|
|
}}
|
|
slotProps={{
|
|
textField: {
|
|
fullWidth: true,
|
|
margin: "dense",
|
|
error: !!errors.recurringStartDate,
|
|
helperText: errors.recurringStartDate?.message,
|
|
},
|
|
}}
|
|
minDateTime={dayjs()} // Enforce future date in UI
|
|
/>
|
|
)}
|
|
/>
|
|
</>)}
|
|
<FormControlLabel control={<Checkbox checked={scheduleForLater} onChange={handleScheduleForLaterChange} />} label="Schedule for Later" />
|
|
{scheduleForLater && (
|
|
<Controller
|
|
name="scheduleDate"
|
|
control={control}
|
|
render={({ field }) => (
|
|
<DateTimePicker
|
|
label="Schedule Date"
|
|
value={field.value ? dayjs(field.value) : null} // Convert to Dayjs
|
|
onChange={(newValue: Dayjs | null) => {
|
|
const utcString = newValue ? newValue.utc().format("YYYY-MM-DDTHH:mm:ss[Z]") : null;
|
|
field.onChange(utcString);
|
|
trigger("scheduleDate");
|
|
}}
|
|
slotProps={{
|
|
textField: {
|
|
fullWidth: true,
|
|
margin: "dense",
|
|
error: !!errors.scheduleDate,
|
|
helperText: errors.scheduleDate?.message,
|
|
},
|
|
}}
|
|
minDateTime={dayjs()} // Enforce future date in UI
|
|
/>
|
|
)}
|
|
/>
|
|
)}
|
|
</Box>
|
|
</>)}
|
|
|
|
{templateViewerOpen && (
|
|
<TemplateViewer
|
|
open={templateViewerOpen}
|
|
template={currentTemplate!}
|
|
onClose={() => { setTemplateViewerOpen(false) }}
|
|
/>
|
|
)}
|
|
{TargetSampleModalOpen && (
|
|
<TargetSampleModal
|
|
open={TargetSampleModalOpen}
|
|
target={currentTarget!}
|
|
onClose={() => { setTargetSampleModalOpen(false) }}
|
|
/>
|
|
)}
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={() => { onClose( 'cancelled'); }} disabled={loading}>Cancel</Button>
|
|
<Button onClick={handleSubmit(handleSave)} color="primary" disabled={loading}>
|
|
{loading ? "Saving..." : "Save"}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
</LocalizationProvider>
|
|
);
|
|
};
|
|
|
|
export default MailingEdit; |