From 4ab15893ce2a49abf8f00561d9457a9c53c36559 Mon Sep 17 00:00:00 2001 From: ShaliniAvindya Date: Wed, 10 Sep 2025 15:33:56 +0530 Subject: [PATCH 01/31] reservation management, billing & invoice sections connected to database --- Client/package-lock.json | 66 +- Client/package.json | 2 +- Client/src/screens/BillingInvoice.jsx | 1307 ++++++++++------- .../BookingManagement.jsx | 358 +++-- .../CancellationsNoShows.jsx | 425 ++---- .../ReservationManagement/CheckInOut.jsx | 315 ++-- .../ReservationManagement/GuestManagement.jsx | 183 +-- .../ReservationManagement/SpecialRequests.jsx | 491 ++++--- Server/models/Billing.js | 55 + Server/models/BillingInvoice.js | 15 - .../models/ReservationManagement/Booking.js | 57 + .../ReservationManagement/Cancellation.js | 34 + Server/models/ReservationManagement/Guest.js | 43 + .../ReservationManagement/SpecialRequest.js | 31 + Server/models/{ => RoomManaagemnt}/Room.js | 0 .../{ => RoomManaagemnt}/RoomAvailability.js | 0 .../{ => RoomManaagemnt}/maintenance.js | 0 .../models/{ => RoomManaagemnt}/roomRate.js | 0 .../{ => RoomManaagemnt}/staffMember.js | 0 .../ReservationManagement/bookingRoutes.js | 277 ++++ .../ReservationManagement/cancelRoutes.js | 220 +++ .../ReservationManagement/checkInOutRoutes.js | 110 ++ .../ReservationManagement/guestRoutes.js | 75 + .../specialRequestRoutes.js | 139 ++ .../{ => RoomManaagemnt}/maintenanceRoutes.js | 7 +- .../roomAvailabilityRoutes.js | 2 +- .../{ => RoomManaagemnt}/roomRateRoutes.js | 2 +- .../routes/{ => RoomManaagemnt}/roomRoutes.js | 3 +- .../staffMemberRoutess.js | 2 +- Server/routes/billing.js | 30 - Server/routes/billingRoutes.js | 205 +++ Server/server.js | 23 +- 32 files changed, 2879 insertions(+), 1598 deletions(-) create mode 100644 Server/models/Billing.js delete mode 100644 Server/models/BillingInvoice.js create mode 100644 Server/models/ReservationManagement/Booking.js create mode 100644 Server/models/ReservationManagement/Cancellation.js create mode 100644 Server/models/ReservationManagement/Guest.js create mode 100644 Server/models/ReservationManagement/SpecialRequest.js rename Server/models/{ => RoomManaagemnt}/Room.js (100%) rename Server/models/{ => RoomManaagemnt}/RoomAvailability.js (100%) rename Server/models/{ => RoomManaagemnt}/maintenance.js (100%) rename Server/models/{ => RoomManaagemnt}/roomRate.js (100%) rename Server/models/{ => RoomManaagemnt}/staffMember.js (100%) create mode 100644 Server/routes/ReservationManagement/bookingRoutes.js create mode 100644 Server/routes/ReservationManagement/cancelRoutes.js create mode 100644 Server/routes/ReservationManagement/checkInOutRoutes.js create mode 100644 Server/routes/ReservationManagement/guestRoutes.js create mode 100644 Server/routes/ReservationManagement/specialRequestRoutes.js rename Server/routes/{ => RoomManaagemnt}/maintenanceRoutes.js (94%) rename Server/routes/{ => RoomManaagemnt}/roomAvailabilityRoutes.js (96%) rename Server/routes/{ => RoomManaagemnt}/roomRateRoutes.js (96%) rename Server/routes/{ => RoomManaagemnt}/roomRoutes.js (95%) rename Server/routes/{ => RoomManaagemnt}/staffMemberRoutess.js (96%) delete mode 100644 Server/routes/billing.js create mode 100644 Server/routes/billingRoutes.js diff --git a/Client/package-lock.json b/Client/package-lock.json index 7c71d4c..e027bd2 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -27,7 +27,7 @@ "i18next-browser-languagedetector": "^8.0.5", "js-cookie": "^3.0.5", "json2csv": "^6.0.0-alpha.2", - "jspdf": "^3.0.1", + "jspdf": "^3.0.2", "leaflet": "^1.9.4", "lodash": "^4.17.21", "lucide-react": "^0.487.0", @@ -1782,6 +1782,12 @@ "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==", "license": "MIT" }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -1986,18 +1992,6 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "node_modules/atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "license": "(MIT OR Apache-2.0)", - "bin": { - "atob": "bin/atob.js" - }, - "engines": { - "node": ">= 4.5.0" - } - }, "node_modules/autoprefixer": { "version": "10.4.21", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", @@ -2148,18 +2142,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/btoa": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", - "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", - "license": "(MIT OR Apache-2.0)", - "bin": { - "btoa": "bin/btoa.js" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2848,6 +2830,17 @@ "node": ">= 6" } }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -3266,6 +3259,12 @@ "loose-envify": "^1.0.0" } }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, "node_modules/is-arguments": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", @@ -3509,14 +3508,13 @@ } }, "node_modules/jspdf": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.1.tgz", - "integrity": "sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.2.tgz", + "integrity": "sha512-G0fQDJ5fAm6UW78HG6lNXyq09l0PrA1rpNY5i+ly17Zb1fMMFSmS+3lw4cnrAPGyouv2Y0ylujbY2Ieq3DSlKA==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.26.7", - "atob": "^2.1.2", - "btoa": "^1.2.1", + "@babel/runtime": "^7.26.9", + "fast-png": "^6.2.0", "fflate": "^0.8.1" }, "optionalDependencies": { @@ -3916,6 +3914,12 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/parchment": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", diff --git a/Client/package.json b/Client/package.json index 860b158..53e8685 100644 --- a/Client/package.json +++ b/Client/package.json @@ -27,7 +27,7 @@ "i18next-browser-languagedetector": "^8.0.5", "js-cookie": "^3.0.5", "json2csv": "^6.0.0-alpha.2", - "jspdf": "^3.0.1", + "jspdf": "^3.0.2", "leaflet": "^1.9.4", "lodash": "^4.17.21", "lucide-react": "^0.487.0", diff --git a/Client/src/screens/BillingInvoice.jsx b/Client/src/screens/BillingInvoice.jsx index cd4be1a..3949677 100644 --- a/Client/src/screens/BillingInvoice.jsx +++ b/Client/src/screens/BillingInvoice.jsx @@ -1,69 +1,202 @@ import React, { useState, useEffect } from 'react'; +import { jsPDF } from 'jspdf'; import { createPortal } from 'react-dom'; -import { useNavigate, useLocation } from 'react-router-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; import Sidebar from './Sidebar'; import { - CreditCard, - DollarSign, - Receipt, - Calendar, - User, - Phone, - Mail, - MapPin, - Clock, - Plus, - Minus, - Edit, - Trash2, - Download, - Printer, - Eye, - Search, - Filter, - RefreshCw, - CheckCircle, - XCircle, - AlertCircle, - Users, - Bed, - Coffee, - Utensils, - Car, - Wifi, - Home, - Star, - Mountain, - Sparkles, - X, - Save, - Send, - Home as HomeIcon, - Settings, - BarChart3, - Package, - UserCheck, - Heart, - ShoppingCart, - Monitor, - FileText, - ChefHat, - ChevronRight, - ChevronLeft, - Menu, + CreditCard, Receipt, FileText, Search, Filter, RefreshCw, CheckCircle, XCircle, AlertCircle, X, Download, Printer, Eye, Menu, History, } from 'lucide-react'; +// Helper to download invoice as PDF +const handleDownloadInvoice = (invoice, reservation) => { + const doc = new jsPDF(); + + doc.setFont("helvetica", "bold"); + doc.setFontSize(24); + doc.setTextColor(31, 41, 55); + doc.text('INVOICE', 20, 20); + + doc.setFontSize(14); + doc.setTextColor(107, 114, 128); + doc.text(invoice.invoiceNumber, 20, 28); + + doc.setFontSize(16); + doc.setTextColor(37, 99, 235); + doc.text('Hotel Paradise', 200, 20, { align: 'right' }); + doc.setFontSize(12); + doc.setTextColor(107, 114, 128); + doc.text('123 Resort Drive', 200, 28, { align: 'right' }); + doc.text('Paradise City, PC 12345', 200, 34, { align: 'right' }); + doc.text('Phone: +1 (555) 123-4567', 200, 40, { align: 'right' }); + + doc.setFontSize(14); + doc.setTextColor(31, 41, 55); + doc.text('Bill To:', 20, 60); + doc.setFont("helvetica", "normal"); + doc.setFontSize(12); + doc.text(reservation?.guestName || 'N/A', 20, 68); + doc.text(reservation?.email || 'N/A', 20, 74); + doc.text(reservation?.phone || 'N/A', 20, 80); + doc.text(reservation?.address || 'N/A', 20, 86); + + const room = invoice.reservation?.room || { name: 'Unknown', roomNumber: 'N/A' }; + const checkIn = invoice.reservation?.checkIn ? new Date(invoice.reservation.checkIn).toLocaleDateString() : 'N/A'; + const checkOut = invoice.reservation?.checkOut ? new Date(invoice.reservation.checkOut).toLocaleDateString() : 'N/A'; + + doc.setFont("helvetica", "bold"); + doc.setFontSize(14); + doc.text('Invoice Details:', 110, 60); + doc.setFont("helvetica", "normal"); + doc.setFontSize(12); + doc.text(`Invoice Date: ${new Date(invoice.issueDate).toLocaleDateString()}`, 110, 68); + doc.text(`Due Date: ${new Date(invoice.dueDate).toLocaleDateString()}`, 110, 74); + doc.text(`Check-in: ${checkIn}`, 110, 80); + doc.text(`Check-out: ${checkOut}`, 110, 86); + doc.text(`Room: ${room.name} (${room.roomNumber})`, 110, 92); + + doc.setFillColor(249, 250, 251); + doc.rect(20, 110, 170, 10, 'F'); + doc.setFont("helvetica", "bold"); + doc.setFontSize(12); + doc.setTextColor(31, 41, 55); + doc.text('Description', 22, 116); + doc.text('Qty', 120, 116, { align: 'right' }); + doc.text('Rate', 150, 116, { align: 'right' }); + doc.text('Total', 188, 116, { align: 'right' }); + + let y = 126; + invoice.items.forEach((item) => { + doc.setFont("helvetica", "normal"); + doc.setTextColor(31, 41, 55); + doc.text(item.description, 22, y, { maxWidth: 90 }); + doc.text(item.quantity.toString(), 120, y, { align: 'right' }); + doc.text(`$${item.unitPrice.toFixed(2)}`, 150, y, { align: 'right' }); + doc.setFont("helvetica", "bold"); + doc.text(`$${item.total.toFixed(2)}`, 188, y, { align: 'right' }); + y += 10; + }); + + const totalsX = 130; + doc.setLineWidth(0.5); + doc.line(130, y, 190, y); + y += 8; + doc.setFont("helvetica", "normal"); + doc.setFontSize(12); + doc.text('Subtotal:', totalsX, y); + doc.text(`$${invoice.subtotal.toFixed(2)}`, 188, y, { align: 'right' }); + y += 8; + doc.text('Tax (10%):', totalsX, y); + doc.text(`$${invoice.tax.toFixed(2)}`, 188, y, { align: 'right' }); + y += 8; + doc.setFont("helvetica", "bold"); + doc.setFontSize(14); + doc.text('Total:', totalsX, y); + doc.text(`$${invoice.total.toFixed(2)}`, 188, y, { align: 'right' }); + y += 8; + doc.setFont("helvetica", "normal"); + doc.setFontSize(12); + doc.setTextColor(22, 163, 74); + doc.text('Paid:', totalsX, y); + doc.text(`$${invoice.paidAmount.toFixed(2)}`, 188, y, { align: 'right' }); + y += 8; + doc.setFont("helvetica", "bold"); + doc.setTextColor(220, 38, 38); + doc.text('Balance Due:', totalsX, y); + doc.text(`$${(invoice.total - invoice.paidAmount).toFixed(2)}`, 188, y, { align: 'right' }); + + y += 16; + doc.setFillColor(...( + invoice.status === 'paid' + ? [220, 252, 231] + : invoice.status === 'overdue' + ? [254, 226, 226] + : [254, 243, 199] + )); + doc.rect(20, y - 6, 170, 12, 'F'); + doc.setFont("helvetica", "bold"); + doc.setFontSize(12); + doc.setTextColor(...( + invoice.status === 'paid' ? [22, 163, 74] : + invoice.status === 'overdue' ? [220, 38, 38] : + [202, 138, 4] + )); + doc.text(invoice.status.toUpperCase(), 22, y); + + doc.save(`invoice_${invoice.invoiceNumber}.pdf`); +}; + +// PaymentHistoryModal component +const PaymentHistoryModal = ({ invoice, onClose }) => { + const reservation = invoice.reservation; + const payments = invoice.payments || []; + + const modalContent = ( +
+
+
+

Payment History for Invoice {invoice.invoiceNumber}

+ +
+
+
+

Guest Information

+

Name: {reservation?.guestName}

+

Email: {reservation?.email}

+

Room: {reservation?.room?.name} ({reservation?.room?.roomNumber})

+
+ {payments.length === 0 ? ( +

No payments recorded for this invoice.

+ ) : ( +
+ + + + + + + + + + + + + {payments.map((payment) => ( + + + + + + + + + ))} + +
Payment IDAmountTypeMethodDateReference
{payment.id}${payment.amount.toFixed(2)}{payment.type.replace('_', ' ')}{payment.method.replace('_', ' ')}{new Date(payment.date).toLocaleDateString()}{payment.reference || 'N/A'}
+
+ )} +
+
+
+ ); + + return createPortal(modalContent, document.body); +}; -// BillingInvoice Component const BillingInvoice = () => { const [reservations, setReservations] = useState([]); const [invoices, setInvoices] = useState([]); const [payments, setPayments] = useState([]); + const [rooms, setRooms] = useState([]); + const [additionalServices, setAdditionalServices] = useState([]); const [selectedReservation, setSelectedReservation] = useState(null); const [selectedInvoice, setSelectedInvoice] = useState(null); const [showBillingForm, setShowBillingForm] = useState(false); const [showPaymentForm, setShowPaymentForm] = useState(false); const [showInvoiceDetails, setShowInvoiceDetails] = useState(false); + const [showPaymentHistory, setShowPaymentHistory] = useState(false); const [activeTab, setActiveTab] = useState('reservations'); const [searchQuery, setSearchQuery] = useState(''); const [filterStatus, setFilterStatus] = useState('all'); @@ -71,253 +204,112 @@ const BillingInvoice = () => { const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarMinimized, setSidebarMinimized] = useState(false); - // Room data - const rooms = [ - { - id: 'R001', - roomNumber: '101', - type: 'single', - name: 'Deluxe Single Room', - capacity: 1, - maxCapacity: 2, - basePrice: 120, - weekendPrice: 150, - floor: 1, - size: 250, - description: 'A comfortable single room with modern amenities and city view.', - amenities: ['wifi', 'tv', 'ac', 'minibar', 'phone'], - images: [ - 'https://images.unsplash.com/photo-1631049307264-da0ec9d70304?w=500&h=300&fit=crop', - 'https://images.unsplash.com/photo-1566665797739-1674de7a421a?w=500&h=300&fit=crop', - ], - rating: 4.2, - }, - { - id: 'R002', - roomNumber: '201', - type: 'double', - name: 'Premium Double Room', - capacity: 2, - maxCapacity: 3, - basePrice: 180, - weekendPrice: 220, - floor: 2, - size: 350, - description: 'Spacious double room with king-size bed and ocean view.', - amenities: ['wifi', 'tv', 'ac', 'minibar', 'balcony', 'oceanview', 'bathtub'], - images: [ - 'https://images.unsplash.com/photo-1618773928121-c32242e63f39?w=500&h=300&fit=crop', - 'https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?w=500&h=300&fit=crop', - ], - rating: 4.7, - }, - { - id: 'R003', - roomNumber: '301', - type: 'suite', - name: 'Executive Suite', - capacity: 4, - maxCapacity: 6, - basePrice: 350, - weekendPrice: 420, - floor: 3, - size: 600, - description: 'Luxurious suite with separate living area and premium amenities.', - amenities: ['wifi', 'tv', 'ac', 'kitchen', 'balcony', 'oceanview', 'bathtub', 'sofa', 'work_desk'], - images: [ - 'https://images.unsplash.com/photo-1560185893-a55cbc8c57e8?w=500&h=300&fit=crop', - 'https://images.unsplash.com/photo-1590490360182-c33d57733427?w=500&h=300&fit=crop', - ], - rating: 4.9, - }, - ]; - - // Additional services/charges - const additionalServices = [ - { id: 'minibar', name: 'Minibar', category: 'room', basePrice: 15 }, - { id: 'restaurant', name: 'Restaurant Dining', category: 'dining', basePrice: 45 }, - { id: 'room_service', name: 'Room Service', category: 'dining', basePrice: 35 }, - { id: 'spa', name: 'Spa Services', category: 'wellness', basePrice: 80 }, - { id: 'laundry', name: 'Laundry Service', category: 'service', basePrice: 25 }, - { id: 'parking', name: 'Parking', category: 'service', basePrice: 20 }, - { id: 'wifi_premium', name: 'Premium WiFi', category: 'technology', basePrice: 10 }, - { id: 'breakfast', name: 'Breakfast', category: 'dining', basePrice: 18 }, - { id: 'airport_transfer', name: 'Airport Transfer', category: 'transport', basePrice: 60 }, - { id: 'late_checkout', name: 'Late Checkout', category: 'service', basePrice: 30 }, - ]; - - // Sample data initialization useEffect(() => { - const sampleReservations = [ - { - id: 'RES001', - guestName: 'John Smith', - email: 'john.smith@email.com', - phone: '+1 555-0123', - address: '123 Main St, New York, NY 10001', - roomId: 'R001', - checkIn: '2025-08-15', - checkOut: '2025-08-18', - guests: 1, - nights: 3, - roomRate: 120, - totalRoomCharges: 360, - status: 'confirmed', - bookingDate: '2025-08-01', - specialRequests: 'Late check-in', - advancePayment: 180, - deposit: 100, - additionalCharges: [ - { serviceId: 'minibar', quantity: 2, unitPrice: 15, total: 30, date: '2025-08-15' }, - { serviceId: 'breakfast', quantity: 3, unitPrice: 18, total: 54, date: '2025-08-16' }, - ], - }, - { - id: 'RES002', - guestName: 'Sarah Johnson', - email: 'sarah.johnson@email.com', - phone: '+1 555-0124', - address: '456 Oak Ave, Los Angeles, CA 90210', - roomId: 'R002', - checkIn: '2025-08-20', - checkOut: '2025-08-25', - guests: 2, - nights: 5, - roomRate: 180, - totalRoomCharges: 900, - status: 'checked_in', - bookingDate: '2025-08-05', - specialRequests: 'Ocean view preferred', - advancePayment: 450, - deposit: 200, - additionalCharges: [ - { serviceId: 'spa', quantity: 1, unitPrice: 80, total: 80, date: '2025-08-21' }, - { serviceId: 'room_service', quantity: 2, unitPrice: 35, total: 70, date: '2025-08-22' }, - { serviceId: 'parking', quantity: 5, unitPrice: 20, total: 100, date: '2025-08-20' }, - ], - }, - { - id: 'RES003', - guestName: 'Michael Brown', - email: 'michael.brown@email.com', - phone: '+1 555-0125', - address: '789 Pine St, Chicago, IL 60601', - roomId: 'R003', - checkIn: '2025-08-10', - checkOut: '2025-08-14', - guests: 4, - nights: 4, - roomRate: 350, - totalRoomCharges: 1400, - status: 'checked_out', - bookingDate: '2025-07-25', - specialRequests: 'Extra towels', - advancePayment: 700, - deposit: 300, - additionalCharges: [ - { serviceId: 'restaurant', quantity: 3, unitPrice: 45, total: 135, date: '2025-08-11' }, - { serviceId: 'minibar', quantity: 4, unitPrice: 15, total: 60, date: '2025-08-12' }, - { serviceId: 'airport_transfer', quantity: 1, unitPrice: 60, total: 60, date: '2025-08-14' }, - ], - }, - ]; - - const samplePayments = [ - { - id: 'PAY001', - reservationId: 'RES001', - amount: 180, - type: 'advance', - method: 'credit_card', - status: 'completed', - date: '2025-08-01', - reference: 'CC-20250801-001', - notes: 'Advance payment for booking', - }, - { - id: 'PAY002', - reservationId: 'RES001', - amount: 100, - type: 'deposit', - method: 'credit_card', - status: 'completed', - date: '2025-08-01', - reference: 'CC-20250801-002', - notes: 'Security deposit', - }, - { - id: 'PAY003', - reservationId: 'RES002', - amount: 450, - type: 'advance', - method: 'bank_transfer', - status: 'completed', - date: '2025-08-05', - reference: 'BT-20250805-001', - notes: 'Bank transfer advance payment', - }, - { - id: 'PAY004', - reservationId: 'RES003', - amount: 1955, - type: 'full_payment', - method: 'credit_card', - status: 'completed', - date: '2025-08-14', - reference: 'CC-20250814-001', - notes: 'Full payment at checkout', - }, - ]; - - const sampleInvoices = [ - { - id: 'INV001', - reservationId: 'RES003', - invoiceNumber: 'INV-2025-001', - issueDate: '2025-08-14', - dueDate: '2025-08-14', - status: 'paid', - subtotal: 1655, - tax: 165.50, - total: 1820.50, - paidAmount: 1955, - items: [ - { description: 'Room Charges (4 nights)', quantity: 4, unitPrice: 350, total: 1400 }, - { description: 'Restaurant Dining', quantity: 3, unitPrice: 45, total: 135 }, - { description: 'Minibar', quantity: 4, unitPrice: 15, total: 60 }, - { description: 'Airport Transfer', quantity: 1, unitPrice: 60, total: 60 }, - ], - }, - ]; - - setReservations(sampleReservations); - setPayments(samplePayments); - setInvoices(sampleInvoices); + const fetchData = async () => { + setLoading(true); + try { + const [bookingsRes, roomsRes, invoicesRes, paymentsRes, servicesRes] = await Promise.all([ + fetch('/api/bookings'), + fetch('/api/billing/rooms'), + fetch('/api/billing/invoices'), + fetch('/api/billing/payments'), + fetch('/api/billing/services'), + ]); + const bookings = await bookingsRes.json(); + const rooms = await roomsRes.json(); + let invoices = await invoicesRes.json(); + const payments = await paymentsRes.json(); + const services = await servicesRes.json(); + + // Map bookings to reservations format + const mappedReservations = bookings.map(booking => ({ + id: booking.id, + guestName: `${booking.firstName} ${booking.lastName}`.trim(), + email: booking.guestEmail, + phone: booking.guestPhone, + address: booking.address || '', + roomId: booking.roomId, + room: rooms.find(room => room.id === booking.roomId) || { name: 'Unknown', roomNumber: 'N/A' }, + checkIn: booking.checkInDate, + checkOut: booking.checkOutDate, + guests: booking.guests, + nights: Math.ceil((new Date(booking.checkOutDate) - new Date(booking.checkInDate)) / (1000 * 60 * 60 * 24)), + roomRate: booking.totalAmount / Math.ceil((new Date(booking.checkOutDate) - new Date(booking.checkInDate)) / (1000 * 60 * 60 * 24)), + totalRoomCharges: booking.totalAmount, + status: booking.status.replace('-', '_'), + bookingDate: booking.createdAt, + specialRequests: booking.specialRequests, + advancePayment: booking.depositAmount, + deposit: booking.depositAmount, + additionalCharges: [ + ...(booking.minibarCharges ? [{ serviceId: 'minibar', quantity: 1, unitPrice: booking.minibarCharges, total: booking.minibarCharges, date: booking.checkInDate }] : []), + ...(booking.additionalServices ? [{ serviceId: 'additional_services', quantity: 1, unitPrice: booking.additionalServices, total: booking.additionalServices, date: booking.checkInDate }] : []), + ...(booking.damageCharges ? [{ serviceId: 'damage', quantity: 1, unitPrice: booking.damageCharges, total: booking.damageCharges, date: booking.checkInDate }] : []), + ], + finalAmount: booking.finalAmount, + })); + + // Attach payments and reservation to invoices + const currentDate = new Date('2025-09-10'); + invoices = invoices.map(invoice => ({ + ...invoice, + status: invoice.paidAmount >= invoice.total + ? 'paid' + : new Date(invoice.dueDate) < currentDate + ? 'overdue' + : 'pending', + reservation: mappedReservations.find(r => r.id === invoice.reservationId) || null, + payments: payments.filter(p => p.reservationId === invoice.reservationId), + })); + + setReservations(mappedReservations); + setRooms(rooms); + setInvoices(invoices); + setPayments(payments); + setAdditionalServices(services); + } catch (err) { + toast.error('Failed to fetch data'); + console.error(err); + } finally { + setLoading(false); + } + }; + fetchData(); }, []); - const getRoomById = (roomId) => rooms.find(room => room.id === roomId); + const getRoomById = (roomId) => rooms.find(room => room.id === roomId) || { name: 'Unknown', roomNumber: 'N/A' }; - const getServiceById = (serviceId) => additionalServices.find(service => service.id === serviceId); + const getServiceById = (serviceId) => additionalServices.find(service => service.id === serviceId) || { name: 'Unknown', basePrice: 0 }; const calculateTotalCharges = (reservation) => { const additionalTotal = reservation.additionalCharges?.reduce((sum, charge) => sum + charge.total, 0) || 0; - return reservation.totalRoomCharges + additionalTotal; + const subtotal = reservation.totalRoomCharges + additionalTotal; + const tax = subtotal * 0.1; + return subtotal + tax; }; - const getPaymentsByReservation = (reservationId) => { - return payments.filter(payment => payment.reservationId === reservationId); - }; + const getPaymentsByReservation = (reservationId) => payments.filter(payment => payment.reservationId === reservationId); - const getTotalPaid = (reservationId) => { - return getPaymentsByReservation(reservationId).reduce((sum, payment) => sum + payment.amount, 0); - }; + const getTotalPaid = (reservationId) => getPaymentsByReservation(reservationId).reduce((sum, payment) => sum + payment.amount, 0); - const getOutstandingBalance = (reservation) => { - const total = calculateTotalCharges(reservation); - const paid = getTotalPaid(reservation.id); - return total - paid; + const getOutstandingBalance = (reservation) => calculateTotalCharges(reservation) - getTotalPaid(reservation.id); + + const getInvoiceStatus = (reservationId) => { + const invoice = invoices + .filter(inv => inv.reservationId === reservationId) + .sort((a, b) => new Date(b.issueDate) - new Date(a.issueDate))[0]; + return invoice ? invoice.status : 'No Invoice'; }; + const uniqueInvoices = Object.values( + invoices.reduce((acc, invoice) => { + if (!acc[invoice.reservationId] || new Date(invoice.issueDate) > new Date(acc[invoice.reservationId].issueDate)) { + acc[invoice.reservationId] = invoice; + } + return acc; + }, {}) + ); + const filteredReservations = reservations.filter(reservation => { const matchesSearch = reservation.guestName.toLowerCase().includes(searchQuery.toLowerCase()) || reservation.id.toLowerCase().includes(searchQuery.toLowerCase()) || @@ -353,13 +345,78 @@ const BillingInvoice = () => { } }; - const removeCharge = (index) => { - setCharges(charges.filter((_, i) => i !== index)); - }; + const removeCharge = (index) => setCharges(charges.filter((_, i) => i !== index)); - const handleSubmit = (e) => { + const handleSubmit = async (e) => { e.preventDefault(); - onSave({ ...reservation, additionalCharges: charges }); + try { + const updatedBooking = { + ...reservation, + minibarCharges: charges.filter(c => c.serviceId === 'minibar').reduce((sum, c) => sum + c.total, 0), + additionalServices: charges.filter(c => c.serviceId !== 'minibar' && c.serviceId !== 'damage').reduce((sum, c) => sum + c.total, 0), + damageCharges: charges.filter(c => c.serviceId === 'damage').reduce((sum, c) => sum + c.total, 0), + }; + await fetch(`/api/bookings/${reservation.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updatedBooking), + }); + toast.success('Charges updated successfully'); + onSave({ ...reservation, additionalCharges: charges }); + const [bookingsRes, invoicesRes, paymentsRes] = await Promise.all([ + fetch('/api/bookings'), + fetch('/api/billing/invoices'), + fetch('/api/billing/payments'), + ]); + const bookings = await bookingsRes.json(); + const rooms = await (await fetch('/api/billing/rooms')).json(); + let invoices = await invoicesRes.json(); + const payments = await paymentsRes.json(); + const mappedReservations = bookings.map(booking => ({ + id: booking.id, + guestName: `${booking.firstName} ${booking.lastName}`.trim(), + email: booking.guestEmail, + phone: booking.guestPhone, + address: booking.address || '', + roomId: booking.roomId, + room: rooms.find(room => room.id === booking.roomId) || { name: 'Unknown', roomNumber: 'N/A' }, + checkIn: booking.checkInDate, + checkOut: booking.checkOutDate, + guests: booking.guests, + nights: Math.ceil((new Date(booking.checkOutDate) - new Date(booking.checkInDate)) / (1000 * 60 * 60 * 24)), + roomRate: booking.totalAmount / Math.ceil((new Date(booking.checkOutDate) - new Date(booking.checkInDate)) / (1000 * 60 * 60 * 24)), + totalRoomCharges: booking.totalAmount, + status: booking.status.replace('-', '_'), + bookingDate: booking.createdAt, + specialRequests: booking.specialRequests, + advancePayment: booking.depositAmount, + deposit: booking.depositAmount, + additionalCharges: [ + ...(booking.minibarCharges ? [{ serviceId: 'minibar', quantity: 1, unitPrice: booking.minibarCharges, total: booking.minibarCharges, date: booking.checkInDate }] : []), + ...(booking.additionalServices ? [{ serviceId: 'additional_services', quantity: 1, unitPrice: booking.additionalServices, total: booking.additionalServices, date: booking.checkInDate }] : []), + ...(booking.damageCharges ? [{ serviceId: 'damage', quantity: 1, unitPrice: booking.damageCharges, total: booking.damageCharges, date: booking.checkInDate }] : []), + ], + finalAmount: booking.finalAmount, + })); + + invoices = invoices.map(invoice => ({ + ...invoice, + status: invoice.paidAmount >= invoice.total + ? 'paid' + : new Date(invoice.dueDate) < currentDate + ? 'overdue' + : 'pending', + reservation: mappedReservations.find(r => r.id === invoice.reservationId) || null, + payments: payments.filter(p => p.reservationId === invoice.reservationId), + })); + + setReservations(mappedReservations); + setInvoices(invoices); + setPayments(payments); + } catch (err) { + toast.error('Failed to update charges'); + console.error(err); + } }; const modalContent = ( @@ -371,14 +428,13 @@ const BillingInvoice = () => { -

Guest Information

Name: {reservation.guestName}
-
Room: {getRoomById(reservation.roomId)?.name} ({getRoomById(reservation.roomId)?.roomNumber})
+
Room: {reservation.room?.name} ({reservation.room?.roomNumber})
Check-in: {new Date(reservation.checkIn).toLocaleDateString()}
Check-out: {new Date(reservation.checkOut).toLocaleDateString()}
Nights: {reservation.nights}
@@ -389,7 +445,7 @@ const BillingInvoice = () => {
Room Rate (per night): - ${reservation.roomRate} + ${reservation.roomRate.toFixed(2)}
Number of Nights: @@ -397,12 +453,11 @@ const BillingInvoice = () => {
Total Room Charges: - ${reservation.totalRoomCharges} + ${reservation.totalRoomCharges.toFixed(2)}
-

Add Additional Charges

@@ -458,7 +513,6 @@ const BillingInvoice = () => {
- {charges.length > 0 && (

Current Additional Charges

@@ -475,41 +529,40 @@ const BillingInvoice = () => { - {charges.map((charge, index) => { - const service = getServiceById(charge.serviceId); - return ( - - {service?.name} - {new Date(charge.date).toLocaleDateString()} - {charge.quantity} - ${charge.unitPrice} - ${charge.total} - - - - - ); - })} + {charges.map((charge, index) => ( + + {getServiceById(charge.serviceId).name} + {new Date(charge.date).toLocaleDateString()} + {charge.quantity} + ${charge.unitPrice.toFixed(2)} + ${charge.total.toFixed(2)} + + + + + ))}
- Additional Charges Total: ${charges.reduce((sum, charge) => sum + charge.total, 0)} + Additional Charges Total: ${charges.reduce((sum, charge) => sum + charge.total, 0).toFixed(2)} +
+
+ Tax (10%): ${(reservation.totalRoomCharges + charges.reduce((sum, charge) => sum + charge.total, 0)) * 0.1.toFixed(2)}
- Grand Total: ${reservation.totalRoomCharges + charges.reduce((sum, charge) => sum + charge.total, 0)} + Grand Total: ${(reservation.totalRoomCharges + charges.reduce((sum, charge) => sum + charge.total, 0) * 1.1).toFixed(2)}
)} -
- - -
- -

{reservation.guestName}

-

Outstanding Balance: ${outstandingBalance.toFixed(2)}

-
- -
- - setPaymentData({ ...paymentData, amount: e.target.value })} - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" - required - /> -
- -
- - -
- -
- - -
- -
- - setPaymentData({ ...paymentData, reference: e.target.value })} - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" - placeholder="Transaction reference" - /> -
- -
- -