mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
feat(reservations): traveler filter on the mobile bookings and transports tabs
This commit is contained in:
@@ -7,7 +7,7 @@ import { openFile } from '../../../../utils/fileDownload'
|
||||
import { useTranslation } from '../../../../i18n'
|
||||
import type { Reservation } from '../../../../types'
|
||||
import MConfirmSheet from '../../settings/MConfirmSheet'
|
||||
import { CountPill, Field, SectionHeader, StatusDot, TabScroller, TravelerAvatars } from './tabChrome'
|
||||
import { CountPill, Field, SectionHeader, StatusDot, TabScroller, TravelerAvatars, TravelerFilterRow } from './tabChrome'
|
||||
import { STATUS_COLOR, type MTabScreenProps } from './tabModel'
|
||||
import { groupTransports, orderedEndpoints, parseTransportMeta } from './transportsModel'
|
||||
import { BOOKING_TYPE_COLOR } from './bookingsModel'
|
||||
@@ -22,9 +22,13 @@ import { BOOKING_TYPE_COLOR } from './bookingsModel'
|
||||
*/
|
||||
export default function MBookingsTab({ planner, shell }: MTabScreenProps) {
|
||||
const { t, reservations, days } = planner
|
||||
const bookings = reservations.filter(r => !planner.TRANSPORT_TYPES.has(r.type))
|
||||
const [travelerFilter, setTravelerFilter] = useState<Set<number>>(new Set())
|
||||
const allBookings = reservations.filter(r => !planner.TRANSPORT_TYPES.has(r.type))
|
||||
const bookings = travelerFilter.size === 0 ? allBookings : allBookings.filter(r => (r.travelers || []).some(tv => travelerFilter.has(tv.user_id)))
|
||||
const groups = groupTransports(bookings, days)
|
||||
const canEdit = planner.can('reservation_edit', planner.trip)
|
||||
const showTravelerFilter = planner.tripMembers.length > 1 && allBookings.some(r => (r.travelers || []).length > 0)
|
||||
const toggleTravelerFilter = (id: number) => setTravelerFilter(prev => { const n = new Set(prev); if (n.has(id)) n.delete(id); else n.add(id); return n })
|
||||
|
||||
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({})
|
||||
const toggle = (id: string) => setCollapsed(c => ({ ...c, [id]: !c[id] }))
|
||||
@@ -34,20 +38,17 @@ export default function MBookingsTab({ planner, shell }: MTabScreenProps) {
|
||||
{ id: 'pending', label: t('reservations.pending'), rows: groups.pending },
|
||||
].filter(s => s.rows.length > 0)
|
||||
|
||||
if (sections.length === 0) {
|
||||
return (
|
||||
<TabScroller>
|
||||
return (
|
||||
<TabScroller>
|
||||
{showTravelerFilter && (
|
||||
<TravelerFilterRow members={planner.tripMembers} active={travelerFilter} onToggle={toggleTravelerFilter} label={t('reservations.travelers.label')} />
|
||||
)}
|
||||
{sections.length === 0 ? (
|
||||
<div className="flex min-h-full flex-1 flex-col items-center justify-center px-8 py-10 text-center">
|
||||
<MDancingTrek scene="bookings" className="mb-2" />
|
||||
<p className="font-geist text-[0.8125rem] font-medium text-m-muted">{t('mobileTrip.bookingsEmpty')}</p>
|
||||
</div>
|
||||
</TabScroller>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<TabScroller>
|
||||
{sections.map(section => (
|
||||
) : sections.map(section => (
|
||||
<div key={section.id}>
|
||||
<SectionHeader
|
||||
label={section.label}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { openFile } from '../../../../utils/fileDownload'
|
||||
import { useTranslation } from '../../../../i18n'
|
||||
import type { Reservation } from '../../../../types'
|
||||
import MConfirmSheet from '../../settings/MConfirmSheet'
|
||||
import { CountPill, Field, SectionHeader, StatusDot, TabScroller, TravelerAvatars } from './tabChrome'
|
||||
import { CountPill, Field, SectionHeader, StatusDot, TabScroller, TravelerAvatars, TravelerFilterRow } from './tabChrome'
|
||||
import { STATUS_COLOR, type MTabScreenProps } from './tabModel'
|
||||
import {
|
||||
TRANSPORT_TYPE_COLOR,
|
||||
@@ -26,9 +26,13 @@ import {
|
||||
*/
|
||||
export default function MTransportsTab({ planner, shell }: MTabScreenProps) {
|
||||
const { t, reservations, days } = planner
|
||||
const transports = reservations.filter(r => planner.TRANSPORT_TYPES.has(r.type))
|
||||
const [travelerFilter, setTravelerFilter] = useState<Set<number>>(new Set())
|
||||
const allTransports = reservations.filter(r => planner.TRANSPORT_TYPES.has(r.type))
|
||||
const transports = travelerFilter.size === 0 ? allTransports : allTransports.filter(r => (r.travelers || []).some(tv => travelerFilter.has(tv.user_id)))
|
||||
const groups = groupTransports(transports, days)
|
||||
const canEdit = planner.can('day_edit', planner.trip)
|
||||
const showTravelerFilter = planner.tripMembers.length > 1 && allTransports.some(r => (r.travelers || []).length > 0)
|
||||
const toggleTravelerFilter = (id: number) => setTravelerFilter(prev => { const n = new Set(prev); if (n.has(id)) n.delete(id); else n.add(id); return n })
|
||||
|
||||
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({})
|
||||
const toggle = (id: string) => setCollapsed(c => ({ ...c, [id]: !c[id] }))
|
||||
@@ -39,20 +43,17 @@ export default function MTransportsTab({ planner, shell }: MTabScreenProps) {
|
||||
{ id: 'transit', label: t('reservations.type.transit'), rows: groups.transit },
|
||||
].filter(s => s.rows.length > 0)
|
||||
|
||||
if (sections.length === 0) {
|
||||
return (
|
||||
<TabScroller>
|
||||
return (
|
||||
<TabScroller>
|
||||
{showTravelerFilter && (
|
||||
<TravelerFilterRow members={planner.tripMembers} active={travelerFilter} onToggle={toggleTravelerFilter} label={t('reservations.travelers.label')} />
|
||||
)}
|
||||
{sections.length === 0 ? (
|
||||
<div className="flex min-h-full flex-col items-center justify-center px-8 py-10 text-center">
|
||||
<MDancingTrek scene="transport" className="mb-2" />
|
||||
<p className="font-geist text-[0.8125rem] font-medium text-m-muted">{t('mobileTrip.transportsEmpty')}</p>
|
||||
</div>
|
||||
</TabScroller>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<TabScroller>
|
||||
{sections.map(section => (
|
||||
) : sections.map(section => (
|
||||
<div key={section.id}>
|
||||
<SectionHeader
|
||||
label={section.label}
|
||||
|
||||
@@ -130,3 +130,32 @@ export function TravelerAvatars({ travelers, label }: {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Avatar toggle row to filter the list by assigned traveler (#1517/#1557). */
|
||||
export function TravelerFilterRow({ members, active, onToggle, label }: {
|
||||
members: { id: number; username: string; avatar_url?: string | null }[]
|
||||
active: Set<number>
|
||||
onToggle: (id: number) => void
|
||||
label: string
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-1.5 px-[18px] pb-2 pt-1" aria-label={label}>
|
||||
{members.map(m => {
|
||||
const on = active.has(m.id)
|
||||
return (
|
||||
<button
|
||||
key={m.id}
|
||||
type="button"
|
||||
onClick={() => onToggle(m.id)}
|
||||
title={m.username}
|
||||
className={`flex h-7 w-7 shrink-0 items-center justify-center overflow-hidden rounded-full border-2 bg-[color:var(--m-ic)] text-[0.625rem] font-bold text-m-muted transition-opacity ${on ? 'border-[color:var(--m-act)]' : 'border-[color:var(--m-rowbr)]'} ${on || active.size === 0 ? 'opacity-100' : 'opacity-40'}`}
|
||||
>
|
||||
{m.avatar_url
|
||||
? <img src={m.avatar_url} className="h-full w-full object-cover" alt="" />
|
||||
: m.username?.[0]?.toUpperCase()}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user