|
|
@@ -28,7 +28,7 @@ import {
|
|
|
Mail,
|
|
|
XCircle,
|
|
|
AlertCircle,
|
|
|
- MessageCircle // 1. 引入新图标
|
|
|
+ MessageCircle
|
|
|
} from 'lucide-react';
|
|
|
import api from '@/lib/api';
|
|
|
import { toast } from 'react-hot-toast';
|
|
|
@@ -46,7 +46,7 @@ interface VisametricTask {
|
|
|
phone: string;
|
|
|
phone_country_code: string;
|
|
|
birthday: string;
|
|
|
- social_media_account?: string; // 2. 新增字段定义
|
|
|
+ social_media_account?: string;
|
|
|
};
|
|
|
grabbed_history: {
|
|
|
slot_date: string;
|
|
|
@@ -60,16 +60,13 @@ interface VisametricTask {
|
|
|
}
|
|
|
|
|
|
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 {
|
|
|
@@ -95,7 +92,6 @@ export default function VisametricCalendarPage() {
|
|
|
fetchTasks();
|
|
|
}, []);
|
|
|
|
|
|
- // === 日历计算逻辑 ===
|
|
|
const monthStart = startOfMonth(currentDate);
|
|
|
const monthEnd = endOfMonth(monthStart);
|
|
|
const startDate = startOfWeek(monthStart, { weekStartsOn: 1 });
|
|
|
@@ -117,7 +113,6 @@ export default function VisametricCalendarPage() {
|
|
|
});
|
|
|
};
|
|
|
|
|
|
- // === 业务操作 ===
|
|
|
const handleUpdatePnr = async () => {
|
|
|
if (!selectedTask) return;
|
|
|
const toastId = toast.loading('正在更新 PNR...');
|
|
|
@@ -147,161 +142,205 @@ export default function VisametricCalendarPage() {
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
- <div className="flex flex-col h-[calc(100vh-64px)] p-4 md:p-6 gap-4 bg-slate-50/50">
|
|
|
+ // 修改 1:使用 dvh 适配移动端浏览器地址栏,同时减小移动端的 padding
|
|
|
+ <div className="flex flex-col h-[calc(100dvh-64px)] md:h-[calc(100vh-64px)] p-2 md:p-6 gap-3 md:gap-4 bg-slate-50/50 overflow-hidden">
|
|
|
|
|
|
{/* === 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 className="flex flex-col md:flex-row items-start md:items-center justify-between gap-3 md:gap-4 bg-white p-3 md:p-4 rounded-xl border border-slate-200 shadow-sm shrink-0">
|
|
|
+
|
|
|
+ {/* 标题区:手机端和刷新按钮同行显示 */}
|
|
|
+ <div className="flex items-center justify-between w-full md:w-auto">
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
+ <div className="p-2 md:p-2.5 bg-blue-600 text-white rounded-lg shadow-sm">
|
|
|
+ <CalendarIcon size={20} className="md:w-6 md:h-6" />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <h1 className="text-lg md:text-xl font-bold text-slate-800 leading-tight">预约日历视图</h1>
|
|
|
+ <p className="text-[10px] md:text-xs text-slate-500 hidden sm:block">可视化管理 Slot 分布,点击卡片进行操作</p>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
+
|
|
|
+ {/* 手机端的刷新按钮,放在右侧省空间 */}
|
|
|
+ <button
|
|
|
+ onClick={fetchTasks}
|
|
|
+ disabled={loading}
|
|
|
+ className="md:hidden p-2 bg-slate-100 text-slate-700 rounded-lg active:bg-slate-200 transition"
|
|
|
+ >
|
|
|
+ <RefreshCw size={18} className={loading ? "animate-spin" : ""} />
|
|
|
+ </button>
|
|
|
</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 className="flex flex-wrap items-center gap-2 w-full md:w-auto">
|
|
|
+ {/* 月份切换器:手机端占满全宽 */}
|
|
|
+ <div className="flex-1 md:flex-none flex items-center justify-between md:justify-start bg-slate-100 p-1 rounded-lg border border-slate-200">
|
|
|
+ <button onClick={prevMonth} className="p-2 md:p-1.5 hover:bg-white rounded-md transition text-slate-600 active:scale-95">
|
|
|
+ <ChevronLeft size={20} />
|
|
|
+ </button>
|
|
|
+ <div className="px-2 md:px-4 py-1.5 font-bold text-slate-800 min-w-[110px] md:w-36 text-center select-none text-sm">
|
|
|
+ {format(currentDate, 'yyyy年 MM月')}
|
|
|
+ </div>
|
|
|
+ <button onClick={nextMonth} className="p-2 md:p-1.5 hover:bg-white rounded-md transition text-slate-600 active:scale-95">
|
|
|
+ <ChevronRight size={20} />
|
|
|
+ </button>
|
|
|
+ <div className="hidden md:block w-px h-6 bg-slate-300 mx-2"></div>
|
|
|
+ <button onClick={goToToday} className="hidden md:block px-3 py-1 text-xs font-bold hover:bg-white rounded-md transition text-slate-600">
|
|
|
+ 今天
|
|
|
+ </button>
|
|
|
</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 onClick={goToToday} className="md:hidden px-4 py-2.5 text-sm font-bold bg-white border border-slate-200 rounded-lg text-slate-600 active:bg-slate-50">
|
|
|
今天
|
|
|
</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>
|
|
|
+ {/* PC端刷新按钮 */}
|
|
|
+ <button
|
|
|
+ onClick={fetchTasks}
|
|
|
+ disabled={loading}
|
|
|
+ className="hidden md: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>
|
|
|
</div>
|
|
|
|
|
|
{/* === 2. 日历主体容器 === */}
|
|
|
+ {/* 修改 2:增加横向滚动 (overflow-x-auto) 适配小屏幕 */}
|
|
|
<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="flex-1 overflow-x-auto overflow-y-hidden custom-scrollbar">
|
|
|
+ {/* 修改 3:限制最小宽度 min-w-[800px],保证排版不乱。在手机上可左右滑动 */}
|
|
|
+ <div className="flex flex-col h-full min-w-[800px] md:min-w-[1000px]">
|
|
|
+
|
|
|
+ {/* 星期表头 */}
|
|
|
+ <div
|
|
|
+ className="grid border-b border-slate-200 bg-slate-50 shrink-0 sticky top-0 z-10"
|
|
|
+ style={{ gridTemplateColumns: 'repeat(7, 1fr)' }}
|
|
|
+ >
|
|
|
+ {['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map(day => (
|
|
|
+ <div key={day} className="py-2.5 md: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);
|
|
|
+ {/* 日期网格 */}
|
|
|
+ <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>
|
|
|
+ return (
|
|
|
+ <div
|
|
|
+ key={day.toString()}
|
|
|
+ className={`
|
|
|
+ border-b border-r border-slate-100 p-1.5 md:p-2 flex flex-col gap-1 min-h-[100px] md: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-xs md:text-sm font-medium w-6 h-6 md:w-7 md: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 overflow-y-auto max-h-[220px] pr-1 custom-scrollbar">
|
|
|
- {dayTasks
|
|
|
- .slice()
|
|
|
- .sort((a, b) => {
|
|
|
- const parseTime = (time: string) => {
|
|
|
- const [hour, minute] = time.split(':').map(Number);
|
|
|
- return hour * 60 + minute;
|
|
|
- };
|
|
|
- return parseTime(a.grabbed_history.slot_time) - parseTime(b.grabbed_history.slot_time);
|
|
|
- })
|
|
|
- .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'}`}>
|
|
|
- {task.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 className="flex flex-col gap-1 overflow-y-auto max-h-[200px] md:max-h-[220px] pr-1 custom-scrollbar">
|
|
|
+ {dayTasks
|
|
|
+ .slice()
|
|
|
+ .sort((a, b) => {
|
|
|
+ const parseTime = (time: string) => {
|
|
|
+ const [hour, minute] = time.split(':').map(Number);
|
|
|
+ return hour * 60 + minute;
|
|
|
+ };
|
|
|
+ return parseTime(a.grabbed_history.slot_time) - parseTime(b.grabbed_history.slot_time);
|
|
|
+ })
|
|
|
+ .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-1.5 md:p-2 rounded border shadow-sm transition-all hover:shadow-md active:scale-95 w-full
|
|
|
+ ${isCancelled
|
|
|
+ ? 'bg-slate-100 border-slate-200 opacity-60'
|
|
|
+ : hasPnr
|
|
|
+ ? 'bg-green-50/50 border-green-200'
|
|
|
+ : 'bg-amber-50/50 border-amber-200'
|
|
|
+ }
|
|
|
+ `}
|
|
|
+ >
|
|
|
+ <div className="flex items-center justify-between text-[10px] md: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 shrink-0 ml-1" />}
|
|
|
</div>
|
|
|
- )}
|
|
|
- </button>
|
|
|
- );
|
|
|
- })}
|
|
|
+ <div className={`text-[9px] md:text-[10px] truncate font-medium ${isCancelled ? 'text-slate-400' : 'text-slate-700'}`}>
|
|
|
+ {task.user_inputs.first_name}
|
|
|
+ </div>
|
|
|
+ {!isCancelled && (
|
|
|
+ <div className={`mt-0.5 md: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>
|
|
|
</div>
|
|
|
+
|
|
|
+ {/* 移动端横向滚动提示 */}
|
|
|
+ <div className="md:hidden absolute bottom-2 left-1/2 -translate-x-1/2 bg-slate-800/60 backdrop-blur text-white text-[10px] px-3 py-1 rounded-full pointer-events-none opacity-80 shadow-sm animate-pulse">
|
|
|
+ 左右滑动查看更多
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
- {/* === 3. 详情模态框 === */}
|
|
|
+ {/* === 3. 详情模态框 (BottomSheet 式适配) === */}
|
|
|
+ {/* 修改 4:移动端时固定在底部弹出,PC端居中弹出 */}
|
|
|
{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]">
|
|
|
+ <div className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center bg-black/50 backdrop-blur-sm sm:p-4 animate-in fade-in duration-200">
|
|
|
+
|
|
|
+ {/* 移动端点击背景关闭遮罩 */}
|
|
|
+ <div className="absolute inset-0 sm:hidden" onClick={() => setIsModalOpen(false)}></div>
|
|
|
+
|
|
|
+ <div className="relative bg-white rounded-t-2xl sm:rounded-2xl shadow-2xl w-full max-w-md overflow-hidden animate-in slide-in-from-bottom-full sm:slide-in-from-bottom-0 sm:zoom-in-95 duration-200 flex flex-col max-h-[85vh] sm:max-h-[90vh]">
|
|
|
|
|
|
+ {/* 顶部小滑块提示 (仅移动端可见) */}
|
|
|
+ <div className="sm:hidden flex justify-center pt-3 pb-1 bg-slate-50 cursor-grab active:cursor-grabbing" onTouchStart={(e) => {
|
|
|
+ // 简易下拉关闭逻辑提示:实际开发中可接入 framer-motion 等库实现真·手势滑动关闭
|
|
|
+ }}>
|
|
|
+ <div className="w-12 h-1.5 bg-slate-300 rounded-full"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
{/* Header */}
|
|
|
- <div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center bg-slate-50">
|
|
|
+ <div className="px-5 py-3 sm: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>
|
|
|
+ <p className="text-[10px] sm: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} />
|
|
|
@@ -309,36 +348,36 @@ export default function VisametricCalendarPage() {
|
|
|
</div>
|
|
|
|
|
|
{/* Body */}
|
|
|
- <div className="p-6 space-y-5 overflow-y-auto">
|
|
|
+ <div className="p-4 sm:p-6 space-y-4 sm: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 flex-col sm:flex-row sm:items-center justify-between p-4 bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl border border-blue-100 shadow-sm gap-3">
|
|
|
<div className="flex items-center gap-3">
|
|
|
- <div className="p-2 bg-blue-100 text-blue-600 rounded-lg">
|
|
|
+ <div className="p-2 bg-blue-100 text-blue-600 rounded-lg shrink-0">
|
|
|
<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">
|
|
|
+ <p className="font-bold text-lg sm: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">
|
|
|
+ <div className="sm:text-right flex sm:flex-col justify-end items-center sm:items-end w-full sm:w-auto pt-2 sm:pt-0 border-t border-blue-100/50 sm:border-t-0 mt-1 sm:mt-0">
|
|
|
{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">
|
|
|
+ <div className="flex sm:flex-col items-center sm:items-end gap-2 sm:gap-0 w-full sm:w-auto justify-between">
|
|
|
+ <span className="text-[10px] font-bold text-green-600 sm:mb-0.5">PNR CODE</span>
|
|
|
+ <span className="font-mono text-base sm: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">
|
|
|
+ <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 w-full sm:w-auto text-center">
|
|
|
Pending PNR
|
|
|
</span>
|
|
|
)}
|
|
|
@@ -351,39 +390,38 @@ export default function VisametricCalendarPage() {
|
|
|
<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 className="truncate w-full">
|
|
|
+ <p className="text-[10px] sm:text-xs text-slate-500">Full Name</p>
|
|
|
+ <p className="text-xs sm:text-sm font-semibold text-slate-900 truncate">{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>
|
|
|
+ <p className="text-[10px] sm:text-xs text-slate-500">Passport No</p>
|
|
|
+ <p className="text-xs sm: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}</p>
|
|
|
+ <p className="text-[10px] sm:text-xs text-slate-500">Phone</p>
|
|
|
+ <p className="text-xs sm:text-sm font-medium text-slate-800">(+{selectedTask.user_inputs.phone_country_code}) {selectedTask.user_inputs.phone}</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>
|
|
|
+ <p className="text-[10px] sm:text-xs text-slate-500">Email</p>
|
|
|
+ <p className="text-xs sm: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">
|
|
|
+ <p className="text-[10px] sm:text-xs text-slate-500">Social Media</p>
|
|
|
+ <p className="text-xs sm:text-sm font-medium text-slate-800 truncate">
|
|
|
{selectedTask.user_inputs.social_media_account || '-'}
|
|
|
</p>
|
|
|
</div>
|
|
|
@@ -392,8 +430,8 @@ export default function VisametricCalendarPage() {
|
|
|
</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 className="flex gap-3 p-3 sm:p-4 bg-red-50 text-red-800 rounded-xl border border-red-100 items-start sm:items-center">
|
|
|
+ <AlertCircle size={20} className="shrink-0 mt-0.5 sm:mt-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>
|
|
|
@@ -403,23 +441,28 @@ export default function VisametricCalendarPage() {
|
|
|
</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>
|
|
|
+ {/* 修改 5:为了在手机端方便操作,把按钮放到单独一行 */}
|
|
|
+ <div className="px-4 py-4 sm:px-6 sm:py-4 bg-white sm:bg-slate-50 border-t border-slate-100 flex flex-col sm:flex-row justify-end gap-3 shrink-0 pb-6 sm:pb-4">
|
|
|
|
|
|
{!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
|
|
|
+ <div className="flex flex-row gap-3 w-full sm:w-auto">
|
|
|
+ <button onClick={handleUpdatePnr} className="flex-1 sm:flex-none flex justify-center items-center gap-2 px-4 py-3 sm:py-2.5 text-sm font-bold bg-white border border-slate-300 text-slate-700 rounded-xl sm:rounded-lg hover:bg-blue-50 active:bg-blue-100 transition shadow-sm">
|
|
|
+ <RefreshCw size={16} /> <span className="hidden sm:inline">更新</span> 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">
|
|
|
+ <button onClick={handleCancelAppointment} className="flex-1 sm:flex-none flex justify-center items-center gap-2 px-4 py-3 sm:py-2.5 text-sm font-bold bg-red-600 text-white rounded-xl sm:rounded-lg hover:bg-red-700 active:bg-red-800 transition shadow-sm">
|
|
|
<XCircle size={16} /> 取消预约
|
|
|
</button>
|
|
|
- </>
|
|
|
+ </div>
|
|
|
)}
|
|
|
+
|
|
|
+ <button onClick={() => setIsModalOpen(false)} className="w-full sm:w-auto px-4 py-3 sm:py-2.5 text-sm font-medium text-slate-600 bg-slate-100 sm:bg-transparent sm:hover:bg-slate-200 rounded-xl sm:rounded-lg transition active:bg-slate-200">
|
|
|
+ 关闭
|
|
|
+ </button>
|
|
|
</div>
|
|
|
+
|
|
|
</div>
|
|
|
</div>
|
|
|
)}
|
|
|
</div>
|
|
|
);
|
|
|
-}
|
|
|
+}
|