'use client';
import { useState, useEffect, useRef, useCallback } from 'react';
import { formatDisplayDate, formatDisplayDateTime } from '@/lib/date';
type Image = { id: number; filename: string; data: string; category: string; room_id: number | null; location_target: string; uploaded_at: string };
type Slide = { id: number; title: string; subtitle: string; image_url: string | null; image_id: number | null; active: boolean; sort_order: number };
type Room = { id: number; name: string; description: string; price: string; photos: string[]; featured_photo: string | null; active: boolean; sort_order: number; amenity_ids?: number[] };
type CalendarEvent = { id: number; date: string; title: string; type: string; color: string; description: string | null; active: boolean };
type SocialEvent = { id: number; name: string; description: string; price: string | null; date: string | null; photos: string[]; featured_photo: string | null; active: boolean; is_private: boolean; max_guests: number | null; sort_order: number };
type MenuItem = { id: number; category: string; name: string; description: string; price: string; is_daily_special: boolean; active: boolean; sort_order: number; photos: string[]; featured_photo: string | null };
type Place = { id: number; name: string; description: string; photos: string[]; featured_photo: string | null; distance_km: string; visit_time: string; suggestion: string; active: boolean; sort_order: number };
type Review = { id: number; author_name: string; author_email: string | null; rating: number; text: string; approved: boolean };
type FooterConfig = { logo: string; favicon: string; tagline: string; facebook_url: string; instagram_url: string; whatsapp_url: string; address: string; phone: string; email: string };
type SocialRes = { id: number; event_id: number; username: string; event_name: string; guests: number; notes: string; status: string; created_at: string };
type Amenity = { id: number; name: string; icon_svg: string; sort_order: number };
type RestaurantReservation = { id: number; user_id: number | null; date: string; time: string; guests: number; table_name: string; created_at: string; username?: string; first_name?: string; last_name?: string };
type RoomReservation = { id: number; room_id: number; user_id: number | null; check_in: string; check_out: string; status: string; total_price: string; guest_name: string | null; guest_contact_method: string | null; guest_contact: string | null; created_at: string; room_name?: string; username?: string; first_name?: string; last_name?: string };
type Order = { id: number; user_id: number | null; order_type: string; room_id: number | null; subtotal: string; service_fee: string; total: string; status: string; notes: string | null; created_at: string; room_name?: string; username?: string; first_name?: string; last_name?: string; items: { menu_item_id: number; name: string; quantity: number; unit_price: string; total_price: string }[] };
const C = {
gold: '#D4A853', goldLight: 'rgba(212,168,83,0.12)', navy: '#010D1E',
warm: '#7A5A3A', bg: '#F7F4EF', card: '#FFFFFF', border: 'rgba(1,13,30,0.08)',
text: '#444444', textLight: '#888888', danger: '#D32F2F', success: '#388E3C',
shadow: '0 1px 4px rgba(1,13,30,0.06)', shadowHover: '0 6px 20px rgba(1,13,30,0.12)',
};
const NAV = [
{ id: 'brand', label: 'Brand', icon: 'π¨' },
{ id: 'gallery', label: 'Images', icon: 'πΌοΈ' },
{ id: 'slides', label: 'Slideshow', icon: 'ποΈ' },
{ id: 'calendar', label: 'Calendar', icon: 'π
' },
{ id: 'social', label: 'Events', icon: 'πͺ' },
{ id: 'rooms', label: 'Rooms', icon: 'π¨' },
{ id: 'room-status', label: 'Room Status', icon: 'π§Ή' },
{ id: 'room-assignments', label: 'Assignments', icon: 'π' },
{ id: 'room-reservations', label: 'Room Bookings', icon: 'ποΈ' },
{ id: 'restaurant-reservations', label: 'Restaurant', icon: 'π½οΈ' },
{ id: 'orders', label: 'Food Orders', icon: 'π¦' },
{ id: 'kitchen', label: 'Kitchen', icon: 'π¨βπ³' },
{ id: 'amenities', label: 'Amenities', icon: 'β¨' },
{ id: 'trips', label: 'Trips', icon: 'βοΈ' },
{ id: 'messages', label: 'Messages', icon: 'π¬' },
{ id: 'menu', label: 'Menu', icon: 'π' },
{ id: 'places', label: 'Places', icon: 'πΊοΈ' },
{ id: 'reviews', label: 'Reviews', icon: 'β' },
{ id: 'users', label: 'Users', icon: 'π₯' },
{ id: 'terms', label: 'Terms', icon: 'π' },
{ id: 'weather', label: 'Weather', icon: 'π€οΈ' },
];
function I({ label, value, onChange, type = 'text', placeholder, style }: any) {
return (
{label && }
onChange(e.target.value)} placeholder={placeholder}
style={{ padding: '8px 12px', borderRadius: 8, border: `1px solid ${C.border}`, fontSize: '14px', outline: 'none', background: '#FAFAFA', ...style }}
onFocus={e => { (e.target.style as any).borderColor = C.gold; (e.target.style as any).background = '#FFFFFF'; }}
onBlur={e => { (e.target.style as any).borderColor = C.border; (e.target.style as any).background = '#FAFAFA'; }} />
);
}
function TA({ label, value, onChange, rows = 3 }: any) {
return (
{label && }
);
}
function S({ label, value, onChange, options, style }: any) {
return (
{label && }
);
}
function Tgl({ label, checked, onChange }: any) {
return (
);
}
function Btn({ children, onClick, variant = 'primary', size = 'md', disabled }: any) {
const sz = size === 'sm' ? { padding: '5px 10px', fontSize: '12px' } : { padding: '8px 16px', fontSize: '14px' };
const bg = variant === 'danger' ? C.danger : variant === 'secondary' ? '#E8E4DF' : C.gold;
const color = variant === 'secondary' ? C.text : '#fff';
return (
);
}
function Badge({ children, color: c }: any) {
return (
{children}
);
}
function Toast({ message, type, onClose }: any) {
useEffect(() => { const t = setTimeout(onClose, 3500); return () => clearTimeout(t); }, [onClose]);
return (
{message}
);
}
function StatCard({ icon, value, label }: any) {
return (
);
}
function Empty({ icon, title, desc }: any) {
return (
{icon}
{title}
{desc &&
{desc}
}
);
}
/* βββ WebP Migration βββ */
function WebPMigration({ onToast, onRefresh }: { onToast: (msg: string, type: string) => void; onRefresh: () => void }) {
const [migrating, setMigrating] = useState(false);
const [stats, setStats] = useState<{ totalImages: number; totalSizeMB: number } | null>(null);
useEffect(() => {
fetch('/api/admin/migrate-images')
.then(res => res.ok ? res.json() : null)
.then(data => data && setStats({ totalImages: data.totalImages, totalSizeMB: data.totalSizeMB }))
.catch(() => {});
}, []);
const runMigration = async () => {
setMigrating(true);
try {
const res = await fetch('/api/admin/migrate-images', { method: 'POST' });
const data = await res.json();
if (res.ok) {
onToast(`Converted ${data.converted} images, saved ${data.totalSavedMB} MB`, 'success');
onRefresh();
} else {
onToast(data.error || 'Migration failed', 'error');
}
} catch {
onToast('Migration failed', 'error');
}
setMigrating(false);
};
return (
π WebP Migration
{stats ? `${stats.totalImages} images Β· ${stats.totalSizeMB} MB total` : 'Checking...'}
{migrating ? 'Migrating...' : 'Convert to WebP'}
);
}
function SecHead({ title, count, action }: any) {
return (
{title} ({count})
{action}
);
}
/* βββ Image helpers βββ */
function readFileBase64(file: File): Promise {
return new Promise((res, rej) => {
const r = new FileReader();
r.onload = () => res(r.result as string);
r.onerror = rej;
r.readAsDataURL(file);
});
}
/* βββ Entity thumbnail βββ */
function Thumb({ urls, w = 200, h = 140 }: { urls: string[]; w?: number; h?: number }) {
if (!urls?.length) return No image
;
return
;
}
/* βββ Thumbnail grid (inline, small) βββ */
function MiniThumbs({ urls, max = 3 }: { urls: string[]; max?: number }) {
if (!urls?.length) return null;
return (
{urls.slice(0, max).map((u, i) => (

))}
{urls.length > max &&
+{urls.length - max}}
);
}
/* βββ Section uploader (per-section image upload) βββ */
function SectionUploader({ category, room_id, label, onUploaded }: { category: string; room_id: number | null; label: string; onUploaded?: () => void }) {
const ref = useRef(null);
return (
<>
ref.current?.click()} size="sm">{label}
{
for (const file of Array.from(e.target.files || [])) {
const b64 = await readFileBase64(file);
try {
const res = await fetch('/api/admin/images', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: file.name, data: b64, category, room_id, location_target: category }),
});
if (res.ok) onUploaded?.();
} catch (err) { console.error(err); }
}
e.target.value = '';
}} />
>
);
}
/* βββ Organized image gallery with sections and borders βββ */
function ImageGallery({ images, onDelete }: { images: Image[]; onDelete: (id: number) => void }) {
const [selectedCategory, setSelectedCategory] = useState('all');
const [hoveredCard, setHoveredCard] = useState(null);
const sectionCategories = [
{ key: 'slides', label: 'ποΈ Slideshow Gallery', filter: (i: Image) => i.category === 'slides' || i.location_target === 'slides' },
{ key: 'food', label: 'π½οΈ Food Gallery', filter: (i: Image) => i.category === 'food' || i.location_target === 'food' },
{ key: 'spa', label: 'π§ Spa Gallery', filter: (i: Image) => i.category === 'spa' || i.location_target === 'spa' },
{ key: 'pool', label: 'π Pool Gallery', filter: (i: Image) => i.category === 'pool' || i.location_target === 'pool' },
{ key: 'calendar', label: 'π
Calendar Events Gallery', filter: (i: Image) => i.category === 'calendar' || i.location_target === 'calendar' },
{ key: 'public', label: 'πͺ Public Events Gallery', filter: (i: Image) => i.category === 'public' || i.location_target === 'public' || i.category === 'social' || i.location_target === 'social' },
{ key: 'private', label: 'π Private Events Gallery', filter: (i: Image) => i.category === 'private' || i.location_target === 'private' },
];
// Group room images by room location_target (e.g. "room-1", "room-2")
const roomImages = images.filter(
(i) => i.category === 'rooms' || i.location_target?.startsWith('room-')
);
const roomsByNumber: Record = roomImages.reduce((acc, img) => {
const key = img.location_target?.replace('room-', '') || 'uncategorized';
if (!acc[key]) acc[key] = [];
acc[key].push(img);
return acc;
}, {} as Record);
const filteredImages = images.filter((img) => {
if (selectedCategory === 'all') return true;
if (selectedCategory === 'rooms') return img.category === 'rooms' || img.location_target?.startsWith('room-');
if (selectedCategory === 'food') return img.category === 'food' || img.location_target === 'food';
if (selectedCategory === 'spa') return img.category === 'spa' || img.location_target === 'spa';
if (selectedCategory === 'pool') return img.category === 'pool' || img.location_target === 'pool';
if (selectedCategory === 'public') return img.category === 'public' || img.location_target === 'public' || img.category === 'social' || img.location_target === 'social';
if (selectedCategory === 'private') return img.category === 'private' || img.location_target === 'private';
return img.category === selectedCategory || img.location_target === selectedCategory;
});
const ImageCard = ({ img }: { img: Image }) => {
const isHov = hoveredCard === img.id;
return (
setHoveredCard(img.id)}
onMouseLeave={() => setHoveredCard(null)}
>
{img.filename}
{/* Category label overlay on hover */}
{isHov && img.location_target && (
{img.location_target}
)}
);
};
const SectionBox = ({ title, count, children }: { title: string; count: number; children: React.ReactNode }) => (
{title}
{count}
{children}
);
return (
{/* Category filter bar with borders */}
{['all', 'slides', 'food', 'spa', 'pool', 'calendar', 'public', 'private', 'rooms', 'other'].map((cat) => (
setSelectedCategory(cat)}
variant={selectedCategory === cat ? 'primary' : 'secondary'}
size="sm"
>
{cat === 'all' ? 'All Images' : cat.charAt(0).toUpperCase() + cat.slice(1)}
))}
{selectedCategory === 'all' ? (
{/* Static sections */}
{sectionCategories.map((section) => {
const imgs = images.filter(section.filter);
if (imgs.length === 0) return null;
return (
{imgs.map((img) => )}
);
})}
{/* Rooms section with room number subdivision */}
{Object.keys(roomsByNumber).length > 0 && (
{Object.entries(roomsByNumber).map(([roomKey, imgs]) => (
ποΈ Room {roomKey}
{imgs.length} photo{imgs.length !== 1 ? 's' : ''}
{imgs.map((img) => )}
))}
)}
{/* Uncategorized/other */}
{(() => {
const otherImages = images.filter(
(i) => !sectionCategories.some(s => s.filter(i)) &&
!(i.category === 'rooms' || i.location_target?.startsWith('room-'))
);
if (otherImages.length === 0) return null;
return (
{otherImages.map((img) => )}
);
})()}
) : (
{filteredImages.length === 0 ? (
No images in this category
) : (
{filteredImages.map((img) => )}
)}
)}
);
}
/* βββ Entity card βββ */
/* βββ Entity card βββ */
function EntityCard({ title, desc, meta, thumb, actions, photoUrls }: any) {
return (
{ e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.boxShadow = C.shadowHover; }}
onMouseLeave={e => { e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = C.shadow; }}>
{thumb ? (

) : photoUrls?.length ?
:
π·
}
{title}
{desc &&
{desc}
}
{meta &&
{meta}
}
{photoUrls?.length ?
: null}
{actions &&
{actions}
}
);
}
/* βββ Confirm dialog βββ */
function Confirm({ message, onConfirm, onCancel }: any) {
return (
);
}
/* βββ Slides Section βββ */
function SlidesSection({ slides, setSlides, images, onToast, onRefreshImages }: any) {
const [editing, setEditing] = useState(null);
const [confirmDel, setConfirmDel] = useState(null);
const saveSlide = async (slide: Slide) => {
try {
// If image_id is set, clear image_url (image is in images table)
const payload = {
...slide,
image_url: slide.image_id ? null : slide.image_url
};
const url = payload.id ? `/api/slides/${payload.id}` : '/api/slides';
const res = await fetch(url, {
method: payload.id ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (res.ok) {
const data = await res.json();
setEditing(null);
setSlides((s: any) => payload.id ? s.map((x: Slide) => x.id === payload.id ? data.data : x) : [...s, data.data]);
onToast('Slide saved!', 'success');
} else {
const err = await res.json();
console.error('Slide save error:', err);
onToast(err.error || 'Error saving slide', 'error');
}
} catch (err) {
console.error('Slide save exception:', err);
onToast('Error saving slide', 'error');
}
};
const delSlide = async (id: number) => {
try {
await fetch(`/api/slides/${id}`, { method: 'DELETE' });
setSlides((s: any) => s.filter((x: Slide) => x.id !== id));
onToast('Slide deleted', 'success');
} catch { onToast('Error deleting slide', 'error'); }
setConfirmDel(null);
};
return (
setEditing({ id: 0, title: '', subtitle: '', image_url: null, image_id: null, active: true, sort_order: slides.length })} size="sm">+ New Slide} />
{slides.length === 0 ? : (
{slides.map((s: Slide) => (
i.id === s.image_id)?.data) : null}
actions={
<>
setEditing(s)}>Edit
setConfirmDel(s.id)}>Del
>
} />
))}
)}
{editing && (
setEditing(null)} />
)}
{confirmDel !== null && setConfirmDel(null)} onConfirm={() => delSlide(confirmDel)} />}
);
}
function SlideEditor({ slide, setSlide, images, onRefreshImages, onSave, onCancel }: any) {
const [showPicker, setShowPicker] = useState(false);
const [uploading, setUploading] = useState(false);
const fileInputRef = useRef(null);
const handleUpload = async (files: FileList) => {
setUploading(true);
try {
for (const file of Array.from(files)) {
const b64 = await readFileBase64(file);
const res = await fetch('/api/admin/images', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: file.name, data: b64, category: 'slides', room_id: null, location_target: 'slides' }),
});
if (res.ok) {
const data = await res.json();
await onRefreshImages?.();
setSlide((prev: any) => ({ ...prev, image_id: data.data.id, image_url: data.data.data }));
} else {
const err = await res.json();
console.error('Image upload error:', err);
alert('Upload failed: ' + (err.error || 'Unknown error'));
}
}
} catch (err) {
console.error('Upload failed:', err);
alert('Upload failed');
} finally {
setUploading(false);
}
};
const s = slide;
return (
{s.id ? 'Edit Slide' : 'New Slide'}
setSlide({ ...s, title: v })} />
setSlide({ ...s, subtitle: v })} />
setShowPicker(!showPicker)} size="sm" variant="secondary">{showPicker ? 'Hide picker' : 'Pick from gallery'}
{ if (e.target.files?.length) handleUpload(e.target.files); e.target.value = ''; }} />
fileInputRef.current?.click()}>{uploading ? 'Uploading...' : 'Upload from computer'}
{s.image_id && images.find((i: Image) => i.id === s.image_id) && (

i.id === s.image_id)!.data} style={{ width: 80, height: 56, objectFit: 'cover', borderRadius: 8, border: `2px solid ${C.gold}` }} />
)}
{showPicker && (
{images.filter((i: Image) => i.category === 'slides').map((img: Image) => (

{ setSlide({ ...s, image_id: img.id, image_url: img.data }); setShowPicker(false); }}
style={{ width: 64, height: 48, objectFit: 'cover', borderRadius: 6, cursor: 'pointer', border: s.image_id === img.id ? `2px solid ${C.gold}` : '1px solid #eee' }} />
))}
{images.filter((i: Image) => i.category === 'slides').length === 0 &&
No images uploaded. Use "Upload from computer" above.}
)}
setSlide({ ...s, active: v })} />
setSlide({ ...s, sort_order: Number(v) })} style={{ width: 80 }} />
Cancel
onSave(s)}>Save
);
}
/* βββ Calendar Section βββ */
function CalendarSection({ events, setEvents, images, onToast }: any) {
const [editing, setEditing] = useState(null);
const [confirmDel, setConfirmDel] = useState(null);
const save = async (ev: CalendarEvent) => {
try {
const url = ev.id ? `/api/calendar_events/${ev.id}` : '/api/calendar_events';
const res = await fetch(url, {
method: ev.id ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(ev),
});
if (res.ok) {
const data = await res.json();
setEditing(null);
setEvents((e: any) => ev.id ? e.map((x: CalendarEvent) => x.id === ev.id ? data.data : x) : [...e, data.data]);
onToast('Event saved!', 'success');
}
} catch { onToast('Error', 'error'); }
};
const del = async (id: number) => {
await fetch(`/api/calendar_events/${id}`, { method: 'DELETE' });
setEvents((e: any) => e.filter((x: CalendarEvent) => x.id !== id));
onToast('Event deleted', 'success');
setConfirmDel(null);
};
return (
setEditing({ id: 0, date: '', title: '', type: 'default', color: '#D4A853', description: null, active: true })} size="sm">+ New Event} />
{events.length === 0 ? : (
{events.map((ev: CalendarEvent) => (
setEditing(ev)}>Edit
setConfirmDel(ev.id)}>Del
>
} />
))}
)}
{editing && setEditing(null)} />}
{confirmDel !== null && setConfirmDel(null)} onConfirm={() => del(confirmDel)} />}
);
}
function CalendarEditor({ event, setEvent, onSave, onCancel }: any) {
const e = { ...event };
return (
{e.id ? 'Edit Event' : 'New Event'}
setEvent({ ...e, title: v })} />
setEvent({ ...e, date: v })} />
setEvent({ ...e, type: v })}
options={[{ value: 'default', label: 'Default' }, { value: 'special', label: 'Special' }, { value: 'holiday', label: 'Holiday' }]} />
setEvent({ ...e, color: v })} />
setEvent({ ...e, description: v || null })} />
setEvent({ ...e, active: v })} />
Cancel
onSave(e)}>Save
);
}
/* βββ Social Events Section βββ */
function SocialSection({ events, setEvents, images, onToast }: any) {
const [editing, setEditing] = useState(null);
const [confirmDelEvent, setConfirmDelEvent] = useState(null);
const [confirmDelReservation, setConfirmDelReservation] = useState(null);
const [tab, setTab] = useState<'public' | 'private' | 'reservations'>('public');
const [reservations, setReservations] = useState([]);
const [statusFilter, setStatusFilter] = useState<'pending' | 'confirmed' | 'cancelled' | 'all'>('pending');
const [loading, setLoading] = useState(false);
const publicEvents = events.filter((e: SocialEvent) => !e.is_private);
const privateEvents = events.filter((e: SocialEvent) => e.is_private);
useEffect(() => {
if (tab === 'reservations') loadReservations();
}, [tab, statusFilter]);
const loadReservations = async () => {
setLoading(true);
try {
const url = statusFilter === 'all' ? '/api/social_event_reservations' : `/api/social_event_reservations?status=${statusFilter}`;
const res = await fetch(url);
const data = await res.json();
setReservations(data.data || []);
} catch { onToast('Error loading reservations', 'error'); }
setLoading(false);
};
const updateStatus = async (id: number, status: 'confirmed' | 'cancelled') => {
try {
await fetch(`/api/social_event_reservations/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status }),
});
setReservations(r => r.map(x => x.id === id ? { ...x, status } : x));
onToast(`Reservation ${status}`, 'success');
} catch { onToast('Error updating reservation', 'error'); }
};
const delReservation = async (id: number) => {
await fetch(`/api/social_event_reservations/${id}`, { method: 'DELETE' });
setReservations(r => r.filter(x => x.id !== id));
onToast('Reservation deleted', 'success');
setConfirmDelReservation(null);
};
const save = async (ev: SocialEvent) => {
try {
const url = ev.id ? `/api/social_events/${ev.id}` : '/api/social_events';
const res = await fetch(url, {
method: ev.id ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(ev),
});
if (res.ok) {
const data = await res.json();
setEditing(null);
setEvents((e: any) => ev.id ? e.map((x: SocialEvent) => x.id === ev.id ? data.data : x) : [...e, data.data]);
onToast('Event saved!', 'success');
}
} catch { onToast('Error', 'error'); }
};
const del = async (id: number) => {
await fetch(`/api/social_events/${id}`, { method: 'DELETE' });
setEvents((e: any) => e.filter((x: SocialEvent) => x.id !== id));
onToast('Event deleted', 'success');
setConfirmDelEvent(null);
};
const formatDate = (d: string | null) => d ? new Date(d).toLocaleDateString() : '-';
const contactMethodIcon = (m: string) => m === 'email' ? 'βοΈ' : m === 'whatsapp' ? 'π±' : 'π';
const renderEventList = (eventList: SocialEvent[], title: string, icon: string) => (
<>
setEditing({ id: 0, name: '', description: '', price: null, date: null, photos: [], featured_photo: null, active: true, is_private: icon === 'π', max_guests: null, sort_order: events.length })} size="sm">+ New Event} />
{eventList.length === 0 ? : (
{eventList.map((ev: SocialEvent) => (
setEditing(ev)}>Edit
setConfirmDelEvent(ev.id)}>Del
>
} />
))}
)}
>
);
return (
setTab('public')}>πͺ Public
setTab('private')}>π Private
setTab('reservations')}>π
Reservations
{tab === 'public' && renderEventList(publicEvents, 'Public Events', 'πͺ')}
{tab === 'private' && renderEventList(privateEvents, 'Private Events', 'π')}
{tab === 'reservations' && (
<>
{(['pending', 'confirmed', 'cancelled', 'all'] as const).map(s => (
setStatusFilter(s)}>
{s.charAt(0).toUpperCase() + s.slice(1)}
))}
{loading ?
: reservations.length === 0 ?
: (
{reservations.map((r: any) => (
{r.event_name}
{r.guests} guests
{r.status}
Contact: {r.contact_name || r.first_name || r.username || '-'}
Method: {r.contact_method ? contactMethodIcon(r.contact_method) : '-'} {r.contact_method || '-'}
{r.contact_email &&
Email: {r.contact_email}
}
{r.contact_phone &&
Phone: {r.contact_phone}
}
{r.notes &&
"{r.notes}"
}
{r.status === 'pending' && (
<>
updateStatus(r.id, 'confirmed')}>β Confirm
updateStatus(r.id, 'cancelled')}>β Cancel
>
)}
{r.status === 'confirmed' && updateStatus(r.id, 'cancelled')}>β Cancel}
{r.status === 'cancelled' && updateStatus(r.id, 'confirmed')}>β Reactivate}
setConfirmDelReservation(r.id)}>π Delete
))}
)}
{confirmDelReservation !== null &&
setConfirmDelReservation(null)} onConfirm={() => delReservation(confirmDelReservation)} />}
>
)}
{editing && setEditing(null)} />}
{confirmDelEvent !== null && setConfirmDelEvent(null)} onConfirm={() => del(confirmDelEvent)} />}
);
}
function SocialEditor({ event, setEvent, images, onSave, onCancel }: any) {
const [showPicker, setShowPicker] = useState(false);
const [localPhotos, setLocalPhotos] = useState(event.photos || []);
const ev = { ...event, photos: localPhotos };
const addPhotos = async (files: FileList) => {
const newPhotos: string[] = [];
for (const f of Array.from(files)) {
const b64 = await readFileBase64(f);
try {
const r = await fetch('/api/admin/images', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: f.name, data: b64, category: event.is_private ? 'private' : 'social', room_id: null, location_target: event.is_private ? 'private' : 'social' }),
});
if (r.ok) { const d = await r.json(); newPhotos.push(d.data.data); }
} catch { const b = await readFileBase64(f); newPhotos.push(b); }
}
setLocalPhotos(prev => [...prev, ...newPhotos]);
};
return (
{ev.id ? 'Edit Event' : 'New Event'}
setEvent({ ...ev, name: v })} />
setEvent({ ...ev, description: v })} />
setEvent({ ...ev, price: v || null })} />
setEvent({ ...ev, date: v || null })} />
document.getElementById('social-photos')?.click()} size="sm" variant="secondary">+ Add Photos
{ addPhotos(e.target.files!); e.target.value = ''; }} />
{localPhotos.map((p, i) => (
))}
setEvent({ ...ev, is_private: v })} />
{ev.is_private && (
setEvent({ ...ev, max_guests: v ? parseInt(v) : null })} placeholder="Leave empty for unlimited" />
)}
setEvent({ ...ev, active: v })} />
Cancel
onSave(ev)}>Save
);
}
/* βββ Rooms Section βββ */
function PrivateEventsSection({ onToast }: { onToast: (msg: string, type: 'success' | 'error') => void }) {
const [reservations, setReservations] = useState([]);
const [statusFilter, setStatusFilter] = useState<'pending' | 'confirmed' | 'cancelled' | 'all'>('pending');
const [loading, setLoading] = useState(true);
const [confirmDel, setConfirmDel] = useState(null);
useEffect(() => {
loadReservations();
}, [statusFilter]);
const loadReservations = async () => {
setLoading(true);
try {
const url = statusFilter === 'all' ? '/api/private-event-reservations' : `/api/private-event-reservations?status=${statusFilter}`;
const res = await fetch(url);
const data = await res.json();
setReservations(data.data || []);
} catch { onToast('Error loading reservations', 'error'); }
setLoading(false);
};
const updateStatus = async (id: number, status: 'confirmed' | 'cancelled') => {
try {
await fetch(`/api/private-event-reservations/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status }),
});
setReservations(r => r.map(x => x.id === id ? { ...x, status } : x));
onToast(`Reservation ${status}`, 'success');
} catch { onToast('Error updating reservation', 'error'); }
};
const del = async (id: number) => {
await fetch(`/api/private-event-reservations/${id}`, { method: 'DELETE' });
setReservations(r => r.filter(x => x.id !== id));
onToast('Reservation deleted', 'success');
setConfirmDel(null);
};
const formatDate = (d: string | null) => d ? new Date(d).toLocaleDateString() : '-';
const formatTime = (t: string | null) => t || '-';
const contactMethodIcon = (m: string) => m === 'email' ? 'βοΈ' : m === 'whatsapp' ? 'π±' : 'π';
return (
{(['pending', 'confirmed', 'cancelled', 'all'] as const).map(s => (
setStatusFilter(s)}>
{s.charAt(0).toUpperCase() + s.slice(1)}
))}
{loading ?
: reservations.length === 0 ?
: (
{reservations.map((r: any) => (
{r.event_name}
{r.guests} guests β’ {formatDate(r.event_date)} β’ {formatTime(r.event_time)}
{r.status}
Contact: {r.contact_name}
Method: {contactMethodIcon(r.contact_method)} {r.contact_method}
{r.contact_email &&
Email: {r.contact_email}
}
{r.contact_phone &&
Phone: {r.contact_phone}
}
{r.notes &&
"{r.notes}"
}
{r.status === 'pending' && (
<>
updateStatus(r.id, 'confirmed')}>β Confirm
updateStatus(r.id, 'cancelled')}>β Cancel
>
)}
{r.status === 'confirmed' && updateStatus(r.id, 'cancelled')}>β Cancel}
{r.status === 'cancelled' && updateStatus(r.id, 'confirmed')}>β Reactivate}
setConfirmDel(r.id)}>π Delete
))}
)}
{confirmDel !== null &&
setConfirmDel(null)} onConfirm={() => del(confirmDel)} />}
);
}
/* βββ Room Reservations Section βββ */
function RoomReservationsSection({ onToast }: { onToast: (msg: string, type: 'success' | 'error') => void }) {
const [reservations, setReservations] = useState([]);
const [statusFilter, setStatusFilter] = useState<'pending' | 'confirmed' | 'cancelled' | 'all'>('pending');
const [loading, setLoading] = useState(true);
const [confirmDel, setConfirmDel] = useState(null);
useEffect(() => { loadReservations(); }, [statusFilter]);
const loadReservations = async () => {
setLoading(true);
try {
const url = statusFilter === 'all' ? '/api/admin/room-reservations?status=all' : `/api/admin/room-reservations?status=${statusFilter}`;
const res = await fetch(url);
const data = await res.json();
setReservations(data.reservations || []);
} catch { onToast('Error loading reservations', 'error'); }
setLoading(false);
};
const updateStatus = async (id: number, status: 'confirmed' | 'cancelled') => {
try {
const res = await fetch('/api/admin/room-reservations', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, status }),
});
if (res.ok) {
setReservations(r => r.map(x => x.id === id ? { ...x, status } : x));
onToast(`Reservation ${status}`, 'success');
}
} catch { onToast('Error updating reservation', 'error'); }
};
const del = async (id: number) => {
try {
await fetch(`/api/admin/room-reservations?id=${id}`, { method: 'DELETE' });
setReservations(r => r.filter(x => x.id !== id));
onToast('Reservation deleted', 'success');
} catch { onToast('Error deleting reservation', 'error'); }
setConfirmDel(null);
};
const formatDate = (d: string) => new Date(d).toLocaleDateString();
const formatPrice = (p: string) => `$${parseFloat(p).toFixed(2)}`;
return (
{(['pending', 'confirmed', 'cancelled', 'all'] as const).map(s => (
setStatusFilter(s)}>
{s.charAt(0).toUpperCase() + s.slice(1)}
))}
{loading ?
: reservations.length === 0 ?
: (
{reservations.map((r) => (
{r.room_name || `Room #${r.room_id}`}
{formatDate(r.check_in)} β {formatDate(r.check_out)} β’ {formatPrice(r.total_price)}
{r.status}
Guest: {r.guest_name || r.username || r.first_name || 'N/A'}
{r.guest_contact &&
Contact: {r.guest_contact}
}
{r.guest_contact_method &&
Method: {r.guest_contact_method}
}
{r.status === 'pending' && (
<>
updateStatus(r.id, 'confirmed')}>β Confirm
updateStatus(r.id, 'cancelled')}>β Cancel
>
)}
{r.status === 'confirmed' && updateStatus(r.id, 'cancelled')}>β Cancel}
{r.status === 'cancelled' && updateStatus(r.id, 'confirmed')}>β Reactivate}
setConfirmDel(r.id)}>π Delete
))}
)}
{confirmDel !== null &&
setConfirmDel(null)} onConfirm={() => del(confirmDel)} />}
);
}
/* βββ Restaurant Reservations Section βββ */
function RestaurantReservationsSection({ onToast }: { onToast: (msg: string, type: 'success' | 'error') => void }) {
const [reservations, setReservations] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => { loadReservations(); }, []);
const loadReservations = async () => {
setLoading(true);
try {
const res = await fetch('/api/admin/restaurant-reservations');
const data = await res.json();
setReservations(data.reservations || []);
} catch { onToast('Error loading reservations', 'error'); }
setLoading(false);
};
const del = async (id: number) => {
try {
await fetch(`/api/admin/restaurant-reservations?id=${id}`, { method: 'DELETE' });
setReservations(r => r.filter(x => x.id !== id));
onToast('Reservation deleted', 'success');
} catch { onToast('Error deleting reservation', 'error'); }
};
return (
{loading ?
: reservations.length === 0 ?
: (
{reservations.map((r) => (
{r.table_name}
{formatDisplayDate(r.date)} at {r.time} β’ {r.guests} guests
{formatDisplayDateTime(r.created_at)}
Guest: {r.username || r.first_name || 'Guest'}
del(r.id)}>π Delete
))}
)}
);
}
/* βββ Orders Section βββ */
function OrdersSection({ onToast }: { onToast: (msg: string, type: 'success' | 'error') => void }) {
const [orders, setOrders] = useState([]);
const [statusFilter, setStatusFilter] = useState<'pending' | 'preparing' | 'ready' | 'delivered' | 'cancelled' | 'all'>('pending');
const [loading, setLoading] = useState(true);
useEffect(() => { loadOrders(); }, [statusFilter]);
const loadOrders = async () => {
setLoading(true);
try {
const url = statusFilter === 'all' ? '/api/orders?status=all' : `/api/orders?status=${statusFilter}`;
const res = await fetch(url);
const data = await res.json();
setOrders(data.orders || []);
} catch { onToast('Error loading orders', 'error'); }
setLoading(false);
};
const updateStatus = async (id: number, status: string) => {
try {
const res = await fetch('/api/orders', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order_id: id, status }),
});
if (res.ok) {
setOrders(o => o.map(x => x.id === id ? { ...x, status } : x));
onToast(`Order ${status}`, 'success');
}
} catch { onToast('Error updating order', 'error'); }
};
const formatPrice = (p: string) => `$${parseFloat(p).toFixed(2)}`;
const statusColor = (s: string) => s === 'pending' ? '#f59e0b' : s === 'preparing' ? '#3b82f6' : s === 'ready' ? '#10b981' : s === 'delivered' ? '#6b7280' : '#ef4444';
return (
{(['pending', 'preparing', 'ready', 'delivered', 'cancelled', 'all'] as const).map(s => (
setStatusFilter(s)}>
{s.charAt(0).toUpperCase() + s.slice(1)}
))}
{loading ?
: orders.length === 0 ?
: (
{orders.map((o) => (
{o.order_type === 'room_delivery' ? `πͺ Room ${o.room_name || o.room_id}` : o.order_type === 'dine_in' ? 'π½οΈ Dine-in' : 'π¦ Pickup'}
{formatPrice(o.total)} β’ {formatDisplayDateTime(o.created_at)}
{o.username &&
π€ {o.first_name || o.username}
}
{o.status}
{o.items?.map((item, i) => (
{item.quantity}x {item.name}
{formatPrice(item.total_price)}
))}
{o.notes &&
"{o.notes}"
}
{o.status === 'pending' && (
<>
updateStatus(o.id, 'preparing')}>π³ Start Preparing
updateStatus(o.id, 'cancelled')}>β Cancel
>
)}
{o.status === 'preparing' && updateStatus(o.id, 'ready')}>β Ready}
{o.status === 'ready' && updateStatus(o.id, 'delivered')}>β Delivered}
))}
)}
);
}
/* Kitchen Section */
function KitchenSection({ onToast }: { onToast: (msg: string, type: 'success' | 'error') => void }) {
const [orders, setOrders] = useState([]);
const [loading, setLoading] = useState(true);
const [autoRefresh, setAutoRefresh] = useState(true);
const loadOrders = async () => {
try {
const res = await fetch('/api/orders?status=all');
const data = await res.json();
const active = (data.orders || []).filter((o: Order) => ['pending', 'preparing', 'ready'].includes(o.status));
setOrders(active);
} catch { onToast('Error loading orders', 'error'); }
setLoading(false);
};
useEffect(() => {
loadOrders();
if (autoRefresh) {
const interval = setInterval(loadOrders, 30000);
return () => clearInterval(interval);
}
}, [autoRefresh]);
const updateStatus = async (id: number, status: string) => {
try {
const res = await fetch('/api/orders', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order_id: id, status }),
});
if (res.ok) {
if (status === 'delivered' || status === 'cancelled') {
setOrders(o => o.filter(x => x.id !== id));
} else {
setOrders(o => o.map(x => x.id === id ? { ...x, status } : x));
}
onToast(`Order ${status}`, 'success');
}
} catch { onToast('Error updating order', 'error'); }
};
const formatPrice = (p: string) => `$${parseFloat(p).toFixed(2)}`;
const statusColor = (s: string) => s === 'pending' ? '#f59e0b' : s === 'preparing' ? '#3b82f6' : '#10b981';
const pending = orders.filter(o => o.status === 'pending');
const preparing = orders.filter(o => o.status === 'preparing');
const ready = orders.filter(o => o.status === 'ready');
const OrderCard = ({ o }: { o: Order }) => (
{o.order_type === 'room_delivery' ? `Room ${o.room_name || o.room_id}` : o.order_type === 'dine_in' ? 'Dine-in' : 'Pickup'}
{formatPrice(o.total)} - {formatDisplayDateTime(o.created_at)}
{o.username &&
User: {o.first_name || o.username}
}
{o.status.toUpperCase()}
{o.items?.map((item, i) => (
{item.quantity}x {item.name}
{formatPrice(item.total_price)}
))}
{o.notes &&
Note: "{o.notes}"
}
{o.status === 'pending' && (
<>
updateStatus(o.id, 'preparing')}>Start Preparing
updateStatus(o.id, 'cancelled')}>Cancel
>
)}
{o.status === 'preparing' && updateStatus(o.id, 'ready')}>Mark Ready}
{o.status === 'ready' && updateStatus(o.id, 'delivered')}>Delivered}
);
return (
Kitchen Dashboard
{loading ?
: orders.length === 0 ? (
) : (
{pending.length > 0 && (
Pending ({pending.length})
{pending.map(o => )}
)}
{preparing.length > 0 && (
Preparing ({preparing.length})
{preparing.map(o => )}
)}
{ready.length > 0 && (
Ready for Delivery ({ready.length})
{ready.map(o => )}
)}
)}
);
}
/* Room Assignments Section */
type RoomAssignment = { id: number; room_id: number; room_name: string; user_id: number; username: string; first_name: string | null; last_name: string | null; email: string; check_in: string; check_out: string; notes: string | null };
function RoomAssignmentsSection({ onToast }: { onToast: (msg: string, type: 'success' | 'error') => void }) {
const [assignments, setAssignments] = useState([]);
const [rooms, setRooms] = useState([]);
const [users, setUsers] = useState<{ id: number; username: string; first_name: string | null; last_name: string | null }[]>([]);
const [loading, setLoading] = useState(true);
const [showForm, setShowForm] = useState(false);
const [form, setForm] = useState({ room_id: '', user_id: '', check_in: '', check_out: '', notes: '' });
useEffect(() => {
Promise.all([
fetch('/api/room-assignments').then(r => r.json()),
fetch('/api/rooms').then(r => r.json()),
fetch('/api/admin/users').then(r => r.json()),
]).then(([assignData, roomData, userData]) => {
setAssignments(assignData.assignments || []);
setRooms(roomData.rooms || []);
setUsers(userData.users || []);
}).catch(() => onToast('Error loading data', 'error'))
.finally(() => setLoading(false));
}, []);
const today = new Date().toISOString().split('T')[0];
const active = assignments.filter(a => a.check_in <= today && a.check_out >= today);
const upcoming = assignments.filter(a => a.check_in > today);
const saveAssignment = async () => {
if (!form.room_id || !form.user_id || !form.check_in || !form.check_out) {
onToast('All fields required', 'error');
return;
}
try {
const res = await fetch('/api/room-assignments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form),
});
if (res.ok) {
const data = await res.json();
setAssignments(a => [...a, data.assignment]);
setForm({ room_id: '', user_id: '', check_in: '', check_out: '', notes: '' });
setShowForm(false);
onToast('Assignment created', 'success');
} else {
const err = await res.json();
onToast(err.error || 'Failed to create assignment', 'error');
}
} catch { onToast('Error saving assignment', 'error'); }
};
const deleteAssignment = async (id: number) => {
if (!confirm('Delete this assignment?')) return;
try {
await fetch(`/api/room-assignments?id=${id}`, { method: 'DELETE' });
setAssignments(a => a.filter(x => x.id !== id));
onToast('Assignment deleted', 'success');
} catch { onToast('Error deleting assignment', 'error'); }
};
return (
setShowForm(true)}>+ Assign Room} />
{showForm && (
New Assignment
setForm(f => ({ ...f, room_id: v }))} options={rooms.map(r => ({ value: r.id.toString(), label: r.name }))} />
setForm(f => ({ ...f, user_id: v }))} options={users.map(u => ({ value: u.id.toString(), label: `${u.username} (${u.first_name || ''} ${u.last_name || ''})`.trim() }))} />
setForm(f => ({ ...f, check_in: v }))} />
setForm(f => ({ ...f, check_out: v }))} />
setForm(f => ({ ...f, notes: v }))} rows={2} />
Save Assignment
{ setShowForm(false); setForm({ room_id: '', user_id: '', check_in: '', check_out: '', notes: '' }); }}>Cancel
)}
{loading ? : (
Currently Active ({active.length})
{active.length === 0 ? (
No active assignments
) : (
{active.map(a => (
{a.room_name}
User: {a.first_name || a.username} {a.last_name || ''} ({a.email})
Dates: {a.check_in} to {a.check_out}
{a.notes &&
Note: {a.notes}
}
deleteAssignment(a.id)}>Remove
))}
)}
{upcoming.length > 0 && (
Upcoming ({upcoming.length})
{upcoming.map(a => (
{a.room_name}
User: {a.first_name || a.username} {a.last_name || ''}
Dates: {a.check_in} to {a.check_out}
deleteAssignment(a.id)}>Remove
))}
)}
)}
);
}
function RoomsSection({ rooms, setRooms, images, onToast }: any) {
const [editing, setEditing] = useState(null);
const [confirmDel, setConfirmDel] = useState(null);
const save = async (room: Room) => {
try {
const url = room.id ? `/api/rooms/${room.id}` : '/api/rooms';
const res = await fetch(url, {
method: room.id ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(room),
});
if (res.ok) {
const data = await res.json();
setEditing(null);
setRooms((r: any) => room.id ? r.map((x: Room) => x.id === room.id ? data.data : x) : [...r, data.data]);
onToast('Room saved!', 'success');
}
} catch { onToast('Error', 'error'); }
};
const del = async (id: number) => {
await fetch(`/api/rooms/${id}`, { method: 'DELETE' });
setRooms((r: any) => r.filter((x: Room) => x.id !== id));
onToast('Room deleted', 'success');
setConfirmDel(null);
};
return (
setEditing({ id: 0, name: '', description: '', price: '', photos: [], featured_photo: null, active: true, sort_order: rooms.length })} size="sm">+ New Room} />
{rooms.length === 0 ? : (
{rooms.map((r: Room) => (
setEditing(r)}>Edit
setConfirmDel(r.id)}>Del
>
} />
))}
)}
{editing && setEditing(null)} />}
{confirmDel !== null && setConfirmDel(null)} onConfirm={() => del(confirmDel)} />}
);
}
/* βββ Room Status Section βββ */
type StaffMember = { id: number; first_name: string | null; last_name: string | null; username: string };
type RoomStatus = {
id: number;
name: string;
price: string;
clean_status: 'clean' | 'dirty' | 'maintenance';
notes: string | null;
active: boolean;
last_cleaned: string | null;
assigned_staff_id: number | null;
assigned_staff: { id: number; first_name: string | null; last_name: string | null } | null;
priority: 'urgent' | 'normal' | 'low';
issues_count: number;
is_vip: boolean;
checkout_time: string | null;
checkout_date: string | null;
occupancy_status: 'available' | 'occupied';
current_guest: string | null;
reservation_id: number | null;
payment_status: 'pending' | 'paid' | 'partial' | null;
upcoming_reservations: Array<{
id: number;
guest_name: string | null;
check_in: string;
check_out: string;
status: string;
payment_status: string;
first_name: string | null;
last_name: string | null;
}>;
};
function RoomStatusSection({ onToast }: { onToast: (msg: string, type: 'success' | 'error') => void }) {
const [rooms, setRooms] = useState([]);
const [staff, setStaff] = useState([]);
const [loading, setLoading] = useState(true);
const [editingRoom, setEditingRoom] = useState(null);
const [editData, setEditData] = useState<{
clean_status: 'clean' | 'dirty' | 'maintenance';
notes: string;
priority: 'urgent' | 'normal' | 'low';
assigned_staff_id: number | null;
issues_count: number;
is_vip: boolean;
checkout_time: string;
checkout_date: string;
}>({
clean_status: 'clean',
notes: '',
priority: 'normal',
assigned_staff_id: null,
issues_count: 0,
is_vip: false,
checkout_time: '',
checkout_date: '',
});
useEffect(() => { loadRooms(); }, []);
const loadRooms = async () => {
setLoading(true);
try {
const res = await fetch('/api/admin/room-status');
const data = await res.json();
setRooms(data.rooms || []);
setStaff(data.staff || []);
} catch { onToast('Error loading room status', 'error'); }
setLoading(false);
};
const updateRoomStatus = async () => {
if (editingRoom === null) return;
try {
const res = await fetch('/api/admin/room-status', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: editingRoom,
clean_status: editData.clean_status,
notes: editData.notes || null,
priority: editData.priority,
assigned_staff_id: editData.assigned_staff_id,
issues_count: editData.issues_count,
is_vip: editData.is_vip,
checkout_time: editData.checkout_time || null,
checkout_date: editData.checkout_date || null,
}),
});
if (res.ok) {
await loadRooms();
onToast('Room status updated', 'success');
}
} catch { onToast('Error updating status', 'error'); }
setEditingRoom(null);
};
const startEdit = (room: RoomStatus) => {
setEditingRoom(room.id);
setEditData({
clean_status: room.clean_status,
notes: room.notes || '',
priority: room.priority,
assigned_staff_id: room.assigned_staff_id,
issues_count: room.issues_count,
is_vip: room.is_vip,
checkout_time: room.checkout_time || '',
checkout_date: room.checkout_date || '',
});
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'clean': return 'β¨';
case 'dirty': return 'π§Ή';
case 'maintenance': return 'π§';
default: return 'β';
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'clean': return { bg: '#d1fae5', color: '#047857', border: '#10b981' };
case 'dirty': return { bg: '#fef3c7', color: '#b45309', border: '#f59e0b' };
case 'maintenance': return { bg: '#fee2e2', color: '#b91c1c', border: '#ef4444' };
default: return { bg: '#f3f4f6', color: '#6b7280', border: '#d1d5db' };
}
};
const getPriorityStyle = (priority: string) => {
switch (priority) {
case 'urgent': return { bg: '#fee2e2', color: '#b91c1c', icon: 'π΄' };
case 'normal': return { bg: '#dbeafe', color: '#1d4ed8', icon: 'π΅' };
case 'low': return { bg: '#f3f4f6', color: '#6b7280', icon: 'βͺ' };
default: return { bg: '#f3f4f6', color: '#6b7280', icon: 'βͺ' };
}
};
const getPaymentBadge = (status: string | null) => {
if (!status) return null;
const colors: Record = {
pending: { bg: '#fef3c7', color: '#b45309' },
partial: { bg: '#dbeafe', color: '#1d4ed8' },
paid: { bg: '#d1fae5', color: '#047857' },
};
const c = colors[status] || colors.pending;
return (
{status}
);
};
const formatDate = (d: string) => new Date(d).toLocaleDateString();
const formatDateTime = (d: string) => new Date(d).toLocaleString();
const formatTime = (t: string) => t ? t.substring(0, 5) : '';
return (
{loading ? (
) : rooms.length === 0 ? (
) : (
{rooms.map(room => {
const statusColors = getStatusColor(room.clean_status);
const priorityStyle = getPriorityStyle(room.priority);
const isEditing = editingRoom === room.id;
return (
{/* Header Row */}
{/* Room Name & Status */}
{getStatusIcon(room.clean_status)}
{room.name}
{room.is_vip && (
β VIP
)}
{room.clean_status}
{room.occupancy_status === 'occupied' ? 'ποΈ Occupied' : 'πͺ Available'}
{priorityStyle.icon} {room.priority}
{room.issues_count > 0 && (
β οΈ {room.issues_count} issue{room.issues_count > 1 ? 's' : ''}
)}
{/* Price */}
${parseFloat(room.price).toFixed(2)}/night
{/* Edit Button */}
isEditing ? setEditingRoom(null) : startEdit(room)}>
{isEditing ? 'Cancel' : 'Edit'}
{/* Details Row */}
{/* Quick Info Grid */}
{/* Current Guest */}
{room.current_guest && (
Guest:
{room.current_guest}
{getPaymentBadge(room.payment_status)}
)}
{/* Checkout */}
{room.checkout_date && (
Checkout:
{formatDate(room.checkout_date)}
{room.checkout_time && at {formatTime(room.checkout_time)}}
)}
{/* Last Cleaned */}
{room.last_cleaned && (
Last cleaned:
{formatDateTime(room.last_cleaned)}
)}
{/* Assigned Staff */}
{room.assigned_staff && (
Staff:
{room.assigned_staff.first_name || ''} {room.assigned_staff.last_name || ''}
)}
{/* Upcoming Reservations */}
{room.upcoming_reservations && room.upcoming_reservations.length > 0 && (
Upcoming:
{room.upcoming_reservations.map((res, i) => (
π
{formatDate(res.check_in)} β {formatDate(res.check_out)}
{res.guest_name || res.first_name || 'Guest'}
{getPaymentBadge(res.payment_status)}
))}
)}
{/* Notes */}
{room.notes && !isEditing && (
π {room.notes}
)}
{/* Edit Form */}
{isEditing && (
setEditData(prev => ({ ...prev, clean_status: v }))}
options={[
{ value: 'clean', label: 'β¨ Clean' },
{ value: 'dirty', label: 'π§Ή Dirty' },
{ value: 'maintenance', label: 'π§ Maintenance' },
]}
/>
setEditData(prev => ({ ...prev, priority: v }))}
options={[
{ value: 'urgent', label: 'π΄ Urgent' },
{ value: 'normal', label: 'π΅ Normal' },
{ value: 'low', label: 'βͺ Low' },
]}
/>
setEditData(prev => ({ ...prev, assigned_staff_id: v ? parseInt(v) : null }))}
options={[
{ value: '', label: 'β Unassigned β' },
...staff.map(s => ({ value: s.id.toString(), label: `${s.first_name || ''} ${s.last_name || ''}`.trim() || s.username }))
]}
/>
setEditData(prev => ({ ...prev, issues_count: parseInt(v) || 0 }))}
placeholder="0"
/>
setEditData(prev => ({ ...prev, checkout_date: v }))}
/>
setEditData(prev => ({ ...prev, checkout_time: v }))}
/>
setEditData(prev => ({ ...prev, notes: v }))}
rows={2}
placeholder="e.g., Need extra towels, AC broken..."
/>
Save
setEditingRoom(null)} size="sm">Cancel
)}
);
})}
)}
);
}
function RoomEditor({ room, setRoom, images, onSave, onCancel }: any) {
const [showPicker, setShowPicker] = useState(false);
const [roomImages, setRoomImages] = useState<{id: number; data: string; thumbnail: string}[]>([]);
const [allAmenities, setAllAmenities] = useState([]);
const [selectedAmenities, setSelectedAmenities] = useState(room.amenity_ids || []);
const r = { ...room, photos: roomImages.map(img => img.thumbnail || img.data), amenity_ids: selectedAmenities };
useEffect(() => {
fetch('/api/amenities')
.then(res => res.json())
.then(d => setAllAmenities(d.data || []))
.catch(() => {});
// Load existing images for this room from images table
if (room.id) {
fetch(`/api/admin/images?room_id=${room.id}`)
.then(res => res.json())
.then(d => setRoomImages(d.data || []))
.catch(() => {});
}
}, [room.id]);
const toggleAmenity = (id: number) => {
setSelectedAmenities(prev =>
prev.includes(id) ? prev.filter(a => a !== id) : [...prev, id]
);
};
const addPhotos = async (files: FileList) => {
const newImages: {id: number; data: string; thumbnail: string}[] = [];
for (const f of Array.from(files)) {
const b64 = await readFileBase64(f);
try {
const res = await fetch('/api/admin/images', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: f.name, data: b64, category: 'rooms', room_id: r.id || null, location_target: `room-${r.id || 'new'}` }),
});
if (res.ok) {
const d = await res.json();
newImages.push({ id: d.data.id, data: d.data.data, thumbnail: d.data.thumbnail || d.data.data });
}
} catch { /* ignore */ }
}
setRoomImages(prev => [...prev, ...newImages]);
};
const removeImage = async (imageId: number) => {
try {
await fetch(`/api/admin/images?id=${imageId}`, { method: 'DELETE' });
setRoomImages(prev => prev.filter(img => img.id !== imageId));
} catch { /* ignore */ }
};
return (
{r.id ? 'Edit Room' : 'New Room'}
setRoom({ ...r, name: v })} />
setRoom({ ...r, description: v })} />
setRoom({ ...r, price: v })} />
document.getElementById(`room-photos-${r.id || 'new'}`)?.click()} size="sm" variant="secondary">+ Add Photos
{ addPhotos(e.target.files!); e.target.value = ''; }} />
{roomImages.map((img, i) => (
))}
setRoom({ ...r, active: v })} />
{allAmenities.map(a => (
))}
Cancel
onSave(r)}>Save
);
}
/* βββ Amenities Section βββ */
function AmenitiesSection({ onToast }: { onToast: (msg: string, type: string) => void }) {
const [amenities, setAmenities] = useState([]);
const [editing, setEditing] = useState(null);
const [confirmDel, setConfirmDel] = useState(null);
useEffect(() => {
fetch('/api/amenities')
.then(r => r.json())
.then(d => setAmenities(d.data || []))
.catch(() => onToast('Failed to load amenities', 'error'));
}, []);
const save = async (amenity: Amenity) => {
try {
const url = amenity.id ? `/api/amenities?id=${amenity.id}` : '/api/amenities';
const res = await fetch(url, {
method: amenity.id ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(amenity),
});
if (res.ok) {
const data = await res.json();
if (amenity.id) {
setAmenities(prev => prev.map(a => a.id === amenity.id ? data.data : a));
} else {
setAmenities(prev => [...prev, data.data]);
}
setEditing(null);
onToast('Amenity saved!', 'success');
} else {
const err = await res.json();
onToast(err.error || 'Failed to save', 'error');
}
} catch { onToast('Error', 'error'); }
};
const del = async (id: number) => {
await fetch(`/api/amenities?id=${id}`, { method: 'DELETE' });
setAmenities(prev => prev.filter(a => a.id !== id));
onToast('Amenity deleted', 'success');
setConfirmDel(null);
};
return (
setEditing({ id: 0, name: '', icon_svg: '', sort_order: amenities.length })} size="sm">+ New Amenity} />
{amenities.length === 0 ? : (
{amenities.sort((a, b) => a.sort_order - b.sort_order).map((a: Amenity) => (
{a.name}
setEditing(a)}>Edit
setConfirmDel(a.id)}>Del
))}
)}
{editing && setEditing(null)} />}
{confirmDel !== null && setConfirmDel(null)} onConfirm={() => del(confirmDel)} />}
);
}
function AmenityEditor({ amenity, setAmenity, onSave, onCancel }: any) {
const defaultIcons = [
{ name: 'Wi-Fi', svg: '' },
{ name: 'TV', svg: '' },
{ name: 'Shower', svg: '' },
{ name: 'Bathtub', svg: '' },
{ name: 'Fireplace', svg: '' },
{ name: 'Minibar', svg: '' },
{ name: 'Coffee', svg: '' },
{ name: 'Safe', svg: '' },
{ name: 'Heating', svg: '' },
{ name: 'Balcony', svg: '' },
{ name: 'King Bed', svg: '' },
{ name: 'Mountain View', svg: '' },
{ name: 'Parking', svg: '' },
{ name: 'Restaurant', svg: '' },
];
return (
{amenity.id ? 'Edit Amenity' : 'New Amenity'}
setAmenity({ ...amenity, name: v })} placeholder="e.g., Wi-Fi, TV, Shower" />
{defaultIcons.map(icon => (
))}
setAmenity({ ...amenity, sort_order: parseInt(v) || 0 })} />
Cancel
onSave(amenity)}>Save
);
}
/* βββ Trips Section βββ */
type Trip = { id: number; user_id: number; username: string; room_id: number | null; room_name: string | null; check_in: string; check_out: string; notes: string | null; status: string };
function TripsSection({ onToast }: { onToast: (msg: string, type: string) => void }) {
const [trips, setTrips] = useState([]);
const [users, setUsers] = useState<{id: number; username: string}[]>([]);
const [rooms, setRooms] = useState<{id: number; name: string}[]>([]);
const [editing, setEditing] = useState(null);
const [creating, setCreating] = useState(false);
useEffect(() => {
const load = async () => {
const [tripsRes, usersRes, roomsRes] = await Promise.all([
fetch('/api/admin/trips'),
fetch('/api/admin/users'),
fetch('/api/rooms'),
]);
if (tripsRes.ok) { const d = await tripsRes.json(); setTrips(d.trips || []); }
if (usersRes.ok) { const d = await usersRes.json(); setUsers(d.users || []); }
if (roomsRes.ok) { const d = await roomsRes.json(); setRooms(Array.isArray(d) ? d : (d.data || [])); }
};
load();
}, []);
const save = async (trip: Trip) => {
try {
const method = trip.id ? 'PUT' : 'POST';
const res = await fetch('/api/admin/trips', {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(trip),
});
if (res.ok) {
const d = await res.json();
if (trip.id) {
setTrips(prev => prev.map(t => t.id === trip.id ? d.trip : t));
} else {
setTrips(prev => [...prev, d.trip]);
}
setEditing(null);
setCreating(false);
onToast('Trip saved', 'success');
} else {
const err = await res.json();
onToast(err.error || 'Failed to save trip', 'error');
}
} catch { onToast('Error saving trip', 'error'); }
};
const del = async (id: number) => {
if (!confirm('Delete this trip?')) return;
try {
await fetch('/api/admin/trips', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) });
setTrips(prev => prev.filter(t => t.id !== id));
onToast('Trip deleted', 'success');
} catch { onToast('Error deleting trip', 'error'); }
};
const emptyTrip: Trip = { id: 0, user_id: 0, username: '', room_id: null, room_name: null, check_in: '', check_out: '', notes: '', status: 'confirmed' };
return (
{ setCreating(true); setEditing(emptyTrip); }} />
{creating && editing && !editing.id && (
New Trip
setEditing({ ...editing, user_id: Number(v) })} options={[{ value: 0, label: 'Select user' }, ...users.map(u => ({ value: u.id, label: u.username }))]} />
setEditing({ ...editing, room_id: v ? Number(v) : null })} options={[{ value: '', label: 'No room' }, ...rooms.map(r => ({ value: r.id, label: r.name }))]} />
setEditing({ ...editing, check_in: v })} />
setEditing({ ...editing, check_out: v })} />
setEditing({ ...editing, status: v })} options={['pending', 'confirmed', 'checked_in', 'checked_out', 'cancelled'].map(s => ({ value: s, label: s.replace('_', ' ') }))} />
setEditing({ ...editing, notes: v })} rows={2} />
save(editing)}>Save Trip
{ setCreating(false); setEditing(null); }}>Cancel
)}
{trips.map(t => (
{t.username || `User ${t.user_id}`}
{t.room_name || 'No room assigned'}
In: {formatDisplayDate(t.check_in)} Β· Out: {formatDisplayDate(t.check_out)}
{t.status.replace('_', ' ')}
{ setCreating(false); setEditing(t); }}>Edit
del(t.id)}>Delete
))}
{!creating && editing && editing.id && (
setEditing(null)}>
e.stopPropagation()}>
Edit Trip
setEditing({ ...editing, user_id: Number(v) })} options={[{ value: 0, label: 'Select user' }, ...users.map(u => ({ value: u.id, label: u.username }))]} />
setEditing({ ...editing, room_id: v ? Number(v) : null })} options={[{ value: '', label: 'No room' }, ...rooms.map(r => ({ value: r.id, label: r.name }))]} />
setEditing({ ...editing, check_in: v })} />
setEditing({ ...editing, check_out: v })} />
setEditing({ ...editing, status: v })} options={['pending', 'confirmed', 'checked_in', 'checked_out', 'cancelled'].map(s => ({ value: s, label: s.replace('_', ' ') }))} />
setEditing({ ...editing, notes: v })} rows={2} />
save(editing)}>Save
setEditing(null)}>Cancel
)}
);
}
/* βββ Messages Section βββ */
type MsgThread = { user_id: number; username: string; message: string; created_at: string };
type Msg = { id: number; sender_role: string; message: string; created_at: string };
function MessagesSection({ onToast }: { onToast: (msg: string, type: string) => void }) {
const [threads, setThreads] = useState([]);
const [selectedUser, setSelectedUser] = useState(null);
const [messages, setMessages] = useState([]);
const [reply, setReply] = useState('');
const messagesEndRef = useRef(null);
useEffect(() => {
const load = async () => {
const res = await fetch('/api/admin/messages');
if (res.ok) { const d = await res.json(); setThreads(d.threads || []); }
};
load();
}, []);
useEffect(() => {
if (selectedUser) {
const load = async () => {
const res = await fetch(`/api/admin/messages?user_id=${selectedUser}`);
if (res.ok) { const d = await res.json(); setMessages(d.messages || []); }
};
load();
}
}, [selectedUser]);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const sendReply = async () => {
if (!reply.trim() || !selectedUser) return;
try {
const res = await fetch('/api/admin/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: selectedUser, message: reply.trim() }),
});
if (res.ok) {
const d = await res.json();
setMessages(prev => [...prev, d.message]);
setReply('');
onToast('Reply sent', 'success');
}
} catch { onToast('Error sending reply', 'error'); }
};
return (
Conversations
{threads.length === 0 ? (
No conversations yet
) : (
threads.map(t => (
setSelectedUser(t.user_id)} style={{ padding: '12px 16px', cursor: 'pointer', background: selectedUser === t.user_id ? C.goldLight : 'transparent', borderBottom: `1px solid ${C.border}` }}
onMouseEnter={e => { if (selectedUser !== t.user_id) (e.currentTarget.style as any).background = '#f5f5f5'; }}
onMouseLeave={e => { if (selectedUser !== t.user_id) (e.currentTarget.style as any).background = 'transparent'; }}>
{t.username}
{t.message}
))
)}
{selectedUser ? (
<>
{threads.find(t => t.user_id === selectedUser)?.username || `User ${selectedUser}`}
{messages.map(m => (
{m.sender_role === 'admin' ? 'Staff' : 'Guest'}
{m.message}
{formatDisplayDateTime(m.created_at)}
))}
setReply(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') sendReply(); }} placeholder="Type a reply..." style={{ flex: 1, padding: '10px 14px', borderRadius: 8, border: `1px solid ${C.border}`, fontSize: '14px', outline: 'none' }} />
Send
>
) : (
Select a conversation to view messages
)}
);
}
/* βββ Menu Section βββ */
function MenuSection({ items, setItems, images, onToast }: any) {
const [editing, setEditing] = useState