PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
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.16, at resources/js/pages/PastDepartures.tsx

297 lines 10.1 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 { toDateValue } from "../lib/dateFormat";
11 import { Button } from "../components/ui/button";
12 import { Input } from "../components/ui/input";
13 import { PageHeader } from "../components/common/PageHeader";
14 import { Card, CardContent } from "../components/ui/card";
15 import {
16 Table,
17 TableBody,
18 TableCell,
19 TableHead,
20 TableHeader,
21 TableRow,
22 } from "../components/ui/table";
23 import { Badge } from "../components/ui/badge";
24 import { apiClient } from "../lib/api-client";
25 import { useToast } from "../components/ui/toast";
26 import { ConfirmationDialog } from "../components/ui/confirmation-dialog";
27
28 // Format date helper
29 const formatDate = (dateString: string): string => {
30 if (!dateString) return "--";
31 try {
32 const date = toDateValue(dateString);
33 return date.toLocaleDateString("en-US", {
34 year: "numeric",
35 month: "short",
36 day: "numeric",
37 });
38 } catch {
39 return dateString;
40 }
41 };
42
43 interface Departure {
44 id: number;
45 trip_id: number;
46 date: string;
47 time?: string;
48 max_capacity: number;
49 booked_count: number;
50 available_capacity: number;
51 status: "upcoming" | "full" | "past" | "cancelled";
52 source: "manual" | "recurring_generated";
53 created_at: string;
54 }
55
56 const PastDepartures: React.FC = () => {
57 const queryClient = useQueryClient();
58 const { showToast } = useToast();
59
60 // Row confirmations use the shared ConfirmationDialog, matching the rest of
61 // the admin, instead of the browser's native (unstyleable) confirm().
62 const [confirmDelete, setConfirmDelete] = useState<{
63 id: number;
64 bookedCount: number;
65 } | null>(null);
66
67 // Get trip_id from URL
68 const urlParams = new URLSearchParams(window.location.search);
69 const tripId = urlParams.get("trip_id")
70 ? parseInt(urlParams.get("trip_id")!)
71 : null;
72 const [searchTerm, setSearchTerm] = useState("");
73 const [page, setPage] = useState(1);
74
75 // Fetch trip details
76 const { data: tripData } = useQuery({
77 queryKey: ["trip", tripId],
78 queryFn: async () => {
79 if (!tripId) return null;
80 const response = await apiClient.get(`/trips/${tripId}`);
81 return response?.data || response;
82 },
83 enabled: !!tripId,
84 });
85
86 // Fetch past departures
87 const { data: departuresData, isLoading } = useQuery({
88 queryKey: ["past-departures", tripId, page, searchTerm],
89 queryFn: async () => {
90 if (!tripId) return { data: [], total: 0 };
91 const response = await apiClient.get(`/trips/${tripId}/departures/past`, {
92 params: {
93 page,
94 per_page: 20,
95 search: searchTerm || undefined,
96 },
97 });
98 return {
99 data: response?.data || [],
100 total: response?.meta?.total || 0,
101 };
102 },
103 enabled: !!tripId,
104 });
105
106 // Delete departure mutation
107 const deleteMutation = useMutation({
108 mutationFn: async (id: number) => {
109 if (!tripId) return;
110 await apiClient.delete(`/trips/${tripId}/departures/${id}`);
111 },
112 onSuccess: () => {
113 queryClient.invalidateQueries({ queryKey: ["past-departures", tripId] });
114 showToast(__("Departure deleted successfully", "yatra"), "success");
115 },
116 onError: (error: any) => {
117 showToast(
118 error?.message || __("Failed to delete departure", "yatra"),
119 "error",
120 );
121 },
122 });
123
124 const handleDelete = (id: number, source: string, bookedCount: number) => {
125 // The old guard required source === "recurring_generated", a value this
126 // plugin never writes (departures are "booking_created" or "manual"), so it
127 // was always true and delete was permanently blocked behind a misleading
128 // message. The server is the authority — it refuses only when real bookings
129 // are still attached — so surface a confirmation and let it decide.
130 setConfirmDelete({ id, bookedCount });
131 };
132
133 const performDelete = () => {
134 if (!confirmDelete) {
135 return;
136 }
137
138 deleteMutation.mutate(confirmDelete.id);
139 setConfirmDelete(null);
140 };
141
142 const handleBack = () => {
143 window.location.href = `?page=yatra&subpage=trips&tab=departures${tripId ? `&trip_id=${tripId}` : ""}`;
144 };
145
146 return (
147 <div className="space-y-6">
148 <PageHeader
149 title={__("Past Departures", "yatra")}
150 description={
151 tripData
152 ? `${__("Past departures for", "yatra")}: ${tripData.title}`
153 : __("View past trip departures", "yatra")
154 }
155 actions={
156 <Button variant="ghost" onClick={handleBack}>
157 <ArrowLeft className="w-4 h-4 mr-2" />
158 {__("Back", "yatra")}
159 </Button>
160 }
161 />
162
163 {!tripId ? (
164 <Card>
165 <CardContent className="pt-6">
166 <div className="text-center py-8">
167 <AlertCircle className="w-12 h-12 text-gray-400 mx-auto mb-4" />
168 <p className="text-gray-500">
169 {__("Trip ID is required", "yatra")}
170 </p>
171 </div>
172 </CardContent>
173 </Card>
174 ) : (
175 <>
176 {/* Search */}
177 <Card>
178 <CardContent className="pt-6">
179 <div className="flex gap-4 items-end">
180 <div className="flex-1">
181 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
182 {__("Search", "yatra")}
183 </label>
184 <Input
185 type="text"
186 placeholder={__("Search by date...", "yatra")}
187 value={searchTerm}
188 onChange={(e) => {
189 setSearchTerm(e.target.value);
190 setPage(1);
191 }}
192 />
193 </div>
194 </div>
195 </CardContent>
196 </Card>
197
198 {/* Past Departures Table */}
199 <Card>
200 <CardContent className="pt-6">
201 {isLoading ? (
202 <div className="text-center py-8 text-gray-500">
203 {__("Loading...", "yatra")}
204 </div>
205 ) : !departuresData?.data?.length ? (
206 <div className="text-center py-8">
207 <Calendar className="w-12 h-12 text-gray-400 mx-auto mb-4" />
208 <p className="text-gray-500">
209 {__("No past departures found", "yatra")}
210 </p>
211 </div>
212 ) : (
213 <Table>
214 <TableHeader>
215 <TableRow>
216 <TableHead>{__("Date", "yatra")}</TableHead>
217 <TableHead>{__("Time", "yatra")}</TableHead>
218 <TableHead>{__("Capacity", "yatra")}</TableHead>
219 <TableHead>{__("Booked", "yatra")}</TableHead>
220 <TableHead>{__("Source", "yatra")}</TableHead>
221 <TableHead>{__("Actions", "yatra")}</TableHead>
222 </TableRow>
223 </TableHeader>
224 <TableBody>
225 {departuresData.data.map((departure: Departure) => (
226 <TableRow key={departure.id}>
227 <TableCell>{formatDate(departure.date)}</TableCell>
228 <TableCell>{departure.time || "--"}</TableCell>
229 <TableCell>{departure.max_capacity}</TableCell>
230 <TableCell>{departure.booked_count}</TableCell>
231 <TableCell>
232 {departure.source === "manual" ? (
233 <Badge className="bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400">
234 {__("Manual", "yatra")}
235 </Badge>
236 ) : (
237 <Badge className="bg-purple-100 text-purple-700 dark:bg-purple-900/20 dark:text-purple-400">
238 {__("Recurring", "yatra")}
239 </Badge>
240 )}
241 </TableCell>
242 <TableCell>
243 {departure.source === "recurring_generated" &&
244 departure.booked_count === 0 && (
245 <Button
246 variant="ghost"
247 size="sm"
248 onClick={() =>
249 handleDelete(
250 departure.id,
251 departure.source,
252 departure.booked_count,
253 )
254 }
255 className="text-red-600 hover:text-red-700"
256 >
257 <Trash2 className="w-4 h-4" />
258 </Button>
259 )}
260 </TableCell>
261 </TableRow>
262 ))}
263 </TableBody>
264 </Table>
265 )}
266 </CardContent>
267 </Card>
268 </>
269 )}
270
271 <ConfirmationDialog
272 isOpen={confirmDelete !== null}
273 onClose={() => setConfirmDelete(null)}
274 onConfirm={performDelete}
275 title={__("Delete Departure", "yatra")}
276 message={
277 confirmDelete && confirmDelete.bookedCount > 0
278 ? __(
279 "This departure still shows bookings against it. Deleting is only possible once no bookings remain. Continue?",
280 "yatra",
281 )
282 : __(
283 "Are you sure you want to permanently delete this departure? This action cannot be undone.",
284 "yatra",
285 )
286 }
287 confirmText={__("Delete", "yatra")}
288 cancelText={__("Cancel", "yatra")}
289 variant="danger"
290 isLoading={deleteMutation.isPending}
291 />
292 </div>
293 );
294 };
295
296 export default PastDepartures;
297