mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 07:53:09 +00:00
d1b8cb20a3
- Found places: the strip shows only when the answer names one of them, the named ones first. A click on a card, or "Add to trip", opens the planner's place form filled in with the place on the day in focus, its Google details (photos, hours, description) loading beside it; "+ Day" and the day grid are gone. - Trip notes: remember_for_trip runs at once without a card. The assistant keeps lasting facts the user mentions (who travels, needs, likes, budget, pace) on its own and offers the unsure ones as a follow-up; the chat shows "Noted for this trip: ..." with the notes a tap away, and reads them again so the dialog never saves over the new note. - City walks: set_city_walk_visit checks a station off on its day (visited, skipped, or the mark taken back), with day_edit as in the day plan, a change card naming the station and its walk, and an undo that restores the status before while the station is as the change left it. - Card heads are a step smaller (32px tile, smaller title and count), and the dot beside "Working on it" in the header is gone.
101 lines
4.2 KiB
TypeScript
101 lines
4.2 KiB
TypeScript
import { type AssistantFoundPlace, categoryIdForKind } from '@trek/shared';
|
|
import { useCallback } from 'react';
|
|
import { useShallow } from 'zustand/react/shallow';
|
|
|
|
import { chatOf, useAssistantStore } from '../../store/assistantStore';
|
|
import { useCanDo } from '../../store/permissionsStore';
|
|
import { useSettingsStore } from '../../store/settingsStore';
|
|
import { useTripStore } from '../../store/tripStore';
|
|
import type { Assignment } from '../../types';
|
|
import { haversineKm } from '../../utils/geo';
|
|
import { safeHexColor } from '../../utils/safeColor';
|
|
import { formatDistance } from '../../utils/units';
|
|
import { useAssistantScope, useAssistantView } from './assistantViewContext';
|
|
import { useCardPoint } from './cards/parts/useCardPoint';
|
|
|
|
/** The trip category a found place most likely belongs in, for its line and its icon. */
|
|
interface StripCategory {
|
|
name: string;
|
|
icon: string;
|
|
color: string;
|
|
}
|
|
|
|
/** The last stop of a day that has a spot on the map. */
|
|
function lastSpot(stops: readonly Assignment[] | undefined): { lat: number; lng: number } | null {
|
|
const sorted = [...(stops ?? [])].sort((a, b) => a.order_index - b.order_index);
|
|
for (let i = sorted.length - 1; i >= 0; i--) {
|
|
const place = sorted[i]?.place;
|
|
if (place && typeof place.lat === 'number' && typeof place.lng === 'number') {
|
|
return { lat: place.lat, lng: place.lng };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* What the strip of found places needs, the same for the window and the
|
|
* phone: the day in focus, how far a place is from that day's last stop, its
|
|
* likely category, whether a place can be added, and the map's part (the pin
|
|
* while the pointer rests on a card, the camera on a click).
|
|
*
|
|
* Adding opens the planner's own place form, filled in with the place and on
|
|
* the day in focus: the traveller sees everything about it and saves it as
|
|
* any other place, so nothing waits for the chat's open decisions.
|
|
*/
|
|
export function useAssistantPlaceStrip() {
|
|
const { tripId, onRevealMap } = useAssistantView();
|
|
const scope = useAssistantScope();
|
|
const can = useCanDo();
|
|
const unit = useSettingsStore((s) => s.settings.distance_unit ?? 'metric');
|
|
const { trip, days, assignments, categories } = useTripStore(
|
|
useShallow((s) => {
|
|
const here = s.trip?.id === tripId;
|
|
return {
|
|
trip: here ? s.trip : null,
|
|
days: here ? s.days : null,
|
|
assignments: here ? s.assignments : null,
|
|
categories: s.categories,
|
|
};
|
|
})
|
|
);
|
|
const focusDayId = useAssistantStore((s) => chatOf(s, tripId).focus?.dayId ?? null);
|
|
const openPlaceDraft = useAssistantStore((s) => s.openPlaceDraft);
|
|
const point = useCardPoint();
|
|
|
|
const focusDay = (days ?? []).find((d) => d.id === focusDayId) ?? null;
|
|
const from = focusDay && assignments ? lastSpot(assignments[String(focusDay.id)]) : null;
|
|
|
|
/** Whether "Add to trip" shows at all: a trip's chat (a journey's has no days), the planner holds the trip and the user may add to it. */
|
|
const canAdd = scope.kind === 'trip' && !!trip && can('place_edit', trip);
|
|
|
|
const categoryOf = (place: AssistantFoundPlace): StripCategory | null => {
|
|
const id = categoryIdForKind(place.kind, categories);
|
|
const category = id === null ? undefined : categories.find((c) => c.id === id);
|
|
if (!category) return null;
|
|
return { name: category.name, icon: category.icon, color: safeHexColor(category.color, 'var(--text-faint)') };
|
|
};
|
|
|
|
const distanceOf = (place: AssistantFoundPlace): string | null =>
|
|
from ? formatDistance(haversineKm(from, place), unit) : null;
|
|
|
|
/** Opens the place form with it; on the phone the chat steps aside first. */
|
|
const open = (place: AssistantFoundPlace) => {
|
|
if (!canAdd || !trip) return;
|
|
onRevealMap?.();
|
|
openPlaceDraft(trip.id, place, focusDay?.id ?? null);
|
|
};
|
|
|
|
/** Shows the place on the map; on the phone the chat steps aside first. */
|
|
const show = point.show;
|
|
|
|
// The pin goes with the strip: the card point takes it away when it unmounts.
|
|
const { hover: hoverPoint } = point;
|
|
const hover = useCallback(
|
|
(place: AssistantFoundPlace | null) =>
|
|
hoverPoint(place ? { ref: place.ref, lat: place.lat, lng: place.lng, name: place.name } : null),
|
|
[hoverPoint]
|
|
);
|
|
|
|
return { canAdd, categoryOf, distanceOf, open, show, hover };
|
|
}
|