PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.13
Yatra – Travel Booking & Tour Operator Software v3.0.13
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 / account / Documents.tsx

Documents.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.13, at resources/js/pages/account/Documents.tsx

304 lines 13.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useState } from "react";
2 import { __ } from "../../lib/i18n";
3 import { formatDate } from "./utils";
4 import { downloadDocument, previewTravelDocument } from "./utils/downloads";
5 import type { TravelDocument } from "./types";
6
7 interface DocumentsProps {
8 documents: TravelDocument[];
9 }
10
11 function documentCategoryLabel(
12 category: TravelDocument["category"] | string,
13 ): string {
14 switch (category) {
15 case "itinerary":
16 return __("Itinerary", "yatra");
17 case "voucher":
18 return __("Voucher", "yatra");
19 case "invoice":
20 return __("Invoice", "yatra");
21 case "downloads":
22 return __("Downloads", "yatra");
23 default:
24 return category;
25 }
26 }
27
28 const Documents: React.FC<DocumentsProps> = ({ documents }) => {
29 const [searchTerm, setSearchTerm] = useState("");
30 const [selectedCategory, setSelectedCategory] = useState<
31 "all" | "itinerary" | "voucher" | "invoice" | "downloads"
32 >("all");
33
34 const displayDocuments = documents;
35
36 // Filter documents based on search and category
37 let filteredDocuments = [...displayDocuments]; // Create a copy to avoid mutation
38
39 // Apply category filter
40 if (selectedCategory !== "all") {
41 filteredDocuments = filteredDocuments.filter(
42 (d) => d.category === selectedCategory,
43 );
44 }
45
46 // Apply search
47 if (searchTerm.trim()) {
48 const searchLower = searchTerm.toLowerCase().trim();
49 filteredDocuments = filteredDocuments.filter((d: any) => {
50 const name = d.name || "";
51 const tripTitle = d.trip_title || "";
52 const category = d.category || "";
53 return (
54 name.toLowerCase().includes(searchLower) ||
55 tripTitle.toLowerCase().includes(searchLower) ||
56 category.toLowerCase().includes(searchLower)
57 );
58 });
59 }
60
61 // Group filtered documents by category
62 const documentsByCategory = {
63 itinerary: filteredDocuments.filter((d) => d.category === "itinerary"),
64 voucher: filteredDocuments.filter((d) => d.category === "voucher"),
65 invoice: filteredDocuments.filter((d) => d.category === "invoice"),
66 downloads: filteredDocuments.filter((d) => d.category === "downloads"),
67 };
68
69 return (
70 <div className="yatra-documents-page space-y-6">
71 {/* Header */}
72 <div className="yatra-documents-header bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6">
73 <div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
74 <div>
75 <h2 className="text-lg font-bold text-gray-900 dark:text-white">
76 {__("Travel Documents", "yatra")}
77 </h2>
78 <p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
79 {__(
80 "Download itineraries, vouchers, invoices, and other documents for each trip.",
81 "yatra",
82 )}
83 </p>
84 </div>
85 <div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:gap-4">
86 {/* Search Input */}
87 <div className="relative">
88 <input
89 type="text"
90 placeholder={__("Search documents...", "yatra")}
91 value={searchTerm}
92 onChange={(e) => setSearchTerm(e.target.value)}
93 className="w-full lg:w-64 pl-4 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
94 />
95 </div>
96 {/* Filter Dropdown */}
97 <div className="relative">
98 <select
99 value={selectedCategory}
100 onChange={(e) =>
101 setSelectedCategory(
102 e.target.value as
103 | "all"
104 | "itinerary"
105 | "voucher"
106 | "invoice"
107 | "downloads",
108 )
109 }
110 className="appearance-none bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg px-4 py-2 pr-4 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent cursor-pointer"
111 >
112 <option value="all">{__("All Documents", "yatra")}</option>
113 <option value="itinerary">{__("Itineraries", "yatra")}</option>
114 <option value="voucher">{__("Vouchers", "yatra")}</option>
115 <option value="invoice">{__("Invoices", "yatra")}</option>
116 <option value="downloads">{__("Downloads", "yatra")}</option>
117 </select>
118 </div>
119 </div>
120 </div>
121 </div>
122
123 {/* Document Statistics */}
124 <div className="yatra-documents-stats flex flex-nowrap gap-6 overflow-x-auto">
125 <div className="flex-shrink-0 yatra-document-stat-card bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl p-6 min-w-0 flex-1">
126 <div>
127 <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
128 {__("Itinerary", "yatra")}
129 </p>
130 <p className="text-xl font-bold text-yatra-primary dark:text-yatra-on-dark">
131 {documentsByCategory.itinerary.length}
132 </p>
133 </div>
134 </div>
135 <div className="flex-shrink-0 yatra-document-stat-card bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl p-6 min-w-0 flex-1">
136 <div>
137 <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
138 {__("Voucher", "yatra")}
139 </p>
140 <p className="text-xl font-bold text-emerald-600 dark:text-emerald-400">
141 {documentsByCategory.voucher.length}
142 </p>
143 </div>
144 </div>
145 <div className="flex-shrink-0 yatra-document-stat-card bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl p-6 min-w-0 flex-1">
146 <div>
147 <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
148 {__("Invoice", "yatra")}
149 </p>
150 <p className="text-xl font-bold text-purple-600 dark:text-purple-400">
151 {documentsByCategory.invoice.length}
152 </p>
153 </div>
154 </div>
155
156 <div className="flex-shrink-0 yatra-document-stat-card bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl p-6 min-w-0 flex-1">
157 <div>
158 <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
159 {__("Downloads", "yatra")}
160 </p>
161 <p className="text-xl font-bold text-amber-600 dark:text-amber-400">
162 {documentsByCategory.downloads.length}
163 </p>
164 </div>
165 </div>
166 </div>
167
168 {/* Documents List */}
169 {filteredDocuments.length === 0 ? (
170 <div className="bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl p-12 text-center">
171 <div className="w-16 h-16 bg-gray-200 dark:bg-gray-700 rounded-full mx-auto mb-4"></div>
172 <p className="text-gray-500 dark:text-gray-400 font-medium mb-1">
173 {__("No documents found", "yatra")}
174 </p>
175 <p className="text-sm text-gray-400 dark:text-gray-500">
176 {__(
177 "No documents are available yet. Please check back later.",
178 "yatra",
179 )}
180 </p>
181 </div>
182 ) : (
183 <div className="space-y-4">
184 {filteredDocuments.map((doc) => (
185 <div
186 key={doc.id}
187 className="group bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm hover:shadow-md transition-all overflow-hidden"
188 >
189 <div className="p-6">
190 <div className="flex items-start gap-4">
191 <div className="flex-1 min-w-0">
192 <div className="flex items-start justify-between gap-4 mb-2">
193 <div className="flex-1 min-w-0">
194 <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
195 {doc.trip_title}
196 </p>
197 <h3 className="text-base font-bold text-gray-900 dark:text-white mb-1 truncate">
198 {doc.name}
199 </h3>
200 <p className="text-sm text-gray-600 dark:text-gray-400">
201 {__("Updated", "yatra")}: {formatDate(doc.updated_at)}
202 </p>
203 {doc.category === "downloads" && (
204 <p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
205 {__("Access", "yatra")}:{" "}
206 <span className="font-medium text-gray-700 dark:text-gray-300">
207 {doc.access_label || doc.access || "-"}
208 </span>
209 {doc.locked && doc.locked_reason ? (
210 <span className="ml-2 text-amber-700 dark:text-amber-300">
211 ({doc.locked_reason})
212 </span>
213 ) : null}
214 </p>
215 )}
216 </div>
217 <div className="flex items-center gap-2 flex-shrink-0">
218 {doc.category === "downloads" && doc.access_label && (
219 <span className="px-3 py-1 rounded-lg text-xs font-medium bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-300 border border-amber-200 dark:border-amber-800">
220 {doc.access_label}
221 </span>
222 )}
223 <span className="px-3 py-1 rounded-lg text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 capitalize">
224 {documentCategoryLabel(doc.category)}
225 </span>
226 </div>
227 </div>
228 <div className="yatra-document-actions flex flex-wrap gap-3 mt-4">
229 {doc.locked || !doc.url ? (
230 <div
231 className="inline-flex items-center px-4 py-2 rounded-lg bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 transition-colors text-sm font-medium cursor-not-allowed"
232 title={
233 doc.locked_reason || __("Not available", "yatra")
234 }
235 >
236 {__("Not Available", "yatra")}
237 </div>
238 ) : (
239 <button
240 type="button"
241 onClick={async () => {
242 try {
243 await downloadDocument({
244 bookingId: doc.booking_id,
245 paymentId: doc.payment_id,
246 documentType: doc.category as
247 | "voucher"
248 | "invoice"
249 | "itinerary"
250 | "all"
251 | "downloads",
252 fallbackUrl: doc.url,
253 });
254 } catch (err) {
255 alert(
256 err instanceof Error
257 ? err.message
258 : __("Download failed.", "yatra"),
259 );
260 }
261 }}
262 className="yatra-document-action yatra-document-action-download inline-flex items-center px-4 py-2 rounded-lg bg-yatra-primary text-white hover:bg-yatra-primary-dark transition-colors text-sm font-medium"
263 >
264 {__("Download", "yatra")}
265 </button>
266 )}
267
268 {doc.url ? (
269 <button
270 type="button"
271 onClick={async () => {
272 try {
273 await previewTravelDocument(doc);
274 } catch (err) {
275 alert(
276 err instanceof Error
277 ? err.message
278 : __("Preview failed.", "yatra"),
279 );
280 }
281 }}
282 className="yatra-document-action yatra-document-action-preview inline-flex items-center px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-sm font-medium"
283 >
284 {__("Preview", "yatra")}
285 </button>
286 ) : (
287 <div className="inline-flex items-center px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-700 text-gray-500 dark:text-gray-500 text-sm font-medium cursor-not-allowed">
288 {__("Preview", "yatra")}
289 </div>
290 )}
291 </div>
292 </div>
293 </div>
294 </div>
295 </div>
296 ))}
297 </div>
298 )}
299 </div>
300 );
301 };
302
303 export default Documents;
304