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 && (
<>
-
+
- {errors.name && (
- {errors.name}
- )}
+ {errors.name && {errors.name}
}
-
+
- {errors.email && (
- {errors.email}
- )}
+ {errors.email && {errors.email}
}
-
+
- {errors.phone && (
- {errors.phone}
- )}
+ {errors.phone && {errors.phone}
}
-
+
- {errors.whatsapp && (
- {errors.whatsapp}
- )}
+ {errors.whatsapp && {errors.whatsapp}
}
+
+
+ {/* Owner Type */}
+
+
+
+ المحدد: {OwnerTypeLabels[formData.ownerType]}
+ [Console] ownerType = {formData.ownerType}
-
+
- {errors.password && (
- {errors.password}
- )}
+ {errors.password && {errors.password}
}
-
+
- {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 ? (
-
-
@@ -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 ? (
-
- {
- e.stopPropagation();
- setIdImages(prev => ({...prev, back: null}));
- setIdImagePreviews(prev => ({...prev, back: ''}));
- }}
- className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center hover:bg-red-600"
- >
+
+ { e.stopPropagation(); setIdImages(prev => ({...prev, back: null})); setIdImagePreviews(prev => ({...prev, back: ''})); }}
+ className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center hover:bg-red-600">
@@ -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 && (
<>
- router.push('/auth/choose-role')}
- className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors"
- >
- إلغاء
-
-
- التالي
-
+ router.push('/auth/choose-role')}
+ className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors">إلغاء
+ التالي
>
- ) : (
+ )}
+ {step === 2 && (
<>
- setStep(1)}
- className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors"
- >
- السابق
-
-
+ setStep(1)}
+ className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors">السابق
+
{isLoading ? (
-
- جاري التسجيل...
+ جاري التسجيل...
- ) : (
- 'إنشاء حساب'
- )}
+ ) : 'إنشاء حساب'}
>
)}
+ {step === 3 && (
+
+ {isLoading ? (
+
+ جاري التحقق...
+
+ ) : 'تحقق من الرمز'}
+
+ )}
@@ -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="أدخل كلمة المرور"
- />
-
setShowPassword(!showPassword)}
- className="absolute inset-y-0 left-0 pl-3 flex items-center"
- >
- {showPassword ? (
-
- ) : (
-
- )}
-
-
- {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="أعد إدخال كلمة المرور"
- />
-
setShowConfirmPassword(!showConfirmPassword)}
- className="absolute inset-y-0 left-0 pl-3 flex items-center"
- >
- {showConfirmPassword ? (
-
- ) : (
-
- )}
-
- {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="أدخل كلمة المرور" />
+
setShowPassword(!showPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
+ {showPassword ? : }
+
+
+ {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="أعد إدخال كلمة المرور" />
+
setShowConfirmPassword(!showConfirmPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
+ {showConfirmPassword ? : }
+
+ {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 && (
+ <>
+ router.push('/auth/choose-role')}
+ className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors">إلغاء
+
+ {isLoading ? (
+
+ جاري التسجيل...
+
+ ) : 'إنشاء حساب'}
+
+ >
+ )}
+ {step === 2 && (
+
+ {isLoading ? (
+
+ جاري التحقق...
+
+ ) : 'تحقق من الرمز'}
+
+ )}
+
+
+ {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;