// --- 依存関係のフォールバック (プレビュー環境でのクラッシュ回避用) ---
if (typeof window !== 'undefined') {
if (!window.React) { window.React = { useState: () => [null, ()=>{}], useEffect: ()=>{}, useMemo: (f)=>f() }; }
if (!window.Icons) {
window.Icons = new Proxy({}, { get: () => function DummyIcon(props) { return window.React.createElement('span', {style:{display:'inline-block', width:'1em', height:'1em', backgroundColor:'#4b5563', borderRadius:'50%'}}, ''); } });
}
if (!window.getCourseDisplay) window.getCourseDisplay = c => c === '30min' ? '30分' : (c === '60min' ? '60分' : c);
if (!window.getCourseShortDisplay) window.getCourseShortDisplay = c => c === '30min' ? '30分' : (c === '60min' ? '60分' : c);
if (!window.fetchApi) window.fetchApi = async () => ({ success: true });
}
const { useState, useEffect, useMemo } = window.React;
const Icons = window.Icons;
const getCourseDisplay = window.getCourseDisplay;
const getCourseShortDisplay = window.getCourseShortDisplay;
const fetchApi = window.fetchApi;
// --- ユーティリティ ---
const checkIsAdmin = (user) => { return user && (String(user.is_admin) === '1' || user.is_admin === true); };
const CONSULTATION_METHODS = ['電話', 'LINE', 'Zoom', 'Google Meet', 'Skype', 'FaceTime', 'Teams'];
// --- トースト通知コンポーネント (UI改善) ---
const Toast = ({ message, type = 'success', onClose }) => {
useEffect(() => {
const timer = setTimeout(onClose, 3000);
return () => clearTimeout(timer);
}, [onClose]);
return (
{type === 'success' ? : }
{message}
);
};
// --- 各種管理コンポーネント ---
const TellerForm = ({ initialData, onSave, onClose, isAdmin, showToast }) => {
const [activeTab, setActiveTab] = useState('basic');
const defaultForm = { name: '', role: '', description: '', image_url: '', personal_url: '', twitter_url: '', instagram_url: '', work_start_time: '10:00', work_end_time: '19:00', userid: '', password: '', email: '', is_admin: 0, is_visible: 1, available_methods: '', connection_metadata: '{}', real_name: '', kana_name: '', private_birth_date: '', zip_code: '', address: '', note: '', color: 'purple', price_30min: 5000, price_60min: 9000, price_extend10min: 1500, commission_rate: 60 };
const [form, setForm] = useState(() => initialData ? { ...defaultForm, ...initialData, password: '' } : defaultForm);
const [selectedMethods, setSelectedMethods] = useState([]);
const [connectData, setConnectData] = useState({});
useEffect(() => {
if (form.available_methods) setSelectedMethods(form.available_methods.split(',').filter(Boolean));
if (form.connection_metadata) { try { setConnectData(typeof form.connection_metadata === 'string' ? JSON.parse(form.connection_metadata) : form.connection_metadata); } catch(e) { setConnectData({}); } }
}, []);
const handleMethodToggle = (method) => { let newMethods = selectedMethods.includes(method) ? selectedMethods.filter(m => m !== method) : [...selectedMethods, method]; setSelectedMethods(newMethods); setForm(prev => ({ ...prev, available_methods: newMethods.join(',') })); };
const handleConnectDataChange = (method, value) => { const newData = { ...connectData, [method]: value }; setConnectData(newData); setForm(prev => ({ ...prev, connection_metadata: JSON.stringify(newData) })); };
const handleImageChange = (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onloadend = () => setForm({...form, image_url: reader.result}); reader.readAsDataURL(file); } };
const themeColors = [
{ id: 'purple', class: 'bg-purple-600', shadow: 'shadow-purple-500/50' }, { id: 'blue', class: 'bg-blue-600', shadow: 'shadow-blue-500/50' },
{ id: 'red', class: 'bg-red-600', shadow: 'shadow-red-500/50' }, { id: 'green', class: 'bg-green-600', shadow: 'shadow-green-500/50' },
{ id: 'gray', class: 'bg-gray-600', shadow: 'shadow-gray-500/50' },
];
const inputClass = "w-full bg-gray-900/80 border border-gray-700 rounded-xl p-3 text-white focus:border-yellow-500 focus:bg-gray-900 focus:ring-2 focus:ring-yellow-500/20 outline-none transition-all duration-300 placeholder-gray-600";
const labelClass = "text-xs text-gray-400 font-bold mb-1.5 block tracking-widest";
return (
{initialData ? 'プロフィール編集' : '新規鑑定師の登録'}
{initialData ? `ID: ${form.userid}` : 'New Profile'}
{['basic', 'methods', ...(isAdmin ? ['internal'] : [])].map(tab => (
))}
{activeTab === 'basic' && (
{isAdmin && (
)}
{form.image_url ?

:
}
プロフィール画像
)}
{activeTab === 'methods' && (
お客様との対話方法を設定します。
チェックを入れた項目が、お客様の予約画面で選択可能になります。各ツールで使用する「あなたのIDやURL」を入力しておくと、支払いが完了したお客様へ自動で送信されます。
{['電話', 'LINE', 'Zoom', 'Google Meet', 'Skype', 'FaceTime', 'Teams'].map(m => (
{selectedMethods.includes(m) && (
handleConnectDataChange(m, e.target.value)} />
)}
))}
)}
{activeTab === 'internal' && isAdmin && (
管理者専用の機密情報です。
このタブに入力された情報はサイト上に公開されず、業務連絡用の情報として厳重に管理されます。
)}
{/* 【改善】保存ボタン領域を最下部にSticky固定 */}
);
};
const AdminTellers = ({ tellers, onEdit, onToggleVisibility, onAddNew }) => (
鑑定師管理
{tellers.map(t => (
{t.image_url ? (

) : (
)}
{t.description}
))}
);
// 【復活させたBookingModalコンポーネント】
const AdminBookingModal = ({ booking, teller, onClose, onUpdateStatus, onRequestExtension }) => {
if (!booking) return null;
const extendPrice = teller?.price_extend10min || 1500;
return (
予約日時
{booking.booking_date}
お客様情報
{booking.user_name} {booking.user_email}
コースと料金
{getCourseDisplay(booking.course)} ¥{booking.final_price?.toLocaleString()}
ステータス変更
{[{ id: 'pending', label: '未払い', c: 'hover:bg-yellow-900/50 hover:border-yellow-600 text-yellow-500', active: 'bg-yellow-600 text-white border-yellow-500 shadow-[0_0_15px_rgba(202,138,4,0.3)]' },
{ id: 'confirmed', label: '確定済', c: 'hover:bg-green-900/50 hover:border-green-600 text-green-500', active: 'bg-green-600 text-white border-green-500 shadow-[0_0_15px_rgba(22,163,74,0.3)]' },
{ id: 'completed', label: '完了', c: 'hover:bg-blue-900/50 hover:border-blue-600 text-blue-500', active: 'bg-blue-600 text-white border-blue-500 shadow-[0_0_15px_rgba(37,99,235,0.3)]' },
{ id: 'cancelled', label: 'キャンセル', c: 'hover:bg-gray-700 hover:border-gray-500 text-gray-400', active: 'bg-gray-700 text-white border-gray-500' }
].map(s => (
))}
{booking.status === 'confirmed' && !booking.extension_status && (
オプション操作
)}
{booking.extension_status === 'pending' && (
⏳ 延長料金 (¥{booking.extension_fee?.toLocaleString() || extendPrice.toLocaleString()}) のお客様支払い待ち
)}
{booking.extension_status === 'paid' && (
)}
);
};
const AdminScheduleTable = ({ selectedTeller, bookings, onBookingClick, onBookingDrop, onToggleHoliday, showToast }) => {
if (!selectedTeller) return 占い師を選択してください
;
const startHour = 8; const endHour = 24; const totalHours = endHour - startHour; const hourWidth = 60;
const hours = Array.from({ length: totalHours + 1 }, (_, i) => startHour + i);
const dates = Array.from({ length: 14 }, (_, i) => { const d = new Date(); d.setDate(d.getDate() + i); return { full: d.toISOString().split('T')[0], display: `${d.getMonth() + 1}/${d.getDate()}` }; });
const [dragOverCell, setDragOverCell] = useState(null);
const handleDragStart = (e, bookingId) => {
e.dataTransfer.setData('bookingId', bookingId);
e.dataTransfer.effectAllowed = 'move';
setTimeout(() => e.target.style.opacity = '0.5', 0);
};
const handleDragEnd = (e) => { e.target.style.opacity = '1'; setDragOverCell(null); };
const handleDragOver = (e, dateStr) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const minutesFromStart = (x / hourWidth) * 60;
const totalMinutes = Math.floor(minutesFromStart / 30) * 30; // 30分単位に丸める
setDragOverCell(`${dateStr}_${totalMinutes}`);
};
const handleDragLeave = () => setDragOverCell(null);
const handleDrop = (e, targetDate) => {
e.preventDefault();
setDragOverCell(null);
const bookingId = e.dataTransfer.getData('bookingId');
if (!bookingId) return;
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const minutesFromStart = (x / hourWidth) * 60;
const totalMinutes = Math.round(minutesFromStart / 30) * 30;
let newH = startHour + Math.floor(totalMinutes / 60);
let newM = totalMinutes % 60;
if (newH < startHour) { newH = startHour; newM = 0; }
if (newH >= endHour) { newH = endHour - 1; newM = 30; }
const timeStr = `${String(newH).padStart(2, '0')}:${String(newM).padStart(2, '0')}`;
onBookingDrop(bookingId, targetDate, timeStr);
};
return (
{dates.map(dateItem => {
const dayBookings = bookings.filter(b => b.fortune_teller_id == selectedTeller.id && (b.booking_date || '').startsWith(dateItem.full) && b.status !== 'cancelled');
const isHoliday = dayBookings.some(b => b.status === 'holiday');
return (
{dateItem.display}
handleDragOver(e, dateItem.full)} onDragLeave={handleDragLeave} onDrop={isHoliday ? undefined : (e) => handleDrop(e, dateItem.full)}>
{isHoliday &&
HOLIDAY
}
{!isHoliday && Array.from({ length: totalHours * 2 }).map((_, i) => {
const isOver = dragOverCell === `${dateItem.full}_${i * 30}`;
return (
)
})}
{!isHoliday && dayBookings.map(b => {
if (b.status === 'holiday') return null;
const [h,m] = (b.booking_date || '00:00 10:00').split(' ')[1].split(':').map(Number);
const left = ((h - startHour) * 60 + m) / 60 * hourWidth;
const colorClass = b.status === 'confirmed' ? 'bg-gradient-to-r from-green-600 to-green-500 border-green-400' : b.status === 'completed' ? 'bg-gradient-to-r from-blue-700 to-blue-600 border-blue-500 opacity-60' : 'bg-gradient-to-r from-yellow-600 to-yellow-500 border-yellow-400';
const isExtended = b.extension_status === 'paid' || b.extension_status === 'pending';
const widthClass = (b.course && b.course.includes('60')) ? '58px' : (isExtended ? '38px' : '28px');
return (
handleDragStart(e, b.id)} onDragEnd={handleDragEnd} onClick={() => onBookingClick(b)} className={`absolute top-2 bottom-2 rounded-md cursor-grab border flex flex-col justify-center px-1 text-[10px] text-white overflow-hidden shadow-lg hover:shadow-xl hover:brightness-110 active:scale-95 transition-all z-10 ${colorClass} ${b.extension_status === 'pending' ? 'animate-pulse' : ''}`} style={{ left: `${left}px`, width: widthClass }} title={`${b.user_name} (${getCourseDisplay(b.course)})`}>
{b.user_name}
{getCourseShortDisplay(b.course)}{isExtended ? '+10m' : ''}
);
})}
);
})}
未払い
確定
完了
※予約枠をドラッグ&ドロップで時間変更できます
);
};
const AdminAnalytics = ({ tellers, bookings, authUser, isAdmin }) => {
const [targetMonth, setTargetMonth] = useState(() => { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; });
const stats = useMemo(() => {
let targetTellers = tellers.filter(t => !checkIsAdmin(t));
if (!isAdmin) targetTellers = targetTellers.filter(t => t.id === authUser.id);
const data = targetTellers.map(t => ({ ...t, totalSales: 0, count: 0, commissionAmount: 0 }));
let total = 0; let totalCommission = 0;
bookings.forEach(b => {
if (b.status === 'cancelled' || b.status === 'holiday' || b.status === 'pending') return;
if (!(b.booking_date || '').startsWith(targetMonth)) return;
const t = data.find(dt => dt.id == b.fortune_teller_id);
if (t) { const price = parseInt(b.final_price || 0) + parseInt(b.extension_fee || 0); t.totalSales += price; total += price; t.count++; }
});
data.forEach(t => { const rate = parseInt(t.commission_rate) || 60; t.commissionAmount = Math.floor(t.totalSales * (rate / 100)); totalCommission += t.commissionAmount; });
return { tellers: data.sort((a,b)=>b.totalSales-a.totalSales), total, totalCommission };
}, [tellers, bookings, authUser, isAdmin, targetMonth]);
return (
{isAdmin ? '売上・報酬分析' : '今月の報酬明細'}
setTargetMonth(e.target.value)} className="bg-gray-900 border border-gray-700 rounded-lg p-2 text-white text-sm outline-none focus:border-yellow-500" />
{isAdmin ? '総売上' : 'あなたの総売上'}
¥{stats.total.toLocaleString()}
{isAdmin ? '占い師への支払報酬額合計' : '今月の獲得報酬 (見込み)'}
¥{stats.totalCommission.toLocaleString()}
詳細データ ({targetMonth})
| 鑑定師 | 件数 | 売上金額 | 歩合率 | 報酬額 (支払額) | {isAdmin && 本部利益 | }
{stats.tellers.map(t => (
{t.image_url ? : }{t.name} |
{t.count}件 |
¥{t.totalSales.toLocaleString()} |
{t.commission_rate}% |
¥{t.commissionAmount.toLocaleString()} |
{isAdmin && ¥{(t.totalSales - t.commissionAmount).toLocaleString()} | }
))}
{stats.tellers.length === 0 && | この月のデータはありません |
}
);
};
const AdminCustomers = ({ showToast }) => {
const [customers, setCustomers] = useState([]);
const [searchQuery, setSearchQuery] = useState('');
useEffect(() => { fetchApi('customer_stats').then(setCustomers); }, []);
const toggleBl = async (email, current) => {
await fetchApi('toggle_blacklist', { method: 'POST', body: JSON.stringify({ email, is_blacklisted: current ? 0 : 1 }) });
fetchApi('customer_stats').then(setCustomers);
showToast(current ? 'ブラックリストから解除しました' : 'ブラックリストに登録しました', current ? 'success' : 'error');
};
const filteredCustomers = customers.filter(c =>
(c.user_name && c.user_name.includes(searchQuery)) ||
(c.user_email && c.user_email.includes(searchQuery))
);
return (
| 顧客名 / メール | 総予約 | キャンセル | 操作 |
{filteredCustomers.map(c => {
const isBl = parseInt(c.is_blacklisted) === 1;
return (
{c.user_name} {c.user_email} {isBl && BL登録中} |
{c.total_bookings} |
{c.cancel_count} |
|
)
})}
{filteredCustomers.length === 0 && | 該当する顧客が見つかりません |
}
);
};
// 【復元】クーポン管理コンポーネント
const AdminCoupons = ({ showToast }) => {
const [coupons, setCoupons] = useState([]);
const [editingCoupon, setEditingCoupon] = useState(null);
const loadData = () => fetchApi('get_coupons').then(setCoupons);
useEffect(() => { loadData(); }, []);
const handleSaveCoupon = async (e) => {
e.preventDefault();
try {
const res = await fetchApi('save_coupon', { method: 'POST', body: JSON.stringify(editingCoupon) });
if (res && res.success) {
setEditingCoupon(null);
loadData();
showToast('クーポンを保存しました');
} else {
showToast(res?.message || '保存に失敗しました', 'error');
}
} catch (error) {
showToast('通信エラーが発生しました', 'error');
}
};
const handleDeleteCoupon = async (id) => {
if(!confirm('本当にこのクーポンを削除しますか?')) return;
try {
await fetchApi('delete_coupon', { method: 'POST', body: JSON.stringify({ id }) });
loadData();
showToast('クーポンを削除しました');
} catch(e) {
showToast('エラーが発生しました', 'error');
}
};
const handleToggleActive = async (coupon) => {
const updatedCoupon = { ...coupon, is_active: parseInt(coupon.is_active) === 1 ? 0 : 1 };
try {
await fetchApi('save_coupon', { method: 'POST', body: JSON.stringify(updatedCoupon) });
loadData();
showToast(updatedCoupon.is_active ? 'クーポンを有効にしました' : 'クーポンを無効にしました');
} catch (e) {
showToast('エラーが発生しました', 'error');
}
};
const inputClass = "w-full bg-gray-900 border border-gray-700 rounded-xl p-3 outline-none focus:border-yellow-500 focus:ring-2 focus:ring-yellow-500/20 text-white text-sm transition-all duration-300";
const btnPrimary = "bg-gradient-to-r from-yellow-600 to-yellow-500 hover:from-yellow-500 hover:to-yellow-400 text-gray-900 px-6 py-2.5 rounded-lg font-bold text-sm shadow-[0_0_15px_rgba(234,179,8,0.3)] transition-all active:scale-95 flex items-center gap-2";
const btnCancel = "bg-gray-700 hover:bg-gray-600 text-white px-6 py-2.5 rounded-lg font-bold text-sm transition-all duration-300 active:scale-95";
return (
{!editingCoupon ? (
クーポン一覧
| コード | 説明 | 割引 | 有効期限 | 状態 | 操作 |
{coupons.map(c => (
| {c.code} |
{c.description} |
{c.discount_type === 'fixed' ? `¥${c.discount_value} OFF` : `${c.discount_value}% OFF`} |
{c.expiry_date ? c.expiry_date : '無期限'} |
|
|
))}
{coupons.length === 0 && (
| クーポンが登録されていません |
)}
) : (
)}
);
};
const AdminContents = ({ tellers, authUser, isAdmin, showToast }) => {
const [activeSubTab, setActiveSubTab] = useState('blogs');
const [news, setNews] = useState([]); const [testimonials, setTestimonials] = useState([]); const [faqs, setFaqs] = useState([]); const [blogs, setBlogs] = useState([]);
const [editingNews, setEditingNews] = useState(null); const [editingTestimonial, setEditingTestimonial] = useState(null); const [editingFaq, setEditingFaq] = useState(null); const [editingBlog, setEditingBlog] = useState(null);
const loadData = () => {
if (isAdmin) { fetchApi('news_admin').then(setNews); fetchApi('testimonials_admin').then(setTestimonials); fetchApi('faqs_admin').then(setFaqs); }
fetchApi('blogs_admin').then(setBlogs);
};
useEffect(() => { loadData(); }, [isAdmin]);
const handleSave = async (apiAction, data, setEditingFn, successMsg) => {
try { await fetchApi(apiAction, { method: 'POST', body: JSON.stringify(data) }); setEditingFn(null); loadData(); showToast(successMsg); }
catch(err) { showToast('保存に失敗しました', 'error'); }
};
const handleDelete = async (apiAction, id, successMsg) => {
if(!confirm('本当に削除しますか?')) return;
try { await fetchApi(apiAction, { method: 'POST', body: JSON.stringify({ id }) }); loadData(); showToast(successMsg); }
catch(err) { showToast('削除に失敗しました', 'error'); }
};
const handleBlogImageChange = (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onloadend = () => setEditingBlog({...editingBlog, image_url: reader.result}); reader.readAsDataURL(file); } };
const inputClass = "w-full bg-gray-900 border border-gray-700 rounded-xl p-3 outline-none focus:border-yellow-500 focus:ring-2 focus:ring-yellow-500/20 text-white text-sm transition-all duration-300";
const btnPrimary = "bg-gradient-to-r from-yellow-600 to-yellow-500 hover:from-yellow-500 text-gray-900 px-6 py-2.5 rounded-lg font-bold text-sm shadow-[0_0_15px_rgba(234,179,8,0.3)] transition-all active:scale-95";
const btnCancel = "bg-gray-700 hover:bg-gray-600 text-white px-6 py-2.5 rounded-lg font-bold text-sm transition-all active:scale-95";
return (
{isAdmin && }
{isAdmin && }
{isAdmin && }
{activeSubTab === 'blogs' && (
{!editingBlog ? (
<>
執筆した記事一覧
| 公開日 | タイトル | {isAdmin && 執筆者 | }公開 | 操作 |
{blogs.map(b => (
| {b.published_date} |
{b.title} {b.category || '未分類'} |
{isAdmin && {b.author_name || '運営'} | }
{parseInt(b.is_visible) ? ● : ●} |
|
))}
{blogs.length === 0 && | 記事がありません |
}
>
) : (
)}
)}
{/* 【復元】お知らせ管理 */}
{activeSubTab === 'news' && isAdmin && (
{!editingNews ? (
<>
お知らせ一覧
| 日付 / タグ | 内容 | 公開 | 操作 |
{news.map(n => (
| {n.published_date} {n.tag} |
{n.text} |
{parseInt(n.is_visible) ? ● : ●} |
|
))}
{news.length === 0 && | お知らせがありません |
}
>
) : (
)}
)}
{/* 【復元】お客様の声管理 */}
{activeSubTab === 'testimonials' && isAdmin && (
{!editingTestimonial ? (
<>
お客様の声一覧
| お名前 / 年代 | 対象鑑定師 | レビュー内容 | 公開 | 操作 |
{testimonials.map(t => (
{t.customer_name} {t.age_group} |
{t.teller_name} |
{[...Array(t.rating||5)].map((_,i)=>)} {t.testimonial_text} |
{parseInt(t.is_visible) ? ● : ●} |
|
))}
{testimonials.length === 0 && | レビューがありません |
}
>
) : (
)}
)}
{/* 【復元】FAQ管理 */}
{activeSubTab === 'faqs' && isAdmin && (
{!editingFaq ? (
<>
よくある質問 (FAQ) 一覧
| 順序 | 質問 / 回答 | 公開 | 操作 |
{faqs.map(f => (
| {f.sort_order} |
Q. {f.question}
A. {f.answer}
|
{parseInt(f.is_visible) ? ● : ●} |
|
))}
{faqs.length === 0 && | FAQがありません |
}
>
) : (
)}
)}
);
};
const AdminViewApp = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [authUser, setAuthUser] = useState(null);
const [userid, setUserid] = useState('admin');
const [password, setPassword] = useState('pass');
const [activeTab, setActiveTab] = useState(() => {
if (typeof window !== 'undefined') {
const params = new URLSearchParams(window.location.search);
return params.get('tab') || 'schedule';
}
return 'schedule';
});
const [tellers, setTellers] = useState([]);
const [bookings, setBookings] = useState([]);
const [selectedTellerId, setSelectedTellerId] = useState(null);
const [viewingBooking, setViewingBooking] = useState(null);
const [showForm, setShowForm] = useState(false);
const [editingTeller, setEditingTeller] = useState(null);
// 【追加】Toast通知の状態
const [toast, setToast] = useState(null);
const isAdmin = authUser && parseInt(authUser.is_admin) === 1;
const showToast = (message, type = 'success') => setToast({ message, type, id: Date.now() });
const handleTabChange = (tab) => {
setActiveTab(tab);
if (typeof window !== 'undefined') {
const url = new URL(window.location.href);
url.searchParams.set('tab', tab);
window.history.replaceState({}, '', url);
}
};
const fetchData = () => {
fetchApi('fortune_tellers_admin').then(data => {
setTellers(data);
if(data.length > 0 && !selectedTellerId) {
if (authUser && parseInt(authUser.is_admin) === 0) { setSelectedTellerId(authUser.id); }
else { setSelectedTellerId(data[0].id); }
}
});
fetchApi('bookings_admin').then(setBookings);
};
useEffect(() => { if (isLoggedIn && authUser) fetchData(); }, [isLoggedIn, authUser]);
const handleLogin = async (e) => {
e.preventDefault();
const res = await fetchApi('login', { method: 'POST', body: JSON.stringify({ userid, password }) });
if (res.success) {
setAuthUser(res.user);
if (parseInt(res.user.is_admin) === 0) { setSelectedTellerId(res.user.id); }
setIsLoggedIn(true);
showToast('ログインしました');
} else {
showToast(res.message, 'error');
}
};
const handleSaveTeller = async (data) => {
const action = editingTeller ? 'update_teller' : 'create_teller';
const payload = editingTeller ? { ...data, id: editingTeller.id } : data;
await fetchApi(action, { method: 'POST', body: JSON.stringify(payload) });
fetchData();
setShowForm(false);
setEditingTeller(null);
};
const toggleVisibility = async (id, current) => {
const nextVal = parseInt(current) === 1 ? 0 : 1;
await fetchApi('toggle_visibility', { method: 'POST', body: JSON.stringify({ id, is_visible: nextVal }) });
fetchData();
showToast(nextVal ? '公開にしました' : '非公開にしました');
};
if (!isLoggedIn) {
return (
{toast &&
setToast(null)} />}
);
}
return (
{toast &&
setToast(null)} />}
管理パネル
{isAdmin ? '管理者権限' : authUser.name}
{isAdmin && (
)}
{!isAdmin && (
)}
{isAdmin && }
{isAdmin && }
{isAdmin && }
{activeTab === 'schedule' && isAdmin && (
表示する占い師を選択
)}
{activeTab === 'schedule' ? (
t.id===selectedTellerId)}
bookings={bookings}
onBookingClick={setViewingBooking}
showToast={showToast}
onBookingDrop={(id, date, time) => {
if (!confirm(`予約日時を ${date} ${time} に変更しますか?\n※お客様には自動で通知されませんので別途ご連絡ください。`)) return;
fetchApi('update_booking_time', { method: 'POST', body: JSON.stringify({ id, booking_date: `${date} ${time}:00` }) })
.then(() => { fetchData(); showToast('予約日時を変更しました'); });
}}
onToggleHoliday={(date) => {
fetchApi('toggle_holiday', { method: 'POST', body: JSON.stringify({ teller_id: selectedTellerId, date }) })
.then(fetchData);
}}
/>
) : (
{activeTab === 'analytics' &&
}
{activeTab === 'customers' &&
}
{activeTab === 'coupons' && isAdmin &&
}
{activeTab === 'contents' &&
}
{activeTab === 'tellers' && isAdmin &&
{setEditingTeller(t); setShowForm(true);}} onToggleVisibility={toggleVisibility} onAddNew={() => {setEditingTeller(null); setShowForm(true);}} />}
)}
{showForm && { setShowForm(false); setEditingTeller(null); }} isAdmin={isAdmin} showToast={showToast} />}
t.id == viewingBooking.fortune_teller_id) : null}
onClose={() => setViewingBooking(null)}
onUpdateStatus={(id, st) => {
fetchApi('booking_status', {method:'POST', body:JSON.stringify({id, status:st})}).then(() => { fetchData(); showToast('ステータスを更新しました'); });
setViewingBooking(null);
}}
onRequestExtension={(id, fee) => {
fetchApi('request_extension', {method: 'POST', body: JSON.stringify({id, fee})}).then(() => {
fetchData(); showToast('延長を許可しました。お客様の支払いを待ちます');
setViewingBooking(prev => ({...prev, extension_status: 'pending', extension_fee: fee}));
});
}}
/>
);
};
if (typeof window !== 'undefined') window.AdminView = AdminViewApp;