import React, { useState, useEffect } from 'react';
import {
Users, User, CalendarDays, Trophy, CheckCircle2,
MapPin, Image as ImageIcon, RefreshCw, Plus,
Settings, Camera, X, FileText, Upload, Download, Edit2, LogOut, Trash2, Loader2
} from 'lucide-react';
// ==========================================
// 🚀 CLOUDFLARE 部署設定
// ==========================================
// 若要串接真實的 Cloudflare Worker,請將下方引號內填入 Worker 網址
// 例如: "https://visit-api.your-subdomain.workers.dev"
// 預設留空 ("") 時,系統會在網頁本地運行,方便預覽測試!
const API_BASE = "https://visit2026.jayee861103.workers.dev";
// --- Mock Data Initialization (僅供預覽模式或初始使用) ---
const initialStaff = [
{ id: 's1', name: '王大明' },
{ id: 's2', name: '陳小華' },
{ id: 's3', name: '林雅婷' }
];
const initialWorkers = Array.from({ length: 30 }).map((_, i) => ({
id: `w${i+1}`,
empId: `A${String(i+1).padStart(3, '0')}`,
name: `Worker ${String.fromCharCode(65 + (i % 26))}${i}`
}));
export default function App() {
const [currentUser, setCurrentUser] = useState(null);
const [activeTab, setActiveTab] = useState('tasks');
const [isLoading, setIsLoading] = useState(false);
// 自訂對話框狀態
const [dialog, setDialog] = useState({ isOpen: false, type: 'alert', message: '', onConfirm: null });
const showAlert = (message) => setDialog({ isOpen: true, type: 'alert', message, onConfirm: null });
const showConfirm = (message, onConfirm) => setDialog({ isOpen: true, type: 'confirm', message, onConfirm });
const closeDialog = () => setDialog({ ...dialog, isOpen: false });
const [staffList, setStaffList] = useState(initialStaff);
const [workerList, setWorkerList] = useState(initialWorkers);
const [schedule, setSchedule] = useState([]);
const [currentStaffId, setCurrentStaffId] = useState('');
const [currentMonth, setCurrentMonth] = useState(3);
// 初始化與讀取 Cloudflare 資料
useEffect(() => {
// 載入 Excel 套件
if (!document.getElementById('xlsx-script')) {
const script = document.createElement('script');
script.id = 'xlsx-script';
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js';
document.head.appendChild(script);
}
// 讀取遠端 API (Cloudflare KV)
const loadData = async () => {
if (!API_BASE) {
if (schedule.length === 0) generateSchedule(initialStaff, initialWorkers);
return;
}
try {
setIsLoading(true);
const res = await fetch(`${API_BASE}/api/data`);
// 先將回應當作文字讀取,避免直接 .json() 解析失敗
const textData = await res.text();
try {
const data = JSON.parse(textData);
if(data.staffList?.length) setStaffList(data.staffList);
if(data.workerList?.length) setWorkerList(data.workerList);
if(data.schedule?.length) {
setSchedule(data.schedule);
} else {
// 如果遠端沒有排程,使用遠端名單(或本地初始名單)生成一次
generateSchedule(data.staffList?.length ? data.staffList : initialStaff, data.workerList?.length ? data.workerList : initialWorkers);
}
} catch (parseError) {
console.warn("JSON 解析失敗,可能後端回傳錯誤訊息:", textData);
// 若解析失敗(例如拿到 Cannot read property...),則使用預設資料並幫忙產生初始排程
if (schedule.length === 0) generateSchedule(initialStaff, initialWorkers);
}
} catch(e) {
console.error("連線 API 失敗,使用本地預設資料", e);
if (schedule.length === 0) generateSchedule(initialStaff, initialWorkers);
} finally {
setIsLoading(false);
}
};
loadData();
}, []);
// 儲存資料至 Cloudflare KV
const saveData = async (newStaff, newWorkers, newSchedule) => {
if (!API_BASE) return;
try {
await fetch(`${API_BASE}/api/data`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ staffList: newStaff, workerList: newWorkers, schedule: newSchedule })
});
} catch (e) {
console.error("儲存至雲端失敗", e);
}
};
// --- Login Logic ---
const handleLogin = (inputValue) => {
const trimmedInput = inputValue.trim();
if (trimmedInput === 'Az123456') {
setCurrentUser({ id: 'admin', name: '系統管理員', role: 'admin' });
setCurrentStaffId(staffList[0]?.id || '');
setActiveTab('admin');
return;
}
const staff = staffList.find(s => s.name === trimmedInput);
if (staff) {
setCurrentUser({ ...staff, role: 'staff' });
setCurrentStaffId(staff.id);
setActiveTab('tasks');
} else {
showAlert('找不到此負責人,或密碼輸入錯誤。');
}
};
const handleLogout = () => setCurrentUser(null);
// --- Core Logic ---
// 改寫:接受參數以便在初始化時傳入正確的清單
const generateSchedule = (staffs = staffList, workers = workerList) => {
if (staffs.length === 0 || workers.length === 0) return;
let shuffledWorkers = [...workers].sort(() => 0.5 - Math.random());
let staffPiles = {};
staffs.forEach(s => staffPiles[s.id] = []);
shuffledWorkers.forEach((w, index) => {
const staff = staffs[index % staffs.length];
staffPiles[staff.id].push(w);
});
const months = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
let newSchedule = [];
staffs.forEach(staff => {
let pile = staffPiles[staff.id];
pile.forEach((w, idx) => {
let m = months[idx % months.length];
newSchedule.push({
id: `sch_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
workerId: w.id,
workerEmpId: w.empId,
workerName: w.name,
staffId: staff.id,
month: m,
status: 'pending',
address: '',
imageUrl: null,
completeDate: null
});
});
});
setSchedule(newSchedule);
saveData(staffs, workers, newSchedule);
// 如果是手動觸發(已有currentUser)才顯示提示
if (currentUser?.role === 'admin') showAlert("已成功為 2026年 (3-12月) 重新隨機分配訪視名單!");
};
const handleReplace = (scheduleId) => {
showConfirm("確定該移工無法訪視(如返鄉)並要進行替換嗎?\n系統將從下個月(或未來月份)抽調一位至本月,並將該移工移至12月份(最後面)。", () => {
const currentTask = schedule.find(s => s.id === scheduleId);
const futureTask = schedule
.filter(s => s.staffId === currentTask.staffId && s.month > currentTask.month && s.status === 'pending')
.sort((a, b) => a.month - b.month)[0];
if (futureTask) {
const newSchedule = schedule.map(s => {
if (s.id === scheduleId) return { ...s, month: 12, status: 'pending' };
if (s.id === futureTask.id) return { ...s, month: currentTask.month };
return s;
});
setSchedule(newSchedule);
saveData(staffList, workerList, newSchedule);
showAlert("已成功替換!該移工已移至 12 月份,並從未來月份抽調一名補上本月。");
} else {
const anyOtherTask = schedule.find(s => s.staffId === currentTask.staffId && s.id !== scheduleId && s.status === 'pending');
if (anyOtherTask) {
const newSchedule = schedule.map(s => {
if (s.id === scheduleId) return { ...s, month: anyOtherTask.month };
if (s.id === anyOtherTask.id) return { ...s, month: currentTask.month };
return s;
});
setSchedule(newSchedule);
saveData(staffList, workerList, newSchedule);
showAlert("已從其他月份抽調一名移工作為替換!");
} else {
showAlert("您已經沒有其他待訪視的移工可以替換了。");
}
}
});
};
const handleComplete = async (scheduleId, address, imageBase64) => {
let finalImageUrl = imageBase64;
// 儲存至 Cloudflare R2
if (API_BASE && imageBase64.startsWith('data:image')) {
try {
const res = await fetch(`${API_BASE}/api/upload`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ imageBase64 })
});
const textData = await res.text();
try {
const data = JSON.parse(textData);
if(data.imageUrl) finalImageUrl = `${API_BASE}${data.imageUrl}`;
} catch(e) {
console.error("解析上傳回傳值失敗:", textData);
showAlert("圖片上傳 R2 發生錯誤,暫存為本地資料。");
}
} catch (e) {
console.error("Upload error", e);
showAlert("圖片上傳連線失敗,暫存為本地資料。");
}
}
const newSchedule = schedule.map(s => {
if (s.id === scheduleId) {
return {
...s,
status: 'completed',
address: address,
imageUrl: finalImageUrl,
completeDate: s.completeDate || new Date().toISOString()
};
}
return s;
});
setSchedule(newSchedule);
saveData(staffList, workerList, newSchedule);
};
// 刪除與清理
const handleDeleteStaff = (id) => {
showConfirm('確定要刪除此負責人嗎?\n注意:刪除後建議重新產生年度分配表。', () => {
const newList = staffList.filter(s => s.id !== id);
setStaffList(newList);
saveData(newList, workerList, schedule);
});
};
const handleDeleteWorker = (id) => {
showConfirm('確定要刪除此移工嗎?\n注意:刪除後建議重新產生年度分配表。', () => {
const newList = workerList.filter(w => w.id !== id);
setWorkerList(newList);
saveData(staffList, newList, schedule);
});
};
const handleClearStaff = () => {
showConfirm('確定要「一鍵清空」所有負責人名單嗎?\n注意:資料刪除後無法復原!', () => {
setStaffList([]);
saveData([], workerList, schedule);
});
};
const handleClearWorkers = () => {
showConfirm('確定要「一鍵清空」所有移工名單嗎?\n注意:資料刪除後無法復原!', () => {
setWorkerList([]);
saveData(staffList, [], schedule);
});
};
// --- Views ---
const renderContent = () => {
if (!currentUser) return
{dialog.message}
請輸入您的姓名登入
本月份尚無分配任務
設定好名單後,點擊右方按鈕將任務隨機平均分配至 2026年 3~12 月份。