app-s01qyw/frontend/src/pages/History.tsx
2026-09-29 20:17:53 +05:30

397 lines
18 KiB
TypeScript

import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { api } from '../api';
import { useAuth } from '../context/AuthContext';
import { StarRating } from '../components/StarRating';
import { Ride, CompanionCircle, CompanionActivity } from '../types/ride';
import { Calendar, Car, CheckCircle2, ChevronDown, ChevronUp, Clock, Filter, MapPin, Navigation, Users, XCircle } from 'lucide-react';
type TabType = 'all' | 'rides' | 'activities';
export default function HistoryPage() {
const { user } = useAuth();
const [activeTab, setActiveTab] = useState<TabType>('all');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [expandedId, setExpandedId] = useState<string | null>(null);
// Fetch rides
const { data: ridesResponse, isLoading: isRidesLoading } = useQuery({
queryKey: ['my-rides-history'],
queryFn: async () => {
const res = await api.get('/api/rides/my');
return res.data;
},
});
// Fetch companion circles to retrieve activities
const { data: circlesResponse, isLoading: isCirclesLoading } = useQuery({
queryKey: ['my-circles-history'],
queryFn: async () => {
const res = await api.get('/api/companion/circles/my');
return res.data;
},
});
const rawRides: Ride[] = Array.isArray(ridesResponse?.rides) ? ridesResponse.rides : [];
const circles: CompanionCircle[] = Array.isArray(circlesResponse?.circles) ? circlesResponse.circles : [];
// Process activities from circles
const activities: (CompanionActivity & { circle_name?: string })[] = [];
circles.forEach((circle) => {
if (Array.isArray((circle as any).activities)) {
(circle as any).activities.forEach((act: CompanionActivity) => {
activities.push({
...act,
circle_name: circle.name,
});
});
}
});
// Filter rides
const filteredRides = rawRides.filter((ride) => {
if (statusFilter !== 'all' && ride.status !== statusFilter) return false;
return true;
});
// Filter activities
const filteredActivities = activities.filter((act) => {
if (statusFilter !== 'all' && act.status !== statusFilter) return false;
return true;
});
const isLoading = isRidesLoading || isCirclesLoading;
const toggleExpand = (id: string) => {
setExpandedId((prev) => (prev === id ? null : id));
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'completed':
return (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-semibold bg-emerald-100 text-emerald-800 border border-emerald-300">
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-600" /> Completed
</span>
);
case 'picked_up':
return (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-semibold bg-sky-100 text-sky-800 border border-sky-300">
<Navigation className="w-3.5 h-3.5 text-sky-600" /> In Transit
</span>
);
case 'accepted':
return (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-semibold bg-amber-100 text-amber-800 border border-amber-300">
<Clock className="w-3.5 h-3.5 text-amber-600" /> Accepted
</span>
);
case 'requested':
return (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-semibold bg-slate-100 text-slate-800 border border-slate-300">
<Clock className="w-3.5 h-3.5 text-slate-600" /> Requested
</span>
);
case 'cancelled':
return (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-semibold bg-rose-100 text-rose-800 border border-rose-300">
<XCircle className="w-3.5 h-3.5 text-rose-600" /> Cancelled
</span>
);
default:
return (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-semibold bg-slate-100 text-slate-800 border border-slate-300">
{status}
</span>
);
}
};
const formatDate = (dateStr?: string) => {
if (!dateStr) return 'N/A';
try {
return new Date(dateStr).toLocaleString(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
} catch {
return dateStr;
}
};
return (
<div className="min-h-[calc(100vh-4rem)] bg-slate-50 py-8 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto space-y-6">
<div>
<h1 className="text-2xl font-bold text-slate-900 font-display">
Activity and Trip History
</h1>
<p className="text-sm text-slate-600 mt-1">
Review past ride bookings, status progressions, driver assignments, and companion circle activities.
</p>
</div>
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between border-b border-slate-200 pb-4">
<div className="flex items-center gap-2 bg-slate-200/80 p-1 rounded-lg">
<button
type="button"
onClick={() => setActiveTab('all')}
className={`px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
activeTab === 'all'
? 'bg-white text-slate-900 shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
All Activity
</button>
<button
type="button"
onClick={() => setActiveTab('rides')}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
activeTab === 'rides'
? 'bg-white text-slate-900 shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
<Car className="w-3.5 h-3.5" /> Rides ({filteredRides.length})
</button>
<button
type="button"
onClick={() => setActiveTab('activities')}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
activeTab === 'activities'
? 'bg-white text-slate-900 shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
<Users className="w-3.5 h-3.5" /> Circle Events ({filteredActivities.length})
</button>
</div>
<div className="flex items-center gap-2 w-full sm:w-auto">
<Filter className="w-4 h-4 text-slate-500" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="text-xs bg-white border border-slate-300 rounded-lg px-2.5 py-1.5 text-slate-700 focus:ring-1 focus:ring-sky-500 font-medium cursor-pointer"
>
<option value="all">All Statuses</option>
<option value="completed">Completed</option>
<option value="picked_up">In Transit</option>
<option value="accepted">Accepted</option>
<option value="requested">Requested</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
</div>
{isLoading ? (
<div className="space-y-3">
{[1, 2, 3].map((i) => (
<div key={i} className="h-24 bg-white border border-slate-200 rounded-xl animate-pulse p-4 flex flex-col justify-between">
<div className="h-4 bg-slate-200 rounded w-1/3"></div>
<div className="h-3 bg-slate-100 rounded w-2/3"></div>
</div>
))}
</div>
) : (
<div className="space-y-4">
{/* Rides List */}
{(activeTab === 'all' || activeTab === 'rides') && (
<div className="space-y-3">
{activeTab === 'all' && filteredRides.length > 0 && (
<h2 className="text-xs font-semibold text-slate-500 uppercase tracking-wider px-1">
Rides ({filteredRides.length})
</h2>
)}
{filteredRides.length === 0 && activeTab === 'rides' && (
<div className="bg-white border border-slate-200 rounded-xl p-8 text-center">
<Car className="w-10 h-10 text-slate-300 mx-auto mb-2" />
<p className="text-sm font-medium text-slate-800">No ride history found</p>
<p className="text-xs text-slate-500 mt-1">
When you request or complete trips, they will be listed here.
</p>
</div>
)}
{filteredRides.map((ride) => {
const isExpanded = expandedId === ride.id;
return (
<div
key={ride.id}
className="bg-white border border-slate-200 rounded-xl overflow-hidden hover:border-slate-300 transition-all shadow-sm"
>
<div
onClick={() => toggleExpand(ride.id)}
className="p-4 flex items-start justify-between gap-4 cursor-pointer hover:bg-slate-50/50 transition-colors"
>
<div className="flex items-start gap-3 min-w-0 flex-1">
<div className="p-2.5 rounded-lg bg-slate-100 text-slate-700 shrink-0 mt-0.5">
<Car className="w-5 h-5 text-slate-800" />
</div>
<div className="min-w-0 flex-1 space-y-1">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs font-medium text-slate-500">
{formatDate(ride.created_at)}
</span>
{getStatusBadge(ride.status)}
</div>
<div className="text-sm font-semibold text-slate-900 truncate flex items-center gap-1.5">
<MapPin className="w-3.5 h-3.5 text-emerald-600 shrink-0" />
<span className="truncate">{ride.pickup_location}</span>
<span className="text-slate-400 font-normal px-0.5">to</span>
<MapPin className="w-3.5 h-3.5 text-rose-600 shrink-0" />
<span className="truncate">{ride.destination_location}</span>
</div>
{ride.rating && ride.rating > 0 && (
<div className="flex items-center gap-1.5 pt-0.5">
<StarRating value={ride.rating} readonly size={14} />
<span className="text-xs text-slate-500 font-medium">
({ride.rating}/5)
</span>
</div>
)}
</div>
</div>
<button
type="button"
className="text-slate-400 hover:text-slate-600 p-1 shrink-0"
>
{isExpanded ? (
<ChevronUp className="w-5 h-5" />
) : (
<ChevronDown className="w-5 h-5" />
)}
</button>
</div>
{isExpanded && (
<div className="px-4 pb-4 pt-2 border-t border-slate-100 bg-slate-50/50 text-xs text-slate-600 space-y-2.5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="bg-white p-3 rounded-lg border border-slate-200 space-y-1">
<p className="font-medium text-slate-500 text-[11px] uppercase tracking-wider">
Passenger Detail
</p>
<p className="text-slate-900 font-semibold">
{ride.passenger_name || ride.passenger_email || 'Passenger'}
</p>
{ride.passenger_phone && (
<p className="text-slate-600">Phone: {ride.passenger_phone}</p>
)}
</div>
<div className="bg-white p-3 rounded-lg border border-slate-200 space-y-1">
<p className="font-medium text-slate-500 text-[11px] uppercase tracking-wider">
Driver Detail
</p>
{ride.driver_email ? (
<>
<p className="text-slate-900 font-semibold">
{ride.driver_name || ride.driver_email}
</p>
{ride.driver_phone && (
<p className="text-slate-600">Phone: {ride.driver_phone}</p>
)}
</>
) : (
<p className="text-slate-500 italic">Driver not assigned yet</p>
)}
</div>
</div>
<div className="flex items-center justify-between text-slate-500 text-[11px] pt-1">
<span>Ride ID: {ride.id}</span>
<span>Updated: {formatDate(ride.updated_at)}</span>
</div>
</div>
)}
</div>
);
})}
</div>
)}
{/* Circle Activities List */}
{(activeTab === 'all' || activeTab === 'activities') && (
<div className="space-y-3 pt-2">
{activeTab === 'all' && filteredActivities.length > 0 && (
<h2 className="text-xs font-semibold text-slate-500 uppercase tracking-wider px-1">
Circle Events ({filteredActivities.length})
</h2>
)}
{filteredActivities.length === 0 && activeTab === 'activities' && (
<div className="bg-white border border-slate-200 rounded-xl p-8 text-center">
<Users className="w-10 h-10 text-slate-300 mx-auto mb-2" />
<p className="text-sm font-medium text-slate-800">No circle events recorded</p>
<p className="text-xs text-slate-500 mt-1">
Join or create a companion circle to plan group transit activities.
</p>
</div>
)}
{filteredActivities.map((act) => (
<div
key={act.id}
className="bg-white border border-slate-200 rounded-xl p-4 space-y-2.5 shadow-sm"
>
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2">
<span className="p-2 rounded-lg bg-sky-50 text-sky-700 shrink-0">
<Calendar className="w-4 h-4" />
</span>
<div>
<h3 className="text-sm font-semibold text-slate-900">{act.title}</h3>
{act.circle_name && (
<p className="text-xs font-medium text-sky-700">{act.circle_name}</p>
)}
</div>
</div>
{getStatusBadge(act.status)}
</div>
{act.description && (
<p className="text-xs text-slate-600 line-clamp-2 pl-10">
{act.description}
</p>
)}
<div className="flex items-center gap-4 text-xs text-slate-500 pl-10 flex-wrap pt-1 border-t border-slate-100">
<span className="flex items-center gap-1">
<MapPin className="w-3.5 h-3.5 text-slate-400" /> {act.location_label}
</span>
<span className="flex items-center gap-1">
<Clock className="w-3.5 h-3.5 text-slate-400" /> {formatDate(act.starts_at)}
</span>
{act.max_attendees && (
<span className="flex items-center gap-1">
<Users className="w-3.5 h-3.5 text-slate-400" /> Max {act.max_attendees}
</span>
)}
</div>
</div>
))}
</div>
)}
{activeTab === 'all' &&
filteredRides.length === 0 &&
filteredActivities.length === 0 && (
<div className="bg-white border border-slate-200 rounded-xl p-10 text-center">
<Car className="w-12 h-12 text-slate-300 mx-auto mb-3" />
<h3 className="text-base font-semibold text-slate-900">No activity found</h3>
<p className="text-xs text-slate-500 mt-1 max-w-sm mx-auto">
There are no rides or companion circle activities matching your filter criteria.
</p>
</div>
)}
</div>
)}
</div>
</div>
);
}