import React, { useState, useEffect, useRef } from 'react'; import { initializeApp } from 'firebase/app'; import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from 'firebase/auth'; import { getFirestore, collection, onSnapshot, addDoc, updateDoc, doc, deleteDoc, query, where, getDocs } from 'firebase/firestore'; import { Calendar as CalendarIcon, ShieldCheck, Plus, Trash2, Check, X, UserPlus, LogOut, Leaf, Eye, EyeOff, LogIn, Image as ImageIcon, CalendarPlus, Share2, ClipboardCopy, Send, MousePointerClick } from 'lucide-react'; // ========================================== // 1. Firebase Initialization // ========================================== const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {}; const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; // ========================================== // 常數與選項設定 // ========================================== const ALLIANCE_OPTIONS = ['建豪阿詩', '君立惠萍', '殿順宜玲', 'Favor', '顏值爆表', '心不老', 'Revival']; const SHIFT_OPTIONS = ['導覽人員 (需求 1位)', '招待人員 10:00-13:30 (需求 5-6位)', '招待人員 13:30-17:00 (需求 5-6位)']; // ========================================== // 2. 主應用程式 Main App // ========================================== export default function App() { const [fbUser, setFbUser] = useState(null); const [activeTab, setActiveTab] = useState('home'); // home, admin, dashboard, login const [registrations, setRegistrations] = useState([]); const [usersInfo, setUsersInfo] = useState([]); // 登入狀態管理 const [isAdmin, setIsAdmin] = useState(false); const [loggedInUser, setLoggedInUser] = useState(null); // 初始化 Firebase Auth useEffect(() => { const initAuth = async () => { try { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) { await signInWithCustomToken(auth, __initial_auth_token); } else { await signInAnonymously(auth); } } catch (error) { console.error("Auth error:", error); } }; initAuth(); const unsubscribe = onAuthStateChanged(auth, (u) => setFbUser(u)); return () => unsubscribe(); }, []); // 取得全域報名資料與使用者資料 useEffect(() => { if (!fbUser) return; const regsRef = collection(db, 'artifacts', appId, 'public', 'data', 'volunteer_registrations'); const usersRef = collection(db, 'artifacts', appId, 'public', 'data', 'volunteer_users'); const unsubRegs = onSnapshot(regsRef, (snapshot) => { const data = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); data.sort((a, b) => b.createdAt - a.createdAt); setRegistrations(data); }); const unsubUsers = onSnapshot(usersRef, (snapshot) => { const data = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); setUsersInfo(data); }); return () => { unsubRegs(); unsubUsers(); }; }, [fbUser]); return (
{/* 導覽列 (後台管理移至右上角) */}
{/* 左側:Logo 與 主要導覽按鈕 */}

setActiveTab('home')}> 嶺頭志工服務網 嶺頭志工

{/* 右上角:後台管理入口 */} {/* 手機版後台圖示 */}
{/* 主要內容區 */}
{activeTab === 'home' && } {activeTab === 'login' && } {activeTab === 'dashboard' && } {activeTab === 'admin' && }
); } // 導覽列按鈕元件 function NavBtn({ active, onClick, icon, text }) { return ( ); } // ========================================== // 3. 首頁 (月曆與報名表) // ========================================== function HomeView({ registrations, db, appId, usersInfo, setActiveTab, setLoggedInUser }) { const formRef = useRef(null); const [selectedDate, setSelectedDate] = useState(''); const handleDateSelect = (dateStr) => { setSelectedDate(dateStr); // 平滑滾動到報名表單 formRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }; return (
{/* 歡迎與月曆區塊 */}

本月排班動態

點擊下方月曆上有空缺的日期,即可快速填寫報名登記表!

{/* 月曆元件 */}
{/* 報名表單 */}
); } // 月曆元件 function CalendarWidget({ registrations, onDateSelect }) { const today = new Date(); const [currentDate, setCurrentDate] = useState(new Date(today.getFullYear(), today.getMonth(), 1)); const year = currentDate.getFullYear(); const month = currentDate.getMonth(); const daysInMonth = new Date(year, month + 1, 0).getDate(); const firstDayOfWeek = new Date(year, month, 1).getDay(); const approvedRegs = registrations.filter(r => r.status === 'approved'); const dayStats = {}; approvedRegs.forEach(reg => { reg.shifts.forEach(shift => { if (!dayStats[shift.date]) { dayStats[shift.date] = { '導覽人員 (需求 1位)': 0, '招待人員 10:00-13:30 (需求 5-6位)': 0, '招待人員 13:30-17:00 (需求 5-6位)': 0 }; } if (dayStats[shift.date][shift.type] !== undefined) { dayStats[shift.date][shift.type] += reg.volunteers.length; } }); }); const nextMonth = () => setCurrentDate(new Date(year, month + 1, 1)); const prevMonth = () => setCurrentDate(new Date(year, month - 1, 1)); const renderDays = () => { const blanks = Array(firstDayOfWeek).fill(null); const days = Array.from({ length: daysInMonth }, (_, i) => i + 1); const allCells = [...blanks, ...days]; return allCells.map((day, i) => { if (!day) return
; const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; const stats = dayStats[dateStr] || { '導覽人員 (需求 1位)': 0, '招待人員 10:00-13:30 (需求 5-6位)': 0, '招待人員 13:30-17:00 (需求 5-6位)': 0 }; return (
onDateSelect(dateStr)} className="bg-white border border-[#E5E0D8] p-1.5 sm:p-3 rounded-lg shadow-sm hover:shadow-md hover:border-[#4A6451] hover:-translate-y-1 transition-all cursor-pointer min-h-[100px] sm:min-h-[130px] flex flex-col group relative" title={`點擊登記 ${dateStr} 排班`} >
{day}
{/* Hover 提示點擊遮罩 */}
); }); }; return (

{year} 年 {month + 1} 月

{renderDays()}
尚有大量缺額
快滿了
已額滿
點擊上方月曆日期,可直接快速帶入登記表!
); } function StatusBadge({ label, max, current }) { let bgColor = "bg-[#E5E0D8] text-[#5C6656]"; // 缺額多 if (current >= max) bgColor = "bg-[#D4A373] text-white"; // 額滿 else if (current > 0) bgColor = "bg-[#A3B18A] text-white"; // 有人報名 return (
{label} {label[0]} {/* 手機版自動縮寫:導, 早, 午 */} {current}/{max}
); } // 報名表單 function RegisterForm({ db, appId, usersInfo, setActiveTab, setLoggedInUser, prefillDate }) { const [userInfo, setUserInfo] = useState({ name: '', phone: '', email: '', lineId: '', churchType: '平鎮浸信會', churchOther: '', alliance: ALLIANCE_OPTIONS[0], group: '', username: '', password: '', confirmPassword: '' }); const [showPwd, setShowPwd] = useState(false); const [shifts, setShifts] = useState([{ date: '', type: SHIFT_OPTIONS[0], companion: '' }]); const [isSubmitting, setIsSubmitting] = useState(false); // 監聽來自月曆點擊的日期,自動帶入表單第一欄 useEffect(() => { if (prefillDate) { setShifts(prev => { const newShifts = [...prev]; newShifts[0].date = prefillDate; return newShifts; }); } }, [prefillDate]); const handleUserChange = (f, v) => setUserInfo({...userInfo, [f]: v}); const handleAddShift = () => setShifts([...shifts, { date: '', type: SHIFT_OPTIONS[0], companion: '' }]); const handleRemoveShift = (i) => setShifts(shifts.filter((_, idx) => idx !== i)); const handleShiftChange = (i, f, v) => { const newS = [...shifts]; newS[i][f] = v; setShifts(newS); }; const handleSubmit = async (e) => { e.preventDefault(); if (userInfo.password !== userInfo.confirmPassword) { alert("❌ 密碼與再次確認密碼不相符!"); return; } if (usersInfo.some(u => u.username === userInfo.username)) { alert("❌ 此使用者名稱已被使用,請更換一個。"); return; } setIsSubmitting(true); try { const finalChurch = userInfo.churchType === '其他' ? userInfo.churchOther : userInfo.churchType; const userData = { username: userInfo.username, password: userInfo.password, name: userInfo.name, phone: userInfo.phone, email: userInfo.email, lineId: userInfo.lineId, church: finalChurch, alliance: userInfo.alliance, group: userInfo.group, createdAt: Date.now() }; await addDoc(collection(db, 'artifacts', appId, 'public', 'data', 'volunteer_users'), userData); const payload = { username: userInfo.username, mainName: userInfo.name, volunteers: [{ name: userInfo.name, phone: userInfo.phone }], shifts: shifts.map(s => { const comps = s.companion.split(',').map(c => c.trim()).filter(c => c); return { date: s.date, type: s.type, extraVolunteers: comps }; }), status: 'pending', createdAt: Date.now() }; await addDoc(collection(db, 'artifacts', appId, 'public', 'data', 'volunteer_registrations'), payload); alert('✅ 報名與帳號建立成功!已自動為您登入,請等待管理員審核。'); setLoggedInUser(userData); setActiveTab('dashboard'); } catch (err) { console.error(err); alert('❌ 送出失敗,請稍後再試。'); } setIsSubmitting(false); }; return (

報名與帳號建立

免繁瑣註冊,填寫完畢系統將自動為您建立專屬帳號。

{/* 區塊 1: 個人資料 */}

1. 聯絡人基本資料

handleUserChange('name', v)} required /> handleUserChange('phone', v)} required /> handleUserChange('email', v)} required /> handleUserChange('lineId', v)} required />
{userInfo.churchType === '其他' && ( handleUserChange('churchOther', v)} required /> )}
handleUserChange('group', v)} required />
{/* 區塊 2: 帳號設定 */}

2. 設定專屬帳號密碼

設定後,未來可直接登入查詢與修改排班。

handleUserChange('username', v)} required placeholder="例如: yourname123" />
handleUserChange('password', v)} required />
handleUserChange('confirmPassword', v)} required />
{/* 區塊 3: 排班時間 */}

3. 排班時間登記

{shifts.map((shift, index) => (
{shifts.length > 1 && ( )}
handleShiftChange(index, 'date', v)} required /> {index === 0 && prefillDate === shift.date && ( ✓ 已由月曆自動帶入 )}
handleShiftChange(index, 'companion', v)} placeholder="例如: 王小華, 李大明" />
))}
); } function Input({ label, type = 'text', value, onChange, required, placeholder }) { return (
onChange(e.target.value)} placeholder={placeholder} className="w-full px-4 py-3 bg-white border-2 border-[#D5D0C5] rounded-xl focus:border-[#4A6451] outline-none transition-all font-medium text-lg text-[#2A3C2F]" />
); } // ========================================== // 4. 志工登入頁面 // ========================================== function LoginView({ usersInfo, setLoggedInUser, setActiveTab }) { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [showPwd, setShowPwd] = useState(false); const handleLogin = (e) => { e.preventDefault(); const user = usersInfo.find(u => u.username === username && u.password === password); if (user) { setLoggedInUser(user); setActiveTab('dashboard'); } else { alert("帳號或密碼錯誤!"); } }; return (

志工登入

); } // ========================================== // 5. 志工專屬後台 (Dashboard) // ========================================== function DashboardView({ user, registrations, db, appId, setLoggedInUser, setActiveTab }) { const canvasRef = useRef(null); const myRegs = registrations.filter(r => r.username === user.username); const handleLogout = () => { setLoggedInUser(null); setActiveTab('home'); }; const generateImage = () => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); ctx.fillStyle = '#1B2A22'; ctx.fillRect(0, 0, 800, 1000); ctx.fillStyle = '#4A6451'; ctx.beginPath(); ctx.arc(800, 0, 300, 0, 2 * Math.PI); ctx.fill(); ctx.fillStyle = '#EAE6D9'; ctx.font = 'bold 48px sans-serif'; ctx.fillText('嶺頭志工 排班確認卡', 50, 100); ctx.fillStyle = '#A3B18A'; ctx.font = 'bold 36px sans-serif'; ctx.fillText(`志工:${user.name}`, 50, 180); ctx.fillStyle = '#F4F1EA'; ctx.font = '28px sans-serif'; let y = 260; myRegs.filter(r => r.status === 'approved').forEach(reg => { reg.shifts.forEach(shift => { ctx.fillText(`🗓️ ${shift.date} | ${shift.type}`, 50, y); if (shift.extraVolunteers && shift.extraVolunteers.length > 0) { y += 40; ctx.fillStyle = '#C2C9BA'; ctx.font = '24px sans-serif'; ctx.fillText(` 同行: ${shift.extraVolunteers.join(', ')}`, 50, y); ctx.fillStyle = '#F4F1EA'; ctx.font = '28px sans-serif'; } y += 60; }); }); if (y === 260) { ctx.fillText('尚無已核准的排班。', 50, y); } ctx.fillStyle = '#7A8675'; ctx.font = 'italic 24px sans-serif'; ctx.fillText('「感謝您委身服事,願神大大賜福您!」', 50, 900); const link = document.createElement('a'); link.download = '嶺頭志工排班.png'; link.href = canvas.toDataURL('image/png'); link.click(); }; const getGCalLink = (shift) => { const dateStr = shift.date.replace(/-/g, ''); let startTime = '100000'; let endTime = '170000'; if(shift.type.includes('10:00')) endTime = '133000'; if(shift.type.includes('13:30')) { startTime = '133000'; endTime = '170000'; } return `https://calendar.google.com/calendar/render?action=TEMPLATE&text=${encodeURIComponent('嶺頭志工服事 - ' + shift.type)}&dates=${dateStr}T${startTime}Z/${dateStr}T${endTime}Z&details=${encodeURIComponent('感謝您的服事!')}`; }; return (

歡迎,{user.name}

{user.church} | {user.alliance} | {user.group}

我的排班紀錄

{myRegs.length === 0 &&

目前尚無排班紀錄。

} {myRegs.map(reg => (
狀態: {reg.status === 'approved' ? '已核准' : reg.status === 'pending' ? '審核中' : '已退回'} 送出時間:{new Date(reg.createdAt).toLocaleDateString()}
    {reg.shifts.map((s, i) => (
  • {s.date} {s.type}
    {s.extraVolunteers && s.extraVolunteers.length > 0 && (
    同行人員: {s.extraVolunteers.join(', ')}
    )}
    {reg.status === 'approved' && ( 加入 Google 行事曆 )}
  • ))}
))}
); } // ========================================== // 6. 管理員後台 // ========================================== function AdminView({ registrations, db, appId, isAdmin, setIsAdmin, usersInfo }) { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [showPwd, setShowPwd] = useState(false); const handleLogin = (e) => { e.preventDefault(); if (username === 'Lingtou' && password === 'lingtou1952') { setIsAdmin(true); } else { alert('帳號或密碼錯誤'); } }; const updateStatus = async (id, newStatus) => { try { await updateDoc(doc(db, 'artifacts', appId, 'public', 'data', 'volunteer_registrations', id), { status: newStatus }); } catch (err) { console.error(err); } }; if (!isAdmin) { return (

管理員登入

); } const pendingRegs = registrations.filter(r => r.status === 'pending'); const approvedRegs = registrations.filter(r => r.status === 'approved'); return (

審核控制台

待審核名單 ({pendingRegs.length})

{pendingRegs.length === 0 &&

目前無待審核資料。

} {pendingRegs.map(reg => ( updateStatus(reg.id, 'approved')} onReject={() => updateStatus(reg.id, 'rejected')} usersInfo={usersInfo} /> ))}

已核准名單 ({approvedRegs.length})

{approvedRegs.map(reg => ( updateStatus(reg.id, 'rejected')} isApproved={true} usersInfo={usersInfo} /> ))}
); } function AdminCard({ reg, onApprove, onReject, isApproved, usersInfo }) { const uInfo = usersInfo.find(u => u.username === reg.username) || {}; const generateMessage = () => { let msg = `【嶺頭志工服事通知】\n親愛的 ${reg.mainName} 平安:\n感謝您報名嶺頭志工服事,您的排班已審核通過!\n\n📌 您的排班日期:\n`; reg.shifts.forEach(s => { msg += `- ${s.date} | ${s.type}\n`; if(s.extraVolunteers && s.extraVolunteers.length > 0) msg += ` (同行: ${s.extraVolunteers.join(', ')})\n`; }); msg += `\n期待與您一同服事,願神賜福您!🌱`; return msg; }; const copyToClipboard = () => { navigator.clipboard.writeText(generateMessage()); alert('已複製提醒文字!'); }; const shareToLine = () => { const url = `https://line.me/R/msg/text/?${encodeURIComponent(generateMessage())}`; window.open(url, '_blank'); }; return (
{reg.mainName} {uInfo.church || '未知教會'} / {uInfo.alliance || '未知聯盟'}
📞 {uInfo.phone || '無電話'} LINE: {uInfo.lineId || '無LINE'}

排班時段

    {reg.shifts.map((s, i) => (
  • {s.date} {s.type} {s.extraVolunteers && s.extraVolunteers.length > 0 && (同行: {s.extraVolunteers.join(', ')})}
  • ))}
{!isApproved ? ( <> ) : ( <> )}
); }