E-Commerce-Module/frontend/src/pages/Admin/CampaignSendPage.jsx

628 lines
No EOL
21 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import {
Box,
Typography,
Paper,
Button,
Stepper,
Step,
StepLabel,
Grid,
Card,
CardContent,
CircularProgress,
Alert,
TextField,
FormControlLabel,
Checkbox,
Radio,
RadioGroup,
FormControl,
FormLabel,
Chip,
Divider,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
IconButton,
List,
ListItem,
ListItemText,
Tooltip,
MenuItem,
Select,
InputLabel
} from '@mui/material';
import {
ArrowBack as ArrowBackIcon,
Send as SendIcon,
Schedule as ScheduleIcon,
Preview as PreviewIcon,
InfoOutlined as InfoIcon,
WarningAmber as WarningIcon,
CheckCircle as CheckCircleIcon
} from '@mui/icons-material';
import { useNavigate, useParams } from 'react-router-dom';
import { format, addHours, addDays, setHours, setMinutes } from 'date-fns';
import {
useEmailCampaign,
useMailingList,
useSendCampaign,
useScheduleCampaign,
usePreviewCampaign
} from '@hooks/emailCampaignHooks';
const CampaignSendPage = () => {
const navigate = useNavigate();
const { id } = useParams();
// Stepper state
const [activeStep, setActiveStep] = useState(0);
const steps = ['Review Campaign', 'Select Delivery Options', 'Confirm & Send'];
// Custom date time picker state
const initialDate = addHours(new Date(), 1);
const [scheduledDate, setScheduledDate] = useState(initialDate);
const [selectedDate, setSelectedDate] = useState(format(initialDate, 'yyyy-MM-dd'));
const [selectedHour, setSelectedHour] = useState(initialDate.getHours());
const [selectedMinute, setSelectedMinute] = useState(initialDate.getMinutes());
// Delivery options
const [deliveryOption, setDeliveryOption] = useState('send_now');
const [confirmChecked, setConfirmChecked] = useState(false);
// Preview dialog
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
const [testEmailAddress, setTestEmailAddress] = useState('');
// Fetch campaign and related data
const { data: campaign, isLoading: campaignLoading, error: campaignError } = useEmailCampaign(id);
// Load lists information for selected lists
const { data: listData, isLoading: listLoading } = useMailingList(
campaign?.list_ids?.length > 0 ? campaign.list_ids[0] : null
);
// Mutations
const sendCampaign = useSendCampaign();
const scheduleCampaign = useScheduleCampaign();
const previewCampaign = usePreviewCampaign();
// Generate hours and minutes for dropdowns
const hours = Array.from({ length: 24 }, (_, i) => i);
const minutes = Array.from({ length: 60 }, (_, i) => i);
// Update the scheduledDate when date/time components change
useEffect(() => {
try {
const dateObj = new Date(selectedDate);
dateObj.setHours(selectedHour);
dateObj.setMinutes(selectedMinute);
setScheduledDate(dateObj);
} catch (error) {
console.error("Invalid date selection:", error);
}
}, [selectedDate, selectedHour, selectedMinute]);
// Handle step changes
const handleNext = () => {
setActiveStep((prevStep) => Math.min(prevStep + 1, steps.length - 1));
};
const handleBack = () => {
setActiveStep((prevStep) => Math.max(prevStep - 1, 0));
};
// Handle delivery option change
const handleDeliveryOptionChange = (event) => {
setDeliveryOption(event.target.value);
};
// Handle date change
const handleDateChange = (event) => {
setSelectedDate(event.target.value);
};
// Handle hour change
const handleHourChange = (event) => {
setSelectedHour(Number(event.target.value));
};
// Handle minute change
const handleMinuteChange = (event) => {
setSelectedMinute(Number(event.target.value));
};
// Handle preview
const handleOpenPreview = () => {
setPreviewDialogOpen(true);
};
// Send test email
const handleSendTest = async () => {
if (!testEmailAddress || !campaign) return;
try {
await previewCampaign.mutateAsync({
campaignId: campaign.id,
email: testEmailAddress
});
// Clear the field after successful send
if (!previewCampaign.error) {
setTestEmailAddress('');
}
} catch (error) {
console.error('Failed to send test email:', error);
}
};
// Handle final campaign send/schedule
const handleSendCampaign = async () => {
if (!campaign) return;
try {
if (deliveryOption === 'send_now') {
await sendCampaign.mutateAsync(campaign.id);
} else {
await scheduleCampaign.mutateAsync({
campaignId: campaign.id,
scheduledDate: scheduledDate.toISOString()
});
}
// Navigate back to campaigns list on success
if (!sendCampaign.error && !scheduleCampaign.error) {
navigate('/admin/email-campaigns');
}
} catch (error) {
console.error('Failed to send/schedule campaign:', error);
}
};
// Get estimated recipient count
const getRecipientCount = () => {
if (!campaign || !campaign.list_ids || campaign.list_ids.length === 0) {
return 0;
}
return listData?.subscriber_count || 0;
};
// Format hour for display (add leading zero if needed)
const formatTimeValue = (value) => {
return value.toString().padStart(2, '0');
};
// Validate that selected date is in the future
const isDateInPast = () => {
const now = new Date();
return scheduledDate < now;
};
// Loading state
if (campaignLoading) {
return <Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}><CircularProgress /></Box>;
}
// Error state
if (campaignError) {
return <Alert severity="error" sx={{ my: 2 }}>{campaignError.message}</Alert>;
}
// Campaign not found
if (!campaign) {
return (
<Alert severity="warning" sx={{ my: 2 }}>
Campaign not found. Please select a valid campaign.
</Alert>
);
}
return (
<Box>
{/* Header with back button */}
<Box mb={3} display="flex" alignItems="center">
<IconButton onClick={() => navigate(`/admin/email-campaigns/${id}`)} sx={{ mr: 1 }}>
<ArrowBackIcon />
</IconButton>
<Typography variant="h4">
Send Campaign: {campaign.name}
</Typography>
</Box>
{/* Stepper */}
<Stepper activeStep={activeStep} sx={{ mb: 4 }}>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
{/* Step content */}
<Box mb={4}>
{/* Step 1: Review Campaign */}
{activeStep === 0 && (
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>Review Campaign Details</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Card variant="outlined" sx={{ height: '100%' }}>
<CardContent>
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
Campaign Details
</Typography>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 2fr', gap: 1 }}>
<Typography variant="body2" color="text.secondary">Name:</Typography>
<Typography variant="body2">{campaign.name}</Typography>
<Typography variant="body2" color="text.secondary">Subject:</Typography>
<Typography variant="body2">{campaign.subject}</Typography>
<Typography variant="body2" color="text.secondary">From:</Typography>
<Typography variant="body2">
{campaign.from_name} ({campaign.from_email})
</Typography>
{campaign.preheader && (
<>
<Typography variant="body2" color="text.secondary">Preheader:</Typography>
<Typography variant="body2">{campaign.preheader}</Typography>
</>
)}
</Box>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card variant="outlined" sx={{ height: '100%' }}>
<CardContent>
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
Audience
</Typography>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" color="text.secondary" gutterBottom>
Mailing Lists:
</Typography>
{listLoading ? (
<CircularProgress size={20} />
) : (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
<Chip
label={`${listData?.name} (${listData?.subscriber_count || 0} subscribers)`}
color="primary"
size="small"
/>
</Box>
)}
</Box>
<Box sx={{ mt: 3 }}>
<Typography variant="body2" gutterBottom>
<strong>Estimated Recipients:</strong> {getRecipientCount()}
</Typography>
{getRecipientCount() === 0 && (
<Alert severity="warning" sx={{ mt: 1 }}>
No recipients in selected mailing lists. Your campaign won't be delivered to anyone.
</Alert>
)}
</Box>
</CardContent>
</Card>
</Grid>
</Grid>
<Box sx={{ mt: 4, display: 'flex', justifyContent: 'space-between' }}>
<Button onClick={handleOpenPreview} startIcon={<PreviewIcon />}>
Preview Campaign
</Button>
<Button
variant="contained"
onClick={handleNext}
disabled={getRecipientCount() === 0}
>
Next: Delivery Options
</Button>
</Box>
</Paper>
)}
{/* Step 2: Delivery Options */}
{activeStep === 1 && (
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>Delivery Options</Typography>
<FormControl component="fieldset" sx={{ mb: 4 }}>
<FormLabel component="legend">When should this campaign be sent?</FormLabel>
<RadioGroup
value={deliveryOption}
onChange={handleDeliveryOptionChange}
>
<FormControlLabel
value="send_now"
control={<Radio />}
label="Send immediately"
/>
<FormControlLabel
value="schedule"
control={<Radio />}
label="Schedule for later"
/>
</RadioGroup>
</FormControl>
{deliveryOption === 'schedule' && (
<Box sx={{ mb: 4, ml: 4 }}>
{/* Custom Date Time Picker */}
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<TextField
label="Select Date"
type="date"
fullWidth
value={selectedDate}
onChange={handleDateChange}
InputLabelProps={{ shrink: true }}
inputProps={{ min: format(new Date(), 'yyyy-MM-dd') }}
/>
</Grid>
<Grid item xs={12} sm={6}>
<Grid container spacing={2}>
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel id="hour-select-label">Hour</InputLabel>
<Select
labelId="hour-select-label"
value={selectedHour}
onChange={handleHourChange}
label="Hour"
>
{hours.map((hour) => (
<MenuItem key={hour} value={hour}>
{formatTimeValue(hour)}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel id="minute-select-label">Minute</InputLabel>
<Select
labelId="minute-select-label"
value={selectedMinute}
onChange={handleMinuteChange}
label="Minute"
>
{minutes.map((minute) => (
<MenuItem key={minute} value={minute}>
{formatTimeValue(minute)}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
</Grid>
</Grid>
</Grid>
{isDateInPast() && (
<Alert severity="error" sx={{ mt: 2 }}>
Selected time is in the past. Please choose a future date and time.
</Alert>
)}
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
Selected Date/Time: {format(scheduledDate, 'PPpp')}
</Typography>
<Typography variant="caption" color="text.secondary" display="block" mt={1}>
Campaigns are sent in your local timezone: {Intl.DateTimeFormat().resolvedOptions().timeZone}
</Typography>
</Box>
)}
<Divider sx={{ my: 3 }} />
<Typography variant="subtitle1" gutterBottom>
Send Test Email
</Typography>
<Typography variant="body2" paragraph>
Send a test email to verify how your campaign will look before sending to your list.
</Typography>
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
<TextField
label="Test Email Address"
variant="outlined"
size="small"
fullWidth
value={testEmailAddress}
onChange={(e) => setTestEmailAddress(e.target.value)}
/>
<Button
variant="outlined"
onClick={handleSendTest}
disabled={!testEmailAddress || previewCampaign.isLoading}
>
{previewCampaign.isLoading ? <CircularProgress size={24} /> : 'Send Test'}
</Button>
</Box>
{previewCampaign.isSuccess && (
<Alert severity="success" sx={{ mb: 2 }}>
Test email sent successfully!
</Alert>
)}
{previewCampaign.error && (
<Alert severity="error" sx={{ mb: 2 }}>
{previewCampaign.error.message || 'Failed to send test email'}
</Alert>
)}
<Box sx={{ mt: 4, display: 'flex', justifyContent: 'space-between' }}>
<Button onClick={handleBack}>
Back
</Button>
<Button
variant="contained"
onClick={handleNext}
disabled={deliveryOption === 'schedule' && isDateInPast()}
>
Next: Review & Send
</Button>
</Box>
</Paper>
)}
{/* Step 3: Confirm & Send */}
{activeStep === 2 && (
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>Confirm & Send</Typography>
<Alert severity="info" sx={{ mb: 3 }}>
<Typography variant="subtitle1">Please review your campaign before sending</Typography>
<Typography variant="body2">
Once a campaign is sent, it cannot be recalled or edited.
</Typography>
</Alert>
<Box sx={{ mb: 4 }}>
<Typography variant="subtitle1" gutterBottom>Sending Summary</Typography>
<List disablePadding>
<ListItem divider>
<ListItemText
primary="Campaign"
secondary={campaign.name}
/>
</ListItem>
<ListItem divider>
<ListItemText
primary="Subject Line"
secondary={campaign.subject}
/>
</ListItem>
<ListItem divider>
<ListItemText
primary="From"
secondary={`${campaign.from_name} (${campaign.from_email})`}
/>
</ListItem>
<ListItem divider>
<ListItemText
primary="Recipients"
secondary={`${getRecipientCount()} subscribers`}
/>
</ListItem>
<ListItem>
<ListItemText
primary="Delivery"
secondary={
deliveryOption === 'send_now'
? 'Send immediately'
: `Scheduled for ${format(scheduledDate, 'PPpp')}`
}
/>
</ListItem>
</List>
</Box>
<Box sx={{ mb: 3 }}>
<FormControlLabel
control={
<Checkbox
checked={confirmChecked}
onChange={(e) => setConfirmChecked(e.target.checked)}
/>
}
label="I confirm that this campaign is ready to send"
/>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Button onClick={handleBack}>
Back
</Button>
<Button
variant="contained"
color="primary"
startIcon={deliveryOption === 'send_now' ? <SendIcon /> : <ScheduleIcon />}
onClick={handleSendCampaign}
disabled={!confirmChecked || sendCampaign.isLoading || scheduleCampaign.isLoading || (deliveryOption === 'schedule' && isDateInPast())}
>
{sendCampaign.isLoading || scheduleCampaign.isLoading ? (
<CircularProgress size={24} />
) : (
deliveryOption === 'send_now' ? 'Send Campaign' : 'Schedule Campaign'
)}
</Button>
</Box>
{(sendCampaign.error || scheduleCampaign.error) && (
<Alert severity="error" sx={{ mt: 3 }}>
{sendCampaign.error?.message || scheduleCampaign.error?.message || 'An error occurred'}
</Alert>
)}
</Paper>
)}
</Box>
{/* Preview Dialog */}
<Dialog
open={previewDialogOpen}
onClose={() => setPreviewDialogOpen(false)}
maxWidth="md"
fullWidth
>
<DialogTitle>Campaign Preview</DialogTitle>
<DialogContent>
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" gutterBottom>Subject:</Typography>
<Typography variant="body1">{campaign.subject}</Typography>
{campaign.preheader && (
<>
<Typography variant="subtitle2" gutterBottom sx={{ mt: 2 }}>Preheader:</Typography>
<Typography variant="body1">{campaign.preheader}</Typography>
</>
)}
<Divider sx={{ my: 2 }} />
<Typography variant="subtitle2" gutterBottom>Email Content:</Typography>
<Box
sx={{
border: '1px solid',
borderColor: 'divider',
p: 2,
maxHeight: '60vh',
overflow: 'auto',
bgcolor: 'background.paper'
}}
>
<Box dangerouslySetInnerHTML={{ __html: campaign.content }} />
</Box>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setPreviewDialogOpen(false)}>Close</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default CampaignSendPage;