From 3d634b4fcee71a437d1f424d50b81f7e4d21a284 Mon Sep 17 00:00:00 2001 From: Iliyan Angelov Date: Thu, 4 Dec 2025 01:07:34 +0200 Subject: [PATCH] updates --- .../add_guest_requests_table.cpython-312.pyc | Bin 0 -> 5363 bytes ...nventory_management_tables.cpython-312.pyc | Bin 0 -> 14712 bytes ...otos_to_housekeeping_tasks.cpython-312.pyc | Bin 0 -> 1121 bytes ...inancial_audit_trail_table.cpython-312.pyc | Bin 0 -> 6619 bytes .../versions/add_guest_requests_table.py | 73 ++ .../add_inventory_management_tables.py | 171 +++ .../add_photos_to_housekeeping_tasks.py | 28 + .../versions/add_staff_shifts_tables.py | 97 ++ ...2351965_add_financial_audit_trail_table.py | 106 ++ Backend/src/__pycache__/main.cpython-312.pyc | Bin 27199 -> 27679 bytes .../__pycache__/user_routes.cpython-312.pyc | Bin 15165 -> 19501 bytes Backend/src/auth/routes/user_routes.py | 141 ++- .../booking_routes.cpython-312.pyc | Bin 110235 -> 113565 bytes .../__pycache__/upsell_routes.cpython-312.pyc | Bin 0 -> 4426 bytes Backend/src/bookings/routes/booking_routes.py | 77 +- Backend/src/bookings/routes/upsell_routes.py | 93 ++ .../complaint_routes.cpython-312.pyc | Bin 20264 -> 26891 bytes .../guest_profile_routes.cpython-312.pyc | Bin 28309 -> 31340 bytes .../routes/complaint_routes.py | 177 ++- .../routes/guest_profile_routes.py | 64 + Backend/src/hotel_services/models/__init__.py | 8 + .../__pycache__/__init__.cpython-312.pyc | Bin 174 -> 964 bytes .../__pycache__/guest_request.cpython-312.pyc | Bin 0 -> 3465 bytes .../housekeeping_task.cpython-312.pyc | Bin 3124 -> 3164 bytes .../inventory_item.cpython-312.pyc | Bin 0 -> 3427 bytes .../inventory_reorder_request.cpython-312.pyc | Bin 0 -> 2801 bytes ...inventory_task_consumption.cpython-312.pyc | Bin 0 -> 1564 bytes .../inventory_transaction.cpython-312.pyc | Bin 0 -> 2222 bytes .../__pycache__/staff_shift.cpython-312.pyc | Bin 0 -> 5604 bytes .../hotel_services/models/guest_request.py | 70 ++ .../models/housekeeping_task.py | 1 + .../hotel_services/models/inventory_item.py | 67 ++ .../models/inventory_reorder_request.py | 53 + .../models/inventory_task_consumption.py | 24 + .../models/inventory_transaction.py | 43 + .../src/hotel_services/models/staff_shift.py | 122 ++ Backend/src/hotel_services/routes/__init__.py | 2 + .../__pycache__/__init__.cpython-312.pyc | Bin 174 -> 281 bytes .../guest_request_routes.cpython-312.pyc | Bin 0 -> 21489 bytes .../inventory_routes.cpython-312.pyc | Bin 0 -> 28204 bytes .../staff_shift_routes.cpython-312.pyc | Bin 0 -> 26479 bytes .../routes/guest_request_routes.py | 401 +++++++ .../hotel_services/routes/inventory_routes.py | 565 +++++++++ .../routes/staff_shift_routes.py | 464 ++++++++ Backend/src/main.py | 7 +- Backend/src/models/__init__.py | 19 + .../__pycache__/__init__.cpython-312.pyc | Bin 6433 -> 7419 bytes .../audit_trail_routes.cpython-312.pyc | Bin 7390 -> 8943 bytes .../invoice_routes.cpython-312.pyc | Bin 14397 -> 16515 bytes .../payment_routes.cpython-312.pyc | Bin 76410 -> 77151 bytes .../src/payments/routes/audit_trail_routes.py | 70 +- Backend/src/payments/routes/invoice_routes.py | 54 +- Backend/src/payments/routes/payment_routes.py | 18 +- .../financial_audit_service.cpython-312.pyc | Bin 4656 -> 5578 bytes .../services/financial_audit_service.py | 19 +- .../favorite_routes.cpython-312.pyc | Bin 9876 -> 10585 bytes .../__pycache__/review_routes.cpython-312.pyc | Bin 15487 -> 21973 bytes Backend/src/reviews/routes/favorite_routes.py | 44 +- Backend/src/reviews/routes/review_routes.py | 157 ++- .../advanced_room_routes.cpython-312.pyc | Bin 49401 -> 62036 bytes .../__pycache__/room_routes.cpython-312.pyc | Bin 58065 -> 63373 bytes .../src/rooms/routes/advanced_room_routes.py | 365 +++++- Backend/src/rooms/routes/room_routes.py | 123 +- .../__pycache__/auth.cpython-312.pyc | Bin 7961 -> 8363 bytes Backend/src/security/middleware/auth.py | 23 +- .../__pycache__/role_helpers.cpython-312.pyc | Bin 2944 -> 3327 bytes Backend/src/shared/utils/role_helpers.py | 8 + .../system_settings_routes.cpython-312.pyc | Bin 70185 -> 79231 bytes .../system/routes/system_settings_routes.py | 210 ++++ Frontend/src/App.tsx | 31 + .../complaints/services/complaintService.ts | 80 ++ .../services/guestRequestService.ts | 82 ++ .../components/HousekeepingManagement.tsx | 2 +- .../components/MaintenanceManagement.tsx | 44 +- .../inventory/services/inventoryService.ts | 140 +++ .../rooms/services/advancedRoomService.ts | 31 + .../staffShifts/services/staffShiftService.ts | 150 +++ .../src/pages/accountant/DashboardPage.tsx | 2 +- .../accountant/InvoiceManagementPage.tsx | 2 +- .../src/pages/admin/BannerManagementPage.tsx | 3 +- .../pages/admin/PromotionManagementPage.tsx | 3 +- .../src/pages/customer/GuestRequestsPage.tsx | 457 +++++++ .../src/pages/housekeeping/DashboardPage.tsx | 1058 +++++++++++++++-- .../staff/AdvancedRoomManagementPage.tsx | 373 +++++- .../pages/staff/GuestCommunicationPage.tsx | 708 +++++++++++ .../staff/GuestRequestManagementPage.tsx | 759 ++++++++++++ .../staff/IncidentComplaintManagementPage.tsx | 800 +++++++++++++ .../pages/staff/ReceptionDashboardPage.tsx | 425 ++++++- .../src/pages/staff/UpsellManagementPage.tsx | 721 +++++++++++ Frontend/src/routes/staffRoutes.tsx | 8 + .../src/shared/components/SidebarStaff.tsx | 26 +- Frontend/src/shared/services/apiClient.ts | 30 +- 92 files changed, 9678 insertions(+), 221 deletions(-) create mode 100644 Backend/alembic/versions/__pycache__/add_guest_requests_table.cpython-312.pyc create mode 100644 Backend/alembic/versions/__pycache__/add_inventory_management_tables.cpython-312.pyc create mode 100644 Backend/alembic/versions/__pycache__/add_photos_to_housekeeping_tasks.cpython-312.pyc create mode 100644 Backend/alembic/versions/__pycache__/d032f2351965_add_financial_audit_trail_table.cpython-312.pyc create mode 100644 Backend/alembic/versions/add_guest_requests_table.py create mode 100644 Backend/alembic/versions/add_inventory_management_tables.py create mode 100644 Backend/alembic/versions/add_photos_to_housekeeping_tasks.py create mode 100644 Backend/alembic/versions/add_staff_shifts_tables.py create mode 100644 Backend/alembic/versions/d032f2351965_add_financial_audit_trail_table.py create mode 100644 Backend/src/bookings/routes/__pycache__/upsell_routes.cpython-312.pyc create mode 100644 Backend/src/bookings/routes/upsell_routes.py create mode 100644 Backend/src/hotel_services/models/__pycache__/guest_request.cpython-312.pyc create mode 100644 Backend/src/hotel_services/models/__pycache__/inventory_item.cpython-312.pyc create mode 100644 Backend/src/hotel_services/models/__pycache__/inventory_reorder_request.cpython-312.pyc create mode 100644 Backend/src/hotel_services/models/__pycache__/inventory_task_consumption.cpython-312.pyc create mode 100644 Backend/src/hotel_services/models/__pycache__/inventory_transaction.cpython-312.pyc create mode 100644 Backend/src/hotel_services/models/__pycache__/staff_shift.cpython-312.pyc create mode 100644 Backend/src/hotel_services/models/guest_request.py create mode 100644 Backend/src/hotel_services/models/inventory_item.py create mode 100644 Backend/src/hotel_services/models/inventory_reorder_request.py create mode 100644 Backend/src/hotel_services/models/inventory_task_consumption.py create mode 100644 Backend/src/hotel_services/models/inventory_transaction.py create mode 100644 Backend/src/hotel_services/models/staff_shift.py create mode 100644 Backend/src/hotel_services/routes/__pycache__/guest_request_routes.cpython-312.pyc create mode 100644 Backend/src/hotel_services/routes/__pycache__/inventory_routes.cpython-312.pyc create mode 100644 Backend/src/hotel_services/routes/__pycache__/staff_shift_routes.cpython-312.pyc create mode 100644 Backend/src/hotel_services/routes/guest_request_routes.py create mode 100644 Backend/src/hotel_services/routes/inventory_routes.py create mode 100644 Backend/src/hotel_services/routes/staff_shift_routes.py create mode 100644 Frontend/src/features/complaints/services/complaintService.ts create mode 100644 Frontend/src/features/guestRequests/services/guestRequestService.ts create mode 100644 Frontend/src/features/inventory/services/inventoryService.ts create mode 100644 Frontend/src/features/staffShifts/services/staffShiftService.ts create mode 100644 Frontend/src/pages/customer/GuestRequestsPage.tsx create mode 100644 Frontend/src/pages/staff/GuestCommunicationPage.tsx create mode 100644 Frontend/src/pages/staff/GuestRequestManagementPage.tsx create mode 100644 Frontend/src/pages/staff/IncidentComplaintManagementPage.tsx create mode 100644 Frontend/src/pages/staff/UpsellManagementPage.tsx diff --git a/Backend/alembic/versions/__pycache__/add_guest_requests_table.cpython-312.pyc b/Backend/alembic/versions/__pycache__/add_guest_requests_table.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..09d421143778da27722bd4468cf5e735ad8d39ee GIT binary patch literal 5363 zcmcIoTTC0-8TQy?dpx!Yc%5(ww-N|3;o`7iAx@g6Kti(FCFzE)QY+oj;4_#Z;~DSF zIEf>5#Y(NJJ~bGr98*|${Cmwjpf_0g|3BY3|HbG0XTA;wSqD7d{3R;31Railk&g8^&Bn%cXgqZwN8EuZ*mq;{v&fp7LtWlI72(qrISuVxPd{Rh3k0TNTs=(_) z;~*5V#_ni$Pe=4zN3^@~-1%5E3ct>1@nPBTh;|VQJC*b>RPIz(`&7cAy_4dqw@sEX z4OFZepmf2zam7chrBfDj!m^`9fErP{+v-kutk!mooHjnQO}AQ$W&^npz2sh`jjnCX zewZE`{~%0y$-782fyXvtKSajHFAbuvdu^8)yW>$&(D1d^f3>`z|r~-x1J4@k3 z@F01>wnw$Z@k3@gfhtiIszx<{Zh9Wk=)}^=Bl@oP5d1n+j~Y-T`q>Wr`Xl2v+3*8~ z_VyB;S~TPN2iT<9hFNAuGuqoRd}J%P*bonjH`kF7PnR6A)rMGpfYwKktuseNJi6AA zk|Va+5KH6hvn5Atw;`4$dOAvu*l9y7P4smAD8$g-2u7YHg`!Dn(LC2|`1?sSAbsx0 z(ms&xKC*Pn50XCpR_X9QS~{&AZ}FtTrR-<=iM=CiCw|plE_&C(BRLeTgkCyt?fpLz zxyOb~d{a8)_iV_-kEKKIwIR3eM$S4SeZ#nHXDWuBA{x0CapEAK(G^insKulYGjc|f zNcM=?QAN2g%1KT{5lZ!g53pBNloa%_H=_wEX#~ualhd*2Rrxw=4=gUdJqGr0Pc7CI=k9;ZIr7 ziM>EYEH$3fe)B5MMtiz$IiiE@U>RGHHhbNd|}7Mu?WzA<4Yjm-9AzZn5qr-&Xi z#0y4?IG?m|SbVhm=0So53?hqg6T!86`P)H-tM_uP_^RKBW!!CwtBeFyl2PM;v2f$M zgPj_WJp+oANy*rI1HvZ>aYA2}Gb!x3qpKty=n;rX%#iEM2-pUPuPLekj<_jg2f$(w zybyLcd|MS$yqeufwI&l~Bs{{@*ek~d*_XZ_=^9s3LRV6L)HNt*_jM)Rbsd}{b@Utm zpRRsBaUbHbi)2xFVM_t?O+Ux6X7H{$c5)$|FI&0dtTIRjUM*CmsK$J%7CQ z`K@)P>RX2Wv+qy7C(YB&8Fq?&9>@(Y_pCBEEPDG3=Sy~uoe$(PtIW?WYGnGOnfO%v z`MuoLRVH>Ywev;O%htKp^+3(DTMJzN^6m9N<+Hklve(}Aiuz42eTsdSAS2S+AU3Kgm56CV=HoY!i&n9C@w@r`EB&U+= zfr=-6&pw{LyI#=*(ha8c86tgtTe@|+d*;H_h1omLFYj1?$BWR*%DKw<>fDG;D?+WA ztC_ETP1`I$v}&7cn{R&|US%#@79iBKb7$u}Ubn0=S1c;Ap?}UlAKZgr$TqJszp!NA zzJ@(3%ZDOM-CxCC#TG9u1H_OCkq@;mjea%pYGP4Z20h-lkhfRiwyk&%_3uo>Uzvv4 z+S#&PUG7+c>4)(qt$NnKFtKp2z+5_*RyTVr*Osd-6fIzmtMG5KZx)4?wW^VMC2a5UfDzdW*hEq~|k8vUDZjhMCgy@haJKKA~~ zy_Mm7VtkDjck#~`!ae!kp_RzW$^36e*XYD9{)Iv~mcR7D%4hlEPx49WZ*)o>fME7_wQkbHQD)eB%pm0S|waA1ocr#Nz8t9^vtONt)Cq>XKzMXHkKiu4i#&{_)E~K*RMWV$!+k{Ref^0(9R1%ArQO$37I9N=Uw!5P$ z4@LjC u%t2ND!%_K9M+a#PJkeg6-v)-=r+@;x%Bd=+ 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 literal 0 HcmV?d00001 diff --git a/Backend/alembic/versions/__pycache__/add_photos_to_housekeeping_tasks.cpython-312.pyc b/Backend/alembic/versions/__pycache__/add_photos_to_housekeeping_tasks.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..029b575efbb5b92efdd4f7ab38de021972680ba3 GIT binary patch literal 1121 zcmah|O>fgc5cO9a$4#JLsBnQ?3eBOlo2r$F5Qnx>TcidA1tbf}a=lBF#`c=EQwXP2 zLggp)2XLWk1zZtG`~WUJP^ntP2`=1<0vArqI*FQAAV%JKJG*1g&U@>RxtuP5y?A9- zwT+p9xISAQdg~TMXV`5iGJ;F7N!)mZ*$4*p>bp1Bos76&1@ENzF-~>Eb+tP?MkMv@ z^Dm#W-A;_;J!O|y1>5b^5ru|ONR`=OFHLPqnc60N1v=G&y5~8o9xkLP{{qu_&o1%m ze!!&2Vd`9auH3kDv%Dj;c#Yo=j0*9wu@*cr=5bVuNoXvR7<=Qhgw#MfW7ctNI6#Ku zVSlyi8XK5$2_vJ2(Z9+)5jI%59a zdq4YO;qAh^rTtqgpT_PUDEDYTWS$U2frUg66DI!`1W6|dl*^R!PBY*NQk$VZL`m48 zh@R!UyeJ(2$1;dK4%&x52k`|M5#MMZzPwr9AG-4L{DD$T-YG~-r8utBpj^nYv~45e z+BQpfSnCJ^H_`Fj3icZ`%WDiBFHE2+_tpR zI0SVfn*<5Rl%MrfE5EgCK0$R4U#FK~m*3-P2HcS>isBDR6_sCS1aaiEF!DthJ<^2i fXj41vzu3$j_78*04K($ZDv9^SpSmPYCGGwJy!#h! literal 0 HcmV?d00001 diff --git a/Backend/alembic/versions/__pycache__/d032f2351965_add_financial_audit_trail_table.cpython-312.pyc b/Backend/alembic/versions/__pycache__/d032f2351965_add_financial_audit_trail_table.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f36e5455aa47bb15d2dbca722101822a906ed166 GIT binary patch literal 6619 zcmcIpT}&I<6`rvNdoW`lBmu(iX1zeN>-+%WFZ|?Z7YJEMASA#>>-@|PzGE1$XS_4x zBxXrVrAlqI4@uQZX_QC_N+cA46drlZW2C;^Eq0eqQ>jw74|z)!`m!(Wx%P}b0h53V zBhURg_nhyZbMDWad;KS`*F(W0&DKdRyD93Qq+&e|v+(9iDBPtG6`&A}9AP@*2smg` zc7~k+2685x7jXq#K+6lbkt@I=XCNP556VaGUo(LM$XUp_xYO^MoD>k^Ly|1WK}iVn zLJUbdud9L-=5=8tEP6Z_#A}i!DYCDptIdb%8tOy!4NXT{o14-tqSoeX4jm1(G`F;d zf~_r{PE{0i(bok@o3FmEzUlCh`onb%zWRo?x;psP)TJJe=M^!@Pa6&;+7DS?B(8lM zHaL4124{y3nm$Nn6_7gNee>J=ltYL!s0e+NqA5MkVjOo_rEETD7H`rHYKoeo=jb%y zren%64p^(748IdOr<~X5JB<3ht%uZ*y)A>By4RwN7h0ukju=~eah7hCtR7Jwa?QKv zm^9rclM9ozwU<~(*&GJlg9;Fbyr>Wrp<=WHm7twy*Zl4|F3kmfmCg~5!ehhA)mNFV zeOFq-o@VslIAXaC@tq#ou#S7S9I;}{5kIgYmfAg-E~5|8NAr8<%%zX=Hf;OH+l1@0 z;g;vn_mlZbYc;{#t(vdi;N0D3!+qE2Z+t%Ow;|@z^i#X08}{pGHdM0zta>u@e&ZwM zw;{f3C2bt>fDQ4(RSH5iqhoGg?p-T^7;8*z$i5tW<)bt@2=5`-Q`YVyxg_=g_hEQz zHY`yA+_mu5ZCGL-a36uUJ}WV2dit#|MdezHY`c8*+E&zH`2{%<13kFr8?E{NeJWDFdORu`MNuJ6+IirEz!DidG6W?O4U2?SY>`wk6q6B9 z3N1D}xkcKU@j%)nVi9SKm;c(BXwNb6LniRv{`<&hW?Vqx9jqM{m7)QE`qk+@M}ukm68T>R?4 z#FWDHAtEB`0ytGL@?=F9HN#^L9WUsU1*TUG)dGq-A8F5Bp?&tx0+^_gt* zVc+`Q*#~cZtn4U(AJAGySPQTsS7X|6qdZre>DnBSruzP!h6hf7LO3`kM&c?9b4ew$ z(_lwMoga~8Siq!|I+yZf@n{B@Rwqp}9GYObI+buNA{*`=Sru-bbN7yAS=u zdTtlpDq7~MUUG%Edv5jIK@U0}_TKBge|C|19!qe2D=x}glxm4QxiEWi=HhbkC%CE^ zxAZO-`*39gZt7kx-iv+pxUp-wxDr<%UEY0QrNFhn@HI#6*}LL(c|Tq$G%;}Lf%%SS zr=Oml>v^7+;JUNm;U^uly)(U!&n`A5xX&$RHbTu}j6HG8dS*Ne7Z$%w_`Z12mEg{s zK)CeN`I2W9Pb(HjUeF1DFCP9n!3C^d#-CKq`e*zLM-r7y&lx-%NN|_3sPdDM+3}h2 z$Kl1{1lO4XB!=`n?O7a597=bUk-(+<=N-@7Pu+9*i=e|7RyU&$)Q8vaUB4e+025m> z?01NfXJ*bk?pr*S;5t(DAGxX@xT;58OPt?czx=3w;mX1}+#5=8qX28NZH{~uS2r$9 zE`)K0lx zCbtck-uGhWxZp(&ONu5(lEQxVBupliTi=E;T+>Ip(WPLg9JX(ve=Nm@zLLXx8- zX(mY{B=CtROVRKkRf+Pc55GE)C8VIAQtDTPe~8fYk#b#5e}OMUJpiu;H`MS2{)ut@ zrtepMxTF>z8%#1oFRl6^+&YkC&S&a1xcx$s8O+p8epCJrm3OOuSB=Y#EtMR{y+V>1 zF&V9f6H6s0@!4RKL8}_pmDqg4@uiXz_{>O>39jm+ajB#UcYK{>0T+1spClmFyMyx$qd=g>@hgq#lqqVRfGnz ze`;hVymrzw{cnehX8u!1(Pe+5%KlF6d(Bc@#WV}C&9ry9co!tz9n+pSu1=bsqF2s3 Kis^$X None: + # Create guest_requests table + op.create_table( + 'guest_requests', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('booking_id', sa.Integer(), nullable=False), + sa.Column('room_id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('request_type', sa.Enum( + 'extra_towels', 'extra_pillows', 'room_cleaning', 'turndown_service', + 'amenities', 'maintenance', 'room_service', 'other', + name='requesttype' + ), nullable=False), + sa.Column('status', sa.Enum( + 'pending', 'in_progress', 'fulfilled', 'cancelled', + name='requeststatus' + ), nullable=False, server_default='pending'), + sa.Column('priority', sa.Enum( + 'low', 'normal', 'high', 'urgent', + name='requestpriority' + ), nullable=False, server_default='normal'), + sa.Column('title', sa.String(255), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('assigned_to', sa.Integer(), nullable=True), + sa.Column('fulfilled_by', sa.Integer(), nullable=True), + sa.Column('requested_at', sa.DateTime(), nullable=False), + sa.Column('started_at', sa.DateTime(), nullable=True), + sa.Column('fulfilled_at', sa.DateTime(), nullable=True), + sa.Column('guest_notes', sa.Text(), nullable=True), + sa.Column('staff_notes', sa.Text(), nullable=True), + sa.Column('response_time_minutes', sa.Integer(), nullable=True), + sa.Column('fulfillment_time_minutes', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['booking_id'], ['bookings.id'], ), + sa.ForeignKeyConstraint(['room_id'], ['rooms.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.ForeignKeyConstraint(['assigned_to'], ['users.id'], ), + sa.ForeignKeyConstraint(['fulfilled_by'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_guest_requests_id'), 'guest_requests', ['id'], unique=False) + op.create_index(op.f('ix_guest_requests_booking_id'), 'guest_requests', ['booking_id'], unique=False) + op.create_index(op.f('ix_guest_requests_room_id'), 'guest_requests', ['room_id'], unique=False) + op.create_index(op.f('ix_guest_requests_requested_at'), 'guest_requests', ['requested_at'], unique=False) + + +def downgrade() -> None: + op.drop_index(op.f('ix_guest_requests_requested_at'), table_name='guest_requests') + op.drop_index(op.f('ix_guest_requests_room_id'), table_name='guest_requests') + op.drop_index(op.f('ix_guest_requests_booking_id'), table_name='guest_requests') + op.drop_index(op.f('ix_guest_requests_id'), table_name='guest_requests') + op.drop_table('guest_requests') + diff --git a/Backend/alembic/versions/add_inventory_management_tables.py b/Backend/alembic/versions/add_inventory_management_tables.py new file mode 100644 index 00000000..b7a30e02 --- /dev/null +++ b/Backend/alembic/versions/add_inventory_management_tables.py @@ -0,0 +1,171 @@ +"""add_inventory_management_tables + +Revision ID: inventory_management_001 +Revises: add_photos_to_housekeeping_tasks +Create Date: 2025-01-02 12:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + + +# revision identifiers, used by Alembic. +revision = 'inventory_management_001' +down_revision = 'add_photos_housekeeping' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Create inventory_items table + op.create_table( + 'inventory_items', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(255), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('category', sa.Enum( + 'cleaning_supplies', 'linens', 'toiletries', 'amenities', + 'maintenance', 'food_beverage', 'other', + name='inventorycategory' + ), nullable=False), + sa.Column('unit', sa.Enum( + 'piece', 'box', 'bottle', 'roll', 'pack', 'liter', + 'kilogram', 'meter', 'other', + name='inventoryunit' + ), nullable=False), + sa.Column('current_quantity', sa.Numeric(precision=10, scale=2), nullable=False, server_default='0'), + sa.Column('minimum_quantity', sa.Numeric(precision=10, scale=2), nullable=False, server_default='0'), + sa.Column('maximum_quantity', sa.Numeric(precision=10, scale=2), nullable=True), + sa.Column('reorder_quantity', sa.Numeric(precision=10, scale=2), nullable=True), + sa.Column('unit_cost', sa.Numeric(precision=10, scale=2), nullable=True), + sa.Column('supplier', sa.String(255), nullable=True), + sa.Column('supplier_contact', sa.Text(), nullable=True), + sa.Column('storage_location', sa.String(255), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False, server_default='1'), + sa.Column('is_tracked', sa.Boolean(), nullable=False, server_default='1'), + sa.Column('barcode', sa.String(100), nullable=True), + sa.Column('sku', sa.String(100), nullable=True), + sa.Column('notes', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['created_by'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_inventory_items_id'), 'inventory_items', ['id'], unique=False) + op.create_index(op.f('ix_inventory_items_name'), 'inventory_items', ['name'], unique=False) + op.create_index(op.f('ix_inventory_items_barcode'), 'inventory_items', ['barcode'], unique=True) + op.create_index(op.f('ix_inventory_items_sku'), 'inventory_items', ['sku'], unique=True) + + # Create inventory_transactions table + op.create_table( + 'inventory_transactions', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('item_id', sa.Integer(), nullable=False), + sa.Column('transaction_type', sa.Enum( + 'consumption', 'adjustment', 'received', 'transfer', + 'damaged', 'returned', + name='transactiontype' + ), nullable=False), + sa.Column('quantity', sa.Numeric(precision=10, scale=2), nullable=False), + sa.Column('quantity_before', sa.Numeric(precision=10, scale=2), nullable=False), + sa.Column('quantity_after', sa.Numeric(precision=10, scale=2), nullable=False), + sa.Column('reference_type', sa.String(50), nullable=True), + sa.Column('reference_id', sa.Integer(), nullable=True), + sa.Column('notes', sa.Text(), nullable=True), + sa.Column('cost', sa.Numeric(precision=10, scale=2), nullable=True), + sa.Column('performed_by', sa.Integer(), nullable=True), + sa.Column('transaction_date', sa.DateTime(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['item_id'], ['inventory_items.id'], ), + sa.ForeignKeyConstraint(['performed_by'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_inventory_transactions_id'), 'inventory_transactions', ['id'], unique=False) + op.create_index(op.f('ix_inventory_transactions_item_id'), 'inventory_transactions', ['item_id'], unique=False) + op.create_index(op.f('ix_inventory_transactions_reference_id'), 'inventory_transactions', ['reference_id'], unique=False) + op.create_index(op.f('ix_inventory_transactions_transaction_date'), 'inventory_transactions', ['transaction_date'], unique=False) + + # Create inventory_reorder_requests table + op.create_table( + 'inventory_reorder_requests', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('item_id', sa.Integer(), nullable=False), + sa.Column('requested_quantity', sa.Numeric(precision=10, scale=2), nullable=False), + sa.Column('current_quantity', sa.Numeric(precision=10, scale=2), nullable=False), + sa.Column('minimum_quantity', sa.Numeric(precision=10, scale=2), nullable=False), + sa.Column('status', sa.Enum( + 'pending', 'approved', 'ordered', 'received', 'cancelled', + name='reorderstatus' + ), nullable=False, server_default='pending'), + sa.Column('priority', sa.String(20), nullable=False, server_default='normal'), + sa.Column('requested_by', sa.Integer(), nullable=False), + sa.Column('requested_at', sa.DateTime(), nullable=False), + sa.Column('notes', sa.Text(), nullable=True), + sa.Column('approved_by', sa.Integer(), nullable=True), + sa.Column('approved_at', sa.DateTime(), nullable=True), + sa.Column('approval_notes', sa.Text(), nullable=True), + sa.Column('order_number', sa.String(100), nullable=True), + sa.Column('expected_delivery_date', sa.DateTime(), nullable=True), + sa.Column('received_quantity', sa.Numeric(precision=10, scale=2), nullable=True), + sa.Column('received_at', sa.DateTime(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['item_id'], ['inventory_items.id'], ), + sa.ForeignKeyConstraint(['requested_by'], ['users.id'], ), + sa.ForeignKeyConstraint(['approved_by'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_inventory_reorder_requests_id'), 'inventory_reorder_requests', ['id'], unique=False) + op.create_index(op.f('ix_inventory_reorder_requests_item_id'), 'inventory_reorder_requests', ['item_id'], unique=False) + op.create_index(op.f('ix_inventory_reorder_requests_order_number'), 'inventory_reorder_requests', ['order_number'], unique=False) + op.create_index(op.f('ix_inventory_reorder_requests_requested_at'), 'inventory_reorder_requests', ['requested_at'], unique=False) + + # Create inventory_task_consumptions table + op.create_table( + 'inventory_task_consumptions', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('task_id', sa.Integer(), nullable=False), + sa.Column('item_id', sa.Integer(), nullable=False), + sa.Column('quantity', sa.Numeric(precision=10, scale=2), nullable=False), + sa.Column('notes', sa.Text(), nullable=True), + sa.Column('recorded_by', sa.Integer(), nullable=True), + sa.Column('recorded_at', sa.DateTime(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['task_id'], ['housekeeping_tasks.id'], ), + sa.ForeignKeyConstraint(['item_id'], ['inventory_items.id'], ), + sa.ForeignKeyConstraint(['recorded_by'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_inventory_task_consumptions_id'), 'inventory_task_consumptions', ['id'], unique=False) + op.create_index(op.f('ix_inventory_task_consumptions_task_id'), 'inventory_task_consumptions', ['task_id'], unique=False) + op.create_index(op.f('ix_inventory_task_consumptions_item_id'), 'inventory_task_consumptions', ['item_id'], unique=False) + op.create_index(op.f('ix_inventory_task_consumptions_recorded_at'), 'inventory_task_consumptions', ['recorded_at'], unique=False) + + +def downgrade() -> None: + op.drop_index(op.f('ix_inventory_task_consumptions_recorded_at'), table_name='inventory_task_consumptions') + op.drop_index(op.f('ix_inventory_task_consumptions_item_id'), table_name='inventory_task_consumptions') + op.drop_index(op.f('ix_inventory_task_consumptions_task_id'), table_name='inventory_task_consumptions') + op.drop_index(op.f('ix_inventory_task_consumptions_id'), table_name='inventory_task_consumptions') + op.drop_table('inventory_task_consumptions') + + op.drop_index(op.f('ix_inventory_reorder_requests_requested_at'), table_name='inventory_reorder_requests') + op.drop_index(op.f('ix_inventory_reorder_requests_order_number'), table_name='inventory_reorder_requests') + op.drop_index(op.f('ix_inventory_reorder_requests_item_id'), table_name='inventory_reorder_requests') + op.drop_index(op.f('ix_inventory_reorder_requests_id'), table_name='inventory_reorder_requests') + op.drop_table('inventory_reorder_requests') + + op.drop_index(op.f('ix_inventory_transactions_transaction_date'), table_name='inventory_transactions') + op.drop_index(op.f('ix_inventory_transactions_reference_id'), table_name='inventory_transactions') + op.drop_index(op.f('ix_inventory_transactions_item_id'), table_name='inventory_transactions') + op.drop_index(op.f('ix_inventory_transactions_id'), table_name='inventory_transactions') + op.drop_table('inventory_transactions') + + op.drop_index(op.f('ix_inventory_items_sku'), table_name='inventory_items') + op.drop_index(op.f('ix_inventory_items_barcode'), table_name='inventory_items') + op.drop_index(op.f('ix_inventory_items_name'), table_name='inventory_items') + op.drop_index(op.f('ix_inventory_items_id'), table_name='inventory_items') + op.drop_table('inventory_items') + diff --git a/Backend/alembic/versions/add_photos_to_housekeeping_tasks.py b/Backend/alembic/versions/add_photos_to_housekeeping_tasks.py new file mode 100644 index 00000000..7a909ce1 --- /dev/null +++ b/Backend/alembic/versions/add_photos_to_housekeeping_tasks.py @@ -0,0 +1,28 @@ +"""add_photos_to_housekeeping_tasks + +Revision ID: add_photos_housekeeping +Revises: d032f2351965 +Create Date: 2025-01-02 12:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + + +# revision identifiers, used by Alembic. +revision = 'add_photos_housekeeping' +down_revision = 'd032f2351965' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Add photos column to housekeeping_tasks table + op.add_column('housekeeping_tasks', sa.Column('photos', sa.JSON(), nullable=True)) + + +def downgrade() -> None: + # Remove photos column + op.drop_column('housekeeping_tasks', 'photos') + diff --git a/Backend/alembic/versions/add_staff_shifts_tables.py b/Backend/alembic/versions/add_staff_shifts_tables.py new file mode 100644 index 00000000..f77eaea4 --- /dev/null +++ b/Backend/alembic/versions/add_staff_shifts_tables.py @@ -0,0 +1,97 @@ +"""add_staff_shifts_tables + +Revision ID: staff_shifts_001 +Revises: guest_requests_001 +Create Date: 2025-01-02 16:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = 'staff_shifts_001' +down_revision = 'guest_requests_001' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Create staff_shifts table + op.create_table( + 'staff_shifts', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('staff_id', sa.Integer(), nullable=False), + sa.Column('shift_date', sa.DateTime(), nullable=False), + sa.Column('shift_type', sa.Enum('morning', 'afternoon', 'night', 'full_day', 'custom', name='shifttype'), nullable=False), + sa.Column('start_time', sa.Time(), nullable=False), + sa.Column('end_time', sa.Time(), nullable=False), + sa.Column('status', sa.Enum('scheduled', 'in_progress', 'completed', 'cancelled', 'no_show', name='shiftstatus'), nullable=False, server_default='scheduled'), + sa.Column('actual_start_time', sa.DateTime(), nullable=True), + sa.Column('actual_end_time', sa.DateTime(), nullable=True), + sa.Column('break_duration_minutes', sa.Integer(), nullable=True, server_default='30'), + sa.Column('assigned_by', sa.Integer(), nullable=True), + sa.Column('department', sa.String(length=100), nullable=True), + sa.Column('notes', sa.Text(), nullable=True), + sa.Column('handover_notes', sa.Text(), nullable=True), + sa.Column('tasks_completed', sa.Integer(), nullable=True, server_default='0'), + sa.Column('tasks_assigned', sa.Integer(), nullable=True, server_default='0'), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['assigned_by'], ['users.id'], ), + sa.ForeignKeyConstraint(['staff_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_staff_shifts_staff_id'), 'staff_shifts', ['staff_id'], unique=False) + op.create_index(op.f('ix_staff_shifts_shift_date'), 'staff_shifts', ['shift_date'], unique=False) + + # Create staff_tasks table + op.create_table( + 'staff_tasks', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('shift_id', sa.Integer(), nullable=True), + sa.Column('staff_id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=255), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('task_type', sa.String(length=100), nullable=False), + sa.Column('priority', sa.Enum('low', 'normal', 'high', 'urgent', name='stafftaskpriority'), nullable=False, server_default='normal'), + sa.Column('status', sa.Enum('pending', 'assigned', 'in_progress', 'completed', 'cancelled', 'on_hold', name='stafftaskstatus'), nullable=False, server_default='pending'), + sa.Column('scheduled_start', sa.DateTime(), nullable=True), + sa.Column('scheduled_end', sa.DateTime(), nullable=True), + sa.Column('actual_start', sa.DateTime(), nullable=True), + sa.Column('actual_end', sa.DateTime(), nullable=True), + sa.Column('estimated_duration_minutes', sa.Integer(), nullable=True), + sa.Column('actual_duration_minutes', sa.Integer(), nullable=True), + sa.Column('assigned_by', sa.Integer(), nullable=True), + sa.Column('due_date', sa.DateTime(), nullable=True), + sa.Column('related_booking_id', sa.Integer(), nullable=True), + sa.Column('related_room_id', sa.Integer(), nullable=True), + sa.Column('related_guest_request_id', sa.Integer(), nullable=True), + sa.Column('related_maintenance_id', sa.Integer(), nullable=True), + sa.Column('notes', sa.Text(), nullable=True), + sa.Column('completion_notes', sa.Text(), nullable=True), + sa.Column('is_recurring', sa.Boolean(), nullable=False, server_default='0'), + sa.Column('recurrence_pattern', sa.String(length=100), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['assigned_by'], ['users.id'], ), + sa.ForeignKeyConstraint(['related_booking_id'], ['bookings.id'], ), + sa.ForeignKeyConstraint(['related_guest_request_id'], ['guest_requests.id'], ), + sa.ForeignKeyConstraint(['related_maintenance_id'], ['room_maintenance.id'], ), + sa.ForeignKeyConstraint(['related_room_id'], ['rooms.id'], ), + sa.ForeignKeyConstraint(['shift_id'], ['staff_shifts.id'], ), + sa.ForeignKeyConstraint(['staff_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_staff_tasks_shift_id'), 'staff_tasks', ['shift_id'], unique=False) + op.create_index(op.f('ix_staff_tasks_staff_id'), 'staff_tasks', ['staff_id'], unique=False) + + +def downgrade() -> None: + op.drop_index(op.f('ix_staff_tasks_staff_id'), table_name='staff_tasks') + op.drop_index(op.f('ix_staff_tasks_shift_id'), table_name='staff_tasks') + op.drop_table('staff_tasks') + op.drop_index(op.f('ix_staff_shifts_shift_date'), table_name='staff_shifts') + op.drop_index(op.f('ix_staff_shifts_staff_id'), table_name='staff_shifts') + op.drop_table('staff_shifts') + diff --git a/Backend/alembic/versions/d032f2351965_add_financial_audit_trail_table.py b/Backend/alembic/versions/d032f2351965_add_financial_audit_trail_table.py new file mode 100644 index 00000000..91fdef05 --- /dev/null +++ b/Backend/alembic/versions/d032f2351965_add_financial_audit_trail_table.py @@ -0,0 +1,106 @@ +"""add_financial_audit_trail_table + +Revision ID: d032f2351965 +Revises: 6f7f8689fc98 +Create Date: 2025-12-03 23:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + + +# revision identifiers, used by Alembic. +revision = 'd032f2351965' +down_revision = '6f7f8689fc98' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Check if table already exists + from sqlalchemy import inspect + bind = op.get_bind() + inspector = inspect(bind) + tables = inspector.get_table_names() + + if 'financial_audit_trail' not in tables: + # Create financial_audit_trail table + op.create_table( + 'financial_audit_trail', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + + # Action details + sa.Column('action_type', sa.Enum( + 'payment_created', 'payment_completed', 'payment_refunded', + 'payment_failed', 'invoice_created', 'invoice_updated', + 'invoice_paid', 'refund_processed', 'price_modified', + 'discount_applied', 'promotion_applied', + name='financialactiontype' + ), nullable=False), + sa.Column('action_description', sa.Text(), nullable=False), + + # Related entities + sa.Column('payment_id', sa.Integer(), nullable=True), + sa.Column('invoice_id', sa.Integer(), nullable=True), + sa.Column('booking_id', sa.Integer(), nullable=True), + + # Financial details + sa.Column('amount', sa.Numeric(precision=10, scale=2), nullable=True), + sa.Column('previous_amount', sa.Numeric(precision=10, scale=2), nullable=True), + sa.Column('currency', sa.String(length=3), nullable=True, server_default='USD'), + + # User information + sa.Column('performed_by', sa.Integer(), nullable=False), + sa.Column('performed_by_email', sa.String(length=255), nullable=True), + + # Additional context + sa.Column('audit_metadata', sa.JSON(), nullable=True), + sa.Column('notes', sa.Text(), nullable=True), + + # Timestamp + sa.Column('created_at', sa.DateTime(), nullable=False), + + # Primary key + sa.PrimaryKeyConstraint('id'), + + # Foreign keys + sa.ForeignKeyConstraint(['payment_id'], ['payments.id'], name='fk_financial_audit_payment'), + sa.ForeignKeyConstraint(['invoice_id'], ['invoices.id'], name='fk_financial_audit_invoice'), + sa.ForeignKeyConstraint(['booking_id'], ['bookings.id'], name='fk_financial_audit_booking'), + sa.ForeignKeyConstraint(['performed_by'], ['users.id'], name='fk_financial_audit_user') + ) + + # Create indexes + op.create_index(op.f('ix_financial_audit_trail_id'), 'financial_audit_trail', ['id'], unique=False) + op.create_index(op.f('ix_financial_audit_trail_action_type'), 'financial_audit_trail', ['action_type'], unique=False) + op.create_index(op.f('ix_financial_audit_trail_payment_id'), 'financial_audit_trail', ['payment_id'], unique=False) + op.create_index(op.f('ix_financial_audit_trail_invoice_id'), 'financial_audit_trail', ['invoice_id'], unique=False) + op.create_index(op.f('ix_financial_audit_trail_booking_id'), 'financial_audit_trail', ['booking_id'], unique=False) + op.create_index(op.f('ix_financial_audit_trail_performed_by'), 'financial_audit_trail', ['performed_by'], unique=False) + op.create_index(op.f('ix_financial_audit_trail_created_at'), 'financial_audit_trail', ['created_at'], unique=False) + + # Create composite indexes + op.create_index('idx_financial_audit_created', 'financial_audit_trail', ['created_at'], unique=False) + op.create_index('idx_financial_audit_action', 'financial_audit_trail', ['action_type', 'created_at'], unique=False) + op.create_index('idx_financial_audit_user', 'financial_audit_trail', ['performed_by', 'created_at'], unique=False) + op.create_index('idx_financial_audit_booking', 'financial_audit_trail', ['booking_id', 'created_at'], unique=False) + + +def downgrade() -> None: + # Drop indexes first + op.drop_index('idx_financial_audit_booking', table_name='financial_audit_trail') + op.drop_index('idx_financial_audit_user', table_name='financial_audit_trail') + op.drop_index('idx_financial_audit_action', table_name='financial_audit_trail') + op.drop_index('idx_financial_audit_created', table_name='financial_audit_trail') + + op.drop_index(op.f('ix_financial_audit_trail_created_at'), table_name='financial_audit_trail') + op.drop_index(op.f('ix_financial_audit_trail_performed_by'), table_name='financial_audit_trail') + op.drop_index(op.f('ix_financial_audit_trail_booking_id'), table_name='financial_audit_trail') + op.drop_index(op.f('ix_financial_audit_trail_invoice_id'), table_name='financial_audit_trail') + op.drop_index(op.f('ix_financial_audit_trail_payment_id'), table_name='financial_audit_trail') + op.drop_index(op.f('ix_financial_audit_trail_action_type'), table_name='financial_audit_trail') + op.drop_index(op.f('ix_financial_audit_trail_id'), table_name='financial_audit_trail') + + # Drop table + op.drop_table('financial_audit_trail') diff --git a/Backend/src/__pycache__/main.cpython-312.pyc b/Backend/src/__pycache__/main.cpython-312.pyc index c4431ea3184dd24786a25a95d9ad9cb1ade5b2b7..fabf3a55770cd45498ad8d89ee8c0b2c5440275e 100644 GIT binary patch delta 5011 zcmbuCdrTBZ9LIMK4~hz?JR_d?Xe*|P64GjG(rRBM4K|dNR2fL{J0|iXI9IN<|P6c~lYbgi7B@ZAnbDkF=W3>;c{#d&kn-KYsJu-)}zi z{mo-HyG^~IWB~Yg`1wue=%sCr(pM+%@()wZi8vYAI@7!pa5{foI=>6&!+cC2!MovZ zya(=);+y!rxBwR5eQ+P%5BK8(@Blst4@x;bzaAIDLR*@iBNz8e_AW{}NZhDqIb#r9cM%6+RA+;}h@% zu7NdDjOS0{Q}C4Jr|}m21+0~P%%8?*;2B&8>u^1+mtqF~EItR%;Re`%8)2gqGxF#0 z1$Y5B!6xZGlm8lDgcor$Y{uqG@Dgr;Ew~l7O35w!W!whaBwygK;C9$9`Dy%BD%>IY zB7Y5c!cN=;yQF&){|@ek-PF&5Jt%1>*j&!m+VMi`Xnek7jN{B4hkB~Bt5miG;9TM# z%i)UYPhHUQ%EUi&fy>ZjLqL#O;hgf759_>ebmqh*ZC0|NyItZ+=soUex~uGRvzvwP z@JZ?FNoc0;M!aAW+&#T-a@y=h{P^U!8@fReZFcnxRw=?=(H|+L&9X0mMy0qcT|NbR z^!(Y*kz~=uDd2XT%|)eCqn-3o@ziifeN-}KdZpb6s7yIdqJl%s6W-B^U$TkHmFx-} zbPV|_* zLcGNZAJtb#w@;+Q*a&hs+2*hKOS*I7xUPmBE!eI>T`p-wU(($!aV1~Ut?pIE;pcMP!3IltgHv)crKw z%n+Xs(PHYi5Q8Q({IZPnu#~?-N;Sbi8qk{#kS)%86Ebs+ZZ^4*lLIU~s zr8j_@#H@~d&N@qP$VLW}QOHS=lA<*zJqwAZ6aj6qwG0uhb3~IiEiFadq)#(h(pP5y z%ix;3YV|!@%T~*^nBr;R9?6JZ0q&ARu}l0O(6Cia5y2#kkiOU`@PPQNeMbF=#)~W~ z*LJJC?=w=99~(>-#zliaNJ8AgkiTh6kl1OW$(Si@X7?jxM_hzAiwX}(P2AIJ6%u|MZ*(;4OuhKJLA#Mhx)`G%{6lW9zhc&GG#WGXhIDb6Z8er0?O8-cgM+2mg$l*yfH`1n{q}g7P?uchna6!I@GqESZmOxBUa~Fzw0a; zIWbH2!w^ynmzr;|F?yM~$-2&>Bk(yUed>4(eJt0{%mC{UAfnVPLI1dtTP$T|DL*ks zG$A}DeVZi)nHgf{TW0Q1WA)YQ$NCJj7pQf1su5@MdaWT9jU~P#rlj~8-!tY1W_~0+ zNee>QL9{+$G^8MQoCS8!1Qj6O$@76Pc`7-3dJa7#>|qptwj4=L@D3j0Ub(|*Z*bZ_ zRUC*>(ep(uQKh!{zF0Z8hnv&OJ!@%9-RniRr_CdQx-iT0=}jtNB!B6ngL4;g4;QKP QyuBVS3CaUrj{x)k1J>r|=>Px# delta 4463 zcmbuCe^69a6vy9P*fmhrNcj;6$e4*%R#wjRhmAI4{s<>@{DF5dXLWD2_%5t(7i`N~ zYO0yC#)Yc_qL!JYNEkd3F$6U=gcJlyTFf6!(=Qrlw3((+XYAg)zTLO$6Q8sD$GPX8 zb3X5T?vI7HpZ0^cK49*Qi;ETLXCHpt{*0y6oNP!=ug^L@V&;16g!A-avmg5Rofh%Dp-V8V6Dp-ZLz%6(wTuLU#J?Pm1ckp_< z_yyhxcj9VT&FA^zmv|T4g?GclAMZ;L39NsFU8Trd9en@d`B_ z)YnN@hS7??PP{4%zooB}-f}z5^>yM)Z^xOwPI{XrouG-6l2MU-Uq927(pG9670Gw- zbNx!etD<a1%F$OIZ^j+*u;|Fc6X_yirg zJ3a*6s*CpA>3yR?7k(%DL%p;izmk+BM{*5o^h#;IRyF(Shr+jCeMZVA6K`T9jmi-< zTJEP;=xH>(hF-ZH`o+oV#7O=CwcUe~BPEgzJ)w0-8f$MKsw1->2%K?UL@jj;B^9cq z(dc16{XlpMb&_kTPYpZ8-|n}PCnt>}v&JM2>r&0vj7ju#qcgU*R%Ho-7f2Irf-O$$ zfjxZ8fEj%3g}r=CgK2aeE~emAm`ZsJxhg4&{>=3jp2f@2l(q?KN^?sC7DkrkiBWKpn)&?RGJwylGRx$v9@>F zI^h5sC`+GX?4j))U==UDkH%5|poSKUw`jx%IMI50hSqZ}6B>}BP(SHTw}83wqZyrJ z379?`7|7h|S#sTU8;AiulT=vbLXsT+{NG07W%{fl*)V%sG`LK*XHNxxkpAomaaU>B zXLP!(l1mySBj!B;u99i<9x-00@d_Ew>oR~VWLi!(_=kLylbQH04NIJfcDf23Qa+mx zk|Q}8CKi>hk>7G2Hr}MJEXU8^4!|Ih7d&e;(fvCiUs>P;;G&Ec#sb=?W6?VBE4i^~ z92k_xE}m$h4b9Ch2Y<=GX09jytJpJYWH)cDL|q1!6D+{%MGS>}6Y zPLXq#G}CETltnICl1O4+yiw|+b21@sqNkhHoMEPimE3PFu!?S%ePuwSm*x7H`GFOT zbfT3-l0B&OBTJoSsW_+G=|Wg(p`RswV#deJ05j*P@x@r}O3lxtD=!J0C%@;-pK*Z| zTx8}FtBSQc?8_Yzdd2>_&ulMPDHhq#a{nce*!lbJD=b)!is|l2?0lu00&y=(2PRUn ztT@(1&xl=fr&~^bd7&xhf?(|xtQUki~FITw8#MV6oxIorO@01)Y4H6`KA@xt}- T#`B}2dy6GN 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 delta 4281 zcmb^zS!^4}b!NF-QItqZBz51U4pWjXTecEAlHyB>C10^Er-|tpGR0jYV zihLop+Z3rWEu1s~3IqLUDg{lT{`4bgKb*pDT0nt{9z@(W{Yrld*#?ThEzrIe1`t4dDh@6sje9W4tNc6c2<0lt@IQ zYN_g-pbNfv33VqV9MpnS!~C-1Yx2Xs1ix1;o2S-pb_9y@d5JgEP;bRTWtJAs+SMwx zYN2`^rUr1e3w850i^m#QzF8_e`W!8IASe`e_7SflNf=TE;#eV}{B45WNjSFE4SG z&Gm~Ds%AtpN^*SM&`e9HEu!me2cCNdPZUgOX2diZYr_?&o*YlIc*JDS;#!GJqt&cj zY!04}YSSk8hS_m#1F4}>)(Ue$%{Jg{)ja+qm#bk2k2!?-$+0Qci~Bnf96?acms~gK zsl~AS4>pNyTioLf+xHK`&OeYnLtnBj2)Yn-BiITcBnvY$?y+R@Oe`@mnn=aRz**>P zhE|6>;sF1&qG3)j5pu=0a1vT74q7%kR{0q01a9m>Fo0~D80yb3B5W9e@a{pJ2?c#H z%M_#xjbU&PBOb-G=Mi9l+0i+KB5D{#D|9*Evtx)8%B|^j0C6J-FaT@}K*kv}VhO{H zB%)SEF+XDM=OTJa6QN-*;Ms2g2stb*I}p)HKp)GhC{oZd7DoI@0EPoV;hUexwxxpcVYEK=L=xs#@!QI!pu0Cb~dFMChiyWw*n3OFklz$DRX?|_TwQ% z7@KiSM+{>o$yAZNmf6BYG&bWEHDyVPGA&|iDm%exaAWt{nF1pfjY1)YoL~jeC|FR* zM8?H5Bbj1RZPdIltud9K4u*m{o+wZukz=}Hi_Nqp@jr9(T~lFV!gH(0FxASBc2rwHW*O z1DC(NvC)cek>zaRcy@E~YRk{1ei^uCKfGgjzFB+P zzD{#!VAe~UD#_}+HeN1G)2R})-6fCaY!V9jYcJP5*=O)v)e;bd1@ z9>oxd4@6+TeBt{l?o;KfDivjkfbH%6uTklCf0;)fEzXhHrfT@@-L*0}vkv+3e>w^k zO470wZQ<}Tn{3$+qaP265h7gEkmDm`!dH{GrY0bD1y@n;s_D|4(B~N7ZHiobg>F=zsA(GiuY9G>a{eU()A1M zn|Me(6@FHRb5ZfAP2S5fC!UUjh+KOJ#8o10h1`|LuH%s#2*kUPyDr#w5SM?sufHj* z3;w%!Xsz-wFRjbHJy-sg4OaPoANHr=H+m#OJ|mxBA}dNG|Myc{c(iIGf9vUQ@!{<| zxNk=@5ASH=p91bz&rHgne^OM*XM4K`LqK|k?kDuxP%qebcPGy85$|xn?Vh)Fc)RW1 zc011dZGexiHq1an2r4Enncql@r@U@jZ03>=lInfMM}2r%@cSVr|7n4bH+DENX~g*B zke}b*>El~F#BI@zw}mR7fe1hq=h9xatIE16mZlnb#P4@!+fPVmX{re`r#vWh0sw{Y z$nKBu`igR2b9PH4n;l5nkmRK`IfW*=L40X@mN;LsN@ne^*k|YrId3~bX6U=N85(jP z&dWWKiVy+FLVkTuC;!);`kiT9imWQjOC#{|6B`hw;)dumZL!2C^Yb_NHqRXbWJXan zU4v&#B-WGCNmJp)UQfox`VS(!B$q}K-O z2ZH2IfWrJv&^y>@yVKJ;SY!LJ#t!oj8*P9Ob@8cveT^BXaaND$(Mc_SLG;WS1-b)h z4H*WDUF9F`tEDdf*}nRAHzGU$GDYzum|$$ehDY>xISlrz%*!_q)wN=aXLVau#$<$P z>ZX(lJ;o-{(oSkRv>*m6JC6Jtpt9U_*`Xf1Xp9J`U;a>lnGq>m7n^|t#x!ka8sebpR>SelzUmrdf0Cu`i$0uA9-=hQT{jTmEzv#N#w*1Vo rBa=(9 0: raise HTTPException(status_code=400, detail='Cannot delete user with active bookings') + db.delete(user) db.commit() + + # SECURITY: Log user deletion for audit trail + try: + await audit_service.log_action( + db=db, + action='user_deleted', + resource_type='user', + user_id=current_user.id, + resource_id=id, + ip_address=client_ip, + user_agent=user_agent, + request_id=request_id, + details=deleted_user_info, + status='success' + ) + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.warning(f'Failed to log user deletion audit: {e}') + return success_response(message='User deleted successfully') except HTTPException: raise diff --git a/Backend/src/bookings/routes/__pycache__/booking_routes.cpython-312.pyc b/Backend/src/bookings/routes/__pycache__/booking_routes.cpython-312.pyc index cca1b89f383855c944214353936f7eed83bc553c..d4c4dffcb9b4b251b17af9493fe206c17431e183 100644 GIT binary patch delta 19229 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 sU#kC1k1S$ZH}`0SOP0r$dr6K!!;IdxocH2%@%X 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 bx$~S}3h|BXj9Idtnc>hrdV#%UMR)!ufLy%$ diff --git a/Backend/src/bookings/routes/__pycache__/upsell_routes.cpython-312.pyc b/Backend/src/bookings/routes/__pycache__/upsell_routes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ebe41c947fbb5cc666b3e67de81d231329b0728d GIT binary patch literal 4426 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 UMH5mIjU*9$PlF`?bC~=84{e+omH+?% diff --git a/Backend/src/guest_management/routes/__pycache__/guest_profile_routes.cpython-312.pyc b/Backend/src/guest_management/routes/__pycache__/guest_profile_routes.cpython-312.pyc index 54c3d84a25e6548822ae6ec7cc16214ee441973d..5a7354dc2f0620ac212e53366cadb11167dac724 100644 GIT binary patch delta 5784 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 diff --git a/Backend/src/guest_management/routes/complaint_routes.py b/Backend/src/guest_management/routes/complaint_routes.py index 7d8a9509..be7274fa 100644 --- a/Backend/src/guest_management/routes/complaint_routes.py +++ b/Backend/src/guest_management/routes/complaint_routes.py @@ -1,7 +1,7 @@ """ Routes for guest complaint management. """ -from fastapi import APIRouter, Depends, HTTPException, Query +from fastapi import APIRouter, Depends, HTTPException, Query, Request from sqlalchemy.orm import Session from sqlalchemy import and_, or_, func from typing import Optional @@ -18,6 +18,7 @@ from ..schemas.complaint import ( AddComplaintUpdateRequest, ResolveComplaintRequest ) from ...shared.utils.response_helpers import success_response +from ...analytics.services.audit_service import audit_service logger = get_logger(__name__) router = APIRouter(prefix='/complaints', tags=['complaints']) @@ -26,10 +27,15 @@ router = APIRouter(prefix='/complaints', tags=['complaints']) @router.post('/') async def create_complaint( complaint_data: CreateComplaintRequest, + request: Request, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Create a new guest complaint.""" + client_ip = request.client.host if request.client else None + user_agent = request.headers.get('User-Agent') + request_id = getattr(request.state, 'request_id', None) + try: # Verify booking ownership if booking_id provided if complaint_data.booking_id: @@ -74,6 +80,31 @@ async def create_complaint( db.commit() db.refresh(complaint) + # SECURITY: Log complaint creation for audit trail + try: + await audit_service.log_action( + db=db, + action='complaint_created', + resource_type='complaint', + user_id=current_user.id, + resource_id=complaint.id, + ip_address=client_ip, + user_agent=user_agent, + request_id=request_id, + details={ + 'complaint_id': complaint.id, + 'title': complaint.title, + 'category': complaint.category.value, + 'priority': complaint.priority.value, + 'status': complaint.status.value, + 'booking_id': complaint.booking_id, + 'room_id': complaint.room_id + }, + status='success' + ) + except Exception as e: + logger.warning(f'Failed to log complaint creation audit: {e}') + return success_response( data={'complaint': { 'id': complaint.id, @@ -127,6 +158,15 @@ async def get_complaints( # Staff/admin can filter by assignee query = query.filter(GuestComplaint.assigned_to == assigned_to) + from sqlalchemy.orm import joinedload + from ...rooms.models.room import Room + + # Eager load relationships + query = query.options( + joinedload(GuestComplaint.guest), + joinedload(GuestComplaint.assignee) + ) + # Apply filters if status: try: @@ -151,16 +191,29 @@ async def get_complaints( complaints_data = [] for complaint in complaints: + # Get room number if room_id exists + room_number = None + if complaint.room_id: + room = db.query(Room).filter(Room.id == complaint.room_id).first() + if room: + room_number = room.room_number + complaints_data.append({ 'id': complaint.id, 'title': complaint.title, + 'description': complaint.description, 'category': complaint.category.value, 'priority': complaint.priority.value, 'status': complaint.status.value, 'guest_id': complaint.guest_id, + 'guest_name': complaint.guest.full_name if complaint.guest else None, 'booking_id': complaint.booking_id, 'room_id': complaint.room_id, + 'room_number': room_number, 'assigned_to': complaint.assigned_to, + 'assigned_staff_name': complaint.assignee.full_name if complaint.assignee else None, + 'resolution_notes': complaint.resolution, + 'resolved_at': complaint.resolved_at.isoformat() if complaint.resolved_at else None, 'created_at': complaint.created_at.isoformat(), 'updated_at': complaint.updated_at.isoformat() }) @@ -192,21 +245,37 @@ async def get_complaint( ): """Get a specific complaint with details.""" try: - complaint = db.query(GuestComplaint).filter(GuestComplaint.id == complaint_id).first() + from sqlalchemy.orm import joinedload + from ...rooms.models.room import Room + + complaint = db.query(GuestComplaint).options( + joinedload(GuestComplaint.guest), + joinedload(GuestComplaint.assignee), + joinedload(GuestComplaint.room) + ).filter(GuestComplaint.id == complaint_id).first() if not complaint: raise HTTPException(status_code=404, detail='Complaint not found') # Check access from ...shared.utils.role_helpers import is_admin, is_staff - if not (is_admin(current_user, db) or is_staff(current_user, db)): + is_admin_user = is_admin(current_user, db) + is_staff_user = is_staff(current_user, db) + if not (is_admin_user or is_staff_user): if complaint.guest_id != current_user.id: raise HTTPException(status_code=403, detail='Access denied') # Get updates - updates = db.query(ComplaintUpdate).filter( + updates = db.query(ComplaintUpdate).options( + joinedload(ComplaintUpdate.updater) + ).filter( ComplaintUpdate.complaint_id == complaint_id ).order_by(ComplaintUpdate.created_at.asc()).all() + # Get room number + room_number = None + if complaint.room: + room_number = complaint.room.room_number + complaint_data = { 'id': complaint.id, 'title': complaint.title, @@ -215,15 +284,18 @@ async def get_complaint( 'priority': complaint.priority.value, 'status': complaint.status.value, 'guest_id': complaint.guest_id, + 'guest_name': complaint.guest.full_name if complaint.guest else None, 'booking_id': complaint.booking_id, 'room_id': complaint.room_id, + 'room_number': room_number, 'assigned_to': complaint.assigned_to, - 'resolution': complaint.resolution, + 'assigned_staff_name': complaint.assignee.full_name if complaint.assignee else None, + 'resolution_notes': complaint.resolution, 'resolved_at': complaint.resolved_at.isoformat() if complaint.resolved_at else None, 'resolved_by': complaint.resolved_by, 'guest_satisfaction_rating': complaint.guest_satisfaction_rating, 'guest_feedback': complaint.guest_feedback, - 'internal_notes': complaint.internal_notes if (is_admin(current_user, db) or is_staff(current_user, db)) else None, + 'internal_notes': complaint.internal_notes if (is_admin_user or is_staff_user) else None, 'attachments': complaint.attachments, 'requires_follow_up': complaint.requires_follow_up, 'follow_up_date': complaint.follow_up_date.isoformat() if complaint.follow_up_date else None, @@ -234,6 +306,7 @@ async def get_complaint( 'update_type': u.update_type, 'description': u.description, 'updated_by': u.updated_by, + 'updated_by_name': u.updater.full_name if u.updater else None, 'created_at': u.created_at.isoformat() } for u in updates] } @@ -253,17 +326,27 @@ async def get_complaint( async def update_complaint( complaint_id: int, update_data: UpdateComplaintRequest, + request: Request, current_user: User = Depends(authorize_roles('admin', 'staff')), db: Session = Depends(get_db) ): """Update a complaint (admin/staff only).""" + client_ip = request.client.host if request.client else None + user_agent = request.headers.get('User-Agent') + request_id = getattr(request.state, 'request_id', None) + try: complaint = db.query(GuestComplaint).filter(GuestComplaint.id == complaint_id).first() if not complaint: db.rollback() raise HTTPException(status_code=404, detail='Complaint not found') - # Track changes + # Track changes for audit + old_values = { + 'status': complaint.status.value, + 'priority': complaint.priority.value, + 'assigned_to': complaint.assigned_to + } changes = [] if update_data.status: @@ -308,6 +391,53 @@ async def update_complaint( db.add(update) db.commit() + + # SECURITY: Log complaint status change for audit trail + if update_data.status and old_values['status'] != update_data.status: + try: + await audit_service.log_action( + db=db, + action='complaint_status_changed', + resource_type='complaint', + user_id=current_user.id, + resource_id=complaint.id, + ip_address=client_ip, + user_agent=user_agent, + request_id=request_id, + details={ + 'complaint_id': complaint.id, + 'old_status': old_values['status'], + 'new_status': update_data.status, + 'guest_id': complaint.guest_id, + 'resolved_by': current_user.id if update_data.status == 'resolved' else None + }, + status='success' + ) + except Exception as e: + logger.warning(f'Failed to log complaint status change audit: {e}') + + # SECURITY: Log complaint assignment change for audit trail + if update_data.assigned_to is not None and old_values['assigned_to'] != update_data.assigned_to: + try: + await audit_service.log_action( + db=db, + action='complaint_assigned', + resource_type='complaint', + user_id=current_user.id, + resource_id=complaint.id, + ip_address=client_ip, + user_agent=user_agent, + request_id=request_id, + details={ + 'complaint_id': complaint.id, + 'old_assigned_to': old_values['assigned_to'], + 'new_assigned_to': update_data.assigned_to, + 'guest_id': complaint.guest_id + }, + status='success' + ) + except Exception as e: + logger.warning(f'Failed to log complaint assignment audit: {e}') db.refresh(complaint) return success_response( @@ -332,16 +462,23 @@ async def update_complaint( async def resolve_complaint( complaint_id: int, resolve_data: ResolveComplaintRequest, + request: Request, current_user: User = Depends(authorize_roles('admin', 'staff')), db: Session = Depends(get_db) ): """Resolve a complaint.""" + client_ip = request.client.host if request.client else None + user_agent = request.headers.get('User-Agent') + request_id = getattr(request.state, 'request_id', None) + try: complaint = db.query(GuestComplaint).filter(GuestComplaint.id == complaint_id).first() if not complaint: db.rollback() raise HTTPException(status_code=404, detail='Complaint not found') + old_status = complaint.status.value + complaint.status = ComplaintStatus.resolved complaint.resolution = resolve_data.resolution complaint.resolved_at = datetime.utcnow() @@ -366,6 +503,32 @@ async def resolve_complaint( db.add(update) db.commit() + + # SECURITY: Log complaint resolution for audit trail + try: + await audit_service.log_action( + db=db, + action='complaint_resolved', + resource_type='complaint', + user_id=current_user.id, + resource_id=complaint.id, + ip_address=client_ip, + user_agent=user_agent, + request_id=request_id, + details={ + 'complaint_id': complaint.id, + 'old_status': old_status, + 'new_status': 'resolved', + 'guest_id': complaint.guest_id, + 'resolved_by': current_user.id, + 'satisfaction_rating': resolve_data.guest_satisfaction_rating, + 'has_feedback': bool(resolve_data.guest_feedback) + }, + status='success' + ) + except Exception as e: + logger.warning(f'Failed to log complaint resolution audit: {e}') + db.refresh(complaint) return success_response( diff --git a/Backend/src/guest_management/routes/guest_profile_routes.py b/Backend/src/guest_management/routes/guest_profile_routes.py index 88a0c01b..84de4f0f 100644 --- a/Backend/src/guest_management/routes/guest_profile_routes.py +++ b/Backend/src/guest_management/routes/guest_profile_routes.py @@ -378,6 +378,70 @@ async def remove_tag_from_guest( db.rollback() raise HTTPException(status_code=500, detail=str(e)) +# Get Communications +@router.get('/communications') +async def get_communications( + user_id: Optional[int] = Query(None), + communication_type: Optional[str] = Query(None), + direction: Optional[str] = Query(None), + page: int = Query(1, ge=1), + limit: int = Query(20, ge=1, le=100), + current_user: User = Depends(authorize_roles('admin', 'staff')), + db: Session = Depends(get_db) +): + """Get guest communications with filtering""" + try: + from sqlalchemy import or_, func, desc + from sqlalchemy.orm import joinedload + + query = db.query(GuestCommunication).options( + joinedload(GuestCommunication.user), + joinedload(GuestCommunication.staff) + ) + + if user_id: + query = query.filter(GuestCommunication.user_id == user_id) + + if communication_type: + query = query.filter(GuestCommunication.communication_type == CommunicationType(communication_type)) + + if direction: + query = query.filter(GuestCommunication.direction == CommunicationDirection(direction)) + + total = query.count() + communications = query.order_by(desc(GuestCommunication.created_at)).offset((page - 1) * limit).limit(limit).all() + + return { + 'status': 'success', + 'data': { + 'communications': [ + { + 'id': comm.id, + 'user_id': comm.user_id, + 'guest_name': comm.user.full_name if comm.user else None, + 'communication_type': comm.communication_type.value, + 'direction': comm.direction.value, + 'subject': comm.subject, + 'content': comm.content, + 'booking_id': comm.booking_id, + 'is_automated': comm.is_automated, + 'created_at': comm.created_at.isoformat() if comm.created_at else None, + 'staff_name': comm.staff.full_name if comm.staff else None, + } + for comm in communications + ], + 'pagination': { + 'total': total, + 'page': page, + 'limit': limit, + 'total_pages': (total + limit - 1) // limit + } + } + } + except Exception as e: + logger.error(f'Error fetching communications: {str(e)}', exc_info=True) + raise HTTPException(status_code=500, detail=f'Failed to fetch communications: {str(e)}') + # Create Communication Record @router.post('/{user_id}/communications') async def create_communication( diff --git a/Backend/src/hotel_services/models/__init__.py b/Backend/src/hotel_services/models/__init__.py index e69de29b..e00d88b6 100644 --- a/Backend/src/hotel_services/models/__init__.py +++ b/Backend/src/hotel_services/models/__init__.py @@ -0,0 +1,8 @@ +from .housekeeping_task import HousekeepingTask, HousekeepingStatus, HousekeepingType +from .inventory_item import InventoryItem, InventoryCategory, InventoryUnit +from .inventory_transaction import InventoryTransaction, TransactionType +from .inventory_reorder_request import InventoryReorderRequest, ReorderStatus +from .inventory_task_consumption import InventoryTaskConsumption +from .guest_request import GuestRequest, RequestType, RequestStatus, RequestPriority +from .staff_shift import StaffShift, StaffTask, ShiftType, ShiftStatus, StaffTaskPriority, StaffTaskStatus + diff --git a/Backend/src/hotel_services/models/__pycache__/__init__.cpython-312.pyc b/Backend/src/hotel_services/models/__pycache__/__init__.cpython-312.pyc index c775b9aaf9f7ae29c1bdef0d4cdad9051d0c44c7..425a2e9a9d3f94ab7279a5bd6571782dea8c2aad 100644 GIT binary patch literal 964 zcmZ8fyN=U96dmXNa^CNyKy)q^(}NJ&MIx)!BD9+{(^ye5Nu2oFVP>)-e?iAL@C|$d z4J!(ykx&XMI;8DZLSn9yI1y7E&pr3t>+9qD)iiTNpTA$8xzA&S{K8;=bR3@hkNWY6 zjLF#W2pH6W7>z-k#tr0RUILOd2`QR_G);$G+{-|gW+6v&VVv;tP@n}csTsyeuLvbt z3hybe3>8{|Dy>3|)}T)7A)odd(4)OS^jKD0m3zTT zumxjF*Pr%>Vxg+X%#9q%mH1D!UNT*2Uii1nmjPd2NCv7Bh37-brkXxd418BwF;$N= zeLnQXa3tNpSEZw%kG*566$!6dz{iYVv-c||q%wE$b}q|M?MR8kp9j8J!4lhA2~~K5 zHT%;1?t-ttK4HV%_my_xJMISDm1`@daym7c+|1mGRN3$zx|0o=utz?;?mAQ=$-ajw zMbuXF)_SBWvm?75sYm8<{Udk+A*n$X-My`jt9|7u%%l-Am~I@>^57xQq5zjsbq)f) z_0HkjZ{>Lu6;RYYh;RmHbnuOvC@`mJT~t}b$`V2up@NdUK!Y5C4)H3|HH11s1EGn~ z((swwYa|a>_ACHqPyG+}YbF*lSlYcnGVk;(2o`$%?6cu$!Thl;_()52#u1F)x+5lR z2*%74w&S>Zl^o~ka{Z8bp~;oUUh*fpJMOcvH2jGhhOr@oJ2Kdi%R6$pA-xT``bl1G UNdG(OZ^--ynSUd5{#dL30Vm)ymjD0& delta 88 zcmX@YzK+rSG%qg~0}vdUsgnt!AA<;V@RThf<(Y0sIloex2gI+4$u zd+zVv^L6K(n?I-1Ne-UB{N^L=cfkD}2kj@;V&41GOJO4-MGRiz zji?kgVp7bAOL0Sxgb?DwdcsIbNuQ7Cq9ID6&+~dpN~uvbrl*aJlnHTRZh}+eUvjEI zvLrR<+tOAayr1*ITk7($3BV@bi_Q615wNNEV!M588n79Xc^+wL%h&m0_B9TQ0*jrq z^qOI^_yyA;6+&6;x*#6`Pa;3j!Ky4c>eJ%oC1qQb^^buo{vgYFOgc zh!j*p zvTVWkiOz7Us&(j<6cyF~oY!e5Xl}S z2ap^@GJ@m~60{+(b02d1+XQ28W1PX?OrJXuxaK?Awdc`=Uz-Rr_qgZR z#hg0^+@te>F9_V>OQ*KQBD>QR<_J%sY8d|(Bc1va3*$ z>)Ey{-&$2r#jRCfYulC{oF}~yg)Ld1FN*?=t6Lr-0%DDi)c;<@fR7kdqh!zAsbJN1 z3i?u=s1LQU+ilqQ7TK^b+iC0mx5y6oGR*9jPY2&38}Vg>@31hsnC|#{=zk&KxjxhJ z@Gz`DqGsC}kdFG}uFiPWALmH25?bu;^fD|d84 zOOU~b0=5jCdnnYPaUfR=(eJ@jG3>usTqQF~O?O`Z2Za$|NIwSN-vCfP@2`mXJQFOl zR>f}G+Mv}E7=I0PG5!YV!TviFG-$Nlo+^H@tAMh!EIVjtt3TmcrFi*ZVNIn1GRc-} zvZk`Q-};b2H$XzMiTK?FJH6j|Wd~YsP<637vzYA{dcSP><&6mtPrp9WC<3C6=0KpZ zI5ANrcA08byfH9Qv2D0p5EVK&IMVJ2@|`+OpxTtbea$JGP-WZd0j)SJ2}-FG@Y`0x zj2PtCgk}kS9bhvvaxmzD&Qpbp3X$=CA{&}nLri~5G3I&aOcJsW@edRRxj>$ZXji-y zHpXMjSkza`G98zE%`Cg+l`q&`@bLyjkzn7o7oh+{^C}#2ue?fR=i>|0ONEtbFLBX5 zyOHZ_#Gef|udE$7vvOng+D$Ka%e}afDa_w`I@cUr+jnxg_NOr~^HXHOod#?V^f=+fbpp%)jwQa{i5vURQDp-likxey_Pu)Q4{+bu{Ckn9otCtH+q}*o0^w8 z1%CD%SU9pQF8#{u8F!~PdJisqvb=XWa_Ej`Cov4 z^n))6zqsIV<9`J#v$f^~Tq69F6&!n>;Pt!Lo>eGOPn0clMys5FYM|VK+ceGK;)w0g z;I_9Pcg5R)f0TBevG0UWgUjlV=xu;tkGH=7@{dR;6#5%?`vrG*O)bwq+Kf(&6H2}^ KfaoG=pkn}uutymH delta 295 zcmca3u|5OXx;i@#CqM|6Gnov<`6j7~8Ax+_10v`FL#i`k; zsRfyN=^=^5*|&ry-(|bYs6BZ%`?JX_Io>d?oIH*5#pW)q5=O@0$^W^11hp8Q7$=l` MWdPAd(m?kB0EO5^b^rhX diff --git a/Backend/src/hotel_services/models/__pycache__/inventory_item.cpython-312.pyc b/Backend/src/hotel_services/models/__pycache__/inventory_item.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d5fe91a571783e094110eed7f34b1e898e88ba17 GIT binary patch literal 3427 zcma(TO>Z05aY-(}N%2FJC|Qv$+mdC*wipL#ixx=?Sd{hQL@q2gZMKNFE50Xb&HbR? z?#g0-6k8xaK!y*YkwcUl0}H5n(^0*&1qu{2ynv-w7)6kiZY``{a%t!7lGI0CJ1cQ^ z-n^OF`Fb<&ugPSLgXfb!yd{4J*mrEvc|vXE#oqw=fGX?b`#fWq$HWR zEUTFEa|^fT3qG14SdmOw*Nhdp2I~j{4ih-$_n8qu;jc>uu6PD{(AQqPxCqD>9OeWc z;sihP2?696coYzVh!;XAD1=c+h@h|#tw#z`nwiyBv1aO|J_#RLhBwVrn7!bHXjrwH zB4dMw6j{TXL1U&aD;PWkT2xZ8CYub6tC9@as}dD1>=j?rAifRsA!N1u)>aLV6rM#uu+7MW5p=S9oI$K#Og$? zPO|JP420olTmbMnw>xmD#rN$d`|KOLnPGcwH$7y}?q<)}H+OUA?GJYoy}z3xr#pf# zPs&@c{lk)iB0>yBg?K$yh|^>@Ilq8N(_l@;5a7U~zD7ffx@js*`H&TQ4jK^{qF{b8N#JWx6T;OEH{2b`_D2sYGb6sk|rS_em zzUfl?Pf#z_^M!#UJz^G`7)>2-YDT#bpz#`!Rf*KaDvX<;tRcKclad9!T&oZaUEJLN z514eL;HMGIQWR-X!DIp`1s}P}UVisQ!w4@)mSXO+p})_L5@BE;F`j_O6S6e?F5B*o*nW;nM$#ugPW1|+$J>L z!C*r(A*MiqSr2nrRCKTlTqkVQ(Wq>QEaa=0#sD=58=i=U7bQ}GVMzl<)uKTS#;8H! zAVAn1i-s(-qUrZ(49W5eDFOOin|CM1U$ z+{un{a{63i7BIkuneiuR`!IcSWA=yNMpNtIjq+A*r|d-M?CX2+%!3P!-iMc(!L70r z|CwFd%MLVhP2R~~v2X6B`W~Rx(B-H7SGI$#>0dpYzVl@Ij*}AY874To{@K&)*j8}+ zvuD?CKe=|>i7(ite}%Y#(~p)n7Mo|k)V8c0$;r>O_}pH8_|-(y$-jGouT27tnf!zM z4YG0d;cvHiC;cOPmRYvcSa?`%mYmd8Feg9UK+Up~e;agoCM<0%d|BNpIr*P@q)dMO zezR{I+fugI*D<6>RZqeX*`Mr;I+o=w|U_IgcybA_PY@v_FjC+ctR93KBC*9NMo;>&9xOMBEKJUP>TJuUX0T}H$l%CLu z>QQMYvNFcAtT2H;H$G-FuE(ZpSY`-$&l{Zm{o@ zPe6iQJd6(j{4?P5`ToJJJahk_bK}ps;lFe7=iHkIeAt(31RCY9K7RD^0SAkNY}A)q dpKRp58hkW(z`^36f84j~TQB{aV{bPa{{<2qf?NOq literal 0 HcmV?d00001 diff --git a/Backend/src/hotel_services/models/__pycache__/inventory_reorder_request.cpython-312.pyc b/Backend/src/hotel_services/models/__pycache__/inventory_reorder_request.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc4bbed611da02bd363034a2338721ad67315180 GIT binary patch literal 2801 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 literal 0 HcmV?d00001 diff --git a/Backend/src/hotel_services/models/__pycache__/inventory_transaction.cpython-312.pyc b/Backend/src/hotel_services/models/__pycache__/inventory_transaction.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3495eb741c278f61f6c9ec8e43cef3ebf6574991 GIT binary patch literal 2222 zcmah~-EZ4e6t@#6aX#HNNz=7!2V+tf1kE%G(#AGK>DG@yCb7aK53t(m6MqG}2d{9pq2|3~Mf}WIErM-_m>88H z4x*x%s(}@S#uUYX2@Xxc_Ig*-YwN% zApQrV9^kNmLny$0o55}=h5db?5hQH1vj$}u*bH|uBE!HxievkT&B#UoY=B3Ox6czu zMA6PJ3iTk8FGZwYijdlfKD2K|phpKZMaM(1z1tMtZU+3>w>SGjUA>5$e;3dQig$fx zh|awO9wLuIcyRfoABSEF`V!sv4f)vrL54Py-CKtNc^IXTwAt5*qdZbi7y4;-wo^Pk zO+p7=SW?z}AxPsjq8TdG+6we^NHY<>L6fRuS2eRtFzce5{rl3IjSXvDLl+7GDw>Y2 zs|z|NN5Q26Pgo1@vH7^iGQP*$y(v0S-Atko^vrtcFFii++4k|+5o3>`xsnp?=1-uC3kEXgC>Y|Ma2{C+%U>GjgEMxByHVZpz zP{;a}hD_*oiwfncWzz&Gg4+y?l!ZEBK_f{93Ojb$tbTdWEH+1J4AKE_q^LGwL!S!F z88Xa7y)GYN+>s8EfU$zms4yb3Z@3>liAt-H=KNN6d%hK&b0>FG+0`p+%R7a$4@b}a zoZI-@da)+eppyKd>9FbAj$&6D@$wz93=Ubv~%TZEB_fYF`aLS>&L$@-Y;&SZ>8tlIS@|_HlzosR($%_lph}! znnH%gtZTZemzS_nC*2b9lIUgL%avartYrMu#DSx!eZg_aoR+#{p|eB^Zsv z$7oTjj6>4Y1^C<%mI)fPY~s&vFGB~j_FO#Ue{#-|_W{hNt92U4-$9<||Kyb4IpsG_ udCZMH=0^VD;*Yu4p9x`pXtms!`1aJ=sb?H4p7jm$Gkjy>Uyi*!kN*REfk`0% literal 0 HcmV?d00001 diff --git a/Backend/src/hotel_services/models/__pycache__/staff_shift.cpython-312.pyc b/Backend/src/hotel_services/models/__pycache__/staff_shift.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..83821bdf68d7dc894b6d589a8b99a7d369d55cce GIT binary patch literal 5604 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 literal 0 HcmV?d00001 diff --git a/Backend/src/hotel_services/models/guest_request.py b/Backend/src/hotel_services/models/guest_request.py new file mode 100644 index 00000000..b1ad1b94 --- /dev/null +++ b/Backend/src/hotel_services/models/guest_request.py @@ -0,0 +1,70 @@ +from sqlalchemy import Column, Integer, String, Text, Enum, ForeignKey, DateTime, Boolean +from sqlalchemy.orm import relationship +from datetime import datetime +import enum +from ...shared.config.database import Base + +class RequestType(str, enum.Enum): + extra_towels = 'extra_towels' + extra_pillows = 'extra_pillows' + room_cleaning = 'room_cleaning' + turndown_service = 'turndown_service' + amenities = 'amenities' + maintenance = 'maintenance' + room_service = 'room_service' + other = 'other' + +class RequestStatus(str, enum.Enum): + pending = 'pending' + in_progress = 'in_progress' + fulfilled = 'fulfilled' + cancelled = 'cancelled' + +class RequestPriority(str, enum.Enum): + low = 'low' + normal = 'normal' + high = 'high' + urgent = 'urgent' + +class GuestRequest(Base): + __tablename__ = 'guest_requests' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + booking_id = Column(Integer, ForeignKey('bookings.id'), nullable=False, index=True) + room_id = Column(Integer, ForeignKey('rooms.id'), nullable=False, index=True) + user_id = Column(Integer, ForeignKey('users.id'), nullable=False) # Guest who made the request + + request_type = Column(Enum(RequestType), nullable=False) + status = Column(Enum(RequestStatus), nullable=False, default=RequestStatus.pending) + priority = Column(Enum(RequestPriority), nullable=False, default=RequestPriority.normal) + + title = Column(String(255), nullable=False) + description = Column(Text, nullable=True) + + # Assignment + assigned_to = Column(Integer, ForeignKey('users.id'), nullable=True) # Housekeeping staff + fulfilled_by = Column(Integer, ForeignKey('users.id'), nullable=True) + + # Timestamps + requested_at = Column(DateTime, nullable=False, default=datetime.utcnow, index=True) + started_at = Column(DateTime, nullable=True) + fulfilled_at = Column(DateTime, nullable=True) + + # Notes + guest_notes = Column(Text, nullable=True) + staff_notes = Column(Text, nullable=True) + + # Response time tracking + response_time_minutes = Column(Integer, nullable=True) # Time from request to start + fulfillment_time_minutes = Column(Integer, nullable=True) # Time from start to fulfillment + + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + booking = relationship('Booking') + room = relationship('Room') + guest = relationship('User', foreign_keys=[user_id]) + assigned_staff = relationship('User', foreign_keys=[assigned_to]) + fulfilled_staff = relationship('User', foreign_keys=[fulfilled_by]) + diff --git a/Backend/src/hotel_services/models/housekeeping_task.py b/Backend/src/hotel_services/models/housekeeping_task.py index 51dd6541..085c2089 100644 --- a/Backend/src/hotel_services/models/housekeeping_task.py +++ b/Backend/src/hotel_services/models/housekeeping_task.py @@ -41,6 +41,7 @@ class HousekeepingTask(Base): checklist_items = Column(JSON, nullable=True) # Array of {item: string, completed: bool, notes: string} notes = Column(Text, nullable=True) issues_found = Column(Text, nullable=True) + photos = Column(JSON, nullable=True) # Array of photo URLs: ["/uploads/housekeeping/photo1.jpg", ...] # Quality control inspected_by = Column(Integer, ForeignKey('users.id'), nullable=True) diff --git a/Backend/src/hotel_services/models/inventory_item.py b/Backend/src/hotel_services/models/inventory_item.py new file mode 100644 index 00000000..059bba66 --- /dev/null +++ b/Backend/src/hotel_services/models/inventory_item.py @@ -0,0 +1,67 @@ +from sqlalchemy import Column, Integer, String, Numeric, Boolean, Text, Enum, ForeignKey, DateTime, JSON +from sqlalchemy.orm import relationship +from datetime import datetime +import enum +from ...shared.config.database import Base + +class InventoryCategory(str, enum.Enum): + cleaning_supplies = 'cleaning_supplies' + linens = 'linens' + toiletries = 'toiletries' + amenities = 'amenities' + maintenance = 'maintenance' + food_beverage = 'food_beverage' + other = 'other' + +class InventoryUnit(str, enum.Enum): + piece = 'piece' + box = 'box' + bottle = 'bottle' + roll = 'roll' + pack = 'pack' + liter = 'liter' + kilogram = 'kilogram' + meter = 'meter' + other = 'other' + +class InventoryItem(Base): + __tablename__ = 'inventory_items' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + name = Column(String(255), nullable=False, index=True) + description = Column(Text, nullable=True) + category = Column(Enum(InventoryCategory), nullable=False, default=InventoryCategory.other) + unit = Column(Enum(InventoryUnit), nullable=False, default=InventoryUnit.piece) + + # Stock tracking + current_quantity = Column(Numeric(10, 2), nullable=False, default=0) + minimum_quantity = Column(Numeric(10, 2), nullable=False, default=0) # Reorder threshold + maximum_quantity = Column(Numeric(10, 2), nullable=True) # Max stock level + reorder_quantity = Column(Numeric(10, 2), nullable=True) # Suggested reorder amount + + # Pricing + unit_cost = Column(Numeric(10, 2), nullable=True) + supplier = Column(String(255), nullable=True) + supplier_contact = Column(Text, nullable=True) + + # Location + storage_location = Column(String(255), nullable=True) # e.g., "Housekeeping Storage Room A" + + # Status + is_active = Column(Boolean, nullable=False, default=True) + is_tracked = Column(Boolean, nullable=False, default=True) # Whether to track consumption + + # Metadata + barcode = Column(String(100), nullable=True, unique=True, index=True) + sku = Column(String(100), nullable=True, unique=True, index=True) + notes = Column(Text, nullable=True) + + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + created_by = Column(Integer, ForeignKey('users.id'), nullable=True) + + # Relationships + transactions = relationship('InventoryTransaction', back_populates='item', cascade='all, delete-orphan') + reorder_requests = relationship('InventoryReorderRequest', back_populates='item', cascade='all, delete-orphan') + task_consumptions = relationship('InventoryTaskConsumption', back_populates='item', cascade='all, delete-orphan') + diff --git a/Backend/src/hotel_services/models/inventory_reorder_request.py b/Backend/src/hotel_services/models/inventory_reorder_request.py new file mode 100644 index 00000000..6495e83f --- /dev/null +++ b/Backend/src/hotel_services/models/inventory_reorder_request.py @@ -0,0 +1,53 @@ +from sqlalchemy import Column, Integer, String, Text, Enum, ForeignKey, DateTime, Numeric, Boolean +from sqlalchemy.orm import relationship +from datetime import datetime +import enum +from ...shared.config.database import Base + +class ReorderStatus(str, enum.Enum): + pending = 'pending' + approved = 'approved' + ordered = 'ordered' + received = 'received' + cancelled = 'cancelled' + +class InventoryReorderRequest(Base): + __tablename__ = 'inventory_reorder_requests' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + item_id = Column(Integer, ForeignKey('inventory_items.id'), nullable=False, index=True) + + # Request details + requested_quantity = Column(Numeric(10, 2), nullable=False) + current_quantity = Column(Numeric(10, 2), nullable=False) # Stock at time of request + minimum_quantity = Column(Numeric(10, 2), nullable=False) # Threshold + + # Status + status = Column(Enum(ReorderStatus), nullable=False, default=ReorderStatus.pending) + priority = Column(String(20), nullable=False, default='normal') # low, normal, high, urgent + + # Request info + requested_by = Column(Integer, ForeignKey('users.id'), nullable=False) + requested_at = Column(DateTime, nullable=False, default=datetime.utcnow, index=True) + notes = Column(Text, nullable=True) + + # Approval + approved_by = Column(Integer, ForeignKey('users.id'), nullable=True) + approved_at = Column(DateTime, nullable=True) + approval_notes = Column(Text, nullable=True) + + # Order tracking + order_number = Column(String(100), nullable=True, index=True) + expected_delivery_date = Column(DateTime, nullable=True) + received_quantity = Column(Numeric(10, 2), nullable=True) + received_at = Column(DateTime, nullable=True) + + # Timestamps + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + item = relationship('InventoryItem', back_populates='reorder_requests') + requester = relationship('User', foreign_keys=[requested_by]) + approver = relationship('User', foreign_keys=[approved_by]) + diff --git a/Backend/src/hotel_services/models/inventory_task_consumption.py b/Backend/src/hotel_services/models/inventory_task_consumption.py new file mode 100644 index 00000000..695d23d1 --- /dev/null +++ b/Backend/src/hotel_services/models/inventory_task_consumption.py @@ -0,0 +1,24 @@ +from sqlalchemy import Column, Integer, Numeric, ForeignKey, DateTime, Text +from sqlalchemy.orm import relationship +from datetime import datetime +from ...shared.config.database import Base + +class InventoryTaskConsumption(Base): + __tablename__ = 'inventory_task_consumptions' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + task_id = Column(Integer, ForeignKey('housekeeping_tasks.id'), nullable=False, index=True) + item_id = Column(Integer, ForeignKey('inventory_items.id'), nullable=False, index=True) + + quantity = Column(Numeric(10, 2), nullable=False) + notes = Column(Text, nullable=True) + + recorded_by = Column(Integer, ForeignKey('users.id'), nullable=True) + recorded_at = Column(DateTime, nullable=False, default=datetime.utcnow, index=True) + + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + + # Relationships + task = relationship('HousekeepingTask') + item = relationship('InventoryItem', back_populates='task_consumptions') + diff --git a/Backend/src/hotel_services/models/inventory_transaction.py b/Backend/src/hotel_services/models/inventory_transaction.py new file mode 100644 index 00000000..8c05dc72 --- /dev/null +++ b/Backend/src/hotel_services/models/inventory_transaction.py @@ -0,0 +1,43 @@ +from sqlalchemy import Column, Integer, String, Numeric, Text, Enum, ForeignKey, DateTime, JSON +from sqlalchemy.orm import relationship +from datetime import datetime +import enum +from ...shared.config.database import Base + +class TransactionType(str, enum.Enum): + consumption = 'consumption' # Used in tasks + adjustment = 'adjustment' # Manual adjustment (correction) + received = 'received' # Stock received from supplier + transfer = 'transfer' # Transfer between locations + damaged = 'damaged' # Damaged/lost items + returned = 'returned' # Returned to supplier + +class InventoryTransaction(Base): + __tablename__ = 'inventory_transactions' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + item_id = Column(Integer, ForeignKey('inventory_items.id'), nullable=False, index=True) + + transaction_type = Column(Enum(TransactionType), nullable=False) + quantity = Column(Numeric(10, 2), nullable=False) # Positive for received, negative for consumption + quantity_before = Column(Numeric(10, 2), nullable=False) # Stock before transaction + quantity_after = Column(Numeric(10, 2), nullable=False) # Stock after transaction + + # Reference + reference_type = Column(String(50), nullable=True) # e.g., 'housekeeping_task', 'purchase_order' + reference_id = Column(Integer, nullable=True, index=True) # ID of related record + + # Details + notes = Column(Text, nullable=True) + cost = Column(Numeric(10, 2), nullable=True) # Cost for this transaction + + # User tracking + performed_by = Column(Integer, ForeignKey('users.id'), nullable=True) + + # Timestamp + transaction_date = Column(DateTime, nullable=False, default=datetime.utcnow, index=True) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + + # Relationships + item = relationship('InventoryItem', back_populates='transactions') + diff --git a/Backend/src/hotel_services/models/staff_shift.py b/Backend/src/hotel_services/models/staff_shift.py new file mode 100644 index 00000000..5c832811 --- /dev/null +++ b/Backend/src/hotel_services/models/staff_shift.py @@ -0,0 +1,122 @@ +from sqlalchemy import Column, Integer, String, Text, Enum, ForeignKey, DateTime, Boolean, Time, Numeric +from sqlalchemy.orm import relationship +from datetime import datetime, date, time +import enum +from ...shared.config.database import Base + +class ShiftType(str, enum.Enum): + morning = 'morning' # 6 AM - 2 PM + afternoon = 'afternoon' # 2 PM - 10 PM + night = 'night' # 10 PM - 6 AM + full_day = 'full_day' # 8 AM - 8 PM + custom = 'custom' # Custom hours + +class ShiftStatus(str, enum.Enum): + scheduled = 'scheduled' + in_progress = 'in_progress' + completed = 'completed' + cancelled = 'cancelled' + no_show = 'no_show' + +class StaffShift(Base): + __tablename__ = 'staff_shifts' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + staff_id = Column(Integer, ForeignKey('users.id'), nullable=False, index=True) + + # Shift timing + shift_date = Column(DateTime, nullable=False, index=True) # Date of the shift + shift_type = Column(Enum(ShiftType), nullable=False) + start_time = Column(Time, nullable=False) + end_time = Column(Time, nullable=False) + + # Status + status = Column(Enum(ShiftStatus), nullable=False, default=ShiftStatus.scheduled) + + # Actual times (for tracking) + actual_start_time = Column(DateTime, nullable=True) + actual_end_time = Column(DateTime, nullable=True) + + # Break times + break_duration_minutes = Column(Integer, nullable=True, default=30) # Total break time + + # Assignment + assigned_by = Column(Integer, ForeignKey('users.id'), nullable=True) # Admin/staff who assigned + department = Column(String(100), nullable=True) # reception, housekeeping, maintenance, etc. + + # Notes + notes = Column(Text, nullable=True) + handover_notes = Column(Text, nullable=True) # Notes for next shift + + # Performance tracking + tasks_completed = Column(Integer, nullable=True, default=0) + tasks_assigned = Column(Integer, nullable=True, default=0) + + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + staff = relationship('User', foreign_keys=[staff_id]) + assigner = relationship('User', foreign_keys=[assigned_by]) + tasks = relationship('StaffTask', back_populates='shift', cascade='all, delete-orphan') + +class StaffTaskPriority(str, enum.Enum): + low = 'low' + normal = 'normal' + high = 'high' + urgent = 'urgent' + +class StaffTaskStatus(str, enum.Enum): + pending = 'pending' + assigned = 'assigned' + in_progress = 'in_progress' + completed = 'completed' + cancelled = 'cancelled' + on_hold = 'on_hold' + +class StaffTask(Base): + __tablename__ = 'staff_tasks' + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + shift_id = Column(Integer, ForeignKey('staff_shifts.id'), nullable=True, index=True) + staff_id = Column(Integer, ForeignKey('users.id'), nullable=False, index=True) + + # Task details + title = Column(String(255), nullable=False) + description = Column(Text, nullable=True) + task_type = Column(String(100), nullable=False) # housekeeping, maintenance, guest_service, etc. + priority = Column(Enum(StaffTaskPriority), nullable=False, default=StaffTaskPriority.normal) + status = Column(Enum(StaffTaskStatus), nullable=False, default=StaffTaskStatus.pending) + + # Timing + scheduled_start = Column(DateTime, nullable=True) + scheduled_end = Column(DateTime, nullable=True) + actual_start = Column(DateTime, nullable=True) + actual_end = Column(DateTime, nullable=True) + estimated_duration_minutes = Column(Integer, nullable=True) + actual_duration_minutes = Column(Integer, nullable=True) + + # Assignment + assigned_by = Column(Integer, ForeignKey('users.id'), nullable=True) + due_date = Column(DateTime, nullable=True) + + # Related entities + related_booking_id = Column(Integer, ForeignKey('bookings.id'), nullable=True) + related_room_id = Column(Integer, ForeignKey('rooms.id'), nullable=True) + related_guest_request_id = Column(Integer, ForeignKey('guest_requests.id'), nullable=True) + related_maintenance_id = Column(Integer, ForeignKey('room_maintenance.id'), nullable=True) + + # Notes and completion + notes = Column(Text, nullable=True) + completion_notes = Column(Text, nullable=True) + is_recurring = Column(Boolean, nullable=False, default=False) + recurrence_pattern = Column(String(100), nullable=True) # daily, weekly, monthly + + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + + # Relationships + staff = relationship('User', foreign_keys=[staff_id]) + assigner = relationship('User', foreign_keys=[assigned_by]) + shift = relationship('StaffShift', back_populates='tasks') + diff --git a/Backend/src/hotel_services/routes/__init__.py b/Backend/src/hotel_services/routes/__init__.py index e69de29b..e2a8be14 100644 --- a/Backend/src/hotel_services/routes/__init__.py +++ b/Backend/src/hotel_services/routes/__init__.py @@ -0,0 +1,2 @@ +from . import inventory_routes, guest_request_routes, staff_shift_routes + diff --git a/Backend/src/hotel_services/routes/__pycache__/__init__.cpython-312.pyc b/Backend/src/hotel_services/routes/__pycache__/__init__.cpython-312.pyc index 6fb3ef38e365637ab4969ce4f3512d0eab92e353..3500959363799bfe717fc88b421dccaaef274d5f 100644 GIT binary patch delta 195 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 literal 0 HcmV?d00001 diff --git a/Backend/src/hotel_services/routes/__pycache__/inventory_routes.cpython-312.pyc b/Backend/src/hotel_services/routes/__pycache__/inventory_routes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..32ad694ebc57a31872f6bc550b81d0b21c4199ce GIT binary patch literal 28204 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 delta 1448 zcmZvbNo-SB7=`mlWoWF}hIqC!*v>!_$HWPNFw+1DW0*pKKM9GkO)%VJ2ir{=CKD)- z0)>X3p@cC&>7puASXAwn6&1Tyl_HfzSCv{+v1lu`o4$Khy6ThO(Ra@I?|n~i_m9{D_6&=_{+t5j!*iPHA zBS$;1lXilL&_!M7rf&35j~Xr$yU(f~T?7;35?#V&x{NDy1y|{+nyD9` z;u>AUIE~{vUB?Z&ftz#_x9Apb({0?LJD8ve+@-s?NB3}_?yGfSG2=5lpa=Mz_)Wg8 zMuiRHOFX29ctnrzm>%N^J;76Ys^#_P#TxHwG&wxCx@q##JEyeCKMbs~+puub^X^M+ z=9IZ39Z6;)(QG1>>=+)1Prfb~&(}tH)YQpK%yseGLlbVSN- zDN!jgDaBGsq?Af2lj7qp%T6}C{rsCHkl$YYY;xbs9*uu2a%oonv#3(Dafvm+ZRTR$ zWpx|8`kJXKKIZcAZR>Q~|L^#XwbXPgSJ{jrL3y>vBfb> z-zsdg^Q&32j6s>!NeS^UHjihHj#EX^R5BY+W~eVds5$7JGDQ+^N27Zq*>sD%&I&0j`G}*`wn}2Flr|}=rL5s64xeGI#C1Aa`Bz7w zwx0iTgp3g7YOu%%DpfA4*&LXMg;@qXwJ@ZYb1e|A-I zuf@+{cO@UUG#Ccunjya6F6STg__H-3|tyh|UW?DJxY3Api7OwKXD5x+_ L8=0Q|N-zEadT*-> diff --git a/Backend/src/payments/routes/__pycache__/audit_trail_routes.cpython-312.pyc b/Backend/src/payments/routes/__pycache__/audit_trail_routes.cpython-312.pyc index 17e5ff5d0bb7a25ef355dc3682154c254bde9eee..685956e7a5eb21d60533302522e5e7bedbf0a85c 100644 GIT binary patch delta 3653 zcmbtXYitwQ6}~g$@pC+Wj6Jr;j$`L(0~=yq5j4sBw3!ZpjkI}oEpY4Tu*?6 zWQ`EpLRD!&^P{w-rTb^aibzGu&;Hq!N|!3IYb%jRE!s_`R_ebRHkDRTq24==6IiiY zwRDZ{)v-YGNV-qwheAbb4=-8q-v#z8|$5zFi6_SFEZHg!BO?olrpj{EOzN9bfPx^J9 zR{~inDd~0&C72B*LlmHa?2_wlQ=mXn0?9BI(9j88qtuN1HmwUmvRynnuv~x=3~B?kx8SHHmA1i<%*oLJ zHiUJnD7=d5F>27YkyTlARo1YI8s#W#!k=c@3tMDUT7<2a*xS|cAldkGslqFA0Asa8 zaC$GLxPC1~8pdC6Y=rAxaNIQEiLckL(wSzu>>NP3QIyJNGh?IO2<1@O*?&q#DK(QD zODVc!_Dheu-=KkZfb z)Vxl4Yjv3xp<_xs1M&vwED(S;t6ep-6FoJw_6=QA;;+E3>6!xysH=}Q3do~DEB+&o z=7z!z26|@r+SwVQhwxpS_P*mi0A7f)cg1`4HdGn-%4|tsD9-q54smc>Ej8$eff-{> zbCIeBI!mc<)+}(1w&E(RJzg#Fiop)@4W(C==^FwEHP#k2hbA}n$Z905z0diyZrcGf zIoEM3ifjA%){a)pl#RJ`Ixnjq@?|;$Q3DaVQzPUA4MRCUf-MB7{V>=Qf!~ zq$k7;sD+4HHHACve`<$0IX~8_M&xsuyjo@trhcf%Pg}L$a-yIT92v{uWOCzUa7X0n zJ|cf}O+JAW9YJzRm0??CK#@~lLRJ*vTVR!<;fky zU3(_4$4H$?WfVPPY%Rcw^Y~<jWF+Cl_d3E4(S2T}2< zahOquv$C2Z&x*Da4SDlrOD3O7=TJ63B9ZF~X*R-yjx@m;61 z?_IVm+Je_7O19Xoo;wqz4X;1q-0$tbvcFi@_0iBreZ}Cw0yp?bXk2P+UWzm=Mq3u6 z4U4U@#}>9x+*{#5-AU@P%Obif9uPQ0E&DhR|8KzA`DF=M9Pe>gxT3H3zHo1FPP}I> zdIyWg-oDH&m{X7V;4P-O<-`JivS>c}OhfLSg&*UyaFOb7pf2qe_c_7GL0g{$K5psS zgqc4Ex;FH6fH~HKnYjS5nv@y!k;}JC=MJ=CI_`b?yBU^O;p*G4okH)8C*iyW8n@!)%N22fCoPV7e za5rr=B6%c-p!P_N(vustpgmrwO5$O3fNUWR4ILze^itd=GYGdYy~UC?qc>O7Pte!3 zRy07guV}1jkSJEWDSC^rW-`*N&GIloM+iAe$Pgh(OyWi~N^rF?67)79BZNRq@W#_1@4p*Ca_2JM7EdC`ieTaV;SGy$N#Ew#nE@3w0cTt0D*V@Ep|F>)HuD`XX z0>JhjdKmzrm&VxMOKa!dP3DseyiznP&$JDJ&Q3C5E>gWBb*bCl?FM&U1mAW0dj;n1 zy4|rJH}i>`!^|h)_FkU3$FmsU6Buk1Z`OVu=#26C(@IJi`H`HRXp_&4tc{tnQJpx2 zX9^;}_3)JZTOdT)wda8bir1o2m~v>ZOCsgejz|qg{rw7BL298q+6PkOA$>%dhlj^f zS$TL^uYcKya^qy6F}RRrCWA+s{%**u(M$Ou_yR+qSNo~CGn<3sio6R=V;T9!Bma9X z9P~4vgPq#8;QNK$G?k_no&M{>HG758xuq43)wzlhxB|M?ROQXUEnRQCR#&l9c`M+> zt0PyAS8P?DtJ&MDJP#bctNAP0ilfRqfsL=Ys@Rk-bz~X%3)A1J! upj{QZD(^LDKMA!Y)=~I)INTM6fF-wBVF|#g;$0>J$)?a_nm{y#pZdSNN?rs2 delta 2243 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 delta 2999 zcmaJ?Yit|G5x%|S@g<5hsRt!Xv>ueD(0bUC^@=Rn78To-pRpBNb`pmoc_)Q7AIkES z!Zsb%snN!%;o98*Et(bx@}o#9BT>pfPJyD4(x3s_A5`Tw?Li3A{)&VCpl!4Wj27t3 z9x17YS_1rbc4u~P=bPD?Z(kw5ns)xKwA3!ZFZY|aShVGqvx>a-K}(M)&JlKl_*tD~ zb_LWX#jlntO{#U$s%&GQiak=R()I%>0IIF4D!bCo`Xzrw#~i@}1wj;45&j;fV7y&z zUT3_6y(9IIR`$8Hm9>lZ($1^$oUu=Mwuf~Puhdnz!*ixhWG9Q8{MFqX9NnX|%}MMF zsgC{DS}pTG**eARTTA#8MI+v8L_aAK%Hnk%Kl+H!*1BTC4mxJ1y;HkQ`jr^ zVGEUE7O=AgyT5{VqQ*?s^h{jSiJHcnF{$KP-!2d+0`5(}0q~{p^|yq)$s!M~u&+vP zb_VOn)nQ>+78XndnaZ%oWDW+Ve18mMwO>VGH9C1>Y z9Vl%fL-*bQRvx)WlP{-YQMEpq(&{HuGf9Q^;n(qQ_ZV2{w*4s@i7ASj)GaZ?n0B!x z=T>t!8uqe(IU8pOP~40#h%kh(8(|N^UVwLH%8fd3VLQSoLJ%O0q}1b&j)0`Qr&KLe z_*5uzArw>SVOaO0=^cOq84e)BQ~#N#ZOOxa>Z;kc;%a^?Gk@Z)tN+gUM`xCXj^)Ne zpN&P9#v*?himZ6*-Wj=5a@X@z&hZrc(iLlwfkEGJqp;9GMr5+k=ot~Eg>GPQNLn0{ zVe+vkfg=0%rr(>I55Q;XHxM{G`#^f+4yID)W67yC>e%&)pOJC4vvP`zvNtQ6Ap1Y6 zd}0=Px_1+}q`e@$D+C&8H6Dd-(`^8{{L+j{FVISq&6BaXrc%z9E>Fg2TBA>+<#B`) z2xAD(Ae=-vg)okA8sQ9pE&+$SbDE|SDK_ElfBr0LUO)&TV5oHasc?Ko-A8GPZWJ*d z%7B9gaKjOVwb=1nTOtMlEbraXFo6bZA$7o%hqM56gbi1fSG@$wJK>+MfoD{RV>`q# zo2WW0mg#a>NyL)6JsgcfDr(`Rmi=(^=VA%+P4^@8vA2Ew<_Q!>*%!XXgJBRaR&gil zXQq{~h7l1&W-H^|`jk1@hs zpJ2b<;`j5M#>c!y08eas0S`w|vU1&$)_h5U8vo*&Pt|L=rtof8>6V=b%`{78gL2;31HNUM$*e$1j00ewk7 z6te&JcM`1 zx%D#cuRvS&^IE%P*1W9ExzlYqO4sE9Y+z4sYnY9Iz+(%Ux?xyRkF4lsMUAVPs+*&! zL?Wg=Uat0{;UR#>%aPOASFA|%D%gAv?S2AaEhKRX%eU^{`e&>C`?hPgn=N;(n+x*w zk`-57?uq?(T?ca317EL{RIw-9yV~$ky-G$TC_=k~lCUVEyeN4_1ZlCNIk-z&+$Dqj zu^^2|**~_Q5}Ud2$6%XLf>tY1Ks3rJK1nFd$9J^yf@5&9$>5|Cb?Z_cr^pF!PQ2Dr zFHU*m!P@XY%Xq-dE7rQ4Nuc>K0#6rc!L5aSF{-(DT{TZ*IWJTV`W@Kw<7{(hGqLG% zS_@B3MonzqQJ#%-%}f9Fe*mDNNz7T29ci{wlq0>u}5mu`wBCsU87$!bG`7l=XJu&Z4Ig-5rY zOsn)r3et!2yIf>{?`!ViER}-6>Z3xz;L4L$lS(nsLi}p?u2G6IOrQa4SfpmAWm)urhf&8R*6;l7H7n8Z8+{Cr*)AHtHr z@A1ewm`EuzadkI+AJ(9A3F-TiAmk-N2s7Uz zLUBIJ3&L^n8L=qG#nh@iE41O5;bbNW}9fIVcx`t-1E@~IK?BX@*+t2TK37FUz*+n2LA&wzPU;O diff --git a/Backend/src/payments/routes/__pycache__/payment_routes.cpython-312.pyc b/Backend/src/payments/routes/__pycache__/payment_routes.cpython-312.pyc index 381dc70e4359846187b6626313deca60300e7afa..039143d3d2f75c906cb0ed209d808611e48cd88a 100644 GIT binary patch delta 4657 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 start) - if end: - total_query = total_query.filter(FinancialAuditTrail.created_at <= end) - - total_count = total_query.count() - total_pages = (total_count + limit - 1) // limit + try: + total_query = db.query(FinancialAuditTrail) + if payment_id: + total_query = total_query.filter(FinancialAuditTrail.payment_id == payment_id) + if invoice_id: + total_query = total_query.filter(FinancialAuditTrail.invoice_id == invoice_id) + if booking_id: + total_query = total_query.filter(FinancialAuditTrail.booking_id == booking_id) + if action_type_enum: + total_query = total_query.filter(FinancialAuditTrail.action_type == action_type_enum) + if user_id: + total_query = total_query.filter(FinancialAuditTrail.performed_by == user_id) + if start: + total_query = total_query.filter(FinancialAuditTrail.created_at >= start) + if end: + total_query = total_query.filter(FinancialAuditTrail.created_at <= end) + + total_count = total_query.count() + total_pages = (total_count + limit - 1) // limit + except (ProgrammingError, OperationalError): + # If table doesn't exist, count is 0 + total_count = 0 + total_pages = 0 return success_response( data={ @@ -120,6 +126,26 @@ async def get_financial_audit_trail( ) except HTTPException: raise + except (ProgrammingError, OperationalError) as e: + # Handle case where financial_audit_trail table doesn't exist + error_msg = str(e) + if 'doesn\'t exist' in error_msg or 'Table' in error_msg: + logger.warning(f'Financial audit trail table not found: {error_msg}') + return success_response( + data={ + 'audit_trail': [], + 'pagination': { + 'page': page, + 'limit': limit, + 'total': 0, + 'total_pages': 0 + }, + 'note': 'Financial audit trail table not yet created. Please run database migrations.' + }, + message='Financial audit trail is not available (table not created)' + ) + logger.error(f'Database error retrieving financial audit trail: {str(e)}', exc_info=True) + raise HTTPException(status_code=500, detail='Database error occurred while retrieving audit trail') except Exception as e: logger.error(f'Error retrieving financial audit trail: {str(e)}', exc_info=True) raise HTTPException(status_code=500, detail='An error occurred while retrieving audit trail') @@ -158,6 +184,14 @@ async def get_audit_record( ) except HTTPException: raise + except (ProgrammingError, OperationalError) as e: + # Handle case where financial_audit_trail table doesn't exist + error_msg = str(e) + if 'doesn\'t exist' in error_msg or 'Table' in error_msg: + logger.warning(f'Financial audit trail table not found: {error_msg}') + raise HTTPException(status_code=404, detail='Financial audit trail table not yet created. Please run database migrations.') + logger.error(f'Database error retrieving audit record: {str(e)}', exc_info=True) + raise HTTPException(status_code=500, detail='Database error occurred while retrieving audit record') except Exception as e: logger.error(f'Error retrieving audit record: {str(e)}', exc_info=True) raise HTTPException(status_code=500, detail='An error occurred while retrieving audit record') diff --git a/Backend/src/payments/routes/invoice_routes.py b/Backend/src/payments/routes/invoice_routes.py index b73e4c29..711f2ac1 100644 --- a/Backend/src/payments/routes/invoice_routes.py +++ b/Backend/src/payments/routes/invoice_routes.py @@ -24,6 +24,14 @@ router = APIRouter(prefix='/invoices', tags=['invoices']) @router.get('/') async def get_invoices(request: Request, booking_id: Optional[int]=Query(None), status_filter: Optional[str]=Query(None, alias='status'), page: int=Query(1, ge=1), limit: int=Query(10, ge=1, le=100), current_user: User=Depends(get_current_user), db: Session=Depends(get_db)): try: + # SECURITY: Verify booking ownership when booking_id is provided + if booking_id and not can_access_all_invoices(current_user, db): + booking = db.query(Booking).filter(Booking.id == booking_id).first() + if not booking: + raise HTTPException(status_code=404, detail='Booking not found') + if booking.user_id != current_user.id: + raise HTTPException(status_code=403, detail='Forbidden: You do not have permission to access invoices for this booking') + user_id = None if can_access_all_invoices(current_user, db) else current_user.id result = InvoiceService.get_invoices(db=db, user_id=user_id, booking_id=booking_id, status=status_filter, page=page, limit=limit) return success_response(data=result) @@ -38,8 +46,10 @@ async def get_invoice_by_id(id: int, current_user: User=Depends(get_current_user invoice = InvoiceService.get_invoice(id, db) if not invoice: raise HTTPException(status_code=404, detail='Invoice not found') - if not can_access_all_invoices(current_user, db) and invoice['user_id'] != current_user.id: - raise HTTPException(status_code=403, detail='Forbidden') + # SECURITY: Verify invoice ownership for non-admin/accountant users + if not can_access_all_invoices(current_user, db): + if 'user_id' not in invoice or invoice['user_id'] != current_user.id: + raise HTTPException(status_code=403, detail='Forbidden: You do not have permission to access this invoice') return success_response(data={'invoice': invoice}) except HTTPException: raise @@ -48,9 +58,10 @@ async def get_invoice_by_id(id: int, current_user: User=Depends(get_current_user logger.error(f'Error fetching invoice by id: {str(e)}', exc_info=True) raise HTTPException(status_code=500, detail=str(e)) -@router.post('/') -async def create_invoice(request: Request, invoice_data: CreateInvoiceRequest, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)): +@router.post('/', dependencies=[Depends(authorize_roles('admin', 'staff', 'accountant'))]) +async def create_invoice(request: Request, invoice_data: CreateInvoiceRequest, current_user: User=Depends(authorize_roles('admin', 'staff', 'accountant')), db: Session=Depends(get_db)): try: + # Defense in depth: Additional business logic check (authorize_roles already verified) if not can_create_invoices(current_user, db): raise HTTPException(status_code=403, detail='Forbidden') booking_id = invoice_data.booking_id @@ -137,13 +148,46 @@ async def mark_invoice_as_paid(request: Request, id: int, payment_data: MarkInvo raise HTTPException(status_code=500, detail=str(e)) @router.delete('/{id}') -async def delete_invoice(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)): +async def delete_invoice(id: int, request: Request, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)): + client_ip = request.client.host if request.client else None + user_agent = request.headers.get('User-Agent') + request_id = get_request_id(request) + try: invoice = db.query(Invoice).filter(Invoice.id == id).first() if not invoice: raise HTTPException(status_code=404, detail='Invoice not found') + + # Capture invoice info before deletion for audit + deleted_invoice_info = { + 'invoice_id': invoice.id, + 'invoice_number': invoice.invoice_number, + 'user_id': invoice.user_id, + 'booking_id': invoice.booking_id, + 'total_amount': float(invoice.total_amount) if invoice.total_amount else 0.0, + 'status': invoice.status.value if hasattr(invoice.status, 'value') else str(invoice.status), + } + db.delete(invoice) db.commit() + + # SECURITY: Log invoice deletion for audit trail + try: + await audit_service.log_action( + db=db, + action='invoice_deleted', + resource_type='invoice', + user_id=current_user.id, + resource_id=id, + ip_address=client_ip, + user_agent=user_agent, + request_id=request_id, + details=deleted_invoice_info, + status='success' + ) + except Exception as e: + logger.warning(f'Failed to log invoice deletion audit: {e}') + return success_response(message='Invoice deleted successfully') except HTTPException: raise diff --git a/Backend/src/payments/routes/payment_routes.py b/Backend/src/payments/routes/payment_routes.py index dc1753cf..f2bd2dfe 100644 --- a/Backend/src/payments/routes/payment_routes.py +++ b/Backend/src/payments/routes/payment_routes.py @@ -79,6 +79,14 @@ async def cancel_booking_on_payment_failure(booking: Booking, db: Session, reaso @router.get('/') async def get_payments(booking_id: Optional[int]=Query(None), status_filter: Optional[str]=Query(None, alias='status'), page: int=Query(1, ge=1), limit: int=Query(10, ge=1, le=100), current_user: User=Depends(get_current_user), db: Session=Depends(get_db)): try: + # SECURITY: Verify booking ownership when booking_id is provided + if booking_id and not can_access_all_payments(current_user, db): + booking = db.query(Booking).filter(Booking.id == booking_id).first() + if not booking: + raise HTTPException(status_code=404, detail='Booking not found') + if booking.user_id != current_user.id: + raise HTTPException(status_code=403, detail='Forbidden: You do not have permission to access payments for this booking') + if booking_id: query = db.query(Payment).filter(Payment.booking_id == booking_id) else: @@ -168,12 +176,16 @@ async def get_payments_by_booking_id(booking_id: int, current_user: User=Depends @router.get('/{id}') async def get_payment_by_id(id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)): try: - payment = db.query(Payment).filter(Payment.id == id).first() + # SECURITY: Load booking relationship to verify ownership + payment = db.query(Payment).options(joinedload(Payment.booking)).filter(Payment.id == id).first() if not payment: raise HTTPException(status_code=404, detail='Payment not found') + # SECURITY: Verify payment ownership for non-admin/accountant users if not can_access_all_payments(current_user, db): - if payment.booking and payment.booking.user_id != current_user.id: - raise HTTPException(status_code=403, detail='Forbidden') + if not payment.booking: + raise HTTPException(status_code=403, detail='Forbidden: Payment does not belong to any booking') + if payment.booking.user_id != current_user.id: + raise HTTPException(status_code=403, detail='Forbidden: You do not have permission to access this payment') payment_dict = {'id': payment.id, 'booking_id': payment.booking_id, 'amount': float(payment.amount) if payment.amount else 0.0, 'payment_method': payment.payment_method.value if isinstance(payment.payment_method, PaymentMethod) else payment.payment_method, 'payment_type': payment.payment_type.value if isinstance(payment.payment_type, PaymentType) else payment.payment_type, 'deposit_percentage': payment.deposit_percentage, 'related_payment_id': payment.related_payment_id, 'payment_status': payment.payment_status.value if isinstance(payment.payment_status, PaymentStatus) else payment.payment_status, 'transaction_id': payment.transaction_id, 'payment_date': payment.payment_date.isoformat() if payment.payment_date else None, 'notes': payment.notes, 'created_at': payment.created_at.isoformat() if payment.created_at else None} if payment.booking: payment_dict['booking'] = {'id': payment.booking.id, 'booking_number': payment.booking.booking_number} diff --git a/Backend/src/payments/services/__pycache__/financial_audit_service.cpython-312.pyc b/Backend/src/payments/services/__pycache__/financial_audit_service.cpython-312.pyc index 1f9773eb894e0c09b0c49b1056c8794829c46ce7..95c366108cc4cd9e8fab0083c732a0471e997402 100644 GIT binary patch delta 2063 zcma)7&2JM&6rc5a*Is+=U4O+%jFTk^Nelsk1BH+fpd^8&l#d3a#)k&u-2}th4znAQ zpqNn9L+PcmRuw^rHm6E0v_i_I>W5Sbf}+xct>zF}k(x^V0~S!#iXPf|ORNB@)UM<= zzu%iTZ{E!OW_+*d$1UDR9*+x=@9Jmm=`Z{RZ#DHvp`~$#LNX=WRW{>DI5N(JGs7h~ z%g3m$j4RJHsdVj3?nC+$nRaHzOnj3%gV?<4gD`WJ8Ki@!z3f5`+=~8#;_+ z_xniZ@7UISK@t0CCc zTAq0?^{I_on*WOalJe_yYgy2UtqI|wykNY?1l7DKv(w@wq}gQ$v^c6)RRYg9R?vDY zJEz54=(ep=HHC)Ae&mHK!W8ww$W7}bDoEzYgf)jr!?sdU<8`*H=9x#%DO$Egmz2r0rk9yh$+N0* zd4GRlHMC0yEM;@LG?E+7%59Q%Aw4!mwo^)u%V}LwbEBh#?UgQX?@f~6q^3xUag~i5 zdA8LZ3znIS;|iX_UgLYVj&+mzF=zbBwzXk9V1}47t?9Vzm`LI*$y8<(>!7&4$&@k% z2Nm-f?T)wuZ-4<&W7yH`D|1%Ra7G)|e8epoA3E-O{RC;piBhg2Q))JPo!u-4|>hyep`@v@8ka3ciW*U(W zi?qI1q_`*V);CuArri}~<$0N!<}GE~pvCoiLnZ3`zv+vf=3iFkC`#FgI_-J)m9Tpd z#0JnkK~11ZdI(KWF~-pN77uPBc`>BC+ycswUQxzg=~yg)TOi0f9Eh-9B*lfY}e`02s*e zeDY2}dx_CJ#7HlGxXbA9bg=`_IX16*s;L>%?%@2(f$AHbg-)ZZ$k+Y3vMw3se@N!Y zV$GtvSbu6sJiQ#Q`IuWKsxaMTD=T zXX)#Cvj$i#az&p+2zp8*)^FOtmo;{ZjWexlwUc%=xWTH9A(+&^R;vS;v%)xFE>u-l ztlLov#Z5PG9{Fo$8w*j>1I${2Ap6j4od!Y8|>I4>38epWBwn$%?9N8`sDi=ruH7gCfA_BVep61Lt23 zkEFB7Y$}~phpm@-n1o+Srxbhw;;h0!EYFv$>WJM5R^$%6!q9gS1u${h97X{x9hqv8ytH&!FkA!&M9 z9hWtJ9#QD)>P%^%U3_aLy4tQa8v`jKq)7&1t4%64v0;R({-J$GHRGWm$0P48q!%o-ZE1^G+|HF3=Ed!KX%W-Nm^V^9FYBiX`63z&rpS0Q!?yAucdz$3Sh8Fd{F$+!>_8sI@{KgUlRc z#N{}y7zh6U?~F6B^DMxl00)J6pkSb7a46n;$Si%_o|Df*YGUiUox{$q<$ct-+4*Gl{QEMx%MA08>$+$TVO&@(aDC?o-SFl^&`LD08SR zP&RVLdVbep4k#DpC)e-4b%bR>c_*}p@efL2&3h^AW zb3O6qH8ut+kMzQwfU|uO$u1Kh_5%GnTUwitg2X#tcfI=(cM|8^|Djg}lsZ%zC>@QA z?~I@9`=^)zI*r_XuhcED45;KL&-c9tm<7@XyJ!1unK>X`WM^3xn1>vX9rJ;XAl=+C zC;Hmtol&k6lY0VZ2R(;&KV8X^OZ0{}mXq6^tF^`}$pHT|IfdT#CJV{@l1baktL;~h z*PCG!w~nv2_^*?ux<6@WV0H@tN0TYidA~6GB+$F_{ac;qb*pi7jy%H?n7=KkU?RqN iPY`hGFB#zy?fT=vvls_^D8Yt+f)l^wPxjGha@M~T2prr1 diff --git a/Backend/src/payments/services/financial_audit_service.py b/Backend/src/payments/services/financial_audit_service.py index 44e9f36e..50194c10 100644 --- a/Backend/src/payments/services/financial_audit_service.py +++ b/Backend/src/payments/services/financial_audit_service.py @@ -2,6 +2,7 @@ Service for creating and managing financial audit trail records. """ from sqlalchemy.orm import Session +from sqlalchemy.exc import ProgrammingError, OperationalError from typing import Optional, Dict, Any from datetime import datetime from ..models.financial_audit_trail import FinancialAuditTrail, FinancialActionType @@ -56,6 +57,15 @@ class FinancialAuditService: logger.info(f"Financial audit trail created: {action_type.value} by user {performed_by}") return audit_record + except (ProgrammingError, OperationalError) as e: + error_msg = str(e) + if 'doesn\'t exist' in error_msg or 'Table' in error_msg: + logger.warning(f"Financial audit trail table not found, skipping audit logging: {error_msg}") + # Don't fail the main operation if audit table doesn't exist + return None + logger.error(f"Database error creating financial audit trail: {str(e)}", exc_info=True) + # Don't fail the main operation if audit logging fails + raise except Exception as e: logger.error(f"Error creating financial audit trail: {str(e)}", exc_info=True) # Don't fail the main operation if audit logging fails @@ -95,7 +105,14 @@ class FinancialAuditService: query = query.order_by(FinancialAuditTrail.created_at.desc()) query = query.limit(limit).offset(offset) - return query.all() + try: + return query.all() + except (ProgrammingError, OperationalError) as e: + error_msg = str(e) + if 'doesn\'t exist' in error_msg or 'Table' in error_msg: + logger.warning(f"Financial audit trail table not found: {error_msg}") + return [] + raise financial_audit_service = FinancialAuditService() diff --git a/Backend/src/reviews/routes/__pycache__/favorite_routes.cpython-312.pyc b/Backend/src/reviews/routes/__pycache__/favorite_routes.cpython-312.pyc index c2b682aa0a58967bec877da1a9a774aa9af95308..4d4706a88698cd24c2b6c0952d637edc6f9c52e0 100644 GIT binary patch literal 10585 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 diff --git a/Backend/src/reviews/routes/__pycache__/review_routes.cpython-312.pyc b/Backend/src/reviews/routes/__pycache__/review_routes.cpython-312.pyc index dfb109676d0925c56b8cbf3f629418a6e72d32de..67fe535778840f8a6730487338932503793ed8ef 100644 GIT binary patch literal 21973 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�C+N^+pSg0=zY4=BmkKB@yOL51=dM5+n8 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 X8LEJ65mi8rRvaexMPTW5aN@rKgigmh diff --git a/Backend/src/reviews/routes/favorite_routes.py b/Backend/src/reviews/routes/favorite_routes.py index 4775479a..3d615689 100644 --- a/Backend/src/reviews/routes/favorite_routes.py +++ b/Backend/src/reviews/routes/favorite_routes.py @@ -13,8 +13,15 @@ router = APIRouter(prefix='/favorites', tags=['favorites']) @router.get('/') async def get_favorites(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)): - role = db.query(Role).filter(Role.id == current_user.role_id).first() - if role and role.name in ['admin', 'staff', 'accountant']: + # PERFORMANCE: Use eager-loaded role relationship if available + if hasattr(current_user, 'role') and current_user.role is not None: + role_name = current_user.role.name + else: + # Fallback: query if relationship wasn't loaded + role = db.query(Role).filter(Role.id == current_user.role_id).first() + role_name = role.name if role else 'customer' + + if role_name in ['admin', 'staff', 'accountant']: raise HTTPException(status_code=403, detail='Admin, staff, and accountant users cannot have favorites') try: favorites = db.query(Favorite).filter(Favorite.user_id == current_user.id).order_by(Favorite.created_at.desc()).all() @@ -35,8 +42,15 @@ async def get_favorites(current_user: User=Depends(get_current_user), db: Sessio @router.post('/{room_id}') async def add_favorite(room_id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)): - role = db.query(Role).filter(Role.id == current_user.role_id).first() - if role and role.name in ['admin', 'staff', 'accountant']: + # PERFORMANCE: Use eager-loaded role relationship if available + if hasattr(current_user, 'role') and current_user.role is not None: + role_name = current_user.role.name + else: + # Fallback: query if relationship wasn't loaded + role = db.query(Role).filter(Role.id == current_user.role_id).first() + role_name = role.name if role else 'customer' + + if role_name in ['admin', 'staff', 'accountant']: raise HTTPException(status_code=403, detail='Admin, staff, and accountant users cannot add favorites') try: room = db.query(Room).filter(Room.id == room_id).first() @@ -58,8 +72,15 @@ async def add_favorite(room_id: int, current_user: User=Depends(get_current_user @router.delete('/{room_id}') async def remove_favorite(room_id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)): - role = db.query(Role).filter(Role.id == current_user.role_id).first() - if role and role.name in ['admin', 'staff', 'accountant']: + # PERFORMANCE: Use eager-loaded role relationship if available + if hasattr(current_user, 'role') and current_user.role is not None: + role_name = current_user.role.name + else: + # Fallback: query if relationship wasn't loaded + role = db.query(Role).filter(Role.id == current_user.role_id).first() + role_name = role.name if role else 'customer' + + if role_name in ['admin', 'staff', 'accountant']: raise HTTPException(status_code=403, detail='Admin, staff, and accountant users cannot remove favorites') try: favorite = db.query(Favorite).filter(Favorite.user_id == current_user.id, Favorite.room_id == room_id).first() @@ -76,8 +97,15 @@ async def remove_favorite(room_id: int, current_user: User=Depends(get_current_u @router.get('/check/{room_id}') async def check_favorite(room_id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)): - role = db.query(Role).filter(Role.id == current_user.role_id).first() - if role and role.name in ['admin', 'staff', 'accountant']: + # PERFORMANCE: Use eager-loaded role relationship if available + if hasattr(current_user, 'role') and current_user.role is not None: + role_name = current_user.role.name + else: + # Fallback: query if relationship wasn't loaded + role = db.query(Role).filter(Role.id == current_user.role_id).first() + role_name = role.name if role else 'customer' + + if role_name in ['admin', 'staff', 'accountant']: return {'status': 'success', 'data': {'isFavorited': False}} try: favorite = db.query(Favorite).filter(Favorite.user_id == current_user.id, Favorite.room_id == room_id).first() diff --git a/Backend/src/reviews/routes/review_routes.py b/Backend/src/reviews/routes/review_routes.py index 417ba231..359aa48c 100644 --- a/Backend/src/reviews/routes/review_routes.py +++ b/Backend/src/reviews/routes/review_routes.py @@ -1,7 +1,7 @@ -from fastapi import APIRouter, Depends, HTTPException, status, Query +from fastapi import APIRouter, Depends, HTTPException, status, Query, Request from ...shared.utils.response_helpers import success_response, error_response from sqlalchemy.orm import Session, joinedload -from sqlalchemy import func +from sqlalchemy import func, and_ from typing import Optional from ...shared.config.database import get_db from ...shared.config.logging_config import get_logger @@ -9,7 +9,9 @@ from ...security.middleware.auth import get_current_user, authorize_roles from ...auth.models.user import User from ..models.review import Review, ReviewStatus from ...rooms.models.room import Room +from ...bookings.models.booking import Booking, BookingStatus from ..schemas.review import CreateReviewRequest +from ...analytics.services.audit_service import audit_service logger = get_logger(__name__) router = APIRouter(prefix='/reviews', tags=['reviews']) @@ -124,7 +126,11 @@ async def get_all_reviews(status_filter: Optional[str]=Query(None, alias='status raise HTTPException(status_code=500, detail=str(e)) @router.post('/') -async def create_review(review_data: CreateReviewRequest, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)): +async def create_review(review_data: CreateReviewRequest, request: Request, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)): + client_ip = request.client.host if request.client else None + user_agent = request.headers.get('User-Agent') + request_id = getattr(request.state, 'request_id', None) + try: room_id = review_data.room_id rating = review_data.rating @@ -145,6 +151,25 @@ async def create_review(review_data: CreateReviewRequest, current_user: User=Dep detail=error_response(message='You have already reviewed this room') ) + # BUSINESS RULE: Verify user has actually stayed in this room + # Users can only review rooms they've booked and checked out from + from ...shared.utils.role_helpers import is_admin, is_staff + if not (is_admin(current_user, db) or is_staff(current_user, db)): + # Check if user has a checked-out booking for this room + past_booking = db.query(Booking).filter( + and_( + Booking.user_id == current_user.id, + Booking.room_id == room_id, + Booking.status == BookingStatus.checked_out + ) + ).first() + + if not past_booking: + raise HTTPException( + status_code=403, + detail=error_response(message='You can only review rooms you have stayed in. Please complete a booking and check out before leaving a review.') + ) + review = Review( user_id=current_user.id, room_id=room_id, @@ -156,6 +181,28 @@ async def create_review(review_data: CreateReviewRequest, current_user: User=Dep db.commit() db.refresh(review) + # SECURITY: Log review creation for audit trail + try: + await audit_service.log_action( + db=db, + action='review_created', + resource_type='review', + user_id=current_user.id, + resource_id=review.id, + ip_address=client_ip, + user_agent=user_agent, + request_id=request_id, + details={ + 'room_id': review.room_id, + 'rating': review.rating, + 'status': 'pending', + 'comment_length': len(review.comment) if review.comment else 0 + }, + status='success' + ) + except Exception as e: + logger.warning(f'Failed to log review creation audit: {e}') + review_dict = { 'id': review.id, 'user_id': review.user_id, @@ -181,18 +228,47 @@ async def create_review(review_data: CreateReviewRequest, current_user: User=Dep ) @router.patch('/{id}/approve', dependencies=[Depends(authorize_roles('admin'))]) -async def approve_review(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)): +async def approve_review(id: int, request: Request, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)): + client_ip = request.client.host if request.client else None + user_agent = request.headers.get('User-Agent') + request_id = getattr(request.state, 'request_id', None) + try: - review = db.query(Review).filter(Review.id == id).first() + review = db.query(Review).options(joinedload(Review.room)).filter(Review.id == id).first() if not review: raise HTTPException( status_code=404, detail=error_response(message='Review not found') ) + old_status = review.status.value if hasattr(review.status, 'value') else str(review.status) review.status = ReviewStatus.approved db.commit() db.refresh(review) + # SECURITY: Log review approval for audit trail + try: + await audit_service.log_action( + db=db, + action='review_approved', + resource_type='review', + user_id=current_user.id, + resource_id=review.id, + ip_address=client_ip, + user_agent=user_agent, + request_id=request_id, + details={ + 'review_user_id': review.user_id, + 'room_id': review.room_id, + 'room_number': review.room.room_number if review.room else None, + 'rating': review.rating, + 'old_status': old_status, + 'new_status': 'approved' + }, + status='success' + ) + except Exception as e: + logger.warning(f'Failed to log review approval audit: {e}') + review_dict = { 'id': review.id, 'user_id': review.user_id, @@ -218,18 +294,47 @@ async def approve_review(id: int, current_user: User=Depends(authorize_roles('ad ) @router.patch('/{id}/reject', dependencies=[Depends(authorize_roles('admin'))]) -async def reject_review(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)): +async def reject_review(id: int, request: Request, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)): + client_ip = request.client.host if request.client else None + user_agent = request.headers.get('User-Agent') + request_id = getattr(request.state, 'request_id', None) + try: - review = db.query(Review).filter(Review.id == id).first() + review = db.query(Review).options(joinedload(Review.room)).filter(Review.id == id).first() if not review: raise HTTPException( status_code=404, detail=error_response(message='Review not found') ) + old_status = review.status.value if hasattr(review.status, 'value') else str(review.status) review.status = ReviewStatus.rejected db.commit() db.refresh(review) + # SECURITY: Log review rejection for audit trail + try: + await audit_service.log_action( + db=db, + action='review_rejected', + resource_type='review', + user_id=current_user.id, + resource_id=review.id, + ip_address=client_ip, + user_agent=user_agent, + request_id=request_id, + details={ + 'review_user_id': review.user_id, + 'room_id': review.room_id, + 'room_number': review.room.room_number if review.room else None, + 'rating': review.rating, + 'old_status': old_status, + 'new_status': 'rejected' + }, + status='success' + ) + except Exception as e: + logger.warning(f'Failed to log review rejection audit: {e}') + review_dict = { 'id': review.id, 'user_id': review.user_id, @@ -255,13 +360,47 @@ async def reject_review(id: int, current_user: User=Depends(authorize_roles('adm ) @router.delete('/{id}', dependencies=[Depends(authorize_roles('admin'))]) -async def delete_review(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)): +async def delete_review(id: int, request: Request, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)): + client_ip = request.client.host if request.client else None + user_agent = request.headers.get('User-Agent') + request_id = getattr(request.state, 'request_id', None) + try: - review = db.query(Review).filter(Review.id == id).first() + review = db.query(Review).options(joinedload(Review.room)).filter(Review.id == id).first() if not review: raise HTTPException(status_code=404, detail='Review not found') + + # Capture review details before deletion for audit + review_details = { + 'review_id': review.id, + 'review_user_id': review.user_id, + 'room_id': review.room_id, + 'room_number': review.room.room_number if review.room else None, + 'rating': review.rating, + 'status': review.status.value if hasattr(review.status, 'value') else str(review.status), + 'comment_length': len(review.comment) if review.comment else 0 + } + db.delete(review) db.commit() + + # SECURITY: Log review deletion for audit trail + try: + await audit_service.log_action( + db=db, + action='review_deleted', + resource_type='review', + user_id=current_user.id, + resource_id=id, + ip_address=client_ip, + user_agent=user_agent, + request_id=request_id, + details=review_details, + status='success' + ) + except Exception as e: + logger.warning(f'Failed to log review deletion audit: {e}') + return {'status': 'success', 'message': 'Review deleted successfully'} except HTTPException: raise diff --git a/Backend/src/rooms/routes/__pycache__/advanced_room_routes.cpython-312.pyc b/Backend/src/rooms/routes/__pycache__/advanced_room_routes.cpython-312.pyc index 3e0eb0231372a6ab7a012cc70dfa03f9d125df2d..2e2437a9c90cdf61804d0cb874d9eb60856ce75e 100644 GIT binary patch delta 22741 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 QbYO|NZ+uPi1Werj2lQ)Wr2qf` delta 13439 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 ZKuvW6^StEn4Xa$NOkTfnLh6OJ{vX^(`B4A> diff --git a/Backend/src/rooms/routes/__pycache__/room_routes.cpython-312.pyc b/Backend/src/rooms/routes/__pycache__/room_routes.cpython-312.pyc index 44877587b05e7684c939d9c59ba63fd4740faea1..c235dbd149dd03b3dedef6e3fe2ffc3bf149c671 100644 GIT binary patch delta 12670 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 VyyM)WmsVUJZTNuln$xi0{{vNyh5-No delta 8621 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 0: + raise HTTPException( + status_code=400, + detail=f'Cannot delete room with {active_bookings} active booking(s). Please cancel or complete bookings first.' + ) + + # Check for historical bookings - prevent deletion to maintain data integrity + historical_bookings = db.query(Booking).filter(Booking.room_id == id).count() + if historical_bookings > 0: + raise HTTPException( + status_code=400, + detail=f'Cannot delete room with {historical_bookings} historical booking(s). Consider marking the room as inactive or under maintenance instead.' + ) + + # Capture room info before deletion for audit + deleted_room_info = { + 'room_id': room.id, + 'room_number': room.room_number, + 'floor': room.floor, + 'room_type_id': room.room_type_id, + 'status': room.status.value if isinstance(room.status, RoomStatus) else str(room.status), + 'price': float(room.price) if room.price else None, + } + db.delete(room) db.commit() + + # SECURITY: Log room deletion for audit trail + try: + await audit_service.log_action( + db=db, + action='room_deleted', + resource_type='room', + user_id=current_user.id, + resource_id=id, + ip_address=client_ip, + user_agent=user_agent, + request_id=request_id, + details=deleted_room_info, + status='success' + ) + except Exception as e: + logger.warning(f'Failed to log room deletion audit: {e}') + return {'status': 'success', 'message': 'Room deleted successfully'} except HTTPException: raise @@ -537,8 +592,11 @@ async def delete_room(id: int, current_user: User=Depends(authorize_roles('admin raise HTTPException(status_code=500, detail=str(e)) @router.post('/bulk-delete', dependencies=[Depends(authorize_roles('admin'))]) -async def bulk_delete_rooms(room_ids: BulkDeleteRoomsRequest, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)): +async def bulk_delete_rooms(room_ids: BulkDeleteRoomsRequest, request: Request, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)): """Bulk delete rooms with validated input using Pydantic schema.""" + client_ip = request.client.host if request.client else None + user_agent = request.headers.get('User-Agent') + request_id = getattr(request.state, 'request_id', None) try: ids = room_ids.room_ids @@ -550,11 +608,72 @@ async def bulk_delete_rooms(room_ids: BulkDeleteRoomsRequest, current_user: User db.rollback() raise HTTPException(status_code=404, detail=f'Rooms with IDs {not_found_ids} not found') + # SECURITY: Check for active bookings before deletion + from ...bookings.models.booking import Booking, BookingStatus + rooms_with_active_bookings = db.query(Booking.room_id).filter( + Booking.room_id.in_(ids), + Booking.status.in_([BookingStatus.pending, BookingStatus.confirmed, BookingStatus.checked_in]) + ).distinct().all() + + if rooms_with_active_bookings: + room_ids_with_bookings = [r[0] for r in rooms_with_active_bookings] + db.rollback() + raise HTTPException( + status_code=400, + detail=f'Cannot delete rooms with IDs {room_ids_with_bookings} - they have active bookings. Please cancel or complete bookings first.' + ) + + # Check for historical bookings - prevent deletion to maintain data integrity + rooms_with_historical_bookings = db.query(Booking.room_id).filter( + Booking.room_id.in_(ids) + ).distinct().all() + + if rooms_with_historical_bookings: + room_ids_with_bookings = [r[0] for r in rooms_with_historical_bookings] + db.rollback() + raise HTTPException( + status_code=400, + detail=f'Cannot delete rooms with IDs {room_ids_with_bookings} - they have historical bookings. Consider marking them as inactive or under maintenance instead.' + ) + + # Capture room info before deletion for audit + deleted_rooms_info = [] + for room in rooms: + deleted_rooms_info.append({ + 'room_id': room.id, + 'room_number': room.room_number, + 'floor': room.floor, + 'room_type_id': room.room_type_id, + 'status': room.status.value if isinstance(room.status, RoomStatus) else str(room.status), + }) + # Delete rooms deleted_count = db.query(Room).filter(Room.id.in_(ids)).delete(synchronize_session=False) # Commit transaction db.commit() + + # SECURITY: Log bulk room deletion for audit trail + try: + await audit_service.log_action( + db=db, + action='rooms_bulk_deleted', + resource_type='room', + user_id=current_user.id, + resource_id=None, # Multiple resources + ip_address=client_ip, + user_agent=user_agent, + request_id=request_id, + details={ + 'deleted_count': deleted_count, + 'deleted_room_ids': ids, + 'deleted_rooms': deleted_rooms_info + }, + status='success' + ) + except Exception as e: + logger.warning(f'Failed to log bulk room deletion audit: {e}') + return success_response( data={'deleted_count': deleted_count, 'deleted_ids': ids}, message=f'Successfully deleted {deleted_count} room(s)' diff --git a/Backend/src/security/middleware/__pycache__/auth.cpython-312.pyc b/Backend/src/security/middleware/__pycache__/auth.cpython-312.pyc index fe7f2ed7bfcec90285c3e38aff7520267de41967..5e9d9ded490a160a1b42667ca8178cd1fc9d6677 100644 GIT binary patch delta 2499 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 RDChVh2h%0tPn`6#{0B*( User: - role = db.query(Role).filter(Role.id == current_user.role_id).first() - if not role: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='User role not found') - user_role_name = role.name + # PERFORMANCE: Use eager-loaded relationship if available, otherwise query + # This reduces database queries since get_current_user now eager loads the role + if hasattr(current_user, 'role') and current_user.role is not None: + user_role_name = current_user.role.name + else: + # Fallback: query role if relationship wasn't loaded (shouldn't happen, but safe) + role = db.query(Role).filter(Role.id == current_user.role_id).first() + if not role: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='User role not found') + user_role_name = role.name + if user_role_name not in allowed_roles: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='You do not have permission to access this resource') return current_user @@ -168,7 +176,8 @@ def get_current_user_optional( except (JWTError, ValueError): return None - user = db.query(User).filter(User.id == user_id).first() + # PERFORMANCE: Eager load role relationship for consistency + user = db.query(User).options(joinedload(User.role)).filter(User.id == user_id).first() if user is None: return None diff --git a/Backend/src/shared/utils/__pycache__/role_helpers.cpython-312.pyc b/Backend/src/shared/utils/__pycache__/role_helpers.cpython-312.pyc index 3196c7157be9d5511b2313e2bc3f2b569d4f102b..6cc9c814fc0399e2e6f76e8ac2370f4d788cdbd6 100644 GIT binary patch delta 1248 zcmaJ=&1(};5Z||X&F*%SM$^xviQOixZTxE0mXcBmR*MHk(H5az%9`D_o93gu-J(Lo z2zn62Hoil9tauO+!T+G()q^Q|kbt1z9}uZt>Y>hT1gi}$`+hTT-pu^o&de6ucSB8Y z{r*M)p2V|3>z!O`>LA}seP22ZuSpHb6b;!_=DbGmKD{2i-JEFQK2j8~5&oF;+;uz{ zh)+Q<#Ft`CXR(5irZs&p-fOFGPkZ-z)$}Q$K+?*7yW?IJ?JtSY>z=3m|8q+>@f|Wu z4)Q@U9<57ckSQT9I6=N7b`lSNB=+}KD9dNeL}NulIcpgeX+gJj$6?F|WEGmzvnJmZ zwN7R7R?1v*tb8uvVG^7%83DaG@yt)Ofr1wM0YFjsq6Su-wZfCa$3Unm$_?SKYH)RE zz3qdlZA?F(c{)?>9sStczLqQ{*TzfZWncGa{?K!!I||XI;}a2K^Gq1`N1vNx z8XVy)wzuwxy1g>Byc0QLoltKYiJ&zg*3lVg0dp;Z~(QiscQ_)m(N=-CA`wWwKh+Olmj&9b$HJZOuixn$+$E55xu zR-Y>XTz|Rs1K(knZ+uHlmTB@g-->jOfA)5(9ax4K0@Oi@lU6<`MU~@VI09>%@5=2w zE6Jo*sTC>Jf%Y2vQP6DsXEf{x1h())vJw~s%K$o^K&S;?mNPMzstav3KGgn6@ktIf zoC5n0|0%~^PU|A}c7I0?r1acuR?q44<}6mrW<%hYgk=W+io#d5dG*|S?7bQ-(3qo_vstHy<}NJiIt assf0O^R9SJw{cgM`G6YXC)EoSseb_irveNB delta 882 zcmZuv&ubGw6yDivnn^ZIBu&kaHZ@I(>6RE36||@rsGwK`+ul6HHQCmLWLIW4s8BTr z|A54IuqP?rM9?1m2YT@$g5ag(Vk_QT=&gwLy{$2*UD)sanD3i6@6GHdTCvc_!NGup z-?#Vq>YT3=inGm5ZcBL-(S{(+TJoL*uh_)3WmfYL?eEV~dhr`};MIJ+OyBldH4ZiT z98+Oc&ZdHWM$em2fD;JPQBQB1vIbp2N!mW}&P z)iO1YprV2!bQNO4GtbiaFou+}D+n!VOAWW37t7C=H^PY>S@9+};DmB(B#w)nVzO9} z*7J<8;4e;l)-SN)G`v(wY#4qjv1k-oU0$_IdZke{MH0gZXx_&sNiGrywv3QYi$6@; z(H9rRG0Yw%&IrLMJPjn^jyDIx1KQN#+Ci#PE?Z60(M?BWP?v;z11Z)Qe;7zl9xhf< zTyENq)i8vZK z>MYIo|EV4+>t?B;oBEPbYTAad#VO>S#@`Mjw50FqPX4R#IXVb2qgJoK@!* z=q+@WyOvdV@6NpzceL)11obYc2SF!sw|$LmkWcry$1gUlO0#ZU7Uwafmu~M%@KsGD kIZLiA>_}) str: if not user or not user.role_id: return 'customer' try: + # PERFORMANCE: Use eager-loaded relationship if available to avoid query + if hasattr(user, 'role') and user.role is not None: + return user.role.name + # Fallback: query if relationship not loaded role = db.query(Role).filter(Role.id == user.role_id).first() return role.name if role else 'customer' except Exception: @@ -31,6 +35,10 @@ def is_customer(user: User, db: Session) -> bool: """Check if user is customer""" return get_user_role_name(user, db) == 'customer' +def is_housekeeping(user: User, db: Session) -> bool: + """Check if user is housekeeping""" + return get_user_role_name(user, db) == 'housekeeping' + def can_access_all_payments(user: User, db: Session) -> bool: """Check if user can see all payments (admin or accountant)""" role_name = get_user_role_name(user, db) diff --git a/Backend/src/system/routes/__pycache__/system_settings_routes.cpython-312.pyc b/Backend/src/system/routes/__pycache__/system_settings_routes.cpython-312.pyc index c09b0cebbc25af1d8de4daed521ad34b681a7162..d91735abcc603be2538ce4f0b458e5b9494c55e7 100644 GIT binary patch delta 20946 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 XWx=Qk(f5{1r_VN$M@;4lP}Bbd{s9V| diff --git a/Backend/src/system/routes/system_settings_routes.py b/Backend/src/system/routes/system_settings_routes.py index bd5114a8..76cb4bcb 100644 --- a/Backend/src/system/routes/system_settings_routes.py +++ b/Backend/src/system/routes/system_settings_routes.py @@ -15,6 +15,7 @@ from ...auth.models.user import User from ..models.system_settings import SystemSettings from ...shared.utils.mailer import send_email from ...rooms.services.room_service import get_base_url +from ...analytics.services.audit_service import audit_service def normalize_image_url(image_url: str, base_url: str) -> str: if not image_url: @@ -63,9 +64,14 @@ async def get_platform_currency( @router.put("/currency") async def update_platform_currency( currency_data: dict, + request: Request, current_user: User = Depends(authorize_roles("admin")), db: Session = Depends(get_db) ): + client_ip = request.client.host if request.client else None + user_agent = request.headers.get('User-Agent') + request_id = getattr(request.state, 'request_id', None) + try: currency = currency_data.get("currency", "").upper() @@ -81,6 +87,8 @@ async def update_platform_currency( SystemSettings.key == "platform_currency" ).first() + old_value = setting.value if setting else None + if setting: setting.value = currency setting.updated_by_id = current_user.id @@ -96,6 +104,27 @@ async def update_platform_currency( db.commit() db.refresh(setting) + # SECURITY: Log system setting change for audit trail + try: + await audit_service.log_action( + db=db, + action='system_setting_changed', + resource_type='system_settings', + user_id=current_user.id, + resource_id=setting.id, + ip_address=client_ip, + user_agent=user_agent, + request_id=request_id, + details={ + 'setting_key': 'platform_currency', + 'old_value': old_value, + 'new_value': currency + }, + status='success' + ) + except Exception as e: + logger.warning(f'Failed to log system setting change audit: {e}') + return { "status": "success", "message": "Platform currency updated successfully", @@ -199,9 +228,13 @@ async def get_stripe_settings( @router.put("/stripe") async def update_stripe_settings( stripe_data: dict, + request: Request, current_user: User = Depends(authorize_roles("admin")), db: Session = Depends(get_db) ): + client_ip = request.client.host if request.client else None + user_agent = request.headers.get('User-Agent') + request_id = getattr(request.state, 'request_id', None) try: secret_key = stripe_data.get("stripe_secret_key", "").strip() publishable_key = stripe_data.get("stripe_publishable_key", "").strip() @@ -229,11 +262,16 @@ async def update_stripe_settings( ) + settings_changed = [] + old_values = {} + if secret_key: setting = db.query(SystemSettings).filter( SystemSettings.key == "stripe_secret_key" ).first() + old_values['stripe_secret_key'] = setting.value if setting else None + if setting: setting.value = secret_key setting.updated_by_id = current_user.id @@ -245,6 +283,7 @@ async def update_stripe_settings( updated_by_id=current_user.id ) db.add(setting) + settings_changed.append('stripe_secret_key') if publishable_key: @@ -252,6 +291,8 @@ async def update_stripe_settings( SystemSettings.key == "stripe_publishable_key" ).first() + old_values['stripe_publishable_key'] = setting.value if setting else None + if setting: setting.value = publishable_key setting.updated_by_id = current_user.id @@ -263,6 +304,7 @@ async def update_stripe_settings( updated_by_id=current_user.id ) db.add(setting) + settings_changed.append('stripe_publishable_key') if webhook_secret: @@ -270,6 +312,8 @@ async def update_stripe_settings( SystemSettings.key == "stripe_webhook_secret" ).first() + old_values['stripe_webhook_secret'] = setting.value if setting else None + if setting: setting.value = webhook_secret setting.updated_by_id = current_user.id @@ -281,9 +325,44 @@ async def update_stripe_settings( updated_by_id=current_user.id ) db.add(setting) + settings_changed.append('stripe_webhook_secret') db.commit() + # SECURITY: Log payment gateway configuration change for audit trail + if settings_changed: + try: + def mask_key(key_value: str) -> str: + if not key_value or len(key_value) < 4: + return None + return "*" * (len(key_value) - 4) + key_value[-4:] + + masked_old = {k: mask_key(v) if v else None for k, v in old_values.items() if k in settings_changed} + masked_new = {k: mask_key(v) if v else None for k, v in { + 'stripe_secret_key': secret_key if secret_key else None, + 'stripe_publishable_key': publishable_key if publishable_key else None, + 'stripe_webhook_secret': webhook_secret if webhook_secret else None + }.items() if k in settings_changed} + + await audit_service.log_action( + db=db, + action='payment_gateway_config_changed', + resource_type='system_settings', + user_id=current_user.id, + resource_id=None, + ip_address=client_ip, + user_agent=user_agent, + request_id=request_id, + details={ + 'gateway': 'stripe', + 'settings_changed': settings_changed, + 'old_values': masked_old, + 'new_values': masked_new + }, + status='success' + ) + except Exception as e: + logger.warning(f'Failed to log payment gateway config change audit: {e}') def mask_key(key_value: str) -> str: if not key_value or len(key_value) < 4: @@ -367,9 +446,14 @@ async def get_paypal_settings( @router.put("/paypal") async def update_paypal_settings( paypal_data: dict, + request: Request, current_user: User = Depends(authorize_roles("admin")), db: Session = Depends(get_db) ): + client_ip = request.client.host if request.client else None + user_agent = request.headers.get('User-Agent') + request_id = getattr(request.state, 'request_id', None) + try: client_id = paypal_data.get("paypal_client_id", "").strip() client_secret = paypal_data.get("paypal_client_secret", "").strip() @@ -382,6 +466,21 @@ async def update_paypal_settings( detail="Invalid PayPal mode. Must be 'sandbox' or 'live'" ) + settings_changed = [] + old_values = {} + + # Get old values before updating + if client_id: + old_setting = db.query(SystemSettings).filter(SystemSettings.key == "paypal_client_id").first() + old_values['paypal_client_id'] = old_setting.value if old_setting else None + + if client_secret: + old_setting = db.query(SystemSettings).filter(SystemSettings.key == "paypal_client_secret").first() + old_values['paypal_client_secret'] = old_setting.value if old_setting else None + + if mode: + old_setting = db.query(SystemSettings).filter(SystemSettings.key == "paypal_mode").first() + old_values['paypal_mode'] = old_setting.value if old_setting else None if client_id: setting = db.query(SystemSettings).filter( @@ -399,6 +498,7 @@ async def update_paypal_settings( updated_by_id=current_user.id ) db.add(setting) + settings_changed.append('paypal_client_id') if client_secret: @@ -417,6 +517,7 @@ async def update_paypal_settings( updated_by_id=current_user.id ) db.add(setting) + settings_changed.append('paypal_client_secret') if mode: @@ -435,9 +536,44 @@ async def update_paypal_settings( updated_by_id=current_user.id ) db.add(setting) + settings_changed.append('paypal_mode') db.commit() + # SECURITY: Log payment gateway configuration change for audit trail + if settings_changed: + try: + def mask_key(key_value: str) -> str: + if not key_value or len(key_value) < 4: + return None + return "*" * (len(key_value) - 4) + key_value[-4:] + + masked_old = {k: mask_key(v) if v else None for k, v in old_values.items() if k in settings_changed} + masked_new = {k: mask_key(v) if v else None for k, v in { + 'paypal_client_id': client_id if client_id else None, + 'paypal_client_secret': client_secret if client_secret else None, + 'paypal_mode': mode if mode else None + }.items() if k in settings_changed} + + await audit_service.log_action( + db=db, + action='payment_gateway_config_changed', + resource_type='system_settings', + user_id=current_user.id, + resource_id=None, + ip_address=client_ip, + user_agent=user_agent, + request_id=request_id, + details={ + 'gateway': 'paypal', + 'settings_changed': settings_changed, + 'old_values': masked_old, + 'new_values': masked_new + }, + status='success' + ) + except Exception as e: + logger.warning(f'Failed to log payment gateway config change audit: {e}') def mask_key(key_value: str) -> str: if not key_value or len(key_value) < 4: @@ -548,9 +684,14 @@ async def get_borica_settings( @router.put("/borica") async def update_borica_settings( borica_data: dict, + request: Request, current_user: User = Depends(authorize_roles("admin")), db: Session = Depends(get_db) ): + client_ip = request.client.host if request.client else None + user_agent = request.headers.get('User-Agent') + request_id = getattr(request.state, 'request_id', None) + try: terminal_id = borica_data.get("borica_terminal_id", "").strip() merchant_id = borica_data.get("borica_merchant_id", "").strip() @@ -565,6 +706,20 @@ async def update_borica_settings( detail="Invalid Borica mode. Must be 'test' or 'production'" ) + settings_changed = [] + old_values = {} + + # Get old values before updating + if terminal_id: + old_setting = db.query(SystemSettings).filter(SystemSettings.key == "borica_terminal_id").first() + old_values['borica_terminal_id'] = old_setting.value if old_setting else None + if merchant_id: + old_setting = db.query(SystemSettings).filter(SystemSettings.key == "borica_merchant_id").first() + old_values['borica_merchant_id'] = old_setting.value if old_setting else None + if mode: + old_setting = db.query(SystemSettings).filter(SystemSettings.key == "borica_mode").first() + old_values['borica_mode'] = old_setting.value if old_setting else None + if terminal_id: setting = db.query(SystemSettings).filter( SystemSettings.key == "borica_terminal_id" @@ -897,9 +1052,14 @@ async def get_smtp_settings( @router.put("/smtp") async def update_smtp_settings( smtp_data: dict, + request: Request, current_user: User = Depends(authorize_roles("admin")), db: Session = Depends(get_db) ): + client_ip = request.client.host if request.client else None + user_agent = request.headers.get('User-Agent') + request_id = getattr(request.state, 'request_id', None) + try: smtp_host = smtp_data.get("smtp_host", "").strip() smtp_port = smtp_data.get("smtp_port", "").strip() @@ -938,6 +1098,14 @@ async def update_smtp_settings( detail="Invalid email address format for 'From Email'" ) + settings_changed = [] + old_values = {} + + # Get old values before updating + smtp_keys = ['smtp_host', 'smtp_port', 'smtp_user', 'smtp_password', 'smtp_from_email', 'smtp_from_name', 'smtp_use_tls'] + for key in smtp_keys: + old_setting = db.query(SystemSettings).filter(SystemSettings.key == key).first() + old_values[key] = old_setting.value if old_setting else None def update_setting(key: str, value: str, description: str): setting = db.query(SystemSettings).filter( @@ -955,6 +1123,9 @@ async def update_smtp_settings( updated_by_id=current_user.id ) db.add(setting) + + if key not in settings_changed: + settings_changed.append(key) if smtp_host: @@ -1009,6 +1180,45 @@ async def update_smtp_settings( db.commit() + # SECURITY: Log SMTP configuration change for audit trail + if settings_changed: + try: + def mask_password(password_value: str) -> str: + if not password_value or len(password_value) < 4: + return None + return "*" * (len(password_value) - 4) + password_value[-4:] + + masked_old = {k: (mask_password(v) if 'password' in k.lower() else v) if v else None + for k, v in old_values.items() if k in settings_changed} + masked_new = {k: (mask_password(v) if 'password' in k.lower() else v) if v else None + for k, v in { + 'smtp_host': smtp_host if smtp_host else None, + 'smtp_port': smtp_port if smtp_port else None, + 'smtp_user': smtp_user if smtp_user else None, + 'smtp_password': smtp_password if smtp_password else None, + 'smtp_from_email': smtp_from_email if smtp_from_email else None, + 'smtp_from_name': smtp_from_name if smtp_from_name else None, + 'smtp_use_tls': 'true' if smtp_use_tls else 'false' if smtp_use_tls is not None else None + }.items() if k in settings_changed} + + await audit_service.log_action( + db=db, + action='smtp_config_changed', + resource_type='system_settings', + user_id=current_user.id, + resource_id=None, + ip_address=client_ip, + user_agent=user_agent, + request_id=request_id, + details={ + 'settings_changed': settings_changed, + 'old_values': masked_old, + 'new_values': masked_new + }, + status='success' + ) + except Exception as e: + logger.warning(f'Failed to log SMTP config change audit: {e}') def mask_password(password_value: str) -> str: if not password_value or len(password_value) < 4: diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx index 251e9381..46d5b982 100644 --- a/Frontend/src/App.tsx +++ b/Frontend/src/App.tsx @@ -63,6 +63,7 @@ const ProfilePage = lazy(() => import('./pages/customer/ProfilePage')); const LoyaltyPage = lazy(() => import('./pages/customer/LoyaltyPage')); const GroupBookingPage = lazy(() => import('./pages/customer/GroupBookingPage')); const ComplaintPage = lazy(() => import('./pages/customer/ComplaintPage')); +const GuestRequestsPage = lazy(() => import('./pages/customer/GuestRequestsPage')); const GDPRPage = lazy(() => import('./pages/customer/GDPRPage')); const GDPRDeletionConfirmPage = lazy(() => import('./pages/customer/GDPRDeletionConfirmPage')); const AboutPage = lazy(() => import('./features/content/pages/AboutPage')); @@ -118,6 +119,10 @@ const StaffLoyaltyManagementPage = lazy(() => import('./pages/staff/LoyaltyManag const StaffGuestProfilePage = lazy(() => import('./pages/staff/GuestProfilePage')); const StaffAdvancedRoomManagementPage = lazy(() => import('./pages/staff/AdvancedRoomManagementPage')); const ChatManagementPage = lazy(() => import('./pages/staff/ChatManagementPage')); +const GuestRequestManagementPage = lazy(() => import('./pages/staff/GuestRequestManagementPage')); +const GuestCommunicationPage = lazy(() => import('./pages/staff/GuestCommunicationPage')); +const IncidentComplaintManagementPage = lazy(() => import('./pages/staff/IncidentComplaintManagementPage')); +const UpsellManagementPage = lazy(() => import('./pages/staff/UpsellManagementPage')); const StaffLayout = lazy(() => import('./pages/StaffLayout')); const AccountantDashboardPage = lazy(() => import('./pages/accountant/DashboardPage')); @@ -452,6 +457,16 @@ function App() { } /> + + + + + + } + /> } /> + } + /> + } + /> + } + /> + } + /> } diff --git a/Frontend/src/features/complaints/services/complaintService.ts b/Frontend/src/features/complaints/services/complaintService.ts new file mode 100644 index 00000000..982649f9 --- /dev/null +++ b/Frontend/src/features/complaints/services/complaintService.ts @@ -0,0 +1,80 @@ +import apiClient from '../../../shared/services/apiClient'; + +export interface Complaint { + id: number; + guest_id: number; + guest_name?: string; + booking_id?: number; + room_id?: number; + room_number?: string; + category: string; + priority: string; + status: string; + title: string; + description: string; + attachments?: string[]; + assigned_to?: number; + assigned_staff_name?: string; + resolved_at?: string; + resolution_notes?: string; + created_at: string; + updated_at: string; +} + +export interface ComplaintUpdate { + id: number; + complaint_id: number; + update_type: string; + description: string; + updated_by: number; + updated_by_name?: string; + created_at: string; +} + +const complaintService = { + async getComplaints(params?: { + status?: string; + priority?: string; + category?: string; + assigned_to?: number; + page?: number; + limit?: number; + }) { + const response = await apiClient.get('/complaints', { params }); + return response.data; + }, + + async getComplaint(complaintId: number) { + const response = await apiClient.get(`/complaints/${complaintId}`); + return response.data; + }, + + async updateComplaint(complaintId: number, data: { + status?: string; + priority?: string; + assigned_to?: number; + resolution_notes?: string; + }) { + const response = await apiClient.put(`/complaints/${complaintId}`, data); + return response.data; + }, + + async resolveComplaint(complaintId: number, data: { + resolution_notes: string; + compensation_amount?: number; + }) { + const response = await apiClient.post(`/complaints/${complaintId}/resolve`, data); + return response.data; + }, + + async addComplaintUpdate(complaintId: number, data: { + description: string; + update_type?: string; + }) { + const response = await apiClient.post(`/complaints/${complaintId}/updates`, data); + return response.data; + }, +}; + +export default complaintService; + diff --git a/Frontend/src/features/guestRequests/services/guestRequestService.ts b/Frontend/src/features/guestRequests/services/guestRequestService.ts new file mode 100644 index 00000000..4432b3bb --- /dev/null +++ b/Frontend/src/features/guestRequests/services/guestRequestService.ts @@ -0,0 +1,82 @@ +import apiClient from '../../../shared/services/apiClient'; + +export interface GuestRequest { + id: number; + booking_id: number; + room_id: number; + room_number?: string; + user_id: number; + guest_name?: string; + guest_email?: string; + request_type: string; + status: string; + priority: string; + title: string; + description?: string; + guest_notes?: string; + staff_notes?: string; + assigned_to?: number; + assigned_staff_name?: string; + fulfilled_by?: number; + fulfilled_staff_name?: string; + requested_at: string; + started_at?: string; + fulfilled_at?: string; + response_time_minutes?: number; + fulfillment_time_minutes?: number; +} + +const guestRequestService = { + async getGuestRequests(params?: { + status?: string; + request_type?: string; + room_id?: number; + assigned_to?: number; + priority?: string; + page?: number; + limit?: number; + }) { + const response = await apiClient.get('/guest-requests', { params }); + return response.data; + }, + + async getGuestRequest(requestId: number) { + const response = await apiClient.get(`/guest-requests/${requestId}`); + return response.data; + }, + + async createGuestRequest(data: { + booking_id: number; + room_id: number; + request_type: string; + title: string; + description?: string; + priority?: string; + guest_notes?: string; + }) { + const response = await apiClient.post('/guest-requests', data); + return response.data; + }, + + async updateGuestRequest(requestId: number, data: { + status?: string; + assigned_to?: number; + staff_notes?: string; + }) { + const response = await apiClient.put(`/guest-requests/${requestId}`, data); + return response.data; + }, + + async assignRequest(requestId: number) { + const response = await apiClient.post(`/guest-requests/${requestId}/assign`); + return response.data; + }, + + async fulfillRequest(requestId: number, staff_notes?: string) { + const response = await apiClient.post(`/guest-requests/${requestId}/fulfill`, { staff_notes }); + return response.data; + }, +}; + +export default guestRequestService; + diff --git a/Frontend/src/features/hotel_services/components/HousekeepingManagement.tsx b/Frontend/src/features/hotel_services/components/HousekeepingManagement.tsx index 3a7d5da1..d38a0a8d 100644 --- a/Frontend/src/features/hotel_services/components/HousekeepingManagement.tsx +++ b/Frontend/src/features/hotel_services/components/HousekeepingManagement.tsx @@ -240,7 +240,7 @@ const HousekeepingManagement: React.FC = () => { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { - if (editingTask) { + if (editingTask && editingTask.id) { // For staff, only allow updating status and checklist items if (!isAdmin) { const data = { diff --git a/Frontend/src/features/hotel_services/components/MaintenanceManagement.tsx b/Frontend/src/features/hotel_services/components/MaintenanceManagement.tsx index 08c38980..21dded60 100644 --- a/Frontend/src/features/hotel_services/components/MaintenanceManagement.tsx +++ b/Frontend/src/features/hotel_services/components/MaintenanceManagement.tsx @@ -290,6 +290,7 @@ const MaintenanceManagement: React.FC = () => { Type Status Priority + Reported By Scheduled Assigned Actions @@ -317,6 +318,9 @@ const MaintenanceManagement: React.FC = () => { {record.priority} + + {record.reported_by_name || '-'} + {new Date(record.scheduled_start).toLocaleDateString()} @@ -620,12 +624,12 @@ const MaintenanceManagement: React.FC = () => {

{viewingRecord.title}

- {viewingRecord.description && ( -
- -

{viewingRecord.description}

-
- )} +
+ +

+ {viewingRecord.description || 'No description provided'} +

+
@@ -642,6 +646,21 @@ const MaintenanceManagement: React.FC = () => {
+
+ {viewingRecord.reported_by_name && ( +
+ +

{viewingRecord.reported_by_name}

+
+ )} + {viewingRecord.assigned_staff_name && ( +
+ +

{viewingRecord.assigned_staff_name}

+
+ )} +
+
@@ -655,10 +674,17 @@ const MaintenanceManagement: React.FC = () => { )}
- {viewingRecord.assigned_staff_name && ( + {viewingRecord.notes && (
- -

{viewingRecord.assigned_staff_name}

+ +

{viewingRecord.notes}

+
+ )} + + {viewingRecord.completion_notes && ( +
+ +

{viewingRecord.completion_notes}

)} diff --git a/Frontend/src/features/inventory/services/inventoryService.ts b/Frontend/src/features/inventory/services/inventoryService.ts new file mode 100644 index 00000000..800a0004 --- /dev/null +++ b/Frontend/src/features/inventory/services/inventoryService.ts @@ -0,0 +1,140 @@ +import apiClient from '../../../shared/services/apiClient'; + +export interface InventoryItem { + id: number; + name: string; + description?: string; + category: string; + unit: string; + current_quantity: number; + minimum_quantity: number; + maximum_quantity?: number; + reorder_quantity?: number; + unit_cost?: number; + supplier?: string; + storage_location?: string; + is_active: boolean; + is_tracked: boolean; + barcode?: string; + sku?: string; + is_low_stock: boolean; + created_at?: string; +} + +export interface InventoryTransaction { + id: number; + transaction_type: string; + quantity: number; + quantity_before: number; + quantity_after: number; + notes?: string; + cost?: number; + transaction_date: string; +} + +export interface ReorderRequest { + id: number; + item_id: number; + item_name: string; + requested_quantity: number; + current_quantity: number; + minimum_quantity: number; + status: string; + priority: string; + notes?: string; + requested_at: string; +} + +const inventoryService = { + async getInventoryItems(params?: { + category?: string; + low_stock?: boolean; + is_active?: boolean; + page?: number; + limit?: number; + }) { + const response = await apiClient.get('/inventory/items', { params }); + return response.data; + }, + + async getInventoryItem(itemId: number) { + const response = await apiClient.get(`/inventory/items/${itemId}`); + return response.data; + }, + + async createInventoryItem(data: { + name: string; + description?: string; + category: string; + unit: string; + minimum_quantity: number; + maximum_quantity?: number; + reorder_quantity?: number; + unit_cost?: number; + supplier?: string; + supplier_contact?: string; + storage_location?: string; + barcode?: string; + sku?: string; + notes?: string; + }) { + const response = await apiClient.post('/inventory/items', data); + return response.data; + }, + + async updateInventoryItem(itemId: number, data: Partial) { + const response = await apiClient.put(`/inventory/items/${itemId}`, data); + return response.data; + }, + + async createTransaction(data: { + item_id: number; + transaction_type: string; + quantity: number; + notes?: string; + cost?: number; + reference_type?: string; + reference_id?: number; + }) { + const response = await apiClient.post('/inventory/transactions', data); + return response.data; + }, + + async getItemTransactions(itemId: number, params?: { page?: number; limit?: number }) { + const response = await apiClient.get(`/inventory/items/${itemId}/transactions`, { params }); + return response.data; + }, + + async createReorderRequest(data: { + item_id: number; + requested_quantity: number; + priority?: string; + notes?: string; + }) { + const response = await apiClient.post('/inventory/reorder-requests', data); + return response.data; + }, + + async getReorderRequests(params?: { status?: string; page?: number; limit?: number }) { + const response = await apiClient.get('/inventory/reorder-requests', { params }); + return response.data; + }, + + async recordTaskConsumption(data: { + task_id: number; + item_id: number; + quantity: number; + notes?: string; + }) { + const response = await apiClient.post('/inventory/task-consumption', data); + return response.data; + }, + + async getLowStockItems() { + const response = await apiClient.get('/inventory/low-stock'); + return response.data; + }, +}; + +export default inventoryService; + diff --git a/Frontend/src/features/rooms/services/advancedRoomService.ts b/Frontend/src/features/rooms/services/advancedRoomService.ts index 1a370b60..9708d7ae 100644 --- a/Frontend/src/features/rooms/services/advancedRoomService.ts +++ b/Frontend/src/features/rooms/services/advancedRoomService.ts @@ -15,6 +15,8 @@ export interface MaintenanceRecord { actual_end?: string; assigned_to?: number; assigned_staff_name?: string; + reported_by?: number; + reported_by_name?: string; priority: 'low' | 'medium' | 'high' | 'urgent'; blocks_room: boolean; block_start?: string; @@ -22,7 +24,9 @@ export interface MaintenanceRecord { estimated_cost?: number; actual_cost?: number; notes?: string; + completion_notes?: string; created_at: string; + updated_at?: string; } export interface HousekeepingTask { @@ -44,6 +48,7 @@ export interface HousekeepingTask { actual_duration_minutes?: number; room_status?: 'available' | 'occupied' | 'maintenance' | 'cleaning'; is_room_status_only?: boolean; // Flag to indicate this is from room status, not a task + photos?: string[]; // Array of photo URLs } export interface ChecklistItem { @@ -185,6 +190,7 @@ const advancedRoomService = { date?: string; page?: number; limit?: number; + include_cleaning_rooms?: boolean; }) { const response = await apiClient.get('/advanced-rooms/housekeeping', { params }); return response.data; @@ -212,11 +218,36 @@ const advancedRoomService = { quality_score?: number; inspected_by?: number; inspection_notes?: string; + photos?: string[]; + assigned_to?: number; }) { const response = await apiClient.put(`/advanced-rooms/housekeeping/${taskId}`, data); return response.data; }, + async uploadHousekeepingTaskPhoto(taskId: number, file: File) { + const formData = new FormData(); + formData.append('image', file); + const response = await apiClient.post(`/advanced-rooms/housekeeping/${taskId}/upload-photo`, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + return response.data; + }, + + async reportMaintenanceIssue(taskId: number, data: { + title: string; + description: string; + maintenance_type?: 'corrective' | 'emergency'; + priority?: 'low' | 'medium' | 'high' | 'urgent'; + blocks_room?: boolean; + notes?: string; + }) { + const response = await apiClient.post(`/advanced-rooms/housekeeping/${taskId}/report-maintenance-issue`, data); + return response.data; + }, + // Room Inspections async getRoomInspections(params?: { room_id?: number; diff --git a/Frontend/src/features/staffShifts/services/staffShiftService.ts b/Frontend/src/features/staffShifts/services/staffShiftService.ts new file mode 100644 index 00000000..a73fa878 --- /dev/null +++ b/Frontend/src/features/staffShifts/services/staffShiftService.ts @@ -0,0 +1,150 @@ +import apiClient from '../../../shared/services/apiClient'; + +export interface StaffShift { + id: number; + staff_id: number; + staff_name?: string; + shift_date: string; + shift_type: 'morning' | 'afternoon' | 'night' | 'full_day' | 'custom'; + start_time: string; + end_time: string; + status: 'scheduled' | 'in_progress' | 'completed' | 'cancelled' | 'no_show'; + actual_start_time?: string; + actual_end_time?: string; + break_duration_minutes?: number; + department?: string; + notes?: string; + handover_notes?: string; + tasks_completed?: number; + tasks_assigned?: number; + assigned_by_name?: string; +} + +export interface StaffTask { + id: number; + shift_id?: number; + staff_id: number; + staff_name?: string; + title: string; + description?: string; + task_type: string; + priority: 'low' | 'normal' | 'high' | 'urgent'; + status: 'pending' | 'assigned' | 'in_progress' | 'completed' | 'cancelled' | 'on_hold'; + scheduled_start?: string; + scheduled_end?: string; + actual_start?: string; + actual_end?: string; + estimated_duration_minutes?: number; + actual_duration_minutes?: number; + due_date?: string; + related_booking_id?: number; + related_room_id?: number; + related_guest_request_id?: number; + related_maintenance_id?: number; + notes?: string; + completion_notes?: string; + assigned_by_name?: string; +} + +const staffShiftService = { + async getShifts(params?: { + staff_id?: number; + shift_date?: string; + status?: string; + department?: string; + page?: number; + limit?: number; + }) { + const response = await apiClient.get('/staff-shifts', { params }); + return response.data; + }, + + async getShift(shiftId: number) { + const response = await apiClient.get(`/staff-shifts/${shiftId}`); + return response.data; + }, + + async createShift(data: { + staff_id?: number; + shift_date: string; + shift_type: string; + start_time: string; + end_time: string; + status?: string; + break_duration_minutes?: number; + department?: string; + notes?: string; + }) { + const response = await apiClient.post('/staff-shifts', data); + return response.data; + }, + + async updateShift(shiftId: number, data: { + shift_date?: string; + shift_type?: string; + start_time?: string; + end_time?: string; + status?: string; + break_duration_minutes?: number; + department?: string; + notes?: string; + handover_notes?: string; + }) { + const response = await apiClient.put(`/staff-shifts/${shiftId}`, data); + return response.data; + }, + + async getTasks(params?: { + staff_id?: number; + shift_id?: number; + status?: string; + priority?: string; + page?: number; + limit?: number; + }) { + const response = await apiClient.get('/staff-shifts/tasks', { params }); + return response.data; + }, + + async createTask(data: { + shift_id?: number; + staff_id?: number; + title: string; + description?: string; + task_type?: string; + priority?: string; + status?: string; + scheduled_start?: string; + scheduled_end?: string; + estimated_duration_minutes?: number; + due_date?: string; + related_booking_id?: number; + related_room_id?: number; + related_guest_request_id?: number; + related_maintenance_id?: number; + notes?: string; + }) { + const response = await apiClient.post('/staff-shifts/tasks', data); + return response.data; + }, + + async updateTask(taskId: number, data: { + title?: string; + description?: string; + priority?: string; + status?: string; + completion_notes?: string; + notes?: string; + }) { + const response = await apiClient.put(`/staff-shifts/tasks/${taskId}`, data); + return response.data; + }, + + async getWorkloadSummary(date?: string) { + const response = await apiClient.get('/staff-shifts/workload', { params: { date } }); + return response.data; + }, +}; + +export default staffShiftService; + diff --git a/Frontend/src/pages/accountant/DashboardPage.tsx b/Frontend/src/pages/accountant/DashboardPage.tsx index b39b4be4..5b898d16 100644 --- a/Frontend/src/pages/accountant/DashboardPage.tsx +++ b/Frontend/src/pages/accountant/DashboardPage.tsx @@ -83,7 +83,7 @@ const AccountantDashboardPage: React.FC = () => { if (response.success && response.data?.payments) { setRecentPayments(response.data.payments); // Calculate financial summary - const completedPayments = response.data.payments.filter((p: Payment) => p.payment_status === 'completed' || p.payment_status === 'paid'); + const completedPayments = response.data.payments.filter((p: Payment) => p.payment_status === 'completed'); const pendingPayments = response.data.payments.filter((p: Payment) => p.payment_status === 'pending'); const totalRevenue = completedPayments.reduce((sum: number, p: Payment) => sum + (p.amount || 0), 0); diff --git a/Frontend/src/pages/accountant/InvoiceManagementPage.tsx b/Frontend/src/pages/accountant/InvoiceManagementPage.tsx index 49c159af..ddfaf78f 100644 --- a/Frontend/src/pages/accountant/InvoiceManagementPage.tsx +++ b/Frontend/src/pages/accountant/InvoiceManagementPage.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { Search, Plus, Edit, Trash2, Eye, FileText, Filter } from 'lucide-react'; import invoiceService, { Invoice } from '../../features/payments/services/invoiceService'; import { toast } from 'react-toastify'; diff --git a/Frontend/src/pages/admin/BannerManagementPage.tsx b/Frontend/src/pages/admin/BannerManagementPage.tsx index 23b47275..384a0d94 100644 --- a/Frontend/src/pages/admin/BannerManagementPage.tsx +++ b/Frontend/src/pages/admin/BannerManagementPage.tsx @@ -680,8 +680,7 @@ const BannerManagementPage: React.FC = () => {
- - + )} diff --git a/Frontend/src/pages/admin/PromotionManagementPage.tsx b/Frontend/src/pages/admin/PromotionManagementPage.tsx index 14b259f6..4b44bf54 100644 --- a/Frontend/src/pages/admin/PromotionManagementPage.tsx +++ b/Frontend/src/pages/admin/PromotionManagementPage.tsx @@ -513,8 +513,7 @@ const PromotionManagementPage: React.FC = () => { - - + )} diff --git a/Frontend/src/pages/customer/GuestRequestsPage.tsx b/Frontend/src/pages/customer/GuestRequestsPage.tsx new file mode 100644 index 00000000..311ff6d5 --- /dev/null +++ b/Frontend/src/pages/customer/GuestRequestsPage.tsx @@ -0,0 +1,457 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + Plus, + Clock, + CheckCircle, + XCircle, + AlertCircle, + Bell, + MessageSquare, + Package, + Wrench, + Sparkles, + RefreshCw, + Filter, +} from 'lucide-react'; +import { toast } from 'react-toastify'; +import guestRequestService, { GuestRequest } from '../../features/guestRequests/services/guestRequestService'; +import { getMyBookings, Booking } from '../../features/bookings/services/bookingService'; +import { formatDate, formatRelativeTime } from '../../shared/utils/format'; +import { logger } from '../../shared/utils/logger'; +import Loading from '../../shared/components/Loading'; +import EmptyState from '../../shared/components/EmptyState'; +import useAuthStore from '../../store/useAuthStore'; + +const GuestRequestsPage: React.FC = () => { + const { userInfo } = useAuthStore(); + const navigate = useNavigate(); + const [loading, setLoading] = useState(true); + const [requests, setRequests] = useState([]); + const [bookings, setBookings] = useState([]); + const [showCreateModal, setShowCreateModal] = useState(false); + const [selectedBooking, setSelectedBooking] = useState(null); + const [filterStatus, setFilterStatus] = useState(''); + const [requestForm, setRequestForm] = useState({ + booking_id: '', + room_id: '', + request_type: 'other', + title: '', + description: '', + priority: 'normal', + guest_notes: '', + }); + + const requestTypes = [ + { value: 'extra_towels', label: 'Extra Towels', icon: Package }, + { value: 'extra_pillows', label: 'Extra Pillows', icon: Package }, + { value: 'room_cleaning', label: 'Room Cleaning', icon: Sparkles }, + { value: 'turndown_service', label: 'Turndown Service', icon: Sparkles }, + { value: 'amenities', label: 'Amenities', icon: Package }, + { value: 'maintenance', label: 'Maintenance', icon: Wrench }, + { value: 'room_service', label: 'Room Service', icon: Bell }, + { value: 'other', label: 'Other', icon: MessageSquare }, + ]; + + useEffect(() => { + fetchData(); + }, [filterStatus]); + + const fetchData = async () => { + try { + setLoading(true); + + // Fetch active bookings - only checked-in bookings can create requests + const bookingsResponse = await getMyBookings(); + + if (bookingsResponse.success && bookingsResponse.data?.bookings) { + // Only allow requests for checked-in bookings (guests must be in the room) + const checkedInBookings = bookingsResponse.data.bookings.filter( + (b: Booking) => b.status === 'checked_in' + ); + setBookings(checkedInBookings); + } + + // Fetch guest requests + const params: any = {}; + if (filterStatus) { + params.status = filterStatus; + } + + const requestsResponse = await guestRequestService.getGuestRequests(params); + + if (requestsResponse.status === 'success' && requestsResponse.data?.requests) { + setRequests(requestsResponse.data.requests); + } + } catch (error: any) { + logger.error('Error fetching data', error); + toast.error('Failed to load requests'); + } finally { + setLoading(false); + } + }; + + const handleCreateRequest = async () => { + if (!requestForm.booking_id || !requestForm.room_id || !requestForm.title.trim()) { + toast.error('Please fill in all required fields'); + return; + } + + try { + await guestRequestService.createGuestRequest({ + booking_id: parseInt(requestForm.booking_id), + room_id: parseInt(requestForm.room_id), + request_type: requestForm.request_type, + title: requestForm.title, + description: requestForm.description, + priority: requestForm.priority, + guest_notes: requestForm.guest_notes, + }); + + toast.success('Request submitted successfully! Our staff will attend to it shortly.'); + setShowCreateModal(false); + setRequestForm({ + booking_id: '', + room_id: '', + request_type: 'other', + title: '', + description: '', + priority: 'normal', + guest_notes: '', + }); + await fetchData(); + } catch (error: any) { + logger.error('Error creating request', error); + toast.error(error.response?.data?.detail || 'Failed to submit request'); + } + }; + + const getStatusIcon = (status: string) => { + switch (status) { + case 'fulfilled': + return ; + case 'in_progress': + return ; + case 'cancelled': + return ; + default: + return ; + } + }; + + const getStatusBadge = (status: string) => { + const styles = { + pending: 'bg-amber-100 text-amber-800 border-amber-200', + in_progress: 'bg-blue-100 text-blue-800 border-blue-200', + fulfilled: 'bg-green-100 text-green-800 border-green-200', + cancelled: 'bg-red-100 text-red-800 border-red-200', + }; + + return ( + + {status.replace('_', ' ').toUpperCase()} + + ); + }; + + const getPriorityBadge = (priority: string) => { + const styles = { + low: 'bg-gray-100 text-gray-800', + normal: 'bg-blue-100 text-blue-800', + high: 'bg-orange-100 text-orange-800', + urgent: 'bg-red-100 text-red-800', + }; + + return ( + + {priority.toUpperCase()} + + ); + }; + + const getRequestTypeLabel = (type: string) => { + return requestTypes.find(t => t.value === type)?.label || type.replace('_', ' '); + }; + + if (loading) { + return ; + } + + return ( +
+
+ {/* Header */} +
+
+
+

Guest Requests

+

Submit and track your service requests

+
+ +
+ + {/* Filters */} +
+
+ + +
+ +
+
+ + {/* Info Banner */} + {bookings.length === 0 && ( +
+
+ +
+

Check-in Required

+

+ You need to be checked in to submit service requests. Once you're in your room, you can request extra towels, room cleaning, maintenance, and more. +

+
+
+
+ )} + + {/* Requests List */} + {requests.length === 0 ? ( + 0 ? "Create Request" : undefined} + onAction={bookings.length > 0 ? () => setShowCreateModal(true) : undefined} + /> + ) : ( +
+ {requests.map((request) => ( +
+
+
+
+ {getStatusIcon(request.status)} +

{request.title}

+ {getPriorityBadge(request.priority)} +
+

+ Type: {getRequestTypeLabel(request.request_type)} + {request.room_number && ( + <> + {' • '} + Room: {request.room_number} + + )} +

+ {request.description && ( +

{request.description}

+ )} + {request.staff_notes && ( +
+

Staff Response:

+

{request.staff_notes}

+
+ )} +
+
+ {getStatusBadge(request.status)} +

+ {formatRelativeTime(request.requested_at)} +

+
+
+ +
+
+ {request.assigned_staff_name && ( + Assigned to: {request.assigned_staff_name} + )} +
+ {request.fulfilled_at && ( +
+ Fulfilled: {formatDate(request.fulfilled_at)} +
+ )} +
+
+ ))} +
+ )} + + {/* Create Request Modal */} + {showCreateModal && ( +
+
+
+
+

Create New Request

+ +
+
+
+
+ + +
+ +
+ + +
+ +
+ + setRequestForm({ ...requestForm, title: e.target.value })} + placeholder="Brief description of your request" + className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#d4af37]" + required + /> +
+ +
+ +