|
@@ -3,7 +3,7 @@
|
|
|
import { useState } from 'react';
|
|
import { useState } from 'react';
|
|
|
import {
|
|
import {
|
|
|
RotateCcw, CheckCircle, ChevronDown, ChevronUp, Terminal, FileJson,
|
|
RotateCcw, CheckCircle, ChevronDown, ChevronUp, Terminal, FileJson,
|
|
|
- User, History, Edit
|
|
|
|
|
|
|
+ User, History, Edit, MessageCircle, MessageSquare, X
|
|
|
} from 'lucide-react';
|
|
} from 'lucide-react';
|
|
|
import LocalTime from '@/components/common/LocalTime';
|
|
import LocalTime from '@/components/common/LocalTime';
|
|
|
|
|
|
|
@@ -36,6 +36,10 @@ interface TaskTableProps {
|
|
|
|
|
|
|
|
export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, onEdit }: TaskTableProps) {
|
|
export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, onEdit }: TaskTableProps) {
|
|
|
const [expandedRows, setExpandedRows] = useState<Set<number>>(new Set());
|
|
const [expandedRows, setExpandedRows] = useState<Set<number>>(new Set());
|
|
|
|
|
+ const [isMessageOpen, setIsMessageOpen] = useState(false);
|
|
|
|
|
+ const [messageChannel, setMessageChannel] = useState<'sms' | 'whatsapp'>('sms');
|
|
|
|
|
+ const [messageTask, setMessageTask] = useState<VasTask | null>(null);
|
|
|
|
|
+ const [messageText, setMessageText] = useState('');
|
|
|
|
|
|
|
|
const toggleRow = (id: number) => {
|
|
const toggleRow = (id: number) => {
|
|
|
const newSet = new Set(expandedRows);
|
|
const newSet = new Set(expandedRows);
|
|
@@ -44,6 +48,42 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
|
|
|
setExpandedRows(newSet);
|
|
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.';
|
|
|
|
|
+ setMessageTask(task);
|
|
|
|
|
+ setMessageChannel(channel);
|
|
|
|
|
+ setMessageText(template);
|
|
|
|
|
+ setIsMessageOpen(true);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const closeMessageModal = () => {
|
|
|
|
|
+ setIsMessageOpen(false);
|
|
|
|
|
+ setMessageTask(null);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const getRecipientName = (task: VasTask) => {
|
|
|
|
|
+ if (!task.user_inputs) return '-';
|
|
|
|
|
+ const { first_name, last_name, social_media_account } = task.user_inputs;
|
|
|
|
|
+ if (social_media_account) return social_media_account;
|
|
|
|
|
+ const full = `${first_name || ''} ${last_name || ''}`.trim();
|
|
|
|
|
+ return full || 'Customer';
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const getRecipientPhone = (task: VasTask) => {
|
|
|
|
|
+ if (!task.user_inputs) return '-';
|
|
|
|
|
+ const code = task.user_inputs.phone_country_code || '';
|
|
|
|
|
+ const rawPhone = String(task.user_inputs.phone || '');
|
|
|
|
|
+ const phone = rawPhone.replace(/^0+/, '');
|
|
|
|
|
+ const combined = `${code} ${phone}`.trim();
|
|
|
|
|
+ return combined || '-';
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const getSmsSender = (task: VasTask) => {
|
|
|
|
|
+ return 'Visafly';
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
const getUserSummary = (inputs: any) => {
|
|
const getUserSummary = (inputs: any) => {
|
|
|
if (!inputs) return '-';
|
|
if (!inputs) return '-';
|
|
|
const name = inputs.social_media_account ? (inputs.social_media_account): `${inputs.first_name} ${inputs.last_name}`;
|
|
const name = inputs.social_media_account ? (inputs.social_media_account): `${inputs.first_name} ${inputs.last_name}`;
|
|
@@ -159,6 +199,8 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
|
|
|
<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={() => 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={() => 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={() => 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={() => 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>
|
|
</div>
|
|
|
</td>
|
|
</td>
|
|
|
</tr>
|
|
</tr>
|
|
@@ -196,6 +238,8 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
|
|
|
<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={() => onRetry(task.id)} className="p-2 bg-blue-50 text-blue-600 rounded-lg active:scale-95"><RotateCcw 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={() => 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>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -234,6 +278,61 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
|
|
|
);
|
|
);
|
|
|
})}
|
|
})}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+ {isMessageOpen && messageTask && (
|
|
|
|
|
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
|
|
|
|
|
+ <div className="bg-white rounded-xl shadow-2xl w-full max-w-lg flex flex-col animate-in zoom-in duration-200">
|
|
|
|
|
+ <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'}
|
|
|
|
|
+ </h3>
|
|
|
|
|
+ <p className="text-xs text-slate-500 mt-1">Order #{messageTask.order_id}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <button onClick={closeMessageModal} className="text-slate-400 hover:text-slate-600 p-1 rounded-full hover:bg-slate-100 transition">
|
|
|
|
|
+ <X size={20} />
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="p-6 space-y-3">
|
|
|
|
|
+ <div className="rounded-lg border border-slate-200 bg-slate-50 p-3 text-xs text-slate-600 space-y-1">
|
|
|
|
|
+ <div className="flex justify-between">
|
|
|
|
|
+ <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 === 'sms' && (
|
|
|
|
|
+ <div className="flex justify-between">
|
|
|
|
|
+ <span className="font-semibold text-slate-500">Sender</span>
|
|
|
|
|
+ <span className="text-slate-700">{getSmsSender(messageTask)}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <label className="text-xs font-bold uppercase text-slate-500">Message (English)</label>
|
|
|
|
|
+ <textarea
|
|
|
|
|
+ rows={4}
|
|
|
|
|
+ value={messageText}
|
|
|
|
|
+ onChange={(e) => setMessageText(e.target.value)}
|
|
|
|
|
+ className="w-full border border-slate-200 rounded-lg p-3 text-sm text-slate-700 focus:ring-2 focus:ring-blue-500 outline-none"
|
|
|
|
|
+ />
|
|
|
|
|
+ <p className="text-xs text-slate-400">Keep it concise (about 20 words).</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="px-6 py-4 border-t bg-slate-50 flex justify-end gap-3">
|
|
|
|
|
+ <button onClick={closeMessageModal} className="px-4 py-2 border border-slate-200 rounded-lg text-slate-600 text-sm hover:bg-slate-100">
|
|
|
|
|
+ Cancel
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={closeMessageModal}
|
|
|
|
|
+ className="px-5 py-2 bg-slate-900 text-white rounded-lg text-sm font-semibold hover:bg-slate-800"
|
|
|
|
|
+ >
|
|
|
|
|
+ Send
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
@@ -326,4 +425,4 @@ function TaskDetailContent({ task }: { task: VasTask }) {
|
|
|
|
|
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
|
-}
|
|
|
|
|
|
|
+}
|