From 24b40450dd7fe332ad4b321dffeeacaac999a0e7 Mon Sep 17 00:00:00 2001 From: Iliyan Angelov Date: Sat, 29 Nov 2025 17:23:06 +0200 Subject: [PATCH] updates --- .../invoice_routes.cpython-312.pyc | Bin 12091 -> 14331 bytes Backend/src/routes/invoice_routes.py | 34 ++ .../invoice_service.cpython-312.pyc | Bin 21271 -> 23007 bytes Backend/src/services/invoice_service.py | 63 ++- Frontend/src/App.tsx | 18 + Frontend/src/components/layout/Header.tsx | 472 ++++++++---------- Frontend/src/components/layout/Navbar.tsx | 135 +++++ Frontend/src/components/layout/index.ts | 1 + .../src/components/rooms/BannerCarousel.tsx | 16 +- .../src/components/rooms/SearchRoomForm.tsx | 58 +-- .../src/pages/admin/BannerManagementPage.tsx | 133 ++--- .../src/pages/admin/BlogManagementPage.tsx | 51 +- .../src/pages/admin/BookingManagementPage.tsx | 364 ++++++++++++-- .../admin/EmailCampaignManagementPage.tsx | 400 ++++++++------- Frontend/src/pages/admin/InvoiceEditPage.tsx | 341 +++++++++++++ .../src/pages/admin/LoyaltyManagementPage.tsx | 102 ++-- .../src/pages/admin/PackageManagementPage.tsx | 49 +- .../src/pages/admin/PageContentDashboard.tsx | 118 +++-- .../pages/admin/SecurityManagementPage.tsx | 133 +++-- .../src/pages/admin/UserManagementPage.tsx | 13 +- Frontend/src/pages/customer/InvoicePage.tsx | 136 ++++- .../src/pages/staff/BookingManagementPage.tsx | 38 +- Frontend/src/services/api/invoiceService.ts | 49 +- 23 files changed, 1911 insertions(+), 813 deletions(-) create mode 100644 Frontend/src/components/layout/Navbar.tsx create mode 100644 Frontend/src/pages/admin/InvoiceEditPage.tsx diff --git a/Backend/src/routes/__pycache__/invoice_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/invoice_routes.cpython-312.pyc index d897a5595a343aba805aaef7ef35bcd824d6035d..d741b9c1f380bed138bfe33207ab8e4b1d7bccd6 100644 GIT binary patch delta 3379 zcmZ`5TWl29b?(f*cXr2*wbxHP*amz2gz_fFyo-6nbqpaPp;^|Ru~}yy&fQtS%bHCH zqNa*!sw*{BP$l@PElf+a{h)26P$?9ueq>#!3?ovtYMZtnRUJS|X+PR~?(BLuh>SGn zo_k*R+;h)8=l=4I|Hiz(^>|zy{bs&w3$1ARzPAd!_hCzerAkxA0zm)jr>-R(!q(bG z&8IeLjuD5t?ws{%PUN)>6BWvOvd{8kG@E&3Q4ng*)Y<0HvMwrM*b{LL2SO@V=nUHc zv;*iMpW6rP{UE|qZY3b%sHO)((G#)IuolPFE7> z##C*yY#^6nH^b0HHk9u|eI!-hjJ`pxl(%jNr~h#gQA^;0jXlzC!g0eqGQBayX+V{(jicS=-^=r zlv1n$UJgJ5z|M07Fcvfjg5iqL(1!zRA~J5~g4bMW$tws5ev@4BmA4o6?lSkz3_cS8 zw(9`7Rom3S5(Dl~Vef3`cmixrWbAdn7wS&{&h}^vMq-Jmj!yz5x5K9m3f*CoymEPA zhb)s=`g$-s$TOf&Nyg-w66O(enQf8zJy`JDS>$&Lv7&-bz+TR+CR#+PAW(F@H)-e=0!jdD*~j{3_s90$^o?-TeWU z^NP*9n1z6aL2MfU7Aq)RW~|KY_7~NZIHc`REcRw7I*QL{F1J>p^~sX%(;;=TJFZ35 zPAw7)g@-NFME`BV`$@%ZU^XbB?Qg zPR{v!3Cyd`C{kUMu9Pr_z&5fGR8_~PY$UkSS3$Lw6&W;9@?m3Ji#z3xdvtf6sUp=q zC*+qqi>O=nlzU7DlXll7P8I2XgOac7689q^<(%_erD1{ZT~6grQ~U?Q`@(svl?F<0 zHaQ$O*7B@+np*OL-MmO)q=v2euuvnAL+nez>eAQh2y-QQt%KYK?!AE~PG@Zsl$$xs zBknAxx0$v$H<0=#MH;P6a;3EXg@Ui$Igz44E!9`&rztVN40V)O%jU|-w`wXpzG4aT zR;fQNo)J%?liVrc2zL@)6HX$fq93yxvzyv7Zw%C91sQdLQ7x+Bq8c1vi5NJpN5Z(& z6mmg=nsln-4#flGI5rZ)ksxbe43qboEt%F@S|5_$muW|rqcL3`p#@DfkgNy>ug45~ zJTWw;4eN#lR!BN!YFn4}n4GLM(RgB*Hum^PA{?FoI(~;bk*sH(2qaW`f#h8Omzl4d zYZ@HXVIQAbZ%GG?r!drI<&jf@=5)6iB4{H z0OM9C0=&Cyk0ksWZFlZ*3AbHVI=L+gl&x$ezfdlv@kMIL%s+1~BXh@NrnQfwgwYST49 zm*T_6X<97V#I=-)prkq{w4kqa2^XBzx)fHV)z<$O>7%g5cWFu)^;2>P3(}?vE!sx>upKXCMmQbHL23Hq6nx^ijYdP-Nc))#P*um z-I4@RBUB_*E>!OVf-6F;IDqzo)Jhx>;($0J#7bP?1gBO&4{(5)c}^UnP)q(aJ3sHe z`R3C<$$s--oW=AIgeycSv6|5S9jqm zX<7*{7c@-xX5qk=gt@i2%e#Q<`UVAL5Q22gfR&9*bi>gb29n3m0nvvW1ElS+t%rG8o`MpZ00A^yGF)q{_@Z-;r|r(VsWUcDR70%{Ql&_3l^wYDbqCr zbk-MhH zbFHd#1njD*zid_;COC3W(#8q;T2|9(*mbAvLAeE)4I?fi$K8zHE1nzAH20wgWC;ey zH>T@#qh{(l@7h2G+oPSeM79nK8=vdRyqzaqq+#P4v)SD6Tsbw|TW_L{iwf6CqT&A? n{~>O_zhYBJf>$+@W%vMQ` diff --git a/Backend/src/routes/invoice_routes.py b/Backend/src/routes/invoice_routes.py index 2300a5e1..86876e47 100644 --- a/Backend/src/routes/invoice_routes.py +++ b/Backend/src/routes/invoice_routes.py @@ -166,4 +166,38 @@ async def get_invoices_by_booking(booking_id: int, current_user: User=Depends(ge except Exception as e: db.rollback() logger.error(f'Error fetching invoices by booking: {str(e)}', exc_info=True) + raise HTTPException(status_code=500, detail=str(e)) + +@router.post('/{id}/send-email') +async def send_invoice_email(request: Request, id: int, current_user: User=Depends(authorize_roles('admin', 'staff', 'accountant')), db: Session=Depends(get_db)): + try: + invoice = db.query(Invoice).filter(Invoice.id == id).first() + if not invoice: + raise HTTPException(status_code=404, detail='Invoice not found') + + from ..routes.booking_routes import _generate_invoice_email_html + from ..models.user import User as UserModel + from ..utils.mailer import send_email + + invoice_dict = InvoiceService.invoice_to_dict(invoice) + invoice_html = _generate_invoice_email_html(invoice_dict, is_proforma=invoice.is_proforma) + invoice_type = 'Proforma Invoice' if invoice.is_proforma else 'Invoice' + + user = db.query(UserModel).filter(UserModel.id == invoice.user_id).first() + if not user: + raise HTTPException(status_code=404, detail='User not found') + + await send_email( + to=user.email, + subject=f'{invoice_type} {invoice.invoice_number}', + html=invoice_html + ) + + logger.info(f'{invoice_type} {invoice.invoice_number} sent to {user.email}') + return success_response(message=f'{invoice_type} sent successfully to {user.email}') + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f'Error sending invoice email: {str(e)}', exc_info=True) raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/Backend/src/services/__pycache__/invoice_service.cpython-312.pyc b/Backend/src/services/__pycache__/invoice_service.cpython-312.pyc index 9dc912e08f8daca8b381301e0c835046561f9b28..07e1b940688f030addbcaf3a81da8a61b67b8864 100644 GIT binary patch delta 7753 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 delta 6265 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 z Dict[str, Any]: from sqlalchemy.orm import selectinload + from ..models.service_usage import ServiceUsage + from ..models.room import Room + from ..models.room_type import RoomType + from ..models.service import Service + logger.info(f'Creating invoice from booking {booking_id}', extra={'booking_id': booking_id, 'request_id': request_id}) - booking = db.query(Booking).options(selectinload(Booking.service_usages).selectinload('service'), selectinload(Booking.room).selectinload('room_type'), selectinload(Booking.payments)).filter(Booking.id == booking_id).first() + booking = db.query(Booking).options( + selectinload(Booking.service_usages).selectinload(ServiceUsage.service), + selectinload(Booking.room).selectinload(Room.room_type), + selectinload(Booking.payments) + ).filter(Booking.id == booking_id).first() if not booking: logger.error(f'Booking {booking_id} not found', extra={'booking_id': booking_id, 'request_id': request_id}) raise ValueError('Booking not found') user = db.query(User).filter(User.id == booking.user_id).first() if not user: raise ValueError('User not found') + + # Get tax_rate from system settings if not provided or is 0 + if tax_rate == 0.0: + tax_rate_setting = db.query(SystemSettings).filter(SystemSettings.key == 'tax_rate').first() + if tax_rate_setting and tax_rate_setting.value: + try: + tax_rate = float(tax_rate_setting.value) + except (ValueError, TypeError): + tax_rate = 0.0 + invoice_number = generate_invoice_number(db, is_proforma=is_proforma) booking_total = float(booking.total_price) if invoice_amount is not None: @@ -61,7 +81,34 @@ class InvoiceService: else: status = InvoiceStatus.draft paid_date = None - invoice = Invoice(invoice_number=invoice_number, booking_id=booking_id, user_id=booking.user_id, issue_date=datetime.utcnow(), due_date=datetime.utcnow() + timedelta(days=due_days), paid_date=paid_date, subtotal=subtotal, tax_rate=tax_rate, tax_amount=tax_amount, discount_amount=discount_amount, total_amount=total_amount, amount_paid=amount_paid, balance_due=balance_due, status=status, is_proforma=is_proforma, company_name=kwargs.get('company_name'), company_address=kwargs.get('company_address'), company_phone=kwargs.get('company_phone'), company_email=kwargs.get('company_email'), company_tax_id=kwargs.get('company_tax_id'), company_logo_url=kwargs.get('company_logo_url'), customer_name=user.full_name or f'{user.email}', customer_email=user.email, customer_address=user.address, customer_phone=user.phone, customer_tax_id=kwargs.get('customer_tax_id'), notes=kwargs.get('notes'), terms_and_conditions=kwargs.get('terms_and_conditions'), payment_instructions=kwargs.get('payment_instructions'), created_by_id=created_by_id) + # Get company information from system settings if not provided + company_name = kwargs.get('company_name') + company_address = kwargs.get('company_address') + company_phone = kwargs.get('company_phone') + company_email = kwargs.get('company_email') + company_tax_id = kwargs.get('company_tax_id') + company_logo_url = kwargs.get('company_logo_url') + + # If company info not provided, fetch from system settings + if not company_name or not company_address or not company_phone or not company_email: + company_settings = db.query(SystemSettings).filter( + SystemSettings.key.in_(['company_name', 'company_address', 'company_phone', 'company_email', 'company_logo_url']) + ).all() + + settings_dict = {setting.key: setting.value for setting in company_settings if setting.value} + + if not company_name and settings_dict.get('company_name'): + company_name = settings_dict['company_name'] + if not company_address and settings_dict.get('company_address'): + company_address = settings_dict['company_address'] + if not company_phone and settings_dict.get('company_phone'): + company_phone = settings_dict['company_phone'] + if not company_email and settings_dict.get('company_email'): + company_email = settings_dict['company_email'] + if not company_logo_url and settings_dict.get('company_logo_url'): + company_logo_url = settings_dict['company_logo_url'] + + invoice = Invoice(invoice_number=invoice_number, booking_id=booking_id, user_id=booking.user_id, issue_date=datetime.utcnow(), due_date=datetime.utcnow() + timedelta(days=due_days), paid_date=paid_date, subtotal=subtotal, tax_rate=tax_rate, tax_amount=tax_amount, discount_amount=discount_amount, total_amount=total_amount, amount_paid=amount_paid, balance_due=balance_due, status=status, is_proforma=is_proforma, company_name=company_name, company_address=company_address, company_phone=company_phone, company_email=company_email, company_tax_id=company_tax_id, company_logo_url=company_logo_url, customer_name=user.full_name or f'{user.email}', customer_email=user.email, customer_address=user.address, customer_phone=user.phone, customer_tax_id=kwargs.get('customer_tax_id'), notes=kwargs.get('notes'), terms_and_conditions=kwargs.get('terms_and_conditions'), payment_instructions=kwargs.get('payment_instructions'), created_by_id=created_by_id) db.add(invoice) db.flush() services_total = sum((float(su.total_price) for su in booking.service_usages)) @@ -110,9 +157,15 @@ class InvoiceService: if 'tax_rate' in kwargs or 'discount_amount' in kwargs: tax_rate = kwargs.get('tax_rate', invoice.tax_rate) discount_amount = kwargs.get('discount_amount', invoice.discount_amount) - invoice.tax_amount = (invoice.subtotal - discount_amount) * (float(tax_rate) / 100) - invoice.total_amount = invoice.subtotal + invoice.tax_amount - discount_amount - invoice.balance_due = invoice.total_amount - invoice.amount_paid + # Convert decimal types to float for arithmetic operations + subtotal = float(invoice.subtotal) if invoice.subtotal else 0.0 + discount_amount = float(discount_amount) if discount_amount else 0.0 + tax_rate = float(tax_rate) if tax_rate else 0.0 + amount_paid = float(invoice.amount_paid) if invoice.amount_paid else 0.0 + + invoice.tax_amount = (subtotal - discount_amount) * (tax_rate / 100) + invoice.total_amount = subtotal + invoice.tax_amount - discount_amount + invoice.balance_due = invoice.total_amount - amount_paid if invoice.balance_due <= 0 and invoice.status != InvoiceStatus.paid: invoice.status = InvoiceStatus.paid invoice.paid_date = datetime.utcnow() diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx index bb43b132..9575c996 100644 --- a/Frontend/src/App.tsx +++ b/Frontend/src/App.tsx @@ -55,6 +55,7 @@ const PayPalReturnPage = lazy(() => import('./pages/customer/PayPalReturnPage')) const PayPalCancelPage = lazy(() => import('./pages/customer/PayPalCancelPage')); const BoricaReturnPage = lazy(() => import('./pages/customer/BoricaReturnPage')); const InvoicePage = lazy(() => import('./pages/customer/InvoicePage')); +const InvoiceEditPage = lazy(() => import('./pages/admin/InvoiceEditPage')); const ProfilePage = lazy(() => import('./pages/customer/ProfilePage')); const LoyaltyPage = lazy(() => import('./pages/customer/LoyaltyPage')); const GroupBookingPage = lazy(() => import('./pages/customer/GroupBookingPage')); @@ -75,6 +76,7 @@ const PaymentManagementPage = lazy(() => import('./pages/admin/PaymentManagement const UserManagementPage = lazy(() => import('./pages/admin/UserManagementPage')); const GuestProfilePage = lazy(() => import('./pages/admin/GuestProfilePage')); const GroupBookingManagementPage = lazy(() => import('./pages/admin/GroupBookingManagementPage')); +const AdminBookingManagementPage = lazy(() => import('./pages/admin/BookingManagementPage')); const PageContentDashboardPage = lazy(() => import('./pages/admin/PageContentDashboard')); const AnalyticsDashboardPage = lazy(() => import('./pages/admin/AnalyticsDashboardPage')); const BusinessDashboardPage = lazy(() => import('./pages/admin/BusinessDashboardPage')); @@ -467,6 +469,10 @@ function App() { path="invoices" element={} /> + } + /> } @@ -487,6 +493,10 @@ function App() { path="group-bookings" element={} /> + } + /> } @@ -587,6 +597,14 @@ function App() { path="invoices" element={} /> + } + /> + } + /> } diff --git a/Frontend/src/components/layout/Header.tsx b/Frontend/src/components/layout/Header.tsx index 48f964d6..93f90c9c 100644 --- a/Frontend/src/components/layout/Header.tsx +++ b/Frontend/src/components/layout/Header.tsx @@ -4,8 +4,6 @@ import { Hotel, User, LogOut, - Menu, - X, LogIn, UserPlus, Heart, @@ -20,6 +18,7 @@ import { useCompanySettings } from '../../contexts/CompanySettingsContext'; import { useAuthModal } from '../../contexts/AuthModalContext'; import { normalizeImageUrl } from '../../utils/imageUtils'; import InAppNotificationBell from '../notifications/InAppNotificationBell'; +import Navbar from './Navbar'; interface HeaderProps { isAuthenticated?: boolean; @@ -76,6 +75,199 @@ const Header: React.FC = ({ setIsMobileMenuOpen(false); }; + // Mobile menu content with user authentication + const mobileMenuContent = ( + <> + {!isAuthenticated ? ( + <> + + + + ) : ( + <> +
+ Hello, {userInfo?.name} +
+ + setIsMobileMenuOpen(false) + } + className="flex items-center + space-x-2 px-4 py-3 text-white/90 + hover:bg-[#d4af37]/10 hover:text-[#d4af37] + rounded-sm transition-all duration-300 + border-l-2 border-transparent + hover:border-[#d4af37] font-light tracking-wide" + > + + Profile + + {userInfo?.role !== 'admin' && userInfo?.role !== 'staff' && userInfo?.role !== 'accountant' && ( + <> + + setIsMobileMenuOpen(false) + } + className="flex items-center + space-x-2 px-4 py-3 text-white/90 + hover:bg-[#d4af37]/10 hover:text-[#d4af37] + rounded-sm transition-all duration-300 + border-l-2 border-transparent + hover:border-[#d4af37] font-light tracking-wide" + > + + Favorites + + + setIsMobileMenuOpen(false) + } + className="flex items-center + space-x-2 px-4 py-3 text-white/90 + hover:bg-[#d4af37]/10 hover:text-[#d4af37] + rounded-sm transition-all duration-300 + border-l-2 border-transparent + hover:border-[#d4af37] font-light tracking-wide" + > + + My Bookings + + + setIsMobileMenuOpen(false) + } + className="flex items-center + space-x-2 px-4 py-3 text-white/90 + hover:bg-[#d4af37]/10 hover:text-[#d4af37] + rounded-sm transition-all duration-300 + border-l-2 border-transparent + hover:border-[#d4af37] font-light tracking-wide" + > + + Loyalty Program + + + setIsMobileMenuOpen(false) + } + className="flex items-center + space-x-2 px-4 py-3 text-white/90 + hover:bg-[#d4af37]/10 hover:text-[#d4af37] + rounded-sm transition-all duration-300 + border-l-2 border-transparent + hover:border-[#d4af37] font-light tracking-wide" + > + + Group Bookings + + + )} + {userInfo?.role === 'admin' && ( + + setIsMobileMenuOpen(false) + } + className="flex items-center + space-x-2 px-4 py-3 text-white/90 + hover:bg-[#d4af37]/10 hover:text-[#d4af37] + rounded-sm transition-all duration-300 + border-l-2 border-transparent + hover:border-[#d4af37] font-light tracking-wide" + > + + Admin + + )} + {userInfo?.role === 'staff' && ( + + setIsMobileMenuOpen(false) + } + className="flex items-center + space-x-2 px-4 py-3 text-white/90 + hover:bg-[#d4af37]/10 hover:text-[#d4af37] + rounded-sm transition-all duration-300 + border-l-2 border-transparent + hover:border-[#d4af37] font-light tracking-wide" + > + + Staff Dashboard + + )} + {userInfo?.role === 'accountant' && ( + + setIsMobileMenuOpen(false) + } + className="flex items-center + space-x-2 px-4 py-3 text-white/90 + hover:bg-[#d4af37]/10 hover:text-[#d4af37] + rounded-sm transition-all duration-300 + border-l-2 border-transparent + hover:border-[#d4af37] font-light tracking-wide" + > + + Accountant Dashboard + + )} +
+ + + )} + + ); + return (
@@ -128,58 +320,13 @@ const Header: React.FC = ({
- {} - + setIsMobileMenuOpen(false)} + mobileMenuContent={mobileMenuContent} + /> - {}
@@ -381,226 +528,7 @@ const Header: React.FC = ({
)} - - - - {isMobileMenuOpen && ( -
-
- setIsMobileMenuOpen(false)} - className="px-4 py-3 text-white/90 - hover:bg-[#d4af37]/10 hover:text-[#d4af37] - rounded-sm transition-all duration-300 - border-l-2 border-transparent - hover:border-[#d4af37] font-light tracking-wide" - > - Home - - setIsMobileMenuOpen(false)} - className="px-4 py-3 text-white/90 - hover:bg-[#d4af37]/10 hover:text-[#d4af37] - rounded-sm transition-all duration-300 - border-l-2 border-transparent - hover:border-[#d4af37] font-light tracking-wide" - > - Rooms - - setIsMobileMenuOpen(false)} - className="px-4 py-3 text-white/90 - hover:bg-[#d4af37]/10 hover:text-[#d4af37] - rounded-sm transition-all duration-300 - border-l-2 border-transparent - hover:border-[#d4af37] font-light tracking-wide" - > - About - - setIsMobileMenuOpen(false)} - className="px-4 py-3 text-white/90 - hover:bg-[#d4af37]/10 hover:text-[#d4af37] - rounded-sm transition-all duration-300 - border-l-2 border-transparent - hover:border-[#d4af37] font-light tracking-wide" - > - Contact - - setIsMobileMenuOpen(false)} - className="px-4 py-3 text-white/90 - hover:bg-[#d4af37]/10 hover:text-[#d4af37] - rounded-sm transition-all duration-300 - border-l-2 border-transparent - hover:border-[#d4af37] font-light tracking-wide" - > - Blog - - -
- {!isAuthenticated ? ( - <> - - - - ) : ( - <> -
- Hello, {userInfo?.name} -
- - setIsMobileMenuOpen(false) - } - className="flex items-center - space-x-2 px-4 py-3 text-white/90 - hover:bg-[#d4af37]/10 hover:text-[#d4af37] - rounded-sm transition-all duration-300 - border-l-2 border-transparent - hover:border-[#d4af37] font-light tracking-wide" - > - - Profile - - {userInfo?.role !== 'admin' && userInfo?.role !== 'staff' && ( - <> - - setIsMobileMenuOpen(false) - } - className="flex items-center - space-x-2 px-4 py-3 text-white/90 - hover:bg-[#d4af37]/10 hover:text-[#d4af37] - rounded-sm transition-all duration-300 - border-l-2 border-transparent - hover:border-[#d4af37] font-light tracking-wide" - > - - Favorites - - - setIsMobileMenuOpen(false) - } - className="flex items-center - space-x-2 px-4 py-3 text-white/90 - hover:bg-[#d4af37]/10 hover:text-[#d4af37] - rounded-sm transition-all duration-300 - border-l-2 border-transparent - hover:border-[#d4af37] font-light tracking-wide" - > - - My Bookings - - - )} - {userInfo?.role === 'admin' && ( - - setIsMobileMenuOpen(false) - } - className="flex items-center - space-x-2 px-4 py-3 text-white/90 - hover:bg-[#d4af37]/10 hover:text-[#d4af37] - rounded-sm transition-all duration-300 - border-l-2 border-transparent - hover:border-[#d4af37] font-light tracking-wide" - > - - Admin - - )} - {userInfo?.role === 'staff' && ( - - setIsMobileMenuOpen(false) - } - className="flex items-center - space-x-2 px-4 py-3 text-white/90 - hover:bg-[#d4af37]/10 hover:text-[#d4af37] - rounded-sm transition-all duration-300 - border-l-2 border-transparent - hover:border-[#d4af37] font-light tracking-wide" - > - - Staff Dashboard - - )} - - - )} -
-
-
- )}
diff --git a/Frontend/src/components/layout/Navbar.tsx b/Frontend/src/components/layout/Navbar.tsx new file mode 100644 index 00000000..c5626472 --- /dev/null +++ b/Frontend/src/components/layout/Navbar.tsx @@ -0,0 +1,135 @@ +import React, { useRef } from 'react'; +import { Link } from 'react-router-dom'; +import { Menu, X } from 'lucide-react'; +import { useClickOutside } from '../../hooks/useClickOutside'; + +interface NavbarProps { + isMobileMenuOpen: boolean; + onMobileMenuToggle: () => void; + onLinkClick?: () => void; + renderMobileLinksOnly?: boolean; + mobileMenuContent?: React.ReactNode; +} + +export const navLinks = [ + { to: '/', label: 'Home' }, + { to: '/rooms', label: 'Rooms' }, + { to: '/about', label: 'About' }, + { to: '/contact', label: 'Contact' }, + { to: '/blog', label: 'Blog' }, +]; + +const Navbar: React.FC = ({ + isMobileMenuOpen, + onMobileMenuToggle, + onLinkClick, + renderMobileLinksOnly = false, + mobileMenuContent +}) => { + const mobileMenuContainerRef = useRef(null); + + useClickOutside(mobileMenuContainerRef, () => { + if (isMobileMenuOpen) { + onMobileMenuToggle(); + } + }); + + const handleLinkClick = () => { + if (onLinkClick) { + onLinkClick(); + } + }; + + // If only rendering mobile links (for use inside Header's mobile menu container) + if (renderMobileLinksOnly) { + return ( + <> + {navLinks.map((link) => ( + + {link.label} + + ))} + + ); + } + + return ( +
+ {/* Desktop Navigation */} + + + {/* Mobile Menu Button */} + + + {/* Mobile Menu Dropdown - Absolute positioned */} + {isMobileMenuOpen && ( +
+
+ {navLinks.map((link) => ( + + {link.label} + + ))} + {mobileMenuContent && ( + <> +
+ {mobileMenuContent} + + )} +
+
+ )} +
+ ); +}; + +export default Navbar; + diff --git a/Frontend/src/components/layout/index.ts b/Frontend/src/components/layout/index.ts index 8af7503a..7af0b450 100644 --- a/Frontend/src/components/layout/index.ts +++ b/Frontend/src/components/layout/index.ts @@ -1,5 +1,6 @@ export { default as Header } from './Header'; export { default as Footer } from './Footer'; +export { default as Navbar } from './Navbar'; export { default as SidebarAdmin } from './SidebarAdmin'; export { default as SidebarStaff } from './SidebarStaff'; export { default as SidebarAccountant } from './SidebarAccountant'; diff --git a/Frontend/src/components/rooms/BannerCarousel.tsx b/Frontend/src/components/rooms/BannerCarousel.tsx index ec30ff48..caa3c278 100644 --- a/Frontend/src/components/rooms/BannerCarousel.tsx +++ b/Frontend/src/components/rooms/BannerCarousel.tsx @@ -261,9 +261,9 @@ const BannerCarousel: React.FC = ({ {} {children && ( -
+
-
+
{children}
@@ -277,7 +277,7 @@ const BannerCarousel: React.FC = ({ +
+
+
+
+
+

+ {editingBanner ? 'Edit Banner' : 'Create Banner'} +

+

+ {editingBanner ? 'Modify banner information' : 'Create a new promotional banner'} +

+
+ +
- +
+
-
{}
-