Compare commits
5 Commits
c5d9ad7b70
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 61e527fab3 | |||
| d0b626ac8c | |||
| 792bed6250 | |||
| bf45a48504 | |||
| eaa4206b0b |
@ -1,40 +1,40 @@
|
|||||||
# name: Build frontend
|
name: Build frontend
|
||||||
# on:
|
on:
|
||||||
# push:
|
push:
|
||||||
# branches:
|
branches:
|
||||||
# - main
|
- main
|
||||||
# jobs:
|
jobs:
|
||||||
# build:
|
build:
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# steps:
|
steps:
|
||||||
# - name: Clone repository
|
- name: Clone repository
|
||||||
# uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
# with:
|
with:
|
||||||
# repository: Rahaf/SweetHome
|
repository: Rahaf/SweetHome
|
||||||
# github-server-url: http://45.93.137.91:3000
|
github-server-url: http://45.93.137.91:3000
|
||||||
|
|
||||||
# - name: Stopping server
|
- name: Stopping server
|
||||||
# run: sudo systemctl stop sweetHome
|
run: sudo systemctl stop sweetHome
|
||||||
|
|
||||||
# - name: Copy repository to output file
|
- name: Copy repository to output file
|
||||||
# run: |
|
run: |
|
||||||
# sudo cp -r $GITHUB_WORKSPACE/* /opt/sweetHome/
|
sudo cp -r $GITHUB_WORKSPACE/* /opt/sweetHome/
|
||||||
# sudo chown -R $(whoami) /opt/sweetHome
|
sudo chown -R $(whoami) /opt/sweetHome
|
||||||
|
|
||||||
# - name: Setup Node.js 22.x
|
- name: Setup Node.js 22.x
|
||||||
# uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
# with:
|
with:
|
||||||
# node-version: 22.x
|
node-version: 22.x
|
||||||
|
|
||||||
# - name: Install dependencies
|
- name: Install dependencies
|
||||||
# working-directory: /opt/sweetHome
|
working-directory: /opt/sweetHome
|
||||||
# run: npm install
|
run: npm install
|
||||||
|
|
||||||
# - name: Build next project
|
- name: Build next project
|
||||||
# working-directory: /opt/sweetHome
|
working-directory: /opt/sweetHome
|
||||||
# run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
# - name: Starting the server
|
- name: Starting the server
|
||||||
# run: |
|
run: |
|
||||||
# sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
# sudo systemctl start sweetHome
|
sudo systemctl start sweetHome
|
||||||
|
|||||||
@ -91,7 +91,6 @@ export default function NotificationHandler() {
|
|||||||
|
|
||||||
// This MUST be synchronous from a user gesture
|
// This MUST be synchronous from a user gesture
|
||||||
const permission = await Notification.requestPermission();
|
const permission = await Notification.requestPermission();
|
||||||
console.log("[FCM] Permission result:", permission);
|
|
||||||
|
|
||||||
if (permission === "granted") {
|
if (permission === "granted") {
|
||||||
await setupFCM();
|
await setupFCM();
|
||||||
@ -149,4 +148,4 @@ export default function NotificationHandler() {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -84,24 +84,15 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const loginFn = loginMethod === "email" ? loginWithEmail : loginWithPhone;
|
const loginFn = loginMethod === "email" ? loginWithEmail : loginWithPhone;
|
||||||
console.log(
|
|
||||||
"[Login] Attempting login via",
|
|
||||||
loginMethod,
|
|
||||||
":",
|
|
||||||
formData.credential,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await loginFn(formData.credential, formData.password);
|
const result = await loginFn(formData.credential, formData.password);
|
||||||
|
|
||||||
console.log("[Login] Response status:", result.status);
|
|
||||||
|
|
||||||
if (result.status === 200) {
|
if (result.status === 200) {
|
||||||
const token =
|
const token =
|
||||||
typeof result.data === "string"
|
typeof result.data === "string"
|
||||||
? result.data
|
? result.data
|
||||||
: result.data?.token || result.data?.accessToken;
|
: result.data?.token || result.data?.accessToken;
|
||||||
AuthService.addToken(token);
|
AuthService.addToken(token);
|
||||||
console.log("[Login] Token stored");
|
|
||||||
|
|
||||||
// Fetch user profile to get full name
|
// Fetch user profile to get full name
|
||||||
const authUser = AuthService.getUser();
|
const authUser = AuthService.getUser();
|
||||||
@ -119,7 +110,6 @@ export default function LoginPage() {
|
|||||||
email: profile.email || authUser.email,
|
email: profile.email || authUser.email,
|
||||||
phone: profile.phone || profile.phoneNumber || authUser.phone,
|
phone: profile.phone || profile.phoneNumber || authUser.phone,
|
||||||
});
|
});
|
||||||
console.log("[Login] User profile cached");
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[Login] Failed to fetch profile:", err);
|
console.warn("[Login] Failed to fetch profile:", err);
|
||||||
@ -127,7 +117,6 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userRole = AuthService.isOwner() ? "owner" : "customer";
|
const userRole = AuthService.isOwner() ? "owner" : "customer";
|
||||||
console.log("[Login] User role:", userRole);
|
|
||||||
|
|
||||||
setIsSuccess(true);
|
setIsSuccess(true);
|
||||||
toast.success("تم تسجيل الدخول بنجاح!", {
|
toast.success("تم تسجيل الدخول بنجاح!", {
|
||||||
@ -138,14 +127,12 @@ export default function LoginPage() {
|
|||||||
router.push("/");
|
router.push("/");
|
||||||
}, 1500);
|
}, 1500);
|
||||||
} else if (result.status === 206) {
|
} else if (result.status === 206) {
|
||||||
console.log("[Login] 206 — OTP required");
|
|
||||||
const tempToken =
|
const tempToken =
|
||||||
typeof result.data === "string"
|
typeof result.data === "string"
|
||||||
? result.data
|
? result.data
|
||||||
: result.data?.token || result.data?.accessToken;
|
: result.data?.token || result.data?.accessToken;
|
||||||
if (tempToken) {
|
if (tempToken) {
|
||||||
AuthService.addToken(tempToken);
|
AuthService.addToken(tempToken);
|
||||||
console.log("[Login] Temp token stored for OTP");
|
|
||||||
}
|
}
|
||||||
toast("يرجى إدخال رمز التحقق", {
|
toast("يرجى إدخال رمز التحقق", {
|
||||||
icon: "🔐",
|
icon: "🔐",
|
||||||
@ -159,7 +146,6 @@ export default function LoginPage() {
|
|||||||
} else {
|
} else {
|
||||||
await sendPhoneOTP();
|
await sendPhoneOTP();
|
||||||
}
|
}
|
||||||
console.log("[Login] OTP sent successfully");
|
|
||||||
} catch (otpErr) {
|
} catch (otpErr) {
|
||||||
console.warn("[Login] OTP send failed, proceeding anyway:", otpErr);
|
console.warn("[Login] OTP send failed, proceeding anyway:", otpErr);
|
||||||
}
|
}
|
||||||
@ -197,10 +183,8 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const verifyFn = loginMethod === "email" ? verifyEmail : verifyPhone;
|
const verifyFn = loginMethod === "email" ? verifyEmail : verifyPhone;
|
||||||
console.log("[OTP] Verifying code:", otpCode);
|
|
||||||
|
|
||||||
const result = await verifyFn(otpCode);
|
const result = await verifyFn(otpCode);
|
||||||
console.log("[OTP] Verify response status:", result.status);
|
|
||||||
|
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
const finalToken =
|
const finalToken =
|
||||||
@ -209,7 +193,6 @@ export default function LoginPage() {
|
|||||||
: result.data?.token || result.data?.accessToken;
|
: result.data?.token || result.data?.accessToken;
|
||||||
if (finalToken && typeof finalToken === "string") {
|
if (finalToken && typeof finalToken === "string") {
|
||||||
AuthService.addToken(finalToken);
|
AuthService.addToken(finalToken);
|
||||||
console.log("[OTP] Final token stored");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSuccess(true);
|
setIsSuccess(true);
|
||||||
@ -218,7 +201,6 @@ export default function LoginPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log("[OTP] Redirecting to home");
|
|
||||||
router.push("/");
|
router.push("/");
|
||||||
}, 1500);
|
}, 1500);
|
||||||
} else {
|
} else {
|
||||||
@ -235,7 +217,6 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
const resendOTP = async () => {
|
const resendOTP = async () => {
|
||||||
try {
|
try {
|
||||||
console.log("[OTP] Resending OTP via", loginMethod);
|
|
||||||
if (loginMethod === "email") {
|
if (loginMethod === "email") {
|
||||||
await sendEmailOTP();
|
await sendEmailOTP();
|
||||||
} else {
|
} else {
|
||||||
@ -677,7 +658,6 @@ export default function LoginPage() {
|
|||||||
setStep("login");
|
setStep("login");
|
||||||
setOtpCode("");
|
setOtpCode("");
|
||||||
setOtpError("");
|
setOtpError("");
|
||||||
console.log("[OTP] Going back to login");
|
|
||||||
}}
|
}}
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
>
|
>
|
||||||
@ -732,4 +712,4 @@ export default function LoginPage() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -695,7 +695,6 @@ export default function OwnerBookingsPage() {
|
|||||||
>
|
>
|
||||||
<OwnerBookingCalendar
|
<OwnerBookingCalendar
|
||||||
property={{ bookings }}
|
property={{ bookings }}
|
||||||
onDateSelect={(date) => console.log('Date selected:', date)}
|
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -234,7 +234,6 @@ export default function AddPropertyPage() {
|
|||||||
getCurrencies().then((data) => {
|
getCurrencies().then((data) => {
|
||||||
if (Array.isArray(data) && data.length > 0) {
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
setCurrencies(data);
|
setCurrencies(data);
|
||||||
console.log('[AddProperty] Currencies loaded:', data);
|
|
||||||
}
|
}
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.warn('[AddProperty] Failed to load currencies:', err);
|
console.warn('[AddProperty] Failed to load currencies:', err);
|
||||||
@ -382,7 +381,6 @@ const handleMapClick = async (coords) => {
|
|||||||
|
|
||||||
const handleImageUpload = async (files) => {
|
const handleImageUpload = async (files) => {
|
||||||
const newImages = Array.from(files);
|
const newImages = Array.from(files);
|
||||||
console.log('[AddProperty] handleImageUpload called with', newImages.length, 'files');
|
|
||||||
|
|
||||||
if (formData.images.length + newImages.length > 10) {
|
if (formData.images.length + newImages.length > 10) {
|
||||||
toast.error('يمكنك رفع 10 صور كحد أقصى');
|
toast.error('يمكنك رفع 10 صور كحد أقصى');
|
||||||
@ -416,7 +414,6 @@ const handleMapClick = async (coords) => {
|
|||||||
try {
|
try {
|
||||||
const path = await uploadPicture(file);
|
const path = await uploadPicture(file);
|
||||||
setUploadedImagePaths(prev => [...prev, path]);
|
setUploadedImagePaths(prev => [...prev, path]);
|
||||||
console.log('[AddProperty] Image uploaded:', path);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[AddProperty] Image upload failed:', err);
|
console.error('[AddProperty] Image upload failed:', err);
|
||||||
toast.error('فشل رفع الصورة: ' + file.name);
|
toast.error('فشل رفع الصورة: ' + file.name);
|
||||||
@ -675,9 +672,7 @@ const handleMapClick = async (coords) => {
|
|||||||
price: parseFloat(formData.salePrice) || 0,
|
price: parseFloat(formData.salePrice) || 0,
|
||||||
currencyId: selectedCurrencyId,
|
currencyId: selectedCurrencyId,
|
||||||
};
|
};
|
||||||
console.log('[AddProperty] Sale payload:', JSON.stringify(payload, null, 2));
|
|
||||||
const res = await addSaleProperty(payload);
|
const res = await addSaleProperty(payload);
|
||||||
console.log('[AddProperty] Sale API response:', res);
|
|
||||||
toast.success('تم إضافة عقار للبيع بنجاح!');
|
toast.success('تم إضافة عقار للبيع بنجاح!');
|
||||||
} else {
|
} else {
|
||||||
const rentTypeMap = { daily: RentType.DAILY, monthly: RentType.MONTHLY, both: RentType.MONTHLY };
|
const rentTypeMap = { daily: RentType.DAILY, monthly: RentType.MONTHLY, both: RentType.MONTHLY };
|
||||||
@ -692,9 +687,7 @@ const handleMapClick = async (coords) => {
|
|||||||
type: formData.furnished ? RentPropertyType.FURNISHED : RentPropertyType.UNFURNISHED,
|
type: formData.furnished ? RentPropertyType.FURNISHED : RentPropertyType.UNFURNISHED,
|
||||||
allowedPaymentPeriod: formData.allowedPaymentPeriod || '',
|
allowedPaymentPeriod: formData.allowedPaymentPeriod || '',
|
||||||
};
|
};
|
||||||
console.log('[AddProperty] Rent payload:', JSON.stringify(payload, null, 2));
|
|
||||||
const res = await addRentProperty(payload);
|
const res = await addRentProperty(payload);
|
||||||
console.log('[AddProperty] Rent API response:', res);
|
|
||||||
toast.success('تم إضافة عقار للإيجار بنجاح!');
|
toast.success('تم إضافة عقار للإيجار بنجاح!');
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@ -1191,8 +1191,6 @@ export default function OwnerPropertiesPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log("[OwnerProperties] Fetching listings for user:", userId);
|
|
||||||
|
|
||||||
const [rentData, saleData] = await Promise.allSettled([
|
const [rentData, saleData] = await Promise.allSettled([
|
||||||
getMyRentListings(),
|
getMyRentListings(),
|
||||||
getMySaleListings(),
|
getMySaleListings(),
|
||||||
@ -1215,13 +1213,6 @@ export default function OwnerPropertiesPage() {
|
|||||||
: []
|
: []
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
console.log(
|
|
||||||
"[OwnerProperties] Rent:",
|
|
||||||
rentList.length,
|
|
||||||
"Sale:",
|
|
||||||
saleList.length,
|
|
||||||
);
|
|
||||||
|
|
||||||
const normalizeServices = (details) => {
|
const normalizeServices = (details) => {
|
||||||
const rawServices = details.services || {};
|
const rawServices = details.services || {};
|
||||||
const serviceList = Array.isArray(rawServices)
|
const serviceList = Array.isArray(rawServices)
|
||||||
|
|||||||
@ -459,9 +459,7 @@ export default function HomePage() {
|
|||||||
lng: p.location.lng,
|
lng: p.location.lng,
|
||||||
address: p.location.address
|
address: p.location.address
|
||||||
}))}
|
}))}
|
||||||
onPropertyClick={(property) => {
|
onPropertyClick={() => {}}
|
||||||
console.log('Property clicked:', property);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-[400px] flex flex-col items-center justify-center bg-gray-50">
|
<div className="h-[400px] flex flex-col items-center justify-center bg-gray-50">
|
||||||
@ -622,4 +620,4 @@ export default function HomePage() {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -105,7 +105,6 @@
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@ -118,9 +117,108 @@ const ENDPOINTS = {
|
|||||||
en: '/Configuration/GetENPrivacyPolicy',
|
en: '/Configuration/GetENPrivacyPolicy',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function tryParseJson(value) {
|
||||||
|
let current = value;
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i += 1) {
|
||||||
|
if (typeof current !== 'string') break;
|
||||||
|
|
||||||
|
const trimmed = current.trim();
|
||||||
|
if (!trimmed) return trimmed;
|
||||||
|
|
||||||
|
try {
|
||||||
|
current = JSON.parse(trimmed);
|
||||||
|
} catch {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeItem(item, index) {
|
||||||
|
if (!item || typeof item !== 'object') return null;
|
||||||
|
|
||||||
|
const title =
|
||||||
|
typeof item.title === 'string' ? item.title.trim() : '';
|
||||||
|
const description =
|
||||||
|
typeof item.description === 'string' ? item.description.trim() : '';
|
||||||
|
|
||||||
|
if (!title && !description) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: index + 1,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePolicyContent(raw) {
|
||||||
|
const decoded = tryParseJson(raw);
|
||||||
|
|
||||||
|
if (Array.isArray(decoded)) {
|
||||||
|
const items = decoded.map((item, index) => normalizeItem(item, index)).filter(Boolean);
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
rawText: typeof raw === 'string' ? raw.trim() : '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decoded && typeof decoded === 'object') {
|
||||||
|
const possibleArray = Array.isArray(decoded.items)
|
||||||
|
? decoded.items
|
||||||
|
: Array.isArray(decoded.data)
|
||||||
|
? decoded.data
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (possibleArray) {
|
||||||
|
const items = possibleArray.map((item, index) => normalizeItem(item, index)).filter(Boolean);
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
rawText: typeof raw === 'string' ? raw.trim() : '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const singleItem = normalizeItem(decoded, 0);
|
||||||
|
if (singleItem) {
|
||||||
|
return {
|
||||||
|
items: [singleItem],
|
||||||
|
rawText: typeof raw === 'string' ? raw.trim() : '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof decoded === 'string') {
|
||||||
|
const cleaned = decoded.replace(/\\r\\n/g, '\n').replace(/\\n/g, '\n').trim();
|
||||||
|
|
||||||
|
const matches = [...cleaned.matchAll(/"title"\s*:\s*"([^"]+)"\s*,\s*"description"\s*:\s*"([^"]+)"/g)];
|
||||||
|
if (matches.length > 0) {
|
||||||
|
return {
|
||||||
|
items: matches.map((match, index) => ({
|
||||||
|
id: index + 1,
|
||||||
|
title: match[1].trim(),
|
||||||
|
description: match[2].trim(),
|
||||||
|
})),
|
||||||
|
rawText: cleaned,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
rawText: cleaned,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
rawText: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default function PrivacyPage() {
|
export default function PrivacyPage() {
|
||||||
const [language, setLanguage] = useState('ar');
|
const [language, setLanguage] = useState('ar');
|
||||||
const [policyText, setPolicyText] = useState('');
|
const [policyItems, setPolicyItems] = useState([]);
|
||||||
|
const [rawPolicyText, setRawPolicyText] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
@ -146,13 +244,14 @@ export default function PrivacyPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
|
const normalized = normalizePolicyContent(text);
|
||||||
|
|
||||||
console.log('API RESPONSE:', text);
|
setPolicyItems(normalized.items);
|
||||||
|
setRawPolicyText(normalized.rawText);
|
||||||
setPolicyText(text.trim());
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.name !== 'AbortError') {
|
if (err?.name !== 'AbortError') {
|
||||||
setPolicyText('');
|
setPolicyItems([]);
|
||||||
|
setRawPolicyText('');
|
||||||
setError(
|
setError(
|
||||||
language === 'ar'
|
language === 'ar'
|
||||||
? 'تعذر تحميل النص من الخادم.'
|
? 'تعذر تحميل النص من الخادم.'
|
||||||
@ -207,9 +306,37 @@ export default function PrivacyPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
|
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
|
||||||
<div className="whitespace-pre-wrap leading-8 text-gray-800">
|
{policyItems.length > 0 ? (
|
||||||
{policyText}
|
<div className="space-y-4">
|
||||||
</div>
|
{policyItems.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className="rounded-2xl border border-gray-200 bg-gray-50 p-5"
|
||||||
|
>
|
||||||
|
{item.title && (
|
||||||
|
<div className="mb-3 flex items-start gap-3">
|
||||||
|
<div className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-amber-100 text-sm font-bold text-amber-700">
|
||||||
|
{item.id}
|
||||||
|
</div>
|
||||||
|
<h3 className="text-base font-bold text-gray-900">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{item.description && (
|
||||||
|
<p className="text-sm leading-8 text-gray-700 whitespace-pre-wrap">
|
||||||
|
{item.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="whitespace-pre-wrap leading-8 text-gray-800">
|
||||||
|
{rawPolicyText}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,29 +1,22 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Image from 'next/image';
|
|
||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
Mail,
|
Mail,
|
||||||
Phone,
|
Phone,
|
||||||
MessageCircle,
|
MessageCircle,
|
||||||
Camera,
|
Check,
|
||||||
Save,
|
|
||||||
X,
|
X,
|
||||||
CheckCircle,
|
|
||||||
AlertCircle,
|
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Building,
|
Building,
|
||||||
Home,
|
Home,
|
||||||
Calendar,
|
Calendar,
|
||||||
MapPin,
|
MapPin,
|
||||||
Edit,
|
|
||||||
Loader2,
|
Loader2,
|
||||||
Upload,
|
|
||||||
Check,
|
|
||||||
Pencil
|
Pencil
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import toast, { Toaster } from 'react-hot-toast';
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
@ -34,10 +27,10 @@ export default function ProfilePage() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
|
||||||
|
const [editingBio, setEditingBio] = useState(false);
|
||||||
const [editingField, setEditingField] = useState(null);
|
const [bioDraft, setBioDraft] = useState('');
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
@ -47,21 +40,8 @@ export default function ProfilePage() {
|
|||||||
location: '',
|
location: '',
|
||||||
joinedDate: ''
|
joinedDate: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
const [tempValues, setTempValues] = useState({});
|
|
||||||
const [avatar, setAvatar] = useState(null);
|
|
||||||
const [avatarPreview, setAvatarPreview] = useState('');
|
const [avatarPreview, setAvatarPreview] = useState('');
|
||||||
const [errors, setErrors] = useState({});
|
|
||||||
|
|
||||||
const fileInputRef = useRef(null);
|
|
||||||
const inputRefs = {
|
|
||||||
name: useRef(null),
|
|
||||||
email: useRef(null),
|
|
||||||
phone: useRef(null),
|
|
||||||
whatsapp: useRef(null),
|
|
||||||
location: useRef(null),
|
|
||||||
bio: useRef(null)
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const authUser = AuthService.getUser();
|
const authUser = AuthService.getUser();
|
||||||
@ -74,15 +54,11 @@ export default function ProfilePage() {
|
|||||||
role: AuthService.isOwner() ? 'owner' : 'customer',
|
role: AuthService.isOwner() ? 'owner' : 'customer',
|
||||||
};
|
};
|
||||||
setUser(userData);
|
setUser(userData);
|
||||||
console.log('[Profile] User from JWT:', userData);
|
|
||||||
|
|
||||||
// Fetch full profile from API using user ID (SID from JWT)
|
|
||||||
async function fetchProfile() {
|
async function fetchProfile() {
|
||||||
try {
|
try {
|
||||||
const fetchFn = userData.role === 'owner' ? getOwnerByUserId : getCustomerByUserId;
|
const fetchFn = userData.role === 'owner' ? getOwnerByUserId : getCustomerByUserId;
|
||||||
console.log('[Profile] Fetching profile via', userData.role === 'owner' ? 'Owner' : 'Customer', 'GetByUserId:', userData.id);
|
|
||||||
const profile = await fetchFn(userData.id);
|
const profile = await fetchFn(userData.id);
|
||||||
console.log('[Profile] API profile:', profile);
|
|
||||||
|
|
||||||
if (profile) {
|
if (profile) {
|
||||||
const profileData = {
|
const profileData = {
|
||||||
@ -97,16 +73,15 @@ export default function ProfilePage() {
|
|||||||
: new Date().toLocaleDateString('ar-SA', { month: 'long', year: 'numeric' }),
|
: new Date().toLocaleDateString('ar-SA', { month: 'long', year: 'numeric' }),
|
||||||
};
|
};
|
||||||
setFormData(profileData);
|
setFormData(profileData);
|
||||||
setTempValues(profileData);
|
setBioDraft(profileData.bio);
|
||||||
localStorage.setItem('userProfile', JSON.stringify(profileData));
|
localStorage.setItem('userProfile', JSON.stringify(profileData));
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[Profile] API fetch failed, falling back to JWT/localStorage:', err);
|
// Ignore API errors and fall back to local data.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to JWT + localStorage
|
|
||||||
const savedProfile = localStorage.getItem('userProfile');
|
const savedProfile = localStorage.getItem('userProfile');
|
||||||
let profileData;
|
let profileData;
|
||||||
if (savedProfile) {
|
if (savedProfile) {
|
||||||
@ -123,7 +98,7 @@ export default function ProfilePage() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
setFormData(profileData);
|
setFormData(profileData);
|
||||||
setTempValues(profileData);
|
setBioDraft(profileData.bio || '');
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,114 +113,22 @@ export default function ProfilePage() {
|
|||||||
}
|
}
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
useEffect(() => {
|
const startBioEditing = () => {
|
||||||
if (editingField && inputRefs[editingField]?.current) {
|
setBioDraft(formData.bio || '');
|
||||||
inputRefs[editingField].current.focus();
|
setEditingBio(true);
|
||||||
}
|
|
||||||
}, [editingField]);
|
|
||||||
|
|
||||||
const handleImageUpload = (file) => {
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
if (!file.type.startsWith('image/')) {
|
|
||||||
toast.error('الرجاء اختيار صورة صالحة');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.size > 2 * 1024 * 1024) {
|
|
||||||
toast.error('حجم الصورة يجب أن يكون أقل من 2 ميجابايت');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onloadend = () => {
|
|
||||||
setAvatarPreview(reader.result);
|
|
||||||
localStorage.setItem('userAvatar', reader.result);
|
|
||||||
toast.success('تم تغيير الصورة بنجاح');
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateField = (field, value) => {
|
const cancelBioEditing = () => {
|
||||||
if (field === 'email' && value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
setBioDraft(formData.bio || '');
|
||||||
return 'البريد الإلكتروني غير صالح';
|
setEditingBio(false);
|
||||||
}
|
|
||||||
if ((field === 'phone' || field === 'whatsapp') && value && !/^(09|05)[0-9]{8}$/.test(value)) {
|
|
||||||
return 'رقم الهاتف غير صالح (يجب أن يبدأ 09 أو 05)';
|
|
||||||
}
|
|
||||||
if (field === 'name' && !value?.trim()) {
|
|
||||||
return 'الاسم مطلوب';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const startEditing = (field) => {
|
const saveBio = () => {
|
||||||
setEditingField(field);
|
const updatedData = { ...formData, bio: bioDraft };
|
||||||
setTempValues(prev => ({ ...prev, [field]: formData[field] }));
|
|
||||||
setErrors(prev => ({ ...prev, [field]: null }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelEditing = () => {
|
|
||||||
setEditingField(null);
|
|
||||||
setTempValues({});
|
|
||||||
setErrors({});
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveField = (field) => {
|
|
||||||
const value = tempValues[field];
|
|
||||||
const error = validateField(field, value);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
setErrors(prev => ({ ...prev, [field]: error }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedData = { ...formData, [field]: value };
|
|
||||||
setFormData(updatedData);
|
setFormData(updatedData);
|
||||||
|
|
||||||
localStorage.setItem('userProfile', JSON.stringify(updatedData));
|
localStorage.setItem('userProfile', JSON.stringify(updatedData));
|
||||||
|
setEditingBio(false);
|
||||||
if (field === 'name') {
|
toast.success('تم تحديث نبذة عني بنجاح');
|
||||||
const updatedUser = { ...user, name: value };
|
|
||||||
setUser(updatedUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
setEditingField(null);
|
|
||||||
setTempValues({});
|
|
||||||
toast.success(`تم تحديث ${getFieldLabel(field)} بنجاح`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (e, field) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
saveField(field);
|
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
cancelEditing();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFieldLabel = (field) => {
|
|
||||||
const labels = {
|
|
||||||
name: 'الاسم',
|
|
||||||
email: 'البريد الإلكتروني',
|
|
||||||
phone: 'رقم الهاتف',
|
|
||||||
whatsapp: 'رقم الواتساب',
|
|
||||||
location: 'الموقع',
|
|
||||||
bio: 'نبذة عني'
|
|
||||||
};
|
|
||||||
return labels[field] || field;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFieldIcon = (field) => {
|
|
||||||
const icons = {
|
|
||||||
name: User,
|
|
||||||
email: Mail,
|
|
||||||
phone: Phone,
|
|
||||||
whatsapp: MessageCircle,
|
|
||||||
location: MapPin,
|
|
||||||
bio: User
|
|
||||||
};
|
|
||||||
const Icon = icons[field];
|
|
||||||
return Icon ? <Icon className="w-5 h-5 text-gray-400" /> : null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const fadeInUp = {
|
const fadeInUp = {
|
||||||
@ -268,7 +151,7 @@ export default function ProfilePage() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 py-8">
|
<div className="min-h-screen bg-gray-50 py-8">
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
|
|
||||||
<div className="container mx-auto px-4 max-w-4xl">
|
<div className="container mx-auto px-4 max-w-4xl">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: -20 }}
|
initial={{ opacity: 0, y: -20 }}
|
||||||
@ -307,8 +190,7 @@ export default function ProfilePage() {
|
|||||||
<div className="px-8 pb-8">
|
<div className="px-8 pb-8">
|
||||||
<div className="flex justify-center -mt-16 mb-6">
|
<div className="flex justify-center -mt-16 mb-6">
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<div className="w-32 h-32 rounded-full border-4 border-white bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center text-white text-4xl font-bold shadow-xl overflow-hidden cursor-pointer"
|
<div className="w-32 h-32 rounded-full border-4 border-white bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center text-white text-4xl font-bold shadow-xl overflow-hidden">
|
||||||
onClick={() => fileInputRef.current?.click()}>
|
|
||||||
{avatarPreview ? (
|
{avatarPreview ? (
|
||||||
<img
|
<img
|
||||||
src={avatarPreview}
|
src={avatarPreview}
|
||||||
@ -319,109 +201,19 @@ export default function ProfilePage() {
|
|||||||
formData.name?.charAt(0).toUpperCase() || 'U'
|
formData.name?.charAt(0).toUpperCase() || 'U'
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
|
||||||
className="absolute bottom-0 right-0 w-8 h-8 bg-amber-500 rounded-full flex items-center justify-center text-white hover:bg-amber-600 transition-colors shadow-lg"
|
|
||||||
title="تغيير الصورة"
|
|
||||||
>
|
|
||||||
<Camera className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
onChange={(e) => handleImageUpload(e.target.files?.[0])}
|
|
||||||
className="hidden"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center mb-6">
|
<div className="text-center mb-6">
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
{editingField === 'name' ? (
|
<h1 className="text-3xl font-bold text-gray-900">{formData.name}</h1>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
ref={inputRefs.name}
|
|
||||||
type="text"
|
|
||||||
value={tempValues.name || ''}
|
|
||||||
onChange={(e) => setTempValues({...tempValues, name: e.target.value})}
|
|
||||||
onKeyDown={(e) => handleKeyDown(e, 'name')}
|
|
||||||
className="text-3xl font-bold text-center border-b-2 border-amber-500 outline-none px-2 py-1 w-64"
|
|
||||||
placeholder="الاسم الكامل"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() => saveField('name')}
|
|
||||||
className="p-2 bg-green-500 text-white rounded-full hover:bg-green-600 transition-colors"
|
|
||||||
title="حفظ"
|
|
||||||
>
|
|
||||||
<Check className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={cancelEditing}
|
|
||||||
className="p-2 bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors"
|
|
||||||
title="إلغاء"
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900">{formData.name}</h1>
|
|
||||||
<button
|
|
||||||
onClick={() => startEditing('name')}
|
|
||||||
className="p-2 text-gray-400 hover:text-amber-500 transition-colors"
|
|
||||||
title="تعديل الاسم"
|
|
||||||
>
|
|
||||||
<Pencil className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{errors.name && (
|
|
||||||
<p className="text-red-500 text-sm mt-1">{errors.name}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center gap-2 text-gray-500 mt-2">
|
<div className="flex items-center justify-center gap-2 text-gray-500 mt-2">
|
||||||
<MapPin className="w-4 h-4" />
|
<MapPin className="w-4 h-4" />
|
||||||
{editingField === 'location' ? (
|
<span>{formData.location || 'الموقع غير محدد'}</span>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
ref={inputRefs.location}
|
|
||||||
type="text"
|
|
||||||
value={tempValues.location || ''}
|
|
||||||
onChange={(e) => setTempValues({...tempValues, location: e.target.value})}
|
|
||||||
onKeyDown={(e) => handleKeyDown(e, 'location')}
|
|
||||||
className="border-b border-amber-500 outline-none px-2 py-1"
|
|
||||||
placeholder="الموقع"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() => saveField('location')}
|
|
||||||
className="text-green-500 hover:text-green-600"
|
|
||||||
>
|
|
||||||
<Check className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={cancelEditing}
|
|
||||||
className="text-red-500 hover:text-red-600"
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span>{formData.location || 'الموقع غير محدد'}</span>
|
|
||||||
<button
|
|
||||||
onClick={() => startEditing('location')}
|
|
||||||
className="text-gray-400 hover:text-amber-500 transition-colors"
|
|
||||||
>
|
|
||||||
<Pencil className="w-3 h-3" />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center gap-2 text-gray-500 mt-1">
|
<div className="flex items-center justify-center gap-2 text-gray-500 mt-1">
|
||||||
<Calendar className="w-4 h-4" />
|
<Calendar className="w-4 h-4" />
|
||||||
<span>عضو منذ {formData.joinedDate}</span>
|
<span>عضو منذ {formData.joinedDate}</span>
|
||||||
@ -434,53 +226,11 @@ export default function ProfilePage() {
|
|||||||
<label className="text-sm font-medium text-gray-600">
|
<label className="text-sm font-medium text-gray-600">
|
||||||
البريد الإلكتروني
|
البريد الإلكتروني
|
||||||
</label>
|
</label>
|
||||||
{editingField !== 'email' && (
|
|
||||||
<button
|
|
||||||
onClick={() => startEditing('email')}
|
|
||||||
className="opacity-0 group-hover:opacity-100 transition-opacity text-gray-400 hover:text-amber-500"
|
|
||||||
>
|
|
||||||
<Pencil className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-gray-900">
|
||||||
{editingField === 'email' ? (
|
<Mail className="w-5 h-5 text-gray-400" />
|
||||||
<div className="space-y-2">
|
<span>{formData.email}</span>
|
||||||
<div className="flex gap-2">
|
</div>
|
||||||
<input
|
|
||||||
ref={inputRefs.email}
|
|
||||||
type="email"
|
|
||||||
value={tempValues.email || ''}
|
|
||||||
onChange={(e) => setTempValues({...tempValues, email: e.target.value})}
|
|
||||||
onKeyDown={(e) => handleKeyDown(e, 'email')}
|
|
||||||
className="flex-1 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-amber-500 focus:border-transparent"
|
|
||||||
placeholder="example@domain.com"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() => saveField('email')}
|
|
||||||
className="p-2 bg-green-500 text-white rounded-lg hover:bg-green-600"
|
|
||||||
title="حفظ"
|
|
||||||
>
|
|
||||||
<Check className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={cancelEditing}
|
|
||||||
className="p-2 bg-red-500 text-white rounded-lg hover:bg-red-600"
|
|
||||||
title="إلغاء"
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{errors.email && (
|
|
||||||
<p className="text-red-500 text-xs">{errors.email}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-2 text-gray-900">
|
|
||||||
<Mail className="w-5 h-5 text-gray-400" />
|
|
||||||
<span>{formData.email}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-50 p-4 rounded-xl group">
|
<div className="bg-gray-50 p-4 rounded-xl group">
|
||||||
@ -488,51 +238,11 @@ export default function ProfilePage() {
|
|||||||
<label className="text-sm font-medium text-gray-600">
|
<label className="text-sm font-medium text-gray-600">
|
||||||
رقم الهاتف
|
رقم الهاتف
|
||||||
</label>
|
</label>
|
||||||
{editingField !== 'phone' && (
|
|
||||||
<button
|
|
||||||
onClick={() => startEditing('phone')}
|
|
||||||
className="opacity-0 group-hover:opacity-100 transition-opacity text-gray-400 hover:text-amber-500"
|
|
||||||
>
|
|
||||||
<Pencil className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-gray-900">
|
||||||
{editingField === 'phone' ? (
|
<Phone className="w-5 h-5 text-gray-400" />
|
||||||
<div className="space-y-2">
|
<span>{formData.phone || 'غير محدد'}</span>
|
||||||
<div className="flex gap-2">
|
</div>
|
||||||
<input
|
|
||||||
ref={inputRefs.phone}
|
|
||||||
type="tel"
|
|
||||||
value={tempValues.phone || ''}
|
|
||||||
onChange={(e) => setTempValues({...tempValues, phone: e.target.value})}
|
|
||||||
onKeyDown={(e) => handleKeyDown(e, 'phone')}
|
|
||||||
className="flex-1 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-amber-500 focus:border-transparent"
|
|
||||||
placeholder="09XXXXXXXX"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() => saveField('phone')}
|
|
||||||
className="p-2 bg-green-500 text-white rounded-lg hover:bg-green-600"
|
|
||||||
>
|
|
||||||
<Check className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={cancelEditing}
|
|
||||||
className="p-2 bg-red-500 text-white rounded-lg hover:bg-red-600"
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{errors.phone && (
|
|
||||||
<p className="text-red-500 text-xs">{errors.phone}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-2 text-gray-900">
|
|
||||||
<Phone className="w-5 h-5 text-gray-400" />
|
|
||||||
<span>{formData.phone || 'غير محدد'}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-50 p-4 rounded-xl group">
|
<div className="bg-gray-50 p-4 rounded-xl group">
|
||||||
@ -540,51 +250,11 @@ export default function ProfilePage() {
|
|||||||
<label className="text-sm font-medium text-gray-600">
|
<label className="text-sm font-medium text-gray-600">
|
||||||
رقم الواتساب
|
رقم الواتساب
|
||||||
</label>
|
</label>
|
||||||
{editingField !== 'whatsapp' && (
|
|
||||||
<button
|
|
||||||
onClick={() => startEditing('whatsapp')}
|
|
||||||
className="opacity-0 group-hover:opacity-100 transition-opacity text-gray-400 hover:text-amber-500"
|
|
||||||
>
|
|
||||||
<Pencil className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-gray-900">
|
||||||
{editingField === 'whatsapp' ? (
|
<MessageCircle className="w-5 h-5 text-gray-400" />
|
||||||
<div className="space-y-2">
|
<span>{formData.whatsapp || 'غير محدد'}</span>
|
||||||
<div className="flex gap-2">
|
</div>
|
||||||
<input
|
|
||||||
ref={inputRefs.whatsapp}
|
|
||||||
type="tel"
|
|
||||||
value={tempValues.whatsapp || ''}
|
|
||||||
onChange={(e) => setTempValues({...tempValues, whatsapp: e.target.value})}
|
|
||||||
onKeyDown={(e) => handleKeyDown(e, 'whatsapp')}
|
|
||||||
className="flex-1 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-amber-500 focus:border-transparent"
|
|
||||||
placeholder="09XXXXXXXX"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() => saveField('whatsapp')}
|
|
||||||
className="p-2 bg-green-500 text-white rounded-lg hover:bg-green-600"
|
|
||||||
>
|
|
||||||
<Check className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={cancelEditing}
|
|
||||||
className="p-2 bg-red-500 text-white rounded-lg hover:bg-red-600"
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{errors.whatsapp && (
|
|
||||||
<p className="text-red-500 text-xs">{errors.whatsapp}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-2 text-gray-900">
|
|
||||||
<MessageCircle className="w-5 h-5 text-gray-400" />
|
|
||||||
<span>{formData.whatsapp || 'غير محدد'}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-50 p-4 rounded-xl">
|
<div className="bg-gray-50 p-4 rounded-xl">
|
||||||
@ -612,36 +282,36 @@ export default function ProfilePage() {
|
|||||||
<label className="text-sm font-medium text-gray-600">
|
<label className="text-sm font-medium text-gray-600">
|
||||||
نبذة عني
|
نبذة عني
|
||||||
</label>
|
</label>
|
||||||
{editingField !== 'bio' && (
|
{!editingBio && (
|
||||||
<button
|
<button
|
||||||
onClick={() => startEditing('bio')}
|
onClick={startBioEditing}
|
||||||
className="opacity-0 group-hover:opacity-100 transition-opacity text-gray-400 hover:text-amber-500"
|
className="opacity-0 group-hover:opacity-100 transition-opacity text-gray-400 hover:text-amber-500"
|
||||||
>
|
>
|
||||||
<Pencil className="w-4 h-4" />
|
<Pencil className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{editingField === 'bio' ? (
|
{editingBio ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<textarea
|
<textarea
|
||||||
ref={inputRefs.bio}
|
value={bioDraft}
|
||||||
value={tempValues.bio || ''}
|
onChange={(e) => setBioDraft(e.target.value)}
|
||||||
onChange={(e) => setTempValues({...tempValues, bio: e.target.value})}
|
|
||||||
rows="4"
|
rows="4"
|
||||||
|
autoFocus
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-amber-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-amber-500 focus:border-transparent"
|
||||||
placeholder="اكتب نبذة عن نفسك..."
|
placeholder="اكتب نبذة عن نفسك..."
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => saveField('bio')}
|
onClick={saveBio}
|
||||||
className="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 flex items-center gap-2"
|
className="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<Check className="w-4 h-4" />
|
<Check className="w-4 h-4" />
|
||||||
حفظ
|
حفظ
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={cancelEditing}
|
onClick={cancelBioEditing}
|
||||||
className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 flex items-center gap-2"
|
className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
|
|||||||
@ -616,10 +616,9 @@
|
|||||||
|
|
||||||
|
|
||||||
// app/register/tenant/page.js
|
// app/register/tenant/page.js
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@ -697,7 +696,6 @@ export default function TenantRegisterPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
console.log('[CustomerRegister] Submitting customer registration...');
|
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
firstName: formData.firstName,
|
firstName: formData.firstName,
|
||||||
@ -714,7 +712,6 @@ export default function TenantRegisterPage() {
|
|||||||
try {
|
try {
|
||||||
// لا توجد صور للمستأجر
|
// لا توجد صور للمستأجر
|
||||||
const res = await addCustomer(payload, null, null);
|
const res = await addCustomer(payload, null, null);
|
||||||
console.log('[CustomerRegister] addCustomer response:', res);
|
|
||||||
|
|
||||||
if (res.status === 200 || res.ok) {
|
if (res.status === 200 || res.ok) {
|
||||||
const tempToken = res.data;
|
const tempToken = res.data;
|
||||||
|
|||||||
@ -252,9 +252,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@ -371,7 +368,6 @@ export default function SettingsPage() {
|
|||||||
throw new Error(responseText || `فشل إرسال البلاغ (HTTP ${res.status})`);
|
throw new Error(responseText || `فشل إرسال البلاغ (HTTP ${res.status})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Send report success:', responseText);
|
|
||||||
toast.success(responseText || 'تم إرسال البلاغ بنجاح');
|
toast.success(responseText || 'تم إرسال البلاغ بنجاح');
|
||||||
setShowReportDialog(false);
|
setShowReportDialog(false);
|
||||||
setReportSubject('');
|
setReportSubject('');
|
||||||
|
|||||||
495
app/utils/api.js
495
app/utils/api.js
@ -373,99 +373,20 @@
|
|||||||
|
|
||||||
// // ─── Booking/Reservation Management ───
|
// // ─── Booking/Reservation Management ───
|
||||||
|
|
||||||
// export async function confirmDepositPayment(bookingId) {
|
import AuthService from "../services/AuthService";
|
||||||
// return apiFetch('/Reservations/ConfirmDepositPayment', {
|
const API_BASE =
|
||||||
// method: 'POST',
|
process.env.NEXT_PUBLIC_API_URL || "https://45.93.137.91.nip.io/api";
|
||||||
// body: JSON.stringify({ bookingId }),
|
const REPORT_API_BASE =
|
||||||
// });
|
process.env.NEXT_PUBLIC_REPORT_API_URL || "http://45.93.137.91/api";
|
||||||
// }
|
|
||||||
|
|
||||||
// export async function adminConfirmDeposit(reservationId, adminId, comment = null) {
|
|
||||||
// const token = AuthService.getToken();
|
|
||||||
// const endpoint = `${API_BASE}/Reservations/AdminConfirmDeposit/admin-confirm-deposit`;
|
|
||||||
// const normalizedComment =
|
|
||||||
// typeof comment === 'string' && comment.trim()
|
|
||||||
// ? comment.trim()
|
|
||||||
// : null;
|
|
||||||
// const payload = {
|
|
||||||
// reservationId,
|
|
||||||
// adminId,
|
|
||||||
// comment: normalizedComment,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// console.log('[API] AdminConfirmDeposit request', {
|
|
||||||
// method: 'PUT',
|
|
||||||
// endpoint,
|
|
||||||
// payload,
|
|
||||||
// adminIdSource: 'jwt-user-id',
|
|
||||||
// hasToken: Boolean(token),
|
|
||||||
// tokenPreview: token ? `${token.slice(0, 18)}...${token.slice(-8)}` : null,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const res = await fetch(endpoint, {
|
|
||||||
// method: 'PUT',
|
|
||||||
// headers: {
|
|
||||||
// 'Content-Type': 'application/json',
|
|
||||||
// ...(token && { Authorization: `Bearer ${token}` }),
|
|
||||||
// },
|
|
||||||
// body: JSON.stringify(payload),
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const text = await res.text();
|
|
||||||
// let data = null;
|
|
||||||
|
|
||||||
// console.log('[API] AdminConfirmDeposit raw response', {
|
|
||||||
// status: res.status,
|
|
||||||
// ok: res.ok,
|
|
||||||
// endpoint,
|
|
||||||
// rawText: text,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// data = text ? JSON.parse(text) : null;
|
|
||||||
// if (data && typeof data === 'object' && 'data' in data) {
|
|
||||||
// data = data.data;
|
|
||||||
// }
|
|
||||||
// } catch {
|
|
||||||
// data = text;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const message = typeof data === 'object' && data?.message ? data.message : null;
|
|
||||||
|
|
||||||
// console.log('[API] AdminConfirmDeposit parsed response', {
|
|
||||||
// status: res.status,
|
|
||||||
// ok: res.ok,
|
|
||||||
// message,
|
|
||||||
// data,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return { status: res.status, data, ok: res.ok, message };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export async function updateBookingStatus(bookingId, status) {
|
|
||||||
// return apiFetch('/Reservations/UpdateStatus', {
|
|
||||||
// method: 'PUT',
|
|
||||||
// body: JSON.stringify({ bookingId, status }),
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import AuthService from '../services/AuthService';
|
|
||||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api';
|
|
||||||
const REPORT_API_BASE = process.env.NEXT_PUBLIC_REPORT_API_URL || 'http://45.93.137.91/api';
|
|
||||||
|
|
||||||
|
|
||||||
function isFormData(value) {
|
function isFormData(value) {
|
||||||
return typeof FormData !== 'undefined' && value instanceof FormData;
|
return typeof FormData !== "undefined" && value instanceof FormData;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ApiBlockedError extends Error {
|
class ApiBlockedError extends Error {
|
||||||
constructor(message = 'Your account is blocked') {
|
constructor(message = "Your account is blocked") {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = 'ApiBlockedError';
|
this.name = "ApiBlockedError";
|
||||||
this.status = 451;
|
this.status = 451;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -475,8 +396,11 @@ export function isApiBlockedError(error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function redirectToBlockedPage() {
|
function redirectToBlockedPage() {
|
||||||
if (typeof window !== 'undefined' && window.location.pathname !== '/blocked') {
|
if (
|
||||||
window.location.replace('/blocked');
|
typeof window !== "undefined" &&
|
||||||
|
window.location.pathname !== "/blocked"
|
||||||
|
) {
|
||||||
|
window.location.replace("/blocked");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -488,7 +412,7 @@ function assertNotBlocked(response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildApiUrl(base, endpoint) {
|
function buildApiUrl(base, endpoint) {
|
||||||
return `${base.replace(/\/$/, '')}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`;
|
return `${base.replace(/\/$/, "")}${endpoint.startsWith("/") ? endpoint : `/${endpoint}`}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -505,32 +429,37 @@ async function apiFetch(endpoint, options = {}) {
|
|||||||
const hasBody = options.body != null;
|
const hasBody = options.body != null;
|
||||||
const bodyIsFormData = isFormData(options.body);
|
const bodyIsFormData = isFormData(options.body);
|
||||||
|
|
||||||
if (hasBody && !bodyIsFormData && !headers['Content-Type'] && !headers['content-type']) {
|
if (
|
||||||
headers['Content-Type'] = 'application/json';
|
hasBody &&
|
||||||
|
!bodyIsFormData &&
|
||||||
|
!headers["Content-Type"] &&
|
||||||
|
!headers["content-type"]
|
||||||
|
) {
|
||||||
|
headers["Content-Type"] = "application/json";
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `${API_BASE}${endpoint}`;
|
const url = `${API_BASE}${endpoint}`;
|
||||||
|
|
||||||
console.log('API Request:', url);
|
console.log("API Request:", url);
|
||||||
console.log('API Method:', options.method || 'GET');
|
console.log("API Method:", options.method || "GET");
|
||||||
console.log('API Body:', hasBody ? options.body : null);
|
console.log("API Body:", hasBody ? options.body : null);
|
||||||
|
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
...options,
|
...options,
|
||||||
headers,
|
headers,
|
||||||
body:
|
body:
|
||||||
hasBody && !bodyIsFormData && typeof options.body !== 'string'
|
hasBody && !bodyIsFormData && typeof options.body !== "string"
|
||||||
? JSON.stringify(options.body)
|
? JSON.stringify(options.body)
|
||||||
: options.body,
|
: options.body,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('API Response Status:', res.status);
|
console.log("API Response Status:", res.status);
|
||||||
console.log('API Response OK:', res.ok);
|
console.log("API Response OK:", res.ok);
|
||||||
assertNotBlocked(res);
|
assertNotBlocked(res);
|
||||||
|
|
||||||
if (!res.ok && res.status !== 206) {
|
if (!res.ok && res.status !== 206) {
|
||||||
const text = await res.text().catch(() => '');
|
const text = await res.text().catch(() => "");
|
||||||
console.error('API Error Response:', text || res.statusText);
|
console.error("API Error Response:", text || res.statusText);
|
||||||
throw new Error(`API ${res.status}: ${text || res.statusText}`);
|
throw new Error(`API ${res.status}: ${text || res.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -539,7 +468,7 @@ async function apiFetch(endpoint, options = {}) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(text);
|
const json = JSON.parse(text);
|
||||||
if (json && typeof json === 'object' && 'data' in json) {
|
if (json && typeof json === "object" && "data" in json) {
|
||||||
return json.data;
|
return json.data;
|
||||||
}
|
}
|
||||||
return json;
|
return json;
|
||||||
@ -556,15 +485,15 @@ async function authFetch(endpoint, body, token = null) {
|
|||||||
|
|
||||||
const bodyIsFormData = isFormData(body);
|
const bodyIsFormData = isFormData(body);
|
||||||
if (!bodyIsFormData) {
|
if (!bodyIsFormData) {
|
||||||
headers['Content-Type'] = 'application/json';
|
headers["Content-Type"] = "application/json";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
headers['Authorization'] = `Bearer ${token}`;
|
headers["Authorization"] = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE}${endpoint}`, {
|
const res = await fetch(`${API_BASE}${endpoint}`, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers,
|
headers,
|
||||||
body: bodyIsFormData ? body : JSON.stringify(body),
|
body: bodyIsFormData ? body : JSON.stringify(body),
|
||||||
});
|
});
|
||||||
@ -576,23 +505,29 @@ async function authFetch(endpoint, body, token = null) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
data = text ? JSON.parse(text) : null;
|
data = text ? JSON.parse(text) : null;
|
||||||
if (data && typeof data === 'object' && 'data' in data) {
|
if (data && typeof data === "object" && "data" in data) {
|
||||||
data = data.data;
|
data = data.data;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
data = text;
|
data = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = typeof data === 'object' && data?.message ? data.message : null;
|
const message =
|
||||||
|
typeof data === "object" && data?.message ? data.message : null;
|
||||||
|
|
||||||
return { status: res.status, data, ok: res.ok || res.status === 206, message };
|
return {
|
||||||
|
status: res.status,
|
||||||
|
data,
|
||||||
|
ok: res.ok || res.status === 206,
|
||||||
|
message,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reportFetch(endpoint, body) {
|
async function reportFetch(endpoint, body) {
|
||||||
const res = await fetch(buildApiUrl(REPORT_API_BASE, endpoint), {
|
const res = await fetch(buildApiUrl(REPORT_API_BASE, endpoint), {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
@ -604,22 +539,28 @@ async function reportFetch(endpoint, body) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
data = text ? JSON.parse(text) : null;
|
data = text ? JSON.parse(text) : null;
|
||||||
if (data && typeof data === 'object' && 'data' in data) {
|
if (data && typeof data === "object" && "data" in data) {
|
||||||
data = data.data;
|
data = data.data;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
data = text;
|
data = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = typeof data === 'object' && data?.message ? data.message : null;
|
const message =
|
||||||
|
typeof data === "object" && data?.message ? data.message : null;
|
||||||
|
|
||||||
return { status: res.status, data, ok: res.ok || res.status === 206, message };
|
return {
|
||||||
|
status: res.status,
|
||||||
|
data,
|
||||||
|
ok: res.ok || res.status === 206,
|
||||||
|
message,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Rent Properties ───
|
// ─── Rent Properties ───
|
||||||
|
|
||||||
export async function getRentProperties() {
|
export async function getRentProperties() {
|
||||||
return apiFetch('/RentProperties/GetRentProperties');
|
return apiFetch("/RentProperties/GetRentProperties");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRentProperty(id) {
|
export async function getRentProperty(id) {
|
||||||
@ -628,20 +569,22 @@ export async function getRentProperty(id) {
|
|||||||
|
|
||||||
export async function getRentPropertyLocations(params = {}) {
|
export async function getRentPropertyLocations(params = {}) {
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
if (params.maxOffset != null) qs.set('maxOffset', params.maxOffset);
|
if (params.maxOffset != null) qs.set("maxOffset", params.maxOffset);
|
||||||
if (params.minOffset != null) qs.set('minOffset', params.minOffset);
|
if (params.minOffset != null) qs.set("minOffset", params.minOffset);
|
||||||
const query = qs.toString();
|
const query = qs.toString();
|
||||||
return apiFetch(`/RentProperties/GetRentPropertiesLocations${query ? `?${query}` : ''}`);
|
return apiFetch(
|
||||||
|
`/RentProperties/GetRentPropertiesLocations${query ? `?${query}` : ""}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Sale Properties ───
|
// ─── Sale Properties ───
|
||||||
|
|
||||||
export async function getSaleProperties() {
|
export async function getSaleProperties() {
|
||||||
return apiFetch('/SaleProperties/GetSaleProperties');
|
return apiFetch("/SaleProperties/GetSaleProperties");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSaleProperty(id) {
|
export async function getSaleProperty(id) {
|
||||||
const items = await apiFetch('/SaleProperties/GetSaleProperties');
|
const items = await apiFetch("/SaleProperties/GetSaleProperties");
|
||||||
if (!Array.isArray(items)) return items;
|
if (!Array.isArray(items)) return items;
|
||||||
return items.find((p) => p.id == id) || items[0];
|
return items.find((p) => p.id == id) || items[0];
|
||||||
}
|
}
|
||||||
@ -655,7 +598,7 @@ export async function getProperty(id) {
|
|||||||
// ─── Recommendations ───
|
// ─── Recommendations ───
|
||||||
|
|
||||||
export async function getRecommendations() {
|
export async function getRecommendations() {
|
||||||
return apiFetch('/Recommendations/GetRecommendations');
|
return apiFetch("/Recommendations/GetRecommendations");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTopRecommendations(count = 10) {
|
export async function getTopRecommendations(count = 10) {
|
||||||
@ -664,36 +607,46 @@ export async function getTopRecommendations(count = 10) {
|
|||||||
|
|
||||||
// ─── Reservations ───
|
// ─── Reservations ───
|
||||||
|
|
||||||
export async function getAvailableDateRanges(propertyId, fromDate = null, toDate = null) {
|
export async function getAvailableDateRanges(
|
||||||
|
propertyId,
|
||||||
|
fromDate = null,
|
||||||
|
toDate = null,
|
||||||
|
) {
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
if (fromDate) qs.set('fromDate', fromDate);
|
if (fromDate) qs.set("fromDate", fromDate);
|
||||||
if (toDate) qs.set('toDate', toDate);
|
if (toDate) qs.set("toDate", toDate);
|
||||||
const query = qs.toString();
|
const query = qs.toString();
|
||||||
|
|
||||||
return apiFetch(
|
return apiFetch(
|
||||||
`/Reservations/GetAvailableDates/available/${propertyId}${query ? `?${query}` : ''}`
|
`/Reservations/GetAvailableDates/available/${propertyId}${query ? `?${query}` : ""}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getReservations() {
|
export async function getReservations() {
|
||||||
return apiFetch('/Reservations/GetAllReservations');
|
return apiFetch("/Reservations/GetAllReservations");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getReservation(id) {
|
export async function getReservation(id) {
|
||||||
return apiFetch(`/Reservations/GetReservation?id=${id}`);
|
return apiFetch(`/Reservations/GetReservation?id=${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkAvailability(propertyId, fromDate = null, toDate = null) {
|
export async function checkAvailability(
|
||||||
|
propertyId,
|
||||||
|
fromDate = null,
|
||||||
|
toDate = null,
|
||||||
|
) {
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
if (fromDate) qs.set('fromDate', fromDate);
|
if (fromDate) qs.set("fromDate", fromDate);
|
||||||
if (toDate) qs.set('toDate', toDate);
|
if (toDate) qs.set("toDate", toDate);
|
||||||
const query = qs.toString();
|
const query = qs.toString();
|
||||||
return apiFetch(`/Reservations/GetAvailable/${propertyId}${query ? `?${query}` : ''}`);
|
return apiFetch(
|
||||||
|
`/Reservations/GetAvailable/${propertyId}${query ? `?${query}` : ""}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function bookReservation(propertyInfoId, startDate, endDate) {
|
export async function bookReservation(propertyInfoId, startDate, endDate) {
|
||||||
return apiFetch('/Reservations/BookReservation/book', {
|
return apiFetch("/Reservations/BookReservation/book", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
propertyInfoId,
|
propertyInfoId,
|
||||||
startDate,
|
startDate,
|
||||||
@ -705,11 +658,11 @@ export async function bookReservation(propertyInfoId, startDate, endDate) {
|
|||||||
// ─── Terms ───
|
// ─── Terms ───
|
||||||
|
|
||||||
export async function getARTerms() {
|
export async function getARTerms() {
|
||||||
return apiFetch('/Configuration/GetARTerms');
|
return apiFetch("/Configuration/GetARTerms");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getENTerms() {
|
export async function getENTerms() {
|
||||||
return apiFetch('/Configuration/GetENTerms');
|
return apiFetch("/Configuration/GetENTerms");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Profile ───
|
// ─── Profile ───
|
||||||
@ -725,39 +678,39 @@ export async function getOwnerByUserId(userId) {
|
|||||||
// ─── Properties ───
|
// ─── Properties ───
|
||||||
|
|
||||||
export async function getMyRentListings() {
|
export async function getMyRentListings() {
|
||||||
return apiFetch('/RentProperties/GetMyRentListings');
|
return apiFetch("/RentProperties/GetMyRentListings");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addRentProperty(data) {
|
export async function addRentProperty(data) {
|
||||||
return apiFetch('/RentProperties/AddRentProperty', {
|
return apiFetch("/RentProperties/AddRentProperty", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: data,
|
body: data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function editRentProperty(id, data) {
|
export async function editRentProperty(id, data) {
|
||||||
return apiFetch(`/RentProperties/EditRentProperty/${id}`, {
|
return apiFetch(`/RentProperties/EditRentProperty/${id}`, {
|
||||||
method: 'PUT',
|
method: "PUT",
|
||||||
body: data,
|
body: data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function editSaleProperty(id, data) {
|
export async function editSaleProperty(id, data) {
|
||||||
return apiFetch(`/SaleProperties/EditSaleProperty/${id}`, {
|
return apiFetch(`/SaleProperties/EditSaleProperty/${id}`, {
|
||||||
method: 'PUT',
|
method: "PUT",
|
||||||
body: data,
|
body: data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addSaleProperty(data) {
|
export async function addSaleProperty(data) {
|
||||||
return apiFetch('/SaleProperties/AddSaleProperty', {
|
return apiFetch("/SaleProperties/AddSaleProperty", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: data,
|
body: data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMySaleListings() {
|
export async function getMySaleListings() {
|
||||||
return apiFetch('/SaleProperties/GetMySaleListings');
|
return apiFetch("/SaleProperties/GetMySaleListings");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSalePropertyById(id) {
|
export async function getSalePropertyById(id) {
|
||||||
@ -766,14 +719,14 @@ export async function getSalePropertyById(id) {
|
|||||||
|
|
||||||
export async function updateRentPropertyStatus(id, status) {
|
export async function updateRentPropertyStatus(id, status) {
|
||||||
return apiFetch(`/RentProperties/UpdateStatus/${id}`, {
|
return apiFetch(`/RentProperties/UpdateStatus/${id}`, {
|
||||||
method: 'PUT',
|
method: "PUT",
|
||||||
body: { status },
|
body: { status },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateSalePropertyStatus(id, status) {
|
export async function updateSalePropertyStatus(id, status) {
|
||||||
return apiFetch(`/SaleProperties/UpdateStatus/${id}`, {
|
return apiFetch(`/SaleProperties/UpdateStatus/${id}`, {
|
||||||
method: 'PUT',
|
method: "PUT",
|
||||||
body: { status },
|
body: { status },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -781,19 +734,19 @@ export async function updateSalePropertyStatus(id, status) {
|
|||||||
// ─── Currencies ───
|
// ─── Currencies ───
|
||||||
|
|
||||||
export async function getCurrencies() {
|
export async function getCurrencies() {
|
||||||
return apiFetch('/Currency/GetAll');
|
return apiFetch("/Currency/GetAll");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Files ───
|
// ─── Files ───
|
||||||
|
|
||||||
export async function uploadPicture(file) {
|
export async function uploadPicture(file) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('image', file);
|
formData.append("image", file);
|
||||||
|
|
||||||
const token = AuthService.getToken();
|
const token = AuthService.getToken();
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE}/Files/UploadPicture`, {
|
const res = await fetch(`${API_BASE}/Files/UploadPicture`, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
...(token && { Authorization: `Bearer ${token}` }),
|
...(token && { Authorization: `Bearer ${token}` }),
|
||||||
},
|
},
|
||||||
@ -820,7 +773,7 @@ async function multipartAuthFetch(endpoint, formData) {
|
|||||||
const token = AuthService.getToken();
|
const token = AuthService.getToken();
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE}${endpoint}`, {
|
const res = await fetch(`${API_BASE}${endpoint}`, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
...(token && { Authorization: `Bearer ${token}` }),
|
...(token && { Authorization: `Bearer ${token}` }),
|
||||||
},
|
},
|
||||||
@ -834,100 +787,129 @@ async function multipartAuthFetch(endpoint, formData) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
data = text ? JSON.parse(text) : null;
|
data = text ? JSON.parse(text) : null;
|
||||||
if (data && typeof data === 'object' && 'data' in data) {
|
if (data && typeof data === "object" && "data" in data) {
|
||||||
data = data.data;
|
data = data.data;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
data = text;
|
data = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { status: res.status, data, ok: res.ok || res.status === 206, message: data?.message };
|
return {
|
||||||
|
status: res.status,
|
||||||
|
data,
|
||||||
|
ok: res.ok || res.status === 206,
|
||||||
|
message: data?.message,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addOwner(data, frontImage = null, backImage = null, licenseImage = null) {
|
export async function addOwner(
|
||||||
|
data,
|
||||||
|
frontImage = null,
|
||||||
|
backImage = null,
|
||||||
|
licenseImage = null,
|
||||||
|
) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
formData.append('FirstName', data.firstName || data.FirstName || '');
|
formData.append("FirstName", data.firstName || data.FirstName || "");
|
||||||
formData.append('LastName', data.lastName || data.LastName || '');
|
formData.append("LastName", data.lastName || data.LastName || "");
|
||||||
formData.append('Email', data.email || data.Email || '');
|
formData.append("Email", data.email || data.Email || "");
|
||||||
|
|
||||||
const phoneValue = data.phone || data.phoneNumber || data.Phone || data.PhoneNumber || '';
|
const phoneValue =
|
||||||
|
data.phone || data.phoneNumber || data.Phone || data.PhoneNumber || "";
|
||||||
const whatsappValue =
|
const whatsappValue =
|
||||||
data.whatsAppNumber || data.whatsapp || data.WhatsAppNumber || data.WhatsApp || '';
|
data.whatsAppNumber ||
|
||||||
|
data.whatsapp ||
|
||||||
|
data.WhatsAppNumber ||
|
||||||
|
data.WhatsApp ||
|
||||||
|
"";
|
||||||
|
|
||||||
formData.append('PhoneNumber', phoneValue);
|
formData.append("PhoneNumber", phoneValue);
|
||||||
formData.append('Phone', phoneValue);
|
formData.append("Phone", phoneValue);
|
||||||
formData.append('WhatsAppNumber', whatsappValue);
|
formData.append("WhatsAppNumber", whatsappValue);
|
||||||
|
|
||||||
formData.append('NationalNumber', data.nationalNumber || data.NationalNumber || '');
|
formData.append(
|
||||||
formData.append('Password', data.password || data.Password || '');
|
"NationalNumber",
|
||||||
formData.append('Type', String(data.type ?? data.ownerType ?? data.Type ?? 0));
|
data.nationalNumber || data.NationalNumber || "",
|
||||||
formData.append('Language', String(data.language ?? data.Language ?? 1));
|
);
|
||||||
|
formData.append("Password", data.password || data.Password || "");
|
||||||
|
formData.append(
|
||||||
|
"Type",
|
||||||
|
String(data.type ?? data.ownerType ?? data.Type ?? 0),
|
||||||
|
);
|
||||||
|
formData.append("Language", String(data.language ?? data.Language ?? 1));
|
||||||
|
|
||||||
if (frontImage) formData.append('FrontIdCarImagePath', frontImage);
|
if (frontImage) formData.append("FrontIdCarImagePath", frontImage);
|
||||||
if (backImage) formData.append('RearIdCarImagePath', backImage);
|
if (backImage) formData.append("RearIdCarImagePath", backImage);
|
||||||
if (licenseImage) formData.append('LicenseImagePath', licenseImage);
|
if (licenseImage) formData.append("LicenseImagePath", licenseImage);
|
||||||
|
|
||||||
return multipartAuthFetch('/Owner/Add', formData);
|
return multipartAuthFetch("/Owner/Add", formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addCustomer(data, frontImage = null, backImage = null) {
|
export async function addCustomer(data, frontImage = null, backImage = null) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('FirstName', data.firstName || data.FirstName || '');
|
formData.append("FirstName", data.firstName || data.FirstName || "");
|
||||||
formData.append('LastName', data.lastName || data.LastName || '');
|
formData.append("LastName", data.lastName || data.LastName || "");
|
||||||
formData.append('Email', data.email || '');
|
formData.append("Email", data.email || "");
|
||||||
formData.append('PhoneNumber', data.phoneNumber || '');
|
formData.append("PhoneNumber", data.phoneNumber || "");
|
||||||
formData.append('WhatsAppNumber', data.whatsAppNumber || '');
|
formData.append("WhatsAppNumber", data.whatsAppNumber || "");
|
||||||
formData.append('Phone', data.phone || '');
|
formData.append("Phone", data.phone || "");
|
||||||
formData.append('NationalNumber', data.nationalNumber || '');
|
formData.append("NationalNumber", data.nationalNumber || "");
|
||||||
formData.append('Password', data.password || '');
|
formData.append("Password", data.password || "");
|
||||||
formData.append('Type', String(data.customerType ?? data.Type ?? 0));
|
formData.append("Type", String(data.customerType ?? data.Type ?? 0));
|
||||||
formData.append('Language', '0');
|
formData.append("Language", "0");
|
||||||
|
|
||||||
if (frontImage) formData.append('FrontIdCarImagePath', frontImage);
|
if (frontImage) formData.append("FrontIdCarImagePath", frontImage);
|
||||||
if (backImage) formData.append('RearIdCarImagePath', backImage);
|
if (backImage) formData.append("RearIdCarImagePath", backImage);
|
||||||
|
|
||||||
return multipartAuthFetch('/Customer/Add', formData);
|
return multipartAuthFetch("/Customer/Add", formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Auth: Login ───
|
// ─── Auth: Login ───
|
||||||
|
|
||||||
export async function loginWithEmail(credential, password) {
|
export async function loginWithEmail(credential, password) {
|
||||||
return authFetch('/Auth/LogInWithEmail', {
|
return authFetch("/Auth/LogInWithEmail", {
|
||||||
credential,
|
credential,
|
||||||
password,
|
password,
|
||||||
device: 0,
|
device: 0,
|
||||||
appVersion: '',
|
appVersion: "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loginWithPhone(credential, password) {
|
export async function loginWithPhone(credential, password) {
|
||||||
return authFetch('/Auth/LogInWithPhoneNumber', {
|
return authFetch("/Auth/LogInWithPhoneNumber", {
|
||||||
credential,
|
credential,
|
||||||
password,
|
password,
|
||||||
device: 0,
|
device: 0,
|
||||||
appVersion: '',
|
appVersion: "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Auth: OTP ───
|
// ─── Auth: OTP ───
|
||||||
|
|
||||||
export async function sendEmailOTP() {
|
export async function sendEmailOTP() {
|
||||||
return apiFetch('/Auth/SendEmailOTP', { method: 'POST' });
|
return apiFetch("/Auth/SendEmailOTP", { method: "POST" });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendPhoneOTP() {
|
export async function sendPhoneOTP() {
|
||||||
return apiFetch('/Auth/SendPhoneNumberOTP', { method: 'POST' });
|
return apiFetch("/Auth/SendPhoneNumberOTP", { method: "POST" });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function verifyEmail(code) {
|
export async function verifyEmail(code) {
|
||||||
const token = AuthService.getToken();
|
const token = AuthService.getToken();
|
||||||
return authFetch(`/Auth/VerifyEmail?code=${encodeURIComponent(code)}`, {}, token);
|
return authFetch(
|
||||||
|
`/Auth/VerifyEmail?code=${encodeURIComponent(code)}`,
|
||||||
|
{},
|
||||||
|
token,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function verifyPhone(code) {
|
export async function verifyPhone(code) {
|
||||||
const token = AuthService.getToken();
|
const token = AuthService.getToken();
|
||||||
return authFetch(`/Auth/VerifyPhoneNumber?code=${encodeURIComponent(code)}`, {}, token);
|
return authFetch(
|
||||||
|
`/Auth/VerifyPhoneNumber?code=${encodeURIComponent(code)}`,
|
||||||
|
{},
|
||||||
|
token,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Helpers ───
|
// ─── Helpers ───
|
||||||
@ -937,39 +919,83 @@ export function isEmail(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isPhoneNumber(value) {
|
export function isPhoneNumber(value) {
|
||||||
return /^\+?\d{7,15}$/.test(value.replace(/[\s\-()]/g, ''));
|
return /^\+?\d{7,15}$/.test(value.replace(/[\s\-()]/g, ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Favorites ───
|
// ─── Favorites ───
|
||||||
|
|
||||||
export async function getUserFavoriteProperties() {
|
export async function getUserFavoriteProperties() {
|
||||||
return apiFetch('/FavoriteProperty/GetUserFavoriteProperties');
|
return apiFetch("/FavoriteProperty/GetUserFavoriteProperties");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addFavoriteProperty(propId) {
|
export async function addFavoriteProperty(propId) {
|
||||||
return apiFetch(`/FavoriteProperty/Add?propId=${propId}`, { method: 'POST' });
|
return apiFetch(`/FavoriteProperty/Add?propId=${propId}`, { method: "POST" });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeFavoriteProperty(favePropId) {
|
export async function removeFavoriteProperty(favePropId) {
|
||||||
return apiFetch(`/FavoriteProperty/Remove?favePropId=${favePropId}`, { method: 'DELETE' });
|
return apiFetch(`/FavoriteProperty/Remove?favePropId=${favePropId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserNotifications() {
|
export async function getUserNotifications() {
|
||||||
return apiFetch('/Notifications/GetUserNotifications');
|
return apiFetch("/Notifications/GetUserNotifications");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Booking/Reservation Management ───
|
// ─── Booking/Reservation Management ───
|
||||||
|
|
||||||
export async function confirmDepositPayment(bookingId) {
|
export async function confirmDepositPayment(bookingId) {
|
||||||
return apiFetch('/Reservations/ConfirmDepositPayment', {
|
return apiFetch("/Reservations/ConfirmDepositPayment", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: { bookingId },
|
body: { bookingId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function adminConfirmDeposit(
|
||||||
|
reservationId,
|
||||||
|
adminId,
|
||||||
|
comment = null,
|
||||||
|
) {
|
||||||
|
const token = AuthService.getToken();
|
||||||
|
const endpoint = `${API_BASE}/Reservations/AdminConfirmDeposit/admin-confirm-deposit`;
|
||||||
|
const normalizedComment =
|
||||||
|
typeof comment === "string" && comment.trim() ? comment.trim() : null;
|
||||||
|
const payload = {
|
||||||
|
reservationId,
|
||||||
|
adminId,
|
||||||
|
comment: normalizedComment,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch(endpoint, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(token && { Authorization: `Bearer ${token}` }),
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = await res.text();
|
||||||
|
let data = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = text ? JSON.parse(text) : null;
|
||||||
|
if (data && typeof data === "object" && "data" in data) {
|
||||||
|
data = data.data;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
data = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message =
|
||||||
|
typeof data === "object" && data?.message ? data.message : null;
|
||||||
|
|
||||||
|
return { status: res.status, data, ok: res.ok, message };
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateBookingStatus(bookingId, status) {
|
export async function updateBookingStatus(bookingId, status) {
|
||||||
return apiFetch('/Reservations/UpdateStatus', {
|
return apiFetch("/Reservations/UpdateStatus", {
|
||||||
method: 'PUT',
|
method: "PUT",
|
||||||
body: { bookingId, status },
|
body: { bookingId, status },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -977,31 +1003,31 @@ export async function updateBookingStatus(bookingId, status) {
|
|||||||
// ─── Owner / Reservations ───
|
// ─── Owner / Reservations ───
|
||||||
|
|
||||||
export async function getOwnerReservationRequests() {
|
export async function getOwnerReservationRequests() {
|
||||||
return apiFetch('/Reservations/GetOwnerResevationRequests');
|
return apiFetch("/Reservations/GetOwnerResevationRequests");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOwnerReservationsByStatuses(filterStatuses) {
|
export async function getOwnerReservationsByStatuses(filterStatuses) {
|
||||||
return apiFetch('/Reservations/GetAllReservationsByStateForOwner', {
|
return apiFetch("/Reservations/GetAllReservationsByStateForOwner", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: { filterStatuses },
|
body: { filterStatuses },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserReservations() {
|
export async function getUserReservations() {
|
||||||
return apiFetch('/Reservations/GetUserResevations');
|
return apiFetch("/Reservations/GetUserResevations");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ownerConfirmReservation(id) {
|
export async function ownerConfirmReservation(id) {
|
||||||
return apiFetch(`/Reservations/OwnerConfirmReservation/owner-confirm/${id}`, {
|
return apiFetch(`/Reservations/OwnerConfirmReservation/owner-confirm/${id}`, {
|
||||||
method: 'PUT',
|
method: "PUT",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Payments ───
|
// ─── Payments ───
|
||||||
|
|
||||||
export async function payDeposit(data) {
|
export async function payDeposit(data) {
|
||||||
return apiFetch('/Reservations/PayDeposit/pay-deposit', {
|
return apiFetch("/Reservations/PayDeposit/pay-deposit", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: data,
|
body: data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1010,12 +1036,12 @@ export async function payDeposit(data) {
|
|||||||
|
|
||||||
export async function getOwnerContactInformation(propertyInformationId) {
|
export async function getOwnerContactInformation(propertyInformationId) {
|
||||||
return apiFetch(
|
return apiFetch(
|
||||||
`/Owner/GetOwnerContactInformation?propertyInformationId=${propertyInformationId}`
|
`/Owner/GetOwnerContactInformation?propertyInformationId=${propertyInformationId}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOwnerStatistics() {
|
export async function getOwnerStatistics() {
|
||||||
return apiFetch('/Statistics/GetOwnerStatistics');
|
return apiFetch("/Statistics/GetOwnerStatistics");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Agent Registration ───
|
// ─── Agent Registration ───
|
||||||
@ -1024,7 +1050,7 @@ export async function registerRealEstateAgent(formData) {
|
|||||||
const token = AuthService.getToken();
|
const token = AuthService.getToken();
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE}/RealEstateAgent/Add`, {
|
const res = await fetch(`${API_BASE}/RealEstateAgent/Add`, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
...(token && { Authorization: `Bearer ${token}` }),
|
...(token && { Authorization: `Bearer ${token}` }),
|
||||||
},
|
},
|
||||||
@ -1038,7 +1064,7 @@ export async function registerRealEstateAgent(formData) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
data = text ? JSON.parse(text) : null;
|
data = text ? JSON.parse(text) : null;
|
||||||
if (data && typeof data === 'object' && 'data' in data) data = data.data;
|
if (data && typeof data === "object" && "data" in data) data = data.data;
|
||||||
} catch {
|
} catch {
|
||||||
data = text;
|
data = text;
|
||||||
}
|
}
|
||||||
@ -1047,7 +1073,7 @@ export async function registerRealEstateAgent(formData) {
|
|||||||
status: res.status,
|
status: res.status,
|
||||||
data,
|
data,
|
||||||
ok: res.ok || res.status === 206,
|
ok: res.ok || res.status === 206,
|
||||||
message: data?.message || (typeof data === 'string' ? data : null),
|
message: data?.message || (typeof data === "string" ? data : null),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1057,23 +1083,25 @@ export async function changePassword(oldPassword, newPassword) {
|
|||||||
return apiFetch(
|
return apiFetch(
|
||||||
`/User/ChangePassword?oldPassword=${encodeURIComponent(oldPassword)}&newPassword=${encodeURIComponent(newPassword)}`,
|
`/User/ChangePassword?oldPassword=${encodeURIComponent(oldPassword)}&newPassword=${encodeURIComponent(newPassword)}`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: "PUT",
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Forget Password (OTP flow) ───
|
// ─── Forget Password (OTP flow) ───
|
||||||
|
|
||||||
export async function requestForgetPasswordOtp(email) {
|
export async function requestForgetPasswordOtp(email) {
|
||||||
return apiFetch(`/User/ForgetPassword?email=${encodeURIComponent(email)}`, { method: 'POST' });
|
return apiFetch(`/User/ForgetPassword?email=${encodeURIComponent(email)}`, {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function verifyForgetPasswordOtp(email, code, newPassword) {
|
export async function verifyForgetPasswordOtp(email, code, newPassword) {
|
||||||
return apiFetch(
|
return apiFetch(
|
||||||
`/User/VerifyForgetPasswordOTP?email=${encodeURIComponent(email)}&code=${encodeURIComponent(code)}&newPassword=${encodeURIComponent(newPassword)}`,
|
`/User/VerifyForgetPasswordOTP?email=${encodeURIComponent(email)}&code=${encodeURIComponent(code)}&newPassword=${encodeURIComponent(newPassword)}`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1086,16 +1114,19 @@ export async function resetPassword(token) {
|
|||||||
// ─── Delete Account ───
|
// ─── Delete Account ───
|
||||||
|
|
||||||
export async function deleteMyAccount(password) {
|
export async function deleteMyAccount(password) {
|
||||||
return apiFetch(`/User/DeleteMyAccount?password=${encodeURIComponent(password)}`, {
|
return apiFetch(
|
||||||
method: 'DELETE',
|
`/User/DeleteMyAccount?password=${encodeURIComponent(password)}`,
|
||||||
});
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Set FCM Token ───
|
// ─── Set FCM Token ───
|
||||||
|
|
||||||
export async function setFCMToken(token, deviceType = 2) {
|
export async function setFCMToken(token, deviceType = 2) {
|
||||||
return apiFetch('/User/SetFCMToken', {
|
return apiFetch("/User/SetFCMToken", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: { token, deviceType },
|
body: { token, deviceType },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1105,52 +1136,54 @@ export async function setFCMToken(token, deviceType = 2) {
|
|||||||
export async function filterRentProperties(params = {}) {
|
export async function filterRentProperties(params = {}) {
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
Object.entries(params).forEach(([k, v]) => {
|
Object.entries(params).forEach(([k, v]) => {
|
||||||
if (v != null && v !== '') qs.set(k, v);
|
if (v != null && v !== "") qs.set(k, v);
|
||||||
});
|
});
|
||||||
const query = qs.toString();
|
const query = qs.toString();
|
||||||
return apiFetch(`/RentProperties/FilterRentProperties${query ? `?${query}` : ''}`);
|
return apiFetch(
|
||||||
|
`/RentProperties/FilterRentProperties${query ? `?${query}` : ""}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Reports ───
|
// ─── Reports ───
|
||||||
|
|
||||||
export async function sendGeneralReport(subject, reportBody) {
|
export async function sendGeneralReport(subject, reportBody) {
|
||||||
return reportFetch('/Reports/SendGeneralReport', {
|
return reportFetch("/Reports/SendGeneralReport", {
|
||||||
subject,
|
subject,
|
||||||
body: reportBody,
|
body: reportBody,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function submitReport(subject, body) {
|
export async function submitReport(subject, body) {
|
||||||
return apiFetch('/Reports/SendGeneralReport', {
|
return apiFetch("/Reports/SendGeneralReport", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: { subject, body },
|
body: { subject, body },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function submitReservationReport(data) {
|
export async function submitReservationReport(data) {
|
||||||
return apiFetch('/ReservationReports', {
|
return apiFetch("/ReservationReports", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: data,
|
body: data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateReservationReport(id, data) {
|
export async function updateReservationReport(id, data) {
|
||||||
return apiFetch(`/ReservationReports/${id}`, {
|
return apiFetch(`/ReservationReports/${id}`, {
|
||||||
method: 'PUT',
|
method: "PUT",
|
||||||
body: data,
|
body: data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function submitSaleReport(data) {
|
export async function submitSaleReport(data) {
|
||||||
return apiFetch('/SaleReports', {
|
return apiFetch("/SaleReports", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: data,
|
body: data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateSaleReport(id, data) {
|
export async function updateSaleReport(id, data) {
|
||||||
return apiFetch(`/SaleReports/${id}`, {
|
return apiFetch(`/SaleReports/${id}`, {
|
||||||
method: 'PUT',
|
method: "PUT",
|
||||||
body: data,
|
body: data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1158,8 +1191,8 @@ export async function updateSaleReport(id, data) {
|
|||||||
// ─── Terms (Add or Update) ───
|
// ─── Terms (Add or Update) ───
|
||||||
|
|
||||||
export async function addOrUpdateTerms(terms) {
|
export async function addOrUpdateTerms(terms) {
|
||||||
return apiFetch('/Terms/AddOrUpdateTerms', {
|
return apiFetch("/Terms/AddOrUpdateTerms", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: terms,
|
body: terms,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user