Browse Source

feat: implement CustomTripDestinationDetailPage component and related custom trip features

master
Muhammad A. Ghorbani 2 weeks ago
parent
commit
6e4223d5fe
  1. 289
      src/features/custom-trip/CustomTripDestinationDetailPage.tsx
  2. 5
      src/features/custom-trip/CustomTripPage.tsx
  3. 53
      src/features/custom-trip/components/CustomTripDestinationsTab.tsx
  4. 821
      src/features/custom-trip/services/custom-trip-api.ts

289
src/features/custom-trip/CustomTripDestinationDetailPage.tsx

@ -93,6 +93,7 @@ export default function CustomTripDestinationDetailPage() {
const [destIsActive, setDestIsActive] = useState(true) const [destIsActive, setDestIsActive] = useState(true)
const [isDestSavedSuccess, setIsDestSavedSuccess] = useState(false) const [isDestSavedSuccess, setIsDestSavedSuccess] = useState(false)
const [isDestDeleteOpen, setIsDestDeleteOpen] = useState(false) const [isDestDeleteOpen, setIsDestDeleteOpen] = useState(false)
const [isEditDestModalOpen, setIsEditDestModalOpen] = useState(false)
const [isTogglingStatus, setIsTogglingStatus] = useState(false) const [isTogglingStatus, setIsTogglingStatus] = useState(false)
const [isSavingDestination, setIsSavingDestination] = useState(false) const [isSavingDestination, setIsSavingDestination] = useState(false)
@ -438,12 +439,9 @@ export default function CustomTripDestinationDetailPage() {
if (primaryNameEn) setDestNameEn(primaryNameEn) if (primaryNameEn) setDestNameEn(primaryNameEn)
if (primaryDesc) setDestDescription(primaryDesc) if (primaryDesc) setDestDescription(primaryDesc)
setIsDestSavedSuccess(true) setIsDestSavedSuccess(true)
// Slight smooth delay to let user see the button loading state
await new Promise((resolve) => setTimeout(resolve, 450))
// Navigate back to the destinations list tab with a fresh refresh token
navigate('/custom-trip/destinations', { state: { refresh: Date.now() } })
setIsEditDestModalOpen(false)
setIsSavingDestination(false)
setTimeout(() => setIsDestSavedSuccess(false), 3000)
} catch (err) { } catch (err) {
console.error('Failed to save destination:', err) console.error('Failed to save destination:', err)
setIsSavingDestination(false) setIsSavingDestination(false)
@ -872,34 +870,218 @@ export default function CustomTripDestinationDetailPage() {
</div> </div>
{/* ========================================================================= */} {/* ========================================================================= */}
{/* TAB 1: DESTINATION INFO */}
{/* TAB 1: DESTINATION INFO (شناسنامه و مشخصات مقصد) */}
{/* ========================================================================= */} {/* ========================================================================= */}
{currentTab === 'info' && ( {currentTab === 'info' && (
<div className="space-y-6 pt-1 sm:pt-2"> <div className="space-y-6 pt-1 sm:pt-2">
<form onSubmit={handleSaveDestinationInfo} className="space-y-4">
<div className="rounded-2xl bg-card border border-border/70 p-5 sm:p-6 shadow-xs space-y-5">
<div className="flex items-center justify-between border-b border-border/60 pb-4">
{/* کارت اصلی شناسنامه شهر */}
<div className="rounded-2xl bg-card border border-border/70 p-5 sm:p-6 shadow-xs space-y-6">
{/* هدر: نام شهر، وضعیت و دکمه‌های اکشن */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 border-b border-border/60 pb-5">
<div className="flex items-start gap-3">
<span className="text-3xl select-none">{destFlag || destination.flag || '📍'}</span>
<div> <div>
<h2 className="text-base sm:text-lg font-black text-foreground flex items-center gap-2">
<span className="text-2xl select-none">{destFlag || '📍'}</span>
<span>اطلاعات و شناسنامه شهر {destination.name}</span>
<div className="flex items-center gap-2.5 flex-wrap">
<h2 className="text-lg sm:text-xl font-black text-foreground">
{destination.name}
</h2> </h2>
<p className="text-xs text-muted-foreground mt-0.5">
ویرایش مشخصات اصلی، موقعیت کشور و توضیحات مقصد
{destination.nameEn && (
<span className="text-xs font-mono text-muted-foreground font-normal">
({destination.nameEn})
</span>
)}
<span
className={cn(
'inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-semibold',
destIsActive
? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20'
: 'bg-muted text-muted-foreground border border-border-soft'
)}
>
<span
className={cn(
'size-1.5 rounded-full',
destIsActive ? 'bg-emerald-500 animate-pulse' : 'bg-muted-foreground'
)}
/>
<span>{destIsActive ? 'شهر فعال در سامانه' : 'شهر غیرفعال'}</span>
</span>
</div>
<p className="text-xs text-muted-foreground mt-1">
شناسنامه شهر، اماکن متبرکه، هتل‌های مرتبط و ترجمه‌های بین‌المللی مقصد
</p> </p>
</div> </div>
</div>
{/* دکمه‌های عملیات */}
<div className="flex items-center gap-2 shrink-0">
<Button
onClick={() => setIsEditDestModalOpen(true)}
className="rounded-xl h-10 px-4 text-xs sm:text-sm font-bold gap-2 bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs cursor-pointer"
>
<Ic name="pencil" className="size-4" />
<span>ویرایش در پاپ‌آپ</span>
</Button>
<Button
variant="outline"
onClick={() => setIsDestDeleteOpen(true)}
className="rounded-xl h-10 px-3 text-xs font-medium text-rose-600 hover:bg-rose-500/10 hover:text-rose-700 border-border/70 cursor-pointer"
title="حذف مقصد"
>
<Ic name="trash" className="size-4" />
</Button>
</div>
</div>
{/* اعلان موفقیت ذخیره در صورت وجود */}
{isDestSavedSuccess && ( {isDestSavedSuccess && (
<div className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-emerald-500/10 border border-emerald-500/30 text-emerald-600 text-xs font-bold animate-fadeIn">
<Ic name="checkCircle" className="size-4" />
<span>تغییرات با موفقیت ذخیره شد</span>
<div className="flex items-center gap-2 p-3 rounded-xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-600 text-xs font-bold animate-fadeIn">
<Ic name="checkCircle" className="size-4 shrink-0" />
<span>مشخصات شهر مقصد با موفقیت در سامانه ذخیره و به‌روزرسانی شد.</span>
</div>
)}
{/* کارت‌های آماری سریع */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="p-3.5 rounded-xl bg-muted/30 border border-border/60">
<span className="text-[11px] text-muted-foreground block">کشور مقصد</span>
<span className="text-sm font-bold text-foreground mt-1 flex items-center gap-1.5">
<span className="text-base select-none">{destFlag || destination.flag || '📍'}</span>
<span>{destCountry || destination.country}</span>
</span>
</div>
<div
onClick={() => handleTabChange('hotels')}
className="p-3.5 rounded-xl bg-muted/30 border border-border/60 cursor-pointer hover:border-primary/50 transition-colors"
title="مشاهده هتل‌ها"
>
<span className="text-[11px] text-muted-foreground block">هتل‌های فعال</span>
<span className="text-sm font-mono font-bold text-foreground mt-1 flex items-center gap-1.5">
<Ic name="bed" className="size-4 text-primary" />
<span>{toFa(destHotels.length)} هتل</span>
</span>
</div>
<div
onClick={() => handleTabChange('transports')}
className="p-3.5 rounded-xl bg-muted/30 border border-border/60 cursor-pointer hover:border-primary/50 transition-colors"
title="مشاهده مسیرهای حمل‌ونقل"
>
<span className="text-[11px] text-muted-foreground block">مسیرهای متصل</span>
<span className="text-sm font-mono font-bold text-foreground mt-1 flex items-center gap-1.5">
<Ic name="route" className="size-4 text-primary" />
<span>{toFa(destRelations.length)} مسیر</span>
</span>
</div>
<div className="p-3.5 rounded-xl bg-muted/30 border border-border/60">
<span className="text-[11px] text-muted-foreground block">وضعیت در سامانه</span>
<div className="mt-1 flex items-center justify-between">
<span className="text-xs font-bold text-foreground">
{destIsActive ? 'فعال' : 'غیرفعال'}
</span>
<Switch
checked={destIsActive}
onCheckedChange={handleToggleStatus}
isLoading={isTogglingStatus}
disabled={isTogglingStatus}
aria-label="تغییر وضعیت فعال یا غیرفعال بودن شهر مقصد"
/>
</div>
</div>
</div>
{/* ماژول نمایش مشخصات چندزبانه با ServiceTranslation در حالت Readonly */}
<div className="space-y-4 rounded-2xl border border-border/70 bg-card p-4 sm:p-5">
<div className="border-b border-border/60 pb-3">
<ServiceTranslation
activeLang={activeLangTab}
onLangChange={(lang) => setActiveLangTab(lang)}
completedLangs={{
ar: Boolean(names.ar?.trim()),
fa: Boolean(names.fa?.trim()),
en: Boolean(names.en?.trim()),
ru: Boolean(names.ru?.trim()),
id: Boolean(names.id?.trim()),
}}
primaryLang="ar"
title="شناسنامه و متون چندزبانه مقصد"
variant="pills"
/>
</div>
{/* محتوای زبان انتخاب‌شده */}
<div className="space-y-4 pt-1" dir={currentLang.dir}>
<div className="space-y-1">
<span className="text-[11px] text-muted-foreground font-semibold block">
نام شهر مقصد ({currentLang.name}):
</span>
<div className="p-3 rounded-xl bg-muted/40 border border-border/60 text-sm font-bold text-foreground">
{names[activeLangTab] || destination.name || '---'}
</div>
</div>
{holyPlacesMap[activeLangTab] && (
<div className="space-y-1.5">
<span className="text-[11px] text-muted-foreground font-semibold block">
اماکن مقدسه و زیارتگاه‌ها ({currentLang.name}):
</span>
<div className="flex flex-wrap gap-1.5 p-3 rounded-xl bg-muted/40 border border-border/60">
{holyPlacesMap[activeLangTab]
.split(/[،,]/)
.map((p) => p.trim())
.filter(Boolean)
.map((place, idx) => (
<span
key={idx}
className="inline-flex items-center gap-1 px-2.5 py-1 rounded-lg bg-card border border-border/70 text-xs font-semibold text-foreground shadow-2xs"
>
<span>🕌</span>
<span>{place}</span>
</span>
))}
</div>
</div> </div>
)} )}
<div className="space-y-1">
<span className="text-[11px] text-muted-foreground font-semibold block">
توضیحات و راهنمای شهر ({currentLang.name}):
</span>
<div className="p-3.5 rounded-xl bg-muted/40 border border-border/60 text-xs text-foreground leading-relaxed whitespace-pre-wrap">
{descriptionsMap[activeLangTab] || destination.description || 'توضیحاتی برای این زبان ثبت نشده است.'}
</div> </div>
</div>
</div>
</div>
</div>
{/* ========================================================================= */}
{/* MODAL: EDIT DESTINATION INFO (طبق مدل popup.tsx / dialog.tsx) */}
{/* ========================================================================= */}
<Dialog open={isEditDestModalOpen} onOpenChange={setIsEditDestModalOpen}>
<DialogContent className="sm:max-w-xl md:max-w-2xl w-full rounded-2xl bg-card border border-border-soft p-0 max-h-[88vh] flex flex-col overflow-hidden shadow-2xl">
{/* هدر ثابت مدال */}
<DialogHeader className="p-5 sm:p-6 pb-4 pe-12 border-b border-border/60 shrink-0 bg-card text-start">
<div className="flex items-center gap-3">
<div className="size-10 rounded-xl bg-primary/10 text-primary flex items-center justify-center shrink-0">
<Ic name="map" className="size-5" />
</div>
<div>
<DialogTitle className="text-base sm:text-lg font-bold text-foreground">
ویرایش اطلاعات و شناسنامه شهر {destination.name}
</DialogTitle>
<DialogDescription className="text-xs text-muted-foreground mt-0.5">
ویرایش مشخصات اصلی، موقعیت کشور و توضیحات چندزبانه مقصد
</DialogDescription>
</div>
</div>
</DialogHeader>
{/* فرم: بدنه اسکرول‌شونده و فوتر فیکس‌شده */}
<form onSubmit={handleSaveDestinationInfo} className="flex flex-col flex-1 overflow-hidden min-h-0">
<div className="flex-1 overflow-y-auto p-5 sm:p-6 space-y-4 text-xs sm:text-sm">
{/* تنظیمات کلی شهر مقصد (کشور و وضعیت) */} {/* تنظیمات کلی شهر مقصد (کشور و وضعیت) */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 p-4 rounded-2xl bg-card border border-border/70 shadow-2xs">
{/* 1. Country Selection with Radix Select */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 p-3.5 rounded-xl bg-card border border-border/70 shadow-2xs">
{/* انتخاب کشور مقصد */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-xs font-bold text-foreground block"> <label className="text-xs font-bold text-foreground block">
کشور مقصد: کشور مقصد:
@ -928,7 +1110,7 @@ export default function CustomTripDestinationDetailPage() {
</Select> </Select>
</div> </div>
{/* 2. Active Status Toggle Switch with Mini Spinner */}
{/* وضعیت شهر در سامانه */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-xs font-bold text-foreground block"> <label className="text-xs font-bold text-foreground block">
وضعیت شهر در سامانه: وضعیت شهر در سامانه:
@ -954,17 +1136,15 @@ export default function CustomTripDestinationDetailPage() {
</div> </div>
<Switch <Switch
checked={destIsActive} checked={destIsActive}
onCheckedChange={handleToggleStatus}
isLoading={isTogglingStatus}
disabled={isTogglingStatus}
aria-label="تغییر وضعیت فعال یا غیرفعال بودن شهر مقصد"
onCheckedChange={setDestIsActive}
aria-label="تغییر وضعیت فعال بودن شهر مقصد"
/> />
</div> </div>
</div> </div>
</div> </div>
{/* ماژول چندزبانه ترجمه با ServiceTranslation */}
<div className="space-y-4 rounded-2xl border border-border/70 bg-card p-4 sm:p-5">
{/* ماژول چندزبانه ترجمه */}
<div className="space-y-4 rounded-2xl border border-border/70 bg-card p-4">
<div className="border-b border-border/60 pb-3"> <div className="border-b border-border/60 pb-3">
<ServiceTranslation <ServiceTranslation
activeLang={activeLangTab} activeLang={activeLangTab}
@ -983,9 +1163,8 @@ export default function CustomTripDestinationDetailPage() {
/> />
</div> </div>
{/* فیلدهای ترجمه‌پذیر متناسب با زبان تب فعال */}
<div className="space-y-4 pt-1" dir={currentLang.dir}>
{/* نام شهر مقصد به زبان انتخاب‌شده */}
{/* فیلدهای ترجمه‌پذیر متناسب با زبان فعال */}
<div className="space-y-3.5 pt-1" dir={currentLang.dir}>
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="text-xs font-bold text-foreground flex items-center gap-1.5"> <label className="text-xs font-bold text-foreground flex items-center gap-1.5">
@ -1012,7 +1191,22 @@ export default function CustomTripDestinationDetailPage() {
/> />
</div> </div>
{/* توضیحات و راهنمای شهر به زبان انتخاب‌شده */}
<div className="space-y-1.5">
<label className="text-xs font-bold text-foreground flex items-center gap-1.5">
<span>اماکن مقدسه و زیارتگاه‌ها ({currentLang.name})</span>
<span className="text-[11px] text-muted-foreground font-normal">(با ویرگول جدا کنید)</span>
</label>
<Input
value={holyPlacesMap[activeLangTab] || ''}
onChange={(e) =>
setHolyPlacesMap((prev) => ({ ...prev, [activeLangTab]: e.target.value }))
}
dir={currentLang.dir}
placeholder={`اماکن متبرکه به زبان ${currentLang.name}...`}
className="h-10 rounded-xl text-xs bg-card border-border/70 focus-visible:border-primary focus-visible:ring-1 focus-visible:ring-primary shadow-2xs"
/>
</div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-xs font-bold text-foreground block"> <label className="text-xs font-bold text-foreground block">
توضیحات و راهنمای شهر ({currentLang.name}): توضیحات و راهنمای شهر ({currentLang.name}):
@ -1023,39 +1217,38 @@ export default function CustomTripDestinationDetailPage() {
setDescriptionsMap((prev) => ({ ...prev, [activeLangTab]: e.target.value })) setDescriptionsMap((prev) => ({ ...prev, [activeLangTab]: e.target.value }))
} }
dir={currentLang.dir} dir={currentLang.dir}
rows={4}
rows={3}
placeholder={`شرح کوتاه اماکن مقدسه، شرایط سفر و ویژگی‌های شهر به زبان ${currentLang.name}...`} placeholder={`شرح کوتاه اماکن مقدسه، شرایط سفر و ویژگی‌های شهر به زبان ${currentLang.name}...`}
className="w-full p-3 text-xs rounded-xl bg-card border border-border/70 text-foreground placeholder:text-muted-foreground leading-relaxed focus:border-primary focus:ring-1 focus:ring-primary focus:outline-none resize-y shadow-2xs" className="w-full p-3 text-xs rounded-xl bg-card border border-border/70 text-foreground placeholder:text-muted-foreground leading-relaxed focus:border-primary focus:ring-1 focus:ring-primary focus:outline-none resize-y shadow-2xs"
/> />
</div> </div>
</div> </div>
</div> </div>
</div>
{/* Save Footer Bar */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pt-3 border-t border-border/60">
{/* فوتر فیکس‌شده در پایین مدال */}
<DialogFooter className="p-4 sm:p-5 border-t border-border-soft bg-card/95 backdrop-blur-xs flex items-center justify-end gap-2.5 shrink-0 mt-0">
<Button
type="button"
variant="outline"
onClick={() => setIsEditDestModalOpen(false)}
className="rounded-xl text-xs h-9 px-4 border-border-soft hover:bg-muted"
>
انصراف
</Button>
<Button <Button
type="submit" type="submit"
isLoading={isSavingDestination} isLoading={isSavingDestination}
loadingText={t('common.loading')} loadingText={t('common.loading')}
disabled={isSavingDestination}
className="h-10 px-6 rounded-xl font-bold text-xs sm:text-sm bg-primary text-white hover:bg-primary/90 shadow-xs"
className="rounded-xl text-xs font-bold h-9 px-5 bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs"
> >
<Ic name="check" className="size-4" /> <Ic name="check" className="size-4" />
<span>ذخیره و ثبت تغییرات شهر مقصد</span> <span>ذخیره و ثبت تغییرات شهر مقصد</span>
</Button> </Button>
<Button
type="button"
variant="ghost"
onClick={() => setIsDestDeleteOpen(true)}
className="h-10 px-4 rounded-xl text-xs font-bold text-rose-600 hover:bg-rose-500/10 hover:text-rose-700"
>
<Ic name="trash" className="size-4" />
<span>حذف این شهر مقصد</span>
</Button>
</div>
</div>
</DialogFooter>
</form> </form>
</DialogContent>
</Dialog>
{/* Delete Destination Dialog */} {/* Delete Destination Dialog */}
<DeleteConfirmDialog <DeleteConfirmDialog

5
src/features/custom-trip/CustomTripPage.tsx

@ -17,6 +17,8 @@ import {
saveCustomTripOrigin, saveCustomTripOrigin,
deleteCustomTripOrigin, deleteCustomTripOrigin,
fetchCustomTripDestinations, fetchCustomTripDestinations,
saveCustomTripDestination,
deleteCustomTripDestination,
fetchCustomTripTransportRelations, fetchCustomTripTransportRelations,
fetchCustomTripHotels, fetchCustomTripHotels,
} from './services/custom-trip-api' } from './services/custom-trip-api'
@ -139,14 +141,17 @@ export default function CustomTripPage() {
// Destinations CRUD // Destinations CRUD
const handleAddDestination = (item: CustomTripDestinationItem) => { const handleAddDestination = (item: CustomTripDestinationItem) => {
setDestinations((prev) => [item, ...prev]) setDestinations((prev) => [item, ...prev])
saveCustomTripDestination(item).catch(() => {})
} }
const handleUpdateDestination = (item: CustomTripDestinationItem) => { const handleUpdateDestination = (item: CustomTripDestinationItem) => {
setDestinations((prev) => prev.map((d) => (d.id === item.id ? item : d))) setDestinations((prev) => prev.map((d) => (d.id === item.id ? item : d)))
saveCustomTripDestination(item).catch(() => {})
} }
const handleDeleteDestination = (id: string | number) => { const handleDeleteDestination = (id: string | number) => {
setDestinations((prev) => prev.filter((d) => d.id !== id)) setDestinations((prev) => prev.filter((d) => d.id !== id))
deleteCustomTripDestination(id).catch(() => {})
} }
// Relations CRUD // Relations CRUD

53
src/features/custom-trip/components/CustomTripDestinationsTab.tsx

@ -689,34 +689,67 @@ export function CustomTripDestinationsTab({
</span> </span>
</td> </td>
<td className="px-4 py-3.5 text-center"> <td className="px-4 py-3.5 text-center">
<span
className={`inline-flex items-center gap-1.5 text-xs font-semibold px-2.5 py-1 rounded-full ${
<button
type="button"
onClick={(e) => {
e.stopPropagation()
if (onUpdateDestination) {
onUpdateDestination({ ...item, isActive: !item.isActive })
}
}}
className={`inline-flex items-center gap-1.5 text-xs font-semibold px-2.5 py-1 rounded-full transition-all cursor-pointer ${
item.isActive item.isActive
? 'bg-emerald-500/10 text-emerald-600'
: 'bg-muted text-muted-foreground'
? 'bg-emerald-500/10 text-emerald-600 hover:bg-emerald-500/20'
: 'bg-muted text-muted-foreground hover:bg-muted/80'
}`} }`}
title="تغییر وضعیت فعال/غیرفعال"
> >
<span <span
className={`size-1.5 rounded-full ${ className={`size-1.5 rounded-full ${
item.isActive ? 'bg-emerald-500 animate-pulse' : 'bg-muted-foreground' item.isActive ? 'bg-emerald-500 animate-pulse' : 'bg-muted-foreground'
}`} }`}
/> />
{item.isActive ? t('customTrip.active') : t('customTrip.inactive')}
</span>
<span>{item.isActive ? t('customTrip.active') : t('customTrip.inactive')}</span>
</button>
</td> </td>
<td className="px-4 py-3.5 text-center"> <td className="px-4 py-3.5 text-center">
<div className="flex items-center justify-center">
<div className="flex items-center justify-center gap-1">
<Button <Button
variant="outline"
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
handleOpenEdit(item)
}}
className="size-8 p-0 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground cursor-pointer"
title="ویرایش مشخصات در پاپ‌آپ"
>
<Ic name="pencil" className="size-3.5" />
</Button>
<Button
variant="ghost"
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
navigate(`/custom-trip/destinations/${item.id}`) navigate(`/custom-trip/destinations/${item.id}`)
}} }}
className="h-8 px-3 rounded-xl text-xs gap-1.5 font-bold text-primary border-primary/30 group-hover:bg-primary group-hover:text-white transition-all shadow-2xs"
className="size-8 p-0 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground cursor-pointer"
title="مشاهده و مدیریت هتل‌ها و مسیرها"
> >
<Ic name="eye" className="size-3.5" /> <Ic name="eye" className="size-3.5" />
<span>مشاهده</span>
</Button>
<Button
variant="ghost"
size="sm"
type="button"
onClick={(e) => {
e.stopPropagation()
setItemToDelete(item)
}}
className="size-8 p-0 rounded-lg hover:bg-rose-500/10 text-muted-foreground hover:text-rose-600 cursor-pointer"
title="حذف مقصد"
>
<Ic name="trash" className="size-3.5" />
</Button> </Button>
</div> </div>
</td> </td>

821
src/features/custom-trip/services/custom-trip-api.ts

@ -129,35 +129,709 @@ const MOCK_ORDERS: CustomTripOrderItem[] = [
// Mock Origins organized by core countries // Mock Origins organized by core countries
const MOCK_ORIGINS: CustomTripOriginItem[] = [ const MOCK_ORIGINS: CustomTripOriginItem[] = [
// عمان (Oman)
{ id: 'muscat', name: 'مسقط', nameEn: 'Muscat', country: 'عُمان', countryCode: 'OM', flag: '🇴🇲', isInternational: true, isActive: true },
{ id: 'salalah', name: 'صلاله', nameEn: 'Salalah', country: 'عُمان', countryCode: 'OM', flag: '🇴🇲', isInternational: true, isActive: true },
// بحرین (Bahrain)
{ id: 'manama', name: 'منامه', nameEn: 'Manama', country: 'بحرین', countryCode: 'BH', flag: '🇧🇭', isInternational: true, isActive: true },
{ id: 'muharraq', name: 'المحرق', nameEn: 'Muharraq', country: 'بحرین', countryCode: 'BH', flag: '🇧🇭', isInternational: true, isActive: true },
// کویت (Kuwait)
{ id: 'kuwait_city', name: 'کویت', nameEn: 'Kuwait City', country: 'کویت', countryCode: 'KW', flag: '🇰🇼', isInternational: true, isActive: true },
{ id: 'al_ahmadi', name: 'الاحمدی', nameEn: 'Al Ahmadi', country: 'کویت', countryCode: 'KW', flag: '🇰🇼', isInternational: true, isActive: true },
// عربستان سعودی (Saudi Arabia)
{ id: 'riyadh', name: 'ریاض', nameEn: 'Riyadh', country: 'عربستان سعودی', countryCode: 'SA', flag: '🇸🇦', isInternational: true, isActive: true },
{ id: 'jeddah', name: 'جده', nameEn: 'Jeddah', country: 'عربستان سعودی', countryCode: 'SA', flag: '🇸🇦', isInternational: true, isActive: true },
{ id: 'dammam', name: 'دمام', nameEn: 'Dammam', country: 'عربستان سعودی', countryCode: 'SA', flag: '🇸🇦', isInternational: true, isActive: true },
{ id: 'qatif', name: 'قطیف', nameEn: 'Qatif', country: 'عربستان سعودی', countryCode: 'SA', flag: '🇸🇦', isInternational: true, isActive: true },
// ایران (Iran)
{ id: 'tehran', name: 'تهران', nameEn: 'Tehran', country: 'ایران', countryCode: 'IR', flag: '🇮🇷', isInternational: false, isActive: true },
{ id: 'mashhad', name: 'مشهد', nameEn: 'Mashhad', country: 'ایران', countryCode: 'IR', flag: '🇮🇷', isInternational: false, isActive: true },
{ id: 'qom', name: 'قم', nameEn: 'Qom', country: 'ایران', countryCode: 'IR', flag: '🇮🇷', isInternational: false, isActive: true },
{ id: 'isfahan', name: 'اصفهان', nameEn: 'Isfahan', country: 'ایران', countryCode: 'IR', flag: '🇮🇷', isInternational: false, isActive: true },
{ id: 'shiraz', name: 'شیراز', nameEn: 'Shiraz', country: 'ایران', countryCode: 'IR', flag: '🇮🇷', isInternational: false, isActive: true },
{ id: 'ahvaz', name: 'اهواز', nameEn: 'Ahvaz', country: 'ایران', countryCode: 'IR', flag: '🇮🇷', isInternational: false, isActive: true },
// عراق (Iraq)
{ id: 'baghdad', name: 'بغداد', nameEn: 'Baghdad', country: 'عراق', countryCode: 'IQ', flag: '🇮🇶', isInternational: true, isActive: true },
{ id: 'basra', name: 'بصره', nameEn: 'Basra', country: 'عراق', countryCode: 'IQ', flag: '🇮🇶', isInternational: true, isActive: true },
// ۱. عربستان سعودی (Saudi Arabia)
{
id: 'riyadh',
name: 'الرياض',
nameEn: 'Riyadh',
country: 'عربستان سعودی',
countryCode: 'SA',
flag: '🇸🇦',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'الرياض' },
fa: { name: 'ریاض' },
en: { name: 'Riyadh' },
ru: { name: 'Эр-Рияд' },
id: { name: 'Riyadh' },
},
},
{
id: 'qatif',
name: 'القطيف',
nameEn: 'Qatif',
country: 'عربستان سعودی',
countryCode: 'SA',
flag: '🇸🇦',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'القطيف' },
fa: { name: 'قطیف' },
en: { name: 'Qatif' },
ru: { name: 'Катиф' },
id: { name: 'Qatif' },
},
},
{
id: 'dammam',
name: 'الدمام',
nameEn: 'Dammam',
country: 'عربستان سعودی',
countryCode: 'SA',
flag: '🇸🇦',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'الدمام' },
fa: { name: 'دمام' },
en: { name: 'Dammam' },
ru: { name: 'Даммам' },
id: { name: 'Dammam' },
},
},
{
id: 'al_ahsa',
name: 'الأحساء',
nameEn: 'Al-Ahsa',
country: 'عربستان سعودی',
countryCode: 'SA',
flag: '🇸🇦',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'الأحساء' },
fa: { name: 'احساء' },
en: { name: 'Al-Ahsa' },
ru: { name: 'Эль-Ахса' },
id: { name: 'Al-Ahsa' },
},
},
{
id: 'jeddah',
name: 'جدة',
nameEn: 'Jeddah',
country: 'عربستان سعودی',
countryCode: 'SA',
flag: '🇸🇦',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'جدة' },
fa: { name: 'جده' },
en: { name: 'Jeddah' },
ru: { name: 'Джидда' },
id: { name: 'Jeddah' },
},
},
{
id: 'medina',
name: 'المدينة المنورة',
nameEn: 'Medina',
country: 'عربستان سعودی',
countryCode: 'SA',
flag: '🇸🇦',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'المدينة المنورة' },
fa: { name: 'مدینه منوره' },
en: { name: 'Medina' },
ru: { name: 'Медина' },
id: { name: 'Madinah' },
},
},
{
id: 'mecca',
name: 'مكة المكرمة',
nameEn: 'Mecca',
country: 'عربستان سعودی',
countryCode: 'SA',
flag: '🇸🇦',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'مكة المكرمة' },
fa: { name: 'مکه مکرمه' },
en: { name: 'Mecca' },
ru: { name: 'Мекка' },
id: { name: 'Makkah' },
},
},
// ۲. امارات متحده عربی (United Arab Emirates)
{
id: 'dubai',
name: 'دبي',
nameEn: 'Dubai',
country: 'امارات متحده عربی',
countryCode: 'AE',
flag: '🇦🇪',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'دبي' },
fa: { name: 'دبی' },
en: { name: 'Dubai' },
ru: { name: 'Дубай' },
id: { name: 'Dubai' },
},
},
{
id: 'abu_dhabi',
name: 'أبوظبي',
nameEn: 'Abu Dhabi',
country: 'امارات متحده عربی',
countryCode: 'AE',
flag: '🇦🇪',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'أبوظبي' },
fa: { name: 'ابوظبی' },
en: { name: 'Abu Dhabi' },
ru: { name: 'Абу-Даби' },
id: { name: 'Abu Dhabi' },
},
},
{
id: 'sharjah',
name: 'الشارقة',
nameEn: 'Sharjah',
country: 'امارات متحده عربی',
countryCode: 'AE',
flag: '🇦🇪',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'الشارقة' },
fa: { name: 'شارجه' },
en: { name: 'Sharjah' },
ru: { name: 'Шарджа' },
id: { name: 'Sharjah' },
},
},
// ۳. قطر (Qatar)
{
id: 'doha',
name: 'الدوحة',
nameEn: 'Doha',
country: 'قطر',
countryCode: 'QA',
flag: '🇶🇦',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'الدوحة' },
fa: { name: 'دوحه' },
en: { name: 'Doha' },
ru: { name: 'Доха' },
id: { name: 'Doha' },
},
},
{
id: 'al_rayyan',
name: 'الريان',
nameEn: 'Al Rayyan',
country: 'قطر',
countryCode: 'QA',
flag: '🇶🇦',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'الريان' },
fa: { name: 'الریان' },
en: { name: 'Al Rayyan' },
ru: { name: 'Эр-Райян' },
id: { name: 'Al Rayyan' },
},
},
// ۴. کویت (Kuwait)
{
id: 'kuwait_city',
name: 'مدينة الكويت',
nameEn: 'Kuwait City',
country: 'کویت',
countryCode: 'KW',
flag: '🇰🇼',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'مدينة الكويت' },
fa: { name: 'شهر کویت' },
en: { name: 'Kuwait City' },
ru: { name: 'Эль-Кувейт' },
id: { name: 'Kota Kuwait' },
},
},
{
id: 'al_ahmadi',
name: 'الأحمدي',
nameEn: 'Al Ahmadi',
country: 'کویت',
countryCode: 'KW',
flag: '🇰🇼',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'الأحمدي' },
fa: { name: 'الاحمدی' },
en: { name: 'Al Ahmadi' },
ru: { name: 'Аль-Ахмади' },
id: { name: 'Al Ahmadi' },
},
},
{
id: 'hawally',
name: 'حولي',
nameEn: 'Hawally',
country: 'کویت',
countryCode: 'KW',
flag: '🇰🇼',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'حولي' },
fa: { name: 'حولی' },
en: { name: 'Hawally' },
ru: { name: 'Хавалли' },
id: { name: 'Hawally' },
},
},
// ۵. بحرین (Bahrain)
{
id: 'manama',
name: 'المنامة',
nameEn: 'Manama',
country: 'بحرین',
countryCode: 'BH',
flag: '🇧🇭',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'المنامة' },
fa: { name: 'منامه' },
en: { name: 'Manama' },
ru: { name: 'Манама' },
id: { name: 'Manama' },
},
},
{
id: 'muharraq',
name: 'المحرق',
nameEn: 'Muharraq',
country: 'بحرین',
countryCode: 'BH',
flag: '🇧🇭',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'المحرق' },
fa: { name: 'المحرق' },
en: { name: 'Muharraq' },
ru: { name: 'Мухаррак' },
id: { name: 'Muharraq' },
},
},
{
id: 'sitra',
name: 'سترة',
nameEn: 'Sitra',
country: 'بحرین',
countryCode: 'BH',
flag: '🇧🇭',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'سترة' },
fa: { name: 'ستره' },
en: { name: 'Sitra' },
ru: { name: 'Ситра' },
id: { name: 'Sitra' },
},
},
{
id: 'riffa',
name: 'الرفاع',
nameEn: 'Riffa',
country: 'بحرین',
countryCode: 'BH',
flag: '🇧🇭',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'الرفاع' },
fa: { name: 'الرفاع' },
en: { name: 'Riffa' },
ru: { name: 'Эр-Рифа' },
id: { name: 'Riffa' },
},
},
// ۶. عُمان (Oman)
{
id: 'muscat',
name: 'مسقط',
nameEn: 'Muscat',
country: 'عُمان',
countryCode: 'OM',
flag: '🇴🇲',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'مسقط' },
fa: { name: 'مسقط' },
en: { name: 'Muscat' },
ru: { name: 'Маскат' },
id: { name: 'Muskat' },
},
},
{
id: 'salalah',
name: 'صلالة',
nameEn: 'Salalah',
country: 'عُمان',
countryCode: 'OM',
flag: '🇴🇲',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'صلالة' },
fa: { name: 'صلاله' },
en: { name: 'Salalah' },
ru: { name: 'Салала' },
id: { name: 'Salalah' },
},
},
{
id: 'sohar',
name: 'صحار',
nameEn: 'Sohar',
country: 'عُمان',
countryCode: 'OM',
flag: '🇴🇲',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'صحار' },
fa: { name: 'صحار' },
en: { name: 'Sohar' },
ru: { name: 'Сохар' },
id: { name: 'Sohar' },
},
},
{
id: 'nizwa',
name: 'نزوى',
nameEn: 'Nizwa',
country: 'عُمان',
countryCode: 'OM',
flag: '🇴🇲',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'نزوى' },
fa: { name: 'نزوی' },
en: { name: 'Nizwa' },
ru: { name: 'Низва' },
id: { name: 'Nizwa' },
},
},
// ۷. عراق (Iraq)
{
id: 'baghdad',
name: 'بغداد',
nameEn: 'Baghdad',
country: 'عراق',
countryCode: 'IQ',
flag: '🇮🇶',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'بغداد' },
fa: { name: 'بغداد' },
en: { name: 'Baghdad' },
ru: { name: 'Багдад' },
id: { name: 'Bagdad' },
},
},
{
id: 'basra',
name: 'البصرة',
nameEn: 'Basra',
country: 'عراق',
countryCode: 'IQ',
flag: '🇮🇶',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'البصرة' },
fa: { name: 'بصره' },
en: { name: 'Basra' },
ru: { name: 'Басра' },
id: { name: 'Basra' },
},
},
{
id: 'erbil',
name: 'أربيل',
nameEn: 'Erbil',
country: 'عراق',
countryCode: 'IQ',
flag: '🇮🇶',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'أربيل' },
fa: { name: 'اربیل' },
en: { name: 'Erbil' },
ru: { name: 'Эрбиль' },
id: { name: 'Erbil' },
},
},
{
id: 'najaf',
name: 'النجف الأشرف',
nameEn: 'Najaf',
country: 'عراق',
countryCode: 'IQ',
flag: '🇮🇶',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'النجف الأشرف' },
fa: { name: 'نجف اشرف' },
en: { name: 'Najaf' },
ru: { name: 'Эн-Наджаф' },
id: { name: 'Najaf' },
},
},
{
id: 'karbala',
name: 'كربلاء المقدسة',
nameEn: 'Karbala',
country: 'عراق',
countryCode: 'IQ',
flag: '🇮🇶',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'كربلاء المقدسة' },
fa: { name: 'کربلای معلی' },
en: { name: 'Karbala' },
ru: { name: 'Кербела' },
id: { name: 'Karbala' },
},
},
// ۸. لبنان (Lebanon)
{
id: 'beirut',
name: 'بيروت',
nameEn: 'Beirut',
country: 'لبنان',
countryCode: 'LB',
flag: '🇱🇧',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'بيروت' },
fa: { name: 'بیروت' },
en: { name: 'Beirut' },
ru: { name: 'Бейрут' },
id: { name: 'Beirut' },
},
},
{
id: 'sidon',
name: 'صيدا',
nameEn: 'Sidon',
country: 'لبنان',
countryCode: 'LB',
flag: '🇱🇧',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'صيدا' },
fa: { name: 'صیدا' },
en: { name: 'Sidon' },
ru: { name: 'Сайда' },
id: { name: 'Sidon' },
},
},
// ۹. سوریه (Syria)
{
id: 'damascus',
name: 'دمشق',
nameEn: 'Damascus',
country: 'سوریه',
countryCode: 'SY',
flag: '🇸🇾',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'دمشق' },
fa: { name: 'دمشق' },
en: { name: 'Damascus' },
ru: { name: 'Дамаск' },
id: { name: 'Damaskus' },
},
},
// ۱۰. اردن (Jordan)
{
id: 'amman',
name: 'عمّان',
nameEn: 'Amman',
country: 'اردن',
countryCode: 'JO',
flag: '🇯🇴',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'عمّان' },
fa: { name: 'امان' },
en: { name: 'Amman' },
ru: { name: 'Амман' },
id: { name: 'Amman' },
},
},
// ۱۱. مصر (Egypt)
{
id: 'cairo',
name: 'القاهرة',
nameEn: 'Cairo',
country: 'مصر',
countryCode: 'EG',
flag: '🇪🇬',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'القاهرة' },
fa: { name: 'قاهره' },
en: { name: 'Cairo' },
ru: { name: 'Каир' },
id: { name: 'Kairo' },
},
},
{
id: 'alexandria',
name: 'الإسكندرية',
nameEn: 'Alexandria',
country: 'مصر',
countryCode: 'EG',
flag: '🇪🇬',
isInternational: true,
isActive: true,
translations: {
ar: { name: 'الإسكندرية' },
fa: { name: 'اسکندریه' },
en: { name: 'Alexandria' },
ru: { name: 'Александрия' },
id: { name: 'Aleksandria' },
},
},
// ۱۲. ایران (Iran)
{
id: 'tehran',
name: 'تهران',
nameEn: 'Tehran',
country: 'ایران',
countryCode: 'IR',
flag: '🇮🇷',
isInternational: false,
isActive: true,
translations: {
ar: { name: 'طهران' },
fa: { name: 'تهران' },
en: { name: 'Tehran' },
ru: { name: 'Тегеран' },
id: { name: 'Teheran' },
},
},
{
id: 'mashhad',
name: 'مشهد',
nameEn: 'Mashhad',
country: 'ایران',
countryCode: 'IR',
flag: '🇮🇷',
isInternational: false,
isActive: true,
translations: {
ar: { name: 'مشهد' },
fa: { name: 'مشهد' },
en: { name: 'Mashhad' },
ru: { name: 'Мешхед' },
id: { name: 'Masyhad' },
},
},
{
id: 'qom',
name: 'قم',
nameEn: 'Qom',
country: 'ایران',
countryCode: 'IR',
flag: '🇮🇷',
isInternational: false,
isActive: true,
translations: {
ar: { name: 'قم' },
fa: { name: 'قم' },
en: { name: 'Qom' },
ru: { name: 'Кум' },
id: { name: 'Qom' },
},
},
{
id: 'isfahan',
name: 'اصفهان',
nameEn: 'Isfahan',
country: 'ایران',
countryCode: 'IR',
flag: '🇮🇷',
isInternational: false,
isActive: true,
translations: {
ar: { name: 'أصفهان' },
fa: { name: 'اصفهان' },
en: { name: 'Isfahan' },
ru: { name: 'Исфахан' },
id: { name: 'Isfahan' },
},
},
{
id: 'shiraz',
name: 'شیراز',
nameEn: 'Shiraz',
country: 'ایران',
countryCode: 'IR',
flag: '🇮🇷',
isInternational: false,
isActive: true,
translations: {
ar: { name: 'شيراز' },
fa: { name: 'شیراز' },
en: { name: 'Shiraz' },
ru: { name: 'Шираз' },
id: { name: 'Syiraz' },
},
},
{
id: 'ahvaz',
name: 'اهواز',
nameEn: 'Ahvaz',
country: 'ایران',
countryCode: 'IR',
flag: '🇮🇷',
isInternational: false,
isActive: true,
translations: {
ar: { name: 'الأهواز' },
fa: { name: 'اهواز' },
en: { name: 'Ahvaz' },
ru: { name: 'Ахваз' },
id: { name: 'Ahvaz' },
},
},
] ]
// Mock Destinations // Mock Destinations
@ -847,7 +1521,7 @@ const STORAGE_KEYS = {
DESTINATIONS: 'aqila_custom_destinations_v2', DESTINATIONS: 'aqila_custom_destinations_v2',
HOTELS: 'aqila_custom_hotels_v2', HOTELS: 'aqila_custom_hotels_v2',
RELATIONS: 'aqila_custom_relations_v2', RELATIONS: 'aqila_custom_relations_v2',
ORIGINS: 'aqila_custom_origins_v2',
ORIGINS: 'aqila_custom_origins_v3',
} }
function getStored<T>(key: string, fallback: T): T { function getStored<T>(key: string, fallback: T): T {
@ -869,7 +1543,12 @@ function setStored<T>(key: string, value: T): void {
} }
export async function fetchCustomTripOrigins(): Promise<CustomTripOriginItem[]> { export async function fetchCustomTripOrigins(): Promise<CustomTripOriginItem[]> {
return getStored(STORAGE_KEYS.ORIGINS, MOCK_ORIGINS)
const stored = getStored<CustomTripOriginItem[] | null>(STORAGE_KEYS.ORIGINS, null)
if (!stored || !Array.isArray(stored) || stored.length < 25) {
setStored(STORAGE_KEYS.ORIGINS, MOCK_ORIGINS)
return MOCK_ORIGINS
}
return stored
} }
export async function saveCustomTripOrigin(item: CustomTripOriginItem): Promise<CustomTripOriginItem> { export async function saveCustomTripOrigin(item: CustomTripOriginItem): Promise<CustomTripOriginItem> {
@ -886,6 +1565,30 @@ export async function deleteCustomTripOrigin(id: string | number): Promise<void>
} }
export async function fetchCustomTripDestinations(): Promise<CustomTripDestinationItem[]> { export async function fetchCustomTripDestinations(): Promise<CustomTripDestinationItem[]> {
try {
const remote = await apiFetch<CustomTripDestinationItem[]>('trip/custom/v2/catalog/destinations/?include_inactive=true')
if (remote && Array.isArray(remote) && remote.length > 0) {
// If local storage has user-modified values (such as added 1s or custom test names), sync them
const stored = getStored<CustomTripDestinationItem[] | null>(STORAGE_KEYS.DESTINATIONS, null)
if (stored && Array.isArray(stored)) {
for (const localItem of stored) {
const match = remote.find((r) => String(r.id) === String(localItem.id) || r.name === localItem.name)
if (match && localItem.name && localItem.name !== match.name) {
match.name = localItem.name
apiFetch(`trip/custom/v2/catalog/destinations/${match.id}/`, {
method: 'PATCH',
body: JSON.stringify({ name: localItem.name }),
}).catch(() => {})
}
}
}
setStored(STORAGE_KEYS.DESTINATIONS, remote)
return remote
}
} catch (err) {
console.warn('[CustomTrip API] Could not fetch destinations from backend, falling back to cache:', err)
}
const stored = getStored<CustomTripDestinationItem[] | null>(STORAGE_KEYS.DESTINATIONS, null) const stored = getStored<CustomTripDestinationItem[] | null>(STORAGE_KEYS.DESTINATIONS, null)
if ( if (
!stored || !stored ||
@ -912,16 +1615,60 @@ export async function fetchCustomTripDestinationById(id: string | number): Promi
} }
export async function saveCustomTripDestination(item: CustomTripDestinationItem): Promise<CustomTripDestinationItem> { export async function saveCustomTripDestination(item: CustomTripDestinationItem): Promise<CustomTripDestinationItem> {
const list = await fetchCustomTripDestinations()
const exists = list.some((d) => d.id === item.id)
const updated = exists ? list.map((d) => (d.id === item.id ? item : d)) : [item, ...list]
let savedItem = item
try {
const payload = {
id: String(item.id),
city_key: String(item.id),
name: item.name,
nameEn: item.nameEn,
name_en: item.nameEn,
country: item.country,
flag: item.flag,
description: item.description,
holyPlaces: item.holyPlaces,
holy_places: item.holyPlaces,
hotelsCount: item.hotelsCount,
hotels_count: item.hotelsCount,
transportsCount: item.transportsCount,
transports_count: item.transportsCount,
isActive: item.isActive,
is_active: item.isActive,
translations: item.translations,
}
const res = await apiFetch<CustomTripDestinationItem>(`trip/custom/v2/catalog/destinations/${item.id}/`, {
method: 'PUT',
body: JSON.stringify(payload),
}).catch(async () => {
return await apiFetch<CustomTripDestinationItem>('trip/custom/v2/catalog/destinations/', {
method: 'POST',
body: JSON.stringify(payload),
})
})
if (res && res.id) {
savedItem = { ...item, ...res }
}
} catch (err) {
console.warn('[CustomTrip API] Could not save destination to backend, cached locally:', err)
}
const list = getStored<CustomTripDestinationItem[]>(STORAGE_KEYS.DESTINATIONS, MOCK_DESTINATIONS)
const exists = list.some((d) => String(d.id) === String(savedItem.id))
const updated = exists ? list.map((d) => (String(d.id) === String(savedItem.id) ? savedItem : d)) : [savedItem, ...list]
setStored(STORAGE_KEYS.DESTINATIONS, updated) setStored(STORAGE_KEYS.DESTINATIONS, updated)
return item
return savedItem
} }
export async function deleteCustomTripDestination(id: string | number): Promise<void> { export async function deleteCustomTripDestination(id: string | number): Promise<void> {
const list = await fetchCustomTripDestinations()
setStored(STORAGE_KEYS.DESTINATIONS, list.filter((d) => d.id !== id))
try {
await apiFetch(`trip/custom/v2/catalog/destinations/${id}/`, {
method: 'DELETE',
})
} catch (err) {
console.warn('[CustomTrip API] Could not delete destination on backend:', err)
}
const list = getStored<CustomTripDestinationItem[]>(STORAGE_KEYS.DESTINATIONS, MOCK_DESTINATIONS)
setStored(STORAGE_KEYS.DESTINATIONS, list.filter((d) => String(d.id) !== String(id)))
} }
export async function fetchCustomTripTransportRelations(): Promise<CustomTripTransportRelationItem[]> { export async function fetchCustomTripTransportRelations(): Promise<CustomTripTransportRelationItem[]> {

Loading…
Cancel
Save