@salla.sa/twilight-components
Version:
Salla Web Component
110 lines (109 loc) • 5.7 kB
JavaScript
/*!
* Crafted with ❤ by Salla
*/
/**
* Helper functions for salla-bullet-delivery component
*/
import { isSaudiArabia } from "./api-service";
const EARTH_RADIUS_KM = 6371;
const deg2rad = (deg) => deg * (Math.PI / 180);
/** Haversine distance in km */
function calculateDistance(lat1, lon1, lat2, lon2) {
const dLat = deg2rad(lat2 - lat1);
const dLon = deg2rad(lon2 - lon1);
const a = Math.sin(dLat / 2) ** 2 +
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLon / 2) ** 2;
return EARTH_RADIUS_KM * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
/** First working-hours slot from API branch.working_hours[].times[]. */
export const getBranchFirstSlot = (workingHours) => workingHours?.[0]?.times?.[0] ?? null;
/** Nearest available branch by distance; single pass O(n). */
export function findNearestBranch(branches, userLat, userLon) {
let nearest = null;
let minD = Number.POSITIVE_INFINITY;
for (const b of branches) {
const lat = b.location?.lat != null ? Number(b.location.lat) : Number.NaN;
const lng = b.location?.lng != null ? Number(b.location.lng) : Number.NaN;
if (Number.isNaN(lat) || Number.isNaN(lng) || b.is_open === false)
continue;
const d = calculateDistance(userLat, userLon, lat, lng);
if (d < minD) {
minD = d;
nearest = b;
}
}
return nearest;
}
/** 24h time string → locale 12h (e.g. "23:55" → "11:55 PM"). */
export function formatTimeTo12h(time24, locale) {
const [h, m] = String(time24).trim().split(':').map(Number);
return Number.isNaN(h) || Number.isNaN(m)
? time24
: new Date(2000, 0, 1, h, m).toLocaleTimeString(locale, { hour: 'numeric', minute: '2-digit', hour12: true });
}
/** Working hours display: "until X" when from is 00:00, else "from - to" in 12h. */
export function formatWorkingHoursDisplay(from, to, locale, untilLabel) {
const f = String(from).trim();
const t = String(to).trim();
return !f || !t ? '' : f === '00:00' ? `${untilLabel} ${formatTimeTo12h(t, locale)}` : `${formatTimeTo12h(f, locale)} - ${formatTimeTo12h(t, locale)}`;
}
export const filterBranches = (branches, searchQuery) => {
if (!searchQuery)
return branches;
const q = searchQuery.toLowerCase();
return branches.filter((b) => b.name.toLowerCase().includes(q) || b.city?.name?.toLowerCase().includes(q));
};
export const getIntentSubtitle = (intent, toAddress, fromBranch) => {
if (intent.type === 'branch') {
const loc = intent.branch_details?.city ?? intent.branch_details?.name;
return loc ? `${fromBranch} ${loc}`.trim() : '';
}
if (intent.type === 'address' && intent.address_details) {
const addr = [intent.address_details.district?.name, intent.address_details.city?.name, intent.address_details.country?.name].filter(Boolean).join('، ') || intent.address_details.short_address || '';
return addr ? `${toAddress} ${addr}`.trim() : '';
}
return '';
};
const idFrom = (val) => (val != null ? Number(val) : undefined);
export const getIntentCountryId = (i) => idFrom(i?.address_details?.country?.id);
export const getIntentCountryCode = (i) => i?.country_code ?? i?.address_details?.country?.code ?? '';
export const getIntentRegionId = (i) => idFrom(i?.address_details?.region?.id);
export const getIntentCityId = (i) => idFrom(i?.address_details?.city?.id);
export const getIntentDistrictId = (i) => idFrom(i?.address_details?.district?.id);
export const getIntentBranchId = (i) => i?.branch_details?.id;
/** Intent coordinates (branch tab only; address flow no longer uses coordinates). */
export const getIntentLatitude = (i) => i?.branch_details?.latitude;
export const getIntentLongitude = (i) => i?.branch_details?.longitude;
export const hasSessionAddressIntent = (intent) => {
if (!intent || intent.type !== 'address' || intent.address_id)
return false;
const c = getIntentCountryId(intent);
const city = getIntentCityId(intent);
const d = getIntentDistrictId(intent);
const code = getIntentCountryCode(intent);
const hasDistrictId = d != null && Number(d) !== 0;
const hasDistrictName = Boolean(intent.address_details?.district?.name?.trim());
const districtOk = !isSaudiArabia(code) || hasDistrictId || hasDistrictName;
return !!(c && city && districtOk);
};
export const requireRegionAndDistrictForSA = (countryCode, regionId, districtIdOrName) => !countryCode || !isSaudiArabia(countryCode)
? true
: (regionId != null && Number(regionId) !== 0) && (districtIdOrName != null && (typeof districtIdOrName === 'number' ? districtIdOrName !== 0 : String(districtIdOrName).trim().length > 0));
export function buildAddressLocationPayloadFromSelection(options) {
const { countryId, countryCode, regionId, cityId, districtId, description = '' } = options;
const desc = description || '';
return {
country_id: Number(countryId) || undefined,
region_id: regionId != null ? Number(regionId) : undefined,
city_id: cityId != null ? Number(cityId) : undefined,
district_id: districtId != null ? Number(districtId) : undefined,
description: desc,
...(countryCode && isSaudiArabia(countryCode) && { local: desc }),
};
}
const GEO_ERROR_MESSAGES = {
[GeolocationPositionError.PERMISSION_DENIED]: 'Location permission denied',
[GeolocationPositionError.POSITION_UNAVAILABLE]: 'Location unavailable',
[GeolocationPositionError.TIMEOUT]: 'Location timeout',
};
export const getGeolocationErrorMessage = (errorCode) => GEO_ERROR_MESSAGES[errorCode] ?? 'An error occurred while detecting location';