From 71170faa355d049187e7eff1890df8fe36a84dd8 Mon Sep 17 00:00:00 2001 From: Habeba Ehab <151371194+codestcode@users.noreply.github.com> Date: Fri, 10 Jul 2026 06:07:41 +0300 Subject: [PATCH] feat: add organization services and gallery management with UI integration --- app/client/organization/[id]/ClientPage.tsx | 109 +++++++++-- app/client/organization/[id]/page.tsx | 5 +- app/organization/home/page.tsx | 83 ++++++++- app/organization/settings/services/page.tsx | 176 ++++++++++++++++++ hooks/useOrgServices.ts | 105 +++++++++++ lib/types.ts | 19 ++ .../20260712010000_org_services_gallery.sql | 135 ++++++++++++++ 7 files changed, 613 insertions(+), 19 deletions(-) create mode 100644 app/organization/settings/services/page.tsx create mode 100644 hooks/useOrgServices.ts create mode 100644 supabase/migrations/20260712010000_org_services_gallery.sql diff --git a/app/client/organization/[id]/ClientPage.tsx b/app/client/organization/[id]/ClientPage.tsx index 46bc1c6..e241ecc 100644 --- a/app/client/organization/[id]/ClientPage.tsx +++ b/app/client/organization/[id]/ClientPage.tsx @@ -3,15 +3,19 @@ import { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' import Image from 'next/image' -import { ArrowLeft, Building2, Star, MapPin, User, Users } from 'lucide-react' +import { ArrowLeft, Building2, Star, MapPin, User, Users, Briefcase, Image as ImageIcon, Phone } from 'lucide-react' import { createClient } from '@/lib/supabase/client' -import type { Organization, OrganizationWorker, OrganizationWorkerPortfolio } from '@/lib/types' +import type { Organization, OrganizationWorker, OrganizationWorkerPortfolio, OrganizationService, OrganizationGallery } from '@/lib/types' interface OrgProfile extends Organization { workers: (OrganizationWorker & { portfolio: OrganizationWorkerPortfolio[] })[] + services: OrganizationService[] + gallery: OrganizationGallery[] + owner_phone?: string | null + owner_email?: string | null } -export default function OrgProfilePage({ params }: { params: any }) { +export default function OrgProfilePage({ id }: { id: string }) { const router = useRouter() const supabase = createClient() const [org, setOrg] = useState(null) @@ -22,20 +26,37 @@ export default function OrgProfilePage({ params }: { params: any }) { const { data: orgData } = await supabase .from('organizations') .select('*') - .eq('id', params.id) + .eq('id', id) .eq('is_active', true) .single() if (orgData) { - const { data: workerData } = await supabase - .from('organization_workers') - .select('*') - .eq('organization_id', orgData.id) - .eq('is_active', true) - .order('created_at', { ascending: true }) + const [workerData, servicesData, galleryData, ownerProfile] = await Promise.all([ + supabase + .from('organization_workers') + .select('*') + .eq('organization_id', orgData.id) + .eq('is_active', true) + .order('created_at', { ascending: true }), + supabase + .from('organization_services') + .select('*') + .eq('organization_id', orgData.id) + .order('sort_order', { ascending: true }), + supabase + .from('organization_gallery') + .select('*') + .eq('organization_id', orgData.id) + .order('sort_order', { ascending: true }), + supabase + .from('profiles') + .select('phone, email') + .eq('id', orgData.owner_id) + .maybeSingle(), + ]) const workersWithPortfolio = await Promise.all( - (workerData || []).map(async (w) => { + (workerData.data || []).map(async (w) => { const { data: portfolio } = await supabase .from('organization_worker_portfolio') .select('*') @@ -45,12 +66,19 @@ export default function OrgProfilePage({ params }: { params: any }) { }) ) - setOrg({ ...orgData, workers: workersWithPortfolio }) + setOrg({ + ...orgData, + workers: workersWithPortfolio, + services: servicesData.data || [], + gallery: galleryData.data || [], + owner_phone: ownerProfile.data?.phone || null, + owner_email: ownerProfile.data?.email || null, + }) } setLoading(false) } fetchOrg() - }, [params.id, supabase]) + }, [id, supabase]) if (loading) { return ( @@ -97,6 +125,12 @@ export default function OrgProfilePage({ params }: { params: any }) { {org.city} )} + {org.owner_phone && ( +
+ + {org.owner_phone} +
+ )} {/* Stats */} @@ -108,13 +142,59 @@ export default function OrgProfilePage({ params }: { params: any }) {

عمال

+
+ +

{org.services.length}

+

خدمات

+

قريباً

- {/* Workers Grid */} + {/* Gallery */} + {org.gallery.length > 0 && ( +
+

+ + معرض الأعمال +

+
+ {org.gallery.map((photo) => ( +
+ +
+ ))} +
+
+ )} + + {/* Services */} + {org.services.length > 0 && ( +
+

الخدمات

+
+ {org.services.map((service) => ( +
+
+
+

{service.name}

+ {service.description && ( +

{service.description}

+ )} +
+ {service.price && ( + {service.price} ج.م + )} +
+
+ ))} +
+
+ )} + + {/* Workers */}

فريق العمل

{org.workers.length === 0 ? ( @@ -141,7 +221,6 @@ export default function OrgProfilePage({ params }: { params: any }) {
- {/* Portfolio Gallery */} {worker.portfolio.length > 0 && (
{worker.portfolio.map((photo) => ( diff --git a/app/client/organization/[id]/page.tsx b/app/client/organization/[id]/page.tsx index a7e5424..955555a 100644 --- a/app/client/organization/[id]/page.tsx +++ b/app/client/organization/[id]/page.tsx @@ -4,6 +4,7 @@ export function generateStaticParams() { return [{ id: 'dummy' }]; } -export default function Page({ params }: { params: any }) { - return ; +export default async function Page({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + return ; } diff --git a/app/organization/home/page.tsx b/app/organization/home/page.tsx index 81844a0..017f3c6 100644 --- a/app/organization/home/page.tsx +++ b/app/organization/home/page.tsx @@ -1,10 +1,10 @@ 'use client' -import { useState } from 'react' +import { useState, useEffect } from 'react' import { useRouter } from 'next/navigation' import { Building2, Users, Plus, Pencil, Trash2, Camera, X, Upload, - Search, UserPlus, User, Crown, ArrowUpCircle + Search, UserPlus, User, Crown, ArrowUpCircle, Image as ImageIcon } from 'lucide-react' import { PageHeader } from '@/components/ui/PageHeader' import { SubPageLayout } from '@/components/ui/SubPageLayout' @@ -44,6 +44,49 @@ export default function OrgHomePage() { const [uploadingWorkerId, setUploadingWorkerId] = useState(null) + const [gallery, setGallery] = useState<{ id: string; image_url: string }[]>([]) + const [uploadingGallery, setUploadingGallery] = useState(false) + + useEffect(() => { + if (!org?.id) return + supabase.from('organization_gallery').select('id, image_url').eq('organization_id', org.id).order('sort_order', { ascending: true }).then(({ data }) => { + if (data) setGallery(data) + }) + }, [org?.id, supabase, org?.worker_count]) + + const handleGalleryUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file || !org) return + setUploadingGallery(true) + const reader = new FileReader() + reader.onloadend = async () => { + try { + const res = await fetch('/api/upload-booking-images', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ images: [reader.result] }), + }) + const result = await res.json() + if (result.success && result.urls[0]) { + await supabase.from('organization_gallery').insert({ + organization_id: org.id, + image_url: result.urls[0], + sort_order: gallery.length, + }) + const { data } = await supabase.from('organization_gallery').select('id, image_url').eq('organization_id', org.id).order('sort_order', { ascending: true }) + if (data) setGallery(data) + } + } catch (err) { console.error(err) } + setUploadingGallery(false) + } + reader.readAsDataURL(file) + } + + const handleRemoveGalleryPhoto = async (photoId: string) => { + await supabase.from('organization_gallery').delete().eq('id', photoId) + setGallery(p => p.filter(g => g.id !== photoId)) + } + const categories = ['سباكة', 'كهرباء', 'نجارة', 'دهان', 'تكييف', 'تبريد', 'عزل أسطح', 'سيراميك', 'حدادة', 'زجاج'] const handleCreateOrg = async (e: React.FormEvent) => { @@ -330,6 +373,42 @@ export default function OrgHomePage() {
)} + {/* Gallery Section */} +
+
+

+ + معرض الأعمال +

+ {gallery.length < 4 && ( + <> + + + + )} +
+ {gallery.length === 0 ? ( +
+ +

أضف صوراً لأعمال المؤسسة (الحد الأقصى 4)

+
+ ) : ( +
+ {gallery.map((photo) => ( +
+ + +
+ ))} +
+ )} +
+

العمال والفنيين

diff --git a/app/organization/settings/services/page.tsx b/app/organization/settings/services/page.tsx new file mode 100644 index 0000000..20b09bb --- /dev/null +++ b/app/organization/settings/services/page.tsx @@ -0,0 +1,176 @@ +'use client' + +import React, { useState } from 'react' +import { useRouter } from 'next/navigation' +import { + Droplet, Zap, Wrench, Home, Scissors, LayoutGrid, + Plus, Trash2, Check +} from 'lucide-react' +import { PageHeader } from '@/components/ui/PageHeader' +import { SubPageLayout } from '@/components/ui/SubPageLayout' +import { PageLoader } from '@/components/ui/PageLoader' +import { SaveButton } from '@/components/ui/forms/SaveButton' +import { useOrgServices } from '@/hooks/useOrgServices' + +const ICON_MAP: Record = { + Droplet, Zap, Wrench, Home, Scissors, LayoutGrid, +} + +export default function OrgServicesPage() { + const router = useRouter() + const { + services, + availableTemplates, + loading, + fetching, + addService, + removeService, + updateService, + handleSave, + } = useOrgServices() + + const [dropdownOpen, setDropdownOpen] = useState(false) + + return ( + + +
+ {fetching ? : ( +
+ {/* Add from templates */} +
+ + + + {dropdownOpen && ( +
+ {availableTemplates.map((t, i) => { + const isAdded = services.some(s => s.name === t.name) + const IconComp = ICON_MAP[t.icon] || Wrench + return ( + + ) + })} +
+ )} +
+ + {/* Service list header */} +
+ +
+ + {services.filter(s => s.name.trim()).length} خدمات + +

الخدمات الحالية

+
+
+ + {/* Services list */} +
+ {services.filter(s => s.name.trim() || s.id.startsWith('new_')).length ? services + .filter(s => s.name.trim() || s.id.startsWith('new_')) + .map(s => { + const isNew = s.id.startsWith('new_') + return ( +
+
+ +
+
+ updateService(s.id, 'name', e.target.value)} + className="bg-[#050814] border border-white/10 rounded-xl px-3 py-2 text-sm text-right outline-none w-full focus:border-[#FF8A00] text-white" + /> +
+ +
+ +
+
+ + updateService(s.id, 'price', e.target.value ? parseFloat(e.target.value) : null)} + className="bg-[#050814] border border-white/10 rounded-xl px-3 py-2 text-center text-sm focus:border-[#FF8A00] outline-none font-bold text-white transition-colors" + dir="ltr" + min="0" + /> +
+
+ + +
+
+ +
+ +