From 4604ae91c5095c13d1296fe140b59c00582e329d Mon Sep 17 00:00:00 2001 From: Habeba Ehab <151371194+codestcode@users.noreply.github.com> Date: Sat, 11 Jul 2026 06:01:08 +0300 Subject: [PATCH 1/2] feat: add client-organization booking, enhance organization management with worker assignment and reviews and dashbored --- app/client/booking/[workerId]/ClientPage.tsx | 242 +++++++++++---- app/client/booking/confirm/page.tsx | 2 +- app/client/organization/[id]/ClientPage.tsx | 266 +++++++++++++--- app/organization/home/page.tsx | 291 +++++++++++++++--- components/payment/CodeEntryForm.tsx | 2 +- components/ui/orders/OrderCard.tsx | 139 ++++++++- lib/types.ts | 13 + .../20260712010000_org_services_gallery.sql | 146 ++++----- .../migrations/20260713010000_org_reviews.sql | 86 ++++++ ...20260713020000_make_worker_id_nullable.sql | 4 + .../20260713030000_finish_job_org_owner.sql | 71 +++++ .../20260713040000_fix_finish_job_md5.sql | 70 +++++ ...260713050000_fix_verify_code_org_owner.sql | 111 +++++++ 13 files changed, 1206 insertions(+), 237 deletions(-) create mode 100644 supabase/migrations/20260713010000_org_reviews.sql create mode 100644 supabase/migrations/20260713020000_make_worker_id_nullable.sql create mode 100644 supabase/migrations/20260713030000_finish_job_org_owner.sql create mode 100644 supabase/migrations/20260713040000_fix_finish_job_md5.sql create mode 100644 supabase/migrations/20260713050000_fix_verify_code_org_owner.sql diff --git a/app/client/booking/[workerId]/ClientPage.tsx b/app/client/booking/[workerId]/ClientPage.tsx index 0b141d5..d709f17 100644 --- a/app/client/booking/[workerId]/ClientPage.tsx +++ b/app/client/booking/[workerId]/ClientPage.tsx @@ -3,7 +3,7 @@ import { useState, useMemo, use, useEffect } from 'react' import { useRouter } from 'next/navigation' import Image from 'next/image' -import { ArrowLeft, Plus, Clock, CalendarDays } from 'lucide-react' +import { ArrowLeft, Plus, Clock, CalendarDays, Building2, Briefcase } from 'lucide-react' import { createClient } from '@/lib/supabase/client' import LocationPicker from '@/components/shared/LocationPicker' @@ -76,11 +76,22 @@ function generateTimeSlots(start: string, end: string): string[] { return slots } +interface OrgService { + id: string + name: string + description: string | null + price: number | null + icon: string | null +} + export default function BookingPage({ params }: { params: Promise<{ workerId: string }> }) { const router = useRouter() const { workerId } = use(params) const supabase = createClient() - + + const searchParams = typeof window !== 'undefined' ? new URLSearchParams(window.location.search) : null + const orgId = searchParams?.get('organization_id') || '' + const weekDates = useMemo(() => getDateRange(14), []) const [selectedDate, setSelectedDate] = useState(todayStr) const [selectedTime, setSelectedTime] = useState('') @@ -91,44 +102,83 @@ export default function BookingPage({ params }: { params: Promise<{ workerId: st const [images, setImages] = useState([]) const [workerProfile, setWorkerProfile] = useState(null) + const [orgProfile, setOrgProfile] = useState(null) + const [orgServices, setOrgServices] = useState([]) + const [selectedService, setSelectedService] = useState(null) const [schedule, setSchedule] = useState([]) const [bookedTimes, setBookedTimes] = useState([]) const [loading, setLoading] = useState(true) + const isOrg = !!orgId + useEffect(() => { let active = true async function loadData() { setLoading(true) - const { data: prof } = await supabase.from('profiles').select('*').eq('id', workerId).single() - if (!active) return - setWorkerProfile(prof) - - if (prof) { - const { data: sched } = await supabase.from('worker_schedule').select('*').eq('worker_id', workerId) - if (active && sched) setSchedule(sched) - + + if (isOrg) { + const { data: org } = await supabase + .from('organizations') + .select('*') + .eq('id', orgId) + .eq('is_active', true) + .single() + if (!active) return + setOrgProfile(org) + + const { data: services } = await supabase + .from('organization_services') + .select('*') + .eq('organization_id', orgId) + .order('sort_order', { ascending: true }) + if (active && services) { + setOrgServices(services) + if (services.length === 1) setSelectedService(services[0]) + } + const { data: books } = await supabase .from('bookings') .select('appointment_time') - .eq('worker_id', workerId) + .eq('organization_id', orgId) .eq('appointment_date', selectedDate) .not('status', 'eq', 'cancelled') - + if (active && books) { setBookedTimes(books.map(b => b.appointment_time.slice(0, 5))) } + } else { + const { data: prof } = await supabase.from('profiles').select('*').eq('id', workerId).single() + if (!active) return + setWorkerProfile(prof) + + if (prof) { + const { data: sched } = await supabase.from('worker_schedule').select('*').eq('worker_id', workerId) + if (active && sched) setSchedule(sched) + + const { data: books } = await supabase + .from('bookings') + .select('appointment_time') + .eq('worker_id', workerId) + .eq('appointment_date', selectedDate) + .not('status', 'eq', 'cancelled') + + if (active && books) { + setBookedTimes(books.map(b => b.appointment_time.slice(0, 5))) + } + } } setLoading(false) } loadData() - return () => { - active = false - } - }, [workerId, selectedDate, supabase]) + return () => { active = false } + }, [workerId, orgId, isOrg, selectedDate, supabase]) const currentDayKey = useMemo(() => getDayKey(selectedDate), [selectedDate]) - + const daySched = useMemo(() => { + if (isOrg) { + return defaultSchedule[currentDayKey] + } const dbSched = schedule.find(s => s.day_id === currentDayKey) if (dbSched) { return { @@ -138,7 +188,7 @@ export default function BookingPage({ params }: { params: Promise<{ workerId: st } } return defaultSchedule[currentDayKey] - }, [schedule, currentDayKey]) + }, [schedule, currentDayKey, isOrg]) const timeSlots = useMemo(() => { if (!daySched || !daySched.active) return [] @@ -160,6 +210,7 @@ export default function BookingPage({ params }: { params: Promise<{ workerId: st const scheduleSummary = useMemo(() => { const dayKeys = ['sat', 'sun', 'mon', 'tue', 'wed', 'thu', 'fri'] return dayKeys.map(key => { + if (isOrg) return { key, ...defaultSchedule[key] } const db = schedule.find(s => s.day_id === key) if (db) { return { @@ -171,7 +222,7 @@ export default function BookingPage({ params }: { params: Promise<{ workerId: st } return { key, ...defaultSchedule[key] } }) - }, [schedule]) + }, [schedule, isOrg]) const handleContinue = () => { if (!selectedTime) return @@ -179,7 +230,6 @@ export default function BookingPage({ params }: { params: Promise<{ workerId: st alert('يرجى إرفاق صورة توضيحية واحدة على الأقل') return } - if (!address && (!lat || !lng)) { alert('يرجى تحديد موقعك الحالي أو إدخال العنوان التفصيلي') return @@ -192,18 +242,16 @@ export default function BookingPage({ params }: { params: Promise<{ workerId: st return } - const params = new URLSearchParams(window.location.search) - const serviceName = params.get('serviceName') || '' - const servicePrice = params.get('servicePrice') || '' - const orgId = params.get('organization_id') || '' - + const serviceName = isOrg ? (selectedService?.name || '') : (new URLSearchParams(window.location.search).get('serviceName') || '') + const servicePrice = isOrg ? (selectedService?.price?.toString() || '') : (new URLSearchParams(window.location.search).get('servicePrice') || '') + let url = `/client/booking/confirm?workerId=${workerId}&date=${selectedDate}&time=${selectedTime}¬es=${encodeURIComponent(notes)}` - if (orgId) url += `&organization_id=${orgId}` + if (isOrg) url += `&organization_id=${orgId}` if (serviceName) url += `&serviceName=${encodeURIComponent(serviceName)}` if (servicePrice) url += `&servicePrice=${servicePrice}` if (address) url += `&address=${encodeURIComponent(address)}` if (lat && lng) url += `&lat=${lat}&lng=${lng}` - + router.push(url) } @@ -236,12 +284,20 @@ export default function BookingPage({ params }: { params: Promise<{ workerId: st if (loading) { return (
-
جاري التحميل...
+
+
+ ) + } + + if (isOrg && !orgProfile) { + return ( +
+
المؤسسة غير موجودة
) } - if (!workerProfile) { + if (!isOrg && !workerProfile) { return (
الحرفي غير موجود
@@ -256,39 +312,103 @@ export default function BookingPage({ params }: { params: Promise<{ workerId: st -

تحديد الموعد

+

{isOrg ? 'حجز خدمة مؤسسة' : 'تحديد الموعد'}

-
-
- -

أوقات العمل الأسبوعية

+ {/* Org Header */} + {isOrg && orgProfile && ( +
+
+ {orgProfile.logo_url ? ( + + ) : ( + + )} +
+
+

{orgProfile.name}

+ {orgProfile.category &&

{orgProfile.category}

} +
-
- {scheduleSummary.map(s => ( -
- {DAY_NAMES_FULL[s.key]?.short} - {s.active && ( - - {to12Hour(s.start)} - {to12Hour(s.end)} - - )} -
- ))} + )} + + {/* Org Services Selection */} + {isOrg && orgServices.length > 0 && ( +
+

+ + اختر الخدمة +

+
+ {orgServices.map(service => { + const isSelected = selectedService?.id === service.id + return ( +
setSelectedService(isSelected ? null : service)} + className={`bg-[#0F172A]/40 border rounded-xl p-3 flex items-center justify-between cursor-pointer transition-all active:scale-[0.99] ${ + isSelected ? 'border-[#FF8A00] bg-[#FF8A00]/5' : 'border-white/5' + }`} + > +
+ {service.name} + {service.description && ( +

{service.description}

+ )} +
+
+ {service.price != null && ( + + {service.price} ج.م + + )} +
+ {isSelected ? '✓' : '+'} +
+
+
+ ) + })} +
-
+ )} + + {/* Worker Schedule Summary (worker only) */} + {!isOrg && ( +
+
+ +

أوقات العمل الأسبوعية

+
+
+ {scheduleSummary.map(s => ( +
+ {DAY_NAMES_FULL[s.key]?.short} + {s.active && ( + + {to12Hour(s.start)} - {to12Hour(s.end)} + + )} +
+ ))} +
+
+ )} + {/* Date Picker */}
@@ -321,6 +441,7 @@ export default function BookingPage({ params }: { params: Promise<{ workerId: st
+ {/* Time Slots */}

المواعيد المتاحة @@ -333,7 +454,7 @@ export default function BookingPage({ params }: { params: Promise<{ workerId: st {!daySched.active ? (
- الحرفي في إجازة هذا اليوم + {isOrg ? 'المؤسسة غير متاحة هذا اليوم' : 'الحرفي في إجازة هذا اليوم'}
) : timeSlots.length === 0 ? (
@@ -345,7 +466,7 @@ export default function BookingPage({ params }: { params: Promise<{ workerId: st const isSelected = selectedTime === time const available = isSlotAvailable(time) const isDisabled = !available - + return (