From eff0b41b78afeb0b0e05cf57deb9249bc50a3ff9 Mon Sep 17 00:00:00 2001 From: Claw AI Date: Fri, 27 Mar 2026 18:01:42 +0000 Subject: [PATCH] Add enums, AuthService, and integrate backend registration endpoints - Add separate enum files: BuildingType, PropertyStatus, BookingStatus, CommissionType, IdentityType, UserRole, City, LoginMethod, OwnerType, CustomerType - Add AuthService (addToken/getToken/deleteToken) - Update api.js: use AuthService, add Owner/Add and Customer/Add endpoints - Update login page to use AuthService for token storage - Rewrite owner register: 3-step flow with OwnerType dropdown, backend integration, OTP verification - Rewrite tenant register: 2-step flow with CustomerType dropdown, backend integration, OTP verification - Update homepage and property detail to use enums instead of hardcoded maps - Update AddPropertyForm to import from enums directly - Add console logs and status toasts linked to API response messages --- app/components/admin/AddPropertyForm.js | 22 +- app/enums/BookingStatus.js | 38 ++ app/enums/BuildingType.js | 33 ++ app/enums/City.js | 38 ++ app/enums/CommissionType.js | 19 + app/enums/CustomerType.js | 17 + app/enums/IdentityType.js | 23 + app/enums/LoginMethod.js | 11 + app/enums/OwnerType.js | 17 + app/enums/PropertyStatus.js | 33 ++ app/enums/UserRole.js | 26 + app/enums/index.js | 15 + app/login/page.js | 13 +- app/page.js | 19 +- app/property/[id]/page.js | 17 +- app/register/owner/page.js | 756 ++++++++++-------------- app/register/tenant/page.js | 645 ++++++++++---------- app/services/AuthService.js | 51 ++ app/utils/api.js | 49 +- app/utils/constants.js | 100 ++-- 20 files changed, 1099 insertions(+), 843 deletions(-) create mode 100644 app/enums/BookingStatus.js create mode 100644 app/enums/BuildingType.js create mode 100644 app/enums/City.js create mode 100644 app/enums/CommissionType.js create mode 100644 app/enums/CustomerType.js create mode 100644 app/enums/IdentityType.js create mode 100644 app/enums/LoginMethod.js create mode 100644 app/enums/OwnerType.js create mode 100644 app/enums/PropertyStatus.js create mode 100644 app/enums/UserRole.js create mode 100644 app/enums/index.js create mode 100644 app/services/AuthService.js diff --git a/app/components/admin/AddPropertyForm.js b/app/components/admin/AddPropertyForm.js index 85d5da4..55da43e 100644 --- a/app/components/admin/AddPropertyForm.js +++ b/app/components/admin/AddPropertyForm.js @@ -3,7 +3,7 @@ import { useState } from 'react'; import { motion } from 'framer-motion'; import { useProperties } from '@/app/contexts/PropertyContext'; -import { COMMISSION_TYPE, CITIES } from '@/app/utils/constants'; +import { CommissionType, City, CitiesList } from '@/app/enums'; import { X, MapPin, Home, DollarSign, Percent } from 'lucide-react'; export default function AddPropertyForm({ onClose, onSuccess }) { @@ -25,7 +25,7 @@ export default function AddPropertyForm({ onClose, onSuccess }) { dailyPrice: 0, commissionRate: 5, - commissionType: COMMISSION_TYPE.FROM_OWNER, + commissionType: CommissionType.FROM_OWNER, securityDeposit: 0, @@ -86,11 +86,11 @@ export default function AddPropertyForm({ onClose, onSuccess }) { const commission = (dailyPrice * commissionRate) / 100; switch(commissionType) { - case COMMISSION_TYPE.FROM_TENANT: + case CommissionType.FROM_TENANT: return dailyPrice + commission; - case COMMISSION_TYPE.FROM_OWNER: + case CommissionType.FROM_OWNER: return dailyPrice; - case COMMISSION_TYPE.FROM_BOTH: + case CommissionType.FROM_BOTH: return dailyPrice + (commission / 2); default: return dailyPrice; @@ -232,8 +232,8 @@ export default function AddPropertyForm({ onClose, onSuccess }) { setFormData({...formData, commissionType: e.target.value})} /> من المالك @@ -242,8 +242,8 @@ export default function AddPropertyForm({ onClose, onSuccess }) { setFormData({...formData, commissionType: e.target.value})} /> من المستأجر @@ -252,8 +252,8 @@ export default function AddPropertyForm({ onClose, onSuccess }) { setFormData({...formData, commissionType: e.target.value})} /> من الاثنين diff --git a/app/enums/BookingStatus.js b/app/enums/BookingStatus.js new file mode 100644 index 0000000..ea4f05a --- /dev/null +++ b/app/enums/BookingStatus.js @@ -0,0 +1,38 @@ +/** + * BookingStatus Enum + * Backend values are strings + * Used in: Reservation workflow + */ +const BookingStatus = Object.freeze({ + PENDING: 'pending', + OWNER_APPROVED: 'owner_approved', + ADMIN_APPROVED: 'admin_approved', + ACTIVE: 'active', + COMPLETED: 'completed', + REJECTED: 'rejected', + CANCELLED: 'cancelled', +}); + +// Map status → Arabic label +const BookingStatusLabels = Object.freeze({ + [BookingStatus.PENDING]: 'بانتظار الموافقة', + [BookingStatus.OWNER_APPROVED]: 'موافقة المالك', + [BookingStatus.ADMIN_APPROVED]: 'موافقة الإدارة', + [BookingStatus.ACTIVE]: 'إيجار نشط', + [BookingStatus.COMPLETED]: 'منتهي', + [BookingStatus.REJECTED]: 'مرفوض', + [BookingStatus.CANCELLED]: 'ملغي', +}); + +// Map status → color class (Tailwind bg) +const BookingStatusColors = Object.freeze({ + [BookingStatus.PENDING]: 'yellow', + [BookingStatus.OWNER_APPROVED]: 'blue', + [BookingStatus.ADMIN_APPROVED]: 'green', + [BookingStatus.ACTIVE]: 'purple', + [BookingStatus.COMPLETED]: 'gray', + [BookingStatus.REJECTED]: 'red', + [BookingStatus.CANCELLED]: 'red', +}); + +export { BookingStatus, BookingStatusLabels, BookingStatusColors }; diff --git a/app/enums/BuildingType.js b/app/enums/BuildingType.js new file mode 100644 index 0000000..abc320a --- /dev/null +++ b/app/enums/BuildingType.js @@ -0,0 +1,33 @@ +/** + * BuildingType Enum + * Backend values are numeric (0, 1, 2) + * Used in: PropertyInformation.buildingType + */ +const BuildingType = Object.freeze({ + APARTMENT: 0, + VILLA: 1, + HOUSE: 2, +}); + +// Map numeric value → Arabic label +const BuildingTypeLabels = Object.freeze({ + [BuildingType.APARTMENT]: 'شقة', + [BuildingType.VILLA]: 'فيلا', + [BuildingType.HOUSE]: 'بيت', +}); + +// Map numeric value → English key (for UI filters) +const BuildingTypeKeys = Object.freeze({ + [BuildingType.APARTMENT]: 'apartment', + [BuildingType.VILLA]: 'villa', + [BuildingType.HOUSE]: 'house', +}); + +// Reverse map: English key → numeric value +const BuildingTypeByKey = Object.freeze({ + apartment: BuildingType.APARTMENT, + villa: BuildingType.VILLA, + house: BuildingType.HOUSE, +}); + +export { BuildingType, BuildingTypeLabels, BuildingTypeKeys, BuildingTypeByKey }; diff --git a/app/enums/City.js b/app/enums/City.js new file mode 100644 index 0000000..69ab0ae --- /dev/null +++ b/app/enums/City.js @@ -0,0 +1,38 @@ +/** + * City Enum + * Syrian cities used in property locations + * Used in: Property search filters, location display + */ +const City = Object.freeze({ + DAMASCUS: 'دمشق', + ALEPPO: 'حلب', + HOMS: 'حمص', + LATAKIA: 'اللاذقية', + DARAA: 'درعا', + TARTOUS: 'طرطوس', + SUWEIDA: 'السويداء', + DEIR_EZZOR: 'دير الزور', + RAQQA: 'الرقة', + IDLIB: 'إدلب', + HASAKAH: 'الحسكة', + QAMISHLI: 'القامشلي', + RURAL_DAMASCUS: 'ريف دمشق', +}); + +// All cities as a flat array +const CitiesList = Object.freeze(Object.values(City)); + +/** + * Extract city name from a full address string + * @param {string} address + * @returns {string} + */ +function extractCity(address) { + if (!address) return ''; + for (const city of CitiesList) { + if (address.includes(city)) return city; + } + return ''; +} + +export { City, CitiesList, extractCity }; diff --git a/app/enums/CommissionType.js b/app/enums/CommissionType.js new file mode 100644 index 0000000..94228d7 --- /dev/null +++ b/app/enums/CommissionType.js @@ -0,0 +1,19 @@ +/** + * CommissionType Enum + * Defines who pays the platform commission + * Used in: Property pricing, booking financials + */ +const CommissionType = Object.freeze({ + FROM_OWNER: 'from_owner', + FROM_TENANT: 'from_tenant', + FROM_BOTH: 'from_both', +}); + +// Map type → Arabic label +const CommissionTypeLabels = Object.freeze({ + [CommissionType.FROM_OWNER]: 'من المالك', + [CommissionType.FROM_TENANT]: 'من المستأجر', + [CommissionType.FROM_BOTH]: 'من الاثنين', +}); + +export { CommissionType, CommissionTypeLabels }; diff --git a/app/enums/CustomerType.js b/app/enums/CustomerType.js new file mode 100644 index 0000000..a230f8f --- /dev/null +++ b/app/enums/CustomerType.js @@ -0,0 +1,17 @@ +/** + * CustomerType Enum + * Backend values for customer sub-types + * Used in: Customer registration (Customer/Add) + */ +const CustomerType = Object.freeze({ + PERSONAL: 'Personal', + FAMILY: 'Family', +}); + +// Map value → Arabic label +const CustomerTypeLabels = Object.freeze({ + [CustomerType.PERSONAL]: 'شخصي', + [CustomerType.FAMILY]: 'عائلي', +}); + +export { CustomerType, CustomerTypeLabels }; diff --git a/app/enums/IdentityType.js b/app/enums/IdentityType.js new file mode 100644 index 0000000..754ad44 --- /dev/null +++ b/app/enums/IdentityType.js @@ -0,0 +1,23 @@ +/** + * IdentityType Enum + * Tenant identity document type + * Used in: Property booking, allowedIdentities filter + */ +const IdentityType = Object.freeze({ + SYRIAN: 'syrian', + PASSPORT: 'passport', +}); + +// Map type → Arabic label +const IdentityTypeLabels = Object.freeze({ + [IdentityType.SYRIAN]: 'هوية سورية', + [IdentityType.PASSPORT]: 'جواز سفر', +}); + +// Map type → flag emoji +const IdentityTypeFlags = Object.freeze({ + [IdentityType.SYRIAN]: '🇸🇾', + [IdentityType.PASSPORT]: '🛂', +}); + +export { IdentityType, IdentityTypeLabels, IdentityTypeFlags }; diff --git a/app/enums/LoginMethod.js b/app/enums/LoginMethod.js new file mode 100644 index 0000000..e54fac5 --- /dev/null +++ b/app/enums/LoginMethod.js @@ -0,0 +1,11 @@ +/** + * LoginMethod Enum + * Authentication method type + * Used in: Login page, OTP verification + */ +const LoginMethod = Object.freeze({ + EMAIL: 'email', + PHONE: 'phone', +}); + +export { LoginMethod }; diff --git a/app/enums/OwnerType.js b/app/enums/OwnerType.js new file mode 100644 index 0000000..18549c6 --- /dev/null +++ b/app/enums/OwnerType.js @@ -0,0 +1,17 @@ +/** + * OwnerType Enum + * Backend values for owner sub-types + * Used in: Owner registration (Owner/Add) + */ +const OwnerType = Object.freeze({ + PERSON: 'peerson', + REAL_ESTATE_AGENCY: 'RealEstateAgency', +}); + +// Map value → Arabic label +const OwnerTypeLabels = Object.freeze({ + [OwnerType.PERSON]: 'شخص', + [OwnerType.REAL_ESTATE_AGENCY]: 'وكالة عقارية', +}); + +export { OwnerType, OwnerTypeLabels }; diff --git a/app/enums/PropertyStatus.js b/app/enums/PropertyStatus.js new file mode 100644 index 0000000..9fafce7 --- /dev/null +++ b/app/enums/PropertyStatus.js @@ -0,0 +1,33 @@ +/** + * PropertyStatus Enum + * Backend values are numeric (0, 1, 2) + * Used in: PropertyInformation.status + */ +const PropertyStatus = Object.freeze({ + AVAILABLE: 0, + BOOKED: 1, + MAINTENANCE: 2, +}); + +// Map numeric value → Arabic label +const PropertyStatusLabels = Object.freeze({ + [PropertyStatus.AVAILABLE]: 'متاح', + [PropertyStatus.BOOKED]: 'محجوز', + [PropertyStatus.MAINTENANCE]: 'صيانة', +}); + +// Map numeric value → English key (for UI filters) +const PropertyStatusKeys = Object.freeze({ + [PropertyStatus.AVAILABLE]: 'available', + [PropertyStatus.BOOKED]: 'booked', + [PropertyStatus.MAINTENANCE]: 'maintenance', +}); + +// Reverse map: English key → numeric value +const PropertyStatusByKey = Object.freeze({ + available: PropertyStatus.AVAILABLE, + booked: PropertyStatus.BOOKED, + maintenance: PropertyStatus.MAINTENANCE, +}); + +export { PropertyStatus, PropertyStatusLabels, PropertyStatusKeys, PropertyStatusByKey }; diff --git a/app/enums/UserRole.js b/app/enums/UserRole.js new file mode 100644 index 0000000..3bf79ee --- /dev/null +++ b/app/enums/UserRole.js @@ -0,0 +1,26 @@ +/** + * UserRole Enum + * User account roles in the system + * Used in: JWT payload, registration, routing + */ +const UserRole = Object.freeze({ + OWNER: 'owner', + TENANT: 'tenant', + ADMIN: 'admin', +}); + +// Map role → Arabic label +const UserRoleLabels = Object.freeze({ + [UserRole.OWNER]: 'مالك عقار', + [UserRole.TENANT]: 'مستأجر', + [UserRole.ADMIN]: 'مدير النظام', +}); + +// Map role → color theme (used in UI) +const UserRoleColors = Object.freeze({ + [UserRole.OWNER]: 'amber', + [UserRole.TENANT]: 'blue', + [UserRole.ADMIN]: 'red', +}); + +export { UserRole, UserRoleLabels, UserRoleColors }; diff --git a/app/enums/index.js b/app/enums/index.js new file mode 100644 index 0000000..20883a5 --- /dev/null +++ b/app/enums/index.js @@ -0,0 +1,15 @@ +/** + * Enums Index + * Central export for all enum modules + */ + +export { BuildingType, BuildingTypeLabels, BuildingTypeKeys, BuildingTypeByKey } from './BuildingType'; +export { PropertyStatus, PropertyStatusLabels, PropertyStatusKeys, PropertyStatusByKey } from './PropertyStatus'; +export { BookingStatus, BookingStatusLabels, BookingStatusColors } from './BookingStatus'; +export { CommissionType, CommissionTypeLabels } from './CommissionType'; +export { IdentityType, IdentityTypeLabels, IdentityTypeFlags } from './IdentityType'; +export { UserRole, UserRoleLabels, UserRoleColors } from './UserRole'; +export { City, CitiesList, extractCity } from './City'; +export { LoginMethod } from './LoginMethod'; +export { OwnerType, OwnerTypeLabels } from './OwnerType'; +export { CustomerType, CustomerTypeLabels } from './CustomerType'; diff --git a/app/login/page.js b/app/login/page.js index 097d01e..dcbb8b2 100644 --- a/app/login/page.js +++ b/app/login/page.js @@ -29,6 +29,7 @@ import { isEmail, isPhoneNumber, } from '../utils/api'; +import AuthService from '../services/AuthService'; export default function LoginPage() { const router = useRouter(); @@ -87,10 +88,10 @@ export default function LoginPage() { console.log('[Login] Response:', result); if (result.status === 200) { - // Login success — store token + // Login success — store token via AuthService const token = typeof result.data === 'string' ? result.data : result.data?.token || result.data; - localStorage.setItem('token', token); - console.log('[Login] Token stored successfully'); + AuthService.addToken(token); + console.log('[Login] Token stored via AuthService'); // Decode token to get user info (basic JWT decode) try { @@ -182,11 +183,11 @@ export default function LoginPage() { console.log('[OTP] Verify response:', result); if (result.ok) { - // Verified — store token if returned + // Verified — store token if returned via AuthService const token = typeof result.data === 'string' ? result.data : result.data?.token || result.data; if (token && typeof token === 'string' && token.includes('.')) { - localStorage.setItem('token', token); - console.log('[OTP] Token stored'); + AuthService.addToken(token); + console.log('[OTP] Token stored via AuthService'); } localStorage.setItem('user', JSON.stringify({ diff --git a/app/page.js b/app/page.js index e5e7cd4..8ebaa72 100644 --- a/app/page.js +++ b/app/page.js @@ -29,6 +29,7 @@ import PropertyMap from './components/home/PropertyMap'; import Link from 'next/link'; import Image from 'next/image'; import { getRentProperties, getSaleProperties } from './utils/api'; +import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from './enums'; // Map API property data to the format the UI expects // API returns { propertyInformationId, deposit, monthlyRent, dailyRent, rating, propertyInformation: {...}, ... } @@ -38,13 +39,8 @@ function mapApiProperty(item, index) { const dailyPrice = item.dailyRent ?? item.monthlyRent ?? item.price ?? 0; const monthlyPrice = item.monthlyRent ?? 0; - // BuildingType: 0=Apartment, 1=Villa, 2=House - const buildingTypeMap = { 0: 'apartment', 1: 'villa', 2: 'house' }; - const propType = buildingTypeMap[info.buildingType] ?? buildingTypeMap[item.type] ?? 'apartment'; - - // Status: 0=Available, 1=Booked, 2=Maintenance - const statusMap = { 0: 'available', 1: 'booked', 2: 'maintenance' }; - const status = statusMap[info.status] ?? statusMap[item.status] ?? 'available'; + const propType = BuildingTypeKeys[info.buildingType] ?? BuildingTypeKeys[item.type] ?? 'apartment'; + const status = PropertyStatusKeys[info.status] ?? PropertyStatusKeys[item.status] ?? 'available'; const features = []; if (item.isSmokeAllow) features.push('يسمح بالتدخين'); @@ -86,14 +82,7 @@ function mapApiProperty(item, index) { }; } -function extractCity(address) { - if (!address) return ''; - const cities = ['دمشق', 'حلب', 'حمص', 'اللاذقية', 'درعا', 'طرطوس', 'السويداء', 'دير الزور', 'الرقة', 'إدلب', 'الحسكة', 'القامشلي', 'ريف دمشق']; - for (const city of cities) { - if (address.includes(city)) return city; - } - return ''; -} +// extractCity is now imported from @/app/enums // Fallback dummy data const FALLBACK_PROPERTIES = [ diff --git a/app/property/[id]/page.js b/app/property/[id]/page.js index fdfe2e2..30a70a5 100644 --- a/app/property/[id]/page.js +++ b/app/property/[id]/page.js @@ -43,6 +43,7 @@ import { ArrowLeft } from 'lucide-react'; import { getRentProperty, getSaleProperty, bookReservation, checkAvailability } from '../../utils/api'; +import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from '../../enums'; // Map API response to the UI format function mapApiDetail(item) { @@ -53,11 +54,8 @@ function mapApiDetail(item) { const dailyPrice = item.dailyRent ?? item.monthlyRent ?? item.price ?? 0; const monthlyPrice = item.monthlyRent ?? 0; - const buildingTypeMap = { 0: 'apartment', 1: 'villa', 2: 'house' }; - const propType = buildingTypeMap[info.buildingType] ?? buildingTypeMap[item.type] ?? 'apartment'; - - const statusMap = { 0: 'available', 1: 'booked', 2: 'maintenance' }; - const status = statusMap[info.status] ?? statusMap[item.status] ?? 'available'; + const propType = BuildingTypeKeys[info.buildingType] ?? BuildingTypeKeys[item.type] ?? 'apartment'; + const status = PropertyStatusKeys[info.status] ?? PropertyStatusKeys[item.status] ?? 'available'; const features = []; if (item.isSmokeAllow) features.push({ name: 'يسمح بالتدخين', available: true, description: '' }); @@ -122,14 +120,7 @@ function mapApiDetail(item) { }; } -function extractCity(address) { - if (!address) return ''; - const cities = ['دمشق', 'حلب', 'حمص', 'اللاذقية', 'درعا', 'طرطوس', 'السويداء', 'دير الزور', 'الرقة', 'إدلب', 'الحسكة', 'القامشلي', 'ريف دمشق']; - for (const city of cities) { - if (address.includes(city)) return city; - } - return ''; -} +// extractCity is now imported from @/app/enums // Fallback data (same as before) const FALLBACK_PROPERTIES = { diff --git a/app/register/owner/page.js b/app/register/owner/page.js index cd1ff02..0944672 100644 --- a/app/register/owner/page.js +++ b/app/register/owner/page.js @@ -1,37 +1,27 @@ 'use client'; -import { useState, useRef, useEffect } from 'react'; +import { useState, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { useRouter } from 'next/navigation'; import Link from 'next/link'; import Image from 'next/image'; import { - User, - Mail, - Phone, - Lock, - Eye, - EyeOff, - MessageCircle, - Camera, - Upload, - X, - CheckCircle, - XCircle, - AlertCircle, - ArrowLeft, - Building, - Loader2, - Home + User, Mail, Phone, Lock, Eye, EyeOff, MessageCircle, + Camera, X, CheckCircle, XCircle, ArrowLeft, Building, + Loader2, Shield, KeyRound } from 'lucide-react'; import toast, { Toaster } from 'react-hot-toast'; +import { addOwner, loginWithEmail, sendEmailOTP, verifyEmail } from '../../utils/api'; +import AuthService from '../../services/AuthService'; +import { OwnerType, OwnerTypeLabels } from '../../enums'; export default function OwnerRegisterPage() { const router = useRouter(); - const [step, setStep] = useState(1); + const [step, setStep] = useState(1); // 1=form, 2=id images, 3=OTP const [showPassword, setShowPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [formData, setFormData] = useState({ name: '', email: '', @@ -39,97 +29,58 @@ export default function OwnerRegisterPage() { whatsapp: '', password: '', confirmPassword: '', + ownerType: OwnerType.PERSON, agreeTerms: false }); - const [idImages, setIdImages] = useState({ - front: null, - back: null - }); - const [idImagePreviews, setIdImagePreviews] = useState({ - front: '', - back: '' - }); + + const [idImages, setIdImages] = useState({ front: null, back: null }); + const [idImagePreviews, setIdImagePreviews] = useState({ front: '', back: '' }); + const [otpCode, setOtpCode] = useState(''); const [errors, setErrors] = useState({}); - + const fileInputFrontRef = useRef(null); const fileInputBackRef = useRef(null); const handleImageUpload = (side, file) => { if (!file) return; - if (!file.type.startsWith('image/')) { toast.error('الرجاء اختيار صورة صالحة'); return; } - if (file.size > 5 * 1024 * 1024) { toast.error('حجم الصورة يجب أن يكون أقل من 5 ميجابايت'); return; } - const reader = new FileReader(); reader.onloadend = () => { - setIdImagePreviews(prev => ({ - ...prev, - [side]: reader.result - })); + setIdImagePreviews(prev => ({ ...prev, [side]: reader.result })); }; reader.readAsDataURL(file); - - setIdImages(prev => ({ - ...prev, - [side]: file - })); - - toast.success(`تم رفع الصورة بنجاح`, { - style: { background: '#dcfce7', color: '#166534' } - }); + setIdImages(prev => ({ ...prev, [side]: file })); + console.log('[OwnerRegister] Image uploaded:', side); + toast.success('تم رفع الصورة بنجاح', { style: { background: '#dcfce7', color: '#166534' } }); }; - const validateEmail = (email) => { - const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return re.test(email); - }; - - const validatePhone = (phone) => { - const re = /^(09|05)[0-9]{8}$/; - return re.test(phone); - }; + const validateEmail = (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + const validatePhone = (phone) => /^(09|05)[0-9]{8}$/.test(phone); const validateStep1 = () => { const newErrors = {}; + if (!formData.name) newErrors.name = 'الاسم الكامل مطلوب'; + else if (formData.name.length < 3) newErrors.name = 'الاسم يجب أن يكون 3 أحرف على الأقل'; - if (!formData.name) { - newErrors.name = 'الاسم الكامل مطلوب'; - } else if (formData.name.length < 3) { - newErrors.name = 'الاسم يجب أن يكون 3 أحرف على الأقل'; - } + if (!formData.email) newErrors.email = 'البريد الإلكتروني مطلوب'; + else if (!validateEmail(formData.email)) newErrors.email = 'البريد الإلكتروني غير صالح'; - if (!formData.email) { - newErrors.email = 'البريد الإلكتروني مطلوب'; - } else if (!validateEmail(formData.email)) { - newErrors.email = 'البريد الإلكتروني غير صالح'; - } + if (!formData.whatsapp) newErrors.whatsapp = 'رقم الواتساب مطلوب'; + else if (!validatePhone(formData.whatsapp)) newErrors.whatsapp = 'رقم الواتساب غير صالح (يجب أن يبدأ 09 أو 05)'; - if (!formData.whatsapp) { - newErrors.whatsapp = 'رقم الواتساب مطلوب'; - } else if (!validatePhone(formData.whatsapp)) { - newErrors.whatsapp = 'رقم الواتساب غير صالح (يجب أن يبدأ 09 أو 05)'; - } + if (formData.phone && !validatePhone(formData.phone)) newErrors.phone = 'رقم الهاتف غير صالح'; - if (formData.phone && !validatePhone(formData.phone)) { - newErrors.phone = 'رقم الهاتف غير صالح'; - } + if (!formData.password) newErrors.password = 'كلمة المرور مطلوبة'; + else if (formData.password.length < 6) newErrors.password = 'كلمة المرور يجب أن تكون 6 أحرف على الأقل'; - if (!formData.password) { - newErrors.password = 'كلمة المرور مطلوبة'; - } else if (formData.password.length < 6) { - newErrors.password = 'كلمة المرور يجب أن تكون 6 أحرف على الأقل'; - } - - if (formData.password !== formData.confirmPassword) { - newErrors.confirmPassword = 'كلمات المرور غير متطابقة'; - } + if (formData.password !== formData.confirmPassword) newErrors.confirmPassword = 'كلمات المرور غير متطابقة'; setErrors(newErrors); return Object.keys(newErrors).length === 0; @@ -137,20 +88,15 @@ export default function OwnerRegisterPage() { const validateStep2 = () => { const newErrors = {}; - - if (!idImages.front) { - newErrors.front = 'صورة الوجه الأمامي للهوية مطلوبة'; - } - if (!idImages.back) { - newErrors.back = 'صورة الوجه الخلفي للهوية مطلوبة'; - } - + if (!idImages.front) newErrors.front = 'صورة الوجه الأمامي للهوية مطلوبة'; + if (!idImages.back) newErrors.back = 'صورة الوجه الخلفي للهوية مطلوبة'; setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleNextStep = () => { if (validateStep1()) { + console.log('[OwnerRegister] Step 1 valid, moving to step 2'); setStep(2); window.scrollTo({ top: 0, behavior: 'smooth' }); } else { @@ -158,6 +104,7 @@ export default function OwnerRegisterPage() { } }; + // ─── Main signup handler ─── const handleSubmit = async (e) => { e.preventDefault(); @@ -165,32 +112,131 @@ export default function OwnerRegisterPage() { toast.error('يرجى إكمال جميع الصور المطلوبة'); return; } - if (!formData.agreeTerms) { toast.error('يجب الموافقة على الشروط والأحكام'); return; } setIsLoading(true); + console.log('[OwnerRegister] Submitting owner registration...'); - setTimeout(() => { + const payload = { + name: formData.name, + email: formData.email, + phoneNumber: formData.phone || '', + whatsAppNumber: formData.whatsapp, + password: formData.password, + ownerType: formData.ownerType, + }; + + try { + const res = await addOwner(payload); + console.log('[OwnerRegister] addOwner response:', res); + + if (res.status === 200 || res.ok) { + // ── Store temp token for OTP flow ── + const tempToken = res.data; + if (tempToken) { + AuthService.addToken(tempToken); + console.log('[OwnerRegister] Temp token stored for OTP'); + } + + const apiMessage = res.message || res.data?.message; + toast.success(apiMessage || 'تم إنشاء الحساب! يرجى التحقق من بريدك الإلكتروني', { + duration: 4000, + }); + + // ── Auto-login to trigger OTP ── + console.log('[OwnerRegister] Auto-login to send OTP...'); + const loginRes = await loginWithEmail(formData.email, formData.password); + console.log('[OwnerRegister] login response:', loginRes); + + if (loginRes.status === 206) { + // OTP sent — move to OTP step + const otpToken = loginRes.data; + if (otpToken) { + AuthService.addToken(otpToken); + console.log('[OwnerRegister] OTP token stored'); + } + const loginMsg = loginRes.message || loginRes.data?.message; + toast(loginMsg || 'تم إرسال رمز التحقق إلى بريدك الإلكتروني', { icon: '📧' }); + setStep(3); + } else if (loginRes.status === 200) { + // Direct login success (no OTP needed) + const loginToken = loginRes.data; + if (loginToken) { + AuthService.addToken(loginToken); + } + toast.success(loginRes.message || 'تم تسجيل الدخول بنجاح!'); + router.push('/'); + } + } else { + // Registration failed + const errMsg = res.message || res.data?.message || 'فشل في إنشاء الحساب'; + console.error('[OwnerRegister] Registration failed:', errMsg); + toast.error(errMsg); + } + } catch (err) { + console.error('[OwnerRegister] Error:', err); + toast.error(err.message || 'حدث خطأ أثناء التسجيل'); + } finally { setIsLoading(false); - toast.success('تم إنشاء الحساب بنجاح!', { - style: { background: '#dcfce7', color: '#166534' }, - duration: 3000 - }); + } + }; - localStorage.setItem('user', JSON.stringify({ - name: formData.name, - email: formData.email, - role: 'owner', - avatar: formData.name.charAt(0).toUpperCase() - })); + // ─── OTP verification handler ─── + const handleVerifyOTP = async () => { + if (!otpCode || otpCode.length < 4) { + toast.error('يرجى إدخال رمز التحقق'); + return; + } - setTimeout(() => { - router.push('/'); - }, 1500); - }, 2000); + setIsLoading(true); + console.log('[OwnerRegister] Verifying OTP:', otpCode); + + try { + const res = await verifyEmail(otpCode); + console.log('[OwnerRegister] VerifyEmail response:', res); + + if (res.status === 200) { + // ── Verified! Remove temp token, redirect to login ── + AuthService.deleteToken(); + console.log('[OwnerRegister] Temp token removed after verification'); + + toast.success(res.message || 'تم التحقق من البريد الإلكتروني بنجاح!', { + duration: 3000, + }); + + setTimeout(() => { + router.push('/login'); + }, 1500); + } else { + const errMsg = res.message || res.data?.message || 'رمز التحقق غير صحيح'; + console.error('[OwnerRegister] Verification failed:', errMsg); + toast.error(errMsg); + } + } catch (err) { + console.error('[OwnerRegister] Verify error:', err); + toast.error(err.message || 'حدث خطأ أثناء التحقق'); + } finally { + setIsLoading(false); + } + }; + + // ─── Resend OTP ─── + const handleResendOTP = async () => { + setIsLoading(true); + console.log('[OwnerRegister] Resending email OTP...'); + + try { + await sendEmailOTP(); + toast.success('تم إرسال رمز تحقق جديد'); + } catch (err) { + console.error('[OwnerRegister] Resend OTP error:', err); + toast.error('فشل في إرسال الرمز'); + } finally { + setIsLoading(false); + } }; const fadeInUp = { @@ -200,37 +246,25 @@ export default function OwnerRegisterPage() { }; const staggerContainer = { - animate: { - transition: { - staggerChildren: 0.1 - } - } + animate: { transition: { staggerChildren: 0.1 } } }; return (
+ {/* Background blobs */}
{[...Array(20)].map((_, i) => ( ))}
@@ -241,35 +275,19 @@ export default function OwnerRegisterPage() { transition={{ duration: 0.5 }} className="relative z-10 w-full max-w-2xl" > + {/* Progress bar */}
- - - - + + العودة -
- خطوة {step} من 2 -
+ خطوة {step} من 3
-
- = 1 ? 'bg-amber-500' : 'bg-gray-700' - }`} - animate={{ scaleX: step >= 1 ? 1 : 0.5 }} - /> - = 2 ? 'bg-amber-500' : 'bg-gray-700' - }`} - animate={{ scaleX: step >= 2 ? 1 : 0.5 }} - /> + {[1, 2, 3].map((s) => ( + = s ? 'bg-amber-500' : 'bg-gray-700'}`} animate={{ scaleX: step >= s ? 1 : 0.5 }} /> + ))}
@@ -281,33 +299,23 @@ export default function OwnerRegisterPage() { transition={{ duration: 0.3 }} className="bg-white/5 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden" > + {/* Header */}
- - - + + - + {step === 3 ? : }

- {step === 1 ? 'معلومات المالك' : 'الوثائق الرسمية'} + {step === 1 ? 'معلومات المالك' : step === 2 ? 'الوثائق الرسمية' : 'التحقق من البريد'}

- {step === 1 - ? 'أدخل معلوماتك الأساسية للتواصل' - : 'يرجى رفع صور الهوية الشخصية للتحقق'} + {step === 1 ? 'أدخل معلوماتك الأساسية' : step === 2 ? 'يرجى رفع صور الهوية للتحقق' : 'أدخل رمز التحقق المرسل إلى بريدك'}

@@ -317,247 +325,138 @@ export default function OwnerRegisterPage() { variants={staggerContainer} initial="initial" animate="animate" - onSubmit={step === 1 ? handleNextStep : handleSubmit} + onSubmit={step === 1 ? handleNextStep : step === 2 ? handleSubmit : (e) => { e.preventDefault(); handleVerifyOTP(); }} className="space-y-6" > - {step === 1 ? ( + {/* ─── STEP 1: Form fields ─── */} + {step === 1 && ( <> - +
- +
- { - setFormData({...formData, name: e.target.value}); - setErrors({...errors, name: null}); - }} - className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${ - errors.name ? 'border-red-500' : 'border-gray-700' - }`} - placeholder="أدخل اسمك الكامل" - /> + { setFormData({...formData, name: e.target.value}); setErrors({...errors, name: null}); }} + className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.name ? 'border-red-500' : 'border-gray-700'}`} + placeholder="أدخل اسمك الكامل" />
- {errors.name && ( -

{errors.name}

- )} + {errors.name &&

{errors.name}

}
- +
- +
- { - setFormData({...formData, email: e.target.value}); - setErrors({...errors, email: null}); - }} - className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${ - errors.email ? 'border-red-500' : 'border-gray-700' - }`} - placeholder="أدخل بريدك الإلكتروني" - /> + { setFormData({...formData, email: e.target.value}); setErrors({...errors, email: null}); }} + className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.email ? 'border-red-500' : 'border-gray-700'}`} + placeholder="أدخل بريدك الإلكتروني" />
- {errors.email && ( -

{errors.email}

- )} + {errors.email &&

{errors.email}

}
- +
- { - setFormData({...formData, phone: e.target.value}); - setErrors({...errors, phone: null}); - }} + { setFormData({...formData, phone: e.target.value}); setErrors({...errors, phone: null}); }} className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all" - placeholder="أدخل رقم هاتفك (اختياري)" - /> + placeholder="أدخل رقم هاتفك (اختياري)" />
- {errors.phone && ( -

{errors.phone}

- )} + {errors.phone &&

{errors.phone}

}
- +
- +
- { - setFormData({...formData, whatsapp: e.target.value}); - setErrors({...errors, whatsapp: null}); - }} - className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${ - errors.whatsapp ? 'border-red-500' : 'border-gray-700' - }`} - placeholder="أدخل رقم الواتساب" - /> + { setFormData({...formData, whatsapp: e.target.value}); setErrors({...errors, whatsapp: null}); }} + className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.whatsapp ? 'border-red-500' : 'border-gray-700'}`} + placeholder="أدخل رقم الواتساب" />
- {errors.whatsapp && ( -

{errors.whatsapp}

- )} + {errors.whatsapp &&

{errors.whatsapp}

} +
+ + {/* Owner Type */} + + + +

المحدد: {OwnerTypeLabels[formData.ownerType]}

+

[Console] ownerType = {formData.ownerType}

- +
- +
- { - setFormData({...formData, password: e.target.value}); - setErrors({...errors, password: null}); - }} - className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${ - errors.password ? 'border-red-500' : 'border-gray-700' - }`} - placeholder="أدخل كلمة المرور" - /> -
- {errors.password && ( -

{errors.password}

- )} + {errors.password &&

{errors.password}

}
- +
- +
- { - setFormData({...formData, confirmPassword: e.target.value}); - setErrors({...errors, confirmPassword: null}); - }} - className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${ - errors.confirmPassword ? 'border-red-500' : 'border-gray-700' - }`} - placeholder="أعد إدخال كلمة المرور" - /> - {formData.confirmPassword && (
- {formData.password === formData.confirmPassword ? ( - - ) : ( - - )} + {formData.password === formData.confirmPassword ? : }
)}
- {errors.confirmPassword && ( -

{errors.confirmPassword}

- )} + {errors.confirmPassword &&

{errors.confirmPassword}

}
- ) : ( + )} + + {/* ─── STEP 2: ID Images ─── */} + {step === 2 && ( <> - -
fileInputFrontRef.current?.click()} - className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${ - idImagePreviews.front - ? 'border-green-500 bg-green-500/10' - : errors.front - ? 'border-red-500 bg-red-500/10' - : 'border-gray-700 hover:border-amber-500 hover:bg-white/5' - }`} - > - handleImageUpload('front', e.target.files?.[0])} - className="hidden" - /> - + +
fileInputFrontRef.current?.click()} + className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${idImagePreviews.front ? 'border-green-500 bg-green-500/10' : errors.front ? 'border-red-500 bg-red-500/10' : 'border-gray-700 hover:border-amber-500 hover:bg-white/5'}`}> + handleImageUpload('front', e.target.files?.[0])} className="hidden" /> {idImagePreviews.front ? (
- Front ID -
@@ -565,56 +464,23 @@ export default function OwnerRegisterPage() { <>

اضغط لرفع الصورة

-

- JPEG, PNG, JPG • حتى 5MB • 800x600 بكسل -

+

JPEG, PNG, JPG • حتى 5MB

)}
- {errors.front && ( -

{errors.front}

- )} + {errors.front &&

{errors.front}

} - -
fileInputBackRef.current?.click()} - className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${ - idImagePreviews.back - ? 'border-green-500 bg-green-500/10' - : errors.back - ? 'border-red-500 bg-red-500/10' - : 'border-gray-700 hover:border-amber-500 hover:bg-white/5' - }`} - > - handleImageUpload('back', e.target.files?.[0])} - className="hidden" - /> - + +
fileInputBackRef.current?.click()} + className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${idImagePreviews.back ? 'border-green-500 bg-green-500/10' : errors.back ? 'border-red-500 bg-red-500/10' : 'border-gray-700 hover:border-amber-500 hover:bg-white/5'}`}> + handleImageUpload('back', e.target.files?.[0])} className="hidden" /> {idImagePreviews.back ? (
- Back ID -
@@ -622,83 +488,87 @@ export default function OwnerRegisterPage() { <>

اضغط لرفع الصورة

-

- JPEG, PNG, JPG • حتى 5MB • 800x600 بكسل -

+

JPEG, PNG, JPG • حتى 5MB

)}
- {errors.back && ( -

{errors.back}

- )} + {errors.back &&

{errors.back}

} - setFormData({...formData, agreeTerms: e.target.checked})} - className="w-4 h-4 rounded border-gray-600 bg-white/5 text-amber-500 focus:ring-amber-500 focus:ring-offset-0" - required - /> + className="w-4 h-4 rounded border-gray-600 bg-white/5 text-amber-500 focus:ring-amber-500 focus:ring-offset-0" required /> )} + {/* ─── STEP 3: OTP ─── */} + {step === 3 && ( + +
+ +

تم إرسال رمز التحقق إلى

+

{formData.email}

+
+ +
+ +
+
+ +
+ setOtpCode(e.target.value)} + className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 text-center tracking-[0.5em] text-xl transition-all" + placeholder="------" /> +
+
+ + +
+ )} + + {/* ─── Navigation Buttons ─── */} - {step === 1 ? ( + {step === 1 && ( <> - - + + - ) : ( + )} + {step === 2 && ( <> - - + )} + {step === 3 && ( + + )}
@@ -706,4 +576,4 @@ export default function OwnerRegisterPage() {
); -} \ No newline at end of file +} diff --git a/app/register/tenant/page.js b/app/register/tenant/page.js index 3b1ab8f..82f4c39 100644 --- a/app/register/tenant/page.js +++ b/app/register/tenant/page.js @@ -5,80 +5,59 @@ import { motion } from 'framer-motion'; import { useRouter } from 'next/navigation'; import Link from 'next/link'; import { - User, - Mail, - Phone, - Lock, - Eye, - EyeOff, - CheckCircle, - XCircle, - ArrowLeft, - Home, - Loader2 + User, Mail, Phone, Lock, Eye, EyeOff, + CheckCircle, XCircle, ArrowLeft, Home, Loader2, + Shield, KeyRound } from 'lucide-react'; import toast, { Toaster } from 'react-hot-toast'; +import { addCustomer, loginWithEmail, sendEmailOTP, verifyEmail } from '../../utils/api'; +import AuthService from '../../services/AuthService'; +import { CustomerType, CustomerTypeLabels } from '../../enums'; export default function TenantRegisterPage() { const router = useRouter(); + const [step, setStep] = useState(1); // 1=form, 2=OTP const [showPassword, setShowPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [formData, setFormData] = useState({ name: '', email: '', phone: '', password: '', confirmPassword: '', + customerType: CustomerType.PERSONAL, agreeTerms: false }); + + const [otpCode, setOtpCode] = useState(''); const [errors, setErrors] = useState({}); - const validateEmail = (email) => { - const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return re.test(email); - }; - - const validatePhone = (phone) => { - const re = /^(09|05)[0-9]{8}$/; - return re.test(phone); - }; + const validateEmail = (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + const validatePhone = (phone) => /^(09|05)[0-9]{8}$/.test(phone); const validateForm = () => { const newErrors = {}; + if (!formData.name) newErrors.name = 'الاسم الكامل مطلوب'; + else if (formData.name.length < 3) newErrors.name = 'الاسم يجب أن يكون 3 أحرف على الأقل'; - if (!formData.name) { - newErrors.name = 'الاسم الكامل مطلوب'; - } else if (formData.name.length < 3) { - newErrors.name = 'الاسم يجب أن يكون 3 أحرف على الأقل'; - } + if (!formData.email) newErrors.email = 'البريد الإلكتروني مطلوب'; + else if (!validateEmail(formData.email)) newErrors.email = 'البريد الإلكتروني غير صالح'; - if (!formData.email) { - newErrors.email = 'البريد الإلكتروني مطلوب'; - } else if (!validateEmail(formData.email)) { - newErrors.email = 'البريد الإلكتروني غير صالح'; - } + if (!formData.phone) newErrors.phone = 'رقم الهاتف مطلوب'; + else if (!validatePhone(formData.phone)) newErrors.phone = 'رقم الهاتف غير صالح (يجب أن يبدأ 09 أو 05)'; - if (!formData.phone) { - newErrors.phone = 'رقم الهاتف مطلوب'; - } else if (!validatePhone(formData.phone)) { - newErrors.phone = 'رقم الهاتف غير صالح (يجب أن يبدأ 09 أو 05)'; - } + if (!formData.password) newErrors.password = 'كلمة المرور مطلوبة'; + else if (formData.password.length < 6) newErrors.password = 'كلمة المرور يجب أن تكون 6 أحرف على الأقل'; - if (!formData.password) { - newErrors.password = 'كلمة المرور مطلوبة'; - } else if (formData.password.length < 6) { - newErrors.password = 'كلمة المرور يجب أن تكون 6 أحرف على الأقل'; - } - - if (formData.password !== formData.confirmPassword) { - newErrors.confirmPassword = 'كلمات المرور غير متطابقة'; - } + if (formData.password !== formData.confirmPassword) newErrors.confirmPassword = 'كلمات المرور غير متطابقة'; setErrors(newErrors); return Object.keys(newErrors).length === 0; }; + // ─── Main signup handler ─── const handleSubmit = async (e) => { e.preventDefault(); @@ -86,32 +65,130 @@ export default function TenantRegisterPage() { toast.error('يرجى تصحيح الأخطاء في النموذج'); return; } - if (!formData.agreeTerms) { toast.error('يجب الموافقة على الشروط والأحكام'); return; } setIsLoading(true); + console.log('[CustomerRegister] Submitting customer registration...'); - setTimeout(() => { + const payload = { + name: formData.name, + email: formData.email, + phoneNumber: formData.phone, + password: formData.password, + customerType: formData.customerType, + }; + + try { + const res = await addCustomer(payload); + console.log('[CustomerRegister] addCustomer response:', res); + + if (res.status === 200 || res.ok) { + // ── Store temp token for OTP flow ── + const tempToken = res.data; + if (tempToken) { + AuthService.addToken(tempToken); + console.log('[CustomerRegister] Temp token stored for OTP'); + } + + const apiMessage = res.message || res.data?.message; + toast.success(apiMessage || 'تم إنشاء الحساب! يرجى التحقق من بريدك الإلكتروني', { + duration: 4000, + }); + + // ── Auto-login to trigger OTP ── + console.log('[CustomerRegister] Auto-login to send OTP...'); + const loginRes = await loginWithEmail(formData.email, formData.password); + console.log('[CustomerRegister] login response:', loginRes); + + if (loginRes.status === 206) { + // OTP sent — move to OTP step + const otpToken = loginRes.data; + if (otpToken) { + AuthService.addToken(otpToken); + console.log('[CustomerRegister] OTP token stored'); + } + const loginMsg = loginRes.message || loginRes.data?.message; + toast(loginMsg || 'تم إرسال رمز التحقق إلى بريدك الإلكتروني', { icon: '📧' }); + setStep(2); + } else if (loginRes.status === 200) { + // Direct login success (no OTP needed) + const loginToken = loginRes.data; + if (loginToken) { + AuthService.addToken(loginToken); + } + toast.success(loginRes.message || 'تم تسجيل الدخول بنجاح!'); + router.push('/'); + } + } else { + // Registration failed + const errMsg = res.message || res.data?.message || 'فشل في إنشاء الحساب'; + console.error('[CustomerRegister] Registration failed:', errMsg); + toast.error(errMsg); + } + } catch (err) { + console.error('[CustomerRegister] Error:', err); + toast.error(err.message || 'حدث خطأ أثناء التسجيل'); + } finally { setIsLoading(false); - toast.success('تم إنشاء الحساب بنجاح!', { - style: { background: '#dcfce7', color: '#166534' }, - duration: 3000 - }); + } + }; - localStorage.setItem('user', JSON.stringify({ - name: formData.name, - email: formData.email, - role: 'tenant', - avatar: formData.name.charAt(0).toUpperCase() - })); + // ─── OTP verification handler ─── + const handleVerifyOTP = async () => { + if (!otpCode || otpCode.length < 4) { + toast.error('يرجى إدخال رمز التحقق'); + return; + } - setTimeout(() => { - router.push('/'); - }, 1500); - }, 2000); + setIsLoading(true); + console.log('[CustomerRegister] Verifying OTP:', otpCode); + + try { + const res = await verifyEmail(otpCode); + console.log('[CustomerRegister] VerifyEmail response:', res); + + if (res.status === 200) { + // ── Verified! Remove temp token, redirect to login ── + AuthService.deleteToken(); + console.log('[CustomerRegister] Temp token removed after verification'); + + toast.success(res.message || 'تم التحقق من البريد الإلكتروني بنجاح!', { + duration: 3000, + }); + + setTimeout(() => { + router.push('/login'); + }, 1500); + } else { + const errMsg = res.message || res.data?.message || 'رمز التحقق غير صحيح'; + console.error('[CustomerRegister] Verification failed:', errMsg); + toast.error(errMsg); + } + } catch (err) { + console.error('[CustomerRegister] Verify error:', err); + toast.error(err.message || 'حدث خطأ أثناء التحقق'); + } finally { + setIsLoading(false); + } + }; + + // ─── Resend OTP ─── + const handleResendOTP = async () => { + setIsLoading(true); + console.log('[CustomerRegister] Resending email OTP...'); + + try { + await sendEmailOTP(); + toast.success('تم إرسال رمز تحقق جديد'); + } catch (err) { + console.error('[CustomerRegister] Resend OTP error:', err); + toast.error('فشل في إرسال الرمز'); + } finally { + setIsLoading(false); + } }; const fadeInUp = { @@ -121,37 +198,25 @@ export default function TenantRegisterPage() { }; const staggerContainer = { - animate: { - transition: { - staggerChildren: 0.1 - } - } + animate: { transition: { staggerChildren: 0.1 } } }; return (
+ {/* Background blobs */}
{[...Array(20)].map((_, i) => ( ))}
@@ -162,45 +227,40 @@ export default function TenantRegisterPage() { transition={{ duration: 0.5 }} className="relative z-10 w-full max-w-md" > - - - - - + {/* Back link */} + + + العودة + {/* Progress */} +
+ {[1, 2].map((s) => ( + = s ? 'bg-blue-500' : 'bg-gray-700'}`} animate={{ scaleX: step >= s ? 1 : 0.5 }} /> + ))} +
+
+ {/* Header */}
- - - + + - + {step === 2 ? : } -

إنشاء حساب مستأجر

-

انضم إلينا وابحث عن منزل أحلامك

+

+ {step === 1 ? 'إنشاء حساب مستأجر' : 'التحقق من البريد'} +

+

+ {step === 1 ? 'انضم إلينا وابحث عن منزل أحلامك' : 'أدخل رمز التحقق المرسل إلى بريدك'} +

@@ -209,230 +269,187 @@ export default function TenantRegisterPage() { variants={staggerContainer} initial="initial" animate="animate" - onSubmit={handleSubmit} + onSubmit={step === 1 ? handleSubmit : (e) => { e.preventDefault(); handleVerifyOTP(); }} className="space-y-6" > - - -
-
- -
- { - setFormData({...formData, name: e.target.value}); - setErrors({...errors, name: null}); - }} - className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${ - errors.name ? 'border-red-500' : 'border-gray-700' - }`} - placeholder="أدخل اسمك الكامل" - /> -
- {errors.name && ( -

{errors.name}

- )} -
+ {/* ─── STEP 1: Form fields ─── */} + {step === 1 && ( + <> + + +
+
+ +
+ { setFormData({...formData, name: e.target.value}); setErrors({...errors, name: null}); }} + className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.name ? 'border-red-500' : 'border-gray-700'}`} + placeholder="أدخل اسمك الكامل" /> +
+ {errors.name &&

{errors.name}

} +
- - -
-
- -
- { - setFormData({...formData, email: e.target.value}); - setErrors({...errors, email: null}); - }} - className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${ - errors.email ? 'border-red-500' : 'border-gray-700' - }`} - placeholder="أدخل بريدك الإلكتروني" - /> -
- {errors.email && ( -

{errors.email}

- )} -
+ + +
+
+ +
+ { setFormData({...formData, email: e.target.value}); setErrors({...errors, email: null}); }} + className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.email ? 'border-red-500' : 'border-gray-700'}`} + placeholder="أدخل بريدك الإلكتروني" /> +
+ {errors.email &&

{errors.email}

} +
- - -
-
- -
- { - setFormData({...formData, phone: e.target.value}); - setErrors({...errors, phone: null}); - }} - className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${ - errors.phone ? 'border-red-500' : 'border-gray-700' - }`} - placeholder="أدخل رقم هاتفك" - /> -
- {errors.phone && ( -

{errors.phone}

- )} -
+ + +
+
+ +
+ { setFormData({...formData, phone: e.target.value}); setErrors({...errors, phone: null}); }} + className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.phone ? 'border-red-500' : 'border-gray-700'}`} + placeholder="أدخل رقم هاتفك" /> +
+ {errors.phone &&

{errors.phone}

} +
- - -
-
- -
- { - setFormData({...formData, password: e.target.value}); - setErrors({...errors, password: null}); - }} - className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${ - errors.password ? 'border-red-500' : 'border-gray-700' - }`} - placeholder="أدخل كلمة المرور" - /> - -
- {errors.password && ( -

{errors.password}

- )} -
+ {/* Customer Type */} + + + +

المحدد: {CustomerTypeLabels[formData.customerType]}

+

[Console] customerType = {formData.customerType}

+
- - -
-
- -
- { - setFormData({...formData, confirmPassword: e.target.value}); - setErrors({...errors, confirmPassword: null}); - }} - className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${ - errors.confirmPassword ? 'border-red-500' : 'border-gray-700' - }`} - placeholder="أعد إدخال كلمة المرور" - /> - - {formData.confirmPassword && ( -
- {formData.password === formData.confirmPassword ? ( - - ) : ( - + + +
+
+ +
+ { setFormData({...formData, password: e.target.value}); setErrors({...errors, password: null}); }} + className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.password ? 'border-red-500' : 'border-gray-700'}`} + placeholder="أدخل كلمة المرور" /> + +
+ {errors.password &&

{errors.password}

} +
+ + + +
+
+ +
+ { setFormData({...formData, confirmPassword: e.target.value}); setErrors({...errors, confirmPassword: null}); }} + className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.confirmPassword ? 'border-red-500' : 'border-gray-700'}`} + placeholder="أعد إدخال كلمة المرور" /> + + {formData.confirmPassword && ( +
+ {formData.password === formData.confirmPassword ? : } +
)}
- )} -
- {errors.confirmPassword && ( -

{errors.confirmPassword}

- )} - + {errors.confirmPassword &&

{errors.confirmPassword}

} + - - setFormData({...formData, agreeTerms: e.target.checked})} - className="w-4 h-4 rounded border-gray-600 bg-white/5 text-blue-500 focus:ring-blue-500 focus:ring-offset-0" - required - /> - - + + setFormData({...formData, agreeTerms: e.target.checked})} + className="w-4 h-4 rounded border-gray-600 bg-white/5 text-blue-500 focus:ring-blue-500 focus:ring-offset-0" required /> + + + + )} - - {isLoading ? ( -
- - جاري إنشاء الحساب... + {/* ─── STEP 2: OTP ─── */} + {step === 2 && ( + +
+ +

تم إرسال رمز التحقق إلى

+

{formData.email}

- ) : ( - 'إنشاء حساب' - )} - - - لديك حساب بالفعل؟{' '} - - تسجيل الدخول - - +
+ +
+
+ +
+ setOtpCode(e.target.value)} + className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 text-center tracking-[0.5em] text-xl transition-all" + placeholder="------" /> +
+
+ + +
+ )} + + {/* ─── Navigation Buttons ─── */} + + {step === 1 && ( + <> + + + + )} + {step === 2 && ( + + )} + + + {step === 1 && ( + + لديك حساب بالفعل؟{' '} + تسجيل الدخول + + )}
); -} \ No newline at end of file +} diff --git a/app/services/AuthService.js b/app/services/AuthService.js new file mode 100644 index 0000000..d572bde --- /dev/null +++ b/app/services/AuthService.js @@ -0,0 +1,51 @@ +/** + * AuthService + * Manages authentication tokens securely using localStorage. + * + * Methods: + * addToken(token) — store JWT token + * getToken() — retrieve JWT token + * deleteToken() — remove JWT token + * + * Usage: + * import AuthService from '@/app/services/AuthService'; + * AuthService.addToken(token); + * const token = AuthService.getToken(); + * AuthService.deleteToken(); + */ +const TOKEN_KEY = 'auth_token'; + +const AuthService = Object.freeze({ + /** + * Store token in localStorage + * @param {string} token — JWT string + */ + addToken(token) { + if (!token || typeof token !== 'string') { + console.error('[AuthService] addToken: invalid token', token); + return; + } + localStorage.setItem(TOKEN_KEY, token); + console.log('[AuthService] Token stored'); + }, + + /** + * Retrieve token from localStorage + * @returns {string|null} + */ + getToken() { + const token = localStorage.getItem(TOKEN_KEY); + console.log('[AuthService] getToken:', token ? '***exists***' : null); + return token; + }, + + /** + * Remove token from localStorage + */ + deleteToken() { + localStorage.removeItem(TOKEN_KEY); + console.log('[AuthService] Token deleted'); + }, +}); + +export default AuthService; diff --git a/app/utils/api.js b/app/utils/api.js index 01c3485..bdabfe6 100644 --- a/app/utils/api.js +++ b/app/utils/api.js @@ -1,7 +1,12 @@ +import AuthService from '../services/AuthService'; + const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://45.93.137.91/api'; +/** + * Generic API fetch — attaches auth token, unwraps { data } envelope + */ async function apiFetch(endpoint, options = {}) { - const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null; + const token = AuthService.getToken(); const headers = { 'Content-Type': 'application/json', @@ -9,7 +14,7 @@ async function apiFetch(endpoint, options = {}) { ...options.headers, }; - console.log('[API] Request:', `${API_BASE}${endpoint}`, options.method || 'GET'); + console.log('[API] Request:', options.method || 'GET', `${API_BASE}${endpoint}`); const res = await fetch(`${API_BASE}${endpoint}`, { ...options, @@ -38,7 +43,9 @@ async function apiFetch(endpoint, options = {}) { } } -// Raw fetch for auth (no token, returns full response for status code handling) +/** + * Auth fetch — no token attached, returns full { status, data, ok } for status-code handling + */ async function authFetch(endpoint, body) { console.log('[Auth] Request:', `${API_BASE}${endpoint}`); @@ -61,7 +68,10 @@ async function authFetch(endpoint, body) { data = text; } - return { status: res.status, data, ok: res.ok || res.status === 206 }; + // Build message from response for toast display + const message = (typeof data === 'object' && data?.message) ? data.message : null; + + return { status: res.status, data, ok: res.ok || res.status === 206, message }; } // ─── Rent Properties ─── @@ -143,9 +153,32 @@ export async function getTerms() { return apiFetch('/Terms/GetTerms'); } -// ─── Auth ─── +// ─── Auth: Registration ─── + +/** + * Register a new owner + * @param {Object} data — { name, email, phoneNumber, whatsAppNumber, password, ownerType } + * @returns {Promise<{status, data, ok, message}>} + */ +export async function addOwner(data) { + console.log('[Auth] Registering owner:', data.email); + return authFetch('/Owner/Add', data); +} + +/** + * Register a new customer/tenant + * @param {Object} data — { name, email, phoneNumber, password, customerType } + * @returns {Promise<{status, data, ok, message}>} + */ +export async function addCustomer(data) { + console.log('[Auth] Registering customer:', data.email); + return authFetch('/Customer/Add', data); +} + +// ─── Auth: Login ─── export async function loginWithEmail(credential, password) { + console.log('[Auth] Login with email:', credential); return authFetch('/Auth/LogInWithEmail', { credential, password, @@ -155,6 +188,7 @@ export async function loginWithEmail(credential, password) { } export async function loginWithPhone(credential, password) { + console.log('[Auth] Login with phone:', credential); return authFetch('/Auth/LogInWithPhoneNumber', { credential, password, @@ -163,6 +197,8 @@ export async function loginWithPhone(credential, password) { }); } +// ─── Auth: OTP ─── + export async function sendEmailOTP() { console.log('[Auth] Sending email OTP...'); return apiFetch('/Auth/SendEmailOTP', { method: 'POST' }); @@ -183,7 +219,8 @@ export async function verifyPhone(code) { return authFetch(`/Auth/VerifyPhoneNumber?code=${encodeURIComponent(code)}`, {}); } -// Helpers +// ─── Helpers ─── + export function isEmail(value) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); } diff --git a/app/utils/constants.js b/app/utils/constants.js index d4b2a23..f6c640d 100644 --- a/app/utils/constants.js +++ b/app/utils/constants.js @@ -1,41 +1,71 @@ -export const PROPERTY_STATUS = { - AVAILABLE: 'available', - BOOKED: 'booked', - MAINTENANCE: 'maintenance' -}; +/** + * Constants — re-exports from enums for backward compatibility + * + * New code should import directly from: + * import { BuildingType, BookingStatus, City, ... } from '@/app/enums'; + * + * Old imports from '@/app/utils/constants' continue to work. + */ -export const BOOKING_STATUS = { - PENDING: 'pending', - OWNER_APPROVED: 'owner_approved', - ADMIN_APPROVED: 'admin_approved', - REJECTED: 'rejected', - ACTIVE: 'active', - COMPLETED: 'completed', - CANCELLED: 'cancelled' -}; +// Re-export all enums +export { + BuildingType, + BuildingTypeLabels, + BuildingTypeKeys, + BuildingTypeByKey, +} from '../enums/BuildingType'; -export const COMMISSION_TYPE = { - FROM_OWNER: 'from_owner', - FROM_TENANT: 'from_tenant', - FROM_BOTH: 'from_both' -}; +export { + PropertyStatus, + PropertyStatusLabels, + PropertyStatusKeys, + PropertyStatusByKey, +} from '../enums/PropertyStatus'; -export const IDENTITY_TYPE = { - SYRIAN: 'syrian', - PASSPORT: 'passport' -}; +export { + BookingStatus, + BookingStatusLabels, + BookingStatusColors, +} from '../enums/BookingStatus'; -export const PAYMENT_METHOD = { +export { + CommissionType, + CommissionTypeLabels, +} from '../enums/CommissionType'; + +export { + IdentityType, + IdentityTypeLabels, + IdentityTypeFlags, +} from '../enums/IdentityType'; + +export { + UserRole, + UserRoleLabels, + UserRoleColors, +} from '../enums/UserRole'; + +export { + City, + CitiesList, + extractCity, +} from '../enums/City'; + +export { LoginMethod } from '../enums/LoginMethod'; +export { OwnerType, OwnerTypeLabels } from '../enums/OwnerType'; +export { CustomerType, CustomerTypeLabels } from '../enums/CustomerType'; + +// ─── Legacy aliases (keep old imports working) ─── +export const PROPERTY_STATUS = PropertyStatusKeys; +export const BOOKING_STATUS = BookingStatus; +export const COMMISSION_TYPE = CommissionType; +export const IDENTITY_TYPE = IdentityType; +export const CITIES = City; + +// ─── Misc constants ─── +export const PAYMENT_METHOD = Object.freeze({ CASH: 'cash', - ELECTRONIC: 'electronic' -}; + ELECTRONIC: 'electronic', +}); -export const CITIES = { - DAMASCUS: 'damascus', - ALEPPO: 'aleppo', - HOMS: 'homs', - LATTAKIA: 'latakia', - DARAA: 'daraa' -}; - -export const DEFAULT_COMMISSION_RATE = 5; \ No newline at end of file +export const DEFAULT_COMMISSION_RATE = 5;