PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.7
Yatra – Travel Booking & Tour Operator Software v3.0.2.7
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.2.7, at resources/js/pages/account/Documents.tsx

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