Add target sample data retrieval and viewer components
- Implemented `GetTargetSample` in `TargetsController` for fetching target sample data. - Updated `ITargetRepository` and `ITargetService` with `GetSampleData` method. - Created `TargetSample` class to structure sample data. - Added `GetSampleData` method in `TargetRepository` with hardcoded sample data. - Enhanced `MailingEdit.tsx` to manage target sample visibility and validation. - Introduced `TargetSampleViewer` and `TemplateViewer` components for displaying data in dialogs. - Made minor adjustments in `Servers.tsx` for better row handling.
This commit is contained in:
parent
a5fd034a31
commit
12bfdf57ce
@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Surge365.MassEmailReact.Application.DTOs;
|
||||
using Surge365.MassEmailReact.Application.Interfaces;
|
||||
using Surge365.MassEmailReact.Domain.Entities;
|
||||
using Surge365.MassEmailReact.Infrastructure.Repositories;
|
||||
|
||||
namespace Surge365.MassEmailReact.API.Controllers
|
||||
{
|
||||
@ -62,5 +63,20 @@ namespace Surge365.MassEmailReact.API.Controllers
|
||||
|
||||
return Ok(updatedTarget);
|
||||
}
|
||||
|
||||
[HttpGet("{targetId}/sample")]
|
||||
public async Task<IActionResult> GetTargetSample(int targetId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sample = await _targetService.GetSampleData(targetId);
|
||||
return Ok(sample);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log the exception (e.g., using ILogger if injected)
|
||||
return StatusCode(500, new { message = "Error fetching sample data", error = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -13,5 +13,6 @@ namespace Surge365.MassEmailReact.Application.Interfaces
|
||||
Task<List<Target>> GetAllAsync(bool activeOnly = true);
|
||||
Task<int?> CreateAsync(Target target);
|
||||
Task<bool> UpdateAsync(Target target);
|
||||
Task<TargetSample> GetSampleData(int targetId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,5 +8,6 @@ namespace Surge365.MassEmailReact.Application.Interfaces
|
||||
Task<List<Target>> GetAllAsync(bool activeOnly = true);
|
||||
Task<int?> CreateAsync(TargetUpdateDto targetDto);
|
||||
Task<bool> UpdateAsync(TargetUpdateDto targetDto);
|
||||
Task<TargetSample> GetSampleData(int targetId);
|
||||
}
|
||||
}
|
||||
|
||||
15
Surge365.MassEmailReact.Domain/Entities/TargetSample.cs
Normal file
15
Surge365.MassEmailReact.Domain/Entities/TargetSample.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Surge365.MassEmailReact.Domain.Entities
|
||||
{
|
||||
public class TargetSample
|
||||
{
|
||||
public List<string> ColumnNames { get; set; } = new List<string>();
|
||||
public List<Dictionary<string, string>> Rows { get; set; } = new List<Dictionary<string, string>>();
|
||||
}
|
||||
|
||||
}
|
||||
@ -118,5 +118,42 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
|
||||
// Targets.Add(target);
|
||||
//}
|
||||
|
||||
public async Task<TargetSample> GetSampleData(int targetId)
|
||||
{
|
||||
// Placeholder hardcoded sample data for testing
|
||||
var sample = new TargetSample
|
||||
{
|
||||
ColumnNames = new List<string> { "id", "name", "email", "age" },
|
||||
Rows = new List<Dictionary<string, string>>
|
||||
{
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "id", "1" },
|
||||
{ "name", "John Doe" },
|
||||
{ "email", "john.doe@example.com" },
|
||||
{ "age", "30" }
|
||||
},
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "id", "2" },
|
||||
{ "name", "Jane Smith" },
|
||||
{ "email", "jane.smith@example.com" },
|
||||
{ "age", "25" }
|
||||
},
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "id", "3" },
|
||||
{ "name", "Bob Johnson" },
|
||||
{ "email", "bob.johnson@example.com" },
|
||||
{ "age", "45" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Simulate async operation (e.g., DB call) for testing
|
||||
await Task.Delay(100); // Optional: Mimics network latency
|
||||
return sample;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,5 +69,9 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
|
||||
|
||||
return await _targetRepository.UpdateAsync(target);
|
||||
}
|
||||
public async Task<TargetSample> GetSampleData(int targetId)
|
||||
{
|
||||
return await _targetRepository.GetSampleData(targetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,14 +11,22 @@ import {
|
||||
FormControlLabel,
|
||||
Box
|
||||
} from "@mui/material";
|
||||
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 TargetSampleViewer from "@/components/modals/TargetSampleViewer"
|
||||
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 EmailList from "@/components/forms/EmailList";
|
||||
import TestEmailList from "@/types/testEmailList";
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
|
||||
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
|
||||
|
||||
type MailingEditProps = {
|
||||
open: boolean;
|
||||
@ -47,6 +55,8 @@ 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(""),
|
||||
@ -59,7 +69,16 @@ const schema = yup.object().shape({
|
||||
return setupData.targets.some(t => t.id === value);
|
||||
}),
|
||||
statusCode: yup.string().default("ED"),
|
||||
scheduleDate: yup.date().nullable(),
|
||||
scheduleDate: yup.date()
|
||||
.nullable()
|
||||
.when("$scheduleForLater", (scheduleForLater, schema) => { // Use context variable
|
||||
return scheduleForLater
|
||||
? schema
|
||||
.required("Schedule date is required when scheduled for later")
|
||||
.min(new Date(), "Schedule date must be in the future")
|
||||
: schema.nullable();
|
||||
}),
|
||||
//scheduleDate: yup.date().nullable(),
|
||||
|
||||
// .when("statusCode", {
|
||||
// is: (value: string) => value === "SC" || value === "SD", // String comparison
|
||||
@ -104,8 +123,14 @@ 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 [targetSampleViewerOpen, setTargetSampleViewerOpen] = useState<boolean>(false);
|
||||
const [currentTarget, setCurrentTarget] = useState<Target | null>(null);
|
||||
|
||||
const { register, trigger, control, handleSubmit, reset, formState: { errors } } = useForm<Mailing>({
|
||||
mode: "onBlur",
|
||||
@ -128,12 +153,41 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
||||
setTestEmailListId(null);
|
||||
setEmails([]);
|
||||
}
|
||||
if (mailing?.templateId) {
|
||||
const template = setupData.templates.find(t => t.id === mailing.templateId) || null;
|
||||
setCurrentTemplate(template);
|
||||
} else {
|
||||
setCurrentTemplate(null);
|
||||
}
|
||||
}, [open, mailing, reset, setupData.testEmailLists]);
|
||||
if (mailing?.targetId) {
|
||||
const target = setupData.targets.find(t => t.id === mailing.targetId) || null;
|
||||
setCurrentTarget(target);
|
||||
} else {
|
||||
setCurrentTarget(null);
|
||||
}
|
||||
}
|
||||
}, [open, mailing, reset, setupData.testEmailLists, setupData.targets]);
|
||||
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 response = await fetch(apiUrl, {
|
||||
method: method,
|
||||
@ -162,13 +216,27 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
||||
|
||||
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 handleTargetSampleViewerOpen = () => {
|
||||
setTargetSampleViewerOpen(!targetSampleViewerOpen);
|
||||
};
|
||||
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>
|
||||
@ -200,6 +268,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
||||
onChange={(_, newValue) => {
|
||||
field.onChange(newValue ? newValue.id : null);
|
||||
trigger("templateId");
|
||||
setCurrentTemplate(newValue);
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
@ -214,6 +283,10 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{currentTemplate && (
|
||||
<Button onClick={handleTemplateViewerOpen}>View Template</Button>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
name="targetId"
|
||||
control={control}
|
||||
@ -226,6 +299,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
||||
onChange={(_, newValue) => {
|
||||
field.onChange(newValue ? newValue.id : null);
|
||||
trigger("targetId");
|
||||
setCurrentTarget(newValue);
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
@ -240,6 +314,9 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{currentTarget && (
|
||||
<Button onClick={handleTargetSampleViewerOpen}>View Target Sample</Button>
|
||||
)}
|
||||
<Autocomplete
|
||||
options={setupData.testEmailLists}
|
||||
getOptionLabel={(option) => option.name}
|
||||
@ -261,9 +338,36 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
||||
</Box>
|
||||
{approved && (<>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', ml: 3 }}>
|
||||
<FormControlLabel control={<Checkbox />} label="Recurring Mailing" />
|
||||
<FormControlLabel control={<Checkbox />} label="Schedule for Later" />
|
||||
<DatePicker />
|
||||
<FormControlLabel control={<Checkbox checked={recurring} onChange={handleRecurringChange} />} label="Recurring Mailing" />
|
||||
{recurring && (<>
|
||||
TODO: RECURRING OPTIONS HERE
|
||||
</>)}
|
||||
<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) => {
|
||||
field.onChange(newValue ? newValue.toDate() : null); // Convert back to Date
|
||||
trigger("scheduleDate"); // Revalidate
|
||||
}}
|
||||
slotProps={{
|
||||
textField: {
|
||||
fullWidth: true,
|
||||
margin: "dense",
|
||||
error: !!errors.scheduleDate,
|
||||
helperText: errors.scheduleDate?.message,
|
||||
},
|
||||
}}
|
||||
minDateTime={dayjs()} // Enforce future date in UI
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</>)}
|
||||
{/*<Controller
|
||||
@ -329,6 +433,21 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
||||
)}
|
||||
/>
|
||||
*/}
|
||||
|
||||
{templateViewerOpen && (
|
||||
<TemplateViewer
|
||||
open={templateViewerOpen}
|
||||
template={currentTemplate!}
|
||||
onClose={() => { setTemplateViewerOpen(false) }}
|
||||
/>
|
||||
)}
|
||||
{targetSampleViewerOpen && (
|
||||
<TargetSampleViewer
|
||||
open={targetSampleViewerOpen}
|
||||
target={currentTarget!}
|
||||
onClose={() => { setTargetSampleViewerOpen(false) }}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => { onClose( 'cancelled'); }} disabled={loading}>Cancel</Button>
|
||||
@ -337,6 +456,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</LocalizationProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -0,0 +1,101 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
IconButton,
|
||||
Box,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
} from "@mui/material";
|
||||
import Target from "@/types/target";
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { DataGrid, GridColDef } from '@mui/x-data-grid';
|
||||
|
||||
type TargetSampleViewerProps = {
|
||||
open: boolean;
|
||||
target: Target;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
type TargetSample = {
|
||||
columnNames: string[];
|
||||
rows: { [key: string]: string }[];
|
||||
};
|
||||
|
||||
const TargetSampleViewer = ({ open, target, onClose }: TargetSampleViewerProps) => {
|
||||
const [targetSample, setTargetSample] = useState<TargetSample | null>(null); // Store the full sample response
|
||||
const [loading, setLoading] = useState(false); // Optional: Add loading state
|
||||
|
||||
// Fetch sample data when dialog opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const fetchSampleData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/targets/${target.id}/sample`);
|
||||
if (!response.ok) throw new Error("Failed to fetch sample data");
|
||||
const data: TargetSample = await response.json();
|
||||
setTargetSample(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching target sample:", error);
|
||||
setTargetSample(null); // Reset on error
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchSampleData();
|
||||
}
|
||||
}, [open, target.id]);
|
||||
|
||||
// Transform columnNames into GridColDef array
|
||||
const columns: GridColDef[] = targetSample?.columnNames?.map((colName) => ({
|
||||
field: colName,
|
||||
headerName: colName.charAt(0).toUpperCase() + colName.slice(1), // Capitalize header
|
||||
flex: 1,
|
||||
sortable: true,
|
||||
})) || [];
|
||||
|
||||
// Transform TargetRow into DataGrid-compatible rows
|
||||
const rows = targetSample?.rows?.map((row, index) => {
|
||||
const rowData: { [key: string]: string } = { id: index.toString() }; // Use index as unique id
|
||||
targetSample.columnNames.forEach((key) => {
|
||||
rowData[key] = row[key] ?? ""; // Map each key-value pair to the row object
|
||||
});
|
||||
return rowData;
|
||||
}) || [];
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="lg" fullWidth>
|
||||
<DialogTitle>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||
<span>View Target Sample "{target.name}"</span>
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
onClick={onClose}
|
||||
sx={{ marginLeft: "auto" }}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DataGrid
|
||||
rows={rows} // Use transformed rows
|
||||
columns={columns} // Use transformed columns
|
||||
autoPageSize
|
||||
loading={loading} // Show loading state
|
||||
sx={{ width: "100%", height: "calc(90vh - 120px)" }}
|
||||
initialState={{
|
||||
pagination: {
|
||||
paginationModel: {
|
||||
pageSize: 20,
|
||||
},
|
||||
},
|
||||
}}
|
||||
pageSizeOptions={[10, 20, 50, 100]}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default TargetSampleViewer;
|
||||
@ -0,0 +1,43 @@
|
||||
import {
|
||||
IconButton,
|
||||
Box,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent
|
||||
} from "@mui/material";
|
||||
import Template from "@/types/template";
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
|
||||
type TemplateViewerProps = {
|
||||
open: boolean;
|
||||
template: Template;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const TemplateViewer = ({ open, template, onClose }: TemplateViewerProps) => {
|
||||
return (
|
||||
<Dialog open={open} onClose={() => { onClose(); }} maxWidth="md" fullWidth >
|
||||
<DialogTitle>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||
<span>View Template "{template.name}"</span>
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
onClick={onClose}
|
||||
sx={{ marginLeft: "auto" }} // Pushes button to the right
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<iframe
|
||||
srcDoc={template.htmlBody ?? ""}
|
||||
sandbox="allow-same-origin"
|
||||
style={{ width: "100%", height: "calc(90vh - 120px)", border: "none" }}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateViewer;
|
||||
Loading…
x
Reference in New Issue
Block a user