PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.5
Yatra – Travel Booking & Tour Operator Software v3.0.5
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / resources / js / pages / PastDepartures.tsx

PastDepartures.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.5, at resources/js/pages/PastDepartures.tsx

265 lines 8.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Past Departures Page
3 * View and manage past departures archive
4 */
5
6 import React, { useState } from "react";
7 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8 import { ArrowLeft, Calendar, Trash2, AlertCircle } from "lucide-react";
9 import { __ } from "../lib/i18n";
10 import { Button } from "../components/ui/button";
11 import { Input } from "../components/ui/input";
12 import { PageHeader } from "../components/common/PageHeader";
13 import { Card, CardContent } from "../components/ui/card";
14 import {
15 Table,
16 TableBody,
17 TableCell,
18 TableHead,
19 TableHeader,
20 TableRow,
21 } from "../components/ui/table";
22 import { Badge } from "../components/ui/badge";
23 import { apiClient } from "../lib/api-client";
24 import { useToast } from "../components/ui/toast";
25
26 // Format date helper
27 const formatDate = (dateString: string): string => {
28 if (!dateString) return "--";
29 try {
30 const date = new Date(dateString);
31 return date.toLocaleDateString("en-US", {
32 year: "numeric",
33 month: "short",
34 day: "numeric",
35 });
36 } catch {
37 return dateString;
38 }
39 };
40
41 interface Departure {
42 id: number;
43 trip_id: number;
44 date: string;
45 time?: string;
46 max_capacity: number;
47 booked_count: number;
48 available_capacity: number;
49 status: "upcoming" | "full" | "past" | "cancelled";
50 source: "manual" | "recurring_generated";
51 created_at: string;
52 }
53
54 const PastDepartures: React.FC = () => {
55 const queryClient = useQueryClient();
56 const { showToast } = useToast();
57
58 // Get trip_id from URL
59 const urlParams = new URLSearchParams(window.location.search);
60 const tripId = urlParams.get("trip_id")
61 ? parseInt(urlParams.get("trip_id")!)
62 : null;
63 const [searchTerm, setSearchTerm] = useState("");
64 const [page, setPage] = useState(1);
65
66 // Fetch trip details
67 const { data: tripData } = useQuery({
68 queryKey: ["trip", tripId],
69 queryFn: async () => {
70 if (!tripId) return null;
71 const response = await apiClient.get(`/trips/${tripId}`);
72 return response?.data || response;
73 },
74 enabled: !!tripId,
75 });
76
77 // Fetch past departures
78 const { data: departuresData, isLoading } = useQuery({
79 queryKey: ["past-departures", tripId, page, searchTerm],
80 queryFn: async () => {
81 if (!tripId) return { data: [], total: 0 };
82 const response = await apiClient.get(`/trips/${tripId}/departures/past`, {
83 params: {
84 page,
85 per_page: 20,
86 search: searchTerm || undefined,
87 },
88 });
89 return {
90 data: response?.data || [],
91 total: response?.meta?.total || 0,
92 };
93 },
94 enabled: !!tripId,
95 });
96
97 // Delete departure mutation
98 const deleteMutation = useMutation({
99 mutationFn: async (id: number) => {
100 if (!tripId) return;
101 await apiClient.delete(`/trips/${tripId}/departures/${id}`);
102 },
103 onSuccess: () => {
104 queryClient.invalidateQueries({ queryKey: ["past-departures", tripId] });
105 showToast(__("Departure deleted successfully", "yatra"), "success");
106 },
107 onError: (error: any) => {
108 showToast(
109 error?.message || __("Failed to delete departure", "yatra"),
110 "error",
111 );
112 },
113 });
114
115 const handleDelete = (id: number, source: string, bookedCount: number) => {
116 if (source !== "recurring_generated" || bookedCount > 0) {
117 showToast(
118 __("Only empty recurring-generated departures can be deleted", "yatra"),
119 "error",
120 );
121 return;
122 }
123
124 if (
125 !confirm(__("Are you sure you want to delete this departure?", "yatra"))
126 ) {
127 return;
128 }
129 deleteMutation.mutate(id);
130 };
131
132 const handleBack = () => {
133 window.location.href = `?page=yatra&subpage=trips&tab=departures${tripId ? `&trip_id=${tripId}` : ""}`;
134 };
135
136 return (
137 <div className="space-y-6">
138 <PageHeader
139 title={__("Past Departures", "yatra")}
140 description={
141 tripData
142 ? `${__("Past departures for", "yatra")}: ${tripData.title}`
143 : __("View past trip departures", "yatra")
144 }
145 actions={
146 <Button variant="ghost" onClick={handleBack}>
147 <ArrowLeft className="w-4 h-4 mr-2" />
148 {__("Back", "yatra")}
149 </Button>
150 }
151 />
152
153 {!tripId ? (
154 <Card>
155 <CardContent className="pt-6">
156 <div className="text-center py-8">
157 <AlertCircle className="w-12 h-12 text-gray-400 mx-auto mb-4" />
158 <p className="text-gray-500">
159 {__("Trip ID is required", "yatra")}
160 </p>
161 </div>
162 </CardContent>
163 </Card>
164 ) : (
165 <>
166 {/* Search */}
167 <Card>
168 <CardContent className="pt-6">
169 <div className="flex gap-4 items-end">
170 <div className="flex-1">
171 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
172 {__("Search", "yatra")}
173 </label>
174 <Input
175 type="text"
176 placeholder={__("Search by date...", "yatra")}
177 value={searchTerm}
178 onChange={(e) => {
179 setSearchTerm(e.target.value);
180 setPage(1);
181 }}
182 />
183 </div>
184 </div>
185 </CardContent>
186 </Card>
187
188 {/* Past Departures Table */}
189 <Card>
190 <CardContent className="pt-6">
191 {isLoading ? (
192 <div className="text-center py-8 text-gray-500">
193 {__("Loading...", "yatra")}
194 </div>
195 ) : !departuresData?.data?.length ? (
196 <div className="text-center py-8">
197 <Calendar className="w-12 h-12 text-gray-400 mx-auto mb-4" />
198 <p className="text-gray-500">
199 {__("No past departures found", "yatra")}
200 </p>
201 </div>
202 ) : (
203 <Table>
204 <TableHeader>
205 <TableRow>
206 <TableHead>{__("Date", "yatra")}</TableHead>
207 <TableHead>{__("Time", "yatra")}</TableHead>
208 <TableHead>{__("Capacity", "yatra")}</TableHead>
209 <TableHead>{__("Booked", "yatra")}</TableHead>
210 <TableHead>{__("Source", "yatra")}</TableHead>
211 <TableHead>{__("Actions", "yatra")}</TableHead>
212 </TableRow>
213 </TableHeader>
214 <TableBody>
215 {departuresData.data.map((departure: Departure) => (
216 <TableRow key={departure.id}>
217 <TableCell>{formatDate(departure.date)}</TableCell>
218 <TableCell>{departure.time || "--"}</TableCell>
219 <TableCell>{departure.max_capacity}</TableCell>
220 <TableCell>{departure.booked_count}</TableCell>
221 <TableCell>
222 {departure.source === "manual" ? (
223 <Badge className="bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400">
224 {__("Manual", "yatra")}
225 </Badge>
226 ) : (
227 <Badge className="bg-purple-100 text-purple-700 dark:bg-purple-900/20 dark:text-purple-400">
228 {__("Recurring", "yatra")}
229 </Badge>
230 )}
231 </TableCell>
232 <TableCell>
233 {departure.source === "recurring_generated" &&
234 departure.booked_count === 0 && (
235 <Button
236 variant="ghost"
237 size="sm"
238 onClick={() =>
239 handleDelete(
240 departure.id,
241 departure.source,
242 departure.booked_count,
243 )
244 }
245 className="text-red-600 hover:text-red-700"
246 >
247 <Trash2 className="w-4 h-4" />
248 </Button>
249 )}
250 </TableCell>
251 </TableRow>
252 ))}
253 </TableBody>
254 </Table>
255 )}
256 </CardContent>
257 </Card>
258 </>
259 )}
260 </div>
261 );
262 };
263
264 export default PastDepartures;
265