jerry 2 maanden geleden
bovenliggende
commit
2a053da519

+ 11 - 0
package-lock.json

@@ -9,6 +9,7 @@
             "version": "0.1.0",
             "dependencies": {
                 "axios": "^1.6.7",
+                "date-fns": "^4.1.0",
                 "lucide-react": "^0.330.0",
                 "next": "14.1.0",
                 "react": "^18",
@@ -780,6 +781,16 @@
                 "node": ">=12"
             }
         },
+        "node_modules/date-fns": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+            "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
+            "license": "MIT",
+            "funding": {
+                "type": "github",
+                "url": "https://github.com/sponsors/kossnocorp"
+            }
+        },
         "node_modules/decimal.js-light": {
             "version": "2.5.1",
             "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",

+ 1 - 0
package.json

@@ -10,6 +10,7 @@
     },
     "dependencies": {
         "axios": "^1.6.7",
+        "date-fns": "^4.1.0",
         "lucide-react": "^0.330.0",
         "next": "14.1.0",
         "react": "^18",

+ 0 - 147
src/app/admin/payment-confirmations/page.tsx

@@ -1,147 +0,0 @@
-'use client';
-
-import { useState, useEffect } from 'react';
-import api from '@/lib/api';
-import { RefreshCw, Search, CheckCheck } from 'lucide-react';
-import Pagination from '@/components/common/Pagination';
-import ConfirmationTable from '@/components/admin/payments/ConfirmationTable';
-import ConfirmationDetailModal from '@/components/admin/payments/ConfirmationDetailModal';
-import { PaymentConfirmation } from '@/types/payment';
-
-export default function PaymentConfirmationsPage() {
-  const [list, setList] = useState<PaymentConfirmation[]>([]);
-  const [loading, setLoading] = useState(true);
-  
-  // 分页与搜索
-  const [page, setPage] = useState(1);
-  const [pageSize] = useState(10);
-  const [total, setTotal] = useState(0);
-  const [keyword, setKeyword] = useState('');
-
-  // 弹窗状态
-  const [selectedItem, setSelectedItem] = useState<PaymentConfirmation | null>(null);
-  const [isDetailOpen, setIsDetailOpen] = useState(false);
-
-  useEffect(() => {
-    fetchData(1);
-  }, []);
-
-  const fetchData = async (targetPage: number) => {
-    setLoading(true);
-    try {
-      const res = await api.post('/api/vas/payment_confirmation/list_all', {}, {
-        params: {
-          page: targetPage,
-          size: pageSize,
-          keyword: keyword
-        }
-      });
-      const data = res.data.data;
-      if (data && Array.isArray(data.items)) {
-        setList(data.items);
-        setTotal(data.total || 0);
-      } else {
-        setList([]);
-        setTotal(0);
-      }
-      setPage(targetPage);
-    } catch (error) {
-      console.error("Fetch confirmations failed", error);
-      setList([]);
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  const handleSearch = () => fetchData(1);
-
-  // 打开详情
-  const handleViewDetail = (item: PaymentConfirmation) => {
-    setSelectedItem(item);
-    setIsDetailOpen(true);
-  };
-
-  // 确认收款
-  const handleApprove = async (item: PaymentConfirmation) => {
-    const moneyStr = `${(item.amount / 100).toFixed(2)} ${item.currency}`;
-    if (!confirm(`确认已收到支付单 #${item.payment_id} 的款项 (${moneyStr}) 吗?`)) return;
-
-    try {
-      // 使用新的确认接口
-      await api.post('/api/vas/payment/confirm_by_admin', {
-        status: 'confirmed',
-        admin_confirmed_at: new Date().toISOString()
-      }, {
-        params: { id: item.id } // 这里的 id 是 confirmation record id
-      });
-      
-      alert('已确认收款');
-      fetchData(page);
-    } catch (e: any) {
-      alert('操作失败: ' + (e.response?.data?.message || '未知错误'));
-    }
-  };
-
-  return (
-    <div className="p-4 md:p-6">
-      
-      {/* Header */}
-      <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
-        <div>
-          <h1 className="text-2xl font-bold text-slate-800 flex items-center gap-2">
-            <CheckCheck className="text-blue-600" /> 支付确认管理
-          </h1>
-          <p className="text-sm text-slate-500 mt-1">处理用户提交的“我已付款”确认请求</p>
-        </div>
-        
-        <div className="flex flex-col sm:flex-row gap-3 w-full md:w-auto">
-          <div className="relative w-full sm:w-auto md:w-64">
-            <input 
-              type="text" 
-              placeholder="搜索 Payment ID / User ID" 
-              className="w-full pl-9 pr-4 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
-              value={keyword}
-              onChange={e => setKeyword(e.target.value)}
-              onKeyDown={e => e.key === 'Enter' && handleSearch()}
-            />
-            <Search size={16} className="absolute left-3 top-2.5 text-gray-400" />
-          </div>
-          
-          <button 
-            onClick={() => fetchData(page)} 
-            className="flex items-center justify-center gap-2 px-4 py-2 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 text-slate-600 transition w-full sm:w-auto"
-            title="刷新"
-          >
-            <RefreshCw size={18} />
-            <span className="md:hidden text-sm font-medium">刷新列表</span>
-          </button>
-        </div>
-      </div>
-
-      {/* Table */}
-      <ConfirmationTable 
-        data={list} 
-        loading={loading}
-        onApprove={handleApprove}
-        onViewDetail={handleViewDetail} 
-      />
-
-      <div className="mt-4">
-        <Pagination 
-          currentPage={page} 
-          total={total} 
-          pageSize={pageSize} 
-          onPageChange={fetchData} 
-        />
-      </div>
-
-      {/* Modal */}
-      <ConfirmationDetailModal 
-        isOpen={isDetailOpen}
-        onClose={() => setIsDetailOpen(false)}
-        data={selectedItem}
-        onApprove={handleApprove}
-      />
-    </div>
-  );
-}

+ 416 - 0
src/app/admin/visametric/page.tsx

@@ -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>
+  );
+}

+ 2 - 2
src/components/admin/AdminSidebar.tsx

@@ -29,11 +29,11 @@ export default function AdminSidebar() {
     { name: '用户管理', href: '/admin/users', icon: Users },
     { name: '工单处理', href: '/admin/tickets', icon: LifeBuoy },
     { name: '订单管理', href: '/admin/orders', icon: ShoppingBag },
-    { name: '支付确认', href: '/admin/payment-confirmations', icon: CheckCheck },
     { name: '支付配置', href: '/admin/payments', icon: CreditCard },
     { name: '商品配置', href: '/admin/products', icon: Settings },
     { name: '系统任务', href: '/admin/tasks', icon: Activity },
-    { name: 'TROOV Slot监控', href: '/admin/slots', icon: CalendarClock },
+    { name: 'TROOV', href: '/admin/slots', icon: CalendarClock },
+    { name: 'VISAMETRIC', href: '/admin/visametric', icon: CalendarClock },
     { name: '卡片管理', href: '/admin/cards', icon: LayoutGrid },
     { name: '远程服务器', href: '/admin/remote-server', icon: Server },
     { name: 'Worker 状态', href: '/admin/workers', icon: Server },

+ 0 - 240
src/components/admin/payments/ConfirmationDetailModal.tsx

@@ -1,240 +0,0 @@
-'use client';
-
-import { useState, useEffect } from 'react';
-import { X, User, CreditCard, ShoppingBag, Loader2, CheckCircle } from 'lucide-react';
-import api from '@/lib/api';
-import LocalTime from '@/components/common/LocalTime';
-import { PaymentConfirmation } from '@/types/payment';
-
-interface ConfirmationDetailModalProps {
-  isOpen: boolean;
-  onClose: () => void;
-  data: PaymentConfirmation | null;
-  onApprove: (item: PaymentConfirmation) => void;
-  // onReject: (item: PaymentConfirmation) => void; // 移除
-}
-
-export default function ConfirmationDetailModal({ isOpen, onClose, data, onApprove }: ConfirmationDetailModalProps) {
-  const [loading, setLoading] = useState(false);
-  
-  // 详情数据状态
-  const [paymentInfo, setPaymentInfo] = useState<any>(null);
-  const [orderInfo, setOrderInfo] = useState<any>(null);
-  const [userInfo, setUserInfo] = useState<any>(null);
-
-  useEffect(() => {
-    if (isOpen && data) {
-      fetchDetails();
-    } else {
-      // 重置数据
-      setPaymentInfo(null);
-      setOrderInfo(null);
-      setUserInfo(null);
-    }
-  }, [isOpen, data]);
-
-  const fetchDetails = async () => {
-    if (!data) return;
-    setLoading(true);
-    try {
-      // 1. 获取用户信息 (已有 user_id)
-      const userRes = await api.get('/api/user/detail', { params: { user_id: data.user_id } });
-      setUserInfo(userRes.data.data);
-
-      // 2. 获取支付详情 (已有 payment_id)
-      const payRes = await api.get('/api/vas/payment/detail', { params: { payment_id: data.payment_id } });
-      const paymentData = payRes.data.data;
-      setPaymentInfo(paymentData);
-
-      // 3. 获取订单详情 (通过支付详情里的 order_id)
-      if (paymentData && paymentData.order_id) {
-        const orderRes = await api.get('/api/vas/order/detail', { params: { order_id: paymentData.order_id } });
-        setOrderInfo(orderRes.data.data);
-      }
-
-    } catch (error) {
-      console.error("Failed to load details", error);
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  if (!isOpen || !data) return null;
-
-  return (
-    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in duration-200">
-      <div className="bg-white rounded-xl shadow-2xl w-full max-w-4xl max-h-[90vh] flex flex-col overflow-hidden">
-        
-        {/* Header */}
-        <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50">
-          <div>
-             <h3 className="font-bold text-gray-900 text-lg">支付确认详情</h3>
-             <p className="text-xs text-gray-500 mt-1">Confirmation ID: #{data.id}</p>
-          </div>
-          <button onClick={onClose} className="p-1 hover:bg-gray-200 rounded-full transition">
-            <X size={24} className="text-gray-500" />
-          </button>
-        </div>
-
-        {/* Content */}
-        <div className="flex-1 overflow-y-auto p-6 bg-slate-50/50">
-          {loading ? (
-            <div className="flex flex-col items-center justify-center py-20 text-slate-400">
-              <Loader2 className="w-10 h-10 animate-spin mb-2" />
-              <p>正在加载关联数据...</p>
-            </div>
-          ) : (
-            <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
-              
-              {/* 1. 用户提交的确认信息 */}
-              <div className="md:col-span-2 bg-white p-5 rounded-xl shadow-sm border border-blue-100">
-                <h4 className="text-sm font-bold text-slate-800 mb-4 flex items-center gap-2">
-                  <CheckCircle size={16} className="text-blue-600" /> 用户提交的确认信息
-                </h4>
-                <div className="grid grid-cols-2 sm:grid-cols-4 gap-4 text-sm">
-                  <div>
-                    <span className="block text-xs text-slate-400">提交金额</span>
-                    <span className="font-bold text-slate-900 text-lg">
-                      {(data.amount / 100).toFixed(2)} {data.currency}
-                    </span>
-                  </div>
-                  <div>
-                    <span className="block text-xs text-slate-400">随机立减</span>
-                    <span className="font-medium text-red-500">
-                       -{(data.random_offset / 100).toFixed(2)}
-                    </span>
-                  </div>
-                  <div>
-                    <span className="block text-xs text-slate-400">用户点击时间</span>
-                    <LocalTime date={data.confirmed_at} className="font-medium text-slate-700"/>
-                  </div>
-                  <div>
-                    <span className="block text-xs text-slate-400">当前状态</span>
-                    <span className={`inline-block px-2 py-0.5 rounded text-xs font-bold ${
-                      data.status === 'pending' ? 'bg-yellow-100 text-yellow-700' : 'bg-gray-100 text-gray-700'
-                    }`}>
-                      {data.status.toUpperCase()}
-                    </span>
-                  </div>
-                </div>
-              </div>
-
-              {/* 2. 支付单详情 (系统记录) */}
-              <div className="bg-white p-5 rounded-xl shadow-sm border border-slate-200">
-                <h4 className="text-sm font-bold text-slate-800 mb-4 flex items-center gap-2">
-                  <CreditCard size={16} className="text-slate-500" /> 关联支付单 (Payment)
-                </h4>
-                {paymentInfo ? (
-                  <div className="space-y-3 text-sm">
-                    <div className="flex justify-between border-b border-slate-50 pb-2">
-                      <span className="text-slate-500">Payment ID</span>
-                      <span className="font-mono">{paymentInfo.id}</span>
-                    </div>
-                    <div className="flex justify-between border-b border-slate-50 pb-2">
-                      <span className="text-slate-500">渠道 Provider</span>
-                      <span className="font-medium uppercase">{paymentInfo.provider}</span>
-                    </div>
-                    <div className="flex justify-between border-b border-slate-50 pb-2">
-                      <span className="text-slate-500">应付金额</span>
-                      <span className="font-bold">
-                        {(paymentInfo.amount / 100).toFixed(2)} {paymentInfo.currency}
-                      </span>
-                    </div>
-                    <div className="flex justify-between">
-                      <span className="text-slate-500">外部流水号</span>
-                      <span className="font-mono text-xs max-w-[150px] truncate" title={paymentInfo.external_trade_no}>
-                        {paymentInfo.external_trade_no || '-'}
-                      </span>
-                    </div>
-                  </div>
-                ) : (
-                  <p className="text-slate-400 text-xs">未找到支付记录</p>
-                )}
-              </div>
-
-              {/* 3. 订单详情 */}
-              <div className="bg-white p-5 rounded-xl shadow-sm border border-slate-200">
-                <h4 className="text-sm font-bold text-slate-800 mb-4 flex items-center gap-2">
-                  <ShoppingBag size={16} className="text-slate-500" /> 关联订单 (Order)
-                </h4>
-                {orderInfo ? (
-                  <div className="space-y-3 text-sm">
-                    <div className="flex justify-between border-b border-slate-50 pb-2">
-                      <span className="text-slate-500">Order ID</span>
-                      <span className="font-mono">{orderInfo.id}</span>
-                    </div>
-                    <div className="flex justify-between border-b border-slate-50 pb-2">
-                      <span className="text-slate-500">商品名称</span>
-                      <span className="font-medium text-right max-w-[180px] truncate" title={orderInfo.product_title}>
-                        {orderInfo.product_title}
-                      </span>
-                    </div>
-                    <div className="flex justify-between">
-                      <span className="text-slate-500">申请人</span>
-                      <span className="font-medium">
-                        {orderInfo.user_inputs?.first_name} {orderInfo.user_inputs?.last_name}
-                      </span>
-                    </div>
-                  </div>
-                ) : (
-                  <p className="text-slate-400 text-xs">未找到订单记录</p>
-                )}
-              </div>
-
-              {/* 4. 用户信息 */}
-              <div className="md:col-span-2 bg-white p-5 rounded-xl shadow-sm border border-slate-200">
-                <h4 className="text-sm font-bold text-slate-800 mb-4 flex items-center gap-2">
-                  <User size={16} className="text-slate-500" /> 下单用户 (User)
-                </h4>
-                {userInfo ? (
-                  <div className="flex items-center gap-6 text-sm">
-                    <div className="w-12 h-12 bg-slate-100 rounded-full flex items-center justify-center overflow-hidden">
-                      {userInfo.avatar_url ? (
-                        <img src={userInfo.avatar_url} className="w-full h-full object-cover" />
-                      ) : (
-                        <User className="text-slate-400" />
-                      )}
-                    </div>
-                    <div className="grid grid-cols-1 sm:grid-cols-3 gap-x-12 gap-y-2 flex-1">
-                      <div>
-                        <span className="block text-xs text-slate-400">昵称</span>
-                        <span className="font-medium">{userInfo.nickname || '-'}</span>
-                      </div>
-                      <div>
-                        <span className="block text-xs text-slate-400">邮箱</span>
-                        <span className="font-medium">{userInfo.email}</span>
-                      </div>
-                      <div>
-                        <span className="block text-xs text-slate-400">手机号</span>
-                        <span className="font-medium">{userInfo.phone || '-'}</span>
-                      </div>
-                    </div>
-                  </div>
-                ) : (
-                  <p className="text-slate-400 text-xs">未找到用户信息</p>
-                )}
-              </div>
-
-            </div>
-          )}
-        </div>
-
-        {/* Footer Actions */}
-        <div className="p-4 bg-white border-t flex justify-end gap-3">
-          <button onClick={onClose} className="px-4 py-2 border border-slate-300 rounded-lg text-slate-700 hover:bg-slate-50 text-sm font-medium">关闭</button>
-          
-          {data.status === 'pending' && (
-            // 移除了驳回按钮
-            <button 
-              onClick={() => { onApprove(data); onClose(); }}
-              className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 text-sm font-bold flex items-center gap-2 shadow-sm"
-            >
-              <CheckCircle size={16} /> 确认已收款
-            </button>
-          )}
-        </div>
-
-      </div>
-    </div>
-  );
-}

+ 0 - 179
src/components/admin/payments/ConfirmationTable.tsx

@@ -1,179 +0,0 @@
-'use client';
-
-import { CheckCircle, XCircle, Clock, User, CreditCard, Sparkles, Eye } from 'lucide-react';
-import LocalTime from '@/components/common/LocalTime';
-import { PaymentConfirmation } from '@/types/payment';
-
-interface ConfirmationTableProps {
-  data: PaymentConfirmation[];
-  loading: boolean;
-  onApprove: (item: PaymentConfirmation) => void;
-  onViewDetail: (item: PaymentConfirmation) => void;
-}
-
-export default function ConfirmationTable({ data, loading, onApprove, onViewDetail }: ConfirmationTableProps) {
-  
-  if (loading) {
-    return (
-      <div className="bg-white rounded-lg shadow p-12 text-center border border-slate-200">
-        <div className="text-gray-500 text-sm">加载数据中...</div>
-      </div>
-    );
-  }
-
-  if (!data || data.length === 0) {
-    return (
-      <div className="bg-white rounded-lg shadow p-12 text-center border border-slate-200">
-        <div className="text-gray-500 text-sm">暂无待确认记录</div>
-      </div>
-    );
-  }
-
-  const getStatusBadge = (status: string) => {
-    switch (status) {
-      case 'pending':
-        return <span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800"><Clock size={12}/> 待审核</span>;
-      case 'approved':
-        return <span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"><CheckCircle size={12}/> 已通过</span>;
-      case 'rejected':
-        return <span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800"><XCircle size={12}/> 已驳回</span>;
-      default:
-        return <span className="bg-gray-100 text-gray-800 text-xs px-2 py-0.5 rounded">{status}</span>;
-    }
-  };
-
-  const formatMoney = (amount: number, currency: string) => {
-    return `${(amount / 100).toFixed(2)} ${currency}`;
-  };
-
-  return (
-    <div className="bg-white rounded-lg shadow border border-slate-200 flex flex-col">
-      
-      {/* Desktop Table */}
-      <div className="hidden md:block overflow-x-auto">
-        <table className="min-w-full divide-y divide-slate-200">
-          <thead className="bg-slate-50">
-            <tr>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase whitespace-nowrap">ID / 提交时间</th>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase whitespace-nowrap">关联支付单</th>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase whitespace-nowrap">确认金额</th>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase whitespace-nowrap">用户</th>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase whitespace-nowrap">用户点击时间</th>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase whitespace-nowrap">状态</th>
-              <th className="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase whitespace-nowrap sticky right-0 bg-slate-50 shadow-sm sm:static sm:shadow-none">操作</th>
-            </tr>
-          </thead>
-          <tbody className="divide-y divide-slate-100">
-            {data.map((item) => (
-              <tr key={item.id} className="hover:bg-slate-50">
-                <td className="px-6 py-4 whitespace-nowrap">
-                  <div className="text-sm font-mono text-slate-900 font-bold">#{item.id}</div>
-                  <div className="text-xs text-slate-400 mt-1"><LocalTime date={item.created_at} /></div>
-                </td>
-                <td className="px-6 py-4 whitespace-nowrap">
-                  <div className="flex items-center text-blue-600 font-mono text-sm">
-                    <CreditCard size={14} className="mr-1" /> PID: {item.payment_id}
-                  </div>
-                </td>
-                <td className="px-6 py-4 whitespace-nowrap">
-                  <div className="flex flex-col">
-                    <span className="text-sm font-bold text-slate-900">{formatMoney(item.amount, item.currency)}</span>
-                    {item.random_offset > 0 && (
-                      <span className="text-xs text-red-500 flex items-center gap-0.5"><Sparkles size={10} /> -{formatMoney(item.random_offset, item.currency)}</span>
-                    )}
-                  </div>
-                </td>
-                <td className="px-6 py-4 whitespace-nowrap">
-                  <div className="flex items-center text-slate-600 text-xs font-mono">
-                    <User size={14} className="mr-1 text-slate-400" />
-                    <span title={item.user_id} className="truncate max-w-[120px]">{item.user_id}</span>
-                  </div>
-                </td>
-                <td className="px-6 py-4 text-sm text-slate-700 whitespace-nowrap">
-                  <LocalTime date={item.confirmed_at} className="font-medium" />
-                </td>
-                <td className="px-6 py-4 whitespace-nowrap">
-                  {getStatusBadge(item.status)}
-                </td>
-                <td className="px-6 py-4 text-right whitespace-nowrap sticky right-0 bg-white sm:static border-l border-slate-100 sm:border-none shadow-sm sm:shadow-none">
-                  <div className="flex justify-end gap-2 items-center">
-                    <button onClick={() => onViewDetail(item)} className="p-1.5 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded transition" title="查看详情">
-                      <Eye size={18} />
-                    </button>
-                    {item.status === 'pending' && (
-                      <>
-                        <div className="w-px h-4 bg-slate-200 mx-1 hidden sm:block"></div>
-                        <button 
-                          onClick={() => onApprove(item)}
-                          className="flex items-center gap-1 px-3 py-1.5 bg-green-50 text-green-700 rounded hover:bg-green-100 text-xs font-bold transition border border-green-200"
-                        >
-                          <CheckCircle size={14} /> 确认
-                        </button>
-                      </>
-                    )}
-                  </div>
-                </td>
-              </tr>
-            ))}
-          </tbody>
-        </table>
-      </div>
-
-      {/* Mobile Cards */}
-      <div className="md:hidden space-y-4">
-        {data.map((item) => (
-          <div key={item.id} className="bg-white p-4 rounded-lg shadow-sm border border-slate-200">
-            <div className="flex justify-between items-start mb-3">
-              <div className="flex flex-col">
-                <span className="text-sm font-mono text-slate-900 font-bold">Confirmation #{item.id}</span>
-                <span className="text-xs text-slate-400 flex items-center gap-1 mt-0.5">
-                  <Clock size={10} /> <LocalTime date={item.created_at} />
-                </span>
-              </div>
-              <div>{getStatusBadge(item.status)}</div>
-            </div>
-
-            <div className="bg-slate-50 p-3 rounded-lg border border-slate-100 text-sm space-y-2 mb-4">
-              <div className="flex justify-between items-center">
-                <span className="text-slate-400 text-xs">关联支付</span>
-                <span className="font-mono text-blue-600 font-medium">PID: {item.payment_id}</span>
-              </div>
-              <div className="flex justify-between items-center">
-                <span className="text-slate-400 text-xs">确认金额</span>
-                <div className="text-right">
-                  <span className="font-bold text-slate-900 block">{formatMoney(item.amount, item.currency)}</span>
-                  {item.random_offset > 0 && (
-                    <span className="text-[10px] text-red-500 flex items-center justify-end gap-0.5">
-                      <Sparkles size={8} /> -{formatMoney(item.random_offset, item.currency)}
-                    </span>
-                  )}
-                </div>
-              </div>
-              <div className="flex justify-between items-center pt-2 border-t border-slate-200">
-                 <span className="text-slate-400 text-xs">用户</span>
-                 <span className="text-xs font-mono text-slate-600 truncate max-w-[150px]">{item.user_id}</span>
-              </div>
-            </div>
-
-            <div className="grid grid-cols-4 gap-2">
-              <button 
-                onClick={() => onViewDetail(item)}
-                className={`flex items-center justify-center py-2.5 bg-white border border-slate-200 text-slate-600 rounded-lg text-sm font-medium active:scale-95 transition ${item.status === 'pending' ? 'col-span-1' : 'col-span-4'}`}
-              >
-                <Eye size={18} />
-              </button>
-              {item.status === 'pending' && (
-                <button 
-                  onClick={() => onApprove(item)}
-                  className="col-span-3 flex items-center justify-center gap-1 py-2.5 bg-green-600 text-white rounded-lg text-sm font-bold active:scale-95 transition shadow-sm"
-                >
-                  <CheckCircle size={16} /> 确认收款
-                </button>
-              )}
-            </div>
-          </div>
-        ))}
-      </div>
-    </div>
-  );
-}