mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 15:06:32 +00:00
feat: quality of life sweep from the feature discussions
Small requests from the discussions, each built end to end: planner and map (cycling mode, date first headings, clear a day, stay times per day, off-route stops, map lock, compact unplanned markers, pending stays, cable car, transit quick picks), places (location filter, category from OSM, open in maps, geo: link, image from an attachment, enrichment on file import, places near a pin, place name language, Amap first in China), lists and costs (packed counter, inherited sharing, Markdown to-dos, payment notes, bookings CSV, my plan PDF), sharing (unplanned places, travel only, without photos), journey (photo zoom, drafts, photo order, trip suggestion, day jump), atlas (region counter, region bucket list, places per country), vacay half company holidays, studio copy and paste plus the book's own layouts, and RESTORE_FROM_BACKUP for a first start restore.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// Trip PDF via browser print window
|
||||
import { createElement } from 'react'
|
||||
import { getCategoryIcon } from '../shared/categoryIcons'
|
||||
import { FileText, Info, Clock, MapPin, Navigation, Train, Plane, Bus, Car, Ship, Sailboat, Bike, CarTaxiFront, Route, Coffee, Ticket, Star, Heart, Camera, Flag, Lightbulb, AlertTriangle, ShoppingBag, Bookmark, Hotel, LogIn, LogOut, KeyRound, BedDouble, Utensils, Users, ParkingSquare, LucideIcon } from 'lucide-react'
|
||||
import { FileText, Info, Clock, MapPin, Navigation, Train, Plane, Bus, Car, Ship, Sailboat, CableCar, Bike, CarTaxiFront, Route, Coffee, Ticket, Star, Heart, Camera, Flag, Lightbulb, AlertTriangle, ShoppingBag, Bookmark, Hotel, LogIn, LogOut, KeyRound, BedDouble, Utensils, Users, ParkingSquare, LucideIcon } from 'lucide-react'
|
||||
import { accommodationsApi, mapsApi, pluginsApi } from '../../api/client'
|
||||
import type { Trip, Day, Place, Category, AssignmentsMap, DayNote, DistanceUnit, BudgetItem } from '../../types'
|
||||
import { isDayInAccommodationRange, getDayOrder } from '../../utils/dayOrder'
|
||||
@@ -20,6 +20,7 @@ import { formatDistance } from '../../utils/units'
|
||||
import { fetchExchangeRates } from '../../hooks/useExchangeRates'
|
||||
import { getFlightLegs, getTrainLegs } from '../../utils/flightLegs'
|
||||
import { isServiceStopType } from '../Roadtrip/roadtripModel'
|
||||
import { onlyMyPlan } from './pdfScope'
|
||||
|
||||
/**
|
||||
* Every day starts a new page by default. On a trip of short days that prints
|
||||
@@ -64,8 +65,8 @@ function noteIconSvg(iconId) {
|
||||
return renderLucideIcon(Icon, { size: 14, strokeWidth: 1.8, color: '#94a3b8' })
|
||||
}
|
||||
|
||||
const RESERVATION_ICON_MAP = { flight: Plane, train: Train, bus: Bus, car: Car, taxi: CarTaxiFront, bicycle: Bike, cruise: Ship, ferry: Sailboat, transport_other: Route, restaurant: Utensils, event: Ticket, tour: Users, parking: ParkingSquare, other: FileText }
|
||||
const RESERVATION_COLOR_MAP = { flight: '#3b82f6', train: '#06b6d4', bus: '#059669', car: '#6b7280', taxi: '#ca8a04', bicycle: '#84cc16', cruise: '#0ea5e9', ferry: '#0d9488', transport_other: '#6b7280', restaurant: '#ef4444', event: '#f59e0b', tour: '#10b981', parking: '#2563eb', other: '#6b7280' }
|
||||
const RESERVATION_ICON_MAP = { flight: Plane, train: Train, bus: Bus, car: Car, taxi: CarTaxiFront, bicycle: Bike, cruise: Ship, ferry: Sailboat, cable_car: CableCar, transport_other: Route, restaurant: Utensils, event: Ticket, tour: Users, parking: ParkingSquare, other: FileText }
|
||||
const RESERVATION_COLOR_MAP = { flight: '#3b82f6', train: '#06b6d4', bus: '#059669', car: '#6b7280', taxi: '#ca8a04', bicycle: '#84cc16', cruise: '#0ea5e9', ferry: '#0d9488', cable_car: '#dc2626', transport_other: '#6b7280', restaurant: '#ef4444', event: '#f59e0b', tour: '#10b981', parking: '#2563eb', other: '#6b7280' }
|
||||
function reservationIconSvg(type) {
|
||||
const Icon = RESERVATION_ICON_MAP[type] || Ticket
|
||||
const color = RESERVATION_COLOR_MAP[type] || '#3b82f6'
|
||||
@@ -203,6 +204,8 @@ interface downloadTripPDFProps {
|
||||
budgetItems?: BudgetItem[]
|
||||
/** Whether the Costs addon is on. Read from the addon store when left out. */
|
||||
costsEnabled?: boolean
|
||||
/** Prints only this member's plan (#2168): the stops and bookings that name them, and the ones that name nobody. */
|
||||
onlyUserId?: number
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,7 +226,10 @@ function planAssignments(assignments: AssignmentsMap, showServiceStops: boolean)
|
||||
|
||||
// `assignments` is normalised here once, to the plan's own list; every read below
|
||||
// (and fetchPlacePhotos) relies on it being an object.
|
||||
export async function downloadTripPDF({ trip, days, places, assignments: stored = {}, categories, dayNotes, reservations = [], t: _t, locale: _locale, timeFormat: _timeFormat, distanceUnit: _distanceUnit, showServiceStops = true, budgetItems, costsEnabled }: downloadTripPDFProps) {
|
||||
export async function downloadTripPDF({ trip, days, places, assignments: allStored = {}, categories, dayNotes, reservations: allReservations = [], t: _t, locale: _locale, timeFormat: _timeFormat, distanceUnit: _distanceUnit, showServiceStops = true, budgetItems, costsEnabled, onlyUserId }: downloadTripPDFProps) {
|
||||
const { assignments: stored, reservations } = onlyUserId != null
|
||||
? onlyMyPlan(allStored, allReservations, onlyUserId)
|
||||
: { assignments: allStored, reservations: allReservations }
|
||||
const assignments = planAssignments(stored, showServiceStops)
|
||||
const breaksPerDay = pageBreakPerDay()
|
||||
const loc = _locale || undefined
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// FE-PDF-SCOPE-001 to FE-PDF-SCOPE-002 (#2168)
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import type { AssignmentsMap } from '../../types'
|
||||
import { hasPersonalPlan, onlyMyPlan } from './pdfScope'
|
||||
|
||||
const assignments = {
|
||||
1: [
|
||||
{ id: 11, participants: [] },
|
||||
{ id: 12, participants: [{ user_id: 7, username: 'me' }] },
|
||||
{ id: 13, participants: [{ user_id: 8, username: 'anna' }] },
|
||||
],
|
||||
2: [{ id: 21 }],
|
||||
} as unknown as AssignmentsMap
|
||||
|
||||
describe('pdfScope', () => {
|
||||
it('FE-PDF-SCOPE-001: offers a personal plan only once somebody was given a part', () => {
|
||||
expect(hasPersonalPlan(assignments, [])).toBe(true)
|
||||
expect(hasPersonalPlan({ 1: [{ id: 1 }] } as unknown as AssignmentsMap, [{ travelers: [] }])).toBe(false)
|
||||
expect(hasPersonalPlan({}, [{ travelers: [{ user_id: 1 }] }])).toBe(true)
|
||||
})
|
||||
|
||||
it('FE-PDF-SCOPE-002: keeps what names me and what names nobody', () => {
|
||||
const reservations = [{ id: 1, travelers: [{ user_id: 7 }] }, { id: 2, travelers: [{ user_id: 8 }] }, { id: 3 }]
|
||||
const out = onlyMyPlan(assignments, reservations, 7)
|
||||
expect(out.assignments[1].map(a => a.id)).toEqual([11, 12])
|
||||
expect(out.assignments[2].map(a => a.id)).toEqual([21])
|
||||
expect(out.reservations.map(r => r.id)).toEqual([1, 3])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { AssignmentsMap } from '../../types'
|
||||
|
||||
interface WithTravelers { travelers?: { user_id: number }[] | null }
|
||||
|
||||
/**
|
||||
* Whether anyone on the trip has been given their own part of it (#2168): a stop
|
||||
* with participants or a booking with travellers. Without either, "my plan" is
|
||||
* the whole plan, and offering it would only be a second way to the same file.
|
||||
*/
|
||||
export function hasPersonalPlan(assignments: AssignmentsMap, reservations: WithTravelers[]): boolean {
|
||||
return Object.values(assignments).some(list => (list || []).some(a => (a.participants || []).length > 0))
|
||||
|| reservations.some(r => (r.travelers || []).length > 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* The plan one member goes on (#2168). A stop or booking that names its people
|
||||
* stays only when it names them; one that names nobody is everybody's and stays.
|
||||
*/
|
||||
export function onlyMyPlan<R extends WithTravelers>(
|
||||
assignments: AssignmentsMap,
|
||||
reservations: R[],
|
||||
userId: number,
|
||||
): { assignments: AssignmentsMap; reservations: R[] } {
|
||||
const mine = (people: { user_id: number }[] | null | undefined) => !people?.length || people.some(p => p.user_id === userId)
|
||||
return {
|
||||
assignments: Object.fromEntries(Object.entries(assignments).map(([dayId, list]) => [dayId, (list || []).filter(a => mine(a.participants))])),
|
||||
reservations: reservations.filter(r => mine(r.travelers)),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user