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 { try { const res = await apiFetch('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 { try { const res = await apiFetch(`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 { const itineraryPayload: Record = {} 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(`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(`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(`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 { const remote = await apiFetch('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 { 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(`trip/custom/v2/catalog/cities/${item.id}/`, { method: 'PUT', body: JSON.stringify(payload), }).catch(() => apiFetch('trip/custom/v2/catalog/cities/', { method: 'POST', body: JSON.stringify(payload), }) ) : await apiFetch('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 { await apiFetch(`trip/custom/v2/catalog/cities/${id}/`, { method: 'DELETE', }) } export async function fetchCustomTripDestinations(): Promise { const remote = await apiFetch( 'trip/custom/v2/catalog/destinations/?include_inactive=true' ) return Array.isArray(remote) ? remote : [] } export async function fetchCustomTripDestinationById( id: string | number ): Promise { try { const detail = await apiFetch( `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 { 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( `trip/custom/v2/catalog/destinations/${item.id}/`, { method: 'PUT', body: JSON.stringify(payload), } ).catch(async () => { return await apiFetch('trip/custom/v2/catalog/destinations/', { method: 'POST', body: JSON.stringify(payload), }) }) return { ...item, ...res } } export async function deleteCustomTripDestination(id: string | number): Promise { await apiFetch(`trip/custom/v2/catalog/destinations/${id}/`, { method: 'DELETE', }) } export async function fetchCustomTripTransports(): Promise { const remote = await apiFetch('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 { const remote = await apiFetch('trip/custom/v2/catalog/transports/') if (!Array.isArray(remote)) return [] const grouped = new Map() 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 { 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 { 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('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 { const remote = await apiFetch('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 { 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(`trip/custom/v2/catalog/hotels/${item.id}/`, { method: 'PUT', body: JSON.stringify(payload), }).catch(() => apiFetch('trip/custom/v2/catalog/hotels/', { method: 'POST', body: JSON.stringify(payload), }) ) : await apiFetch('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 { await apiFetch(`trip/custom/v2/catalog/hotels/${id}/`, { method: 'DELETE', }) }