| @@ -2,11 +2,12 @@ | ||
| 2 | 2 | * View Booking Page |
| 3 | 3 | * Display booking details in a clean, minimal SaaS-style design |
| 4 | 4 | */ |
| 5 | 5 | |
| 6 | -import React, { useMemo } from "react"; | |
| 6 | +import React, { useMemo, useState } from "react"; | |
| 7 | 7 | import { useQuery } from "@tanstack/react-query"; |
| 8 | 8 | import { |
| 9 | + Download, | |
| 9 | 10 | ArrowLeft, |
| 10 | 11 | Mail, |
| 11 | 12 | Phone, |
| 12 | 13 | Calendar, |
| @@ -35,8 +36,9 @@ | ||
| 35 | 36 | CardTitle, |
| 36 | 37 | } from "../components/ui/card"; |
| 37 | 38 | import { ConditionalRender } from "../components/ui/conditional-render"; |
| 38 | 39 | import { ResendEmailMenu } from "../components/bookings/ResendEmailMenu"; |
| 40 | +import { downloadAdminBookingInvoice } from "../lib/invoice-download"; | |
| 39 | 41 | import { Skeleton } from "../components/ui/skeleton"; |
| 40 | 42 | import { formatYatraMoney } from "../lib/currency-display"; |
| 41 | 43 | |
| 42 | 44 | interface GoogleCalendarSyncInfo { |
| @@ -352,8 +354,29 @@ | ||
| 352 | 354 | const handleEdit = () => { |
| 353 | 355 | window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=bookings&action=edit&id=${bookingId}`; |
| 354 | 356 | }; |
| 355 | 357 | |
| 358 | + // The booking invoice works for unpaid bookings too — it renders as a | |
| 359 | + // pro-forma invoice with the gateway's payment instructions. This page has | |
| 360 | + // no toast provider, so a failure is rendered as an inline banner under the | |
| 361 | + // header; a tooltip alone would leave a failed download looking like nothing | |
| 362 | + // happened. | |
| 363 | + const [invoiceError, setInvoiceError] = useState<string>(""); | |
| 364 | + const handleDownloadInvoice = async () => { | |
| 365 | + const id = Number(booking?.id ?? 0); | |
| 366 | + if (!id) { | |
| 367 | + return; | |
| 368 | + } | |
| 369 | + setInvoiceError(""); | |
| 370 | + try { | |
| 371 | + await downloadAdminBookingInvoice(id); | |
| 372 | + } catch (error: any) { | |
| 373 | + setInvoiceError( | |
| 374 | + error?.message || __("Failed to download invoice", "yatra"), | |
| 375 | + ); | |
| 376 | + } | |
| 377 | + }; | |
| 378 | + | |
| 356 | 379 | if (isLoading) { |
| 357 | 380 | return ( |
| 358 | 381 | <div className="space-y-3"> |
| 359 | 382 | {/* Header Skeleton */} |
| @@ -503,8 +526,18 @@ | ||
| 503 | 526 | title={__("Booking Details", "yatra")} |
| 504 | 527 | description={__("View complete booking information", "yatra")} |
| 505 | 528 | actions={ |
| 506 | 529 | <div className="flex gap-2"> |
| 530 | + <ConditionalRender capability="yatra_view_bookings"> | |
| 531 | + <Button | |
| 532 | + variant="outline" | |
| 533 | + onClick={handleDownloadInvoice} | |
| 534 | + className="flex items-center gap-2" | |
| 535 | + > | |
| 536 | + <Download className="w-4 h-4" /> | |
| 537 | + {__("Download invoice", "yatra")} | |
| 538 | + </Button> | |
| 539 | + </ConditionalRender> | |
| 507 | 540 | <ConditionalRender capability="yatra_edit_bookings"> |
| 508 | 541 | <ResendEmailMenu |
| 509 | 542 | bookingId={Number(booking.id)} |
| 510 | 543 | status={booking.booking_status} |
| @@ -526,8 +559,18 @@ | ||
| 526 | 559 | </Button> |
| 527 | 560 | </div> |
| 528 | 561 | } |
| 529 | 562 | /> |
| 563 | + | |
| 564 | + {invoiceError && ( | |
| 565 | + <div | |
| 566 | + role="alert" | |
| 567 | + className="flex items-center gap-2 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-800 dark:bg-red-900/20 dark:text-red-400" | |
| 568 | + > | |
| 569 | + <AlertCircle className="w-4 h-4 shrink-0" /> | |
| 570 | + {invoiceError} | |
| 571 | + </div> | |
| 572 | + )} | |
| 530 | 573 | |
| 531 | 574 | <ConditionalRender capability="yatra_view_bookings"> |
| 532 | 575 | <div className="grid grid-cols-1 lg:grid-cols-3 gap-3"> |
| 533 | 576 | {/* Main Content */} |