mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
The Pro-badged Schedule for Later tile in step 4 was a non-interactive div — no handler, no scheduling backend, and no Pro tier in the product. Send Immediately was the only path that did anything, so the picker itself was decorative. Remove the section (and the now-unused timing state / Clock / Lock imports). The wizard still completes Send Immediately end-to-end via the existing Send Broadcast button below. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
237 lines
7.6 KiB
TypeScript
237 lines
7.6 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { createClient } from '@/lib/supabase/client';
|
|
import { MessageTemplate } from '@/types';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from '@/components/ui/dialog';
|
|
import { ArrowLeft, Send, Loader2, Users, Save } from 'lucide-react';
|
|
|
|
interface AudienceConfig {
|
|
type: string;
|
|
tagIds?: string[];
|
|
csvContacts?: { phone: string; name?: string }[];
|
|
}
|
|
|
|
interface Step4Props {
|
|
name: string;
|
|
onNameChange: (name: string) => void;
|
|
template: MessageTemplate;
|
|
audience: AudienceConfig;
|
|
onSend: () => void;
|
|
onSaveDraft?: () => void;
|
|
onBack: () => void;
|
|
isProcessing: boolean;
|
|
progress: number;
|
|
}
|
|
|
|
export function Step4ScheduleSend({
|
|
name,
|
|
onNameChange,
|
|
template,
|
|
audience,
|
|
onSend,
|
|
onSaveDraft,
|
|
onBack,
|
|
isProcessing,
|
|
progress,
|
|
}: Step4Props) {
|
|
const [showConfirm, setShowConfirm] = useState(false);
|
|
const [estimatedReach, setEstimatedReach] = useState<number>(0);
|
|
const [loadingReach, setLoadingReach] = useState(true);
|
|
|
|
useEffect(() => {
|
|
async function calculateReach() {
|
|
setLoadingReach(true);
|
|
try {
|
|
const supabase = createClient();
|
|
|
|
if (audience.type === 'all') {
|
|
const { count } = await supabase
|
|
.from('contacts')
|
|
.select('*', { count: 'exact', head: true });
|
|
setEstimatedReach(count ?? 0);
|
|
} else if (audience.type === 'tags' && audience.tagIds && audience.tagIds.length > 0) {
|
|
const { data: contactTags } = await supabase
|
|
.from('contact_tags')
|
|
.select('contact_id')
|
|
.in('tag_id', audience.tagIds);
|
|
|
|
const uniqueIds = new Set((contactTags ?? []).map((ct) => ct.contact_id));
|
|
setEstimatedReach(uniqueIds.size);
|
|
} else if (audience.type === 'csv' && audience.csvContacts) {
|
|
setEstimatedReach(audience.csvContacts.length);
|
|
} else {
|
|
setEstimatedReach(0);
|
|
}
|
|
} finally {
|
|
setLoadingReach(false);
|
|
}
|
|
}
|
|
|
|
calculateReach();
|
|
}, [audience]);
|
|
|
|
const audienceLabel =
|
|
audience.type === 'all'
|
|
? 'All Contacts'
|
|
: audience.type === 'tags'
|
|
? `Tags (${audience.tagIds?.length ?? 0} selected)`
|
|
: audience.type === 'csv'
|
|
? 'CSV Upload'
|
|
: 'Custom';
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-white">Review & Send</h2>
|
|
<p className="mt-1 text-sm text-slate-400">
|
|
Name your broadcast, review the details, and send.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Broadcast Name */}
|
|
<div>
|
|
<label className="mb-1.5 block text-sm font-medium text-white">Broadcast Name</label>
|
|
<Input
|
|
value={name}
|
|
onChange={(e) => onNameChange(e.target.value)}
|
|
placeholder="e.g. Summer Sale Announcement"
|
|
className="border-slate-700 bg-slate-800 text-white placeholder:text-slate-500"
|
|
/>
|
|
</div>
|
|
|
|
{/* Summary Card */}
|
|
<div className="rounded-xl border border-slate-800 bg-slate-900/50 p-4 space-y-3">
|
|
<p className="text-sm font-medium text-white">Summary</p>
|
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
|
<div>
|
|
<p className="text-xs text-slate-400">Template</p>
|
|
<p className="text-white">{template.name}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-slate-400">Audience</p>
|
|
<p className="text-white">{audienceLabel}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-slate-400">Estimated Reach</p>
|
|
<div className="flex items-center gap-1.5">
|
|
{loadingReach ? (
|
|
<Loader2 className="h-3 w-3 animate-spin text-violet-500" />
|
|
) : (
|
|
<>
|
|
<Users className="h-3.5 w-3.5 text-violet-400" />
|
|
<p className="font-medium text-white">{estimatedReach.toLocaleString()}</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-slate-400">Language</p>
|
|
<p className="text-white">{template.language ?? 'en_US'}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Processing overlay */}
|
|
{isProcessing && (
|
|
<div className="rounded-xl border border-violet-500/20 bg-violet-500/5 p-4">
|
|
<div className="mb-2 flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Loader2 className="h-4 w-4 animate-spin text-violet-500" />
|
|
<p className="text-sm font-medium text-white">Sending broadcast...</p>
|
|
</div>
|
|
<span className="text-xs font-medium text-violet-400">{progress}%</span>
|
|
</div>
|
|
<div className="h-1.5 w-full rounded-full bg-slate-800">
|
|
<div
|
|
className="h-1.5 rounded-full bg-violet-500 transition-all duration-300"
|
|
style={{ width: `${progress}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-slate-800 pt-4">
|
|
<Button
|
|
variant="outline"
|
|
onClick={onBack}
|
|
disabled={isProcessing}
|
|
className="border-slate-700 text-slate-300"
|
|
>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
Back
|
|
</Button>
|
|
|
|
<div className="flex items-center gap-2">
|
|
{onSaveDraft && (
|
|
<Button
|
|
variant="outline"
|
|
onClick={onSaveDraft}
|
|
disabled={!name.trim() || isProcessing}
|
|
className="border-slate-700 text-slate-300 hover:bg-slate-800 disabled:opacity-50"
|
|
>
|
|
<Save className="h-4 w-4" />
|
|
Save as Draft
|
|
</Button>
|
|
)}
|
|
|
|
<Dialog open={showConfirm} onOpenChange={setShowConfirm}>
|
|
<DialogTrigger
|
|
render={
|
|
<Button
|
|
disabled={!name.trim() || isProcessing}
|
|
className="bg-violet-600 text-white hover:bg-violet-700 disabled:opacity-50"
|
|
/>
|
|
}
|
|
>
|
|
<Send className="h-4 w-4" />
|
|
Send Broadcast
|
|
</DialogTrigger>
|
|
<DialogContent className="border-slate-700 bg-slate-900 sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-white">Confirm Broadcast</DialogTitle>
|
|
<DialogDescription className="text-slate-400">
|
|
You are about to send this broadcast to{' '}
|
|
<span className="font-medium text-white">{estimatedReach.toLocaleString()}</span>{' '}
|
|
contacts using the{' '}
|
|
<span className="font-medium text-white">{template.name}</span> template.
|
|
This action cannot be undone.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setShowConfirm(false)}
|
|
className="border-slate-700 text-slate-300"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={() => {
|
|
setShowConfirm(false);
|
|
onSend();
|
|
}}
|
|
className="bg-violet-600 text-white hover:bg-violet-700"
|
|
>
|
|
<Send className="h-4 w-4" />
|
|
Confirm & Send
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|