You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
673 lines
24 KiB
673 lines
24 KiB
import { apiFetch } from '@/services/http'
|
|
import type {
|
|
CustomTripOrderItem,
|
|
CustomTripOriginItem,
|
|
CustomTripDestinationItem,
|
|
CustomTripTransportItem,
|
|
CustomTripTransportRelationItem,
|
|
CustomTripHotelItem,
|
|
CustomTripOrderStatus,
|
|
} from '../types'
|
|
|
|
// Clear any legacy localStorage keys so panel strictly uses PostgreSQL database
|
|
if (typeof window !== 'undefined' && window.localStorage) {
|
|
const legacyKeys = [
|
|
'aqila_custom_destinations_v5',
|
|
'aqila_custom_hotels_v5',
|
|
'aqila_custom_relations_v5',
|
|
'aqila_custom_origins_v4',
|
|
'aqila_custom_destinations_v4',
|
|
'aqila_custom_hotels_v4',
|
|
'aqila_custom_relations_v4',
|
|
]
|
|
for (const key of legacyKeys) {
|
|
try {
|
|
window.localStorage.removeItem(key)
|
|
} catch {}
|
|
}
|
|
}
|
|
|
|
function cleanLegacyDetail(rawDetail: any): string {
|
|
if (!rawDetail || typeof rawDetail !== 'string') return ''
|
|
const s = rawDetail.trim()
|
|
if (s.startsWith('{') && (s.includes('beginTrip') || s.includes('number_passenger'))) {
|
|
return ''
|
|
}
|
|
return s
|
|
}
|
|
|
|
function addDaysIso(dateStr: string, days: number): string {
|
|
const d = new Date(`${dateStr.slice(0, 10)}T00:00:00Z`)
|
|
if (Number.isNaN(d.getTime())) {
|
|
const fallback = new Date(Date.now() + 7 * 86400000)
|
|
fallback.setUTCDate(fallback.getUTCDate() + days)
|
|
return fallback.toISOString().slice(0, 10)
|
|
}
|
|
d.setUTCDate(d.getUTCDate() + days)
|
|
return d.toISOString().slice(0, 10)
|
|
}
|
|
|
|
function getDefaultStartDateIso(createdStr?: string): string {
|
|
const d = createdStr ? new Date(createdStr) : new Date()
|
|
if (Number.isNaN(d.getTime())) {
|
|
return new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 10)
|
|
}
|
|
d.setUTCDate(d.getUTCDate() + 7)
|
|
return d.toISOString().slice(0, 10)
|
|
}
|
|
|
|
function mapBackendOrderToPanel(raw: any): CustomTripOrderItem {
|
|
const stays = Array.isArray(raw.stays) ? raw.stays : []
|
|
const rawLegs = Array.isArray(raw.legs) ? raw.legs : []
|
|
const passengers = Array.isArray(raw.passengers) ? raw.passengers : []
|
|
const rawServices = Array.isArray(raw.services) ? raw.services : []
|
|
const originCity = raw.origin_city || raw.originCity || ''
|
|
const destinations: string[] = Array.isArray(raw.destinations) ? raw.destinations : []
|
|
|
|
let legs = rawLegs.map((leg: any, idx: number) => {
|
|
const isReturn = Boolean(
|
|
leg.is_return ||
|
|
leg.isReturn ||
|
|
(idx === rawLegs.length - 1 &&
|
|
rawLegs.length > stays.length &&
|
|
stays.length > 0 &&
|
|
leg.to_city === originCity)
|
|
)
|
|
const matchingStay = isReturn
|
|
? null
|
|
: stays[idx] || stays.find((s: any) => s.city === leg.to_city)
|
|
const methodCode = (Number(leg.transport_method) || 1) as 1 | 2 | 3
|
|
return {
|
|
fromCity: leg.from_city || '',
|
|
toCity: isReturn ? leg.to_city || originCity || '' : leg.to_city || '',
|
|
transportMethod:
|
|
leg.transport_method_display ||
|
|
leg.transport_name_snapshot ||
|
|
(methodCode === 2 ? 'قطار (ریلی)' : methodCode === 3 ? 'زمینی (اتوبوس / ون VIP)' : 'پرواز (هوایی)'),
|
|
transportMethodCode: methodCode,
|
|
transportId: leg.transport_id ?? null,
|
|
hotelId: isReturn ? null : matchingStay?.hotel_id ?? null,
|
|
hotelName: isReturn ? '' : matchingStay?.hotel_name_snapshot || '',
|
|
stayNights: isReturn ? 0 : matchingStay?.nights || 3,
|
|
mealPlan: isReturn ? '' : matchingStay?.meal_plan || 'FULL_BOARD',
|
|
departureDate: leg.departure_date || matchingStay?.check_in || null,
|
|
departureTime: leg.departure_time || (isReturn ? '18:00' : '09:00'),
|
|
checkIn: isReturn ? null : matchingStay?.check_in || null,
|
|
checkOut: isReturn ? null : matchingStay?.check_out || null,
|
|
isReturn,
|
|
}
|
|
})
|
|
|
|
// If legacy order had destinations/stays but no legs, synthesize editable legs
|
|
if (legs.length === 0 && destinations.length > 0) {
|
|
legs = destinations.map((destCity, idx) => {
|
|
const matchingStay = stays.find((s: any) => s.city === destCity) || stays[idx]
|
|
return {
|
|
fromCity: idx === 0 ? originCity : destinations[idx - 1],
|
|
toCity: destCity,
|
|
transportMethod: 'پرواز (هوایی)',
|
|
transportMethodCode: 1 as const,
|
|
transportId: null,
|
|
hotelId: matchingStay?.hotel_id ?? null,
|
|
hotelName: matchingStay?.hotel_name_snapshot || '',
|
|
stayNights: matchingStay?.nights || 3,
|
|
mealPlan: matchingStay?.meal_plan || 'FULL_BOARD',
|
|
departureDate: matchingStay?.check_in || null,
|
|
departureTime: '09:00',
|
|
checkIn: matchingStay?.check_in || null,
|
|
checkOut: matchingStay?.check_out || null,
|
|
isReturn: false,
|
|
}
|
|
})
|
|
}
|
|
|
|
// Ensure last leg is Return to Origin City (has transport, no hotel) & dates are sequential
|
|
if (legs.length > 0 && originCity) {
|
|
const hasReturn = legs.some((l) => l.isReturn)
|
|
if (!hasReturn) {
|
|
const lastDest = legs[legs.length - 1].toCity || originCity
|
|
legs.push({
|
|
fromCity: lastDest,
|
|
toCity: originCity,
|
|
transportMethod: 'پرواز (هوایی)',
|
|
transportMethodCode: 1 as const,
|
|
transportId: null,
|
|
hotelId: null,
|
|
hotelName: '',
|
|
stayNights: 0,
|
|
mealPlan: '',
|
|
departureDate: null,
|
|
departureTime: '18:00',
|
|
checkIn: null,
|
|
checkOut: null,
|
|
isReturn: true,
|
|
})
|
|
}
|
|
|
|
let curDate = legs[0].departureDate || getDefaultStartDateIso(raw.created)
|
|
legs = legs.map((l, idx) => {
|
|
const fromCity = idx === 0 ? originCity || l.fromCity : legs[idx - 1].toCity || l.fromCity
|
|
if (l.isReturn) {
|
|
return {
|
|
...l,
|
|
fromCity,
|
|
toCity: originCity,
|
|
hotelId: null,
|
|
hotelName: '',
|
|
stayNights: 0,
|
|
mealPlan: '',
|
|
departureDate: curDate,
|
|
departureTime: l.departureTime || '18:00',
|
|
checkIn: null,
|
|
checkOut: null,
|
|
isReturn: true,
|
|
}
|
|
}
|
|
const nights = Math.max(1, Number(l.stayNights) || 3)
|
|
const depDate = idx === 0 && l.departureDate ? l.departureDate : curDate
|
|
const checkOut = addDaysIso(depDate, nights)
|
|
curDate = checkOut
|
|
return {
|
|
...l,
|
|
fromCity,
|
|
stayNights: nights,
|
|
departureDate: depDate,
|
|
departureTime: l.departureTime || '09:00',
|
|
checkIn: depDate,
|
|
checkOut,
|
|
isReturn: false,
|
|
}
|
|
})
|
|
}
|
|
|
|
const adultsCount = passengers.filter((p: any) => p.passenger_type === 'ADULT').length || raw.passenger_count || 1
|
|
const childrenCount = passengers.filter((p: any) => p.passenger_type === 'CHILD').length || 0
|
|
const infantsCount = passengers.filter((p: any) => p.passenger_type === 'INFANT').length || 0
|
|
|
|
const mappedPassengers = passengers.map((p: any) => ({
|
|
firstName: p.first_name || p.firstName || '',
|
|
lastName: p.last_name || p.lastName || '',
|
|
passportNumber: p.passport_number || p.passportNumber || '',
|
|
phoneNumber: p.phone_number || p.phoneNumber || '',
|
|
birthDate: p.birth_date || p.birthDate || '',
|
|
passengerType: (p.passenger_type || p.passengerType || 'ADULT') as 'ADULT' | 'CHILD' | 'INFANT',
|
|
passportImage: p.passport_image || p.passportImage || null,
|
|
}))
|
|
|
|
const mappedServices = rawServices.map((s: any) => ({
|
|
serviceType: s.service_type || s.serviceType || '',
|
|
quantity: Number(s.quantity) || 1,
|
|
totalPrice: Number(s.total_price ?? s.totalPrice ?? 0),
|
|
}))
|
|
|
|
return {
|
|
id: Number(raw.id),
|
|
trackingCode: raw.tracking_code || raw.trackingCode || '',
|
|
user: {
|
|
id: raw.user?.id || raw.id,
|
|
username: raw.customer_name || raw.user?.username || `user_${raw.id}`,
|
|
fullName: raw.customer_name || raw.user?.fullName || raw.user?.fullname || 'زائر گرامی',
|
|
phoneNumber: raw.contact_phone || raw.whatsapp_number || raw.user?.phoneNumber || '',
|
|
email: raw.user?.email || '',
|
|
},
|
|
originCity,
|
|
destinations,
|
|
legs,
|
|
passengers: mappedPassengers,
|
|
services: mappedServices,
|
|
adultsCount,
|
|
childrenCount,
|
|
infantsCount,
|
|
priceEstimate: Number(raw.final_price ?? raw.price_estimate ?? raw.priceEstimate ?? 0),
|
|
finalPrice: raw.final_price !== null && raw.final_price !== undefined ? Number(raw.final_price) : null,
|
|
factorId: raw.factor_id ?? null,
|
|
factorNumber: raw.factor_number ?? null,
|
|
status: (raw.status || 'PENDING') as CustomTripOrderStatus,
|
|
created: raw.created || new Date().toISOString(),
|
|
detail: cleanLegacyDetail(raw.detail),
|
|
adminNotes: raw.admin_notes || raw.adminNotes || '',
|
|
}
|
|
}
|
|
|
|
export async function fetchCustomTripOrders(): Promise<CustomTripOrderItem[]> {
|
|
try {
|
|
const res = await apiFetch<any>('trip/custom/v2/admin/orders/?page_size=100')
|
|
const list = Array.isArray(res) ? res : Array.isArray(res?.results) ? res.results : []
|
|
return list.map(mapBackendOrderToPanel)
|
|
} catch (err) {
|
|
console.warn('[CustomTrip API] Could not fetch admin orders from v2 endpoint:', err)
|
|
return []
|
|
}
|
|
}
|
|
|
|
export async function fetchCustomTripOrderDetail(orderId: number): Promise<CustomTripOrderItem | null> {
|
|
try {
|
|
const res = await apiFetch<any>(`trip/custom/v2/admin/orders/${orderId}/`)
|
|
if (res && res.id) {
|
|
return mapBackendOrderToPanel(res)
|
|
}
|
|
return null
|
|
} catch (err) {
|
|
console.warn(`[CustomTrip API] Could not fetch order ${orderId} detail:`, err)
|
|
// Fallback: search in orders list
|
|
try {
|
|
const orders = await fetchCustomTripOrders()
|
|
return orders.find((o) => o.id === orderId) || null
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function updateCustomTripOrder(
|
|
orderId: number,
|
|
data: {
|
|
status?: CustomTripOrderStatus
|
|
priceEstimate?: number
|
|
adminNotes?: string
|
|
originCity?: string
|
|
legs?: {
|
|
fromCity: string
|
|
toCity: string
|
|
transportMethodCode?: 1 | 2 | 3
|
|
transportMethod?: string
|
|
hotelId?: number | string | null
|
|
hotelName?: string
|
|
stayNights?: number
|
|
mealPlan?: string
|
|
departureDate?: string | null
|
|
departureTime?: string | null
|
|
checkIn?: string | null
|
|
checkOut?: string | null
|
|
isReturn?: boolean
|
|
}[]
|
|
transferToBilling?: boolean
|
|
}
|
|
): Promise<CustomTripOrderItem> {
|
|
const itineraryPayload: Record<string, any> = {}
|
|
if (data.originCity) {
|
|
itineraryPayload.origin_city = data.originCity
|
|
}
|
|
if (Array.isArray(data.legs) && data.legs.length > 0) {
|
|
itineraryPayload.legs = data.legs.map((leg, idx) => ({
|
|
sequence: idx + 1,
|
|
from_city: leg.fromCity || (idx === 0 && data.originCity ? data.originCity : ''),
|
|
to_city: leg.toCity || (leg.isReturn && data.originCity ? data.originCity : leg.toCity),
|
|
transport_method: leg.transportMethodCode || 1,
|
|
departure_date: leg.departureDate || null,
|
|
departure_time: leg.departureTime || (leg.isReturn ? '18:00' : '09:00'),
|
|
is_return: Boolean(leg.isReturn),
|
|
}))
|
|
itineraryPayload.stays = data.legs
|
|
.filter((leg) => !leg.isReturn)
|
|
.map((leg, idx) => ({
|
|
sequence: idx + 1,
|
|
city: leg.toCity,
|
|
hotel_id: leg.hotelId ?? null,
|
|
hotel_name: leg.hotelName || '',
|
|
nights: Number(leg.stayNights) || 3,
|
|
meal_plan: leg.mealPlan || 'FULL_BOARD',
|
|
check_in: leg.checkIn || leg.departureDate || null,
|
|
check_out: leg.checkOut || null,
|
|
}))
|
|
}
|
|
|
|
// Stage 2: Finalize order and transfer to Financial Invoices (Billing)
|
|
if (
|
|
(data.transferToBilling || data.status === 'AWAITING_PAYMENT') &&
|
|
data.priceEstimate !== undefined &&
|
|
data.priceEstimate > 0
|
|
) {
|
|
const res = await apiFetch<any>(`trip/custom/v2/admin/orders/${orderId}/quote/`, {
|
|
method: 'POST',
|
|
body: {
|
|
final_price: data.priceEstimate,
|
|
admin_notes: data.adminNotes || '',
|
|
...itineraryPayload,
|
|
},
|
|
})
|
|
return mapBackendOrderToPanel(res)
|
|
}
|
|
|
|
if (data.status === 'REJECTED') {
|
|
const noteText = data.adminNotes || 'رد شده توسط مدیریت'
|
|
const res = await apiFetch<any>(`trip/custom/v2/admin/orders/${orderId}/reject/`, {
|
|
method: 'POST',
|
|
body: {
|
|
note: noteText,
|
|
reason: noteText,
|
|
},
|
|
})
|
|
return mapBackendOrderToPanel(res)
|
|
}
|
|
|
|
// Stage 1: Save changes/edits while keeping order in review (or updating status/items)
|
|
const res = await apiFetch<any>(`trip/custom/v2/admin/orders/${orderId}/`, {
|
|
method: 'PATCH',
|
|
body: {
|
|
status: data.status,
|
|
price_estimate: data.priceEstimate,
|
|
final_price: data.status === 'PENDING' ? undefined : data.priceEstimate,
|
|
admin_notes: data.adminNotes,
|
|
...itineraryPayload,
|
|
},
|
|
})
|
|
return mapBackendOrderToPanel(res)
|
|
}
|
|
|
|
export async function fetchCustomTripOrigins(): Promise<CustomTripOriginItem[]> {
|
|
const remote = await apiFetch<any[]>('trip/custom/v2/catalog/cities/')
|
|
if (!Array.isArray(remote)) return []
|
|
return remote.map((c: any) => ({
|
|
id: c.id ?? c.slug,
|
|
name: c.name || c.nameEn || '',
|
|
nameEn: c.nameEn || c.name || '',
|
|
country: c.country || '',
|
|
countryCode: c.countryCode || c.country_code || '',
|
|
flag: c.flag || '📍',
|
|
isInternational: Boolean(c.isInternational),
|
|
isActive: c.isActive !== undefined ? Boolean(c.isActive) : true,
|
|
translations: c.translations,
|
|
}))
|
|
}
|
|
|
|
export async function saveCustomTripOrigin(item: CustomTripOriginItem): Promise<CustomTripOriginItem> {
|
|
const payload = {
|
|
name: item.nameEn || item.name,
|
|
country: item.country,
|
|
countryCode: item.countryCode,
|
|
has_airport: true,
|
|
has_road_access: true,
|
|
}
|
|
const isNumericId = typeof item.id === 'number' || /^\d+$/.test(String(item.id))
|
|
const res = isNumericId
|
|
? await apiFetch<any>(`trip/custom/v2/catalog/cities/${item.id}/`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(payload),
|
|
}).catch(() =>
|
|
apiFetch<any>('trip/custom/v2/catalog/cities/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
})
|
|
)
|
|
: await apiFetch<any>('trip/custom/v2/catalog/cities/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
})
|
|
|
|
return {
|
|
id: res.id ?? item.id,
|
|
name: res.name || item.name,
|
|
nameEn: res.nameEn || item.nameEn,
|
|
country: res.country || item.country,
|
|
countryCode: res.countryCode || item.countryCode,
|
|
flag: res.flag || item.flag || '📍',
|
|
isInternational: res.isInternational !== undefined ? Boolean(res.isInternational) : item.isInternational,
|
|
isActive: res.isActive !== undefined ? Boolean(res.isActive) : item.isActive,
|
|
translations: res.translations || item.translations,
|
|
}
|
|
}
|
|
|
|
export async function deleteCustomTripOrigin(id: string | number): Promise<void> {
|
|
await apiFetch(`trip/custom/v2/catalog/cities/${id}/`, {
|
|
method: 'DELETE',
|
|
})
|
|
}
|
|
|
|
export async function fetchCustomTripDestinations(): Promise<CustomTripDestinationItem[]> {
|
|
const remote = await apiFetch<CustomTripDestinationItem[]>(
|
|
'trip/custom/v2/catalog/destinations/?include_inactive=true'
|
|
)
|
|
return Array.isArray(remote) ? remote : []
|
|
}
|
|
|
|
export async function fetchCustomTripDestinationById(
|
|
id: string | number
|
|
): Promise<CustomTripDestinationItem | null> {
|
|
try {
|
|
const detail = await apiFetch<CustomTripDestinationItem>(
|
|
`trip/custom/v2/catalog/destinations/${id}/`
|
|
)
|
|
if (detail && detail.id) return detail
|
|
} catch {}
|
|
const list = await fetchCustomTripDestinations()
|
|
const found = list.find(
|
|
(d) =>
|
|
String(d.id) === String(id) ||
|
|
d.name === String(id) ||
|
|
(d.nameEn && d.nameEn.toLowerCase() === String(id).toLowerCase())
|
|
)
|
|
return found || null
|
|
}
|
|
|
|
export async function saveCustomTripDestination(
|
|
item: CustomTripDestinationItem
|
|
): Promise<CustomTripDestinationItem> {
|
|
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,
|
|
image: item.image,
|
|
blog_slug: item.blogSlug || item.blog_slug,
|
|
blogSlug: item.blogSlug || item.blog_slug,
|
|
journeys_count: item.journeysCount ?? item.journeys_count,
|
|
journeysCount: item.journeysCount ?? item.journeys_count,
|
|
short_description: item.shortDescription || item.short_description,
|
|
shortDescription: item.shortDescription || item.short_description,
|
|
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),
|
|
})
|
|
})
|
|
return { ...item, ...res }
|
|
}
|
|
|
|
export async function deleteCustomTripDestination(id: string | number): Promise<void> {
|
|
await apiFetch(`trip/custom/v2/catalog/destinations/${id}/`, {
|
|
method: 'DELETE',
|
|
})
|
|
}
|
|
|
|
export async function fetchCustomTripTransports(): Promise<CustomTripTransportItem[]> {
|
|
const remote = await apiFetch<any[]>('trip/custom/v2/catalog/transports/')
|
|
if (!Array.isArray(remote)) return []
|
|
return remote.map((t: any) => ({
|
|
id: Number(t.id),
|
|
fromCity: t.from_city_display || t.from_city_name || '',
|
|
toCity: t.to_city_display || t.to_city_name || '',
|
|
method: (Number(t.method) || 1) as 1 | 2 | 3,
|
|
methodName: t.method_name || (t.method === 1 ? 'پرواز (هوایی)' : t.method === 2 ? 'قطار (ریلی)' : 'زمینی (اتوبوس / ون VIP)'),
|
|
priceAdult: Number(t.price) || 0,
|
|
priceChild: Number(t.price_child) || 0,
|
|
priceInfant: Number(t.price_infant) || 0,
|
|
isActive: true,
|
|
}))
|
|
}
|
|
|
|
export async function fetchCustomTripTransportRelations(): Promise<CustomTripTransportRelationItem[]> {
|
|
const remote = await apiFetch<any[]>('trip/custom/v2/catalog/transports/')
|
|
if (!Array.isArray(remote)) return []
|
|
|
|
const grouped = new Map<string, CustomTripTransportRelationItem>()
|
|
for (const t of remote) {
|
|
const fromCity = t.from_city_display || t.from_city_name || ''
|
|
const toCity = t.to_city_display || t.to_city_name || ''
|
|
const key = `${t.from_city_id}_${t.to_city_id}`
|
|
const existing = grouped.get(key)
|
|
const method = Number(t.method)
|
|
|
|
if (!existing) {
|
|
grouped.set(key, {
|
|
id: `rel_${t.from_city_id}_${t.to_city_id}`,
|
|
fromCity,
|
|
fromCountry: t.from_country || '',
|
|
toCity,
|
|
toCountry: t.to_country || '',
|
|
hasAirplane: method === 1,
|
|
hasTrain: method === 2,
|
|
hasRoad: method === 3,
|
|
airplaneDetails: method === 1 ? `نرخ پایه بزرگسال: $${Number(t.price || 0)}` : undefined,
|
|
trainDetails: method === 2 ? `نرخ پایه بزرگسال: $${Number(t.price || 0)}` : undefined,
|
|
roadDetails: method === 3 ? `نرخ پایه بزرگسال: $${Number(t.price || 0)}` : undefined,
|
|
isActive: true,
|
|
})
|
|
} else {
|
|
if (method === 1) {
|
|
existing.hasAirplane = true
|
|
existing.airplaneDetails = `نرخ پایه بزرگسال: $${Number(t.price || 0)}`
|
|
} else if (method === 2) {
|
|
existing.hasTrain = true
|
|
existing.trainDetails = `نرخ پایه بزرگسال: $${Number(t.price || 0)}`
|
|
} else if (method === 3) {
|
|
existing.hasRoad = true
|
|
existing.roadDetails = `نرخ پایه بزرگسال: $${Number(t.price || 0)}`
|
|
}
|
|
}
|
|
}
|
|
|
|
return Array.from(grouped.values())
|
|
}
|
|
|
|
export async function saveCustomTripTransportRelation(
|
|
item: CustomTripTransportRelationItem
|
|
): Promise<CustomTripTransportRelationItem> {
|
|
const methods: number[] = []
|
|
if (item.hasAirplane) methods.push(1)
|
|
if (item.hasTrain) methods.push(2)
|
|
if (item.hasRoad) methods.push(3)
|
|
if (methods.length === 0) methods.push(1)
|
|
|
|
for (const method of methods) {
|
|
await apiFetch('trip/custom/v2/catalog/transports/', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
fromCity: item.fromCity,
|
|
fromCountry: item.fromCountry,
|
|
toCity: item.toCity,
|
|
toCountry: item.toCountry,
|
|
method,
|
|
}),
|
|
})
|
|
}
|
|
return item
|
|
}
|
|
|
|
export async function deleteCustomTripTransportRelation(id: string | number): Promise<void> {
|
|
const strId = String(id)
|
|
if (strId.startsWith('rel_')) {
|
|
const parts = strId.split('_')
|
|
if (parts.length >= 3) {
|
|
const fromId = Number(parts[1])
|
|
const toId = Number(parts[2])
|
|
const all = await apiFetch<any[]>('trip/custom/v2/catalog/transports/')
|
|
if (Array.isArray(all)) {
|
|
const matches = all.filter(
|
|
(t) => Number(t.from_city_id) === fromId && Number(t.to_city_id) === toId
|
|
)
|
|
for (const m of matches) {
|
|
await apiFetch(`trip/custom/v2/catalog/transports/${m.id}/`, {
|
|
method: 'DELETE',
|
|
})
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
await apiFetch(`trip/custom/v2/catalog/transports/${id}/`, {
|
|
method: 'DELETE',
|
|
})
|
|
}
|
|
|
|
export async function fetchCustomTripHotels(): Promise<CustomTripHotelItem[]> {
|
|
const remote = await apiFetch<any[]>('trip/custom/v2/catalog/hotels/')
|
|
if (!Array.isArray(remote)) return []
|
|
return remote.map((h: any) => ({
|
|
id: h.id,
|
|
name: h.name || '',
|
|
cityName: h.cityName || h.city_name || '',
|
|
stars: Number(h.stars) || 4,
|
|
score: Number(h.score) || 4.8,
|
|
distanceToShrine:
|
|
h.distanceToShrine ||
|
|
(h.distance_to_city_km ? `${h.distance_to_city_km} کیلومتر تا حرم مطهر` : 'نزدیک به حرم مطهر'),
|
|
distanceToShrineKm: Number(h.distanceToShrineKm ?? h.distance_to_city_km ?? 0.3),
|
|
amenities: Array.isArray(h.amenities)
|
|
? h.amenities
|
|
: ['اینترنت رایگان', 'رستوران و بوفه', 'نزدیک به حرم مطهر'],
|
|
description: h.description || 'هتل درجه یک و مجهز جهت اقامت زائرین گرامی.',
|
|
guideNote: h.guideNote || undefined,
|
|
pricePerDayAdult: Number(h.pricePerDayAdult ?? h.price_per_day ?? 80),
|
|
pricePerDayChild: Number(h.pricePerDayChild ?? h.price_per_day_child ?? 40),
|
|
image: '',
|
|
isActive: h.isActive !== undefined ? Boolean(h.isActive) : true,
|
|
translations: h.translations,
|
|
}))
|
|
}
|
|
|
|
export async function saveCustomTripHotel(item: CustomTripHotelItem): Promise<CustomTripHotelItem> {
|
|
const payload = {
|
|
name: item.name,
|
|
cityName: item.cityName,
|
|
stars: item.stars,
|
|
score: item.score,
|
|
distanceToShrine: item.distanceToShrine,
|
|
distanceToShrineKm: item.distanceToShrineKm ?? 0.3,
|
|
amenities: item.amenities,
|
|
description: item.description,
|
|
guideNote: item.guideNote,
|
|
pricePerDayAdult: item.pricePerDayAdult,
|
|
pricePerDayChild: item.pricePerDayChild,
|
|
isActive: item.isActive,
|
|
translations: item.translations,
|
|
}
|
|
const isNumericId = typeof item.id === 'number' || /^\d+$/.test(String(item.id))
|
|
const res = isNumericId
|
|
? await apiFetch<any>(`trip/custom/v2/catalog/hotels/${item.id}/`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(payload),
|
|
}).catch(() =>
|
|
apiFetch<any>('trip/custom/v2/catalog/hotels/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
})
|
|
)
|
|
: await apiFetch<any>('trip/custom/v2/catalog/hotels/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
})
|
|
|
|
return {
|
|
...item,
|
|
id: res.id ?? item.id,
|
|
name: res.name || item.name,
|
|
cityName: res.cityName || item.cityName,
|
|
}
|
|
}
|
|
|
|
export async function deleteCustomTripHotel(id: string | number): Promise<void> {
|
|
await apiFetch(`trip/custom/v2/catalog/hotels/${id}/`, {
|
|
method: 'DELETE',
|
|
})
|
|
}
|