Initial BizOne portal setup
This commit is contained in:
324
frontend/src/components/campaign-detail-actions.tsx
Normal file
324
frontend/src/components/campaign-detail-actions.tsx
Normal file
@ -0,0 +1,324 @@
|
||||
'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',
|
||||
});
|
||||
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',
|
||||
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',
|
||||
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' });
|
||||
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',
|
||||
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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user