From 2ed9756cc1e62fd8632d2891d2fab87ccc7f299a Mon Sep 17 00:00:00 2001 From: nasmans <114198613+nasmans@users.noreply.github.com> Date: Wed, 29 Oct 2025 19:59:46 +0100 Subject: [PATCH] Add dashboard page with booking management --- next-env.d.ts | 6 + package.json | 21 + src/pages/_app.tsx | 8 + src/pages/dashboard/index.tsx | 836 ++++++++++++++++++++++++++++++++++ tsconfig.json | 19 + 5 files changed, 890 insertions(+) create mode 100644 next-env.d.ts create mode 100644 package.json create mode 100644 src/pages/_app.tsx create mode 100644 src/pages/dashboard/index.tsx create mode 100644 tsconfig.json diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..9bc3dd4 --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/package.json b/package.json new file mode 100644 index 0000000..2bdeba0 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "web", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "framer-motion": "10.16.4", + "next": "13.4.7", + "react": "18.2.0", + "react-dom": "18.2.0" + }, + "devDependencies": { + "@types/node": "20.4.8", + "@types/react": "18.2.21", + "typescript": "5.2.2" + } +} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx new file mode 100644 index 0000000..cbd0437 --- /dev/null +++ b/src/pages/_app.tsx @@ -0,0 +1,8 @@ +import type { AppProps } from 'next/app'; +import React from 'react'; + +const App = ({ Component, pageProps }: AppProps) => { + return ; +}; + +export default App; diff --git a/src/pages/dashboard/index.tsx b/src/pages/dashboard/index.tsx new file mode 100644 index 0000000..e1da3ee --- /dev/null +++ b/src/pages/dashboard/index.tsx @@ -0,0 +1,836 @@ +import { AnimatePresence, motion } from 'framer-motion'; +import type { NextPage } from 'next'; +import Head from 'next/head'; +import React, { useEffect, useState } from 'react'; + +type BookingStatus = 'confirmed' | 'pending' | 'cancelled'; + +type Booking = { + id: string; + guestName: string; + reference: string; + date: string; + status: BookingStatus; +}; + +type ContactChannel = 'phone' | 'email'; + +type ContactFieldState = { + value: string; + otp: string; + verifying: boolean; + verified: boolean; + sending: boolean; + verifyingOtp: boolean; + message: string | null; +}; + +type TabKey = 'welcome' | 'bookings' | 'blessed-days' | 'settings'; + +const tabOrder: Array<{ key: TabKey; label: string }> = [ + { key: 'welcome', label: 'مرحباً' }, + { key: 'bookings', label: 'حجوزاتي' }, + { key: 'blessed-days', label: 'الأيام المباركات' }, + { key: 'settings', label: 'الإعدادات' }, +]; + +const DashboardPage: NextPage = () => { + const [activeTab, setActiveTab] = useState('welcome'); + const [bookings, setBookings] = useState([]); + const [bookingsLoading, setBookingsLoading] = useState(false); + const [bookingsError, setBookingsError] = useState(null); + const [actionState, setActionState] = useState<{ + bookingId: string; + action: 'update' | 'cancel'; + } | null>(null); + const [editingBookingId, setEditingBookingId] = useState(null); + const [editingDraft, setEditingDraft] = useState>({}); + + const [phoneState, setPhoneState] = useState({ + value: '', + otp: '', + verifying: false, + verified: false, + sending: false, + verifyingOtp: false, + message: null, + }); + + const [emailState, setEmailState] = useState({ + value: '', + otp: '', + verifying: false, + verified: false, + sending: false, + verifyingOtp: false, + message: null, + }); + + useEffect(() => { + const controller = new AbortController(); + + const bootstrap = async () => { + setBookingsLoading(true); + setBookingsError(null); + try { + const response = await fetch('/api/bookings', { + signal: controller.signal, + }); + if (!response.ok) { + throw new Error('تعذّر تحميل الحجوزات حالياً.'); + } + const payload: { bookings: Booking[] } = await response.json(); + setBookings(payload.bookings); + } catch (error) { + if ((error as Error).name === 'AbortError') { + return; + } + setBookingsError((error as Error).message); + setBookings([ + { + id: 'bk-001', + guestName: 'أحمد علي', + reference: 'REF-01', + date: new Date().toISOString().slice(0, 10), + status: 'confirmed', + }, + { + id: 'bk-002', + guestName: 'سارة يوسف', + reference: 'REF-02', + date: new Date(Date.now() + 86400000).toISOString().slice(0, 10), + status: 'pending', + }, + ]); + } finally { + setBookingsLoading(false); + } + }; + + bootstrap(); + + return () => controller.abort(); + }, []); + + const sendAdminEvent = async (bookingId: string, action: 'update' | 'cancel') => { + try { + await fetch('/api/admin/booking-events', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + bookingId, + action, + timestamp: new Date().toISOString(), + }), + }); + } catch (error) { + console.error('Failed to send admin event', error); + } + }; + + const applyBookingUpdate = (updatedBooking: Booking) => { + setBookings((current) => + current.map((booking) => (booking.id === updatedBooking.id ? updatedBooking : booking)), + ); + }; + + const handleUpdateBooking = async (bookingId: string, payload: Partial) => { + setActionState({ bookingId, action: 'update' }); + setBookingsError(null); + try { + const response = await fetch(`/api/bookings/${bookingId}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + throw new Error('تعذّر تحديث الحجز.'); + } + + const data = (await response.json()) as { booking?: Booking }; + if (data.booking) { + applyBookingUpdate(data.booking); + } else { + setBookings((current) => + current.map((booking) => + booking.id === bookingId ? { ...booking, ...payload } : booking, + ), + ); + } + await sendAdminEvent(bookingId, 'update'); + } catch (error) { + setBookingsError((error as Error).message); + } finally { + setActionState(null); + setEditingBookingId(null); + setEditingDraft({}); + } + }; + + const handleCancelBooking = async (bookingId: string) => { + if (typeof window !== 'undefined' && !window.confirm('هل تريد إلغاء هذا الحجز؟')) { + return; + } + + setActionState({ bookingId, action: 'cancel' }); + setBookingsError(null); + try { + const response = await fetch(`/api/bookings/${bookingId}`, { + method: 'DELETE', + }); + + if (!response.ok) { + throw new Error('تعذّر إلغاء الحجز.'); + } + + setBookings((current) => current.filter((booking) => booking.id !== bookingId)); + await sendAdminEvent(bookingId, 'cancel'); + } catch (error) { + setBookingsError((error as Error).message); + } finally { + setActionState(null); + } + }; + + const initiateContactVerification = async ( + type: ContactChannel, + setter: React.Dispatch>, + state: ContactFieldState, + ) => { + if (!state.value) { + setter((current) => ({ ...current, message: 'الرجاء إدخال قيمة صالحة.' })); + return; + } + + setter((current) => ({ ...current, sending: true, message: null })); + try { + const response = await fetch('/api/settings/send-otp', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ channel: type, value: state.value }), + }); + + if (!response.ok) { + throw new Error('تعذّر إرسال رمز التحقق.'); + } + + setter((current) => ({ + ...current, + sending: false, + verifying: true, + message: 'تم إرسال رمز التحقق. الرجاء إدخاله أدناه.', + })); + } catch (error) { + setter((current) => ({ + ...current, + sending: false, + message: (error as Error).message, + })); + } + }; + + const verifyContactChannel = async ( + type: ContactChannel, + setter: React.Dispatch>, + state: ContactFieldState, + ) => { + if (!state.otp) { + setter((current) => ({ ...current, message: 'الرجاء إدخال رمز التحقق.' })); + return; + } + + setter((current) => ({ ...current, verifyingOtp: true, message: null })); + try { + const response = await fetch('/api/settings/verify-otp', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ channel: type, value: state.value, otp: state.otp }), + }); + + if (!response.ok) { + throw new Error('رمز التحقق غير صحيح.'); + } + + setter((current) => ({ + ...current, + verifyingOtp: false, + verifying: false, + verified: true, + message: 'تم التحقق بنجاح!', + })); + } catch (error) { + setter((current) => ({ + ...current, + verifyingOtp: false, + message: (error as Error).message, + })); + } + }; + + const settingsSection = ( +
+

ضبط بيانات التواصل

+

قم بتحديث بيانات الاتصال لتلقي التنبيهات والإشعارات الحساسة.

+ +
+ +
+ + setPhoneState((current) => ({ ...current, value: event.target.value, message: null })) + } + /> +
+ +
+
+ {phoneState.verifying && ( +
+ + setPhoneState((current) => ({ ...current, otp: event.target.value, message: null })) + } + /> + +
+ )} + {phoneState.message &&

{phoneState.message}

} + {phoneState.verified && مؤكد} +
+ +
+ +
+ + setEmailState((current) => ({ ...current, value: event.target.value, message: null })) + } + /> +
+ +
+
+ {emailState.verifying && ( +
+ + setEmailState((current) => ({ ...current, otp: event.target.value, message: null })) + } + /> + +
+ )} + {emailState.message &&

{emailState.message}

} + {emailState.verified && مؤكد} +
+
+ ); + + const bookingsSection = ( +
+
+
+

حجوزاتي

+

إدارة حجوزاتك ومتابعة حالة التذاكر.

+
+ +
+ + {bookingsLoading &&

...جاري التحميل

} + {bookingsError &&

{bookingsError}

} + +
+ + + + + + + + + + + + {bookings.map((booking) => { + const isEditing = editingBookingId === booking.id; + return ( + + + + + + + + ); + })} + +
الضيفالمرجعالتاريخالحالةالتحكم
{booking.guestName}{booking.reference} + {isEditing ? ( + + setEditingDraft((current) => ({ ...current, date: event.target.value })) + } + /> + ) : ( + booking.date + )} + + {isEditing ? ( + + ) : ( + {translateStatus(booking.status)} + )} + +
+ {isEditing ? ( + <> + + + + ) : ( + <> + + + + + )} +
+
+
+
+ ); + + const welcomeSection = ( +
+

أهلاً بك في لوحة التحكم

+

+ استخدم شريط التنقل في الأعلى للانتقال بين الحجوزات، الأيام المباركات، والإعدادات. +

+
    +
  • تابع حالة الحجوزات الخاصة بك وتأكد من اكتمال بيانات التذاكر.
  • +
  • استكشف الأيام المباركات القادمة ورتب جدولك.
  • +
  • قم بتحديث بيانات التواصل لتصلك الإشعارات فوراً.
  • +
+
+ ); + + const blessedDaysSection = ( +
+

الأيام المباركات

+

تذكير بالأيام المفضلة للزيارة والعبادة. سيتم تخصيص المحتوى قريباً.

+
    +
  • ليلة الجمعة
  • +
  • الأيام العشر من ذي الحجة
  • +
  • العشر الأواخر من رمضان
  • +
+
+ ); + + const renderSection = () => { + switch (activeTab) { + case 'bookings': + return bookingsSection; + case 'blessed-days': + return blessedDaysSection; + case 'settings': + return settingsSection; + case 'welcome': + default: + return welcomeSection; + } + }; + + return ( + <> + + لوحة التحكم + +
+ + + + {renderSection()} + + +
+ + + + ); +}; + +const translateStatus = (status: BookingStatus) => { + switch (status) { + case 'confirmed': + return 'مؤكد'; + case 'pending': + return 'قيد الانتظار'; + case 'cancelled': + return 'ملغى'; + default: + return status; + } +}; + +const handleShowTicket = (bookingId: string) => { + if (typeof window !== 'undefined') { + window.open(`/api/bookings/${bookingId}/ticket`, '_blank', 'noopener,noreferrer'); + } +}; + +export default DashboardPage; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4fa631c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve" + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +}