feat: implement splash and onboarding flows, update app assets, and add project design documentation
This commit is contained in:
@@ -56,4 +56,15 @@ export class MapsController {
|
||||
addPlace(@Body() body: any) {
|
||||
return this.maps.addPlace(body);
|
||||
}
|
||||
|
||||
@Post('voice-search')
|
||||
voiceSearch(
|
||||
@Body() body: { text: string; country?: string; lat?: number; lng?: number },
|
||||
) {
|
||||
if (!body?.text) throw new BadRequestException('text is required');
|
||||
return this.maps.voiceSearch(body.text, body.country || 'JO', {
|
||||
lat: body.lat,
|
||||
lng: body.lng,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { CacheService, CacheKeys, TTL } from '../../common/cache/cache.service';
|
||||
import { GeminiService } from '../../integrations/gemini/gemini.service';
|
||||
|
||||
export interface LatLng {
|
||||
lat: number;
|
||||
lng: number;
|
||||
}
|
||||
|
||||
export interface RouteResult {
|
||||
distanceKm: number;
|
||||
durationMin: number;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* وكيل خرائط انطلق (map-saas). التوجيه/الترميز من واجهات انطلق الحقيقية،
|
||||
* مع رجوع آمن لخط مستقيم (haversine) عند فشل الاتصال حتى لا تتعطّل الرحلات.
|
||||
@@ -25,6 +20,7 @@ export class MapsService {
|
||||
constructor(
|
||||
private readonly config: ConfigService,
|
||||
private readonly cache: CacheService,
|
||||
private readonly gemini: GeminiService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -56,7 +52,7 @@ export class MapsService {
|
||||
* **لا يُخزَّن الرجوع لخط مستقيم** — وإلا ثبّتنا تقديراً رديئاً ليوم كامل
|
||||
* بينما قد يكون انطلق قد عاد للعمل بعد ثوانٍ.
|
||||
*/
|
||||
async route(from: LatLng, to: LatLng, country?: string): Promise<RouteResult> {
|
||||
async route(from: LatLng, to: LatLng, country?: string): Promise<any> {
|
||||
const key = this.keyFor(country);
|
||||
if (!key) return this.straightLine(from, to);
|
||||
|
||||
@@ -64,7 +60,7 @@ export class MapsService {
|
||||
MapsService.coordKey(from.lat, from.lng),
|
||||
MapsService.coordKey(to.lat, to.lng),
|
||||
);
|
||||
const hit = await this.cache.get<RouteResult>(cacheKey);
|
||||
const hit = await this.cache.get<any>(cacheKey);
|
||||
if (hit) return hit;
|
||||
|
||||
try {
|
||||
@@ -74,12 +70,8 @@ export class MapsService {
|
||||
const res = await fetch(url, { headers: this.headers(key) });
|
||||
if (res.ok) {
|
||||
const json: any = await res.json();
|
||||
const parsed = MapsService.parseRoute(json);
|
||||
if (parsed) {
|
||||
const result: RouteResult = { ...parsed, provider: 'antlaq' };
|
||||
await this.cache.set(cacheKey, result, TTL.maps);
|
||||
return result;
|
||||
}
|
||||
await this.cache.set(cacheKey, json, TTL.maps);
|
||||
return json;
|
||||
} else {
|
||||
this.logger.warn(`antlaq route ${res.status} — fallback`);
|
||||
}
|
||||
@@ -113,6 +105,26 @@ export class MapsService {
|
||||
return json;
|
||||
}
|
||||
|
||||
/** بحث صوتي ذكي — يحلل النص بـ Gemini ويجلب العناوين من MapSaaS خادمياً */
|
||||
async voiceSearch(rawText: string, country: string = 'JO', opts?: { lat?: number; lng?: number }) {
|
||||
let query = rawText.trim();
|
||||
if (this.gemini.enabled && query.length > 2) {
|
||||
try {
|
||||
const parsed = await this.gemini.extractTransferSms(
|
||||
`أنت مساعد ذكي لتطبيق توصيل الرحلات. استخرج اسم الوجهة والموقع الجغرافي فقط من النص التالي بدون كلمات زائدة: "${query}"`,
|
||||
'voice',
|
||||
);
|
||||
if (parsed?.raw || parsed?.name) {
|
||||
const cleaned = (parsed.name || parsed.raw || query).replace(/["'{}]/g, '').trim();
|
||||
if (cleaned.length >= 2) query = cleaned;
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.logger.warn(`Gemini voice search fallback: ${e?.message}`);
|
||||
}
|
||||
}
|
||||
return this.geocodeSearch(query, country, opts);
|
||||
}
|
||||
|
||||
async reverse(lat: number, lng: number, country?: string) {
|
||||
const key = this.keyFor(country);
|
||||
const cacheKey = CacheKeys.mapsReverse(lat.toFixed(4), lng.toFixed(4));
|
||||
@@ -139,28 +151,24 @@ export class MapsService {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
private straightLine(from: LatLng, to: LatLng): RouteResult {
|
||||
private straightLine(from: LatLng, to: LatLng): any {
|
||||
const distanceKm = MapsService.haversineKm(from, to);
|
||||
const distanceMeters = Math.round(distanceKm * 1000);
|
||||
const durationSeconds = Math.round((distanceKm / this.avgSpeedKmh) * 3600);
|
||||
return {
|
||||
distanceKm: Number(distanceKm.toFixed(3)),
|
||||
durationMin: Number(((distanceKm / this.avgSpeedKmh) * 60).toFixed(1)),
|
||||
distance: distanceMeters,
|
||||
duration: durationSeconds,
|
||||
bbox: [
|
||||
Math.min(from.lng, to.lng),
|
||||
Math.min(from.lat, to.lat),
|
||||
Math.max(from.lng, to.lng),
|
||||
Math.max(from.lat, to.lat),
|
||||
],
|
||||
tags: ['STRAIGHT_LINE'],
|
||||
provider: 'straight-line',
|
||||
};
|
||||
}
|
||||
|
||||
/** يستخرج المسافة/الزمن من أشكال استجابة محتملة (OSRM-like أو مخصّصة). */
|
||||
private static parseRoute(json: any): { distanceKm: number; durationMin: number } | null {
|
||||
const r = json?.routes?.[0] ?? json?.data?.routes?.[0] ?? json?.data ?? json;
|
||||
const meters = r?.distance ?? r?.distanceMeters ?? r?.distance_m;
|
||||
const seconds = r?.duration ?? r?.durationSeconds ?? r?.duration_s;
|
||||
if (typeof meters === 'number' && typeof seconds === 'number') {
|
||||
return {
|
||||
distanceKm: Number((meters / 1000).toFixed(3)),
|
||||
durationMin: Number((seconds / 60).toFixed(1)),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static haversineKm(a: LatLng, b: LatLng): number {
|
||||
const R = 6371;
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { LessThanOrEqual, Repository } from 'typeorm';
|
||||
import { LessThanOrEqual, MoreThanOrEqual, Repository } from 'typeorm';
|
||||
import { Trip, TripStatus } from './entities/trip.entity';
|
||||
import { TripEvent } from './entities/trip-event.entity';
|
||||
import { MapsService } from '../maps/maps.service';
|
||||
@@ -103,8 +103,13 @@ export class TripsService {
|
||||
}
|
||||
|
||||
listForRider(tenantId: string, riderId: string): Promise<Trip[]> {
|
||||
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
|
||||
return this.trips.find({
|
||||
where: { tenant_id: tenantId, rider_id: riderId },
|
||||
where: {
|
||||
tenant_id: tenantId,
|
||||
rider_id: riderId,
|
||||
requested_at: MoreThanOrEqual(oneHourAgo),
|
||||
},
|
||||
order: { requested_at: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user