|
|
@@ -3,7 +3,7 @@
|
|
|
import { useState } from 'react';
|
|
|
import {
|
|
|
RotateCcw, CheckCircle, ChevronDown, ChevronUp, Terminal, FileJson,
|
|
|
- User, History, Edit, MessageCircle, MessageSquare, X
|
|
|
+ User, History, Edit, MessageCircle, MessageSquare, X, Pause, Mail
|
|
|
} from 'lucide-react';
|
|
|
import LocalTime from '@/components/common/LocalTime';
|
|
|
import api from '@/lib/api';
|
|
|
@@ -31,15 +31,17 @@ interface TaskTableProps {
|
|
|
loading: boolean;
|
|
|
onRetry: (taskId: number) => void;
|
|
|
onManualConfirm: (taskId: number) => void;
|
|
|
+ onPause: (taskId: number) => void;
|
|
|
onEdit: (task: VasTask) => void;
|
|
|
onViewDetail: (task: VasTask) => void;
|
|
|
}
|
|
|
|
|
|
-export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, onEdit }: TaskTableProps) {
|
|
|
+export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, onPause, onEdit }: TaskTableProps) {
|
|
|
const [expandedRows, setExpandedRows] = useState<Set<number>>(new Set());
|
|
|
const [isMessageOpen, setIsMessageOpen] = useState(false);
|
|
|
- const [messageChannel, setMessageChannel] = useState<'sms' | 'whatsapp'>('sms');
|
|
|
+ const [messageChannel, setMessageChannel] = useState<'sms' | 'whatsapp' | 'email'>('sms');
|
|
|
const [messageTask, setMessageTask] = useState<VasTask | null>(null);
|
|
|
+ const [contactDropdownOpen, setContactDropdownOpen] = useState<number | null>(null);
|
|
|
const [messageText, setMessageText] = useState('');
|
|
|
const [messageSending, setMessageSending] = useState(false);
|
|
|
const [messageResult, setMessageResult] = useState<'idle' | 'success' | 'error'>('idle');
|
|
|
@@ -52,10 +54,15 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
|
|
|
setExpandedRows(newSet);
|
|
|
};
|
|
|
|
|
|
- const openMessageModal = (task: VasTask, channel: 'sms' | 'whatsapp') => {
|
|
|
- const template = channel === 'whatsapp'
|
|
|
- ? 'Visafly: Your appointment status changed. Please review updates and confirm next steps.'
|
|
|
- : 'Your appointment status changed. Please review updates and confirm next steps.';
|
|
|
+ const openMessageModal = (task: VasTask, channel: 'sms' | 'whatsapp' | 'email') => {
|
|
|
+ let template = '';
|
|
|
+ if (channel === 'whatsapp') {
|
|
|
+ template = 'Visafly: Your appointment status changed. Please review updates and confirm next steps.';
|
|
|
+ } else if (channel === 'sms') {
|
|
|
+ template = 'Your appointment status changed. Please review updates and confirm next steps.';
|
|
|
+ } else {
|
|
|
+ template = 'Your appointment status changed. Please review updates and confirm next steps.';
|
|
|
+ }
|
|
|
setMessageTask(task);
|
|
|
setMessageChannel(channel);
|
|
|
setMessageText(template);
|
|
|
@@ -73,17 +80,19 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
|
|
|
const handleSendMessage = async () => {
|
|
|
if (!messageTask) return;
|
|
|
const target = getRecipientPhoneRaw(messageTask);
|
|
|
- if (!target) {
|
|
|
- setMessageResult('error');
|
|
|
- setMessageResultText('Missing recipient phone number.');
|
|
|
- return;
|
|
|
- }
|
|
|
+ const email = getRecipientEmail(messageTask);
|
|
|
setMessageSending(true);
|
|
|
setMessageResult('idle');
|
|
|
setMessageResultText('');
|
|
|
|
|
|
try {
|
|
|
if (messageChannel === 'whatsapp') {
|
|
|
+ if (!target) {
|
|
|
+ setMessageResult('error');
|
|
|
+ setMessageResultText('Missing WhatsApp number.');
|
|
|
+ setMessageSending(false);
|
|
|
+ return;
|
|
|
+ }
|
|
|
const res = await api.post('/api/whatsapp/send_no_token', {
|
|
|
chat_id: target,
|
|
|
message: messageText,
|
|
|
@@ -91,7 +100,13 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
|
|
|
const ok = res.data?.code === 0;
|
|
|
setMessageResult(ok ? 'success' : 'error');
|
|
|
setMessageResultText(ok ? 'WhatsApp sent successfully.' : (res.data?.message || 'Failed to send WhatsApp.'));
|
|
|
- } else {
|
|
|
+ } else if (messageChannel === 'sms') {
|
|
|
+ if (!target) {
|
|
|
+ setMessageResult('error');
|
|
|
+ setMessageResultText('Missing phone number.');
|
|
|
+ setMessageSending(false);
|
|
|
+ return;
|
|
|
+ }
|
|
|
const res = await api.post('/api/sms/send', {
|
|
|
send_to: target,
|
|
|
sender: getSmsSender(messageTask),
|
|
|
@@ -100,9 +115,24 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
|
|
|
const ok = res.data?.code === 0;
|
|
|
setMessageResult(ok ? 'success' : 'error');
|
|
|
setMessageResultText(ok ? 'SMS sent successfully.' : (res.data?.message || 'Failed to send SMS.'));
|
|
|
+ } else {
|
|
|
+ if (!email) {
|
|
|
+ setMessageResult('error');
|
|
|
+ setMessageResultText('Missing email address.');
|
|
|
+ setMessageSending(false);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const res = await api.post('/api/email/send', {
|
|
|
+ send_to: email,
|
|
|
+ subject: 'Visafly Notification',
|
|
|
+ content: messageText,
|
|
|
+ });
|
|
|
+ const ok = res.data?.code === 0;
|
|
|
+ setMessageResult(ok ? 'success' : 'error');
|
|
|
+ setMessageResultText(ok ? 'Email sent successfully.' : (res.data?.message || 'Failed to send email.'));
|
|
|
}
|
|
|
} catch (error) {
|
|
|
- const fallback = messageChannel === 'whatsapp' ? 'Failed to send WhatsApp.' : 'Failed to send SMS.';
|
|
|
+ const fallback = messageChannel === 'whatsapp' ? 'Failed to send WhatsApp.' : messageChannel === 'sms' ? 'Failed to send SMS.' : 'Failed to send email.';
|
|
|
setMessageResult('error');
|
|
|
setMessageResultText(fallback);
|
|
|
} finally {
|
|
|
@@ -138,6 +168,11 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
|
|
|
return 'Visafly';
|
|
|
};
|
|
|
|
|
|
+ const getRecipientEmail = (task: VasTask) => {
|
|
|
+ if (!task.user_inputs) return '';
|
|
|
+ return task.user_inputs.email || '';
|
|
|
+ };
|
|
|
+
|
|
|
const getUserSummary = (inputs: any) => {
|
|
|
if (!inputs) return '-';
|
|
|
const name = inputs.social_media_account ? (inputs.social_media_account): `${inputs.first_name} ${inputs.last_name}`;
|
|
|
@@ -179,7 +214,8 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
|
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold uppercase whitespace-nowrap
|
|
|
${status === 'failed' ? 'bg-red-100 text-red-700' :
|
|
|
status === 'grabbed' ? 'bg-purple-100 text-purple-700 animate-pulse' :
|
|
|
- status === 'running' ? 'bg-blue-100 text-blue-700 animate-pulse' :
|
|
|
+ status === 'running' ? 'bg-blue-100 text-blue-700 animate-pulse' :
|
|
|
+ status === 'pause' ? 'bg-orange-100 text-orange-700' :
|
|
|
status === 'completed' ? 'bg-green-100 text-green-700' :
|
|
|
'bg-gray-100 text-gray-600'}`}>
|
|
|
{status}
|
|
|
@@ -252,9 +288,39 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
|
|
|
<div className="flex justify-end gap-1 flex-wrap">
|
|
|
<button onClick={() => onEdit(task)} className="p-1.5 rounded text-indigo-600 hover:bg-indigo-50 border border-transparent hover:border-indigo-100" title="编辑"><Edit size={16} /></button>
|
|
|
<button onClick={() => onRetry(task.id)} className="p-1.5 rounded text-blue-600 hover:bg-blue-50 border border-transparent hover:border-blue-100" title="重置"><RotateCcw size={16} /></button>
|
|
|
+ <button onClick={() => onPause(task.id)} className="p-1.5 rounded text-orange-600 hover:bg-orange-50 border border-transparent hover:border-orange-100" title="暂停"><Pause size={16} /></button>
|
|
|
<button onClick={() => onManualConfirm(task.id)} className="p-1.5 rounded text-green-600 hover:bg-green-50 border border-transparent hover:border-green-100" title="完成"><CheckCircle size={16} /></button>
|
|
|
- <button onClick={() => openMessageModal(task, 'sms')} className="p-1.5 rounded text-amber-600 hover:bg-amber-50 border border-transparent hover:border-amber-100" title="短信提醒"><MessageSquare size={16} /></button>
|
|
|
- <button onClick={() => openMessageModal(task, 'whatsapp')} className="p-1.5 rounded text-emerald-600 hover:bg-emerald-50 border border-transparent hover:border-emerald-100" title="WhatsApp 提醒"><MessageCircle size={16} /></button>
|
|
|
+ <div className="relative">
|
|
|
+ <button
|
|
|
+ onClick={() => setContactDropdownOpen(contactDropdownOpen === task.id ? null : task.id)}
|
|
|
+ className="p-1.5 rounded text-cyan-600 hover:bg-cyan-50 border border-transparent hover:border-cyan-100"
|
|
|
+ title="联系客户"
|
|
|
+ >
|
|
|
+ <Mail size={16} />
|
|
|
+ </button>
|
|
|
+ {contactDropdownOpen === task.id && (
|
|
|
+ <div className="absolute right-0 top-full mt-1 bg-white border border-slate-200 rounded-lg shadow-lg z-10 min-w-[120px]">
|
|
|
+ <button
|
|
|
+ onClick={() => { openMessageModal(task, 'sms'); setContactDropdownOpen(null); }}
|
|
|
+ className="flex items-center gap-2 w-full px-3 py-2 text-sm text-slate-700 hover:bg-amber-50 rounded-t-lg"
|
|
|
+ >
|
|
|
+ <MessageSquare size={14} /> 短信
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ onClick={() => { openMessageModal(task, 'whatsapp'); setContactDropdownOpen(null); }}
|
|
|
+ className="flex items-center gap-2 w-full px-3 py-2 text-sm text-slate-700 hover:bg-emerald-50"
|
|
|
+ >
|
|
|
+ <MessageCircle size={14} /> WhatsApp
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ onClick={() => { openMessageModal(task, 'email'); setContactDropdownOpen(null); }}
|
|
|
+ className="flex items-center gap-2 w-full px-3 py-2 text-sm text-slate-700 hover:bg-blue-50 rounded-b-lg"
|
|
|
+ >
|
|
|
+ <Mail size={14} /> 邮件
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</td>
|
|
|
</tr>
|
|
|
@@ -289,12 +355,27 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
|
|
|
</div>
|
|
|
<span className="text-xs font-mono text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded w-fit truncate max-w-full">{task.routing_key}</span>
|
|
|
</div>
|
|
|
- <div className="flex gap-2 flex-shrink-0">
|
|
|
+<div className="flex gap-2 flex-shrink-0">
|
|
|
<button onClick={() => onRetry(task.id)} className="p-2 bg-blue-50 text-blue-600 rounded-lg active:scale-95"><RotateCcw size={18}/></button>
|
|
|
+ <button onClick={() => onPause(task.id)} className="p-2 bg-orange-50 text-orange-600 rounded-lg active:scale-95"><Pause size={18}/></button>
|
|
|
<button onClick={() => onManualConfirm(task.id)} className="p-2 bg-green-50 text-green-600 rounded-lg active:scale-95"><CheckCircle size={18}/></button>
|
|
|
- <button onClick={() => openMessageModal(task, 'sms')} className="p-2 bg-amber-50 text-amber-600 rounded-lg active:scale-95"><MessageSquare size={18} /></button>
|
|
|
- <button onClick={() => openMessageModal(task, 'whatsapp')} className="p-2 bg-emerald-50 text-emerald-600 rounded-lg active:scale-95"><MessageCircle size={18} /></button>
|
|
|
- </div>
|
|
|
+ <div className="relative">
|
|
|
+ <button onClick={() => setContactDropdownOpen(contactDropdownOpen === task.id ? null : task.id)} className="p-2 bg-cyan-50 text-cyan-600 rounded-lg active:scale-95"><Mail size={18}/></button>
|
|
|
+ {contactDropdownOpen === task.id && (
|
|
|
+ <div className="absolute right-0 top-full mt-1 bg-white border border-slate-200 rounded-lg shadow-lg z-10 min-w-[100px]">
|
|
|
+ <button onClick={() => { openMessageModal(task, 'sms'); setContactDropdownOpen(null); }} className="flex items-center gap-2 w-full px-3 py-2 text-sm text-slate-700 hover:bg-amber-50 rounded-t-lg">
|
|
|
+ <MessageSquare size={14} /> 短信
|
|
|
+ </button>
|
|
|
+ <button onClick={() => { openMessageModal(task, 'whatsapp'); setContactDropdownOpen(null); }} className="flex items-center gap-2 w-full px-3 py-2 text-sm text-slate-700 hover:bg-emerald-50">
|
|
|
+ <MessageCircle size={14} /> WhatsApp
|
|
|
+ </button>
|
|
|
+ <button onClick={() => { openMessageModal(task, 'email'); setContactDropdownOpen(null); }} className="flex items-center gap-2 w-full px-3 py-2 text-sm text-slate-700 hover:bg-blue-50 rounded-b-lg">
|
|
|
+ <Mail size={14} /> 邮件
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
<div className="bg-slate-50 p-3 rounded-lg border border-slate-100 text-sm space-y-2 mb-3">
|
|
|
@@ -339,7 +420,7 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
|
|
|
<div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50">
|
|
|
<div>
|
|
|
<h3 className="font-bold text-slate-900 text-lg">
|
|
|
- {messageChannel === 'sms' ? 'Send SMS' : 'Send WhatsApp'}
|
|
|
+ {messageChannel === 'sms' ? '发送短信' : messageChannel === 'whatsapp' ? '发送 WhatsApp' : '发送邮件'}
|
|
|
</h3>
|
|
|
<p className="text-xs text-slate-500 mt-1">Order #{messageTask.order_id}</p>
|
|
|
</div>
|
|
|
@@ -353,10 +434,17 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
|
|
|
<span className="font-semibold text-slate-500">Recipient</span>
|
|
|
<span className="text-slate-700">{getRecipientName(messageTask)}</span>
|
|
|
</div>
|
|
|
- <div className="flex justify-between">
|
|
|
- <span className="font-semibold text-slate-500">{messageChannel === 'sms' ? 'SMS To' : 'WhatsApp To'}</span>
|
|
|
- <span className="text-slate-700">{getRecipientPhone(messageTask)}</span>
|
|
|
- </div>
|
|
|
+ {messageChannel === 'email' ? (
|
|
|
+ <div className="flex justify-between">
|
|
|
+ <span className="font-semibold text-slate-500">Email To</span>
|
|
|
+ <span className="text-slate-700">{getRecipientEmail(messageTask) || '-'}</span>
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <div className="flex justify-between">
|
|
|
+ <span className="font-semibold text-slate-500">{messageChannel === 'sms' ? 'SMS To' : 'WhatsApp To'}</span>
|
|
|
+ <span className="text-slate-700">{getRecipientPhone(messageTask)}</span>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
{messageChannel === 'sms' && (
|
|
|
<div className="flex justify-between">
|
|
|
<span className="font-semibold text-slate-500">Sender</span>
|