Initial BizOne portal setup

This commit is contained in:
2026-05-11 11:36:33 +07:00
commit 57017dd397
249 changed files with 41305 additions and 0 deletions

View 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}
</>
);
}