Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m20s

This commit is contained in:
Beilin-b
2026-06-17 06:01:32 -07:00
6 changed files with 575 additions and 358 deletions

View File

@ -6,7 +6,7 @@ on:
jobs:
build:
runs-on: ubuntu-latest
steps:
steps:
- name: Clone repository
uses: actions/checkout@v2
with:

View File

@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { Home, Building, Calendar, Heart, Bell, Settings } from "lucide-react";
import { Home, Building, Calendar, Heart, Bell, Settings, CreditCard } from "lucide-react";
import React, { useEffect, useState } from "react";
import { useNotifications } from "@/app/contexts/NotificationsContext";
@ -19,6 +19,7 @@ export default function BottomNav({ isOwner }) {
{ href: "/properties", label: "عقاراتنا", icon: Building },
{ href: bookingsHref, label: "الحجوزات", icon: Calendar },
{ href: "/favorites", label: "المفضلة", icon: Heart },
{ href: "/payments", label: "المدفوعات", icon: CreditCard },
{ href: "/notifications", label: "الإشعارات", icon: Bell, badge: isMounted ? unreadCount : 0 },
{ href: "/settings", label: "الإعدادات", icon: Settings },
];

View File

@ -67,9 +67,7 @@ export default function HeroSearch({ onSearch, isAuthenticated }) {
setActiveTab(tab);
if ((tab === 'rent' || tab === 'sell') && !isAuthenticated) {
setShowLoginDialog(true);
return;
}
handleSearch();
};
const handleSearch = () => {
@ -139,30 +137,32 @@ export default function HeroSearch({ onSearch, isAuthenticated }) {
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-white mb-2">
<div className="flex items-center gap-1">
<Home className="w-4 h-4" />
{t("rentTypeLabel")}
</div>
</label>
<select
value={filters.propertyType}
onChange={(e) => setFilters({...filters, propertyType: e.target.value})}
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'left 1rem center',
backgroundSize: '1rem',
paddingLeft: '2.5rem'
}}
>
{propertyTypes.map(type => (
<option key={type.id} value={type.id}>{type.label}</option>
))}
</select>
</div>
{activeTab === 'rent' && (
<div>
<label className="block text-sm font-medium text-white mb-2">
<div className="flex items-center gap-1">
<Home className="w-4 h-4" />
{t("rentTypeLabel")}
</div>
</label>
<select
value={filters.propertyType}
onChange={(e) => setFilters({...filters, propertyType: e.target.value})}
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'left 1rem center',
backgroundSize: '1rem',
paddingLeft: '2.5rem'
}}
>
{propertyTypes.map(type => (
<option key={type.id} value={type.id}>{type.label}</option>
))}
</select>
</div>
)}
<div>
<label className="block text-sm font-medium text-white mb-2">
<div className="flex items-center gap-1">
@ -234,29 +234,31 @@ export default function HeroSearch({ onSearch, isAuthenticated }) {
</select>
</div>
<div>
<label className="block text-sm font-medium text-white mb-2">نوع الإيجار</label>
<select
value={filters.rentPeriod}
onChange={(e) => setFilters({ ...filters, rentPeriod: e.target.value })}
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'left 1rem center',
backgroundSize: '1rem',
paddingLeft: '2.5rem'
}}
>
{rentPeriods.map((period) => (
<option key={period.id} value={period.id}>
{period.label}
</option>
))}
</select>
</div>
{activeTab === 'rent' && (
<div>
<label className="block text-sm font-medium text-white mb-2">نوع الإيجار</label>
<select
value={filters.rentPeriod}
onChange={(e) => setFilters({ ...filters, rentPeriod: e.target.value })}
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'left 1rem center',
backgroundSize: '1rem',
paddingLeft: '2.5rem'
}}
>
{rentPeriods.map((period) => (
<option key={period.id} value={period.id}>
{period.label}
</option>
))}
</select>
</div>
)}
<div className="md:col-span-2 flex flex-col justify-between p-4 rounded-2xl border border-dashed border-white/30 bg-white/5">
<div className={`${activeTab === 'rent' ? 'md:col-span-2' : 'md:col-span-3'} flex flex-col justify-between p-4 rounded-2xl border border-dashed border-white/30 bg-white/5`}>
<label className="mt-4 flex items-center gap-3 text-white text-sm">
<input
type="checkbox"

View File

@ -154,8 +154,9 @@
import { useEffect, useState, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { motion } from 'framer-motion';
import { CreditCard, Loader2, Home, Calendar, Check, X, Clock } from 'lucide-react';
import { CreditCard, Loader2, Home, Calendar, Check, X, Clock, LogIn, Lock } from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
import AuthService from '@/app/services/AuthService';
import { payDeposit } from '@/app/utils/api';
@ -176,6 +177,7 @@ export default function PaymentsPage() {
const [reservations, setReservations] = useState([]);
const [loading, setLoading] = useState(true);
const [payingId, setPayingId] = useState(null);
const [isGuest, setIsGuest] = useState(null);
const getAuthToken = () => {
if (typeof window === 'undefined') return '';
@ -238,13 +240,14 @@ export default function PaymentsPage() {
}, []);
useEffect(() => {
// Admin check removed
// if (AuthService.isAdmin()) {
// router.push('/');
// return;
// }
if (AuthService.isGuest()) {
setIsGuest(true);
setLoading(false);
return;
}
setIsGuest(false);
loadReservations();
}, [router, loadReservations]);
}, [loadReservations]);
const handlePayDeposit = async (reservation) => {
setPayingId(reservation.id);
@ -276,6 +279,33 @@ export default function PaymentsPage() {
);
}
if (isGuest) {
return (
<div className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white flex items-center justify-center p-4" dir="rtl">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="bg-white rounded-3xl shadow-xl border border-gray-200 p-10 max-w-md w-full text-center"
>
<div className="w-20 h-20 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-6">
<Lock className="w-10 h-10 text-amber-600" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-3">المدفوعات</h2>
<p className="text-gray-600 leading-relaxed mb-8">
دفعاتك مرتبطة بحاسبك لذلك يرجى تسجيل الدخول أولاً
</p>
<Link
href="/login"
className="inline-flex items-center gap-2 bg-amber-500 hover:bg-amber-600 text-white px-8 py-3 rounded-2xl text-lg font-semibold transition shadow-lg shadow-amber-200"
>
<LogIn className="w-5 h-5" />
تسجيل الدخول
</Link>
</motion.div>
</div>
);
}
const canPay = (status) => STATUS_MAP[status] === 'pending' || STATUS_MAP[status] === 'ownerConfirmed';
return (

View File

@ -1,62 +1,143 @@
'use client';
import { useState, useEffect } from 'react';
import { useEffect, useState } from 'react';
import { motion } from 'framer-motion';
import { FileText, Shield, CheckCircle } from 'lucide-react';
import { getTerms } from '../utils/api';
import { FileText, Shield, CheckCircle, Languages, Loader2, AlertCircle } from 'lucide-react';
import { getARTerms, getENTerms } from '../utils/api';
const staticTerms = [
{
title: 'مقدمة',
content:
'مرحباً بك في منصة SweetHome. باستخدامك للمنصة، فإنك توافق على الالتزام بشروط الاستخدام هذه. إذا كنت لا توافق على أي جزء من هذه الشروط، يرجى عدم استخدام المنصة. تحتفظ المنصة بحق تعديل هذه الشروط في أي وقت مع إشعار المستخدمين.',
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.08 },
},
{
title: 'استخدام المنصة',
content:
'يُسمح باستخدام المنصة للأغراض المشروعة فقط. يلتزم المستخدم بعدم استخدام المنصة في أي نشاط غير قانوني أو مخالف للقوانين السارية. كما يلتزم المستخدم بعدم محاولة الوصول غير المصرح به إلى أي جزء من المنصة أو الخوادم أو الأنظمة المتصلة بها.',
},
{
title: 'حقوق ومسؤوليات المالك',
content:
'يتحمل المالك مسؤولية دقة المعلومات المقدمة عن العقار بما في ذلك الصور والوصف والسعر والتوفر. يلتزم المالك بتحديث معلومات العقار بشكل دوري. المنصة غير مسؤولة عن أي نزاعات تنشأ بين المالك والمستأجر. يجب على المالك الالتزام بجميع القوانين المحلية المتعلقة بتأجير العقارات.',
},
{
title: 'حقوق ومسؤوليات المستأجر',
content:
'يلتزم المستأجر باستخدام العقار بطريقة مسؤولة وعدم التسبب في أي ضرر للممتلكات. يجب على المستأجر الالتزام بقوانين المنزل ومواعيد تسجيل الوصول والمغادرة. المنصة غير مسؤولة عن أي سلوك غير لائق من قبل المستأجرين.',
},
{
title: 'الدفع والعمولات',
content:
'تتقاضى المنصة عمولة على كل حصة ناجحة وفقاً للنسبة المحددة في وقت الحجز. جميع المدفوعات تتم عبر قنوات الدفع الآمنة في المنصة. أي رسوم إلغاء أو استرداد تخضع لسياسة الإلغاء المحددة في كل عقار.',
},
{
title: 'خصوصية البيانات',
content:
'نحن نأخذ خصوصية بياناتك على محمل الجد. يتم جمع واستخدام البيانات الشخصية وفقاً لسياسة الخصوصية الخاصة بنا. نحن لا نشارك معلوماتك مع أطراف ثالثة دون موافقتك، إلا عندما يقتضي القانون ذلك.',
},
];
};
const itemVariants = {
hidden: { opacity: 0, y: 24 },
visible: { opacity: 1, y: 0 },
};
const FALLBACK_TERMS = {
ar: [
{
title: 'مقدمة',
description:
'مرحباً بك في منصة SweetHome. باستخدامك للمنصة، فإنك توافق على الالتزام بشروط الاستخدام هذه. إذا كنت لا توافق على أي جزء من هذه الشروط، يرجى عدم استخدام المنصة.',
},
{
title: 'استخدام المنصة',
description:
'يُسمح باستخدام المنصة للأغراض المشروعة فقط. يلتزم المستخدم بعدم استخدام المنصة في أي نشاط غير قانوني.',
},
{
title: 'حقوق ومسؤوليات المالك',
description:
'يتحمل المالك مسؤولية دقة المعلومات المقدمة عن العقار بما في ذلك الصور والوصف والسعر والتوفر.',
},
{
title: 'حقوق ومسؤوليات المستأجر',
description:
'يلتزم المستأجر باستخدام العقار بطريقة مسؤولة وعدم التسبب في أي ضرر للممتلكات.',
},
{
title: 'الدفع والعمولات',
description:
'تتقاضى المنصة عمولة على كل حصة ناجحة وفقاً للنسبة المحددة في وقت الحجز.',
},
{
title: 'خصوصية البيانات',
description:
'نحن نأخذ خصوصية بياناتك على محمل الجد. يتم جمع واستخدام البيانات الشخصية وفقاً لسياسة الخصوصية الخاصة بنا.',
},
],
en: [
{
title: 'Introduction',
description:
'Welcome to SweetHome. By using our platform, you agree to comply with these terms. If you do not agree, please do not use the platform.',
},
{
title: 'Platform Usage',
description:
'The platform may only be used for lawful purposes. Users must not engage in any illegal activity.',
},
{
title: 'Owner Rights & Responsibilities',
description:
'Owners are responsible for the accuracy of property information including images, description, price, and availability.',
},
{
title: 'Tenant Rights & Responsibilities',
description:
'Tenants must use the property responsibly and not cause any damage to the property.',
},
{
title: 'Payment & Commissions',
description:
'The platform charges a commission on each successful booking according to the rate specified at the time of booking.',
},
{
title: 'Data Privacy',
description:
'We take your data privacy seriously. Personal data is collected and used in accordance with our Privacy Policy.',
},
],
};
export default function TermsPage() {
const [terms, setTerms] = useState(staticTerms);
const [terms, setTerms] = useState([]);
const [language, setLanguage] = useState('ar');
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
async function fetchTerms() {
const controller = new AbortController();
const fetchTerms = async () => {
try {
const data = await getTerms();
if (data && Array.isArray(data) && data.length > 0) {
setTerms(data);
setLoading(true);
setError('');
const fetcher = language === 'ar' ? getARTerms : getENTerms;
const data = await fetcher();
if (!data) {
setTerms(FALLBACK_TERMS[language]);
return;
}
const raw = Array.isArray(data) ? data : data.terms || data.items || data.data || [];
if (!Array.isArray(raw) || raw.length === 0) {
setTerms(FALLBACK_TERMS[language]);
return;
}
const mapped = raw.map((item) => ({
title: item.title || item.name || '',
description: item.description || item.content || item.body || item.text || '',
}));
setTerms(mapped);
} catch {
// fall back to static terms
setTerms(FALLBACK_TERMS[language]);
setError('');
} finally {
setLoading(false);
}
}
};
fetchTerms();
}, []);
return () => controller.abort();
}, [language]);
return (
<div className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12" dir="rtl">
<div
dir={language === 'ar' ? 'rtl' : 'ltr'}
className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12"
>
<div className="container mx-auto px-4 max-w-4xl">
<motion.div
initial={{ opacity: 0, y: -20 }}
@ -66,33 +147,95 @@ export default function TermsPage() {
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-amber-100">
<FileText className="w-10 h-10 text-amber-600" />
</div>
<h1 className="text-4xl font-bold text-gray-900 mb-4">شروط الاستخدام</h1>
<div className="flex items-center justify-center gap-4 mb-4">
<h1 className="text-4xl font-bold text-gray-900">
{language === 'ar' ? 'شروط الاستخدام' : 'Terms of Use'}
</h1>
<button
type="button"
onClick={() => setLanguage(language === 'ar' ? 'en' : 'ar')}
className="inline-flex items-center gap-2 rounded-full border border-amber-200 bg-white px-4 py-2 text-sm font-semibold text-gray-700 shadow-sm transition hover:shadow-md"
>
<Languages className="h-4 w-4" />
{language === 'ar' ? 'English' : 'العربية'}
</button>
</div>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
يرجى قراءة شروط الاستخدام التالية بعناية قبل استخدام المنصة
{language === 'ar'
? 'يرجى قراءة شروط الاستخدام التالية بعناية قبل استخدام المنصة'
: 'Please read the following terms of use carefully before using the platform'}
</p>
</motion.div>
<div className="space-y-6">
{terms.map((term, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
>
<div className="flex items-start gap-4">
<div className="w-10 h-10 bg-amber-100 rounded-xl flex items-center justify-center shrink-0 mt-1">
<Shield className="w-5 h-5 text-amber-600" />
{loading && (
<div className="flex items-center justify-center gap-3 mb-8 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-4 text-amber-800">
<Loader2 className="h-5 w-5 animate-spin" />
<span>
{language === 'ar'
? 'جاري تحميل شروط الاستخدام...'
: 'Loading terms of use...'}
</span>
</div>
)}
{error && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8 rounded-2xl border border-red-200 bg-red-50 p-4 flex items-center gap-3 text-red-700"
>
<AlertCircle className="h-5 w-5 shrink-0" />
<span>{error}</span>
</motion.div>
)}
{!loading && terms.length === 0 && !error && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center py-16 text-gray-500"
>
<FileText className="h-16 w-16 mx-auto mb-4 text-gray-300" />
<p className="text-xl font-medium">
{language === 'ar'
? 'لا توجد شروط استخدام متاحة حالياً'
: 'No terms of use available'}
</p>
</motion.div>
)}
{terms.length > 0 && (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="space-y-6"
>
{terms.map((term, index) => (
<motion.div
key={index}
variants={itemVariants}
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
>
<div className="flex items-start gap-4">
<div className="w-10 h-10 bg-amber-100 rounded-xl flex items-center justify-center shrink-0 mt-1">
<Shield className="w-5 h-5 text-amber-600" />
</div>
<div className="min-w-0 flex-1">
{term.title && (
<h2 className="text-xl font-bold text-gray-900 mb-3">{term.title}</h2>
)}
<p className="text-gray-600 leading-relaxed whitespace-pre-wrap">
{term.description}
</p>
</div>
</div>
<div>
<h2 className="text-xl font-bold text-gray-900 mb-3">{term.title}</h2>
<p className="text-gray-600 leading-relaxed">{term.content}</p>
</div>
</div>
</motion.div>
))}
</div>
</motion.div>
))}
</motion.div>
)}
<motion.div
initial={{ opacity: 0 }}
@ -102,9 +245,13 @@ export default function TermsPage() {
>
<CheckCircle className="w-6 h-6 text-amber-600 shrink-0 mt-0.5" />
<div>
<p className="font-bold text-amber-800 mb-1">آخر تحديث</p>
<p className="font-bold text-amber-800 mb-1">
{language === 'ar' ? 'آخر تحديث' : 'Last Updated'}
</p>
<p className="text-amber-700">
تم آخر تحديث لشروط الاستخدام في 1 مايو 2026. يرجى مراجعة هذه الصفحة بشكل دوري للاطلاع على أي تغييرات.
{language === 'ar'
? 'تم آخر تحديث لشروط الاستخدام في 1 مايو 2026. يرجى مراجعة هذه الصفحة بشكل دوري للاطلاع على أي تغييرات.'
: 'Last updated on May 1, 2026. Please review this page periodically for any changes.'}
</p>
</div>
</motion.div>

View File

@ -373,99 +373,20 @@
// // ─── Booking/Reservation Management ───
// export async function confirmDepositPayment(bookingId) {
// return apiFetch('/Reservations/ConfirmDepositPayment', {
// method: 'POST',
// body: JSON.stringify({ 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,
// };
// 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';
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) {
return typeof FormData !== 'undefined' && value instanceof FormData;
return typeof FormData !== "undefined" && value instanceof FormData;
}
class ApiBlockedError extends Error {
constructor(message = 'Your account is blocked') {
constructor(message = "Your account is blocked") {
super(message);
this.name = 'ApiBlockedError';
this.name = "ApiBlockedError";
this.status = 451;
}
}
@ -475,8 +396,11 @@ export function isApiBlockedError(error) {
}
function redirectToBlockedPage() {
if (typeof window !== 'undefined' && window.location.pathname !== '/blocked') {
window.location.replace('/blocked');
if (
typeof window !== "undefined" &&
window.location.pathname !== "/blocked"
) {
window.location.replace("/blocked");
}
}
@ -488,7 +412,7 @@ function assertNotBlocked(response) {
}
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 bodyIsFormData = isFormData(options.body);
if (hasBody && !bodyIsFormData && !headers['Content-Type'] && !headers['content-type']) {
headers['Content-Type'] = 'application/json';
if (
hasBody &&
!bodyIsFormData &&
!headers["Content-Type"] &&
!headers["content-type"]
) {
headers["Content-Type"] = "application/json";
}
const url = `${API_BASE}${endpoint}`;
console.log('API Request:', url);
console.log('API Method:', options.method || 'GET');
console.log('API Body:', hasBody ? options.body : null);
console.log("API Request:", url);
console.log("API Method:", options.method || "GET");
console.log("API Body:", hasBody ? options.body : null);
const res = await fetch(url, {
...options,
headers,
body:
hasBody && !bodyIsFormData && typeof options.body !== 'string'
hasBody && !bodyIsFormData && typeof options.body !== "string"
? JSON.stringify(options.body)
: options.body,
});
console.log('API Response Status:', res.status);
console.log('API Response OK:', res.ok);
console.log("API Response Status:", res.status);
console.log("API Response OK:", res.ok);
assertNotBlocked(res);
if (!res.ok && res.status !== 206) {
const text = await res.text().catch(() => '');
console.error('API Error Response:', text || res.statusText);
const text = await res.text().catch(() => "");
console.error("API Error Response:", text || res.statusText);
throw new Error(`API ${res.status}: ${text || res.statusText}`);
}
@ -539,7 +468,7 @@ async function apiFetch(endpoint, options = {}) {
try {
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;
@ -556,15 +485,15 @@ async function authFetch(endpoint, body, token = null) {
const bodyIsFormData = isFormData(body);
if (!bodyIsFormData) {
headers['Content-Type'] = 'application/json';
headers["Content-Type"] = "application/json";
}
if (token) {
headers['Authorization'] = `Bearer ${token}`;
headers["Authorization"] = `Bearer ${token}`;
}
const res = await fetch(`${API_BASE}${endpoint}`, {
method: 'POST',
method: "POST",
headers,
body: bodyIsFormData ? body : JSON.stringify(body),
});
@ -576,23 +505,29 @@ async function authFetch(endpoint, body, token = null) {
try {
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;
}
} catch {
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) {
const res = await fetch(buildApiUrl(REPORT_API_BASE, endpoint), {
method: 'POST',
method: "POST",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
@ -604,22 +539,28 @@ async function reportFetch(endpoint, body) {
try {
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;
}
} catch {
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 ───
export async function getRentProperties() {
return apiFetch('/RentProperties/GetRentProperties');
return apiFetch("/RentProperties/GetRentProperties");
}
export async function getRentProperty(id) {
@ -628,20 +569,22 @@ export async function getRentProperty(id) {
export async function getRentPropertyLocations(params = {}) {
const qs = new URLSearchParams();
if (params.maxOffset != null) qs.set('maxOffset', params.maxOffset);
if (params.minOffset != null) qs.set('minOffset', params.minOffset);
if (params.maxOffset != null) qs.set("maxOffset", params.maxOffset);
if (params.minOffset != null) qs.set("minOffset", params.minOffset);
const query = qs.toString();
return apiFetch(`/RentProperties/GetRentPropertiesLocations${query ? `?${query}` : ''}`);
return apiFetch(
`/RentProperties/GetRentPropertiesLocations${query ? `?${query}` : ""}`,
);
}
// ─── Sale Properties ───
export async function getSaleProperties() {
return apiFetch('/SaleProperties/GetSaleProperties');
return apiFetch("/SaleProperties/GetSaleProperties");
}
export async function getSaleProperty(id) {
const items = await apiFetch('/SaleProperties/GetSaleProperties');
const items = await apiFetch("/SaleProperties/GetSaleProperties");
if (!Array.isArray(items)) return items;
return items.find((p) => p.id == id) || items[0];
}
@ -655,7 +598,7 @@ export async function getProperty(id) {
// ─── Recommendations ───
export async function getRecommendations() {
return apiFetch('/Recommendations/GetRecommendations');
return apiFetch("/Recommendations/GetRecommendations");
}
export async function getTopRecommendations(count = 10) {
@ -664,36 +607,46 @@ export async function getTopRecommendations(count = 10) {
// ─── Reservations ───
export async function getAvailableDateRanges(propertyId, fromDate = null, toDate = null) {
export async function getAvailableDateRanges(
propertyId,
fromDate = null,
toDate = null,
) {
const qs = new URLSearchParams();
if (fromDate) qs.set('fromDate', fromDate);
if (toDate) qs.set('toDate', toDate);
if (fromDate) qs.set("fromDate", fromDate);
if (toDate) qs.set("toDate", toDate);
const query = qs.toString();
return apiFetch(
`/Reservations/GetAvailableDates/available/${propertyId}${query ? `?${query}` : ''}`
`/Reservations/GetAvailableDates/available/${propertyId}${query ? `?${query}` : ""}`,
);
}
export async function getReservations() {
return apiFetch('/Reservations/GetAllReservations');
return apiFetch("/Reservations/GetAllReservations");
}
export async function getReservation(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();
if (fromDate) qs.set('fromDate', fromDate);
if (toDate) qs.set('toDate', toDate);
if (fromDate) qs.set("fromDate", fromDate);
if (toDate) qs.set("toDate", toDate);
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) {
return apiFetch('/Reservations/BookReservation/book', {
method: 'POST',
return apiFetch("/Reservations/BookReservation/book", {
method: "POST",
body: {
propertyInfoId,
startDate,
@ -704,8 +657,12 @@ export async function bookReservation(propertyInfoId, startDate, endDate) {
// ─── Terms ───
export async function getTerms() {
return apiFetch('/Terms/GetTerms');
export async function getARTerms() {
return apiFetch("/Configuration/GetARTerms");
}
export async function getENTerms() {
return apiFetch("/Configuration/GetENTerms");
}
// ─── Profile ───
@ -721,39 +678,39 @@ export async function getOwnerByUserId(userId) {
// ─── Properties ───
export async function getMyRentListings() {
return apiFetch('/RentProperties/GetMyRentListings');
return apiFetch("/RentProperties/GetMyRentListings");
}
export async function addRentProperty(data) {
return apiFetch('/RentProperties/AddRentProperty', {
method: 'POST',
return apiFetch("/RentProperties/AddRentProperty", {
method: "POST",
body: data,
});
}
export async function editRentProperty(id, data) {
return apiFetch(`/RentProperties/EditRentProperty/${id}`, {
method: 'PUT',
method: "PUT",
body: data,
});
}
export async function editSaleProperty(id, data) {
return apiFetch(`/SaleProperties/EditSaleProperty/${id}`, {
method: 'PUT',
method: "PUT",
body: data,
});
}
export async function addSaleProperty(data) {
return apiFetch('/SaleProperties/AddSaleProperty', {
method: 'POST',
return apiFetch("/SaleProperties/AddSaleProperty", {
method: "POST",
body: data,
});
}
export async function getMySaleListings() {
return apiFetch('/SaleProperties/GetMySaleListings');
return apiFetch("/SaleProperties/GetMySaleListings");
}
export async function getSalePropertyById(id) {
@ -762,14 +719,14 @@ export async function getSalePropertyById(id) {
export async function updateRentPropertyStatus(id, status) {
return apiFetch(`/RentProperties/UpdateStatus/${id}`, {
method: 'PUT',
method: "PUT",
body: { status },
});
}
export async function updateSalePropertyStatus(id, status) {
return apiFetch(`/SaleProperties/UpdateStatus/${id}`, {
method: 'PUT',
method: "PUT",
body: { status },
});
}
@ -777,19 +734,19 @@ export async function updateSalePropertyStatus(id, status) {
// ─── Currencies ───
export async function getCurrencies() {
return apiFetch('/Currency/GetAll');
return apiFetch("/Currency/GetAll");
}
// ─── Files ───
export async function uploadPicture(file) {
const formData = new FormData();
formData.append('image', file);
formData.append("image", file);
const token = AuthService.getToken();
const res = await fetch(`${API_BASE}/Files/UploadPicture`, {
method: 'POST',
method: "POST",
headers: {
...(token && { Authorization: `Bearer ${token}` }),
},
@ -816,7 +773,7 @@ async function multipartAuthFetch(endpoint, formData) {
const token = AuthService.getToken();
const res = await fetch(`${API_BASE}${endpoint}`, {
method: 'POST',
method: "POST",
headers: {
...(token && { Authorization: `Bearer ${token}` }),
},
@ -830,100 +787,129 @@ async function multipartAuthFetch(endpoint, formData) {
try {
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;
}
} catch {
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();
formData.append('FirstName', data.firstName || data.FirstName || '');
formData.append('LastName', data.lastName || data.LastName || '');
formData.append('Email', data.email || data.Email || '');
formData.append("FirstName", data.firstName || data.FirstName || "");
formData.append("LastName", data.lastName || data.LastName || "");
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 =
data.whatsAppNumber || data.whatsapp || data.WhatsAppNumber || data.WhatsApp || '';
data.whatsAppNumber ||
data.whatsapp ||
data.WhatsAppNumber ||
data.WhatsApp ||
"";
formData.append('PhoneNumber', phoneValue);
formData.append('Phone', phoneValue);
formData.append('WhatsAppNumber', whatsappValue);
formData.append("PhoneNumber", phoneValue);
formData.append("Phone", phoneValue);
formData.append("WhatsAppNumber", whatsappValue);
formData.append('NationalNumber', data.nationalNumber || data.NationalNumber || '');
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));
formData.append(
"NationalNumber",
data.nationalNumber || data.NationalNumber || "",
);
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 (backImage) formData.append('RearIdCarImagePath', backImage);
if (licenseImage) formData.append('LicenseImagePath', licenseImage);
if (frontImage) formData.append("FrontIdCarImagePath", frontImage);
if (backImage) formData.append("RearIdCarImagePath", backImage);
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) {
const formData = new FormData();
formData.append('FirstName', data.firstName || data.FirstName || '');
formData.append('LastName', data.lastName || data.LastName || '');
formData.append('Email', data.email || '');
formData.append('PhoneNumber', data.phoneNumber || '');
formData.append('WhatsAppNumber', data.whatsAppNumber || '');
formData.append('Phone', data.phone || '');
formData.append('NationalNumber', data.nationalNumber || '');
formData.append('Password', data.password || '');
formData.append('Type', String(data.customerType ?? data.Type ?? 0));
formData.append('Language', '0');
formData.append("FirstName", data.firstName || data.FirstName || "");
formData.append("LastName", data.lastName || data.LastName || "");
formData.append("Email", data.email || "");
formData.append("PhoneNumber", data.phoneNumber || "");
formData.append("WhatsAppNumber", data.whatsAppNumber || "");
formData.append("Phone", data.phone || "");
formData.append("NationalNumber", data.nationalNumber || "");
formData.append("Password", data.password || "");
formData.append("Type", String(data.customerType ?? data.Type ?? 0));
formData.append("Language", "0");
if (frontImage) formData.append('FrontIdCarImagePath', frontImage);
if (backImage) formData.append('RearIdCarImagePath', backImage);
if (frontImage) formData.append("FrontIdCarImagePath", frontImage);
if (backImage) formData.append("RearIdCarImagePath", backImage);
return multipartAuthFetch('/Customer/Add', formData);
return multipartAuthFetch("/Customer/Add", formData);
}
// ─── Auth: Login ───
export async function loginWithEmail(credential, password) {
return authFetch('/Auth/LogInWithEmail', {
return authFetch("/Auth/LogInWithEmail", {
credential,
password,
device: 0,
appVersion: '',
appVersion: "",
});
}
export async function loginWithPhone(credential, password) {
return authFetch('/Auth/LogInWithPhoneNumber', {
return authFetch("/Auth/LogInWithPhoneNumber", {
credential,
password,
device: 0,
appVersion: '',
appVersion: "",
});
}
// ─── Auth: OTP ───
export async function sendEmailOTP() {
return apiFetch('/Auth/SendEmailOTP', { method: 'POST' });
return apiFetch("/Auth/SendEmailOTP", { method: "POST" });
}
export async function sendPhoneOTP() {
return apiFetch('/Auth/SendPhoneNumberOTP', { method: 'POST' });
return apiFetch("/Auth/SendPhoneNumberOTP", { method: "POST" });
}
export async function verifyEmail(code) {
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) {
const token = AuthService.getToken();
return authFetch(`/Auth/VerifyPhoneNumber?code=${encodeURIComponent(code)}`, {}, token);
return authFetch(
`/Auth/VerifyPhoneNumber?code=${encodeURIComponent(code)}`,
{},
token,
);
}
// ─── Helpers ───
@ -933,39 +919,83 @@ export function isEmail(value) {
}
export function isPhoneNumber(value) {
return /^\+?\d{7,15}$/.test(value.replace(/[\s\-()]/g, ''));
return /^\+?\d{7,15}$/.test(value.replace(/[\s\-()]/g, ""));
}
// ─── Favorites ───
export async function getUserFavoriteProperties() {
return apiFetch('/FavoriteProperty/GetUserFavoriteProperties');
return apiFetch("/FavoriteProperty/GetUserFavoriteProperties");
}
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) {
return apiFetch(`/FavoriteProperty/Remove?favePropId=${favePropId}`, { method: 'DELETE' });
return apiFetch(`/FavoriteProperty/Remove?favePropId=${favePropId}`, {
method: "DELETE",
});
}
export async function getUserNotifications() {
return apiFetch('/Notifications/GetUserNotifications');
return apiFetch("/Notifications/GetUserNotifications");
}
// ─── Booking/Reservation Management ───
export async function confirmDepositPayment(bookingId) {
return apiFetch('/Reservations/ConfirmDepositPayment', {
method: 'POST',
return apiFetch("/Reservations/ConfirmDepositPayment", {
method: "POST",
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) {
return apiFetch('/Reservations/UpdateStatus', {
method: 'PUT',
return apiFetch("/Reservations/UpdateStatus", {
method: "PUT",
body: { bookingId, status },
});
}
@ -973,31 +1003,31 @@ export async function updateBookingStatus(bookingId, status) {
// ─── Owner / Reservations ───
export async function getOwnerReservationRequests() {
return apiFetch('/Reservations/GetOwnerResevationRequests');
return apiFetch("/Reservations/GetOwnerResevationRequests");
}
export async function getOwnerReservationsByStatuses(filterStatuses) {
return apiFetch('/Reservations/GetAllReservationsByStateForOwner', {
method: 'POST',
return apiFetch("/Reservations/GetAllReservationsByStateForOwner", {
method: "POST",
body: { filterStatuses },
});
}
export async function getUserReservations() {
return apiFetch('/Reservations/GetUserResevations');
return apiFetch("/Reservations/GetUserResevations");
}
export async function ownerConfirmReservation(id) {
return apiFetch(`/Reservations/OwnerConfirmReservation/owner-confirm/${id}`, {
method: 'PUT',
method: "PUT",
});
}
// ─── Payments ───
export async function payDeposit(data) {
return apiFetch('/Reservations/PayDeposit/pay-deposit', {
method: 'POST',
return apiFetch("/Reservations/PayDeposit/pay-deposit", {
method: "POST",
body: data,
});
}
@ -1006,12 +1036,12 @@ export async function payDeposit(data) {
export async function getOwnerContactInformation(propertyInformationId) {
return apiFetch(
`/Owner/GetOwnerContactInformation?propertyInformationId=${propertyInformationId}`
`/Owner/GetOwnerContactInformation?propertyInformationId=${propertyInformationId}`,
);
}
export async function getOwnerStatistics() {
return apiFetch('/Statistics/GetOwnerStatistics');
return apiFetch("/Statistics/GetOwnerStatistics");
}
// ─── Agent Registration ───
@ -1020,7 +1050,7 @@ export async function registerRealEstateAgent(formData) {
const token = AuthService.getToken();
const res = await fetch(`${API_BASE}/RealEstateAgent/Add`, {
method: 'POST',
method: "POST",
headers: {
...(token && { Authorization: `Bearer ${token}` }),
},
@ -1034,7 +1064,7 @@ export async function registerRealEstateAgent(formData) {
try {
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 {
data = text;
}
@ -1043,7 +1073,7 @@ export async function registerRealEstateAgent(formData) {
status: res.status,
data,
ok: res.ok || res.status === 206,
message: data?.message || (typeof data === 'string' ? data : null),
message: data?.message || (typeof data === "string" ? data : null),
};
}
@ -1053,23 +1083,25 @@ export async function changePassword(oldPassword, newPassword) {
return apiFetch(
`/User/ChangePassword?oldPassword=${encodeURIComponent(oldPassword)}&newPassword=${encodeURIComponent(newPassword)}`,
{
method: 'PUT',
}
method: "PUT",
},
);
}
// ─── Forget Password (OTP flow) ───
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) {
return apiFetch(
`/User/VerifyForgetPasswordOTP?email=${encodeURIComponent(email)}&code=${encodeURIComponent(code)}&newPassword=${encodeURIComponent(newPassword)}`,
{
method: 'POST',
}
method: "POST",
},
);
}
@ -1082,16 +1114,19 @@ export async function resetPassword(token) {
// ─── Delete Account ───
export async function deleteMyAccount(password) {
return apiFetch(`/User/DeleteMyAccount?password=${encodeURIComponent(password)}`, {
method: 'DELETE',
});
return apiFetch(
`/User/DeleteMyAccount?password=${encodeURIComponent(password)}`,
{
method: "DELETE",
},
);
}
// ─── Set FCM Token ───
export async function setFCMToken(token, deviceType = 2) {
return apiFetch('/User/SetFCMToken', {
method: 'POST',
return apiFetch("/User/SetFCMToken", {
method: "POST",
body: { token, deviceType },
});
}
@ -1101,61 +1136,63 @@ export async function setFCMToken(token, deviceType = 2) {
export async function filterRentProperties(params = {}) {
const qs = new URLSearchParams();
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();
return apiFetch(`/RentProperties/FilterRentProperties${query ? `?${query}` : ''}`);
return apiFetch(
`/RentProperties/FilterRentProperties${query ? `?${query}` : ""}`,
);
}
// ─── Reports ───
export async function sendGeneralReport(subject, reportBody) {
return reportFetch('/Reports/SendGeneralReport', {
return reportFetch("/Reports/SendGeneralReport", {
subject,
body: reportBody,
});
}
export async function submitReport(subject, body) {
return apiFetch('/Reports/SendGeneralReport', {
method: 'POST',
return apiFetch("/Reports/SendGeneralReport", {
method: "POST",
body: { subject, body },
});
}
export async function submitReservationReport(data) {
return apiFetch('/ReservationReports', {
method: 'POST',
return apiFetch("/ReservationReports", {
method: "POST",
body: data,
});
}
export async function updateReservationReport(id, data) {
return apiFetch(`/ReservationReports/${id}`, {
method: 'PUT',
method: "PUT",
body: data,
});
}
export async function submitSaleReport(data) {
return apiFetch('/SaleReports', {
method: 'POST',
return apiFetch("/SaleReports", {
method: "POST",
body: data,
});
}
export async function updateSaleReport(id, data) {
return apiFetch(`/SaleReports/${id}`, {
method: 'PUT',
method: "PUT",
body: data,
});
}
// ─── Terms (Add) ───
// ─── Terms (Add or Update) ───
export async function addTerm(name, description) {
return apiFetch('/Terms', {
method: 'POST',
body: { name, description },
export async function addOrUpdateTerms(terms) {
return apiFetch("/Terms/AddOrUpdateTerms", {
method: "POST",
body: terms,
});
}
}