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

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