diff --git a/Backend/src/system/routes/__pycache__/system_settings_routes.cpython-312.pyc b/Backend/src/system/routes/__pycache__/system_settings_routes.cpython-312.pyc index 8bc55002..10f7bfa2 100644 Binary files a/Backend/src/system/routes/__pycache__/system_settings_routes.cpython-312.pyc and b/Backend/src/system/routes/__pycache__/system_settings_routes.cpython-312.pyc differ diff --git a/Backend/src/system/routes/system_settings_routes.py b/Backend/src/system/routes/system_settings_routes.py index a7c22c81..f9c5a1f2 100644 --- a/Backend/src/system/routes/system_settings_routes.py +++ b/Backend/src/system/routes/system_settings_routes.py @@ -1846,6 +1846,180 @@ async def upload_company_favicon( logger.error(f"Error uploading favicon: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e)) +class UpdateThemeSettingsRequest(BaseModel): + theme_primary_color: Optional[str] = None + theme_primary_light: Optional[str] = None + theme_primary_dark: Optional[str] = None + theme_primary_accent: Optional[str] = None + +@router.get("/theme") +async def get_theme_settings( + db: Session = Depends(get_db) +): + """Get current theme color settings""" + try: + setting_keys = [ + "theme_primary_color", + "theme_primary_light", + "theme_primary_dark", + "theme_primary_accent", + ] + + settings_dict = {} + for key in setting_keys: + setting = db.query(SystemSettings).filter( + SystemSettings.key == key + ).first() + if setting: + settings_dict[key] = setting.value + else: + settings_dict[key] = None + + # Get updated_at and updated_by from any theme setting + theme_setting = db.query(SystemSettings).filter( + SystemSettings.key == "theme_primary_color" + ).first() + + updated_at = None + updated_by = None + if theme_setting: + updated_at = theme_setting.updated_at.isoformat() if theme_setting.updated_at else None + updated_by = theme_setting.updated_by.full_name if theme_setting.updated_by else None + + return { + "status": "success", + "data": { + "theme_primary_color": settings_dict.get("theme_primary_color", "#d4af37"), + "theme_primary_light": settings_dict.get("theme_primary_light", "#f5d76e"), + "theme_primary_dark": settings_dict.get("theme_primary_dark", "#c9a227"), + "theme_primary_accent": settings_dict.get("theme_primary_accent", "#e8c547"), + "updated_at": updated_at, + "updated_by": updated_by, + } + } + except Exception as e: + logger.error(f"Error getting theme settings: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=str(e)) + +@router.put("/theme") +async def update_theme_settings( + request_data: UpdateThemeSettingsRequest, + request: Request, + current_user: User = Depends(authorize_roles("admin")), + db: Session = Depends(get_db) +): + """Update theme color settings (admin only)""" + import re + + client_ip = request.client.host if request.client else None + user_agent = request.headers.get('User-Agent') + request_id = getattr(request.state, 'request_id', None) + + try: + # Validate hex color format + hex_color_pattern = re.compile(r'^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$') + + db_settings = {} + + if request_data.theme_primary_color is not None: + if not hex_color_pattern.match(request_data.theme_primary_color): + raise HTTPException( + status_code=400, + detail="Invalid theme_primary_color format. Must be a valid hex color (e.g., #d4af37)" + ) + db_settings["theme_primary_color"] = request_data.theme_primary_color + + if request_data.theme_primary_light is not None: + if not hex_color_pattern.match(request_data.theme_primary_light): + raise HTTPException( + status_code=400, + detail="Invalid theme_primary_light format. Must be a valid hex color (e.g., #f5d76e)" + ) + db_settings["theme_primary_light"] = request_data.theme_primary_light + + if request_data.theme_primary_dark is not None: + if not hex_color_pattern.match(request_data.theme_primary_dark): + raise HTTPException( + status_code=400, + detail="Invalid theme_primary_dark format. Must be a valid hex color (e.g., #c9a227)" + ) + db_settings["theme_primary_dark"] = request_data.theme_primary_dark + + if request_data.theme_primary_accent is not None: + if not hex_color_pattern.match(request_data.theme_primary_accent): + raise HTTPException( + status_code=400, + detail="Invalid theme_primary_accent format. Must be a valid hex color (e.g., #e8c547)" + ) + db_settings["theme_primary_accent"] = request_data.theme_primary_accent + + # Update or create settings + for key, value in db_settings.items(): + setting = db.query(SystemSettings).filter( + SystemSettings.key == key + ).first() + + if setting: + setting.value = value + setting.updated_at = datetime.utcnow() + setting.updated_by_id = current_user.id + else: + setting = SystemSettings( + key=key, + value=value, + updated_by_id=current_user.id + ) + db.add(setting) + + db.commit() + + # Get updated settings + updated_settings = {} + for key in ["theme_primary_color", "theme_primary_light", "theme_primary_dark", "theme_primary_accent"]: + setting = db.query(SystemSettings).filter( + SystemSettings.key == key + ).first() + if setting: + updated_settings[key] = setting.value + else: + # Return defaults if not set + defaults = { + "theme_primary_color": "#d4af37", + "theme_primary_light": "#f5d76e", + "theme_primary_dark": "#c9a227", + "theme_primary_accent": "#e8c547", + } + updated_settings[key] = defaults.get(key) + + theme_setting = db.query(SystemSettings).filter( + SystemSettings.key == "theme_primary_color" + ).first() + + updated_at = None + updated_by = None + if theme_setting: + updated_at = theme_setting.updated_at.isoformat() if theme_setting.updated_at else None + updated_by = theme_setting.updated_by.full_name if theme_setting.updated_by else None + + return { + "status": "success", + "message": "Theme settings updated successfully", + "data": { + "theme_primary_color": updated_settings.get("theme_primary_color", "#d4af37"), + "theme_primary_light": updated_settings.get("theme_primary_light", "#f5d76e"), + "theme_primary_dark": updated_settings.get("theme_primary_dark", "#c9a227"), + "theme_primary_accent": updated_settings.get("theme_primary_accent", "#e8c547"), + "updated_at": updated_at, + "updated_by": updated_by, + } + } + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error updating theme settings: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=str(e)) + @router.get("/recaptcha") async def get_recaptcha_settings( db: Session = Depends(get_db) diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx index d4b11d73..399678ff 100644 --- a/Frontend/src/App.tsx +++ b/Frontend/src/App.tsx @@ -12,6 +12,7 @@ import { LoadingProvider, useNavigationLoading, useLoading } from './shared/cont import { CookieConsentProvider } from './shared/contexts/CookieConsentContext'; import { CurrencyProvider } from './features/payments/contexts/CurrencyContext'; import { CompanySettingsProvider } from './shared/contexts/CompanySettingsContext'; +import { ThemeProvider } from './shared/contexts/ThemeContext'; import { AuthModalProvider } from './features/auth/contexts/AuthModalContext'; import { AntibotProvider } from './features/auth/contexts/AntibotContext'; import { RoomProvider } from './features/rooms/contexts/RoomContext'; @@ -91,7 +92,7 @@ const GroupBookingManagementPage = lazy(() => import('./pages/admin/GroupBooking const AdminBookingManagementPage = lazy(() => import('./pages/admin/BookingManagementPage')); const PageContentDashboardPage = lazy(() => import('./pages/admin/PageContentDashboard')); const AnalyticsDashboardPage = lazy(() => import('./pages/admin/AnalyticsDashboardPage')); -const BusinessDashboardPage = lazy(() => import('./pages/admin/BusinessDashboardPage')); +const PromotionsManagementPage = lazy(() => import('./pages/admin/PromotionsManagementPage')); const SettingsPage = lazy(() => import('./pages/admin/SettingsPage')); const TaskManagementPage = lazy(() => import('./pages/admin/TaskManagementPage')); const WorkflowManagementPage = lazy(() => import('./pages/admin/WorkflowManagementPage')); @@ -249,7 +250,8 @@ function App() { - + + @@ -600,8 +602,8 @@ function App() { element={} /> } + path="promotions" + element={} /> + diff --git a/Frontend/src/features/auth/components/ForgotPasswordModal.tsx b/Frontend/src/features/auth/components/ForgotPasswordModal.tsx index a525ad37..a7515d56 100644 --- a/Frontend/src/features/auth/components/ForgotPasswordModal.tsx +++ b/Frontend/src/features/auth/components/ForgotPasswordModal.tsx @@ -93,7 +93,7 @@ const ForgotPasswordModal: React.FC = () => {
{/* Modal */} -
+
{/* Close button */}
@@ -172,7 +172,7 @@ const ForgotPasswordModal: React.FC = () => { setIsSuccess(false); clearError(); }} - className="w-full flex items-center justify-center py-2.5 sm:py-3 px-4 border border-gray-300 rounded-lg text-xs sm:text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#d4af37] transition-colors" + className="w-full flex items-center justify-center py-2.5 sm:py-3 px-4 border border-gray-300 rounded-lg text-xs sm:text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[var(--luxury-gold)] transition-colors" > Resend Email @@ -180,7 +180,7 @@ const ForgotPasswordModal: React.FC = () => { @@ -286,7 +286,7 @@ const ForgotPasswordModal: React.FC = () => { If you're having trouble resetting your password, please contact our support team via email{' '} {supportEmail} @@ -295,7 +295,7 @@ const ForgotPasswordModal: React.FC = () => { {' '}or hotline{' '} {supportPhone} diff --git a/Frontend/src/features/auth/components/LoginModal.tsx b/Frontend/src/features/auth/components/LoginModal.tsx index 6a0f20cf..9dd80fd4 100644 --- a/Frontend/src/features/auth/components/LoginModal.tsx +++ b/Frontend/src/features/auth/components/LoginModal.tsx @@ -376,7 +376,7 @@ const LoginModal: React.FC = () => {
{/* Modal */} -
+
{/* Close button */} @@ -633,7 +633,7 @@ const LoginModal: React.FC = () => { Don't have an account?{' '} diff --git a/Frontend/src/features/auth/components/RegisterModal.tsx b/Frontend/src/features/auth/components/RegisterModal.tsx index e4f841e9..a0466901 100644 --- a/Frontend/src/features/auth/components/RegisterModal.tsx +++ b/Frontend/src/features/auth/components/RegisterModal.tsx @@ -15,11 +15,11 @@ import HoneypotField from '../../../shared/components/HoneypotField'; const PasswordRequirement: React.FC<{ met: boolean; text: string }> = ({ met, text }) => (
{met ? ( - + ) : ( )} - + {text}
@@ -165,7 +165,7 @@ const RegisterModal: React.FC = () => {
{/* Modal */} -
+
{/* Close button */} diff --git a/Frontend/src/features/auth/components/ResetPasswordModal.tsx b/Frontend/src/features/auth/components/ResetPasswordModal.tsx index 10227440..471e2f74 100644 --- a/Frontend/src/features/auth/components/ResetPasswordModal.tsx +++ b/Frontend/src/features/auth/components/ResetPasswordModal.tsx @@ -132,7 +132,7 @@ const ResetPasswordModal: React.FC = ({ token }) => {
{/* Modal */} -
+
{/* Close button */} {!isSuccess && (
@@ -212,7 +212,7 @@ const ResetPasswordModal: React.FC = ({ token }) => { closeModal(); openModal('login'); }} - className="inline-flex items-center justify-center w-full py-2.5 sm:py-3 px-4 border border-transparent rounded-lg text-xs sm:text-sm font-medium text-white bg-gradient-to-r from-[#d4af37] to-[#c9a227] hover:from-[#f5d76e] hover:to-[#d4af37] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#d4af37] transition-colors" + className="inline-flex items-center justify-center w-full py-2.5 sm:py-3 px-4 border border-transparent rounded-lg text-xs sm:text-sm font-medium text-white bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] hover:from-[var(--luxury-gold-light)] hover:to-[var(--luxury-gold)] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[var(--luxury-gold)] transition-colors" > Login Now @@ -267,7 +267,7 @@ const ResetPasswordModal: React.FC = ({ token }) => { className={`block w-full pl-9 sm:pl-10 pr-9 sm:pr-10 py-2.5 sm:py-3 border rounded-lg focus:outline-none focus:ring-2 transition-colors text-sm sm:text-base luxury-input ${ errors.password ? 'border-red-300 focus:ring-red-500' - : 'border-gray-300 focus:ring-[#d4af37]' + : 'border-gray-300 focus:ring-[var(--luxury-gold)]' }`} placeholder="••••••••" /> @@ -330,7 +330,7 @@ const ResetPasswordModal: React.FC = ({ token }) => { className={`block w-full pl-9 sm:pl-10 pr-9 sm:pr-10 py-2.5 sm:py-3 border rounded-lg focus:outline-none focus:ring-2 transition-colors text-sm sm:text-base luxury-input ${ errors.confirmPassword ? 'border-red-300 focus:ring-red-500' - : 'border-gray-300 focus:ring-[#d4af37]' + : 'border-gray-300 focus:ring-[var(--luxury-gold)]' }`} placeholder="••••••••" /> @@ -356,7 +356,7 @@ const ResetPasswordModal: React.FC = ({ token }) => { diff --git a/Frontend/src/features/auth/pages/HousekeepingLoginPage.tsx b/Frontend/src/features/auth/pages/HousekeepingLoginPage.tsx index 5605b5fa..9b56abe9 100644 --- a/Frontend/src/features/auth/pages/HousekeepingLoginPage.tsx +++ b/Frontend/src/features/auth/pages/HousekeepingLoginPage.tsx @@ -345,7 +345,7 @@ const HousekeepingLoginPage: React.FC = () => {
-
+
{settings.company_logo_url ? ( { autoComplete="one-time-code" maxLength={8} className={`w-full pl-10 pr-4 py-3 border rounded-lg text-center tracking-widest ${ - mfaErrors.mfaToken ? 'border-red-300 focus:ring-red-500' : 'border-gray-300 focus:ring-[#d4af37]' + mfaErrors.mfaToken ? 'border-red-300 focus:ring-red-500' : 'border-gray-300 focus:ring-[var(--luxury-gold)]' }`} placeholder="000000" /> @@ -402,7 +402,7 @@ const HousekeepingLoginPage: React.FC = () => { @@ -73,7 +73,7 @@ const InvoiceInfoModal: React.FC = ({
@@ -85,7 +85,7 @@ const InvoiceInfoModal: React.FC = ({