|
@@ -0,0 +1,416 @@
|
|
|
|
|
+'use client';
|
|
|
|
|
+
|
|
|
|
|
+import { useState, useEffect } from 'react';
|
|
|
|
|
+import {
|
|
|
|
|
+ format,
|
|
|
|
|
+ startOfMonth,
|
|
|
|
|
+ endOfMonth,
|
|
|
|
|
+ startOfWeek,
|
|
|
|
|
+ endOfWeek,
|
|
|
|
|
+ eachDayOfInterval,
|
|
|
|
|
+ isSameMonth,
|
|
|
|
|
+ isSameDay,
|
|
|
|
|
+ addMonths,
|
|
|
|
|
+ subMonths,
|
|
|
|
|
+ parse,
|
|
|
|
|
+ isToday
|
|
|
|
|
+} from 'date-fns';
|
|
|
|
|
+import {
|
|
|
|
|
+ ChevronLeft,
|
|
|
|
|
+ ChevronRight,
|
|
|
|
|
+ Loader2,
|
|
|
|
|
+ RefreshCw,
|
|
|
|
|
+ Calendar as CalendarIcon,
|
|
|
|
|
+ Clock,
|
|
|
|
|
+ User,
|
|
|
|
|
+ CreditCard,
|
|
|
|
|
+ Phone,
|
|
|
|
|
+ Mail,
|
|
|
|
|
+ XCircle,
|
|
|
|
|
+ AlertCircle,
|
|
|
|
|
+ MessageCircle // 1. 引入新图标
|
|
|
|
|
+} from 'lucide-react';
|
|
|
|
|
+import api from '@/lib/api';
|
|
|
|
|
+import { toast } from 'react-hot-toast';
|
|
|
|
|
+
|
|
|
|
|
+// --- 类型定义 ---
|
|
|
|
|
+interface VisametricTask {
|
|
|
|
|
+ id: number;
|
|
|
|
|
+ order_id: string;
|
|
|
|
|
+ status: string;
|
|
|
|
|
+ user_inputs: {
|
|
|
|
|
+ first_name: string;
|
|
|
|
|
+ last_name: string;
|
|
|
|
|
+ passport_no: string;
|
|
|
|
|
+ email: string;
|
|
|
|
|
+ phone_no: string;
|
|
|
|
|
+ phone_country_code: string;
|
|
|
|
|
+ birthday: string;
|
|
|
|
|
+ social_media_account?: string; // 2. 新增字段定义
|
|
|
|
|
+ };
|
|
|
|
|
+ grabbed_history: {
|
|
|
|
|
+ slot_date: string;
|
|
|
|
|
+ slot_time: string;
|
|
|
|
|
+ pnr_number: string;
|
|
|
|
|
+ };
|
|
|
|
|
+ meta: {
|
|
|
|
|
+ cancelled_at?: string;
|
|
|
|
|
+ } | null;
|
|
|
|
|
+ created_at: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export default function VisametricCalendarPage() {
|
|
|
|
|
+ // === 状态 ===
|
|
|
|
|
+ const [currentDate, setCurrentDate] = useState(new Date());
|
|
|
|
|
+ const [tasks, setTasks] = useState<VisametricTask[]>([]);
|
|
|
|
|
+ const [loading, setLoading] = useState(true);
|
|
|
|
|
+
|
|
|
|
|
+ // 详情弹窗
|
|
|
|
|
+ const [selectedTask, setSelectedTask] = useState<VisametricTask | null>(null);
|
|
|
|
|
+ const [isModalOpen, setIsModalOpen] = useState(false);
|
|
|
|
|
+
|
|
|
|
|
+ // === API 请求 ===
|
|
|
|
|
+ const fetchTasks = async () => {
|
|
|
|
|
+ setLoading(true);
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await api.get('/api/vas/task/list', {
|
|
|
|
|
+ params: {
|
|
|
|
|
+ page: 1,
|
|
|
|
|
+ size: 100,
|
|
|
|
|
+ status: 'grabbed',
|
|
|
|
|
+ keyword: 'auto.slot.dub.de'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ const items = res.data?.data?.items || [];
|
|
|
|
|
+ setTasks(items);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error(error);
|
|
|
|
|
+ toast.error('获取预约数据失败');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setLoading(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ fetchTasks();
|
|
|
|
|
+ }, []);
|
|
|
|
|
+
|
|
|
|
|
+ // === 日历计算逻辑 ===
|
|
|
|
|
+ const monthStart = startOfMonth(currentDate);
|
|
|
|
|
+ const monthEnd = endOfMonth(monthStart);
|
|
|
|
|
+ const startDate = startOfWeek(monthStart, { weekStartsOn: 1 });
|
|
|
|
|
+ const endDate = endOfWeek(monthEnd, { weekStartsOn: 1 });
|
|
|
|
|
+ const calendarDays = eachDayOfInterval({ start: startDate, end: endDate });
|
|
|
|
|
+
|
|
|
|
|
+ const nextMonth = () => setCurrentDate(addMonths(currentDate, 1));
|
|
|
|
|
+ const prevMonth = () => setCurrentDate(subMonths(currentDate, 1));
|
|
|
|
|
+ const goToToday = () => setCurrentDate(new Date());
|
|
|
|
|
+
|
|
|
|
|
+ const getTasksForDay = (day: Date) => {
|
|
|
|
|
+ return tasks.filter(task => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const taskDate = parse(task.grabbed_history.slot_date, 'dd/MM/yyyy', new Date());
|
|
|
|
|
+ return isSameDay(taskDate, day);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // === 业务操作 ===
|
|
|
|
|
+ const handleUpdatePnr = async () => {
|
|
|
|
|
+ if (!selectedTask) return;
|
|
|
|
|
+ const toastId = toast.loading('正在更新 PNR...');
|
|
|
|
|
+ try {
|
|
|
|
|
+ await api.post(`/api/visametric/update_pnr?task_id=${selectedTask.id}`, {});
|
|
|
|
|
+ toast.success('PNR 更新成功', { id: toastId });
|
|
|
|
|
+ setIsModalOpen(false);
|
|
|
|
|
+ fetchTasks();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ toast.error('PNR 更新失败', { id: toastId });
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleCancelAppointment = async () => {
|
|
|
|
|
+ if (!selectedTask) return;
|
|
|
|
|
+ if (!confirm(`确定要取消 ${selectedTask.user_inputs.first_name} 的预约吗?操作不可撤销!`)) return;
|
|
|
|
|
+
|
|
|
|
|
+ const toastId = toast.loading('正在取消预约...');
|
|
|
|
|
+ try {
|
|
|
|
|
+ await api.post(`/api/visametric/cancel_appointment?task_id=${selectedTask.id}`, {});
|
|
|
|
|
+ toast.success('预约已取消', { id: toastId });
|
|
|
|
|
+ setIsModalOpen(false);
|
|
|
|
|
+ fetchTasks();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ toast.error('取消预约失败', { id: toastId });
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="flex flex-col h-[calc(100vh-64px)] p-4 md:p-6 gap-4 bg-slate-50/50">
|
|
|
|
|
+
|
|
|
|
|
+ {/* === 1. 顶部控制栏 === */}
|
|
|
|
|
+ <div className="flex flex-col md:flex-row items-center justify-between gap-4 bg-white p-4 rounded-xl border border-slate-200 shadow-sm shrink-0">
|
|
|
|
|
+ <div className="flex items-center gap-4">
|
|
|
|
|
+ <div className="p-2.5 bg-blue-600 text-white rounded-lg shadow-sm">
|
|
|
|
|
+ <CalendarIcon size={24} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <h1 className="text-xl font-bold text-slate-800">预约日历视图</h1>
|
|
|
|
|
+ <p className="text-xs text-slate-500">可视化管理 Slot 分布,点击卡片进行操作</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="flex items-center bg-slate-100 p-1 rounded-lg border border-slate-200">
|
|
|
|
|
+ <button onClick={prevMonth} className="p-1.5 hover:bg-white rounded-md transition text-slate-600">
|
|
|
|
|
+ <ChevronLeft size={20} />
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <div className="px-4 py-1.5 font-bold text-slate-800 w-36 text-center select-none text-sm">
|
|
|
|
|
+ {format(currentDate, 'yyyy年 MM月')}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <button onClick={nextMonth} className="p-1.5 hover:bg-white rounded-md transition text-slate-600">
|
|
|
|
|
+ <ChevronRight size={20} />
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <div className="w-px h-6 bg-slate-300 mx-2"></div>
|
|
|
|
|
+ <button onClick={goToToday} className="px-3 py-1 text-xs font-bold hover:bg-white rounded-md transition text-slate-600">
|
|
|
|
|
+ 今天
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={fetchTasks}
|
|
|
|
|
+ disabled={loading}
|
|
|
|
|
+ className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg hover:bg-slate-800 transition shadow-sm text-sm font-medium active:scale-95"
|
|
|
|
|
+ >
|
|
|
|
|
+ {loading ? <Loader2 size={16} className="animate-spin" /> : <RefreshCw size={16} />}
|
|
|
|
|
+ <span>刷新数据</span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* === 2. 日历主体容器 === */}
|
|
|
|
|
+ <div className="flex-1 bg-white border border-slate-200 rounded-xl shadow-sm flex flex-col overflow-hidden relative">
|
|
|
|
|
+ <div className="flex flex-col h-full min-w-[1000px]">
|
|
|
|
|
+
|
|
|
|
|
+ {/* 星期表头 */}
|
|
|
|
|
+ <div
|
|
|
|
|
+ className="grid border-b border-slate-200 bg-slate-50 shrink-0"
|
|
|
|
|
+ style={{ gridTemplateColumns: 'repeat(7, 1fr)' }}
|
|
|
|
|
+ >
|
|
|
|
|
+ {['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map(day => (
|
|
|
|
|
+ <div key={day} className="py-3 text-center text-xs font-bold text-slate-500 uppercase tracking-wider border-r border-slate-200 last:border-r-0">
|
|
|
|
|
+ {day}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 日期网格 */}
|
|
|
|
|
+ <div
|
|
|
|
|
+ className="grid auto-rows-fr flex-1 overflow-y-auto"
|
|
|
|
|
+ style={{ gridTemplateColumns: 'repeat(7, 1fr)' }}
|
|
|
|
|
+ >
|
|
|
|
|
+ {calendarDays.map((day) => {
|
|
|
|
|
+ const dayTasks = getTasksForDay(day);
|
|
|
|
|
+ const isCurrentMonth = isSameMonth(day, currentDate);
|
|
|
|
|
+ const isDayToday = isToday(day);
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div
|
|
|
|
|
+ key={day.toString()}
|
|
|
|
|
+ className={`
|
|
|
|
|
+ border-b border-r border-slate-100 p-2 flex flex-col gap-1 min-h-[120px]
|
|
|
|
|
+ ${!isCurrentMonth ? 'bg-slate-50/40 text-slate-400' : 'bg-white'}
|
|
|
|
|
+ ${isDayToday ? 'bg-blue-50/30' : ''}
|
|
|
|
|
+ hover:bg-slate-50 transition-colors
|
|
|
|
|
+ `}
|
|
|
|
|
+ >
|
|
|
|
|
+ {/* 日期头 */}
|
|
|
|
|
+ <div className="flex justify-between items-start mb-1">
|
|
|
|
|
+ <span className={`
|
|
|
|
|
+ text-sm font-medium w-7 h-7 flex items-center justify-center rounded-full
|
|
|
|
|
+ ${isDayToday ? 'bg-blue-600 text-white shadow-sm' : ''}
|
|
|
|
|
+ `}>
|
|
|
|
|
+ {format(day, 'd')}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ {dayTasks.length > 0 && (
|
|
|
|
|
+ <span className="text-[10px] font-bold text-slate-500 bg-slate-100 px-1.5 py-0.5 rounded-full">
|
|
|
|
|
+ {dayTasks.length}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 任务列表 */}
|
|
|
|
|
+ <div className="flex flex-col gap-1.5 overflow-y-auto max-h-[140px] pr-1 custom-scrollbar">
|
|
|
|
|
+ {dayTasks.map(task => {
|
|
|
|
|
+ const isCancelled = !!task.meta?.cancelled_at;
|
|
|
|
|
+ const hasPnr = !!task.grabbed_history.pnr_number;
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <button
|
|
|
|
|
+ key={task.id}
|
|
|
|
|
+ onClick={() => { setSelectedTask(task); setIsModalOpen(true); }}
|
|
|
|
|
+ className={`
|
|
|
|
|
+ text-left p-2 rounded border shadow-sm transition-all hover:shadow-md hover:-translate-y-0.5 w-full group
|
|
|
|
|
+ ${isCancelled
|
|
|
|
|
+ ? 'bg-slate-100 border-slate-200 opacity-60'
|
|
|
|
|
+ : hasPnr
|
|
|
|
|
+ ? 'bg-green-50/50 border-green-200 hover:border-green-300'
|
|
|
|
|
+ : 'bg-amber-50/50 border-amber-200 hover:border-amber-300'
|
|
|
|
|
+ }
|
|
|
|
|
+ `}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div className="flex items-center justify-between text-[11px] font-bold leading-none mb-1">
|
|
|
|
|
+ <span className={isCancelled ? 'text-slate-500 line-through' : hasPnr ? 'text-green-700' : 'text-amber-700'}>
|
|
|
|
|
+ {task.grabbed_history.slot_time}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ {isCancelled && <XCircle size={10} className="text-red-500" />}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className={`text-[10px] truncate font-medium ${isCancelled ? 'text-slate-400' : 'text-slate-700'}`}>
|
|
|
|
|
+ {selectedTask.user_inputs.social_media_account || '-'} {task.user_inputs.first_name} {task.user_inputs.last_name}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {!isCancelled && (
|
|
|
|
|
+ <div className={`mt-1 text-[9px] font-mono truncate ${hasPnr ? 'text-green-600' : 'text-amber-600/70'}`}>
|
|
|
|
|
+ {hasPnr ? task.grabbed_history.pnr_number : 'Wait PNR'}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ );
|
|
|
|
|
+ })}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ })}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* === 3. 详情模态框 === */}
|
|
|
|
|
+ {isModalOpen && selectedTask && (
|
|
|
|
|
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-200">
|
|
|
|
|
+ <div className="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden animate-in zoom-in-95 duration-200 flex flex-col max-h-[90vh]">
|
|
|
|
|
+
|
|
|
|
|
+ {/* Header */}
|
|
|
|
|
+ <div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center bg-slate-50">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <h3 className="font-bold text-slate-800 text-base">预约详情</h3>
|
|
|
|
|
+ <p className="text-xs text-slate-400 font-mono mt-0.5">#{selectedTask.id} • {selectedTask.order_id.split('-')[1]}...</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <button onClick={() => setIsModalOpen(false)} className="p-2 bg-white rounded-full text-slate-400 hover:text-slate-700 shadow-sm border border-slate-200 transition">
|
|
|
|
|
+ <XCircle size={20} />
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Body */}
|
|
|
|
|
+ <div className="p-6 space-y-5 overflow-y-auto">
|
|
|
|
|
+ {/* Status Banner */}
|
|
|
|
|
+ <div className="flex items-center justify-between p-4 bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl border border-blue-100 shadow-sm">
|
|
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
|
|
+ <div className="p-2 bg-blue-100 text-blue-600 rounded-lg">
|
|
|
|
|
+ <Clock size={20} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p className="text-[10px] text-blue-600 font-bold uppercase tracking-wider">Appointment Time</p>
|
|
|
|
|
+ <p className="font-bold text-xl text-slate-800 leading-tight">
|
|
|
|
|
+ {selectedTask.grabbed_history.slot_time}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p className="text-xs text-slate-500 font-medium">{selectedTask.grabbed_history.slot_date}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="text-right">
|
|
|
|
|
+ {selectedTask.meta?.cancelled_at ? (
|
|
|
|
|
+ <span className="inline-block px-3 py-1 bg-red-100 text-red-700 text-xs font-bold rounded-full border border-red-200">
|
|
|
|
|
+ Cancelled
|
|
|
|
|
+ </span>
|
|
|
|
|
+ ) : selectedTask.grabbed_history.pnr_number ? (
|
|
|
|
|
+ <div className="flex flex-col items-end">
|
|
|
|
|
+ <span className="text-[10px] font-bold text-green-600 mb-0.5">PNR CODE</span>
|
|
|
|
|
+ <span className="font-mono text-lg font-bold text-green-700 bg-green-50 px-2 rounded border border-green-100">
|
|
|
|
|
+ {selectedTask.grabbed_history.pnr_number}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <span className="inline-block px-3 py-1 bg-amber-100 text-amber-700 text-xs font-bold rounded-full border border-amber-200 animate-pulse">
|
|
|
|
|
+ Pending PNR
|
|
|
|
|
+ </span>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Info Grid */}
|
|
|
|
|
+ <div className="grid grid-cols-1 gap-4">
|
|
|
|
|
+ <div className="p-3 border border-slate-100 rounded-lg bg-slate-50/50 space-y-3">
|
|
|
|
|
+ <p className="text-xs font-bold text-slate-400 uppercase tracking-wider mb-2">Applicant Details</p>
|
|
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
|
|
+ <User size={16} className="text-slate-400 shrink-0" />
|
|
|
|
|
+ <div className="truncate">
|
|
|
|
|
+ <p className="text-xs text-slate-500">Full Name</p>
|
|
|
|
|
+ <p className="text-sm font-semibold text-slate-900">{selectedTask.user_inputs.first_name} {selectedTask.user_inputs.last_name}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
|
|
+ <CreditCard size={16} className="text-slate-400 shrink-0" />
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p className="text-xs text-slate-500">Passport No</p>
|
|
|
|
|
+ <p className="text-sm font-mono font-semibold text-slate-900">{selectedTask.user_inputs.passport_no}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
|
|
+ <Phone size={16} className="text-slate-400 shrink-0" />
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p className="text-xs text-slate-500">Phone</p>
|
|
|
|
|
+ <p className="text-sm font-medium text-slate-800">(+{selectedTask.user_inputs.phone_country_code}) {selectedTask.user_inputs.phone_no}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
|
|
+ <Mail size={16} className="text-slate-400 shrink-0" />
|
|
|
|
|
+ <div className="truncate w-full">
|
|
|
|
|
+ <p className="text-xs text-slate-500">Email</p>
|
|
|
|
|
+ <p className="text-sm font-medium text-slate-800 truncate" title={selectedTask.user_inputs.email}>{selectedTask.user_inputs.email}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 3. 在这里显示 Social Media Account */}
|
|
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
|
|
+ <MessageCircle size={16} className="text-slate-400 shrink-0" />
|
|
|
|
|
+ <div className="truncate w-full">
|
|
|
|
|
+ <p className="text-xs text-slate-500">Social Media</p>
|
|
|
|
|
+ <p className="text-sm font-medium text-slate-800 truncate">
|
|
|
|
|
+ {selectedTask.user_inputs.social_media_account || '-'}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {selectedTask.meta?.cancelled_at && (
|
|
|
|
|
+ <div className="flex gap-3 p-4 bg-red-50 text-red-800 rounded-xl border border-red-100 items-center">
|
|
|
|
|
+ <AlertCircle size={20} className="shrink-0" />
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p className="font-bold text-sm">此预约已取消</p>
|
|
|
|
|
+ <p className="text-xs opacity-80 mt-0.5">时间: {new Date(selectedTask.meta.cancelled_at).toLocaleString()}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Footer Actions */}
|
|
|
|
|
+ <div className="px-6 py-4 bg-slate-50 border-t border-slate-100 flex justify-end gap-3 shrink-0">
|
|
|
|
|
+ <button onClick={() => setIsModalOpen(false)} className="px-4 py-2.5 text-sm font-medium text-slate-600 hover:bg-slate-200 rounded-lg transition">关闭</button>
|
|
|
|
|
+
|
|
|
|
|
+ {!selectedTask.meta?.cancelled_at && (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <button onClick={handleUpdatePnr} className="flex items-center gap-2 px-4 py-2.5 text-sm font-bold bg-white border border-slate-300 text-slate-700 rounded-lg hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200 transition shadow-sm">
|
|
|
|
|
+ <RefreshCw size={16} /> 更新 PNR
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button onClick={handleCancelAppointment} className="flex items-center gap-2 px-4 py-2.5 text-sm font-bold bg-red-600 text-white rounded-lg hover:bg-red-700 hover:shadow-red-200 transition shadow-sm">
|
|
|
|
|
+ <XCircle size={16} /> 取消预约
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|