332 lines
12 KiB
TypeScript
332 lines
12 KiB
TypeScript
'use client';
|
|
|
|
import { useRouter } from 'next/navigation';
|
|
import { useState } from 'react';
|
|
|
|
type Props = {
|
|
campaign: {
|
|
id: string;
|
|
name: string;
|
|
status: string;
|
|
totalRecipients: number;
|
|
templateName: string;
|
|
language: string;
|
|
messageTitle: string;
|
|
messageBody: string;
|
|
primaryButton: string;
|
|
secondaryButton: string;
|
|
bannerImageUrl: string;
|
|
};
|
|
};
|
|
|
|
function toDateTimeLocal(value: string | null | undefined) {
|
|
if (!value) return '';
|
|
const date = new Date(value);
|
|
const offset = date.getTimezoneOffset();
|
|
const local = new Date(date.getTime() - offset * 60000);
|
|
return local.toISOString().slice(0, 16);
|
|
}
|
|
|
|
export function CampaignDetailActions({ campaign }: Props) {
|
|
const router = useRouter();
|
|
const [isDuplicating, setIsDuplicating] = useState(false);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [isSendingNow, setIsSendingNow] = useState(false);
|
|
const [isScheduling, setIsScheduling] = useState(false);
|
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
const [message, setMessage] = useState<string | null>(null);
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
const [scheduledAt, setScheduledAt] = useState('');
|
|
const [form, setForm] = useState({
|
|
name: campaign.name,
|
|
status: campaign.status,
|
|
totalRecipients: String(campaign.totalRecipients),
|
|
templateName: campaign.templateName,
|
|
language: campaign.language,
|
|
messageTitle: campaign.messageTitle,
|
|
messageBody: campaign.messageBody,
|
|
primaryButton: campaign.primaryButton,
|
|
secondaryButton: campaign.secondaryButton,
|
|
bannerImageUrl: campaign.bannerImageUrl,
|
|
});
|
|
|
|
async function readPayload(response: Response) {
|
|
const payload = await response.json().catch(() => ({}));
|
|
if (!response.ok) {
|
|
throw new Error(typeof payload?.message === 'string' ? payload.message : 'Request failed');
|
|
}
|
|
|
|
return payload;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="campaign-detail-header-actions">
|
|
<button
|
|
type="button"
|
|
className="campaign-detail-secondary-button"
|
|
onClick={() => {
|
|
setMessage(null);
|
|
setIsEditing((value) => !value);
|
|
}}
|
|
>
|
|
<span className="material-symbols-outlined">edit</span>
|
|
{isEditing ? 'Close Edit' : 'Edit'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="campaign-detail-secondary-button"
|
|
disabled={isDuplicating}
|
|
onClick={async () => {
|
|
try {
|
|
setMessage(null);
|
|
setIsDuplicating(true);
|
|
const response = await fetch(`/api/campaigns/${campaign.id}/duplicate`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
});
|
|
const payload = await readPayload(response);
|
|
router.push(`/dashboard/campaigns/${payload.id}`);
|
|
router.refresh();
|
|
} catch (error) {
|
|
setMessage(error instanceof Error ? error.message : 'Failed to duplicate campaign');
|
|
} finally {
|
|
setIsDuplicating(false);
|
|
}
|
|
}}
|
|
>
|
|
<span className="material-symbols-outlined">content_copy</span>
|
|
{isDuplicating ? 'Duplicating...' : 'Duplicate'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="campaign-detail-secondary-button"
|
|
disabled={isSendingNow}
|
|
onClick={async () => {
|
|
try {
|
|
setMessage(null);
|
|
setIsSendingNow(true);
|
|
const response = await fetch(`/api/campaigns/${campaign.id}/send`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ mode: 'now' }),
|
|
});
|
|
await readPayload(response);
|
|
setMessage('Campaign send queued.');
|
|
router.refresh();
|
|
} catch (error) {
|
|
setMessage(error instanceof Error ? error.message : 'Failed to queue campaign');
|
|
} finally {
|
|
setIsSendingNow(false);
|
|
}
|
|
}}
|
|
>
|
|
<span className="material-symbols-outlined">send</span>
|
|
{isSendingNow ? 'Queuing...' : 'Send Now'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="campaign-detail-secondary-button"
|
|
disabled={isScheduling || !scheduledAt}
|
|
onClick={async () => {
|
|
try {
|
|
setMessage(null);
|
|
setIsScheduling(true);
|
|
const response = await fetch(`/api/campaigns/${campaign.id}/send`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ mode: 'scheduled', scheduledAt: new Date(scheduledAt).toISOString() }),
|
|
});
|
|
await readPayload(response);
|
|
setMessage('Campaign scheduled successfully.');
|
|
router.refresh();
|
|
} catch (error) {
|
|
setMessage(error instanceof Error ? error.message : 'Failed to schedule campaign');
|
|
} finally {
|
|
setIsScheduling(false);
|
|
}
|
|
}}
|
|
>
|
|
<span className="material-symbols-outlined">schedule</span>
|
|
{isScheduling ? 'Scheduling...' : 'Schedule'}
|
|
</button>
|
|
<a className="campaign-detail-secondary-button" href={`/api/campaigns/${campaign.id}/export?format=csv`}>
|
|
<span className="material-symbols-outlined">download</span>
|
|
Export CSV
|
|
</a>
|
|
<a className="campaign-detail-primary-button" href={`/api/campaigns/${campaign.id}/export?format=xlsx`}>
|
|
<span className="material-symbols-outlined">download</span>
|
|
Export XLSX
|
|
</a>
|
|
</div>
|
|
|
|
<div className="campaign-detail-inline-tools">
|
|
<label className="campaign-detail-schedule-field">
|
|
<span>Schedule at</span>
|
|
<input
|
|
type="datetime-local"
|
|
value={scheduledAt}
|
|
onChange={(event) => setScheduledAt(event.target.value)}
|
|
/>
|
|
</label>
|
|
<button
|
|
type="button"
|
|
className="campaign-detail-danger-button"
|
|
disabled={isDeleting}
|
|
onClick={async () => {
|
|
if (!window.confirm(`Delete campaign "${campaign.name}"? This action cannot be undone.`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setMessage(null);
|
|
setIsDeleting(true);
|
|
const response = await fetch(`/api/campaigns/${campaign.id}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include',
|
|
});
|
|
if (!response.ok && response.status !== 204) {
|
|
await readPayload(response);
|
|
}
|
|
router.push('/dashboard/campaigns');
|
|
router.refresh();
|
|
} catch (error) {
|
|
setMessage(error instanceof Error ? error.message : 'Failed to delete campaign');
|
|
} finally {
|
|
setIsDeleting(false);
|
|
}
|
|
}}
|
|
>
|
|
<span className="material-symbols-outlined">delete</span>
|
|
{isDeleting ? 'Deleting...' : 'Delete'}
|
|
</button>
|
|
</div>
|
|
|
|
{message ? <p className="campaign-detail-inline-message">{message}</p> : null}
|
|
|
|
{isEditing ? (
|
|
<form
|
|
className="campaign-detail-edit-panel"
|
|
onSubmit={async (event) => {
|
|
event.preventDefault();
|
|
|
|
try {
|
|
setMessage(null);
|
|
setIsSaving(true);
|
|
const response = await fetch(`/api/campaigns/${campaign.id}`, {
|
|
method: 'PATCH',
|
|
credentials: 'include',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
...form,
|
|
totalRecipients: Number(form.totalRecipients || '0'),
|
|
}),
|
|
});
|
|
await readPayload(response);
|
|
setMessage('Campaign updated successfully.');
|
|
setIsEditing(false);
|
|
router.refresh();
|
|
} catch (error) {
|
|
setMessage(error instanceof Error ? error.message : 'Failed to update campaign');
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
}}
|
|
>
|
|
<div className="campaign-detail-edit-grid">
|
|
<label className="campaign-detail-field">
|
|
<span>Name</span>
|
|
<input
|
|
value={form.name}
|
|
onChange={(event) => setForm((current) => ({ ...current, name: event.target.value }))}
|
|
/>
|
|
</label>
|
|
<label className="campaign-detail-field">
|
|
<span>Status</span>
|
|
<select
|
|
value={form.status}
|
|
onChange={(event) => setForm((current) => ({ ...current, status: event.target.value }))}
|
|
>
|
|
<option value="Draft">Draft</option>
|
|
<option value="Scheduled">Scheduled</option>
|
|
<option value="Sent">Sent</option>
|
|
<option value="Failed">Failed</option>
|
|
</select>
|
|
</label>
|
|
<label className="campaign-detail-field">
|
|
<span>Total Recipients</span>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={form.totalRecipients}
|
|
onChange={(event) => setForm((current) => ({ ...current, totalRecipients: event.target.value }))}
|
|
/>
|
|
</label>
|
|
<label className="campaign-detail-field">
|
|
<span>Template Name</span>
|
|
<input
|
|
value={form.templateName}
|
|
onChange={(event) => setForm((current) => ({ ...current, templateName: event.target.value }))}
|
|
/>
|
|
</label>
|
|
<label className="campaign-detail-field">
|
|
<span>Language</span>
|
|
<input
|
|
value={form.language}
|
|
onChange={(event) => setForm((current) => ({ ...current, language: event.target.value }))}
|
|
/>
|
|
</label>
|
|
<label className="campaign-detail-field">
|
|
<span>Banner URL</span>
|
|
<input
|
|
value={form.bannerImageUrl}
|
|
onChange={(event) => setForm((current) => ({ ...current, bannerImageUrl: event.target.value }))}
|
|
/>
|
|
</label>
|
|
<label className="campaign-detail-field">
|
|
<span>Primary Button</span>
|
|
<input
|
|
value={form.primaryButton}
|
|
onChange={(event) => setForm((current) => ({ ...current, primaryButton: event.target.value }))}
|
|
/>
|
|
</label>
|
|
<label className="campaign-detail-field">
|
|
<span>Secondary Button</span>
|
|
<input
|
|
value={form.secondaryButton}
|
|
onChange={(event) => setForm((current) => ({ ...current, secondaryButton: event.target.value }))}
|
|
/>
|
|
</label>
|
|
<label className="campaign-detail-field is-full">
|
|
<span>Message Title</span>
|
|
<input
|
|
value={form.messageTitle}
|
|
onChange={(event) => setForm((current) => ({ ...current, messageTitle: event.target.value }))}
|
|
/>
|
|
</label>
|
|
<label className="campaign-detail-field is-full">
|
|
<span>Message Body</span>
|
|
<textarea
|
|
rows={4}
|
|
value={form.messageBody}
|
|
onChange={(event) => setForm((current) => ({ ...current, messageBody: event.target.value }))}
|
|
/>
|
|
</label>
|
|
</div>
|
|
<div className="campaign-detail-edit-actions">
|
|
<button type="button" className="campaign-detail-secondary-button" onClick={() => setIsEditing(false)}>
|
|
Cancel
|
|
</button>
|
|
<button type="submit" className="campaign-detail-primary-button" disabled={isSaving}>
|
|
<span className="material-symbols-outlined">save</span>
|
|
{isSaving ? 'Saving...' : 'Save Changes'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
) : null}
|
|
</>
|
|
);
|
|
}
|