mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 23:16:46 +00:00
2fbff51299
A waiting change is one row as in the day list: a round choice in front, what it touches in bold, a faint line with what happens and when. No switch, nothing to fold open, no day band: the planner's day list already shows the suggestion among the day's stops. Whether the days still fit is one pill in the card's head, with what to look at on hover. Also mends the comment the rebase onto dev split in json-columns.ts.
75 lines
3.1 KiB
TypeScript
75 lines
3.1 KiB
TypeScript
import { CircleCheck, TriangleAlert } from 'lucide-react';
|
|
import { useMemo } from 'react';
|
|
import { useShallow } from 'zustand/react/shallow';
|
|
|
|
import { useTranslation } from '../../i18n';
|
|
import { useTripStore } from '../../store/tripStore';
|
|
import { Tooltip } from '../shared/Tooltip';
|
|
import { useAssistantView } from './assistantViewContext';
|
|
import { buildDayAfter, changedDays, type DayChange, type TimelineStop } from './dayAfterModel';
|
|
|
|
/** Lines the tooltip names; the rest are only counted on the pill. */
|
|
const MAX_LINES = 3;
|
|
|
|
/** What does not fit on a stop of the day: arriving after the next thing starts, or a way too tight. */
|
|
function problemOf(stop: TimelineStop, t: (key: string, params?: Record<string, string | number>) => string) {
|
|
const lead = stop.lead;
|
|
if (lead?.overlapsWith) return t('assistant.change.timeline.overlap', { place: lead.overlapsWith });
|
|
if (lead?.shortBy != null) return `${stop.name}: ${t('assistant.change.timeline.tight', { count: lead.shortBy })}`;
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Whether the days a waiting card changes still work out in time, as one
|
|
* small pill in the card's head: "fits" in green, or how many things to look
|
|
* at in amber, with what they are on hover. The days themselves show the
|
|
* suggestion in the planner's day list, so the card does not draw them again.
|
|
* Shown only while the planner holds this trip, whose day plans it reads.
|
|
*/
|
|
export default function AssistantFitCheck({ changes }: { changes: readonly DayChange[] }) {
|
|
const { t } = useTranslation();
|
|
const { tripId } = useAssistantView();
|
|
const plan = useTripStore(
|
|
useShallow((s) => ({ here: s.trip?.id === tripId, days: s.days, assignments: s.assignments, places: s.places }))
|
|
);
|
|
|
|
const check = useMemo(() => {
|
|
if (!plan.here || changes.length === 0) return null;
|
|
const stops = changedDays(changes).flatMap((dayId) => {
|
|
const day = plan.days.find((d) => d.id === dayId);
|
|
if (!day) return [];
|
|
return buildDayAfter({
|
|
dayId,
|
|
assignments: plan.assignments,
|
|
places: plan.places,
|
|
changes,
|
|
defaultMode: day.default_transport_mode,
|
|
}).stops;
|
|
});
|
|
const measured = stops.some((stop) => stop.lead?.travelMinutes != null);
|
|
const problems = stops.map((stop) => problemOf(stop, t)).filter((line): line is string => line !== null);
|
|
return measured || problems.length > 0 ? problems : null;
|
|
}, [plan, changes, t]);
|
|
|
|
if (!check) return null;
|
|
const ok = check.length === 0;
|
|
const pill = (
|
|
<span
|
|
className={`inline-flex h-6 flex-shrink-0 items-center gap-1 rounded-full px-2 text-caption font-medium ${
|
|
ok ? 'bg-success-soft text-success' : 'bg-warning-soft text-warning'
|
|
}`}
|
|
>
|
|
{ok ? <CircleCheck size={12} aria-hidden="true" /> : <TriangleAlert size={12} aria-hidden="true" />}
|
|
{ok ? t('assistant.check.ok') : t('assistant.check.issues', { count: check.length })}
|
|
</span>
|
|
);
|
|
if (ok) return pill;
|
|
return (
|
|
<Tooltip label={check.slice(0, MAX_LINES).join('\n')} placement="top">
|
|
<span tabIndex={0} className="rounded-full outline-none focus-visible:ring-2 focus-visible:ring-accent">
|
|
{pill}
|
|
</span>
|
|
</Tooltip>
|
|
);
|
|
}
|