From 4ab7546de093d73117df17358b6e836d3b685d25 Mon Sep 17 00:00:00 2001 From: Iliyan Angelov Date: Fri, 21 Nov 2025 10:55:05 +0200 Subject: [PATCH] updates --- .../alembic/__pycache__/env.cpython-312.pyc | Bin 2790 -> 2705 bytes ...add_privacy_terms_refunds_to_page_type_.py | 36 + ..._add_cancellation_accessibility_faq_to_.py | 35 + ...31_add_mfa_fields_to_users.cpython-312.pyc | Bin 1591 -> 1439 bytes ...erms_refunds_to_page_type_.cpython-312.pyc | Bin 0 -> 1208 bytes ...tle_fields_to_page_content.cpython-312.pyc | Bin 22089 -> 21992 bytes ...e93_add_page_content_table.cpython-312.pyc | Bin 4833 -> 5897 bytes ...ry_section_fields_to_page_.cpython-312.pyc | Bin 2300 -> 2142 bytes ...gration_create_all_tables_.cpython-312.pyc | Bin 24262 -> 24795 bytes ...ty_room_size_view_to_rooms.cpython-312.pyc | Bin 1585 -> 1421 bytes ..._add_system_settings_table.cpython-312.pyc | Bin 3308 -> 3450 bytes ...tion_accessibility_faq_to_.cpython-312.pyc | Bin 0 -> 1266 bytes .../add_about_page_fields.cpython-312.pyc | Bin 2180 -> 2044 bytes ...add_badges_to_page_content.cpython-312.pyc | Bin 1057 -> 902 bytes ...right_text_to_page_content.cpython-312.pyc | Bin 1056 -> 907 bytes .../add_stripe_payment_method.cpython-312.pyc | Bin 1494 -> 1336 bytes ...omotion_fields_to_bookings.cpython-312.pyc | Bin 1941 -> 1794 bytes ...ry_content_fields_to_page_.cpython-312.pyc | Bin 4310 -> 4154 bytes ...dd_map_url_to_page_content.cpython-312.pyc | Bin 1072 -> 928 bytes ..._add_paypal_payment_method.cpython-312.pyc | Bin 1501 -> 1356 bytes ...dd_is_proforma_to_invoices.cpython-312.pyc | Bin 1071 -> 929 bytes ...y_sections_to_page_content.cpython-312.pyc | Bin 2960 -> 2805 bytes Backend/seed_policy_pages.py | 517 +++++++++++++ Backend/src/__pycache__/main.cpython-312.pyc | Bin 15229 -> 16348 bytes Backend/src/main.py | 16 +- .../__pycache__/page_content.cpython-312.pyc | Bin 5333 -> 5785 bytes Backend/src/models/page_content.py | 8 +- .../accessibility_routes.cpython-312.pyc | Bin 0 -> 3372 bytes .../cancellation_routes.cpython-312.pyc | Bin 0 -> 3371 bytes .../__pycache__/faq_routes.cpython-312.pyc | Bin 0 -> 3271 bytes .../privacy_routes.cpython-312.pyc | Bin 3605 -> 7091 bytes .../refunds_routes.cpython-312.pyc | Bin 0 -> 3339 bytes .../__pycache__/terms_routes.cpython-312.pyc | Bin 0 -> 3321 bytes Backend/src/routes/accessibility_routes.py | 44 ++ Backend/src/routes/cancellation_routes.py | 44 ++ Backend/src/routes/faq_routes.py | 44 ++ Backend/src/routes/privacy_routes.py | 131 +++- Backend/src/routes/refunds_routes.py | 45 ++ Backend/src/routes/terms_routes.py | 45 ++ Frontend/src/App.tsx | 32 + .../common/CookiePreferencesLink.tsx | 12 +- .../common/CookiePreferencesModal.tsx | 223 ++++++ Frontend/src/components/layout/Footer.tsx | 52 +- Frontend/src/pages/AccessibilityPage.tsx | 178 +++++ Frontend/src/pages/CancellationPolicyPage.tsx | 179 +++++ Frontend/src/pages/FAQPage.tsx | 178 +++++ Frontend/src/pages/PrivacyPolicyPage.tsx | 187 +++++ Frontend/src/pages/RefundsPolicyPage.tsx | 187 +++++ Frontend/src/pages/TermsPage.tsx | 187 +++++ .../src/pages/admin/PageContentDashboard.tsx | 691 +++++++++++++++++- Frontend/src/pages/customer/BookingPage.tsx | 10 +- .../src/services/api/pageContentService.ts | 32 +- Frontend/src/styles/index.css | 34 + 53 files changed, 3091 insertions(+), 56 deletions(-) create mode 100644 Backend/alembic/versions/0e2dc5df18c3_add_privacy_terms_refunds_to_page_type_.py create mode 100644 Backend/alembic/versions/9bb08492a382_add_cancellation_accessibility_faq_to_.py create mode 100644 Backend/alembic/versions/__pycache__/0e2dc5df18c3_add_privacy_terms_refunds_to_page_type_.cpython-312.pyc create mode 100644 Backend/alembic/versions/__pycache__/9bb08492a382_add_cancellation_accessibility_faq_to_.cpython-312.pyc create mode 100755 Backend/seed_policy_pages.py create mode 100644 Backend/src/routes/__pycache__/accessibility_routes.cpython-312.pyc create mode 100644 Backend/src/routes/__pycache__/cancellation_routes.cpython-312.pyc create mode 100644 Backend/src/routes/__pycache__/faq_routes.cpython-312.pyc create mode 100644 Backend/src/routes/__pycache__/refunds_routes.cpython-312.pyc create mode 100644 Backend/src/routes/__pycache__/terms_routes.cpython-312.pyc create mode 100644 Backend/src/routes/accessibility_routes.py create mode 100644 Backend/src/routes/cancellation_routes.py create mode 100644 Backend/src/routes/faq_routes.py create mode 100644 Backend/src/routes/refunds_routes.py create mode 100644 Backend/src/routes/terms_routes.py create mode 100644 Frontend/src/components/common/CookiePreferencesModal.tsx create mode 100644 Frontend/src/pages/AccessibilityPage.tsx create mode 100644 Frontend/src/pages/CancellationPolicyPage.tsx create mode 100644 Frontend/src/pages/FAQPage.tsx create mode 100644 Frontend/src/pages/PrivacyPolicyPage.tsx create mode 100644 Frontend/src/pages/RefundsPolicyPage.tsx create mode 100644 Frontend/src/pages/TermsPage.tsx diff --git a/Backend/alembic/__pycache__/env.cpython-312.pyc b/Backend/alembic/__pycache__/env.cpython-312.pyc index d39f425a09b771e364b47aa5368fb98cfe1d2eef..7bea133b2ef00c44e4504ae5be67bdc808569556 100644 GIT binary patch delta 460 zcmaDRI#HDGG%qg~0}$j*mCuZ0-N@I-tizB3gsYiAq6`d-43!MZ42BHFjO9#>43R)S zNTh-}lA)YYlg)4PU1q<@IxJgxM1g9Hqk%*N!=1@*StLbgD6gqMk#Zp^??OTOg~W=h zER}aS1vV?P)-fugSpc#Q)xyaK*fhCdmazCu7GXD??8QE-UK(V{9Ec@CDoetytD0X_ zHQ&Q_-6HU!Mc{?t(95b}7vgiSRMdQDV5nqtWBLf8zA!KdyE8R--{2Iv&M9+|Q|2%3|gdDSlSYJ6m1v(!aqsSC_%H(B^P%C7UPUF280!lHhIU;H}1%0+&a y2A&%XY}XmYE;5K+5LaIzbA`d|hLG5F-HEyl{GVA^7!^PBurjKC;F)}!OCA76)qxuT delta 570 zcmbOz`b?DXG%qg~0}wFZlFU?R-^ka43R)S zNTh-}lA)YYlf6nYs5DO@H#5B`u_QA;uUH{7PeDCDEiETAFI8P3H$Np+Z}M(tKSsaF zoGe=%l!4|ICjyBEh9_JC*SQofaw%L;G`!4Z)Zl!Fh5I^-=tUOM8$A4XB-C$+N!<`r zTHt;`ROc%vD=XUOhwQ>r zY^FdL{%4z&WC(KM42TPbRPJ!_UFVRx$RRby?7FncMQM`_NtZb+Z}7|95fYoOJ5l!w zJBu(|gZBp>1}@R-oH7?VWiE5dePCc?Rb8NUhlBq*hvY>L$s6K|TkO9wGjVc#;b7ns zo&29&L53Ts1g!2m7b7Q-1tflbhYD`C=4fSPyf*nDrvlGMHU>tX7Tym)ie>UkPM!L% zK$hxV28}xm5d6k_-y2ut7Isk)`of!ZC diff --git a/Backend/alembic/versions/0e2dc5df18c3_add_privacy_terms_refunds_to_page_type_.py b/Backend/alembic/versions/0e2dc5df18c3_add_privacy_terms_refunds_to_page_type_.py new file mode 100644 index 00000000..42136132 --- /dev/null +++ b/Backend/alembic/versions/0e2dc5df18c3_add_privacy_terms_refunds_to_page_type_.py @@ -0,0 +1,36 @@ +"""add_privacy_terms_refunds_to_page_type_enum + +Revision ID: 0e2dc5df18c3 +Revises: f2a3b4c5d6e7 +Create Date: 2025-11-21 10:25:07.463477 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '0e2dc5df18c3' +down_revision = 'f2a3b4c5d6e7' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # For MySQL/MariaDB, we need to alter the enum column to add new values + # First, modify the column to allow the new enum values + op.execute(""" + ALTER TABLE page_contents + MODIFY COLUMN page_type ENUM('home', 'contact', 'about', 'footer', 'seo', 'privacy', 'terms', 'refunds') + NOT NULL + """) + + +def downgrade() -> None: + # Remove the new enum values + op.execute(""" + ALTER TABLE page_contents + MODIFY COLUMN page_type ENUM('home', 'contact', 'about', 'footer', 'seo') + NOT NULL + """) + diff --git a/Backend/alembic/versions/9bb08492a382_add_cancellation_accessibility_faq_to_.py b/Backend/alembic/versions/9bb08492a382_add_cancellation_accessibility_faq_to_.py new file mode 100644 index 00000000..70dd4b72 --- /dev/null +++ b/Backend/alembic/versions/9bb08492a382_add_cancellation_accessibility_faq_to_.py @@ -0,0 +1,35 @@ +"""add_cancellation_accessibility_faq_to_page_type_enum + +Revision ID: 9bb08492a382 +Revises: 0e2dc5df18c3 +Create Date: 2025-11-21 10:39:56.040401 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9bb08492a382' +down_revision = '0e2dc5df18c3' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Add new enum values: cancellation, accessibility, faq + op.execute(""" + ALTER TABLE page_contents + MODIFY COLUMN page_type ENUM('home', 'contact', 'about', 'footer', 'seo', 'privacy', 'terms', 'refunds', 'cancellation', 'accessibility', 'faq') + NOT NULL + """) + + +def downgrade() -> None: + # Remove the new enum values + op.execute(""" + ALTER TABLE page_contents + MODIFY COLUMN page_type ENUM('home', 'contact', 'about', 'footer', 'seo', 'privacy', 'terms', 'refunds') + NOT NULL + """) + diff --git a/Backend/alembic/versions/__pycache__/08e2f866e131_add_mfa_fields_to_users.cpython-312.pyc b/Backend/alembic/versions/__pycache__/08e2f866e131_add_mfa_fields_to_users.cpython-312.pyc index be1ff3d87b5f6a15a6cb81a46bda55d2f1e05414..cc807098ff88028e43c02f4f83735da8ed6e1da5 100644 GIT binary patch delta 191 zcmdnaGoPF9G%qg~0}$j*mCsCMn#d<%X946*XGmd4Va#F3WsG8E1hJWNn4*|dn4?%y zSfW@{n4;K#m_3C(g|&qxiUUZq0cp-kCQXiwuWmE(Z~%=c&IS?<40kqPWR7Iy;RT84 zLPRE;v05{7O|E7ww^0U)FoLACfW!x8Mn=ZF3}Uw##BXp*HgMb!&}rbkAs_-Icm*4{ UKC>`0hA@8NVrJAX5(6p+05=CISpWb4 delta 322 zcmbQwy`6{eG%qg~0}!-!%Vwr9Pvn!Rw*m5|Go&y?F{Ci2Fy%1jGDR^lg4oPC%uy^U zEK#f}tWj(!%u(z>%#p&8!q&nP#R;U@fizbovnFR%Lt;uwd~RA|d|GB|PD*inNq&53 zacWU97gtbfS!Qu&ex8D-iFrQ_{ncr%3W1}Qj8!q#z5i&Gb1D8MH`39gHLh* diff --git a/Backend/alembic/versions/__pycache__/0e2dc5df18c3_add_privacy_terms_refunds_to_page_type_.cpython-312.pyc b/Backend/alembic/versions/__pycache__/0e2dc5df18c3_add_privacy_terms_refunds_to_page_type_.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd5b9465ec65f3721111e1fd48dba2bb70c054f0 GIT binary patch literal 1208 zcmcgr&u`N(6n4@yX|om$F*HB|SzJmbwzS=kqH03g)*&`^oiuF+a*5K|?aI-DN7 z%TJt{(F$fY*sjB*Hn$s%_zFid6_rLB-p5WD z;3F^;wO#mzNNseyeWQ-qZa{qFIZx+KEfKQY^*Y9q?d)O?8Mcdit*&G2VJZY<#&wRZ zzkCGKem~Meze8<=X^!W=#f{zOFaT#NNNexc-mIMhWju2ZlysJgC;TrJ`*929PE{gUQnj$lV)cPR1V$@+adH9P&2@ P`cT!R?Ae4Wjnw`DMJz9w literal 0 HcmV?d00001 diff --git a/Backend/alembic/versions/__pycache__/1444eb61188e_add_section_title_fields_to_page_content.cpython-312.pyc b/Backend/alembic/versions/__pycache__/1444eb61188e_add_section_title_fields_to_page_content.cpython-312.pyc index de4db8954d890a121a892edabd81c0d4d574a04f..0fa0dbbee64b337cce0fc8793e8cf0368a58fdfd 100644 GIT binary patch delta 421 zcmX@PhVjK}M!wU$yj%=GkT+F6bD#4>J_#=uAa6QD3PTEG4nr&_%$dTG!q&nP#Ra6PXfsX zhC7=()I?dVkC+~}J!pF&KH-XU;)R^TFANMmOnyw?K~yl)M+o(Sfgyw`lIb&u7sd3I zfq^xe=?)9q}a(DrPDNE2{u0+uWu7U7ouLWb+fS z&68z)6*jN&HfNQGX$ESEWAp&&jA2Ru>r4b&@F(Cfnga`f4)kX71v${0DG2015Czm2 z%oG7~pf}K+AO}W)ZGIeB&&(j374{0*MdIjEszT8N_Zgh~H&Uy}>Qn mz;Q!Bu7UT4fC!M_6>Q-8%#zN?=+5{hh#5%xGZxJN>H+|AE_X@* delta 480 zcmaE{n(^csM!wU$yj%=G@K8oRli6(|pG1HMkT;zng&~R|g)xOGhcTBaiir`#X3k-b zVgb@DIjp&CQEWgl)+qKAwkVDi<|s}e=1SpAVQ*oH;s(+jK$@qLS(B@(dGan*jc6{e zpwzO=;>`R!1y2_%1w#`PlhhBMfwO@fuDBh6aWwoBykd zvZ&mVRhl0=Gj@mI6qZ?k>x8N1EctD2JyQL usvp=Gc%&LQZV1RV@ZJy*0TR4|4P2ku@);T38NZ}4=P@$+GZxJNY6k#JHi<$2 diff --git a/Backend/alembic/versions/__pycache__/163657e72e93_add_page_content_table.cpython-312.pyc b/Backend/alembic/versions/__pycache__/163657e72e93_add_page_content_table.cpython-312.pyc index d09e68da1da928eb1c2c74ba98152022e795a745..953fc19d84164fc31c60c15bb28cae4acc052885 100644 GIT binary patch delta 2053 zcmXAqOK%zn6h>#5_rMHqa5wELi!R#!fG(QYV1jLojRC{hJnTXYn1?ZTZ1eEAQl+Z9 zMp4yO7nLHl+bEIx7bUY_1vOi=W<$f*yQvU7iY3_IDvGZjAkK~J6 zxfl83%Y5Z|zVfWvesSD;d3^f(`1IN6>Wv^w#iR#Fc{YhHPl<3mHR7J<#PePdy5f-2 z)N@C|R1lYmgtY2PAIOaKb^$b-Qh{<;D%0&sbvgpBB^@tZm(ztbS)7>?*V1hn3)fOg z#sY!luAGzM@^X=JeRMSUQ1(QDqnMBDkl|e5J*;)9R;qS>?&{t zWuK0CM^u@#l^&C++$TJxl0?E z@4Kdv1c?4Y!GOg#(-{UjOd2On2vy3Hk0Put(us&%>t)AqezrY$K+Ep z4@^Ea0Lh_wN_S@7r#mwzbZ0JzS9oU5*xdDQc<1{WBJ17uBF%bBUZez)viEDcruT@h z>Fv`IaQD6Ag`03%^{x5lSJsJ)FYCiX##i)Vfk3kBbMH~Md>zV`Z%9c%UioGVcg-nn zWvuy`O=8Q+SysVd7jy0qs@B%y5h{?6JY|qzMh@_ozZKO$i(?&|5 z$=Vglntec5v+vOnI8(D*Oh)#_V*ipD+f$Zfd&Q5EK$7t1{BU`H(GQpR@6i#slJ{4b zwESH^-+yDf$8_KyG9CCYnG$HO{K)`ZDv%Dqr2<<4I0CK|*ju=LPDg=bP6L0J=m$=? z&<|X2fk1K@NCn~2!Sx_qI+zQ-#ruzhbb{4|ta0iD`<(iL0Wl1YxG@ZlgSbW@nFLcI zUJI>r2#vTf42`ki&dF<`wJ@5E@MaitBfJyFH3I*y zjc_51q#kZC8HSp~QMk?WD15?_Kr#$p(M`fLx=DCB0!QG=B%FvK$wqb}^YuL879*ty z(qg2>l;B^AbSS%#K3z95q9bso8#!lkJDP|hDMpe+Dw>X>Ohvb%CBUZ&%%UUW zJa)nIJa)~JK;p*M<8YaHHV&7Gm*Q}l_(~~`Gnu#(N7IaV*qq0XiC%obv=>h-AtjI` zm(ojE=*8ETkSOa*_|Sav)c>#Ov-v@~jt6{@K714`i@t|i=)qST_>D;Q86Mt9qA0!= qVt)zo*TSt=-j8ndSAmajtXF{u9<1Pv`A(L^rug2J#V=03#K!;PfEaH8 delta 981 zcmZ`%Z%7ki7{A-z-L`8RmfFgRU6PDJ>h9+LokF!HG))9E#QSp1-LaN)(>b#BWfKC| zw}yfpJ6NLNeY}WZ-|Az~7o!)F{t$uRGg{Ec-f6D{=E3_s_x^sr=ilA)xbIt!700>a4Rl*4Btjr0QZl5;IfGyzoA1W7Q2@w;v}ukKD1?~=vqT9N$tYOS zp(U$ieguS5$Xk%N6^tJG;X+)J#QAtq78BV_Ue4sje0(M?)AWRVKed?3W|&x%XV`!* z;19|{PL70qhm^d?Gl4kE1riDW4ChPG*K=|_FEdeOc!u+G{$7^tWkU=b;=LU2_4N&e zykS2})6eHeY{gbI#^Or@L6nFFMVQPhKSEK08B)(e$_W`xemdLDuyOx#nuYaYd;4Zsd> zy*L2DRm{w(6SXO2syeN=K@h-bNK8Bt&Zu!KbIARiZa&`3OBDiid zVo6!57If5nF@)i}s-b39%vGy~hCTY#&0*b&yXw&-2z*%ETmW9-t3!GZ1VX<$Z0gtuE&=0`%4!E91--C+{zr|&u8X>fIFTS_#k+xuS8%L2T5qqm&+ox&!N3$qO z*@P&rhR;@FV~8S~nx=5yNRs3a14V*g4uW)kBAj1{o_&h2b(Ac-EnOx1Zc7_7_R}T$ SC&iF5*|H-2 z01}J=3*MQ0hCPdsYqA4Jxs4)Fgb^gA1tdN&Gcq#XWe~f~Abx{evVr4n1OExnxwoFbohRCEATPY-^C7PQgrJ5uenVNAq7o{eaq$;=o2`dF7 z10z#iLqlC70|i54D`RsjBLh7HLsJtYBQCCuDfuh{!a(;HUjq^i3}4t7SlKr}WC>>C zkpqdwfJL87c3{uq;=aWmAD@z+93MYSc}q{bLXd|+l|Wcn!OkAYGA rHiP&F4h9~n296s7at*vU1Vn%YuV4e$XEqMTc*ZYc?2I-=B0zlrRy9wJ diff --git a/Backend/alembic/versions/__pycache__/59baf2338f8a_initial_migration_create_all_tables_.cpython-312.pyc b/Backend/alembic/versions/__pycache__/59baf2338f8a_initial_migration_create_all_tables_.cpython-312.pyc index 8852a425ffc1254323da0a010aba86f71a71f9cf..c07faa241f5ad87deb5405976dc83313f7dfd852 100644 GIT binary patch delta 1516 zcmY+EU1(cn7{|~1p6`#039`DYj>5$5tJR_tWw55{(k@Al%U7FhP4n5-E=jXAO`3is zf*@XL3C@e+#c;6Qcu_Lgjk_otPV^>X(CM~&*;U7;yY=~>;Dv#lU!MH_&y({6&ilhX z_IRHi`q||&1bP*}>i_uq#Ll5NE_$f?TF44nmKO>v&KN%PJdcY+F)tP5xJ=(kaV5*+ zD!ny&>sd7`e=5ZdqCzz5d{Eo_=dZUKI*nW5EX??25_JHSNl8VgLn_0Sq;)ueHY`b- zhe5Nx-KDRBG_4VU-#HibT{DTt>~Q zuNq(;5+GSot36pm<<>4}&KNH+srfXTHmLq2#-K>XSU{vv(HFrbBai5kv4SW-nTm1VBfFc{F$0DqoHsZRbHHTn4zo-aQS;rRk+ZkWe7&Gqv*|C{Ds<}|l;c<8%<2jdR|`+@s* zXEsH#RW^t83a>e_HN?LCV~Bpe_2=+6bPsE9>nILJj<$B+`J6r-ANh&CJ{TG9H>)^S zp3%|O6k6YhkF|ZFvvU7`^J!{=Og_x6sLMW7}&MEury3ICTd`x~=b&t`YrztXA<>kiZ9KxB+P q6^=X>`ko5MADKs++M~m-H(kAp>$RrwLb}SBm%WUO#AD?)HT?@bsG=+Y delta 973 zcmZWmO-vI(6y9m4+jePDKy3>pEFvN*wcApDYWx9khi;Yuf@oSFY2$tXgr|<#4f>i*?R{E4I#2Wfkev&0Iref8clGWg= z#h2nV961BoBCOR|>n4@;+FKF#s1OpoKH4u_4|qeu*r<~p3h>?#PkVhnI^-Sn@j*H) zghps#l;bDP^FfO097?-gPTJ8r=pDA%?JdJC-hzh@I%(=up@<7ta?&=d&C$Rx z4NMcwG&!wyr>)6iXY35qOi?dyd^<2zhNlXOc+?1ygXG7EDOYuIp4yYF!i+vhrG zJEPqxm>x&4L&k-8X`);#kC{^tL5GZv#1#pxs7;n3V?$bn;Bnb877rvQ#mSTcRoT-# zf-c$NkM~X`SdoogO}P+km0@4JJ>eGJ$zEi(rW+AFxz%w2nVB@sXxr-8mLV}Uh=!Oc zc@aUI94<30MaFA~Q+TptvPYsC7Q?a06zZCwNllbr*L}ohxLVdGce}K8Y;4MXSTX_Lkm@Pc^wW|c+E)l zmu!zfZn4j+CT(xMq)T}c7SGG;6iJR8tHB!?V z_U#sS=56exz{NX#oyxyk9)8Hr`DgvhmiG5{w5Mm8^&mL#@2^g0U`@7kf400PTVmQM zBJl?mOU)S1XG*`K_IS9lA&)1j8JO)tm@kkxw_>rb1OWV0kN|EJ6Tq-a7`_s<>m;Eo gk7%+xcSZCCp)ZM0zsPO{I0NznZ~_22u!ysN1N72N7XSbN diff --git a/Backend/alembic/versions/__pycache__/6a126cc5b23c_add_capacity_room_size_view_to_rooms.cpython-312.pyc b/Backend/alembic/versions/__pycache__/6a126cc5b23c_add_capacity_room_size_view_to_rooms.cpython-312.pyc index 30060d3e11ee619781001427545496a1226d93d6..fadc4c165d9175f3f5f9c29a4dcf27406b37428c 100644 GIT binary patch delta 191 zcmdnU)630wnwOW00SNM@%4ga#P2`iXvjFm@Go&!2Fy=7iGDa~ng4j$sOi|1!%uy^U zEK#f}Oi^q=%$~xY!rH2Ge4BO~Ko2C>@=;y1V@8#rzVXgBcQ5D)-pIpunwOW00SKZDq%&ifC-OXYvmw6;&>-pwzO=;>`R!1y2_% z1+zp$BeUdW(9!Zdh4n$<~d=@J%?py5f@hSPq z@$r*yv6Q3V%W^%2dHH~-yO`sMPN<>-(dx_9mqtvEEM+qsJ)L7G)rkXS+HCec+ z8?bU z@bwWpcFvx?)ac7Jvbj&$;`?mTURvHDWQdKh--vPc^B>xTOtL8JWYY|G36{d$Wjc#U zvpFoJ*#Z_2ZBiw9R>H8%mN6`|5e^|@9TDa*<`j?ePJbE^<7YXTF`hsZ0m=-|VwU51 z%yPVl83Lx{cnOuT7!#fPIATIX1enzc5fz{!z?l{^7$!vu!=%V!hJZ0ikwc{{hGeHd zj2M;2B$%V}gai`-N<^N=EG`q6#bp{Z1Wbv`IaIF7Qm0;(*N_!?9o34QQlKL6W0Zx6 zsuUuv@`wv+QNfssyrQ62P)iDW1r^bthXCcIc3K0apr$lXjHvei-#%oI#C>6J4}J6< zv=5D~h1P<0eZLXl&ESETXN=u!S2v!HIaq3>&A4=9(X-;YU0iiE^u7&Mf8ZSU8pO5b z7opPqS$DNLt$%k0sH^UG-8_nCmEwqfus}pTU#X1boSKrX4j6IyV|_GjlXTE$f^JU delta 603 zcmew*^+uBKG%qg~0}!nGAe9-wF_BNA-UG;+&XB?o#gM|7!j!|9%M``L2x2qmFh{Wf zX_g$;T(&4SpcrcudkR|=M+$QkClGU`aHg=gutaeKX$~OGQ^~B!RaKIhk`iBBSzMBu z8(*ASQj(dMUL0SNn3R*s#TArVmRX#cpQqsIVx?edmTY94lIWUZVqlsAkxDJLQZThl zN=!2{HnvE!NaS)ZN=+GtFmPt|-02VG0w~T_Utx ze4+Sa$qgY_IILizI!l^$Ld_Ojv)3(Q>PWR*P+Rs6Y(9&LMx1Lwoh) z3?EonSb0HK@OAP{C||&Jg+u!b&tz*JM}B?=9??$>Osu>QIe0#>GEMI0QRU*j#U3A@ zlAjzOKY1fhmxn9R2w-?I6q^Bw56p~=j31fU7+JpZF))hXW)Q#2pbE55MYLPWiD*yn+tiu2R diff --git a/Backend/alembic/versions/__pycache__/9bb08492a382_add_cancellation_accessibility_faq_to_.cpython-312.pyc b/Backend/alembic/versions/__pycache__/9bb08492a382_add_cancellation_accessibility_faq_to_.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ad15f7c72915cab654a4df4e727a2c2a20b21032 GIT binary patch literal 1266 zcmd5+&rcIU6rSCdZo9OKKN6xbW-g{Sv20twKtoJ`VuX~E5IES&WI8h~E8CrAc8io# zO-#J$k-vfPr+D;2)}vm$VJa6+&TI=*6B7>}oXvdi&3kWW-+SN8e#~S{Acy-1+V5ik zd{K+bp%0b*f>e$G1$96*sykX+uj`u9V@|9dr}6q29jhm3tZvAeq)D218?UFNG^8}U zv0Uo#1Ev%a>=NcUIIy{kFd@wM?S}2xK^Hag9tt?}aEqa!>oLUKPTMp$*}m<|Xt-J_ z!}&&|FgG(_!qao5frt5JSYRbeW@)oHN2bkX!EnG}MM4>t3Z>b6v6wFvVX;u2o-fZn zohr=8r)ZkrRBbse)VWs+)1y0w>Cqj@um_Ql4FYQRrz9MKK#QyrUR}28(jX<(PKJIB zbU{w99ey(5a4c50RyN_*;!<@5MkN#O2Fwk7conj~QCWTd5-x94x7TZfAk`PRQrljC zoZaDVmVE-V>OD*XrQ!zfL`swMfC**t8CSv+_C6+ErAkNJSBhZGj!PqTlojRrg&1X! z++=gtnbkJ7U~RivjZ!_LA}v3EN6x;29{3thyjy#- zb_U`T$#W1NO9^%0|0S&_SDgJnwEl@MVaVb|N`Y#F7?%*HD1Yq^ns-yKA1FsgfeOj# zVM4H=BiveUJTwrZoFEh$L*54LoBp1I9kRpP-B9;&m=wc%8)nqr=z7@@@*l8+9Nb`z zADWbT@(}olyAf|fegC)gE|LpvPCE{JChkeEx>J5h!dXnywC{RCi=W>B+T>?2d6op} e`@Q7p#KT_ZbmFdr%&nf;Pb9R|kJ}QWdVT^W!$a5r literal 0 HcmV?d00001 diff --git a/Backend/alembic/versions/__pycache__/add_about_page_fields.cpython-312.pyc b/Backend/alembic/versions/__pycache__/add_about_page_fields.cpython-312.pyc index cc32c87c22815ae28a6890c0da9ff6cb0062e7c3..02eb039157d31fed6594fd2c28892ed59e18a9e9 100644 GIT binary patch delta 191 zcmZn>{KL<8nwOW00SNM@%4a@ep2#O*X946*XGmd4Va#F3WsG8E1hJWNn4*|dn4?%y zSfW@{n4;K#m_3C(g|&qxiUUZq0cp-kCQXiwx1?EkIDkeJp9T^Q40kp&vidOb2!KSw zAtI9_*;5(0CZAw0w^0O&FoLACfW!x8Mn=ZF3}Uw##BXp*HgMb!kZa(*As_-Icm*4{ UKC`efMlgO6U}Ll>5&%#p&8!q&nP#R;U@fizbovnFR%L1Icud}305X-RxRVtQ(PT4ria zN--B#P-LYh&cags^0X^L5@IYcJ4*h(SM)G)~?**L`{)ilkF%eg2u zu_RT&1xQ#a7#SFu>KYpA8W|`Unp+td0Fj;n7;tfIOp#{c5dylr_%x7cV0f~*o5hET zM+PJk4iTCBm@Soy`xbkAd`fqZ?k>x8N1EctD q2JsJU3_MZ|95)2y8hCFAhyV#*!3M6+Z0w8?j9*0987+!LfcgN9aZwil diff --git a/Backend/alembic/versions/__pycache__/add_badges_to_page_content.cpython-312.pyc b/Backend/alembic/versions/__pycache__/add_badges_to_page_content.cpython-312.pyc index 1c38df1e32d85e9ca03ba84505b6dd7af4c3e933..8f8747bff85e3c93c0cda9d7954e6f95f96fa299 100644 GIT binary patch delta 190 zcmZ3;(Z`9$ z0b6D{My|=J%;h#RKoLfelopWqz|6?Vc$Y!!HiP&LZuJI^8v=3-yf*|yfCR5#1J`F3 RM#dn3dzZ-=4K|TY37Nh23*cXsfi`2 z3NApxO2Np$$W+(BP}k5z!O+0U0EF}mz<`TuV@f9@j}Xu^#mYdUf#J#IpN#8G#6bcA z5CImpcE3iy3HA#-uCnMB@dE`lnQpNarIwTy<@srH-(ruCPsvY?kDq**soYf-D9s4c zYz!nmFf%eTeq>@}WckX+z$kv3LHq+71CK@n#|;6w2HqP2B0z#yuz~9{8xvy?;}OdEN)K>~tM z0c&P?My|;z%;h#RKoLfelopWqz|6?Vc$Y!!HiP&LZpj9Y8v=3-yf*|yfCR5#1J`F3 RMxbN9urV^474ZX=0|1TxC;9*Y delta 310 zcmeBXU%cJdxZrD!g$pwzO=;>`R!1y2_% zg+#+7qh#Y0lT_0*Gl)!Tv6VtvnyH~_in)1WQc^0Hb5UwyNveVikg!rPGB7gLH8j*U zGEguyvobIMB0U2z;Nset(#^;t1axSzDv)Skcry7n<3=8FkbodmUt7xyjp`1q9k zH<-#@Wr0$RAT`E7;sY}yBjZOVHb$1Od<=}@w;9AgurcsRHE`SzkZa(*As_-I Ycm*4{KC>|~MlpWjVq!Ea;s@#j03jktwEzGB diff --git a/Backend/alembic/versions/__pycache__/add_stripe_payment_method.cpython-312.pyc b/Backend/alembic/versions/__pycache__/add_stripe_payment_method.cpython-312.pyc index 30dfef5ef50058864bc3d3fdbe3ff65a3a2356ba..1df86a457fb57bf0dab709fd46c8b2c19216e82b 100644 GIT binary patch delta 278 zcmcb{y@QMIG%qg~0}$j*mCroDG?7oj%LT}r&XB^8!kEL5%NWJT2x2qkFhwx~Y33Z3 zT-GR7pcqROTMBCwdkRw&2M}|naHO!cutaeIX?7sZUCE@$x$(O&6CWqgxMEWv(ZFzr zn|HDlvy|8!4z7N#POc9O3?hk4Ul|xUlb9ZG@ND*Fj%4H$04X;|R<4DlJRGb%g6RV@ z1BXhHAW)em(=E25)RNMoJU>mI$xN)h_HsZ`Mvy7GK;i>4BO~Ko2C>@=;&&NTZ*Z$M haNH1(Yv8>hAOa+K1sk|Nv#>Jqe&J(gj9>yA2mopwJg@)& delta 435 zcmZvXJxjwt7{~7}XS6>J2Pvh4 zt6xD}T%FyeRNVXk5!2mk=py)V_y6BL_j{g)d%^EX?M+oNVDT8`-JCeoa@R-f_5dg# zfI^Czu!RVMvoqJ^2+y2vCRzd!SS~?CDiDcsL}nXPnToeONivt1D}AnjuWss;+CATM zyN=zh_ghZew_A>X-k})RoJ+UocG~)J)zar&?H@4aoSvoc?=;M9TBl{BLh*s;)O|;< zGO={iFe^o)R4nc4CDSqtwziBoFn;Q0P%6W|)w7rcAku)MjfN<{7zsd54)7fg5l{)5 za!AIZ0%Z016`et4V^*^l*L;lzXsP^%Kb`X;kXF4kqY7vuc#ePJwNDD_M6_+%Y1sCt z$=@HYuxB{yR$&ry5W;UHKz^zLnEe3RPp}pWKv@r@acVhG$EgJ-YC6C_LK>>krUY?3 F;TN}jZX5sr diff --git a/Backend/alembic/versions/__pycache__/bd309b0742c1_add_promotion_fields_to_bookings.cpython-312.pyc b/Backend/alembic/versions/__pycache__/bd309b0742c1_add_promotion_fields_to_bookings.cpython-312.pyc index 6981abb185ef91aead659155d76f8e54e8f667e1..61dc1af8a1ccd8bc8a6834e33c83ca9157631a40 100644 GIT binary patch delta 200 zcmbQr-^9mvnwOW00SNM@%4g~`Pvn!ZvjFm@Go&!2Fy=7iGDa~ng4j$sOi|1!%uy^U zEK#f}Oi^q=%$~xY!rHdh&W^g=j9WpwzO=;>`R!1y2_% zg`^Z?1Ir`>a}%RvLx@ajv6VuaVWLrzak5E@X{s5Qb5UwyNveVikg!rPGB7gLH8j*U zGEguuvNANYGO*M$u{1NYFy-Rfm{P;cBMfwC@oXT`!0=@98|HGxC!14PIvIInK;rID z@t>@rT->+VX^l`4||*Z!?I0;9%g9 kYT&pbAlJZqLqG&b@Cr6?eP&~0Ok?~a#L8$`Bn{LD09w{eNdN!< diff --git a/Backend/alembic/versions/__pycache__/bfa74be4b256_add_luxury_content_fields_to_page_.cpython-312.pyc b/Backend/alembic/versions/__pycache__/bfa74be4b256_add_luxury_content_fields_to_page_.cpython-312.pyc index c11c964e9a1e9e319279c724bfe1652849c383bc..91f3f8aeb17fde817822008de976f372d0c77bae 100644 GIT binary patch delta 191 zcmcbnxJ!ZWG%qg~0}$j*mCsD$oX96(X946*XGmd4Va#F3WsG8E1hJWNn4*|dn4?%y zSfW@{n4;K#m_3C(g|&qxiUUZq0cp-kCQXiw@02)sIDkeJGcYkQG%(!REW^dg$|C_1 zxdw` delta 318 zcmdm`a7~f#G%qg~0}%Z9EtgrvHIYxk#RkZm&XB?o#gM|7!j!|9%M``L2x2qmFh{YZ zutc$@utu?^Fh{WiF-HnV3R??H6eo~o2hv=X%$l54Et7*em6DS4^GZ_lO1QX!Qp+-n zGxPHlJYB35lF|~*O_EYgl8j8vATp`NK+zOq1Ir`>a}%RvLoVl{)Wnih1s5P;rC?-W zWU6ausB2`PU}$V*WNu|@q-SVgW?*Q{#kDa-iIYbd=-y%mCI*HEh9{dRadNWqsDeZ; z0tukVK(2xJhJXl=;1z7(`pm}97|8fVg`d&BNEWCM03JC`5C8xG diff --git a/Backend/alembic/versions/__pycache__/cce764ef7a50_add_map_url_to_page_content.cpython-312.pyc b/Backend/alembic/versions/__pycache__/cce764ef7a50_add_map_url_to_page_content.cpython-312.pyc index 0f094e2de30dcc0ebfa265ce45d88588204fd140..b0c5e145e58189631cbce57e553c66e6979dc039 100644 GIT binary patch delta 192 zcmdnMv4EZLG%qg~0}$j*mCy8NoX96(X946*XGmd4Va#F3WsG8E1hJWNn4*|dn4?%y zSfW@{n4;K#m_3C(g|&qxiUUZq0cp-kCQXiw59TxSaR7}dRtFLd40l-ACigRKcMmxVVB+%QA~I^YauuU91$6lT*#jOj6U#6HN`kGSEFW6FF+0U@CK zi#35n1H%_K23GdTf=nBEBte1#V8JJo&oRkyao=K(k59=@j*p-GmZ{uT4k*P4QezAx zJ}@&fGJa%YV`TZt$G|9ln?d{o8v~D21IG;kxdz@F0wO?ySFnNWGaEBw0^=7xCPw`t Gkl6sg@KeG7 diff --git a/Backend/alembic/versions/__pycache__/d9aff6c5f0d4_add_paypal_payment_method.cpython-312.pyc b/Backend/alembic/versions/__pycache__/d9aff6c5f0d4_add_paypal_payment_method.cpython-312.pyc index a27ee628989700a71eb1deaa74c15b6e9a0c7ec0..624886ba43dfd58845c67f17121df25753ff0f4d 100644 GIT binary patch delta 274 zcmcc1eTIwgG%qg~0}$j*mCroRG?7oj%LT}r&XB^8!kEL5%NWJT2x2qkFhwx~Y33Z3 zT-GR7pcqROTMBCwdkRw&2M}|naHO!cutaeIX?7sZUCE@$x$%`O6AvfQxMEWv(ZFzL z@=g{Ru{#`G{al?~9~c-!vYEazFmUEDJ>cNk?8ThS$Rhw!Y>rT@g`_wUtT>7312Y4M zN|7K?ktWkEwxZOM(xN;+O`ge|tiASfKv71J8M;8?12ZEd<6Q=^+YI7&8B}j@OEz%a f5Rhx&y&)h1BzOfIxIVM6GV*@mV`fZZ0viYbzkxmy delta 429 zcmY*Uy-LGS6u$Q+=I1sEEeaK}HG>WnDoJW;8{Dj@i-Y4`Lh#;Tp=nDR1qCsKi-Sn% z;Oql9^f~kaMCs-OM6A2F>f(X(edq5xhx3dlIpay!G2o`%m)i64&{(|M=h;1=fB*_9 z>c9vQ1ZT0(5s1iH?94?Hk$7#6$W$T<6^P0mQb6yqay)*iHbUGt#jnY0;%Vf}d54K2F)+hPOHv>MC_8}&M~Yc}4AS&%TZ z#lbTj%WNJcM%&_2c`5-lcZY9q zhJa4dmw;59X+YIK`5+iRJdQ4fu~wN$8$Z(bktZ&1{*#eEnGBTGxWK0>=v9hYavFCJ zEBdSK`?PoL`=eC6-CN;pINRoO$OH)C2a=%pWdK-w1H}ng%M_q3rOHdboa!(61rB;4 P#h+3E>eF=v;-3rO_?Kvw diff --git a/Backend/alembic/versions/__pycache__/f1a2b3c4d5e6_add_is_proforma_to_invoices.cpython-312.pyc b/Backend/alembic/versions/__pycache__/f1a2b3c4d5e6_add_is_proforma_to_invoices.cpython-312.pyc index a7a4de87df5fce5342d83693e03714a06c987808..e76ed1244fb43dc7c2ad88fcc03fddd7c276e9f4 100644 GIT binary patch delta 190 zcmZ3_v5=kbG%qg~0}$j*mCuY|oX96(X946*XGmd4Va#F3WsG8E1hJWNn4*|dn4?%y zSfW@{n4;K#m_3C(g|&qxiUUZq0cp-kCQXiw4;C`=Z~%=cRs#|Z40k5?GHv4F1_=m2 z1-zN%7`Y}FGnd=Q0Yw-=Qd&Ub12ZEd<6Q=^+YI72xFs7nZV1RV@ZJy*0TR4|4P2jD S7#Xt|zpybf>J-379w=}C delta 310 zcmZ3;zMg~cG%qg~0}xcX$z=vJP2`hsu>ta?Go&y?F{Ci2Fy%1jGDR^lg4oPC%uy^U zEK#f}tWj(!%u(z>%#p&8!q&nP#R;U@fizbovnFR%+2osy($QR8L8)b##hLke3Z5=j z3TcLkMoGrWCMl+=W)PXwVk?Cd%fz%avt-jWgA@}k=c3fal2ip3AYr9oWME{fYiOuz zWT0SRU}a=rWniFZ00vxK8&eiC@(2SRTC4^n8W^5T7G&DQBLNZ+fC`*vlH=mO#U3A@ zlAjzOKlvR~xvM--iV>v77)X3zW@Kdi$i&9T@|BN)QT#T8_y-OK9;pV78v=3-yf*|y ZfCR5#1J`FZCdMqrFI-HFdPRajeE=oKNnHQ{ diff --git a/Backend/alembic/versions/__pycache__/ff515d77abbe_add_more_luxury_sections_to_page_content.cpython-312.pyc b/Backend/alembic/versions/__pycache__/ff515d77abbe_add_more_luxury_sections_to_page_content.cpython-312.pyc index 4f9e448f3790cd0f7da27e711776267d0525ac4c..3b8274e4544a5e73ff122e2889309e96dbc5e718 100644 GIT binary patch delta 194 zcmbOr{#BIkG%qg~0}$j*mCszoI+0Jp&H~7r&XB^8!kEL5%NWJT2x2qkFhwz^Fh{YZ zutc$@Fh#KeF?$Mo3Tq2X6bF!I1Jaz8Oqv`U-@az$<^UQ|Y{JOE(7-Tx1G~{?QFd`= z9$}DR3y^4FxHH*;YZ4>ZjX>M+5X2`|0G37NYk1)`o#U_jl3=IrVHb=90F!LyYL|T9ZP-OCY z&PiO{x7g$3Q}UDJ<0m_DmAk3}rGV-gij9H92WCb_#*a*Fj4WUI7#PKGGl+lSVBnEz j;J6_m*T8#2KmIntroduction +

At our hotel, we are committed to protecting your privacy and ensuring the security of your personal information. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you visit our website or use our services.

+ +

Information We Collect

+

We collect information that you provide directly to us, including:

+
    +
  • Personal identification information (name, email address, phone number)
  • +
  • Payment information (credit card details, billing address)
  • +
  • Booking and reservation details
  • +
  • Preferences and special requests
  • +
+ +

How We Use Your Information

+

We use the information we collect to:

+
    +
  • Process and manage your bookings and reservations
  • +
  • Communicate with you about your bookings and our services
  • +
  • Improve our services and customer experience
  • +
  • Send you promotional materials (with your consent)
  • +
  • Comply with legal obligations
  • +
+ +

Data Security

+

We implement appropriate technical and organizational measures to protect your personal information against unauthorized access, alteration, disclosure, or destruction.

+ +

Your Rights

+

You have the right to:

+
    +
  • Access your personal information
  • +
  • Correct inaccurate information
  • +
  • Request deletion of your information
  • +
  • Object to processing of your information
  • +
  • Data portability
  • +
+ +

Contact Us

+

If you have any questions about this Privacy Policy, please contact us at privacy@hotel.com.

+ +

Last updated: """ + datetime.now().strftime("%B %d, %Y") + """

+ """ + + privacy_data = { + "title": "Privacy Policy", + "subtitle": "Your privacy is important to us", + "description": "Learn how we collect, use, and protect your personal information.", + "content": privacy_content, + "meta_title": "Privacy Policy - Luxury Hotel | Data Protection & Privacy", + "meta_description": "Read our privacy policy to understand how we collect, use, and protect your personal information when you use our hotel booking services." + } + + existing = db.query(PageContent).filter(PageContent.page_type == PageType.PRIVACY).first() + + if existing: + for key, value in privacy_data.items(): + setattr(existing, key, value) + existing.updated_at = datetime.utcnow() + print("✓ Updated existing privacy policy page content") + else: + new_content = PageContent( + page_type=PageType.PRIVACY, + **privacy_data + ) + db.add(new_content) + print("✓ Created new privacy policy page content") + + db.commit() + print("\n✅ Privacy policy page content seeded successfully!") + print("=" * 80) + +def seed_terms_conditions(db: Session): + print("=" * 80) + print("SEEDING TERMS & CONDITIONS PAGE CONTENT") + print("=" * 80) + + terms_content = """ +

Agreement to Terms

+

By accessing and using our hotel's website and services, you accept and agree to be bound by the terms and provision of this agreement.

+ +

Booking Terms

+

When making a reservation with us, you agree to:

+
    +
  • Provide accurate and complete information
  • +
  • Be responsible for all charges incurred during your stay
  • +
  • Comply with hotel policies and regulations
  • +
  • Respect other guests and hotel property
  • +
+ +

Payment Terms

+

Payment terms include:

+
    +
  • A 20% deposit is required to secure your booking
  • +
  • The remaining balance is due on arrival at the hotel
  • +
  • All prices are subject to applicable taxes and fees
  • +
  • Payment methods accepted: credit cards, debit cards, and cash
  • +
+ +

Cancellation Policy

+

Our cancellation policy is as follows:

+
    +
  • Cancellations made 48 hours or more before check-in: Full refund of deposit
  • +
  • Cancellations made less than 48 hours before check-in: Deposit is non-refundable
  • +
  • No-shows: Full booking amount is charged
  • +
+ +

Check-in and Check-out

+

Standard check-in time is 3:00 PM and check-out time is 11:00 AM. Early check-in and late check-out may be available upon request and subject to availability.

+ +

Guest Responsibilities

+

Guests are responsible for:

+
    +
  • Any damage to hotel property
  • +
  • Compliance with all hotel rules and regulations
  • +
  • Respecting quiet hours and other guests
  • +
  • Proper use of hotel facilities
  • +
+ +

Limitation of Liability

+

The hotel shall not be liable for any loss, damage, or injury to persons or property during your stay, except where such loss, damage, or injury is caused by our negligence.

+ +

Modifications to Terms

+

We reserve the right to modify these terms at any time. Changes will be effective immediately upon posting on our website.

+ +

Contact Information

+

For questions about these terms, please contact us at legal@hotel.com.

+ +

Last updated: """ + datetime.now().strftime("%B %d, %Y") + """

+ """ + + terms_data = { + "title": "Terms & Conditions", + "subtitle": "Please read these terms carefully", + "description": "Terms and conditions governing your use of our hotel booking services.", + "content": terms_content, + "meta_title": "Terms & Conditions - Luxury Hotel | Booking Terms & Policies", + "meta_description": "Read our terms and conditions to understand the rules and policies governing your bookings and stay at our luxury hotel." + } + + existing = db.query(PageContent).filter(PageContent.page_type == PageType.TERMS).first() + + if existing: + for key, value in terms_data.items(): + setattr(existing, key, value) + existing.updated_at = datetime.utcnow() + print("✓ Updated existing terms & conditions page content") + else: + new_content = PageContent( + page_type=PageType.TERMS, + **terms_data + ) + db.add(new_content) + print("✓ Created new terms & conditions page content") + + db.commit() + print("\n✅ Terms & conditions page content seeded successfully!") + print("=" * 80) + +def seed_refunds_policy(db: Session): + print("=" * 80) + print("SEEDING REFUNDS POLICY PAGE CONTENT") + print("=" * 80) + + refunds_content = """ +

Refund Policy Overview

+

At our hotel, we understand that plans can change. This policy outlines our refund procedures and timelines for various scenarios.

+ +

Booking Cancellations

+

Refunds for cancelled bookings are processed as follows:

+
    +
  • More than 48 hours before check-in: Full refund of the 20% deposit
  • +
  • 48 hours or less before check-in: Deposit is non-refundable
  • +
  • No-show: No refund will be issued
  • +
+ +

Early Check-out

+

If you check out earlier than your reserved departure date:

+
    +
  • You will be charged for the nights you stayed
  • +
  • The remaining 80% balance for unused nights will be refunded
  • +
  • Refunds are processed within 5-7 business days
  • +
+ +

Service Issues

+

If you experience service issues during your stay:

+
    +
  • Please report the issue immediately to our front desk
  • +
  • We will make every effort to resolve the issue promptly
  • +
  • If the issue cannot be resolved, partial or full refunds may be considered on a case-by-case basis
  • +
+ +

Refund Processing Time

+

Refunds are typically processed within:

+
    +
  • Credit/Debit Cards: 5-7 business days
  • +
  • Bank Transfers: 7-10 business days
  • +
  • Cash Payments: Refunds will be processed via bank transfer
  • +
+ +

Refund Method

+

Refunds will be issued to the original payment method used for the booking. If the original payment method is no longer available, please contact us to arrange an alternative refund method.

+ +

Non-Refundable Bookings

+

Some special offers or promotional rates may be non-refundable. This will be clearly stated at the time of booking.

+ +

Force Majeure

+

In cases of force majeure (natural disasters, pandemics, government restrictions, etc.), we will work with you to reschedule your booking or provide appropriate refunds based on the circumstances.

+ +

Dispute Resolution

+

If you have concerns about a refund decision, please contact our customer service team. We are committed to resolving all disputes fairly and promptly.

+ +

Contact Us

+

For refund inquiries, please contact us at refunds@hotel.com or call our customer service line.

+ +

Last updated: """ + datetime.now().strftime("%B %d, %Y") + """

+ """ + + refunds_data = { + "title": "Refunds Policy", + "subtitle": "Our commitment to fair refunds", + "description": "Learn about our refund policies and procedures for bookings and cancellations.", + "content": refunds_content, + "meta_title": "Refunds Policy - Luxury Hotel | Cancellation & Refund Terms", + "meta_description": "Understand our refund policy, including cancellation terms, processing times, and how to request refunds for your hotel bookings." + } + + existing = db.query(PageContent).filter(PageContent.page_type == PageType.REFUNDS).first() + + if existing: + for key, value in refunds_data.items(): + setattr(existing, key, value) + existing.updated_at = datetime.utcnow() + print("✓ Updated existing refunds policy page content") + else: + new_content = PageContent( + page_type=PageType.REFUNDS, + **refunds_data + ) + db.add(new_content) + print("✓ Created new refunds policy page content") + + db.commit() + print("\n✅ Refunds policy page content seeded successfully!") + print("=" * 80) + +def seed_cancellation_policy(db: Session): + print("=" * 80) + print("SEEDING CANCELLATION POLICY PAGE CONTENT") + print("=" * 80) + + cancellation_content = """ +

Cancellation Policy Overview

+

We understand that plans can change. This policy outlines our cancellation procedures and fees.

+ +

Standard Cancellation Terms

+

For standard bookings, the following cancellation terms apply:

+
    +
  • More than 48 hours before check-in: Full refund of the 20% deposit
  • +
  • 48 hours or less before check-in: Deposit is non-refundable
  • +
  • No-show: No refund will be issued
  • +
+ +

Special Rate Bookings

+

Some special rates or promotional offers may have different cancellation terms. Please review your booking confirmation for specific details.

+ +

How to Cancel

+

To cancel your booking:

+
    +
  • Log into your account and visit "My Bookings"
  • +
  • Select the booking you wish to cancel
  • +
  • Click "Cancel Booking" and confirm
  • +
  • Or contact us directly via phone or email
  • +
+ +

Refund Processing

+

Refunds will be processed to the original payment method within 5-10 business days after cancellation confirmation.

+ +

Last updated: """ + datetime.now().strftime("%B %d, %Y") + """

+ """ + + cancellation_data = { + "title": "Cancellation Policy", + "subtitle": "Flexible cancellation options for your peace of mind", + "description": "Learn about our cancellation policy, fees, and refund procedures.", + "content": cancellation_content, + "meta_title": "Cancellation Policy - Luxury Hotel | Booking Cancellation Terms", + "meta_description": "Review our cancellation policy to understand cancellation fees, refund procedures, and terms for modifying or canceling your hotel booking." + } + + existing = db.query(PageContent).filter(PageContent.page_type == PageType.CANCELLATION).first() + + if existing: + for key, value in cancellation_data.items(): + setattr(existing, key, value) + existing.updated_at = datetime.utcnow() + print("✓ Updated existing cancellation policy page content") + else: + new_content = PageContent( + page_type=PageType.CANCELLATION, + **cancellation_data + ) + db.add(new_content) + print("✓ Created new cancellation policy page content") + + db.commit() + print("\n✅ Cancellation policy page content seeded successfully!") + print("=" * 80) + +def seed_accessibility_policy(db: Session): + print("=" * 80) + print("SEEDING ACCESSIBILITY PAGE CONTENT") + print("=" * 80) + + accessibility_content = """ +

Our Commitment to Accessibility

+

We are committed to ensuring that our hotel and website are accessible to all guests, regardless of ability. We strive to provide an inclusive experience for everyone.

+ +

Hotel Accessibility Features

+

Our hotel offers the following accessibility features:

+
    +
  • Wheelchair-accessible rooms with roll-in showers
  • +
  • Accessible parking spaces
  • +
  • Elevator access to all floors
  • +
  • Ramp access to main entrances
  • +
  • Accessible public restrooms
  • +
  • Visual and auditory emergency alarms
  • +
  • Service animal friendly
  • +
+ +

Website Accessibility

+

We are continuously working to improve the accessibility of our website. Our website includes:

+
    +
  • Keyboard navigation support
  • +
  • Screen reader compatibility
  • +
  • Alt text for images
  • +
  • High contrast options
  • +
  • Resizable text
  • +
+ +

Requesting Accommodations

+

If you require specific accommodations during your stay, please contact us at least 48 hours before your arrival. We will do our best to accommodate your needs.

+ +

Feedback

+

We welcome feedback on how we can improve accessibility. Please contact us with your suggestions or concerns.

+ +

Last updated: """ + datetime.now().strftime("%B %d, %Y") + """

+ """ + + accessibility_data = { + "title": "Accessibility", + "subtitle": "Committed to providing an inclusive experience for all guests", + "description": "Learn about our accessibility features and accommodations.", + "content": accessibility_content, + "meta_title": "Accessibility - Luxury Hotel | Accessible Accommodations", + "meta_description": "Discover our commitment to accessibility, including accessible rooms, facilities, and website features for guests with disabilities." + } + + existing = db.query(PageContent).filter(PageContent.page_type == PageType.ACCESSIBILITY).first() + + if existing: + for key, value in accessibility_data.items(): + setattr(existing, key, value) + existing.updated_at = datetime.utcnow() + print("✓ Updated existing accessibility page content") + else: + new_content = PageContent( + page_type=PageType.ACCESSIBILITY, + **accessibility_data + ) + db.add(new_content) + print("✓ Created new accessibility page content") + + db.commit() + print("\n✅ Accessibility page content seeded successfully!") + print("=" * 80) + +def seed_faq_page(db: Session): + print("=" * 80) + print("SEEDING FAQ PAGE CONTENT") + print("=" * 80) + + faq_content = """ +

Frequently Asked Questions

+

Find answers to common questions about our hotel, bookings, and services.

+ +

Booking & Reservations

+

How do I make a reservation?

+

You can make a reservation online through our website, by phone, or by email. Simply select your dates, choose your room, and complete the booking process.

+ +

What is the deposit requirement?

+

A 20% deposit is required to secure your booking. The remaining balance is due upon arrival at the hotel.

+ +

Can I modify my booking?

+

Yes, you can modify your booking by logging into your account and visiting "My Bookings", or by contacting us directly. Changes are subject to availability and may incur fees.

+ +

Check-in & Check-out

+

What are your check-in and check-out times?

+

Check-in is from 3:00 PM, and check-out is by 11:00 AM. Early check-in and late check-out may be available upon request, subject to availability.

+ +

Do you offer early check-in or late check-out?

+

Early check-in and late check-out are available upon request, subject to availability. Additional fees may apply.

+ +

Payment & Cancellation

+

What payment methods do you accept?

+

We accept major credit cards, debit cards, and bank transfers. Payment can be made online or at the hotel.

+ +

What is your cancellation policy?

+

For cancellations made more than 48 hours before check-in, the deposit is fully refundable. Cancellations made 48 hours or less before check-in are non-refundable. Please see our Cancellation Policy page for full details.

+ +

Hotel Services & Amenities

+

What amenities are included?

+

Our hotel offers complimentary Wi-Fi, parking, fitness center access, and more. Please check the room details for specific amenities.

+ +

Do you have parking available?

+

Yes, we offer complimentary parking for all guests. Valet parking is also available for an additional fee.

+ +

Is Wi-Fi available?

+

Yes, complimentary high-speed Wi-Fi is available throughout the hotel.

+ +

Special Requests

+

Can I request a specific room?

+

Yes, you can make special requests when booking. We will do our best to accommodate your preferences, subject to availability.

+ +

Do you accommodate dietary restrictions?

+

Yes, please inform us of any dietary restrictions or allergies when making your reservation, and we will do our best to accommodate your needs.

+ +

Contact & Support

+

How can I contact the hotel?

+

You can contact us by phone, email, or through our website's contact form. Our team is available 24/7 to assist you.

+ +

Last updated: """ + datetime.now().strftime("%B %d, %Y") + """

+ """ + + faq_data = { + "title": "Frequently Asked Questions", + "subtitle": "Find answers to common questions", + "description": "Get answers to frequently asked questions about bookings, services, and policies.", + "content": faq_content, + "meta_title": "FAQ - Luxury Hotel | Frequently Asked Questions", + "meta_description": "Find answers to common questions about hotel bookings, check-in, payment, cancellation, amenities, and more." + } + + existing = db.query(PageContent).filter(PageContent.page_type == PageType.FAQ).first() + + if existing: + for key, value in faq_data.items(): + setattr(existing, key, value) + existing.updated_at = datetime.utcnow() + print("✓ Updated existing FAQ page content") + else: + new_content = PageContent( + page_type=PageType.FAQ, + **faq_data + ) + db.add(new_content) + print("✓ Created new FAQ page content") + + db.commit() + print("\n✅ FAQ page content seeded successfully!") + print("=" * 80) + +def main(): + db = get_db() + try: + print("\n") + seed_privacy_policy(db) + print("\n") + seed_terms_conditions(db) + print("\n") + seed_refunds_policy(db) + print("\n") + seed_cancellation_policy(db) + print("\n") + seed_accessibility_policy(db) + print("\n") + seed_faq_page(db) + print("\n") + print("=" * 80) + print("✅ ALL POLICY PAGES SEEDED SUCCESSFULLY!") + print("=" * 80) + print("\n") + except Exception as e: + print(f"\n❌ Error: {e}") + import traceback + traceback.print_exc() + db.rollback() + finally: + db.close() + +if __name__ == "__main__": + main() + diff --git a/Backend/src/__pycache__/main.cpython-312.pyc b/Backend/src/__pycache__/main.cpython-312.pyc index 4f965054347b05a7ce3c8cf564e9db5e9e081151..5b261f5c433f9f750c698cb90deb5e7ab11d9558 100644 GIT binary patch delta 774 zcmaiyUr19?9LLYO+tO`%9d7sTs!Ox{H)Ay_-JghpMM9LCLDYRH=XQnJ?!>#RP_Tcd z+rkpg*iJu}YWArpM#++`1>UR@h`w|zpIjP~4ZLr8fZMbXN5Ud+P6+BOHlH$z*IG7LTd-_FT(gctq=JZDdT-WbO-1b|1xf3TE0prdeXVg2nb7 zJgqGrFA!V1>=|P4V|agZq+OXHZDo=oPf?&K60DVzJ*6=zp(=EwG7CQMp!GYE^um%q ztXj!?&Iq6VoOhc+c&KcfbpFRpJp(Ip4-LVErpeDt@GVWIkQ zS24DNg+GiD2R99fso9`vqXIFef@>TGf-B@l>pXjkQyH|ynA8@-R+&R>G;B3e{SVIR B=%N4s delta 484 zcmcap|F?|qG%qg~0}$j*mCxL)Hjz()sZV^P#!imO`g~TCmve|tP7t&NvO%=cWC_l= z$s4#NH!tVxVA*QYk5_-AV{ zFci&5VMyW4X^d)|%p&P5-^9d_nvmL*5>Um>z_6MTLQJ0@Q? zDiplMk)IczT9%quQUuaj)H6BHIF0)XkO^{b@m8+Q_l<2?CO@-OoGfdtqw<}Jfzg%i zJ5UxkxjE5-nQ3x^rQ~E83(?7Pwp^26*~r6jx~=@?4c6BfC+FD6O-`_LmTQ#h$h^!b z@sWvz(dsiZ1C!(j5bFaE0~2ezOylH(c9$75Cr`CE63tU!c&EUc&%*ppRV2@z`JFw8 F4FG-nj7b0h diff --git a/Backend/src/main.py b/Backend/src/main.py index 23ee2837..18a043f5 100644 --- a/Backend/src/main.py +++ b/Backend/src/main.py @@ -91,10 +91,8 @@ async def health_check(db: Session=Depends(get_db)): async def metrics(): return {'status': 'success', 'service': settings.APP_NAME, 'version': settings.APP_VERSION, 'environment': settings.ENVIRONMENT, 'timestamp': datetime.utcnow().isoformat()} app.include_router(auth_routes.router, prefix='/api') -app.include_router(privacy_routes.router, prefix='/api') app.include_router(auth_routes.router, prefix=settings.API_V1_PREFIX) -app.include_router(privacy_routes.router, prefix=settings.API_V1_PREFIX) -from .routes import room_routes, booking_routes, payment_routes, invoice_routes, banner_routes, favorite_routes, service_routes, service_booking_routes, promotion_routes, report_routes, review_routes, user_routes, audit_routes, admin_privacy_routes, system_settings_routes, contact_routes, page_content_routes, home_routes, about_routes, contact_content_routes, footer_routes, chat_routes +from .routes import room_routes, booking_routes, payment_routes, invoice_routes, banner_routes, favorite_routes, service_routes, service_booking_routes, promotion_routes, report_routes, review_routes, user_routes, audit_routes, admin_privacy_routes, system_settings_routes, contact_routes, page_content_routes, home_routes, about_routes, contact_content_routes, footer_routes, chat_routes, privacy_routes, terms_routes, refunds_routes, cancellation_routes, accessibility_routes, faq_routes app.include_router(room_routes.router, prefix='/api') app.include_router(booking_routes.router, prefix='/api') app.include_router(payment_routes.router, prefix='/api') @@ -115,6 +113,12 @@ app.include_router(home_routes.router, prefix='/api') app.include_router(about_routes.router, prefix='/api') app.include_router(contact_content_routes.router, prefix='/api') app.include_router(footer_routes.router, prefix='/api') +app.include_router(privacy_routes.router, prefix='/api') +app.include_router(terms_routes.router, prefix='/api') +app.include_router(refunds_routes.router, prefix='/api') +app.include_router(cancellation_routes.router, prefix='/api') +app.include_router(accessibility_routes.router, prefix='/api') +app.include_router(faq_routes.router, prefix='/api') app.include_router(chat_routes.router, prefix='/api') app.include_router(room_routes.router, prefix=settings.API_V1_PREFIX) app.include_router(booking_routes.router, prefix=settings.API_V1_PREFIX) @@ -136,6 +140,12 @@ app.include_router(home_routes.router, prefix=settings.API_V1_PREFIX) app.include_router(about_routes.router, prefix=settings.API_V1_PREFIX) app.include_router(contact_content_routes.router, prefix=settings.API_V1_PREFIX) app.include_router(footer_routes.router, prefix=settings.API_V1_PREFIX) +app.include_router(privacy_routes.router, prefix=settings.API_V1_PREFIX) +app.include_router(terms_routes.router, prefix=settings.API_V1_PREFIX) +app.include_router(refunds_routes.router, prefix=settings.API_V1_PREFIX) +app.include_router(cancellation_routes.router, prefix=settings.API_V1_PREFIX) +app.include_router(accessibility_routes.router, prefix=settings.API_V1_PREFIX) +app.include_router(faq_routes.router, prefix=settings.API_V1_PREFIX) app.include_router(chat_routes.router, prefix=settings.API_V1_PREFIX) app.include_router(page_content_routes.router, prefix='/api') app.include_router(page_content_routes.router, prefix=settings.API_V1_PREFIX) diff --git a/Backend/src/models/__pycache__/page_content.cpython-312.pyc b/Backend/src/models/__pycache__/page_content.cpython-312.pyc index 80f81e007807bdb182ee8e8a9dd49f67d6cae64b..7424f90bebfb1ef3b9d66b3f8d035698c64fc04f 100644 GIT binary patch delta 2319 zcmah~-%lJx9G}^}yS?96dZ!qmz_n7`HdL=zD;7?U1Z~CN7F;SnKUFggm8N((s^PSJ<`~A#! ze(cQ8@>hM)?<0{QAYc7#cWLe9H_?8Wev@>!fDTlMfGQxUibzmBNL0PZqxz6nm5@*M zBS{S)zZygVHH3m{7=_fxYFLioMc9|NrQ2p{MJuk_-imH6S+->A3snPIcDSe+MZH|s zDy3z^j%dZAZds-IQn^%FwZ#SPPC<_1EttJxOB0j18yRKJ_D+vYUY)X~$+7X7!swJ8 zRx$-;ET7Lz=dKm(NJde{rlxWuxqNPV&KAcr*YU4Vg#&m%=*ZNRy=ZGKyBChv#ty7_ zvX+sp3edDB41tF_8c&zCm^Bjf8JLE z0?<9AkoDMxhdx3*xGfymYKZb7E+2MyJwh~DO!N>F35b(oJklhMGh%$5Iky}$z)8wC zsn7)DoCFdyhM)8cA06ZHB1g3Kn3$JX3{H6(r`X0Ansz$}>!2g>r+SL={AqPI@|~Q` z>|%rBC!Y8a+l3@!Xo{yf*cpc5l($i#hcQ&)EC=gl7~W`7kQqZ2&T+8w48yxk3Vn>B z3Kuxos|>?QU*lG#7(*2Y zNbyKh98O7%)4W2}`6q3V@=YpqNru?y6iXiy^R|0Dk6abQOb9=b8qd=RW9SeH2ODJ= zPWc-Z#u-BuvK;IkhT)ASg?AZ472e}ud4}QLCWQiHsKPZ4Ho-8Q48%omJ&Z30dfiqF z47GUc5qf36zZ#We`0GIbdhzhacF&JPB_R_LX&GvGNJ=^BYyiVTO44sz&KfRX1NF z3Z;Fr9x^oFa_ry8T9C{oq%UaIa>WH@(Htb4?O!&kw~+uM)(t`tc93uHM~&*&Y4Xj#d4sb>&Y0Mue7WM&X z(S%2T$DWElSE>*H5x)e7aZh5j%T;V1g)HjQGOCvKw7H4zCtm3Z!4vnbhof7$&D=g9 To&B~vggSiq@?XF;+ywsvPA|gG delta 2102 zcmai#%TE(Q9LINdx9{f)r6|&ZmdApqfcOM_P(ZX+B_4E6R%cB}O3{UykeF;uJa~j` z2=Q;A2TiXYJeqh^lcs-xk9aVt7f;TPcHDMd|Yn3Pq$<~Oux=0!Zx zLv1gFYCtKatKoQQu^Q|s%~wOQ(n8fAc|NBnsIEuo?cuf(Llb678aMV39n*`*L?mL_ zwI}}_W1lE!GHrh1=5xfdXHWd62e|G9^`NC|A)5K$6Lv5|z!4qH9~Ef@irc#8!#ON*dc)R0PL{EXxp!Byeh^!1@7p#KCC&k2;`$Qve$P*dV}$05%LT)nZ1=N=AT*qX0VwurYug2iOT4 zGaA3sNgHgiaez%&jJEyiJIQr;6Q63%Wc9~W+ywtFr@0P?o@oxX_6$DDJ#gkZrW@8u z;L!6x=!G_+(_Du$XIeAq`tfWV)5TU3^;`njWq@4)*qn{=`loHMQU7^>EdXp0U{?XQ z{ zP0c@fe?gUM-a;{-&ppU%HFvSF@}#KgDZ2kw>hxe!kv?Gczeuz%);vUelq=>p7*r*F zh-x+OTA}!u7&-6`oy^%Uy~;5+zZ8mPIG3EV{C&1ZwQGb`$+tvK`rx_veqje>Q(A!L(-{uZ#ZK?T0eidG^dJ(;w#62C_EL*T9Q-zR7mnl;D+mN z$v@V#S|4e6j9SELN<-%-r?X22Wu|+SF4Bymo4$$J+4Ey73A$*g*^DXZM%-A^Bb@1; zp}G#H$2dLEg%mYpWey|{N?JCRqAbFjb?4(LI+D#8G-H?|DzmGL^ol=`vJ9qo$}Y%@w6Oh3W~W+U3tM z6Q(CkjksJy>utHh#q`;h1-iPNWw3w5Y^vny2dUB#5j711o4!Omlg+4!xF#<#&6HGK zjwcNDLuz^wjK&SB$Z^B;EG;S}ITB=zFcXGJHeDaawI#}$(SW&SE!g2q877pqVTTE1 z9oS*QSSNOvAl8Ll7&;s_o6NFw++diF2xrI8aifKwo{nD1rfD>lS&5EPeZj~sMklfc z)s7BlvkPh_6&;Qz7Jx9KI!i6EE)O}#>8`@5I6Hv0*TB*9<5 z2xDF&7T@3%c0})b>a+Pz=Pd_l{}dZH-Yqou7B7*48^<=^xcP?VL)}lL=8aS#)SD0W zS^>1w6RG3YsltK5{DDEM0WCoywcctewDsrP`mIK^G*yn&jFu3Q+HM^#v=8Ll2doyf zv{w4Gp{2btP6t{J5UHuWGOH6U2Vp+<+=b((zd3%oJg-9N)qLpH2cf~-`B|$Ay~020 zwH+F?4xtNaaG~GZ+y(BLVGlktdCEmiUYV<+Qlzwx;lK9-TMVnNBreL~86)Xf4WI=p|%7w(rH*Ws11Ut#P`j08uEs;<(K#&fEM9S>tL^ zs@`EVlp`7Y^jV8871x$fK6|fo2JR}mt#Rc!UFBg+_ZqLlaeyPfOpJZ1?k0@q$aP}u zlPXxwU3D1yR638F@VTal25v~ZtvSl${EqydSEMaZc{UuWxrT7*#fry|$E#eMLasVK zax8PpWX175S?2x(a-?p`bn8p@89ffivA1fg;v^$H!=ur30xQL@^bS=X=V9~?t9m%G z#26g>+G;&LgE;{T;@^A7zu%pxKn&fwaO`E+pdvE_@J9xMd?9T`Lw|-rSj^?AI{}2lk z7xVCoUfd4Kg&@rb>2@%+?tT~u?HoM(Wq0mEEdR={@{J$lydUfYLWMy1n?QIs*tIoY z=o`!Tjpe3R9t2l&-qqcfqjyrzbYr9;UP_`ako2>dm#B{A=RNLw`x$doR~Ko_qVky7z(j&aS^>ODOcdmG6Bk_ufbU zA>>VdgtrJ8<;Qs7q&N9-)PBRA!pFPZ1P?BONmQgs{=b$B2lzpM4Zg&6ZsY`aW2k+& zm3(y&<*&MqgM7aQ<@>F{k=Gsfj|_Q61|44yxml-XReVo^Y?Vy?i6;nmM)KF1jJp^=r7qR zP?3}M%g|X4j^n;1C%4JTN93hPr27%+|CStk2&dr0w$yn|cqj#P?MJ?d=3W_oAdOrT zEN74t{&2=3@c3eM4M=D HJ*EEv;$H#g literal 0 HcmV?d00001 diff --git a/Backend/src/routes/__pycache__/cancellation_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/cancellation_routes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..01b707ee21a929e28ea9365c795f598d1bc726ff GIT binary patch literal 3371 zcmaJ@O>7&-6`o!G?NX#DQWB{jOJWkm7F}6X1J!X_r>QK_mSaV7B{^x?Bnyf=q*q%0 zWOijqu4FlmfhwoS$Uz3`0y!qI0{Cd4JtWSp?SYI8P+rJLfucZj+gLpWZVr8KNy?PX zbU@C&_rA9?Z|A-FX8z{&x(T#@9S$pQfslWqQf>B%u=iIGHi=ARP9+&G&Sm&GpRvVl z8GGE$p`BM98E4!HvQ4(Du8a^DEZL!o8F$=m$xgK)B#0opWoq!LrjL-tn3fl+uWRJ9Z*H&SPcmwneA zw=4efmgT01|69}|c0(LKe|9Rjpi^eJMrodAWzF!8&CHySEhTAQS8`dysp$!QL5pyP zYno~rn7+mEKzF98F3GbXxlvMc=`>{#-e@?VNYjyAR;O9r5Kx&}&eO}@XflyaQdLb@ z<9cc}c*~h*G^H#Vc0G~S3|F%9zLQaXfn}4Gg|fZ^(_(}FUOz;#Nfb!S3YoK@gWboR z%mY$c&RT4)n%Wv8c-z3+S!1e*fYPBr2FxuBVTX}2Oekx|4im^a zvBQM1F6=NttOvU=bU1A;m1CKN&M+Mj&Kg6*jTU}p>_wPJgunJK zOnH@WxHf-AI>$eZ^p( z=|f8s5j$_5DIFRr9vU+JXbBLp?PjRdK2U5QFq_fRQae*CT7pDuzj>t8F<9&vG(%`< ztMzF|OGoWEooG2k#FpxpnO$hdG-L4BBOl(|Ggg|2j>U}Y$_7iVORuDHGQ8& ztH;Zn<0%(VpLo^dQGK65>oMkhA0hi=d!HS<$`GsE3a1G&@Py3Ewp+Yxzr|0J6|Nqm zYMr{jI+DK6p7q#Lc5FGT2k$jb$4zCm6|Nf7Di3qIR(J(Y1Dx>%qVF?xFJU}Kt`U8o zQ^7L1YSZ_bbPo66Q%x3h+>cgUu~p~!Bl#0Ai(Bq$G#sh>4dKd*HIE-3ulCzyvTD0x zTjUnWlI^!-k^3CvNW+BT(iW0QIO0#kY3w<$RWZm2&+ue4>_AG1%L5}dXL&fEQnXycLx15mGWIQ^P3(r79}>35j`;TgDR2y`haDcMvm;x`=c0SPX%28_v>QdGFZSp3NF#5Fu4!xJO1@$uoA zvy&5sBc(7+FLNIjZTU+Ac+ruD28X1H$rc*euX}bnUidV7S1lYnSM-e+gz@ip8`>Tb z&e6Cdcy}9G9uXcw+7%i<@_gtigiiit`V0S`-@H3kXpI49t$XeZ7j_#vx16QEH;R34 z6n?Y%A41l+Q@lyY2tUdLEv<1e)UI%+@$nXSh6fkl7%Jiz|6kLA6a1hZhn2X-jhy7J z4|kkuBVTr-{AJGxkne?1zSkBQdBt|`=&*Za$oAEc1C*~`u>mjy!;u> z8K`Sj8#u6UusF;0C`eK^k)e{r@KX$@bv!cIOi&i(qO(CMSXuv!u*3Ks7RY1aB#gVD z(qFPOpdu}6dFV_V$8q0~SGUQl56SZnN$*24@C|wS0UU&5+hW&M=L0ctGkT}HF!-zc z;_y|cX%BGDPtKVH9(P9f@L~25Uid@D`yJ~eAM`wk9=~(x_S$#E%Z>4$EIzWHdp5%R KzvIzlMf5*vo&s3_ literal 0 HcmV?d00001 diff --git a/Backend/src/routes/__pycache__/faq_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/faq_routes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..470c55a27624fc211dba5dedc4ffe082a0f5ad05 GIT binary patch literal 3271 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 R9+uww9ATTjVo_BJ=zpcT=0N}e literal 0 HcmV?d00001 diff --git a/Backend/src/routes/__pycache__/privacy_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/privacy_routes.cpython-312.pyc index 5a851c3c213c73073f0f17e299925f082062d1fa..e57f8ea2b3ad08d68d1a7476b1b8f52825bc376a 100644 GIT binary patch literal 7091 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 z
u%0u$ZA4$_g()*C?dPoMpBnKXn(Jx7Eo#Y;p!w=xh?ASWrd&%*D_kFnM#^jA} z-{%ipa+GX7#_{p%B?3RU$DZI%X@IbeMFj>82j<=$Hwmy zj<9Vd7DaH9RW{Chdh>&^OQCgU>_KGccHmauBNBiD@Z;cP;}_?QGbI*fr6HWu{eE}8 zc}G4vcBy-v8GrER>HIs1^*8_Z5%GgbJ`yV#ozG!D3UK}e%*TFd(Awc-zJCPQ9DOi! d;Ev;$-bcjEOtLqekBwiP6Js8+C^Mer{{n~MY7_tf literal 3605 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 A;s5{u literal 0 HcmV?d00001 diff --git a/Backend/src/routes/accessibility_routes.py b/Backend/src/routes/accessibility_routes.py new file mode 100644 index 00000000..ecaeeef3 --- /dev/null +++ b/Backend/src/routes/accessibility_routes.py @@ -0,0 +1,44 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from ..config.database import get_db +from ..config.logging_config import get_logger +from ..models.page_content import PageContent, PageType +logger = get_logger(__name__) +router = APIRouter(prefix='/accessibility', tags=['accessibility']) + +def serialize_page_content(content: PageContent) -> dict: + return { + 'id': content.id, + 'page_type': content.page_type.value, + 'title': content.title, + 'subtitle': content.subtitle, + 'description': content.description, + 'content': content.content, + 'meta_title': content.meta_title, + 'meta_description': content.meta_description, + 'meta_keywords': content.meta_keywords, + 'og_title': content.og_title, + 'og_description': content.og_description, + 'og_image': content.og_image, + 'canonical_url': content.canonical_url, + 'is_active': content.is_active, + 'created_at': content.created_at.isoformat() if content.created_at else None, + 'updated_at': content.updated_at.isoformat() if content.updated_at else None + } + +@router.get('/') +async def get_accessibility_content(db: Session=Depends(get_db)): + try: + content = db.query(PageContent).filter(PageContent.page_type == PageType.ACCESSIBILITY).first() + if not content: + return {'status': 'success', 'data': {'page_content': None, 'is_active': False}} + if not content.is_active: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Accessibility page is currently disabled') + content_dict = serialize_page_content(content) + return {'status': 'success', 'data': {'page_content': content_dict}} + except HTTPException: + raise + except Exception as e: + logger.error(f'Error fetching accessibility content: {str(e)}', exc_info=True) + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching accessibility content: {str(e)}') + diff --git a/Backend/src/routes/cancellation_routes.py b/Backend/src/routes/cancellation_routes.py new file mode 100644 index 00000000..816b4940 --- /dev/null +++ b/Backend/src/routes/cancellation_routes.py @@ -0,0 +1,44 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from ..config.database import get_db +from ..config.logging_config import get_logger +from ..models.page_content import PageContent, PageType +logger = get_logger(__name__) +router = APIRouter(prefix='/cancellation', tags=['cancellation']) + +def serialize_page_content(content: PageContent) -> dict: + return { + 'id': content.id, + 'page_type': content.page_type.value, + 'title': content.title, + 'subtitle': content.subtitle, + 'description': content.description, + 'content': content.content, + 'meta_title': content.meta_title, + 'meta_description': content.meta_description, + 'meta_keywords': content.meta_keywords, + 'og_title': content.og_title, + 'og_description': content.og_description, + 'og_image': content.og_image, + 'canonical_url': content.canonical_url, + 'is_active': content.is_active, + 'created_at': content.created_at.isoformat() if content.created_at else None, + 'updated_at': content.updated_at.isoformat() if content.updated_at else None + } + +@router.get('/') +async def get_cancellation_content(db: Session=Depends(get_db)): + try: + content = db.query(PageContent).filter(PageContent.page_type == PageType.CANCELLATION).first() + if not content: + return {'status': 'success', 'data': {'page_content': None, 'is_active': False}} + if not content.is_active: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Cancellation policy page is currently disabled') + content_dict = serialize_page_content(content) + return {'status': 'success', 'data': {'page_content': content_dict}} + except HTTPException: + raise + except Exception as e: + logger.error(f'Error fetching cancellation content: {str(e)}', exc_info=True) + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching cancellation content: {str(e)}') + diff --git a/Backend/src/routes/faq_routes.py b/Backend/src/routes/faq_routes.py new file mode 100644 index 00000000..cc54bd2d --- /dev/null +++ b/Backend/src/routes/faq_routes.py @@ -0,0 +1,44 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from ..config.database import get_db +from ..config.logging_config import get_logger +from ..models.page_content import PageContent, PageType +logger = get_logger(__name__) +router = APIRouter(prefix='/faq', tags=['faq']) + +def serialize_page_content(content: PageContent) -> dict: + return { + 'id': content.id, + 'page_type': content.page_type.value, + 'title': content.title, + 'subtitle': content.subtitle, + 'description': content.description, + 'content': content.content, + 'meta_title': content.meta_title, + 'meta_description': content.meta_description, + 'meta_keywords': content.meta_keywords, + 'og_title': content.og_title, + 'og_description': content.og_description, + 'og_image': content.og_image, + 'canonical_url': content.canonical_url, + 'is_active': content.is_active, + 'created_at': content.created_at.isoformat() if content.created_at else None, + 'updated_at': content.updated_at.isoformat() if content.updated_at else None + } + +@router.get('/') +async def get_faq_content(db: Session=Depends(get_db)): + try: + content = db.query(PageContent).filter(PageContent.page_type == PageType.FAQ).first() + if not content: + return {'status': 'success', 'data': {'page_content': None, 'is_active': False}} + if not content.is_active: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='FAQ page is currently disabled') + content_dict = serialize_page_content(content) + return {'status': 'success', 'data': {'page_content': content_dict}} + except HTTPException: + raise + except Exception as e: + logger.error(f'Error fetching FAQ content: {str(e)}', exc_info=True) + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching FAQ content: {str(e)}') + diff --git a/Backend/src/routes/privacy_routes.py b/Backend/src/routes/privacy_routes.py index 96f2db44..70a233af 100644 --- a/Backend/src/routes/privacy_routes.py +++ b/Backend/src/routes/privacy_routes.py @@ -1,39 +1,110 @@ -from fastapi import APIRouter, Depends, Request, Response, status +from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session +import json +from datetime import datetime from ..config.database import get_db from ..config.logging_config import get_logger -from ..config.settings import settings -from ..middleware.cookie_consent import COOKIE_CONSENT_COOKIE_NAME, _parse_consent_cookie -from ..schemas.admin_privacy import PublicPrivacyConfigResponse -from ..schemas.privacy import CookieCategoryPreferences, CookieConsent, CookieConsentResponse, UpdateCookieConsentRequest +from ..models.page_content import PageContent, PageType from ..services.privacy_admin_service import privacy_admin_service +from ..schemas.privacy import CookieConsent, CookieConsentResponse, UpdateCookieConsentRequest, CookieCategoryPreferences +from ..schemas.admin_privacy import PublicPrivacyConfigResponse logger = get_logger(__name__) router = APIRouter(prefix='/privacy', tags=['privacy']) -@router.get('/cookie-consent', response_model=CookieConsentResponse, status_code=status.HTTP_200_OK) -async def get_cookie_consent(request: Request) -> CookieConsentResponse: - raw_cookie = request.cookies.get(COOKIE_CONSENT_COOKIE_NAME) - consent = _parse_consent_cookie(raw_cookie) - consent.categories.necessary = True - return CookieConsentResponse(data=consent) +def serialize_page_content(content: PageContent) -> dict: + return { + 'id': content.id, + 'page_type': content.page_type.value, + 'title': content.title, + 'subtitle': content.subtitle, + 'description': content.description, + 'content': content.content, + 'meta_title': content.meta_title, + 'meta_description': content.meta_description, + 'meta_keywords': content.meta_keywords, + 'og_title': content.og_title, + 'og_description': content.og_description, + 'og_image': content.og_image, + 'canonical_url': content.canonical_url, + 'is_active': content.is_active, + 'created_at': content.created_at.isoformat() if content.created_at else None, + 'updated_at': content.updated_at.isoformat() if content.updated_at else None + } -@router.post('/cookie-consent', response_model=CookieConsentResponse, status_code=status.HTTP_200_OK) -async def update_cookie_consent(request: UpdateCookieConsentRequest, response: Response) -> CookieConsentResponse: - existing_raw = response.headers.get('cookie') - categories = CookieCategoryPreferences() - if request.analytics is not None: - categories.analytics = request.analytics - if request.marketing is not None: - categories.marketing = request.marketing - if request.preferences is not None: - categories.preferences = request.preferences - categories.necessary = True - consent = CookieConsent(categories=categories, has_decided=True) - response.set_cookie(key=COOKIE_CONSENT_COOKIE_NAME, value=consent.model_dump_json(), httponly=True, secure=settings.is_production, samesite='lax', max_age=365 * 24 * 60 * 60, path='/') - logger.info('Cookie consent updated: analytics=%s, marketing=%s, preferences=%s', consent.categories.analytics, consent.categories.marketing, consent.categories.preferences) - return CookieConsentResponse(data=consent) +@router.get('/') +async def get_privacy_content(db: Session=Depends(get_db)): + try: + content = db.query(PageContent).filter(PageContent.page_type == PageType.PRIVACY).first() + if not content: + return {'status': 'success', 'data': {'page_content': None, 'is_active': False}} + if not content.is_active: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Privacy policy page is currently disabled') + content_dict = serialize_page_content(content) + return {'status': 'success', 'data': {'page_content': content_dict}} + except HTTPException: + raise + except Exception as e: + logger.error(f'Error fetching privacy content: {str(e)}', exc_info=True) + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching privacy content: {str(e)}') -@router.get('/config', response_model=PublicPrivacyConfigResponse, status_code=status.HTTP_200_OK) -async def get_public_privacy_config(db: Session=Depends(get_db)) -> PublicPrivacyConfigResponse: - config = privacy_admin_service.get_public_privacy_config(db) - return PublicPrivacyConfigResponse(data=config) \ No newline at end of file +@router.get('/cookie-consent', response_model=CookieConsentResponse) +async def get_cookie_consent(db: Session=Depends(get_db)): + """ + Get the default cookie consent structure. + Note: Actual consent is stored client-side (localStorage), this endpoint provides the default structure. + """ + try: + # Return default consent structure + # The actual consent state is managed client-side + consent = CookieConsent( + version=1, + updated_at=datetime.utcnow(), + has_decided=False, + categories=CookieCategoryPreferences( + necessary=True, # Always true + analytics=False, + marketing=False, + preferences=False + ) + ) + return CookieConsentResponse(status='success', data=consent) + except Exception as e: + logger.error(f'Error fetching cookie consent: {str(e)}', exc_info=True) + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching cookie consent: {str(e)}') + +@router.post('/cookie-consent', response_model=CookieConsentResponse) +async def update_cookie_consent(payload: UpdateCookieConsentRequest, db: Session=Depends(get_db)): + """ + Update cookie consent preferences. + Note: This endpoint acknowledges the consent update. Actual consent is stored client-side. + """ + try: + # Create updated consent structure + consent = CookieConsent( + version=1, + updated_at=datetime.utcnow(), + has_decided=True, + categories=CookieCategoryPreferences( + necessary=True, # Always true + analytics=payload.analytics if payload.analytics is not None else False, + marketing=payload.marketing if payload.marketing is not None else False, + preferences=payload.preferences if payload.preferences is not None else False + ) + ) + return CookieConsentResponse(status='success', data=consent) + except Exception as e: + logger.error(f'Error updating cookie consent: {str(e)}', exc_info=True) + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error updating cookie consent: {str(e)}') + +@router.get('/config', response_model=PublicPrivacyConfigResponse) +async def get_public_privacy_config(db: Session=Depends(get_db)): + """ + Get public privacy configuration including cookie policy settings and integration configs. + This endpoint is public and does not require authentication. + """ + try: + config = privacy_admin_service.get_public_privacy_config(db) + return PublicPrivacyConfigResponse(status='success', data=config) + except Exception as e: + logger.error(f'Error fetching public privacy config: {str(e)}', exc_info=True) + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching public privacy config: {str(e)}') diff --git a/Backend/src/routes/refunds_routes.py b/Backend/src/routes/refunds_routes.py new file mode 100644 index 00000000..3f3109bf --- /dev/null +++ b/Backend/src/routes/refunds_routes.py @@ -0,0 +1,45 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +import json +from ..config.database import get_db +from ..config.logging_config import get_logger +from ..models.page_content import PageContent, PageType +logger = get_logger(__name__) +router = APIRouter(prefix='/refunds', tags=['refunds']) + +def serialize_page_content(content: PageContent) -> dict: + return { + 'id': content.id, + 'page_type': content.page_type.value, + 'title': content.title, + 'subtitle': content.subtitle, + 'description': content.description, + 'content': content.content, + 'meta_title': content.meta_title, + 'meta_description': content.meta_description, + 'meta_keywords': content.meta_keywords, + 'og_title': content.og_title, + 'og_description': content.og_description, + 'og_image': content.og_image, + 'canonical_url': content.canonical_url, + 'is_active': content.is_active, + 'created_at': content.created_at.isoformat() if content.created_at else None, + 'updated_at': content.updated_at.isoformat() if content.updated_at else None + } + +@router.get('/') +async def get_refunds_content(db: Session=Depends(get_db)): + try: + content = db.query(PageContent).filter(PageContent.page_type == PageType.REFUNDS).first() + if not content: + return {'status': 'success', 'data': {'page_content': None, 'is_active': False}} + if not content.is_active: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Refunds policy page is currently disabled') + content_dict = serialize_page_content(content) + return {'status': 'success', 'data': {'page_content': content_dict}} + except HTTPException: + raise + except Exception as e: + logger.error(f'Error fetching refunds content: {str(e)}', exc_info=True) + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching refunds content: {str(e)}') + diff --git a/Backend/src/routes/terms_routes.py b/Backend/src/routes/terms_routes.py new file mode 100644 index 00000000..f82d58e8 --- /dev/null +++ b/Backend/src/routes/terms_routes.py @@ -0,0 +1,45 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +import json +from ..config.database import get_db +from ..config.logging_config import get_logger +from ..models.page_content import PageContent, PageType +logger = get_logger(__name__) +router = APIRouter(prefix='/terms', tags=['terms']) + +def serialize_page_content(content: PageContent) -> dict: + return { + 'id': content.id, + 'page_type': content.page_type.value, + 'title': content.title, + 'subtitle': content.subtitle, + 'description': content.description, + 'content': content.content, + 'meta_title': content.meta_title, + 'meta_description': content.meta_description, + 'meta_keywords': content.meta_keywords, + 'og_title': content.og_title, + 'og_description': content.og_description, + 'og_image': content.og_image, + 'canonical_url': content.canonical_url, + 'is_active': content.is_active, + 'created_at': content.created_at.isoformat() if content.created_at else None, + 'updated_at': content.updated_at.isoformat() if content.updated_at else None + } + +@router.get('/') +async def get_terms_content(db: Session=Depends(get_db)): + try: + content = db.query(PageContent).filter(PageContent.page_type == PageType.TERMS).first() + if not content: + return {'status': 'success', 'data': {'page_content': None, 'is_active': False}} + if not content.is_active: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Terms & Conditions page is currently disabled') + content_dict = serialize_page_content(content) + return {'status': 'success', 'data': {'page_content': content_dict}} + except HTTPException: + raise + except Exception as e: + logger.error(f'Error fetching terms content: {str(e)}', exc_info=True) + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching terms content: {str(e)}') + diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx index d472f0f8..f8f7cc68 100644 --- a/Frontend/src/App.tsx +++ b/Frontend/src/App.tsx @@ -14,6 +14,7 @@ import { CompanySettingsProvider } from './contexts/CompanySettingsContext'; import { AuthModalProvider } from './contexts/AuthModalContext'; import OfflineIndicator from './components/common/OfflineIndicator'; import CookieConsentBanner from './components/common/CookieConsentBanner'; +import CookiePreferencesModal from './components/common/CookiePreferencesModal'; import AnalyticsLoader from './components/common/AnalyticsLoader'; import Loading from './components/common/Loading'; import ScrollToTop from './components/common/ScrollToTop'; @@ -53,6 +54,12 @@ const InvoicePage = lazy(() => import('./pages/customer/InvoicePage')); const ProfilePage = lazy(() => import('./pages/customer/ProfilePage')); const AboutPage = lazy(() => import('./pages/AboutPage')); const ContactPage = lazy(() => import('./pages/ContactPage')); +const PrivacyPolicyPage = lazy(() => import('./pages/PrivacyPolicyPage')); +const TermsPage = lazy(() => import('./pages/TermsPage')); +const RefundsPolicyPage = lazy(() => import('./pages/RefundsPolicyPage')); +const CancellationPolicyPage = lazy(() => import('./pages/CancellationPolicyPage')); +const AccessibilityPage = lazy(() => import('./pages/AccessibilityPage')); +const FAQPage = lazy(() => import('./pages/FAQPage')); const AdminDashboardPage = lazy(() => import('./pages/admin/DashboardPage')); const InvoiceManagementPage = lazy(() => import('./pages/admin/InvoiceManagementPage')); @@ -188,6 +195,30 @@ function App() { path="contact" element={} /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> {} + diff --git a/Frontend/src/components/common/CookiePreferencesLink.tsx b/Frontend/src/components/common/CookiePreferencesLink.tsx index 7a86dfe2..feebdd37 100644 --- a/Frontend/src/components/common/CookiePreferencesLink.tsx +++ b/Frontend/src/components/common/CookiePreferencesLink.tsx @@ -1,15 +1,7 @@ import React from 'react'; -import { useCookieConsent } from '../../contexts/CookieConsentContext'; const CookiePreferencesLink: React.FC = () => { - const { hasDecided } = useCookieConsent(); - - if (!hasDecided) { - return null; - } - const handleClick = () => { - window.dispatchEvent(new CustomEvent('open-cookie-preferences')); }; @@ -17,9 +9,9 @@ const CookiePreferencesLink: React.FC = () => { ); }; diff --git a/Frontend/src/components/common/CookiePreferencesModal.tsx b/Frontend/src/components/common/CookiePreferencesModal.tsx new file mode 100644 index 00000000..bbb93132 --- /dev/null +++ b/Frontend/src/components/common/CookiePreferencesModal.tsx @@ -0,0 +1,223 @@ +import React, { useEffect, useState } from 'react'; +import { X } from 'lucide-react'; +import { useCookieConsent } from '../../contexts/CookieConsentContext'; +import { useClickOutside } from '../../hooks/useClickOutside'; + +const CookiePreferencesModal: React.FC = () => { + const { consent, updateConsent } = useCookieConsent(); + const [isOpen, setIsOpen] = useState(false); + const [analyticsChecked, setAnalyticsChecked] = useState(false); + const [marketingChecked, setMarketingChecked] = useState(false); + const [preferencesChecked, setPreferencesChecked] = useState(false); + const modalRef = React.useRef(null); + + useClickOutside(modalRef, () => { + if (isOpen) { + setIsOpen(false); + } + }); + + useEffect(() => { + if (consent) { + setAnalyticsChecked(consent.categories.analytics); + setMarketingChecked(consent.categories.marketing); + setPreferencesChecked(consent.categories.preferences); + } + }, [consent]); + + useEffect(() => { + const handleOpenPreferences = () => { + setIsOpen(true); + }; + + window.addEventListener('open-cookie-preferences', handleOpenPreferences); + return () => { + window.removeEventListener('open-cookie-preferences', handleOpenPreferences); + }; + }, []); + + useEffect(() => { + if (isOpen) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = ''; + } + return () => { + document.body.style.overflow = ''; + }; + }, [isOpen]); + + if (!isOpen) { + return null; + } + + const handleAcceptAll = async () => { + await updateConsent({ + analytics: true, + marketing: true, + preferences: true, + }); + setIsOpen(false); + }; + + const handleRejectNonEssential = async () => { + await updateConsent({ + analytics: false, + marketing: false, + preferences: false, + }); + setIsOpen(false); + }; + + const handleSaveSelection = async () => { + await updateConsent({ + analytics: analyticsChecked, + marketing: marketingChecked, + preferences: preferencesChecked, + }); + setIsOpen(false); + }; + + return ( +
+ {/* Backdrop */} +
setIsOpen(false)} + aria-hidden="true" + /> + + {/* Modal */} +
+
+ {/* Close button */} + + +
+ {/* Header */} +
+
+ + Privacy Suite +
+

+ Cookie Preferences +

+

+ Manage your cookie preferences. You can enable or disable different types of cookies below. +

+
+ + {/* Cookie Categories */} +
+
+
+
+ +
+
+

Strictly necessary

+

+ Essential for security, authentication, and core booking flows. These are always enabled. +

+
+
+
+ +
+
+ setAnalyticsChecked(e.target.checked)} + /> + +
+
+ +
+
+ setMarketingChecked(e.target.checked)} + /> + +
+
+ +
+
+ setPreferencesChecked(e.target.checked)} + /> + +
+
+
+ + {/* Action Buttons */} +
+ + + +
+
+
+
+
+ ); +}; + +export default CookiePreferencesModal; + diff --git a/Frontend/src/components/layout/Footer.tsx b/Frontend/src/components/layout/Footer.tsx index e759f1eb..125006b9 100644 --- a/Frontend/src/components/layout/Footer.tsx +++ b/Frontend/src/components/layout/Footer.tsx @@ -34,6 +34,7 @@ import { useCompanySettings } from '../../contexts/CompanySettingsContext'; const Footer: React.FC = () => { const { settings } = useCompanySettings(); const [pageContent, setPageContent] = useState(null); + const [enabledPages, setEnabledPages] = useState>(new Set()); useEffect(() => { const fetchPageContent = async () => { @@ -44,11 +45,41 @@ const Footer: React.FC = () => { } } catch (err: any) { console.error('Error fetching footer content:', err); - } }; + const checkEnabledPages = async () => { + const enabled = new Set(); + const policyPages = [ + { type: 'privacy', url: '/privacy', service: () => pageContentService.getPrivacyContent() }, + { type: 'terms', url: '/terms', service: () => pageContentService.getTermsContent() }, + { type: 'refunds', url: '/refunds', service: () => pageContentService.getRefundsContent() }, + { type: 'cancellation', url: '/cancellation', service: () => pageContentService.getCancellationContent() }, + { type: 'accessibility', url: '/accessibility', service: () => pageContentService.getAccessibilityContent() }, + { type: 'faq', url: '/faq', service: () => pageContentService.getFAQContent() }, + ]; + + await Promise.all( + policyPages.map(async (page) => { + try { + const response = await page.service(); + if (response.status === 'success' && response.data?.page_content?.is_active) { + enabled.add(page.url); + } + } catch (err: any) { + // If 404, page is disabled, don't add to enabled set + if (err.response?.status !== 404) { + console.error(`Error checking ${page.type} page:`, err); + } + } + }) + ); + + setEnabledPages(enabled); + }; + fetchPageContent(); + checkEnabledPages(); }, []); @@ -97,6 +128,9 @@ const Footer: React.FC = () => { { label: 'FAQ', url: '/faq' }, { label: 'Terms of Service', url: '/terms' }, { label: 'Privacy Policy', url: '/privacy' }, + { label: 'Refunds Policy', url: '/refunds' }, + { label: 'Cancellation Policy', url: '/cancellation' }, + { label: 'Accessibility', url: '/accessibility' }, { label: 'Contact Us', url: '/contact' } ]; @@ -104,10 +138,18 @@ const Footer: React.FC = () => { ? pageContent.footer_links.quick_links : defaultQuickLinks; - const supportLinks = pageContent?.footer_links?.support_links && pageContent.footer_links.support_links.length > 0 + const allSupportLinks = pageContent?.footer_links?.support_links && pageContent.footer_links.support_links.length > 0 ? pageContent.footer_links.support_links : defaultSupportLinks; + // Filter support links to only show enabled policy pages + const supportLinks = allSupportLinks.filter((link) => { + // Always show Contact Us + if (link.url === '/contact') return true; + // Only show policy pages if they are enabled + return enabledPages.has(link.url); + }); + return (
{/* Top Gold Accent Line */} @@ -370,9 +412,11 @@ const Footer: React.FC = () => { })()}
- Privacy + Privacy - Terms + Terms + + Refunds
diff --git a/Frontend/src/pages/AccessibilityPage.tsx b/Frontend/src/pages/AccessibilityPage.tsx new file mode 100644 index 00000000..a69d9a0d --- /dev/null +++ b/Frontend/src/pages/AccessibilityPage.tsx @@ -0,0 +1,178 @@ +import React, { useState, useEffect } from 'react'; +import { Accessibility, ArrowLeft } from 'lucide-react'; +import { Link } from 'react-router-dom'; +import { pageContentService } from '../services/api'; +import type { PageContent } from '../services/api/pageContentService'; +import { useCompanySettings } from '../contexts/CompanySettingsContext'; +import Loading from '../components/common/Loading'; + +const AccessibilityPage: React.FC = () => { + const { settings } = useCompanySettings(); + const [pageContent, setPageContent] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchPageContent = async () => { + try { + setLoading(true); + const response = await pageContentService.getAccessibilityContent(); + if (response.status === 'success' && response.data?.page_content) { + const content = response.data.page_content; + + if (content.content) { + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = content.content; + + const allElements = tempDiv.querySelectorAll('*'); + allElements.forEach((el) => { + const htmlEl = el as HTMLElement; + const tagName = htmlEl.tagName.toLowerCase(); + const currentColor = htmlEl.style.color; + + if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') { + if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) { + htmlEl.style.color = '#ffffff'; + } else if (['strong', 'b'].includes(tagName)) { + htmlEl.style.color = '#d4af37'; + } else if (tagName === 'a') { + htmlEl.style.color = '#d4af37'; + } else { + htmlEl.style.color = '#d1d5db'; + } + } + }); + + content.content = tempDiv.innerHTML; + } + + setPageContent(content); + + if (content.meta_title) { + document.title = content.meta_title; + } + if (content.meta_description) { + let metaDescription = document.querySelector('meta[name="description"]'); + if (!metaDescription) { + metaDescription = document.createElement('meta'); + metaDescription.setAttribute('name', 'description'); + document.head.appendChild(metaDescription); + } + metaDescription.setAttribute('content', content.meta_description); + } + } + } catch (err: any) { + console.error('Error fetching page content:', err); + // If page is disabled (404), set pageContent to null to show disabled message + if (err.response?.status === 404) { + setPageContent(null); + } + } finally { + setLoading(false); + } + }; + + fetchPageContent(); + }, []); + + if (loading) { + return ; + } + + if (!pageContent) { + return ( +
+
+ +

Accessibility

+

This page is currently unavailable.

+ + + Back to Home + +
+
+ ); + } + + return ( +
+
+ + + Back to Home + + +
+
+ +
+

+ {pageContent.title || 'Accessibility'} +

+ {pageContent.subtitle && ( +

+ {pageContent.subtitle} +

+ )} +
+ +
+
No content available.

' + }} + /> +
+ + {settings.company_email && ( +
+

+ For accessibility inquiries, contact us at{' '} + + {settings.company_email} + +

+
+ )} +
+
+ ); +}; + +export default AccessibilityPage; + diff --git a/Frontend/src/pages/CancellationPolicyPage.tsx b/Frontend/src/pages/CancellationPolicyPage.tsx new file mode 100644 index 00000000..d9299d48 --- /dev/null +++ b/Frontend/src/pages/CancellationPolicyPage.tsx @@ -0,0 +1,179 @@ +import React, { useState, useEffect } from 'react'; +import { XCircle, ArrowLeft } from 'lucide-react'; +import { Link } from 'react-router-dom'; +import { pageContentService } from '../services/api'; +import type { PageContent } from '../services/api/pageContentService'; +import { useCompanySettings } from '../contexts/CompanySettingsContext'; +import Loading from '../components/common/Loading'; + +const CancellationPolicyPage: React.FC = () => { + const { settings } = useCompanySettings(); + const [pageContent, setPageContent] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchPageContent = async () => { + try { + setLoading(true); + const response = await pageContentService.getCancellationContent(); + if (response.status === 'success' && response.data?.page_content) { + const content = response.data.page_content; + + // Process HTML content to ensure text is visible + if (content.content) { + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = content.content; + + const allElements = tempDiv.querySelectorAll('*'); + allElements.forEach((el) => { + const htmlEl = el as HTMLElement; + const tagName = htmlEl.tagName.toLowerCase(); + const currentColor = htmlEl.style.color; + + if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') { + if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) { + htmlEl.style.color = '#ffffff'; + } else if (['strong', 'b'].includes(tagName)) { + htmlEl.style.color = '#d4af37'; + } else if (tagName === 'a') { + htmlEl.style.color = '#d4af37'; + } else { + htmlEl.style.color = '#d1d5db'; + } + } + }); + + content.content = tempDiv.innerHTML; + } + + setPageContent(content); + + if (content.meta_title) { + document.title = content.meta_title; + } + if (content.meta_description) { + let metaDescription = document.querySelector('meta[name="description"]'); + if (!metaDescription) { + metaDescription = document.createElement('meta'); + metaDescription.setAttribute('name', 'description'); + document.head.appendChild(metaDescription); + } + metaDescription.setAttribute('content', content.meta_description); + } + } + } catch (err: any) { + console.error('Error fetching page content:', err); + // If page is disabled (404), set pageContent to null to show disabled message + if (err.response?.status === 404) { + setPageContent(null); + } + } finally { + setLoading(false); + } + }; + + fetchPageContent(); + }, []); + + if (loading) { + return ; + } + + if (!pageContent) { + return ( +
+
+ +

Cancellation Policy

+

This page is currently unavailable.

+ + + Back to Home + +
+
+ ); + } + + return ( +
+
+ + + Back to Home + + +
+
+ +
+

+ {pageContent.title || 'Cancellation Policy'} +

+ {pageContent.subtitle && ( +

+ {pageContent.subtitle} +

+ )} +
+ +
+
No content available.

' + }} + /> +
+ + {settings.company_email && ( +
+

+ For questions about cancellations, contact us at{' '} + + {settings.company_email} + +

+
+ )} +
+
+ ); +}; + +export default CancellationPolicyPage; + diff --git a/Frontend/src/pages/FAQPage.tsx b/Frontend/src/pages/FAQPage.tsx new file mode 100644 index 00000000..91771162 --- /dev/null +++ b/Frontend/src/pages/FAQPage.tsx @@ -0,0 +1,178 @@ +import React, { useState, useEffect } from 'react'; +import { HelpCircle, ArrowLeft } from 'lucide-react'; +import { Link } from 'react-router-dom'; +import { pageContentService } from '../services/api'; +import type { PageContent } from '../services/api/pageContentService'; +import { useCompanySettings } from '../contexts/CompanySettingsContext'; +import Loading from '../components/common/Loading'; + +const FAQPage: React.FC = () => { + const { settings } = useCompanySettings(); + const [pageContent, setPageContent] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchPageContent = async () => { + try { + setLoading(true); + const response = await pageContentService.getFAQContent(); + if (response.status === 'success' && response.data?.page_content) { + const content = response.data.page_content; + + if (content.content) { + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = content.content; + + const allElements = tempDiv.querySelectorAll('*'); + allElements.forEach((el) => { + const htmlEl = el as HTMLElement; + const tagName = htmlEl.tagName.toLowerCase(); + const currentColor = htmlEl.style.color; + + if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') { + if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) { + htmlEl.style.color = '#ffffff'; + } else if (['strong', 'b'].includes(tagName)) { + htmlEl.style.color = '#d4af37'; + } else if (tagName === 'a') { + htmlEl.style.color = '#d4af37'; + } else { + htmlEl.style.color = '#d1d5db'; + } + } + }); + + content.content = tempDiv.innerHTML; + } + + setPageContent(content); + + if (content.meta_title) { + document.title = content.meta_title; + } + if (content.meta_description) { + let metaDescription = document.querySelector('meta[name="description"]'); + if (!metaDescription) { + metaDescription = document.createElement('meta'); + metaDescription.setAttribute('name', 'description'); + document.head.appendChild(metaDescription); + } + metaDescription.setAttribute('content', content.meta_description); + } + } + } catch (err: any) { + console.error('Error fetching page content:', err); + // If page is disabled (404), set pageContent to null to show disabled message + if (err.response?.status === 404) { + setPageContent(null); + } + } finally { + setLoading(false); + } + }; + + fetchPageContent(); + }, []); + + if (loading) { + return ; + } + + if (!pageContent) { + return ( +
+
+ +

Frequently Asked Questions

+

This page is currently unavailable.

+ + + Back to Home + +
+
+ ); + } + + return ( +
+
+ + + Back to Home + + +
+
+ +
+

+ {pageContent.title || 'Frequently Asked Questions'} +

+ {pageContent.subtitle && ( +

+ {pageContent.subtitle} +

+ )} +
+ +
+
No content available.

' + }} + /> +
+ + {settings.company_email && ( +
+

+ Still have questions? Contact us at{' '} + + {settings.company_email} + +

+
+ )} +
+
+ ); +}; + +export default FAQPage; + diff --git a/Frontend/src/pages/PrivacyPolicyPage.tsx b/Frontend/src/pages/PrivacyPolicyPage.tsx new file mode 100644 index 00000000..e01c8d36 --- /dev/null +++ b/Frontend/src/pages/PrivacyPolicyPage.tsx @@ -0,0 +1,187 @@ +import React, { useState, useEffect } from 'react'; +import { Shield, ArrowLeft } from 'lucide-react'; +import { Link } from 'react-router-dom'; +import { pageContentService } from '../services/api'; +import type { PageContent } from '../services/api/pageContentService'; +import { useCompanySettings } from '../contexts/CompanySettingsContext'; +import Loading from '../components/common/Loading'; + +const PrivacyPolicyPage: React.FC = () => { + const { settings } = useCompanySettings(); + const [pageContent, setPageContent] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchPageContent = async () => { + try { + setLoading(true); + const response = await pageContentService.getPrivacyContent(); + if (response.status === 'success' && response.data?.page_content) { + const content = response.data.page_content; + + // Process HTML content to ensure text is visible + if (content.content) { + // Create a temporary div to parse HTML + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = content.content; + + // Add color styles to elements that don't have them + const allElements = tempDiv.querySelectorAll('*'); + allElements.forEach((el) => { + const htmlEl = el as HTMLElement; + const tagName = htmlEl.tagName.toLowerCase(); + const currentColor = htmlEl.style.color; + + // Only add color if not already set + if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') { + if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) { + htmlEl.style.color = '#ffffff'; + } else if (['strong', 'b'].includes(tagName)) { + htmlEl.style.color = '#d4af37'; + } else if (tagName === 'a') { + htmlEl.style.color = '#d4af37'; + } else { + htmlEl.style.color = '#d1d5db'; + } + } + }); + + // Update content with processed HTML + content.content = tempDiv.innerHTML; + } + + setPageContent(content); + + if (content.meta_title) { + document.title = content.meta_title; + } + if (content.meta_description) { + let metaDescription = document.querySelector('meta[name="description"]'); + if (!metaDescription) { + metaDescription = document.createElement('meta'); + metaDescription.setAttribute('name', 'description'); + document.head.appendChild(metaDescription); + } + metaDescription.setAttribute('content', content.meta_description); + } + } + } catch (err: any) { + console.error('Error fetching page content:', err); + // If page is disabled (404), set pageContent to null to show disabled message + if (err.response?.status === 404) { + setPageContent(null); + } + } finally { + setLoading(false); + } + }; + + fetchPageContent(); + }, []); + + if (loading) { + return ; + } + + if (!pageContent) { + return ( +
+
+ +

Privacy Policy

+

This page is currently unavailable.

+ + + Back to Home + +
+
+ ); + } + + return ( +
+
+ {/* Back Link */} + + + Back to Home + + + {/* Header */} +
+
+ +
+

+ {pageContent.title || 'Privacy Policy'} +

+ {pageContent.subtitle && ( +

+ {pageContent.subtitle} +

+ )} +
+ + {/* Content */} +
+
No content available.

' + }} + /> +
+ + {/* Footer Note */} + {settings.company_email && ( +
+

+ For questions about this policy, contact us at{' '} + + {settings.company_email} + +

+
+ )} +
+
+ ); +}; + +export default PrivacyPolicyPage; + diff --git a/Frontend/src/pages/RefundsPolicyPage.tsx b/Frontend/src/pages/RefundsPolicyPage.tsx new file mode 100644 index 00000000..0c726235 --- /dev/null +++ b/Frontend/src/pages/RefundsPolicyPage.tsx @@ -0,0 +1,187 @@ +import React, { useState, useEffect } from 'react'; +import { RefreshCw, ArrowLeft } from 'lucide-react'; +import { Link } from 'react-router-dom'; +import { pageContentService } from '../services/api'; +import type { PageContent } from '../services/api/pageContentService'; +import { useCompanySettings } from '../contexts/CompanySettingsContext'; +import Loading from '../components/common/Loading'; + +const RefundsPolicyPage: React.FC = () => { + const { settings } = useCompanySettings(); + const [pageContent, setPageContent] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchPageContent = async () => { + try { + setLoading(true); + const response = await pageContentService.getRefundsContent(); + if (response.status === 'success' && response.data?.page_content) { + const content = response.data.page_content; + + // Process HTML content to ensure text is visible + if (content.content) { + // Create a temporary div to parse HTML + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = content.content; + + // Add color styles to elements that don't have them + const allElements = tempDiv.querySelectorAll('*'); + allElements.forEach((el) => { + const htmlEl = el as HTMLElement; + const tagName = htmlEl.tagName.toLowerCase(); + const currentColor = htmlEl.style.color; + + // Only add color if not already set + if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') { + if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) { + htmlEl.style.color = '#ffffff'; + } else if (['strong', 'b'].includes(tagName)) { + htmlEl.style.color = '#d4af37'; + } else if (tagName === 'a') { + htmlEl.style.color = '#d4af37'; + } else { + htmlEl.style.color = '#d1d5db'; + } + } + }); + + // Update content with processed HTML + content.content = tempDiv.innerHTML; + } + + setPageContent(content); + + if (content.meta_title) { + document.title = content.meta_title; + } + if (content.meta_description) { + let metaDescription = document.querySelector('meta[name="description"]'); + if (!metaDescription) { + metaDescription = document.createElement('meta'); + metaDescription.setAttribute('name', 'description'); + document.head.appendChild(metaDescription); + } + metaDescription.setAttribute('content', content.meta_description); + } + } + } catch (err: any) { + console.error('Error fetching page content:', err); + // If page is disabled (404), set pageContent to null to show disabled message + if (err.response?.status === 404) { + setPageContent(null); + } + } finally { + setLoading(false); + } + }; + + fetchPageContent(); + }, []); + + if (loading) { + return ; + } + + if (!pageContent) { + return ( +
+
+ +

Refunds Policy

+

This page is currently unavailable.

+ + + Back to Home + +
+
+ ); + } + + return ( +
+
+ {/* Back Link */} + + + Back to Home + + + {/* Header */} +
+
+ +
+

+ {pageContent.title || 'Refunds Policy'} +

+ {pageContent.subtitle && ( +

+ {pageContent.subtitle} +

+ )} +
+ + {/* Content */} +
+
No content available.

' + }} + /> +
+ + {/* Footer Note */} + {settings.company_email && ( +
+

+ For refund inquiries, contact us at{' '} + + {settings.company_email} + +

+
+ )} +
+
+ ); +}; + +export default RefundsPolicyPage; + diff --git a/Frontend/src/pages/TermsPage.tsx b/Frontend/src/pages/TermsPage.tsx new file mode 100644 index 00000000..2a55e14c --- /dev/null +++ b/Frontend/src/pages/TermsPage.tsx @@ -0,0 +1,187 @@ +import React, { useState, useEffect } from 'react'; +import { Scale, ArrowLeft } from 'lucide-react'; +import { Link } from 'react-router-dom'; +import { pageContentService } from '../services/api'; +import type { PageContent } from '../services/api/pageContentService'; +import { useCompanySettings } from '../contexts/CompanySettingsContext'; +import Loading from '../components/common/Loading'; + +const TermsPage: React.FC = () => { + const { settings } = useCompanySettings(); + const [pageContent, setPageContent] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchPageContent = async () => { + try { + setLoading(true); + const response = await pageContentService.getTermsContent(); + if (response.status === 'success' && response.data?.page_content) { + const content = response.data.page_content; + + // Process HTML content to ensure text is visible + if (content.content) { + // Create a temporary div to parse HTML + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = content.content; + + // Add color styles to elements that don't have them + const allElements = tempDiv.querySelectorAll('*'); + allElements.forEach((el) => { + const htmlEl = el as HTMLElement; + const tagName = htmlEl.tagName.toLowerCase(); + const currentColor = htmlEl.style.color; + + // Only add color if not already set + if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') { + if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) { + htmlEl.style.color = '#ffffff'; + } else if (['strong', 'b'].includes(tagName)) { + htmlEl.style.color = '#d4af37'; + } else if (tagName === 'a') { + htmlEl.style.color = '#d4af37'; + } else { + htmlEl.style.color = '#d1d5db'; + } + } + }); + + // Update content with processed HTML + content.content = tempDiv.innerHTML; + } + + setPageContent(content); + + if (content.meta_title) { + document.title = content.meta_title; + } + if (content.meta_description) { + let metaDescription = document.querySelector('meta[name="description"]'); + if (!metaDescription) { + metaDescription = document.createElement('meta'); + metaDescription.setAttribute('name', 'description'); + document.head.appendChild(metaDescription); + } + metaDescription.setAttribute('content', content.meta_description); + } + } + } catch (err: any) { + console.error('Error fetching page content:', err); + // If page is disabled (404), set pageContent to null to show disabled message + if (err.response?.status === 404) { + setPageContent(null); + } + } finally { + setLoading(false); + } + }; + + fetchPageContent(); + }, []); + + if (loading) { + return ; + } + + if (!pageContent) { + return ( +
+
+ +

Terms & Conditions

+

This page is currently unavailable.

+ + + Back to Home + +
+
+ ); + } + + return ( +
+
+ {/* Back Link */} + + + Back to Home + + + {/* Header */} +
+
+ +
+

+ {pageContent.title || 'Terms & Conditions'} +

+ {pageContent.subtitle && ( +

+ {pageContent.subtitle} +

+ )} +
+ + {/* Content */} +
+
No content available.

' + }} + /> +
+ + {/* Footer Note */} + {settings.company_email && ( +
+

+ For questions about these terms, contact us at{' '} + + {settings.company_email} + +

+
+ )} +
+
+ ); +}; + +export default TermsPage; + diff --git a/Frontend/src/pages/admin/PageContentDashboard.tsx b/Frontend/src/pages/admin/PageContentDashboard.tsx index f38cc40d..4f931121 100644 --- a/Frontend/src/pages/admin/PageContentDashboard.tsx +++ b/Frontend/src/pages/admin/PageContentDashboard.tsx @@ -18,7 +18,10 @@ import { Check, XCircle, Award, - Shield + Shield, + RefreshCw, + Accessibility, + HelpCircle } from 'lucide-react'; import { pageContentService, PageContent, PageType, UpdatePageContentData, bannerService, Banner } from '../../services/api'; import { toast } from 'react-toastify'; @@ -26,7 +29,7 @@ import Loading from '../../components/common/Loading'; import { ConfirmationDialog } from '../../components/common'; import IconPicker from '../../components/admin/IconPicker'; -type ContentTab = 'overview' | 'home' | 'contact' | 'about' | 'footer' | 'seo'; +type ContentTab = 'overview' | 'home' | 'contact' | 'about' | 'footer' | 'seo' | 'privacy' | 'terms' | 'refunds' | 'cancellation' | 'accessibility' | 'faq'; const PageContentDashboard: React.FC = () => { const [activeTab, setActiveTab] = useState('overview'); @@ -38,6 +41,12 @@ const PageContentDashboard: React.FC = () => { about: null, footer: null, seo: null, + privacy: null, + terms: null, + refunds: null, + cancellation: null, + accessibility: null, + faq: null, }); // Form states for each page @@ -46,6 +55,12 @@ const PageContentDashboard: React.FC = () => { const [aboutData, setAboutData] = useState({}); const [footerData, setFooterData] = useState({}); const [seoData, setSeoData] = useState({}); + const [privacyData, setPrivacyData] = useState({ is_active: true }); + const [termsData, setTermsData] = useState({ is_active: true }); + const [refundsData, setRefundsData] = useState({ is_active: true }); + const [cancellationData, setCancellationData] = useState({ is_active: true }); + const [accessibilityData, setAccessibilityData] = useState({ is_active: true }); + const [faqData, setFaqData] = useState({ is_active: true }); // Banner management state const [banners, setBanners] = useState([]); @@ -88,6 +103,12 @@ const PageContentDashboard: React.FC = () => { about: null, footer: null, seo: null, + privacy: null, + terms: null, + refunds: null, + cancellation: null, + accessibility: null, + faq: null, }; contents.forEach((content) => { @@ -241,6 +262,84 @@ const PageContentDashboard: React.FC = () => { canonical_url: contents.seo.canonical_url || '', }); } + + // Privacy + if (contents.privacy) { + setPrivacyData({ + title: contents.privacy.title || '', + subtitle: contents.privacy.subtitle || '', + description: contents.privacy.description || '', + content: contents.privacy.content || '', + meta_title: contents.privacy.meta_title || '', + meta_description: contents.privacy.meta_description || '', + is_active: contents.privacy.is_active ?? true, + }); + } + + // Terms + if (contents.terms) { + setTermsData({ + title: contents.terms.title || '', + subtitle: contents.terms.subtitle || '', + description: contents.terms.description || '', + content: contents.terms.content || '', + meta_title: contents.terms.meta_title || '', + meta_description: contents.terms.meta_description || '', + is_active: contents.terms.is_active ?? true, + }); + } + + // Refunds + if (contents.refunds) { + setRefundsData({ + title: contents.refunds.title || '', + subtitle: contents.refunds.subtitle || '', + description: contents.refunds.description || '', + content: contents.refunds.content || '', + meta_title: contents.refunds.meta_title || '', + meta_description: contents.refunds.meta_description || '', + is_active: contents.refunds.is_active ?? true, + }); + } + + // Cancellation + if (contents.cancellation) { + setCancellationData({ + title: contents.cancellation.title || '', + subtitle: contents.cancellation.subtitle || '', + description: contents.cancellation.description || '', + content: contents.cancellation.content || '', + meta_title: contents.cancellation.meta_title || '', + meta_description: contents.cancellation.meta_description || '', + is_active: contents.cancellation.is_active ?? true, + }); + } + + // Accessibility + if (contents.accessibility) { + setAccessibilityData({ + title: contents.accessibility.title || '', + subtitle: contents.accessibility.subtitle || '', + description: contents.accessibility.description || '', + content: contents.accessibility.content || '', + meta_title: contents.accessibility.meta_title || '', + meta_description: contents.accessibility.meta_description || '', + is_active: contents.accessibility.is_active ?? true, + }); + } + + // FAQ + if (contents.faq) { + setFaqData({ + title: contents.faq.title || '', + subtitle: contents.faq.subtitle || '', + description: contents.faq.description || '', + content: contents.faq.content || '', + meta_title: contents.faq.meta_title || '', + meta_description: contents.faq.meta_description || '', + is_active: contents.faq.is_active ?? true, + }); + } }; const handleSave = async (pageType: PageType, data: UpdatePageContentData) => { @@ -449,6 +548,12 @@ const PageContentDashboard: React.FC = () => { { id: 'home' as ContentTab, label: 'Home', icon: Home }, { id: 'contact' as ContentTab, label: 'Contact', icon: Mail }, { id: 'about' as ContentTab, label: 'About', icon: Info }, + { id: 'privacy' as ContentTab, label: 'Privacy', icon: Shield }, + { id: 'terms' as ContentTab, label: 'Terms', icon: FileText }, + { id: 'refunds' as ContentTab, label: 'Refunds', icon: RefreshCw }, + { id: 'cancellation' as ContentTab, label: 'Cancellation', icon: XCircle }, + { id: 'accessibility' as ContentTab, label: 'Accessibility', icon: Accessibility }, + { id: 'faq' as ContentTab, label: 'FAQ', icon: HelpCircle }, { id: 'footer' as ContentTab, label: 'Footer', icon: FileText }, { id: 'seo' as ContentTab, label: 'SEO', icon: Search }, ]; @@ -528,6 +633,12 @@ const PageContentDashboard: React.FC = () => { { id: 'home' as PageType, label: 'Home Page', icon: Home, color: 'blue', description: 'Manage hero section, featured content' }, { id: 'contact' as PageType, label: 'Contact Page', icon: Mail, color: 'green', description: 'Manage contact information and form' }, { id: 'about' as PageType, label: 'About Page', icon: Info, color: 'amber', description: 'Manage story, values, and features' }, + { id: 'privacy' as PageType, label: 'Privacy Policy', icon: Shield, color: 'red', description: 'Manage privacy policy content' }, + { id: 'terms' as PageType, label: 'Terms & Conditions', icon: FileText, color: 'teal', description: 'Manage terms and conditions' }, + { id: 'refunds' as PageType, label: 'Refunds Policy', icon: RefreshCw, color: 'orange', description: 'Manage refunds policy content' }, + { id: 'cancellation' as PageType, label: 'Cancellation Policy', icon: XCircle, color: 'pink', description: 'Manage cancellation policy content' }, + { id: 'accessibility' as PageType, label: 'Accessibility', icon: Accessibility, color: 'cyan', description: 'Manage accessibility information' }, + { id: 'faq' as PageType, label: 'FAQ', icon: HelpCircle, color: 'violet', description: 'Manage frequently asked questions' }, { id: 'footer' as PageType, label: 'Footer', icon: FileText, color: 'purple', description: 'Manage footer links and social media' }, { id: 'seo' as PageType, label: 'SEO Settings', icon: Search, color: 'indigo', description: 'Manage meta tags and SEO optimization' }, ].map((page) => { @@ -542,6 +653,9 @@ const PageContentDashboard: React.FC = () => { page.color === 'green' ? 'border-green-100/50 hover:border-green-300/60' : page.color === 'amber' ? 'border-amber-100/50 hover:border-amber-300/60' : page.color === 'purple' ? 'border-purple-100/50 hover:border-purple-300/60' : + page.color === 'red' ? 'border-red-100/50 hover:border-red-300/60' : + page.color === 'teal' ? 'border-teal-100/50 hover:border-teal-300/60' : + page.color === 'orange' ? 'border-orange-100/50 hover:border-orange-300/60' : 'border-indigo-100/50 hover:border-indigo-300/60' }`} > @@ -550,6 +664,9 @@ const PageContentDashboard: React.FC = () => { page.color === 'green' ? 'from-green-400' : page.color === 'amber' ? 'from-amber-400' : page.color === 'purple' ? 'from-purple-400' : + page.color === 'red' ? 'from-red-400' : + page.color === 'teal' ? 'from-teal-400' : + page.color === 'orange' ? 'from-orange-400' : 'from-indigo-400' }`}>
@@ -560,6 +677,9 @@ const PageContentDashboard: React.FC = () => { page.color === 'green' ? 'bg-gradient-to-br from-green-500 to-green-600 border-green-400/50' : page.color === 'amber' ? 'bg-gradient-to-br from-amber-500 to-amber-600 border-amber-400/50' : page.color === 'purple' ? 'bg-gradient-to-br from-purple-500 to-purple-600 border-purple-400/50' : + page.color === 'red' ? 'bg-gradient-to-br from-red-500 to-red-600 border-red-400/50' : + page.color === 'teal' ? 'bg-gradient-to-br from-teal-500 to-teal-600 border-teal-400/50' : + page.color === 'orange' ? 'bg-gradient-to-br from-orange-500 to-orange-600 border-orange-400/50' : 'bg-gradient-to-br from-indigo-500 to-indigo-600 border-indigo-400/50' }`}> @@ -568,6 +688,9 @@ const PageContentDashboard: React.FC = () => {

{page.label}

{
)} + {/* Privacy Tab */} + {activeTab === 'privacy' && ( +
+
+
+

Privacy Policy Content

+
+ Enable Page + +
+
+ +
+
+ + setPrivacyData({ ...privacyData, title: e.target.value })} + className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200" + placeholder="Privacy Policy" + /> +
+ +
+ + setPrivacyData({ ...privacyData, subtitle: e.target.value })} + className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200" + placeholder="Your privacy is important to us" + /> +
+ +
+ +