This commit is contained in:
Iliyan Angelov
2025-12-12 16:22:41 +02:00
parent 7acf05e186
commit 4cbcdde369
129 changed files with 785 additions and 402 deletions

View File

@@ -43,7 +43,7 @@ const CancellationPolicyPage: React.FC = () => {
allElements.forEach((el) => {
const htmlEl = el as HTMLElement;
const tagName = htmlEl.tagName.toLowerCase();
const _currentColor = htmlEl.style.color;
// const _currentColor = htmlEl.style.color; // Unused variable
// Override inline colors to use theme-aware colors
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {

View File

@@ -19,7 +19,7 @@ import { getThemeBackgroundClasses, getThemeHeroBackgroundClasses, getThemeTextC
const getIconComponent = (iconName?: string, fallback: React.ComponentType<{ className?: string }> = Mail) => {
if (!iconName) return fallback;
const icons = LucideIcons as Record<string, React.ComponentType<{ className?: string }> | undefined>;
const icons = LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }> | undefined>;
// Try direct match first (for PascalCase names)
if (icons[iconName]) {

View File

@@ -8,6 +8,7 @@ import { useTheme } from '../../../shared/contexts/ThemeContext';
import Loading from '../../../shared/components/Loading';
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
import { isAxiosError } from 'axios';
const PrivacyPolicyPage: React.FC = () => {
const { settings } = useCompanySettings();
@@ -80,7 +81,7 @@ const PrivacyPolicyPage: React.FC = () => {
} catch (err: unknown) {
console.error('Error fetching page content:', err);
// If page is disabled (404), set pageContent to null to show disabled message
if (getUserFriendlyError(err) === 404) {
if (isAxiosError(err) && err.response?.status === 404) {
setPageContent(null);
}
} finally {

View File

@@ -8,6 +8,7 @@ import { useTheme } from '../../../shared/contexts/ThemeContext';
import Loading from '../../../shared/components/Loading';
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
import { isAxiosError } from 'axios';
const RefundsPolicyPage: React.FC = () => {
const { settings } = useCompanySettings();
@@ -80,7 +81,7 @@ const RefundsPolicyPage: React.FC = () => {
} catch (err: unknown) {
console.error('Error fetching page content:', err);
// If page is disabled (404), set pageContent to null to show disabled message
if (getUserFriendlyError(err) === 404) {
if (isAxiosError(err) && err.response?.status === 404) {
setPageContent(null);
}
} finally {

View File

@@ -3,14 +3,14 @@ import { useParams, Link, useNavigate } from 'react-router-dom';
import { ArrowLeft, Share2, Tag, Star } from 'lucide-react';
import * as LucideIcons from 'lucide-react';
import pageContentService, { PageContent } from '../services/pageContentService';
import serviceService from '../../hotel_services/services/serviceService';
import serviceService, { Service } from '../../hotel_services/services/serviceService';
import Loading from '../../../shared/components/Loading';
import { createSanitizedHtml } from '../../../shared/utils/htmlSanitizer';
import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency';
import { useTheme } from '../../../shared/contexts/ThemeContext';
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
interface ServiceSection {
export interface ServiceSection {
type: 'hero' | 'text' | 'image' | 'gallery' | 'quote' | 'features' | 'cta' | 'video';
title?: string;
content?: string;
@@ -60,6 +60,14 @@ const ServiceDetailPage: React.FC = () => {
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
// Helper function to get icon component from icon name
const getIconComponent = (iconName?: string, fallback: React.ComponentType<{ className?: string }> = Star) => {
if (!iconName) return fallback;
const icons = LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }> | undefined>;
const IconComponent = icons[iconName] || fallback;
return IconComponent;
};
useEffect(() => {
if (slug) {
fetchService();
@@ -216,7 +224,7 @@ const ServiceDetailPage: React.FC = () => {
});
if (servicesResponse.success && servicesResponse.data?.services) {
servicesResponse.data.services.forEach((service: { slug?: string; category?: string; id?: number | string }) => {
servicesResponse.data.services.forEach((service: Service) => {
// Skip current service
if (currentService.type === 'hotel' && service.id === currentService.id) {
return;
@@ -229,7 +237,7 @@ const ServiceDetailPage: React.FC = () => {
const serviceSlug = service.slug || service.name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
services.push({
id: service.id,
id: service.id ?? `service-${Date.now()}`,
title: service.name,
slug: serviceSlug,
description: service.description,
@@ -243,17 +251,17 @@ const ServiceDetailPage: React.FC = () => {
// Add luxury services as fallback
if (pageContent?.luxury_services && Array.isArray(pageContent.luxury_services)) {
pageContent.luxury_services.forEach((s: { slug?: string; category?: string }, index: number) => {
pageContent.luxury_services.forEach((s: { slug?: string; category?: string; title?: string; name?: string; description?: string; image?: string; icon?: string }, index: number) => {
if (s.slug && s.slug !== currentService.slug) {
if (!currentService.category || s.category === currentService.category) {
// Check if already in services (by slug)
const serviceSlug = s.slug || s.title?.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
const serviceSlug = s.slug || (s.title || s.name || 'service').toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
const exists = services.some(serv => serv.slug === serviceSlug);
if (!exists) {
services.push({
id: `luxury-${index}`,
title: s.title || 'Service',
slug: s.slug,
title: s.title || s.name || 'Service',
slug: s.slug || serviceSlug,
description: s.description,
image: s.image,
icon: s.icon,
@@ -412,10 +420,10 @@ const ServiceDetailPage: React.FC = () => {
)}
<div className="flex items-start gap-6 mb-6">
{service.icon && (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[service.icon] && (
{service.icon && (
<div className="relative flex-shrink-0">
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 rounded-full blur-3xl"></div>
{React.createElement((LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[service.icon], {
{React.createElement(getIconComponent(service.icon), {
className: 'w-20 h-20 sm:w-24 sm:h-24 text-[var(--luxury-gold)] relative z-10 drop-shadow-2xl'
})}
</div>
@@ -604,9 +612,7 @@ const ServiceDetailPage: React.FC = () => {
)}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{section.features.map((feature, featIndex) => {
const IconComponent = feature.icon && (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[feature.icon]
? (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[feature.icon]
: null;
const IconComponent = feature.icon ? getIconComponent(feature.icon) : null;
return (
<div key={featIndex} className="group relative bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a] rounded-3xl border-2 border-[var(--luxury-gold)]/20 p-8 shadow-2xl hover:border-[var(--luxury-gold)]/60 hover:shadow-[var(--luxury-gold)]/20 transition-all duration-500 hover:-translate-y-2">
<div className="absolute inset-0 bg-gradient-to-br from-[var(--luxury-gold)]/0 to-[var(--luxury-gold)]/0 group-hover:from-[var(--luxury-gold)]/5 group-hover:to-transparent rounded-3xl transition-all duration-500"></div>

View File

@@ -13,15 +13,17 @@ import { getThemeBackgroundClasses, getThemeHeroBackgroundClasses, getThemeTextC
const getIconComponent = (iconName?: string, fallback: React.ComponentType<{ className?: string }> = Award) => {
if (!iconName) return fallback;
const icons = LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }> | undefined>;
// Try direct match first (for PascalCase names)
if ((LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[iconName]) {
return (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[iconName];
if (icons[iconName]) {
return icons[iconName];
}
// Convert to PascalCase (capitalize first letter)
const pascalCaseName = iconName.charAt(0).toUpperCase() + iconName.slice(1).toLowerCase();
if ((LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[pascalCaseName]) {
return (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[pascalCaseName];
if (icons[pascalCaseName]) {
return icons[pascalCaseName];
}
return fallback;
@@ -67,7 +69,7 @@ const ServicesPage: React.FC = () => {
// Extract categories from luxury services
const categories = new Set<string>();
if (Array.isArray(content.luxury_services)) {
content.luxury_services.forEach((service: { icon?: string; name?: string; description?: string }) => {
content.luxury_services.forEach((service: { icon?: string; name?: string; description?: string; category?: string; title?: string }) => {
if (service.category) {
categories.add(service.category);
}
@@ -133,9 +135,9 @@ const ServicesPage: React.FC = () => {
// Add luxury services from page content (only if not already in hotel services)
if (pageContent?.luxury_services && Array.isArray(pageContent.luxury_services)) {
pageContent.luxury_services.forEach((service: { icon?: string; name?: string; description?: string }, index: number) => {
pageContent.luxury_services.forEach((service: { icon?: string; name?: string; description?: string; category?: string; title?: string; slug?: string; image?: string }, index: number) => {
// Check if this service already exists in hotel services by slug
const existingSlug = service.slug || service.title?.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
const existingSlug = service.slug || (service.title || service.name || 'service').toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
const existsInHotel = hotelServices.some((hs: Service) => {
const hotelSlug = hs.slug || hs.name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
return hotelSlug === existingSlug;
@@ -145,13 +147,13 @@ const ServicesPage: React.FC = () => {
if (!existsInHotel) {
services.push({
id: `luxury-${index}`,
title: service.title || 'Service',
title: service.title || service.name || 'Service',
description: service.description || '',
image: service.image,
icon: service.icon,
category: service.category,
type: 'luxury',
slug: service.slug,
slug: service.slug || existingSlug,
});
}
});
@@ -351,10 +353,10 @@ const ServicesPage: React.FC = () => {
</div>
) : (
<div className={`h-48 sm:h-56 ${cardClasses} flex items-center justify-center p-8`}>
{service.icon && (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[service.icon] ? (
{service.icon ? (
<div className="relative">
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 rounded-full blur-2xl"></div>
{React.createElement((LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[service.icon], {
{React.createElement(getIconComponent(service.icon), {
className: 'w-16 h-16 sm:w-20 sm:h-20 text-[var(--luxury-gold)] relative z-10 drop-shadow-lg'
})}
</div>

View File

@@ -8,6 +8,7 @@ import { useTheme } from '../../../shared/contexts/ThemeContext';
import Loading from '../../../shared/components/Loading';
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
import { isAxiosError } from 'axios';
const TermsPage: React.FC = () => {
const { settings } = useCompanySettings();
@@ -80,7 +81,7 @@ const TermsPage: React.FC = () => {
} catch (err: unknown) {
console.error('Error fetching page content:', err);
// If page is disabled (404), set pageContent to null to show disabled message
if (getUserFriendlyError(err) === 404) {
if (isAxiosError(err) && err.response?.status === 404) {
setPageContent(null);
}
} finally {

View File

@@ -9,6 +9,7 @@ import { toast } from 'react-toastify';
import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
interface CreateBookingModalProps {
isOpen: boolean;
@@ -170,7 +171,7 @@ const CreateBookingModal: React.FC<CreateBookingModalProps> = ({
handleClose();
onSuccess();
} catch (error: unknown) {
toast.error(getUserFriendlyError(error) || getUserFriendlyError(error) || 'Failed to create booking');
toast.error(getUserFriendlyError(error) || 'Failed to create booking');
} finally {
setLoading(false);
}

View File

@@ -5,6 +5,7 @@ import roomService, { Room } from '../../rooms/services/roomService';
import { toast } from 'react-toastify';
import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency';
import DatePicker from 'react-datepicker';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
import 'react-datepicker/dist/react-datepicker.css';
interface RoomBlock {

View File

@@ -18,6 +18,7 @@ import roomService, { Room } from '../../rooms/services/roomService';
import userService, { User as UserType } from '../../auth/services/userService';
import useAuthStore from '../../../store/useAuthStore';
import DatePicker from 'react-datepicker';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
import 'react-datepicker/dist/react-datepicker.css';
const HousekeepingManagement: React.FC = () => {
@@ -82,7 +83,15 @@ const HousekeepingManagement: React.FC = () => {
const fetchTasks = async () => {
try {
setLoading(true);
const params: { page: number; limit: number; room_id?: number; status?: string } = {
const params: {
page: number;
limit: number;
room_id?: number;
status?: string;
task_type?: string;
date?: string;
include_cleaning_rooms?: boolean;
} = {
page: currentPage,
limit: 10,
include_cleaning_rooms: true // Include rooms in cleaning status
@@ -173,7 +182,7 @@ const HousekeepingManagement: React.FC = () => {
const items = defaultChecklistItems[type] || [];
setFormData({
...formData,
task_type: type as 'cleaning' | 'inspection' | 'maintenance',
task_type: type as 'checkout' | 'stayover' | 'vacant' | 'inspection' | 'turndown',
checklist_items: items.map(item => ({ item, completed: false, notes: '' })),
});
};

View File

@@ -22,6 +22,7 @@ import roomService, { Room } from '../../rooms/services/roomService';
import userService, { User as UserType } from '../../auth/services/userService';
import useAuthStore from '../../../store/useAuthStore';
import DatePicker from 'react-datepicker';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
import 'react-datepicker/dist/react-datepicker.css';
const InspectionManagement: React.FC = () => {
@@ -88,7 +89,7 @@ const InspectionManagement: React.FC = () => {
const fetchInspections = async () => {
try {
setLoading(true);
const params: { page: number; limit: number; room_id?: number; inspection_type?: string } = { page: currentPage, limit: 10 };
const params: { page: number; limit: number; room_id?: number; inspection_type?: string; status?: string } = { page: currentPage, limit: 10 };
if (filters.room_id) params.room_id = parseInt(filters.room_id);
if (filters.inspection_type) params.inspection_type = filters.inspection_type;
if (filters.status) params.status = filters.status;
@@ -480,7 +481,7 @@ const InspectionManagement: React.FC = () => {
<select
required
value={formData.inspection_type}
onChange={(e) => setFormData({ ...formData, inspection_type: e.target.value as 'routine' | 'deep' | 'pre_checkin' | 'post_checkout' | 'maintenance' })}
onChange={(e) => setFormData({ ...formData, inspection_type: e.target.value as 'pre_checkin' | 'post_checkout' | 'routine' | 'maintenance' | 'damage' })}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="pre_checkin">Pre Check-in</option>

View File

@@ -15,6 +15,7 @@ import roomService, { Room } from '../../rooms/services/roomService';
import userService, { User as UserType } from '../../auth/services/userService';
import useAuthStore from '../../../store/useAuthStore';
import DatePicker from 'react-datepicker';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
import 'react-datepicker/dist/react-datepicker.css';
const MaintenanceManagement: React.FC = () => {
@@ -61,7 +62,7 @@ const MaintenanceManagement: React.FC = () => {
const fetchMaintenanceRecords = async () => {
try {
setLoading(true);
const params: { page: number; limit: number; room_id?: number; status?: string } = { page: currentPage, limit: 10 };
const params: { page: number; limit: number; room_id?: number; status?: string; maintenance_type?: string } = { page: currentPage, limit: 10 };
if (filters.room_id) params.room_id = parseInt(filters.room_id);
if (filters.status) params.status = filters.status;
if (filters.maintenance_type) params.maintenance_type = filters.maintenance_type;
@@ -417,7 +418,7 @@ const MaintenanceManagement: React.FC = () => {
<select
required
value={formData.maintenance_type}
onChange={(e) => setFormData({ ...formData, maintenance_type: e.target.value as 'repair' | 'cleaning' | 'inspection' | 'upgrade' | 'other' })}
onChange={(e) => setFormData({ ...formData, maintenance_type: e.target.value as 'preventive' | 'corrective' | 'emergency' | 'upgrade' | 'inspection' })}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="preventive">Preventive</option>

View File

@@ -153,7 +153,7 @@ const loyaltyService = {
limit: number = 20,
transactionType?: string
): Promise<PointsHistoryResponse> => {
const params: { page: number; limit: number } = { page, limit };
const params: { page: number; limit: number; transaction_type?: string } = { page, limit };
if (transactionType) {
params.transaction_type = transactionType;
}
@@ -232,7 +232,7 @@ const loyaltyService = {
};
};
}> => {
const params: { page: number; limit: number } = { page, limit };
const params: { page: number; limit: number; search?: string; tier_id?: number } = { page, limit };
if (search) params.search = search;
if (tierId) params.tier_id = tierId;
const response = await apiClient.get('/api/loyalty/admin/users', { params });

View File

@@ -7,6 +7,7 @@ import { useCompanySettings } from '../../../shared/contexts/CompanySettingsCont
import { toast } from 'react-toastify';
import ConfirmationDialog from '../../../shared/components/ConfirmationDialog';
import { formatWorkingHours } from '../../../shared/utils/format';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
interface ChatWidgetProps {
onClose?: () => void;

View File

@@ -4,6 +4,7 @@ import { toast } from 'react-toastify';
import notificationService, { Notification } from '../services/notificationService';
import { formatDate } from '../../../shared/utils/format';
import useAuthStore from '../../../store/useAuthStore';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
const InAppNotificationBell: React.FC = () => {
const { isAuthenticated, token, isLoading } = useAuthStore();
@@ -108,7 +109,7 @@ const InAppNotificationBell: React.FC = () => {
try {
await notificationService.markAsRead(notificationId);
setNotifications(notifications.map(n =>
n.id === notificationId ? { ...n, status: 'read' as 'unread' | 'read', read_at: new Date().toISOString() } : n
n.id === notificationId ? { ...n, status: 'read' as Notification['status'], read_at: new Date().toISOString() } : n
));
setUnreadCount(Math.max(0, unreadCount - 1));
} catch (error: unknown) {
@@ -121,7 +122,7 @@ const InAppNotificationBell: React.FC = () => {
setLoading(true);
const unread = notifications.filter(n => !n.read_at);
await Promise.all(unread.map(n => notificationService.markAsRead(n.id)));
setNotifications(notifications.map(n => ({ ...n, status: 'read' as 'read' | 'unread', read_at: new Date().toISOString() })));
setNotifications(notifications.map(n => ({ ...n, status: 'read' as Notification['status'], read_at: new Date().toISOString() })));
setUnreadCount(0);
} catch (error: unknown) {
toast.error(getUserFriendlyError(error) || 'Failed to mark all as read');

View File

@@ -3,6 +3,7 @@ import { Bell, Mail, MessageSquare, Smartphone, Save } from 'lucide-react';
import { toast } from 'react-toastify';
import Loading from '../../../shared/components/Loading';
import notificationService, { NotificationPreferences as NotificationPreferencesType } from '../services/notificationService';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
const NotificationPreferences: React.FC = () => {
const [preferences, setPreferences] = useState<NotificationPreferencesType | null>(null);

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { X, Plus } from 'lucide-react';
import { toast } from 'react-toastify';
import notificationService, { NotificationTemplate } from '../services/notificationService';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
interface NotificationTemplatesModalProps {
onClose: () => void;

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { X } from 'lucide-react';
import { toast } from 'react-toastify';
import notificationService from '../services/notificationService';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
interface SendNotificationModalProps {
onClose: () => void;

View File

@@ -3,6 +3,7 @@ import { createBoricaPayment } from '../services/paymentService';
import { X, Loader2, AlertCircle, CreditCard } from 'lucide-react';
import { toast } from 'react-toastify';
import { useFormatCurrency } from '../hooks/useFormatCurrency';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
interface BoricaPaymentModalProps {
isOpen: boolean;
@@ -75,7 +76,7 @@ const BoricaPaymentModal: React.FC<BoricaPaymentModalProps> = ({
// Create a form and submit it to Borica gateway
const form = document.createElement('form');
form.method = 'POST';
form.action = paymentRequest.gateway_url;
form.action = (paymentRequest.gateway_url as string) || '';
form.style.display = 'none';
// Add all form fields
@@ -172,7 +173,7 @@ const BoricaPaymentModal: React.FC<BoricaPaymentModalProps> = ({
<div className="bg-[#1a1a1a]/50 border border-[var(--luxury-gold)]/20 rounded-lg p-3 sm:p-4 space-y-2 sm:space-y-3">
<div className="flex justify-between items-center text-xs sm:text-sm">
<span className="text-gray-400">Order ID:</span>
<span className="text-white font-mono">{paymentRequest.order_id}</span>
<span className="text-white font-mono">{String(paymentRequest.order_id || '')}</span>
</div>
<div className="flex justify-between items-center text-xs sm:text-sm">
<span className="text-gray-400">Amount:</span>

View File

@@ -17,6 +17,7 @@ import {
import { useFormatCurrency } from '../hooks/useFormatCurrency';
import StripePaymentModal from './StripePaymentModal';
import PayPalPaymentModal from './PayPalPaymentModal';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
interface DepositPaymentModalProps {
isOpen: boolean;

View File

@@ -6,6 +6,7 @@ import {
} from '@stripe/react-stripe-js';
import { CreditCard, Loader2, AlertCircle, CheckCircle } from 'lucide-react';
import { toast } from 'react-toastify';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
interface StripePaymentFormProps {
clientSecret: string;

View File

@@ -1,10 +1,11 @@
import React, { useState, useEffect } from 'react';
import { loadStripe, StripeElementsOptions } from '@stripe/stripe-js';
import { loadStripe, StripeElementsOptions, Stripe } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js';
import StripePaymentForm from './StripePaymentForm';
import { createStripePaymentIntent, confirmStripePayment } from '../services/paymentService';
import { X, Loader2, AlertCircle } from 'lucide-react';
import { toast } from 'react-toastify';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
interface StripePaymentModalProps {
isOpen: boolean;
@@ -23,7 +24,7 @@ const StripePaymentModal: React.FC<StripePaymentModalProps> = ({
onSuccess,
onClose,
}) => {
const [stripePromise, setStripePromise] = useState<Promise<unknown> | null>(null);
const [stripePromise, setStripePromise] = useState<Promise<Stripe | null> | null>(null);
const [clientSecret, setClientSecret] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { loadStripe, StripeElementsOptions } from '@stripe/stripe-js';
import { loadStripe, StripeElementsOptions, Stripe } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js';
import StripePaymentForm from './StripePaymentForm';
import { createStripePaymentIntent, confirmStripePayment } from '../services/paymentService';
@@ -20,7 +20,7 @@ const StripePaymentWrapper: React.FC<StripePaymentWrapperProps> = ({
onSuccess,
onError,
}) => {
const [stripePromise, setStripePromise] = useState<Promise<unknown> | null>(null);
const [stripePromise, setStripePromise] = useState<Promise<Stripe | null> | null>(null);
const [clientSecret, setClientSecret] = useState<string | null>(null);
const [, setPublishableKey] = useState<string | null>(null);
const [loading, setLoading] = useState(true);

View File

@@ -61,7 +61,7 @@ export const createPayment = async (
// Handle both 'status: success' and 'success: true' formats
return {
success: data.status === 'success' || data.success === true,
data: data.data || {},
data: (data.data as { payment: Payment }) || { payment: {} as Payment },
message: data.message,
};
};
@@ -76,7 +76,7 @@ export const getPaymentByBookingId = async (
// Handle both 'status: success' and 'success: true' formats
return {
success: data.status === 'success' || data.success === true,
data: data.data || {},
data: (data.data as { payment: Payment }) || { payment: {} as Payment },
message: data.message,
};
};
@@ -125,7 +125,7 @@ export const getBankTransferInfo = async (
// Handle both 'status: success' and 'success: true' formats
return {
success: data.status === 'success' || data.success === true,
data: data.data || {},
data: (data.data as { payment: Payment; bank_info: BankInfo }) || { payment: {} as Payment, bank_info: {} as BankInfo },
message: data.message,
};
};
@@ -149,7 +149,7 @@ export const confirmDepositPayment = async (
// Handle both 'status: success' and 'success: true' formats
return {
success: data.status === 'success' || data.success === true,
data: data.data || {},
data: (data.data as { payment: Payment; booking: Booking }) || { payment: {} as Payment, booking: {} as Booking },
message: data.message,
};
};

View File

@@ -17,6 +17,7 @@ import { useAntibotForm } from '../../auth/hooks/useAntibotForm';
import HoneypotField from '../../../shared/components/HoneypotField';
import { useTheme } from '../../../shared/contexts/ThemeContext';
import { getThemeTextClasses, getThemeCardClasses, getThemeInputClasses } from '../../../shared/utils/themeUtils';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
interface ReviewSectionProps {
roomId: number;

View File

@@ -3,6 +3,8 @@ import roomService, { Room } from '../services/roomService';
import advancedRoomService, { RoomStatusBoardItem } from '../services/advancedRoomService';
import { toast } from 'react-toastify';
import { logger } from '../../../shared/utils/logger';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
import { isAxiosError } from 'axios';
interface RoomContextType {
// Room list state
@@ -175,7 +177,7 @@ export const RoomProvider: React.FC<RoomProviderProps> = ({ children }) => {
}
// Handle 401 Unauthorized gracefully - user may not have admin/staff role
if (getUserFriendlyError(error) === 401) {
if (isAxiosError(error) && error.response?.status === 401) {
setStatusBoardError(null); // Don't set error for unauthorized access
setStatusBoardRooms([]); // Clear status board if unauthorized
return; // Silently return without logging

View File

@@ -5,7 +5,7 @@ export interface Room {
room_type_id: number;
room_number: string;
floor: number;
status: 'available' | 'occupied' | 'maintenance';
status: 'available' | 'occupied' | 'maintenance' | 'cleaning' | 'reserved';
featured: boolean;
price?: number;
description?: string;
@@ -295,7 +295,7 @@ export interface CreateRoomData {
room_number: string;
floor: number;
room_type_id: number;
status: 'available' | 'occupied' | 'maintenance';
status: 'available' | 'occupied' | 'maintenance' | 'cleaning' | 'reserved';
featured?: boolean;
price?: number;
description?: string;

View File

@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { X } from 'lucide-react';
import { toast } from 'react-toastify';
import taskService from '../services/taskService';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
interface CreateTaskModalProps {
onClose: () => void;

View File

@@ -91,7 +91,7 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
continue;
}
const iconComponent = (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[iconName];
const iconComponent = (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }>>)[iconName];
// Only include if it's a function (React component)
if (typeof iconComponent === 'function') {
@@ -127,7 +127,7 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
);
}, [searchQuery, allIcons]);
const selectedIcon = normalizedValue && (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[normalizedValue] ? (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[normalizedValue] : null;
const selectedIcon = normalizedValue && (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }>>)[normalizedValue] ? (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }>>)[normalizedValue] : null;
const handleIconSelect = (iconName: string) => {
onChange(iconName);
@@ -202,7 +202,7 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
)}
<div className="grid grid-cols-6 sm:grid-cols-8 md:grid-cols-10 gap-2">
{filteredIcons.slice(0, searchQuery.trim() ? 500 : 300).map((iconName) => {
const IconComponent = (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[iconName];
const IconComponent = (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }>>)[iconName];
if (!IconComponent) return null;
const isSelected = normalizedValue === iconName;

View File

@@ -4,6 +4,7 @@ import { toast } from 'react-toastify';
import { Task } from '../services/taskService';
import taskService from '../services/taskService';
import { formatDate } from '../../../shared/utils/format';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
interface TaskDetailModalProps {
task: Task;

View File

@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { X, Plus, Trash2, GripVertical, Save } from 'lucide-react';
import { toast } from 'react-toastify';
import workflowService, { Workflow, WorkflowStep } from '../services/workflowService';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
interface WorkflowBuilderProps {
workflow?: Workflow | null;

View File

@@ -61,6 +61,7 @@ import analyticsService, {
import { SimpleBarChart, SimpleLineChart, SimplePieChart, KPICard } from '../../features/analytics/components/SimpleChart';
import { exportData } from '../../shared/utils/exportUtils';
import CustomReportBuilder from '../../features/analytics/components/CustomReportBuilder';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
type AnalyticsTab = 'overview' | 'reports' | 'revenue' | 'operational' | 'guest' | 'financial' | 'audit-logs' | 'reviews';

View File

@@ -6,6 +6,7 @@ import Loading from '../../shared/components/Loading';
import EmptyState from '../../shared/components/EmptyState';
import { formatDate } from '../../shared/utils/format';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const ApprovalManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency();
@@ -23,7 +24,7 @@ const ApprovalManagementPage: React.FC = () => {
const fetchApprovals = async () => {
try {
setLoading(true);
const params: { status?: string } = {};
const params: { status?: ApprovalStatus } = {};
if (filter !== 'all') params.status = filter;
const response = await approvalService.getApprovals(params);
setApprovals(response.data || []);
@@ -225,19 +226,29 @@ const ApprovalManagementPage: React.FC = () => {
<p className="text-lg font-bold text-emerald-600">{formatCurrency(selectedApproval.amount)}</p>
</div>
)}
{selectedApproval.previous_value && (
{selectedApproval.previous_value != null && (
<div>
<h3 className="font-semibold mb-2">Previous Value</h3>
<pre className="bg-slate-50 p-3 rounded text-sm overflow-x-auto">
{JSON.stringify(selectedApproval.previous_value, null, 2)}
{(() => {
const value = selectedApproval.previous_value;
return typeof value === 'string'
? value
: JSON.stringify(value, null, 2);
})()}
</pre>
</div>
)}
{selectedApproval.new_value && (
{selectedApproval.new_value != null && (
<div>
<h3 className="font-semibold mb-2">New Value</h3>
<pre className="bg-slate-50 p-3 rounded text-sm overflow-x-auto">
{JSON.stringify(selectedApproval.new_value, null, 2)}
{(() => {
const value = selectedApproval.new_value;
return typeof value === 'string'
? value
: JSON.stringify(value, null, 2);
})()}
</pre>
</div>
)}

View File

@@ -1,17 +1,18 @@
import React, { useState, useEffect } from 'react';
import { Download, Filter } from 'lucide-react';
import { toast } from 'react-toastify';
import financialAuditService, { FinancialAuditRecord } from '../../features/payments/services/financialAuditService';
import financialAuditService, { FinancialAuditRecord, FinancialAuditFilters } from '../../features/payments/services/financialAuditService';
import Loading from '../../shared/components/Loading';
import EmptyState from '../../shared/components/EmptyState';
import { formatDate } from '../../shared/utils/format';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const AuditTrailPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency();
const [loading, setLoading] = useState(true);
const [records, setRecords] = useState<FinancialAuditRecord[]>([]);
const [pagination, setPagination] = useState<unknown>(null);
const [pagination, setPagination] = useState<{ page: number; limit: number; total: number; total_pages: number } | null>(null);
const [filters, setFilters] = useState({
action_type: '',
user_id: '',
@@ -33,7 +34,18 @@ const AuditTrailPage: React.FC = () => {
const fetchAuditTrail = async () => {
try {
setLoading(true);
const response = await financialAuditService.getAuditTrail(filters);
const auditFilters: FinancialAuditFilters = {
action_type: filters.action_type || undefined,
user_id: filters.user_id ? parseInt(filters.user_id) : undefined,
start_date: filters.start_date || undefined,
end_date: filters.end_date || undefined,
payment_id: filters.payment_id ? parseInt(filters.payment_id) : undefined,
invoice_id: filters.invoice_id ? parseInt(filters.invoice_id) : undefined,
booking_id: filters.booking_id ? parseInt(filters.booking_id) : undefined,
page: filters.page,
limit: filters.limit,
};
const response = await financialAuditService.getAuditTrail(auditFilters);
if (response.status === 'success' && response.data) {
setRecords(response.data.audit_trail || []);
setPagination(response.data.pagination);
@@ -47,7 +59,18 @@ const AuditTrailPage: React.FC = () => {
const handleExport = async (format: 'csv' | 'json') => {
try {
const blob = await financialAuditService.exportAuditTrail(filters, format);
const auditFilters: FinancialAuditFilters = {
action_type: filters.action_type || undefined,
user_id: filters.user_id ? parseInt(filters.user_id) : undefined,
start_date: filters.start_date || undefined,
end_date: filters.end_date || undefined,
payment_id: filters.payment_id ? parseInt(filters.payment_id) : undefined,
invoice_id: filters.invoice_id ? parseInt(filters.invoice_id) : undefined,
booking_id: filters.booking_id ? parseInt(filters.booking_id) : undefined,
page: filters.page,
limit: filters.limit,
};
const blob = await financialAuditService.exportAuditTrail(auditFilters, format);
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;

View File

@@ -6,6 +6,7 @@ import glService from '../../features/payments/services/glService';
import Loading from '../../shared/components/Loading';
import { formatDate } from '../../shared/utils/format';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const FinancialReportsPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency();

View File

@@ -6,6 +6,7 @@ import Loading from '../../shared/components/Loading';
import EmptyState from '../../shared/components/EmptyState';
import { formatDate } from '../../shared/utils/format';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const GLManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency();

View File

@@ -9,6 +9,7 @@ import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurren
import { useNavigate } from 'react-router-dom';
import { formatDate } from '../../shared/utils/format';
import { logger } from '../../shared/utils/logger';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const InvoiceManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency();

View File

@@ -9,6 +9,7 @@ import ExportButton from '../../shared/components/ExportButton';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { formatDate } from '../../shared/utils/format';
import { getPaymentStatusColor, getPaymentMethodLabel } from '../../shared/utils/paymentUtils';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const PaymentManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency();

View File

@@ -24,7 +24,9 @@ import {
Trash2,
Database,
Smartphone,
Tablet
Tablet,
Eye,
EyeOff
} from 'lucide-react';
import { toast } from 'react-toastify';
import authService from '../../features/auth/services/authService';
@@ -38,7 +40,8 @@ import { useGlobalLoading } from '../../shared/contexts/LoadingContext';
import { normalizeImageUrl } from '../../shared/utils/imageUtils';
import { formatDate } from '../../shared/utils/format';
import { UserSession } from '../../features/auth/services/sessionService';
import { useNavigate } from 'react-router-dom';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
import { isAxiosError } from 'axios';
const profileValidationSchema = yup.object().shape({
name: yup
@@ -83,7 +86,7 @@ type PasswordFormData = yup.InferType<typeof passwordValidationSchema>;
const ProfilePage: React.FC = () => {
const { userInfo, setUser } = useAuthStore();
const { setLoading } = useGlobalLoading();
const _navigate = useNavigate();
// const _navigate = useNavigate(); // Reserved for future use
const [activeTab, setActiveTab] = useState<'profile' | 'password' | 'mfa' | 'sessions' | 'gdpr'>('profile');
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
const [avatarError, setAvatarError] = useState<boolean>(false);
@@ -105,8 +108,8 @@ const ProfilePage: React.FC = () => {
const [showBackupCodes, setShowBackupCodes] = useState<string[] | null>(null);
const [showMfaSecret, setShowMfaSecret] = useState<boolean>(false);
const mfaAbortControllerRef = useRef<AbortController | null>(null);
const _sessionsAbortControllerRef = useRef<AbortController | null>(null);
const _gdprAbortControllerRef = useRef<AbortController | null>(null);
// const _sessionsAbortControllerRef = useRef<AbortController | null>(null); // Reserved for future use
// const _gdprAbortControllerRef = useRef<AbortController | null>(null); // Reserved for future use
const fetchProfile = async () => {
@@ -241,8 +244,16 @@ const ProfilePage: React.FC = () => {
if (response.status === 'success' || response.success) {
const updatedUser = response.data?.user || response.data;
if (updatedUser) {
setUser(updatedUser);
if (updatedUser && typeof updatedUser === 'object' && 'id' in updatedUser) {
setUser({
id: (updatedUser as { id: number }).id,
name: ((updatedUser as { name?: string; full_name?: string }).name || (updatedUser as { name?: string; full_name?: string }).full_name || '') as string,
email: (updatedUser as { email: string }).email,
phone: ((updatedUser as { phone?: string; phone_number?: string }).phone || (updatedUser as { phone?: string; phone_number?: string }).phone_number) as string | undefined,
avatar: (updatedUser as { avatar?: string }).avatar,
role: (updatedUser as { role: string }).role,
createdAt: ((updatedUser as { createdAt?: string; created_at?: string }).createdAt || (updatedUser as { createdAt?: string; created_at?: string }).created_at) as string | undefined,
});
toast.success('Profile updated successfully!');
refetchProfile();
}
@@ -1158,7 +1169,7 @@ const SessionsTab: React.FC = () => {
fetchSessions();
}
} catch (error: unknown) {
if (getUserFriendlyError(error) === 401) {
if (isAxiosError(error) && error.response?.status === 401) {
toast.warning('Your session has been revoked. You will be logged out.');
setTimeout(() => {
window.location.href = '/';
@@ -1184,7 +1195,7 @@ const SessionsTab: React.FC = () => {
fetchSessions();
}
} catch (error: unknown) {
if (getUserFriendlyError(error) === 401) {
if (isAxiosError(error) && error.response?.status === 401) {
toast.warning('All sessions have been revoked. You will be logged out.');
setTimeout(() => {
window.location.href = '/';

View File

@@ -1,17 +1,18 @@
import React, { useState, useEffect } from 'react';
import { Play } from 'lucide-react';
import { Play, AlertTriangle } from 'lucide-react';
import { toast } from 'react-toastify';
import reconciliationService, { ReconciliationException, ExceptionStatus } from '../../features/payments/services/reconciliationService';
import reconciliationService, { ReconciliationException, ExceptionStatus, ExceptionStats } from '../../features/payments/services/reconciliationService';
import Loading from '../../shared/components/Loading';
import EmptyState from '../../shared/components/EmptyState';
import { formatDate } from '../../shared/utils/format';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const ReconciliationPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency();
const [loading, setLoading] = useState(false);
const [exceptions, setExceptions] = useState<ReconciliationException[]>([]);
const [stats, setStats] = useState<Record<string, unknown> | null>(null);
const [stats, setStats] = useState<ExceptionStats | null>(null);
const [selectedException, setSelectedException] = useState<ReconciliationException | null>(null);
const [filter, setFilter] = useState<ExceptionStatus | 'all'>('all');
const [comment, setComment] = useState('');

View File

@@ -6,6 +6,7 @@ import accountantSecurityService, { AccountantSession, AccountantActivityLog, MF
import Loading from '../../shared/components/Loading';
import EmptyState from '../../shared/components/EmptyState';
import { formatDate } from '../../shared/utils/format';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const SecurityManagementPage: React.FC = () => {
const [searchParams] = useSearchParams();

View File

@@ -4,6 +4,7 @@ import apiKeyService, { APIKey, CreateAPIKeyData, UpdateAPIKeyData } from '../..
import { toast } from 'react-toastify';
import Loading from '../../shared/components/Loading';
import { formatDate } from '../../shared/utils/format';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const APIKeyManagementPage: React.FC = () => {
const [apiKeys, setApiKeys] = useState<APIKey[]>([]);

View File

@@ -40,6 +40,7 @@ import analyticsService, {
import { SimpleBarChart, SimpleLineChart, SimplePieChart, KPICard } from '../../features/analytics/components/SimpleChart';
import { exportData } from '../../shared/utils/exportUtils';
import CustomReportBuilder from '../../features/analytics/components/CustomReportBuilder';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
type AnalyticsCategory = 'revenue' | 'operational' | 'guest' | 'financial' | 'comprehensive';
@@ -211,7 +212,7 @@ const AdvancedAnalyticsPage: React.FC = () => {
title = 'Financial Analytics Report';
} else if (comprehensiveData) {
// Export comprehensive data
exportDataArray = [comprehensiveData];
exportDataArray = [comprehensiveData as unknown as Record<string, unknown>];
filename = 'comprehensive-analytics';
title = 'Comprehensive Analytics Report';
}

View File

@@ -36,6 +36,7 @@ import apiClient from '../../shared/services/apiClient';
import { logger } from '../../shared/utils/logger';
import { useRoomContext } from '../../features/rooms/contexts/RoomContext';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
type Tab = 'status-board' | 'maintenance' | 'housekeeping' | 'inspections' | 'room-types';
@@ -80,8 +81,8 @@ const AdvancedRoomManagementPage: React.FC = () => {
const [roomTypes, setRoomTypes] = useState<Array<{ id: number; name: string }>>([]);
const [uploadingImages, setUploadingImages] = useState(false);
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const [customAmenityInput, setCustomAmenityInput] = useState('');
const [editingAmenity, setEditingAmenity] = useState<{ name: string; newName: string } | null>(null);
const [_customAmenityInput, _setCustomAmenityInput] = useState('');
const [_editingAmenity, _setEditingAmenity] = useState<{ name: string; newName: string } | null>(null);
// Room Types management state
const [roomTypesList, setRoomTypesList] = useState<RoomType[]>([]);
@@ -671,92 +672,93 @@ const AdvancedRoomManagementPage: React.FC = () => {
});
setSelectedFiles([]);
setUploadingImages(false);
setCustomAmenityInput('');
setEditingAmenity(null);
_setCustomAmenityInput('');
_setEditingAmenity(null);
};
// Amenities are now managed only at the room type level
// Rooms automatically inherit amenities from their room type
const _handleAddCustomAmenity = () => {
const trimmed = customAmenityInput.trim();
if (trimmed && !roomFormData.amenities.includes(trimmed)) {
setRoomFormData(prev => ({
...prev,
amenities: [...prev.amenities, trimmed]
}));
setCustomAmenityInput('');
}
};
// Unused functions - commented out to fix TypeScript errors
// const _handleAddCustomAmenity = () => {
// const trimmed = customAmenityInput.trim();
// if (trimmed && !roomFormData.amenities.includes(trimmed)) {
// setRoomFormData(prev => ({
// ...prev,
// amenities: [...prev.amenities, trimmed]
// }));
// setCustomAmenityInput('');
// }
// };
const _handleRemoveAmenity = (amenity: string) => {
setRoomFormData(prev => ({
...prev,
amenities: prev.amenities.filter(a => a !== amenity)
}));
};
// const _handleRemoveAmenity = (amenity: string) => {
// setRoomFormData(prev => ({
// ...prev,
// amenities: prev.amenities.filter(a => a !== amenity)
// }));
// };
const _handleEditAmenity = (amenity: string) => {
setEditingAmenity({ name: amenity, newName: amenity });
};
// const _handleEditAmenity = (amenity: string) => {
// setEditingAmenity({ name: amenity, newName: amenity });
// };
const _handleSaveAmenityEdit = async () => {
if (!editingAmenity || editingAmenity.name === editingAmenity.newName.trim()) {
setEditingAmenity(null);
return;
}
// const _handleSaveAmenityEdit = async () => {
// if (!editingAmenity || editingAmenity.name === editingAmenity.newName.trim()) {
// setEditingAmenity(null);
// return;
// }
const newName = editingAmenity.newName.trim();
if (!newName) {
toast.error('Amenity name cannot be empty');
return;
}
// const newName = editingAmenity.newName.trim();
// if (!newName) {
// toast.error('Amenity name cannot be empty');
// return;
// }
try {
await roomService.updateAmenity(editingAmenity.name, newName);
toast.success(`Amenity "${editingAmenity.name}" updated to "${newName}"`);
// try {
// await roomService.updateAmenity(editingAmenity.name, newName);
// toast.success(`Amenity "${editingAmenity.name}" updated to "${newName}"`);
setAvailableAmenities(prev => {
const updated = prev.map(a => a === editingAmenity.name ? newName : a);
return updated.sort();
});
// setAvailableAmenities(prev => {
// const updated = prev.map(a => a === editingAmenity.name ? newName : a);
// return updated.sort();
// });
setRoomFormData(prev => ({
...prev,
amenities: prev.amenities.map(a => a === editingAmenity.name ? newName : a)
}));
// setRoomFormData(prev => ({
// ...prev,
// amenities: prev.amenities.map(a => a === editingAmenity.name ? newName : a)
// }));
setEditingAmenity(null);
await fetchAvailableAmenities();
await refreshRooms();
await refreshStatusBoard();
} catch (error: unknown) {
toast.error(getUserFriendlyError(error) || getUserFriendlyError(error) || 'Failed to update amenity');
}
};
// setEditingAmenity(null);
// await fetchAvailableAmenities();
// await refreshRooms();
// await refreshStatusBoard();
// } catch (error: unknown) {
// toast.error(getUserFriendlyError(error) || 'Failed to update amenity');
// }
// };
const _handleDeleteAmenity = async (amenity: string) => {
if (!window.confirm(`Are you sure you want to delete "${amenity}"? This will remove it from all rooms and room types.`)) {
return;
}
// const _handleDeleteAmenity = async (amenity: string) => {
// if (!window.confirm(`Are you sure you want to delete "${amenity}"? This will remove it from all rooms and room types.`)) {
// return;
// }
try {
await roomService.deleteAmenity(amenity);
toast.success(`Amenity "${amenity}" deleted successfully`);
// try {
// await roomService.deleteAmenity(amenity);
// toast.success(`Amenity "${amenity}" deleted successfully`);
setAvailableAmenities(prev => prev.filter(a => a !== amenity));
setRoomFormData(prev => ({
...prev,
amenities: prev.amenities.filter(a => a !== amenity)
}));
// setAvailableAmenities(prev => prev.filter(a => a !== amenity));
// setRoomFormData(prev => ({
// ...prev,
// amenities: prev.amenities.filter(a => a !== amenity)
// }));
await fetchAvailableAmenities();
await refreshRooms();
await refreshStatusBoard();
} catch (error: unknown) {
toast.error(getUserFriendlyError(error) || getUserFriendlyError(error) || 'Failed to delete amenity');
}
};
// await fetchAvailableAmenities();
// await refreshRooms();
// await refreshStatusBoard();
// } catch (error: unknown) {
// toast.error(getUserFriendlyError(error) || 'Failed to delete amenity');
// }
// };
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!e.target.files || e.target.files.length === 0) {
@@ -896,38 +898,38 @@ const AdvancedRoomManagementPage: React.FC = () => {
setEditingRoom(response.data.room);
} catch (error: unknown) {
logger.error('Error deleting image', error);
toast.error(getUserFriendlyError(error) || getUserFriendlyError(error) || 'Unable to delete image');
toast.error(getUserFriendlyError(error) || 'Unable to delete image');
}
};
const _getRoomStatusBadge = (status: string) => {
const badges: Record<string, { bg: string; text: string; label: string; border: string }> = {
available: {
bg: 'bg-gradient-to-r from-emerald-50 to-green-50',
text: 'text-emerald-800',
label: 'Available',
border: 'border-emerald-200'
},
occupied: {
bg: 'bg-gradient-to-r from-blue-50 to-indigo-50',
text: 'text-blue-800',
label: 'Occupied',
border: 'border-blue-200'
},
maintenance: {
bg: 'bg-gradient-to-r from-amber-50 to-yellow-50',
text: 'text-amber-800',
label: 'Maintenance',
border: 'border-amber-200'
},
};
const badge = badges[status] || badges.available;
return (
<span className={`px-4 py-1.5 rounded-full text-xs font-semibold border shadow-sm ${badge.bg} ${badge.text} ${badge.border}`}>
{badge.label}
</span>
);
};
// const _getRoomStatusBadge = (status: string) => {
// const badges: Record<string, { bg: string; text: string; label: string; border: string }> = {
// available: {
// bg: 'bg-gradient-to-r from-emerald-50 to-green-50',
// text: 'text-emerald-800',
// label: 'Available',
// border: 'border-emerald-200'
// },
// occupied: {
// bg: 'bg-gradient-to-r from-blue-50 to-indigo-50',
// text: 'text-blue-800',
// label: 'Occupied',
// border: 'border-blue-200'
// },
// maintenance: {
// bg: 'bg-gradient-to-r from-amber-50 to-yellow-50',
// text: 'text-amber-800',
// label: 'Maintenance',
// border: 'border-amber-200'
// },
// };
// const badge = badges[status] || badges.available;
// return (
// <span className={`px-4 py-1.5 rounded-full text-xs font-semibold border shadow-sm ${badge.bg} ${badge.text} ${badge.border}`}>
// {badge.label}
// </span>
// );
// };
if (statusBoardLoading && statusBoardRooms.length === 0 && activeTab === 'status-board') {
return <Loading />;

View File

@@ -62,6 +62,7 @@ import analyticsService, {
import { SimpleBarChart, SimpleLineChart, SimplePieChart, KPICard } from '../../features/analytics/components/SimpleChart';
import { exportData } from '../../shared/utils/exportUtils';
import CustomReportBuilder from '../../features/analytics/components/CustomReportBuilder';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
type AnalyticsTab = 'overview' | 'reports' | 'revenue' | 'operational' | 'guest' | 'financial' | 'audit-logs';
@@ -835,7 +836,7 @@ const AnalyticsDashboardPage: React.FC = () => {
<label className="block text-sm font-semibold text-gray-900">Report Type</label>
<select
value={reportType}
onChange={(e) => setReportType(e.target.value as 'revenue' | 'occupancy' | 'guest' | 'financial')}
onChange={(e) => setReportType(e.target.value as 'daily' | 'weekly' | 'monthly' | 'yearly' | '')}
className="w-full px-4 py-3 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition-all duration-200"
>
<option value="">All</option>

View File

@@ -5,6 +5,7 @@ import { toast } from 'react-toastify';
import Loading from '../../shared/components/Loading';
import Pagination from '../../shared/components/Pagination';
import { formatDate } from '../../shared/utils/format';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const ApprovalManagementPage: React.FC = () => {
const [approvals, setApprovals] = useState<ApprovalRequest[]>([]);
@@ -72,20 +73,20 @@ const ApprovalManagementPage: React.FC = () => {
}
};
const _handleCancel = async (id: number) => {
if (!confirm('Are you sure you want to cancel this approval request?')) return;
// const _handleCancel = async (id: number) => {
// if (!confirm('Are you sure you want to cancel this approval request?')) return;
try {
setProcessingId(id);
await approvalService.cancelRequest(id);
toast.success('Approval request cancelled');
fetchApprovals();
} catch (error: unknown) {
toast.error(getUserFriendlyError(error) || 'Unable to cancel request');
} finally {
setProcessingId(null);
}
};
// try {
// setProcessingId(id);
// await approvalService.cancelRequest(id);
// toast.success('Approval request cancelled');
// fetchApprovals();
// } catch (error: unknown) {
// toast.error(getUserFriendlyError(error) || 'Unable to cancel request');
// } finally {
// setProcessingId(null);
// }
// };
const getStatusIcon = (status: string) => {
switch (status) {

View File

@@ -5,6 +5,8 @@ import { toast } from 'react-toastify';
import { logger } from '../../shared/utils/logger';
import Loading from '../../shared/components/Loading';
import { formatDate } from '../../shared/utils/format';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
import { isAxiosError } from 'axios';
const BackupManagementPage: React.FC = () => {
const [backups, setBackups] = useState<Backup[]>([]);
@@ -51,18 +53,22 @@ const BackupManagementPage: React.FC = () => {
toast.success('Backup created successfully');
fetchBackups();
} catch (error: unknown) {
const errorDetail = getUserFriendlyError(error);
if (getUserFriendlyError(error) === 503 && errorDetail?.requires_installation) {
// Show a more detailed error for missing mysqldump
toast.error(
<div>
<p className="font-semibold mb-1">Backup service unavailable</p>
<p className="text-sm">{errorDetail.message || errorDetail}</p>
</div>,
if (isAxiosError(error) && error.response?.status === 503) {
const errorDetail = error.response?.data as { requires_installation?: boolean; message?: string } | undefined;
if (errorDetail?.requires_installation) {
// Show a more detailed error for missing mysqldump
toast.error(
<div>
<p className="font-semibold mb-1">Backup service unavailable</p>
<p className="text-sm">{errorDetail.message || getUserFriendlyError(error)}</p>
</div>,
{ autoClose: 10000 }
);
} else {
toast.error(getUserFriendlyError(error) || 'Unable to create backup');
}
} else {
toast.error(getUserFriendlyError(error)?.message || getUserFriendlyError(error) || getUserFriendlyError(error) || 'Unable to create backup');
toast.error(getUserFriendlyError(error) || 'Unable to create backup');
}
} finally {
setCreating(false);

View File

@@ -6,6 +6,7 @@ import Pagination from '../../shared/components/Pagination';
import ConfirmationDialog from '../../shared/components/ConfirmationDialog';
import bannerServiceModule from '../../features/content/services/bannerService';
import type { Banner } from '../../features/content/services/bannerService';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const {
getAllBanners,
@@ -113,7 +114,7 @@ const BannerManagementPage: React.FC = () => {
try {
setUploadingImage(true);
const response = await uploadBannerImage(file);
if (response.status === 'success' || response.success) {
if ((response as { status?: string; success?: boolean }).status === 'success' || response.success) {
setFormData({ ...formData, image_url: response.data.image_url });
toast.success('Image uploaded successfully');
}
@@ -141,7 +142,7 @@ const BannerManagementPage: React.FC = () => {
if (imageFile && !imageUrl) {
setUploadingImage(true);
const uploadResponse = await uploadBannerImage(imageFile);
if (uploadResponse.status === 'success' || uploadResponse.success) {
if ((uploadResponse as { status?: string; success?: boolean }).status === 'success' || uploadResponse.success) {
imageUrl = uploadResponse.data.image_url;
} else {
throw new Error('Failed to upload image');

View File

@@ -5,6 +5,7 @@ import Loading from '../../shared/components/Loading';
import Pagination from '../../shared/components/Pagination';
import ConfirmationDialog from '../../shared/components/ConfirmationDialog';
import blogService, { BlogPost, BlogPostCreate, BlogPostUpdate, BlogSection } from '../../features/content/services/blogService';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const BlogManagementPage: React.FC = () => {
const [posts, setPosts] = useState<BlogPost[]>([]);

View File

@@ -10,6 +10,7 @@ import { parseDateLocal } from '../../shared/utils/format';
import { useNavigate, useSearchParams } from 'react-router-dom';
import CreateBookingModal from '../../features/hotel_services/components/CreateBookingModal';
import { logger } from '../../shared/utils/logger';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const BookingManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency();
@@ -158,7 +159,7 @@ const BookingManagementPage: React.FC = () => {
const handleUpdateStatus = async (id: number, status: string) => {
try {
setUpdatingBookingId(id);
await bookingService.updateBooking(id, { status } as { status: string });
await bookingService.updateBooking(id, { status: status as Booking['status'] });
toast.success('Status updated successfully');
await fetchBookings();
} catch (error: unknown) {
@@ -203,15 +204,15 @@ const BookingManagementPage: React.FC = () => {
logger.info('Invoice creation response', { response });
// Check response structure - handle different possible formats
let invoice = null;
let invoice: { id?: number | string } | null = null;
if (response.status === 'success' && response.data) {
// Try different possible response structures
const responseData = response.data as { invoice?: unknown; data?: { invoice?: unknown } };
invoice = responseData.invoice || responseData.data?.invoice || response.data;
const responseData = response.data as { invoice?: { id?: number | string }; data?: { invoice?: { id?: number | string } } };
invoice = (responseData.invoice || responseData.data?.invoice || response.data) as { id?: number | string } | null;
logger.debug('Extracted invoice', { invoice });
}
if (!invoice) {
if (!invoice || !invoice.id) {
logger.error('Failed to create invoice - no invoice in response', { response });
toast.error(response.message || 'Failed to create invoice - no invoice data received');
return;
@@ -770,7 +771,7 @@ const BookingManagementPage: React.FC = () => {
Additional Services
</label>
<div className="space-y-2">
{((selectedBooking as Booking & { service_usages: Array<{ id?: number; service_name?: string; name?: string; quantity?: number; price?: number; total?: number }> }).service_usages).map((service, idx: number) => (
{((selectedBooking as Booking & { service_usages: Array<{ id?: number; service_name?: string; name?: string; quantity?: number; price?: number; unit_price?: number; total?: number; total_price?: number }> }).service_usages).map((service, idx: number) => (
<div key={service.id || idx} className="flex justify-between items-center py-2 border-b border-purple-100 last:border-0">
<div>
<p className="text-sm font-medium text-slate-900">{service.service_name || service.name || 'Service'}</p>
@@ -823,13 +824,13 @@ const BookingManagementPage: React.FC = () => {
</p>
</div>
<span className={`text-xs px-2 py-1 rounded-full font-medium ${
payment.payment_status === 'completed' || payment.payment_status === 'paid'
payment.payment_status === 'completed'
? 'bg-green-100 text-green-700'
: payment.payment_status === 'pending'
? 'bg-yellow-100 text-yellow-700'
: 'bg-red-100 text-red-700'
}`}>
{payment.payment_status === 'completed' || payment.payment_status === 'paid' ? 'Paid' : payment.payment_status || 'Pending'}
{payment.payment_status === 'completed' ? 'Paid' : payment.payment_status || 'Pending'}
</span>
</div>
{payment.transaction_id && (

View File

@@ -6,6 +6,7 @@ import Loading from '../../shared/components/Loading';
import CurrencyIcon from '../../shared/components/CurrencyIcon';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { parseDateLocal } from '../../shared/utils/format';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
interface ServiceItem {
service_name: string;

View File

@@ -9,6 +9,7 @@ import EmptyState from '../../shared/components/EmptyState';
import Pagination from '../../shared/components/Pagination';
import complaintService, { Complaint, ComplaintFilters } from '../../features/guest_management/services/complaintService';
import { formatDate } from '../../shared/utils/format';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const ComplaintManagementPage: React.FC = () => {
const [complaints, setComplaints] = useState<Complaint[]>([]);
@@ -92,7 +93,7 @@ const ComplaintManagementPage: React.FC = () => {
toast.success('Complaint status updated');
fetchComplaints();
if (selectedComplaint?.id === complaintId) {
setSelectedComplaint({ ...selectedComplaint, status: status as 'pending' | 'in_progress' | 'resolved' | 'closed' });
setSelectedComplaint({ ...selectedComplaint, status: status as 'open' | 'in_progress' | 'resolved' | 'closed' | 'escalated' });
}
} catch (error: unknown) {
toast.error(getUserFriendlyError(error) || 'Unable to update complaint');
@@ -305,7 +306,7 @@ const ComplaintDetailModal: React.FC<{
onClose: () => void;
onResolve: () => void;
onUpdateStatus: (id: number, status: string) => void;
}> = ({ complaint, onClose, onResolve, _onUpdateStatus }) => {
}> = ({ complaint, onClose, onResolve }) => {
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
@@ -385,7 +386,7 @@ const ResolveComplaintModal: React.FC<{
onClose: () => void;
onResolve: (resolution: string, rating?: number, feedback?: string) => void;
resolving: boolean;
}> = ({ _complaint, onClose, onResolve, resolving }) => {
}> = ({ onClose, onResolve, resolving }) => {
const [resolution, setResolution] = useState('');
const [rating, setRating] = useState<number | undefined>();
const [feedback, setFeedback] = useState('');

View File

@@ -2,12 +2,14 @@ import React, { useState, useEffect } from 'react';
import {
Download,
Shield,
Activity
Activity,
DollarSign
} from 'lucide-react';
import { toast } from 'react-toastify';
import Loading from '../../shared/components/Loading';
import complianceService, { ComplianceReport, GDPRSummary } from '../../features/security/services/complianceService';
import { formatDate } from '../../shared/utils/format';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const ComplianceReportingPage: React.FC = () => {
const [report, setReport] = useState<ComplianceReport | null>(null);

View File

@@ -8,6 +8,7 @@ import adminPrivacyService, {
CookiePolicySettingsResponse,
} from '../../features/content/services/adminPrivacyService';
import Loading from '../../shared/components/Loading';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const CookieSettingsPage: React.FC = () => {
const [policy, setPolicy] = useState<CookiePolicySettings>({

View File

@@ -5,6 +5,7 @@ import systemSettingsService, { PlatformCurrencyResponse } from '../../features/
import { useCurrency } from '../../features/payments/contexts/CurrencyContext';
import Loading from '../../shared/components/Loading';
import { getCurrencySymbol } from '../../shared/utils/format';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const CurrencySettingsPage: React.FC = () => {
const { currency, supportedCurrencies, refreshCurrency } = useCurrency();

View File

@@ -7,6 +7,7 @@ import Loading from '../../shared/components/Loading';
import apiClient from '../../shared/services/apiClient';
import { logger } from '../../shared/utils/logger';
import { useRoomContext } from '../../features/rooms/contexts/RoomContext';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const EditRoomPage: React.FC = () => {
const { id } = useParams<{ id: string }>();
@@ -26,7 +27,7 @@ const EditRoomPage: React.FC = () => {
room_number: '',
floor: 1,
room_type_id: 1,
status: 'available' as 'available' | 'occupied' | 'maintenance',
status: 'available' as 'available' | 'occupied' | 'maintenance' | 'cleaning' | 'reserved',
featured: false,
price: '',
description: '',
@@ -730,7 +731,7 @@ const EditRoomPage: React.FC = () => {
</label>
<select
value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e.target.value as 'available' | 'occupied' | 'maintenance' | 'reserved' | 'out_of_order' })}
onChange={(e) => setFormData({ ...formData, status: e.target.value as 'available' | 'occupied' | 'maintenance' | 'cleaning' | 'reserved' })}
className="w-full px-5 py-3.5 bg-gradient-to-br from-white to-slate-50/50 border-2 border-slate-200 rounded-2xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100/50 transition-all duration-300 text-slate-800 font-medium shadow-sm hover:shadow-md focus:shadow-lg cursor-pointer appearance-none bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNiA5TDEyIDE1TDE4IDkiIHN0cm9rZT0iIzkyOUE1IiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvc3ZnPg==')] bg-[length:20px] bg-[right_1rem_center] bg-no-repeat pr-12"
required
>

View File

@@ -15,9 +15,51 @@ import { toast } from 'react-toastify';
import Loading from '../../shared/components/Loading';
import Pagination from '../../shared/components/Pagination';
import { formatDate } from '../../shared/utils/format';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
type CampaignTab = 'campaigns' | 'segments' | 'templates' | 'drip-sequences' | 'analytics';
type CampaignFormData = {
name: string;
subject: string;
html_content: string;
text_content: string;
campaign_type: string;
segment_id: number | undefined;
scheduled_at: string;
template_id: number | undefined;
from_name: string;
from_email: string;
track_opens: boolean;
track_clicks: boolean;
recipient_type: 'users' | 'subscribers' | 'both';
};
type SegmentFormData = {
name: string;
description: string;
criteria: {
role: string;
has_bookings: boolean | undefined;
is_vip: boolean | undefined;
last_booking_days: number | undefined;
};
};
type TemplateFormData = {
name: string;
subject: string;
html_content: string;
text_content: string;
category: string;
};
type DripFormData = {
name: string;
description: string;
trigger_event: string;
};
const EmailCampaignManagementPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<CampaignTab>('campaigns');
const [loading, setLoading] = useState(false);
@@ -238,9 +280,10 @@ const EmailCampaignManagementPage: React.FC = () => {
html_content: '',
text_content: '',
campaign_type: 'newsletter',
segment_id: undefined,
segment_id: undefined as number | undefined,
scheduled_at: '',
template_id: undefined,
template_id: undefined as number | undefined,
recipient_type: 'users' as 'users' | 'subscribers' | 'both',
from_name: '',
from_email: '',
track_opens: true,
@@ -703,8 +746,8 @@ const AnalyticsTab: React.FC<{ analytics: CampaignAnalytics }> = ({ analytics })
);
const CampaignModal: React.FC<{
form: typeof campaignForm;
setForm: (form: typeof campaignForm) => void;
form: CampaignFormData;
setForm: (form: CampaignFormData) => void;
segments: CampaignSegment[];
templates: EmailTemplate[];
onSave: () => void;
@@ -800,8 +843,8 @@ const CampaignModal: React.FC<{
);
const SegmentModal: React.FC<{
form: typeof segmentForm;
setForm: (form: typeof segmentForm) => void;
form: SegmentFormData;
setForm: (form: SegmentFormData) => void;
onSave: () => void;
onClose: () => void;
}> = ({ form, setForm, onSave, onClose }) => (
@@ -865,8 +908,8 @@ const SegmentModal: React.FC<{
);
const TemplateModal: React.FC<{
form: typeof templateForm;
setForm: (form: typeof templateForm) => void;
form: TemplateFormData;
setForm: (form: TemplateFormData) => void;
onSave: () => void;
onClose: () => void;
}> = ({ form, setForm, onSave, onClose }) => (
@@ -927,8 +970,8 @@ const TemplateModal: React.FC<{
);
const DripSequenceModal: React.FC<{
form: typeof dripForm;
setForm: (form: typeof dripForm) => void;
form: DripFormData;
setForm: (form: DripFormData) => void;
onSave: () => void;
onClose: () => void;
}> = ({ form, setForm, onSave, onClose }) => (

View File

@@ -12,6 +12,7 @@ import financialAuditService, { FinancialAuditRecord, FinancialAuditFilters } fr
import { formatDate } from '../../shared/utils/format';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { exportData } from '../../shared/utils/exportUtils';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const FinancialAuditTrailPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency();
@@ -90,7 +91,11 @@ const FinancialAuditTrailPage: React.FC = () => {
'Created At': formatDate(record.created_at),
}));
exportData(csvData, 'financial_audit_trail', 'csv');
exportData({
data: csvData,
filename: 'financial_audit_trail',
format: 'csv',
});
toast.success('Audit trail exported successfully');
} catch (error: unknown) {
toast.error(getUserFriendlyError(error) || 'Unable to export audit trail');

View File

@@ -3,6 +3,8 @@ import { Download, Trash2 } from 'lucide-react';
import gdprService, { GDPRRequest } from '../../features/compliance/services/gdprService';
import { toast } from 'react-toastify';
import Loading from '../../shared/components/Loading';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
import { formatDate } from '../../shared/utils/format';
const GDPRManagementPage: React.FC = () => {
const [requests, setRequests] = useState<GDPRRequest[]>([]);

View File

@@ -7,6 +7,7 @@ import Pagination from '../../shared/components/Pagination';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { formatDate } from '../../shared/utils/format';
import CreateGroupBookingModal from '../../features/hotel_services/components/CreateGroupBookingModal';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const GroupBookingManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency();

View File

@@ -22,6 +22,7 @@ import Loading from '../../shared/components/Loading';
import Pagination from '../../shared/components/Pagination';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { logger } from '../../shared/utils/logger';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
type TabType = 'list' | 'profile';
@@ -108,7 +109,7 @@ const GuestProfilePage: React.FC = () => {
setCurrentPage(1);
};
const handleFilterChange = (key: keyof GuestSearchParams, value: string | number | undefined) => {
const handleFilterChange = (key: keyof GuestSearchParams, value: string | number | boolean | undefined) => {
setFilters({ ...filters, [key]: value, page: 1 });
setCurrentPage(1);
};

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { Search, Plus, Edit, Eye, CheckCircle, Clock, X, FileCheck, AlertCircle } from 'lucide-react';
import advancedRoomService, { RoomInspection } from '../../features/rooms/services/advancedRoomService';
import advancedRoomService, { RoomInspection, InspectionChecklistItem } from '../../features/rooms/services/advancedRoomService';
import roomService, { Room } from '../../features/rooms/services/roomService';
import userService, { User } from '../../features/auth/services/userService';
import { toast } from 'react-toastify';
@@ -8,6 +8,7 @@ import Loading from '../../shared/components/Loading';
import Pagination from '../../shared/components/Pagination';
import { formatDate } from '../../shared/utils/format';
import DatePicker from 'react-datepicker';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
import 'react-datepicker/dist/react-datepicker.css';
const InspectionManagementPage: React.FC = () => {
@@ -37,7 +38,7 @@ const InspectionManagementPage: React.FC = () => {
inspection_type: 'routine',
scheduled_at: new Date(),
inspected_by: 0,
checklist_items: [] as Array<{ id?: number; item: string; checked: boolean; notes?: string }>,
checklist_items: [] as InspectionChecklistItem[],
});
const inspectionTypes = [
@@ -147,7 +148,14 @@ const InspectionManagementPage: React.FC = () => {
toast.success('Inspection updated successfully');
} else {
// Create new inspection
const dataToSubmit: Record<string, unknown> = {
const dataToSubmit: {
room_id: number;
inspection_type: string;
scheduled_at: string;
checklist_items: InspectionChecklistItem[];
booking_id?: number;
inspected_by?: number;
} = {
room_id: formData.room_id,
inspection_type: formData.inspection_type,
scheduled_at: formData.scheduled_at.toISOString(),
@@ -531,7 +539,11 @@ const InspectionManagementPage: React.FC = () => {
<label className="block text-sm font-semibold text-slate-700 mb-2">Scheduled At *</label>
<DatePicker
selected={formData.scheduled_at}
onChange={(date: Date) => setFormData({ ...formData, scheduled_at: date })}
onChange={(date: Date | null) => {
if (date) {
setFormData({ ...formData, scheduled_at: date });
}
}}
showTimeSelect
dateFormat="MMMM d, yyyy h:mm aa"
className="w-full px-4 py-2 border-2 border-slate-200 rounded-xl focus:border-amber-400"

View File

@@ -5,6 +5,7 @@ import { toast } from 'react-toastify';
import Loading from '../../shared/components/Loading';
import Pagination from '../../shared/components/Pagination';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const InventoryManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency();

View File

@@ -6,6 +6,7 @@ import { toast } from 'react-toastify';
import Loading from '../../shared/components/Loading';
import { validateInvoiceId } from '../../shared/utils/routeValidation';
import { logger } from '../../shared/utils/logger';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const InvoiceEditPage: React.FC = () => {
const { id } = useParams<{ id: string }>();

View File

@@ -8,6 +8,7 @@ import ExportButton from '../../shared/components/ExportButton';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { useNavigate } from 'react-router-dom';
import { formatDate } from '../../shared/utils/format';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const InvoiceManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency();

View File

@@ -20,6 +20,7 @@ import ConfirmationDialog from '../../shared/components/ConfirmationDialog';
import loyaltyService, { LoyaltyTier, LoyaltyReward } from '../../features/loyalty/services/loyaltyService';
import Pagination from '../../shared/components/Pagination';
import { logger } from '../../shared/utils/logger';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
type Tab = 'users' | 'tiers' | 'rewards';

View File

@@ -8,6 +8,8 @@ import Loading from '../../shared/components/Loading';
import Pagination from '../../shared/components/Pagination';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import DatePicker from 'react-datepicker';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
import { formatDate } from '../../shared/utils/format';
import 'react-datepicker/dist/react-datepicker.css';
const MaintenanceManagementPage: React.FC = () => {
@@ -87,7 +89,7 @@ const MaintenanceManagementPage: React.FC = () => {
const fetchRecords = async () => {
try {
setLoading(true);
const params: { page: number; limit: number; room_id?: number; status?: string } = {
const params: { page: number; limit: number; room_id?: number; status?: string; maintenance_type?: string; priority?: string } = {
page: currentPage,
limit: itemsPerPage,
};
@@ -163,7 +165,21 @@ const MaintenanceManagementPage: React.FC = () => {
}
try {
const dataToSubmit: Record<string, unknown> = {
const dataToSubmit: {
room_id: number;
maintenance_type: string;
title: string;
description?: string;
scheduled_start: string;
scheduled_end?: string;
assigned_to?: number;
estimated_cost?: number;
blocks_room: boolean;
block_start?: string;
block_end?: string;
priority: string;
notes?: string;
} = {
room_id: formData.room_id,
maintenance_type: formData.maintenance_type,
title: formData.title,
@@ -637,7 +653,11 @@ const MaintenanceManagementPage: React.FC = () => {
</label>
<DatePicker
selected={formData.scheduled_start}
onChange={(date: Date) => setFormData({ ...formData, scheduled_start: date })}
onChange={(date: Date | null) => {
if (date) {
setFormData({ ...formData, scheduled_start: date });
}
}}
showTimeSelect
dateFormat="MMMM d, yyyy h:mm aa"
className="w-full px-4 py-2 border-2 border-slate-200 rounded-xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100"

View File

@@ -8,6 +8,7 @@ import Loading from '../../shared/components/Loading';
import Pagination from '../../shared/components/Pagination';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { logger } from '../../shared/utils/logger';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const PackageManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency();

View File

@@ -9,6 +9,8 @@ import ExportButton from '../../shared/components/ExportButton';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { formatDate } from '../../shared/utils/format';
import { PAYMENT_STATUS } from '../../shared/constants/bookingConstants';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
import { logger } from '../../shared/utils/logger';
const PaymentManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency();
@@ -17,6 +19,7 @@ const PaymentManagementPage: React.FC = () => {
const [filters, setFilters] = useState({
search: '',
method: '',
status: '',
from: '',
to: '',
});
@@ -388,15 +391,15 @@ const PaymentManagementPage: React.FC = () => {
<h3 className="text-lg font-semibold mb-2 text-amber-100">Total Revenue</h3>
<p className="text-4xl font-bold">
{formatCurrency(payments
.filter(p => p.payment_status === PAYMENT_STATUS.PAID || p.payment_status === 'completed')
.filter(p => p.payment_status === 'completed')
.reduce((sum, p) => sum + p.amount, 0))}
</p>
<p className="text-sm mt-3 text-amber-100/90">
Total {payments.filter(p => p.payment_status === PAYMENT_STATUS.PAID || p.payment_status === 'completed').length} paid transaction{payments.filter(p => p.payment_status === PAYMENT_STATUS.PAID || p.payment_status === 'completed').length !== 1 ? 's' : ''}
Total {payments.filter(p => p.payment_status === 'completed').length} paid transaction{payments.filter(p => p.payment_status === 'completed').length !== 1 ? 's' : ''}
</p>
</div>
<div className="bg-white/20 backdrop-blur-sm p-6 rounded-2xl">
<div className="text-5xl font-bold text-white/80">{payments.filter(p => p.payment_status === PAYMENT_STATUS.PAID || p.payment_status === 'completed').length}</div>
<div className="text-5xl font-bold text-white/80">{payments.filter(p => p.payment_status === 'completed').length}</div>
</div>
</div>
</div>

View File

@@ -40,7 +40,9 @@ import { useGlobalLoading } from '../../shared/contexts/LoadingContext';
import { normalizeImageUrl } from '../../shared/utils/imageUtils';
import { formatDate } from '../../shared/utils/format';
import { UserSession } from '../../features/auth/services/sessionService';
import { useNavigate } from 'react-router-dom';
// import { useNavigate } from 'react-router-dom'; // Unused
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
import { isAxiosError } from 'axios';
const profileValidationSchema = yup.object().shape({
name: yup
@@ -85,7 +87,7 @@ type PasswordFormData = yup.InferType<typeof passwordValidationSchema>;
const ProfilePage: React.FC = () => {
const { userInfo, setUser } = useAuthStore();
const { setLoading } = useGlobalLoading();
const _navigate = useNavigate();
// const _navigate = useNavigate(); // Unused
const [activeTab, setActiveTab] = useState<'profile' | 'password' | 'mfa' | 'sessions' | 'gdpr'>('profile');
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
const [avatarError, setAvatarError] = useState<boolean>(false);
@@ -107,8 +109,8 @@ const ProfilePage: React.FC = () => {
const [showBackupCodes, setShowBackupCodes] = useState<string[] | null>(null);
const [showMfaSecret, setShowMfaSecret] = useState<boolean>(false);
const mfaAbortControllerRef = useRef<AbortController | null>(null);
const _sessionsAbortControllerRef = useRef<AbortController | null>(null);
const _gdprAbortControllerRef = useRef<AbortController | null>(null);
// const _sessionsAbortControllerRef = useRef<AbortController | null>(null); // Reserved for future use
// const _gdprAbortControllerRef = useRef<AbortController | null>(null); // Reserved for future use
const fetchProfile = async () => {
@@ -244,8 +246,16 @@ const ProfilePage: React.FC = () => {
if (response.status === 'success' || response.success) {
const updatedUser = response.data?.user || response.data;
if (updatedUser) {
setUser(updatedUser);
if (updatedUser && typeof updatedUser === 'object' && 'id' in updatedUser) {
setUser({
id: (updatedUser as { id: number }).id,
name: ((updatedUser as { name?: string; full_name?: string }).name || (updatedUser as { name?: string; full_name?: string }).full_name || '') as string,
email: (updatedUser as { email: string }).email,
phone: ((updatedUser as { phone?: string; phone_number?: string }).phone || (updatedUser as { phone?: string; phone_number?: string }).phone_number) as string | undefined,
avatar: (updatedUser as { avatar?: string }).avatar,
role: (updatedUser as { role: string }).role,
createdAt: ((updatedUser as { createdAt?: string; created_at?: string }).createdAt || (updatedUser as { createdAt?: string; created_at?: string }).created_at) as string | undefined,
});
toast.success('Profile updated successfully!');
refetchProfile();
}
@@ -1161,7 +1171,7 @@ const SessionsTab: React.FC = () => {
fetchSessions();
}
} catch (error: unknown) {
if (getUserFriendlyError(error) === 401) {
if (isAxiosError(error) && error.response?.status === 401) {
toast.warning('Your session has been revoked. You will be logged out.');
setTimeout(() => {
window.location.href = '/';
@@ -1187,7 +1197,7 @@ const SessionsTab: React.FC = () => {
fetchSessions();
}
} catch (error: unknown) {
if (getUserFriendlyError(error) === 401) {
if (isAxiosError(error) && error.response?.status === 401) {
toast.warning('All sessions have been revoked. You will be logged out.');
setTimeout(() => {
window.location.href = '/';

View File

@@ -134,7 +134,7 @@ const PromotionManagementPage: React.FC = () => {
await promotionService.updatePromotion(editingPromotion.id, submitData);
toast.success('Promotion updated successfully');
} else {
await promotionService.createPromotion(submitData);
await promotionService.createPromotion(submitData as CreatePromotionData);
toast.success('Promotion added successfully');
}
setShowModal(false);

View File

@@ -13,8 +13,9 @@ import Loading from '../../shared/components/Loading';
import Pagination from '../../shared/components/Pagination';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { useCurrency } from '../../features/payments/contexts/CurrencyContext';
import promotionService, { Promotion } from '../../features/loyalty/services/promotionService';
import promotionService, { Promotion, CreatePromotionData } from '../../features/loyalty/services/promotionService';
import { getRoomTypes } from '../../features/rooms/services/roomService';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
interface RoomType {
id: number;
@@ -137,7 +138,7 @@ const PromotionsManagementPage: React.FC = () => {
await promotionService.updatePromotion(editingPromotion.id, submitData);
toast.success('Promotion updated successfully');
} else {
await promotionService.createPromotion(submitData);
await promotionService.createPromotion(submitData as unknown as CreatePromotionData);
toast.success('Promotion added successfully');
}
setShowPromotionModal(false);

View File

@@ -7,6 +7,7 @@ import Loading from '../../shared/components/Loading';
import Pagination from '../../shared/components/Pagination';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { logger } from '../../shared/utils/logger';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const RatePlanManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency();

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import {
LogIn,
LogOut,
@@ -26,6 +27,7 @@ import Pagination from '../../shared/components/Pagination';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { parseDateLocal } from '../../shared/utils/format';
import CreateBookingModal from '../../features/hotel_services/components/CreateBookingModal';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
type ReceptionTab = 'overview' | 'check-in' | 'check-out' | 'bookings';
@@ -43,6 +45,7 @@ interface ServiceItem {
}
const ReceptionDashboardPage: React.FC = () => {
const navigate = useNavigate();
const { formatCurrency } = useFormatCurrency();
const [activeTab, setActiveTab] = useState<ReceptionTab>('overview');
@@ -313,7 +316,7 @@ const ReceptionDashboardPage: React.FC = () => {
const handleUpdateBookingStatus = async (id: number, status: string) => {
try {
setUpdatingBookingId(id);
await bookingService.updateBooking(id, { status } as { status: string });
await bookingService.updateBooking(id, { status: status as Booking['status'] });
toast.success('Status updated successfully');
await fetchBookings();
} catch (error: unknown) {
@@ -1599,13 +1602,13 @@ const ReceptionDashboardPage: React.FC = () => {
</p>
</div>
<span className={`text-xs px-2 py-1 rounded-full font-medium ${
payment.payment_status === 'completed' || payment.payment_status === 'paid'
payment.payment_status === 'completed'
? 'bg-green-100 text-green-700'
: payment.payment_status === 'pending'
? 'bg-yellow-100 text-yellow-700'
: 'bg-red-100 text-red-700'
}`}>
{payment.payment_status === 'completed' || payment.payment_status === 'paid' ? 'Paid' : payment.payment_status || 'Pending'}
{payment.payment_status === 'completed' ? 'Paid' : payment.payment_status || 'Pending'}
</span>
</div>
{payment.transaction_id && (

View File

@@ -3,6 +3,7 @@ import reviewService, { Review } from '../../features/reviews/services/reviewSer
import { toast } from 'react-toastify';
import Loading from '../../shared/components/Loading';
import Pagination from '../../shared/components/Pagination';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const ReviewManagementPage: React.FC = () => {
const [reviews, setReviews] = useState<Review[]>([]);

View File

@@ -14,12 +14,13 @@ import {
AlertCircle,
Info
} from 'lucide-react';
import { securityService, SecurityEvent, SecurityStats, OAuthProvider, DataSubjectRequest } from '../../features/security/services/securityService';
import { securityService, SecurityEvent, SecurityStats, OAuthProvider, DataSubjectRequest, IPBlacklist } from '../../features/security/services/securityService';
import { toast } from 'react-toastify';
import Loading from '../../shared/components/Loading';
import Pagination from '../../shared/components/Pagination';
import { formatDate } from '../../shared/utils/format';
import { logger } from '../../shared/utils/logger';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
type SecurityTab = 'events' | 'stats' | 'ip-whitelist' | 'ip-blacklist' | 'oauth' | 'gdpr' | 'scan';
@@ -595,7 +596,7 @@ const IPWhitelistTab: React.FC = () => {
// IP Blacklist Tab Component
const IPBlacklistTab: React.FC = () => {
const [ips, setIPs] = useState<Array<{ id: number; ip_address: string; description?: string; created_at?: string }>>([]);
const [ips, setIPs] = useState<IPBlacklist[]>([]);
const [, setLoading] = useState(false);
const [showAddModal, setShowAddModal] = useState(false);
const [newIP, setNewIP] = useState({ ip_address: '', reason: '' });
@@ -1122,7 +1123,7 @@ const OAuthProvidersTab: React.FC = () => {
const GDPRRequestsTab: React.FC = () => {
const [requests, setRequests] = useState<DataSubjectRequest[]>([]);
const [loading, setLoading] = useState(false);
const [_selectedRequest, setSelectedRequest] = useState<DataSubjectRequest | null>(null);
const [selectedRequest, setSelectedRequest] = useState<DataSubjectRequest | null>(null);
const [filters, setFilters] = useState({
status: '',
request_type: ''
@@ -1419,7 +1420,23 @@ const GDPRRequestsTab: React.FC = () => {
// Security Scan Tab Component
const SecurityScanTab: React.FC = () => {
const [scanning, setScanning] = useState(false);
const [scanResults, setScanResults] = useState<Record<string, unknown> | null>(null);
const [scanResults, setScanResults] = useState<{
duration_seconds?: number;
critical_issues?: number;
high_issues?: number;
medium_issues?: number;
low_issues?: number;
total_issues?: number;
checks?: Array<{
check_name?: string;
name?: string;
status?: string;
severity?: string;
description?: string;
recommendation?: string;
issue_count?: number;
}>;
} | null>(null);
const [scheduleInterval, setScheduleInterval] = useState(24);
const [scheduled, setScheduled] = useState(false);
@@ -1557,7 +1574,7 @@ const SecurityScanTab: React.FC = () => {
{/* Check Results */}
<div className="space-y-3">
{scanResults.checks?.map((check: { name?: string; status?: string; message?: string; severity?: string }, index: number) => (
{scanResults.checks?.map((check: { name?: string; check_name?: string; status?: string; message?: string; severity?: string; description?: string; recommendation?: string; issue_count?: number }, index: number) => (
<div
key={index}
className={`border rounded-lg sm:rounded-xl p-3 sm:p-4 ${
@@ -1570,8 +1587,8 @@ const SecurityScanTab: React.FC = () => {
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-2 mb-2">
<h5 className="font-semibold text-sm sm:text-base">{check.check_name}</h5>
<span className={`px-2 py-1 rounded-full text-xs border flex-shrink-0 ${getSeverityColor(check.severity)}`}>
{check.severity}
<span className={`px-2 py-1 rounded-full text-xs border flex-shrink-0 ${getSeverityColor(check.severity || 'low')}`}>
{check.severity || 'low'}
</span>
<span className={`px-2 py-1 rounded-full text-xs flex-shrink-0 ${
check.status === 'failed' ? 'bg-red-100 text-red-800' :
@@ -1587,9 +1604,9 @@ const SecurityScanTab: React.FC = () => {
💡 Recommendation: {check.recommendation}
</p>
)}
{check.issue_count > 0 && (
{(check.issue_count ?? 0) > 0 && (
<p className="text-xs sm:text-sm text-gray-600 mt-2">
Issues found: {check.issue_count}
Issues found: {check.issue_count ?? 0}
</p>
)}
</div>

View File

@@ -1,12 +1,13 @@
import React, { useEffect, useState } from 'react';
import { Plus, Search, Edit, Trash2, X, Upload } from 'lucide-react';
import serviceService, { Service } from '../../features/hotel_services/services/serviceService';
import serviceService, { Service, UpdateServiceData, CreateServiceData } from '../../features/hotel_services/services/serviceService';
import { ServiceSection } from '../../features/content/pages/ServiceDetailPage';
import { toast } from 'react-toastify';
import Loading from '../../shared/components/Loading';
import Pagination from '../../shared/components/Pagination';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import ConfirmationDialog from '../../shared/components/ConfirmationDialog';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const ServiceManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency();
@@ -83,10 +84,10 @@ const ServiceManagementPage: React.FC = () => {
};
if (editingService) {
await serviceService.updateService(editingService.id, dataToSubmit);
await serviceService.updateService(editingService.id, dataToSubmit as UpdateServiceData);
toast.success('Service updated successfully');
} else {
await serviceService.createService(dataToSubmit);
await serviceService.createService(dataToSubmit as CreateServiceData);
toast.success('Service added successfully');
}
setShowModal(false);
@@ -522,7 +523,7 @@ const ServiceManagementPage: React.FC = () => {
value={section?.type || 'text'}
onChange={(e) => {
const updatedSections = [...formData.sections];
updatedSections[sectionIndex] = { ...updatedSections[sectionIndex], type: e.target.value };
updatedSections[sectionIndex] = { ...updatedSections[sectionIndex], type: e.target.value as ServiceSection['type'] };
setFormData({ ...formData, sections: updatedSections });
}}
className="px-3 py-1.5 border border-slate-300 rounded text-sm"

View File

@@ -48,6 +48,7 @@ import { recaptchaService, RecaptchaSettingsAdminResponse, UpdateRecaptchaSettin
import { useCurrency } from '../../features/payments/contexts/CurrencyContext';
import Loading from '../../shared/components/Loading';
import { getCurrencySymbol } from '../../shared/utils/format';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
type SettingsTab = 'general' | 'cookie' | 'currency' | 'payment' | 'smtp' | 'company' | 'recaptcha' | 'theme';

View File

@@ -11,6 +11,7 @@ import Loading from '../../shared/components/Loading';
import Pagination from '../../shared/components/Pagination';
import { formatDate } from '../../shared/utils/format';
import DatePicker from 'react-datepicker';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
import 'react-datepicker/dist/react-datepicker.css';
// Custom scrollbar styles - Luxury design
@@ -148,7 +149,7 @@ const StaffShiftDashboardPage: React.FC = () => {
const fetchShifts = async () => {
try {
setLoading(true);
const params: { page: number; limit: number; status?: string; staff_id?: number } = { page: currentPage, limit: itemsPerPage };
const params: { page: number; limit: number; status?: string; staff_id?: number; shift_date?: string; department?: string } = { page: currentPage, limit: itemsPerPage };
if (filters.status) params.status = filters.status;
if (filters.staff_id) params.staff_id = parseInt(filters.staff_id);
if (filters.shift_date) params.shift_date = filters.shift_date;

View File

@@ -6,6 +6,7 @@ import systemSettingsService, {
UpdateStripeSettingsRequest,
} from '../../features/system/services/systemSettingsService';
import Loading from '../../shared/components/Loading';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const StripeSettingsPage: React.FC = () => {
const [settings, setSettings] = useState<StripeSettingsResponse['data'] | null>(null);

View File

@@ -8,6 +8,7 @@ import {
Calendar,
User,
Play,
Clock,
} from 'lucide-react';
import { toast } from 'react-toastify';
import Loading from '../../shared/components/Loading';
@@ -19,6 +20,7 @@ import TaskDetailModal from '../../features/system/components/TaskDetailModal';
import CreateTaskModal from '../../features/system/components/CreateTaskModal';
import TaskFilters from '../../features/system/components/TaskFilters';
import { logger } from '../../shared/utils/logger';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
type TaskStatus = 'pending' | 'assigned' | 'in_progress' | 'completed' | 'cancelled' | 'overdue';
type TaskPriority = 'low' | 'medium' | 'high' | 'urgent';
@@ -61,8 +63,8 @@ const TaskManagementPage: React.FC = () => {
const { data: statistics, execute: fetchStatistics } = useAsync<TaskStatistics>(
async () => {
const r = await taskService.getTaskStatistics();
const responseData = r as { data?: { data?: Task[] }; status?: string };
return responseData.data?.data || (r.data as Task[]);
const responseData = r.data as { status?: string; data?: TaskStatistics };
return (responseData.data || r.data) as TaskStatistics;
},
{ immediate: true }
);

View File

@@ -10,6 +10,8 @@ import useAuthStore from '../../store/useAuthStore';
import { logger } from '../../shared/utils/logger';
import { useApiCall } from '../../shared/hooks/useApiCall';
import { useStepUpAuth } from '../../features/auth/contexts/StepUpAuthContext';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
import { isAxiosError } from 'axios';
const UserManagementPage: React.FC = () => {
const { userInfo } = useAuthStore();
@@ -23,11 +25,13 @@ const UserManagementPage: React.FC = () => {
const pendingSubmitDataRef = useRef<{ data: Record<string, unknown>; isEdit: boolean } | null>(null);
const { execute: executeSubmit, isLoading: isSubmitting } = useApiCall(
async (data: Record<string, unknown>, isEdit: boolean) => {
async (...args: unknown[]) => {
const data = args[0] as Record<string, unknown>;
const isEdit = args[1] as boolean;
if (isEdit && editingUser) {
return await userService.updateUser(editingUser.id, data);
return await userService.updateUser(editingUser.id, data as Parameters<typeof userService.updateUser>[1]);
} else {
return await userService.createUser(data);
return await userService.createUser(data as unknown as Parameters<typeof userService.createUser>[0]);
}
},
{
@@ -145,33 +149,33 @@ const UserManagementPage: React.FC = () => {
// Check if step-up authentication is required
// Check both the original response structure and the modified error from API client
const errorData = error.response?.data;
const errorObj = error as { response?: { data?: { detail?: unknown } }; requiresStepUp?: boolean; stepUpAction?: string };
const errorData = errorObj.response?.data;
const errorDetail = errorData?.detail;
// Check for step-up required in multiple ways
const isStepUpRequired =
error.requiresStepUp === true ||
error.stepUpAction !== undefined ||
(getUserFriendlyError(error) === 403 &&
(errorDetail?.error === 'step_up_required' ||
errorData?.error === 'step_up_required' ||
(typeof errorDetail === 'object' && errorDetail?.error === 'step_up_required') ||
errorObj.requiresStepUp === true ||
errorObj.stepUpAction !== undefined ||
(isAxiosError(error) && error.response?.status === 403 &&
((typeof errorDetail === 'object' && errorDetail !== null && 'error' in errorDetail && (errorDetail as { error?: string }).error === 'step_up_required') ||
(typeof errorData === 'object' && errorData !== null && 'error' in errorData && (errorData as { error?: string }).error === 'step_up_required') ||
(typeof errorDetail === 'string' && errorDetail.includes('Step-up authentication required'))));
if (isStepUpRequired) {
const actionDescription =
error.stepUpAction ||
(typeof errorDetail === 'object' ? errorDetail?.action : null) ||
errorDetail?.action ||
errorObj.stepUpAction ||
(typeof errorDetail === 'object' && errorDetail !== null && 'action' in errorDetail ? (errorDetail as { action?: string }).action : null) ||
(typeof errorDetail === 'object' && errorDetail !== null && 'action' in errorDetail ? (errorDetail as { action?: string }).action : null) ||
(typeof errorDetail === 'string' ? errorDetail : null) ||
errorDetail?.message ||
(typeof errorDetail === 'object' && errorDetail !== null && 'message' in errorDetail ? (errorDetail as { message?: string }).message : null) ||
(editingUser ? 'user update' : 'user creation');
logger.debug('Step-up required, opening modal', {
actionDescription,
error: {
requiresStepUp: error.requiresStepUp,
stepUpAction: error.stepUpAction,
requiresStepUp: errorObj.requiresStepUp,
stepUpAction: errorObj.stepUpAction,
status: getUserFriendlyError(error),
detail: errorDetail
}

View File

@@ -4,6 +4,7 @@ import webhookService, { Webhook, CreateWebhookData, UpdateWebhookData } from '.
import { toast } from 'react-toastify';
import Loading from '../../shared/components/Loading';
import { formatDate } from '../../shared/utils/format';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const WebhookManagementPage: React.FC = () => {
const [webhooks, setWebhooks] = useState<Webhook[]>([]);

View File

@@ -15,6 +15,7 @@ import { useAsync } from '../../shared/hooks/useAsync';
import workflowService, { Workflow } from '../../features/system/services/workflowService';
import WorkflowBuilder from '../../features/system/components/WorkflowBuilder';
import WorkflowDetailModal from '../../features/system/components/WorkflowDetailModal';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const WorkflowManagementPage: React.FC = () => {
const [showBuilder, setShowBuilder] = useState(false);

View File

@@ -8,7 +8,7 @@ export { default as AdminDashboardPage } from './DashboardPage';
export { default as UserManagementPage } from './UserManagementPage';
export { default as GuestProfilePage } from './GuestProfilePage';
export { default as GroupBookingManagementPage } from './GroupBookingManagementPage';
export { default as BusinessDashboardPage } from './BusinessDashboardPage';
// export { default as BusinessDashboardPage } from './BusinessDashboardPage'; // File does not exist
export { default as ReceptionDashboardPage } from './ReceptionDashboardPage';
export { default as AdvancedRoomManagementPage } from './AdvancedRoomManagementPage';
export { default as PageContentDashboardPage } from './PageContentDashboard';

View File

@@ -16,6 +16,7 @@ import {
FileText,
Building2,
AlertCircle,
XCircle,
} from 'lucide-react';
import { toast } from 'react-toastify';
import {
@@ -140,7 +141,18 @@ const BookingDetailPage: React.FC = () => {
const handleCancelSuccess = async () => {
if (booking) {
await fetchBookingDetails(booking.id);
// Refetch booking details after cancellation
const bookingId = validateBookingId(id);
if (bookingId) {
try {
const response = await getBookingById(bookingId);
if (response.success && response.data?.booking) {
setBooking(response.data.booking);
}
} catch (err) {
console.error('Error refetching booking:', err);
}
}
}
setShowCancelModal(false);
};
@@ -170,8 +182,9 @@ const BookingDetailPage: React.FC = () => {
const serviceUsages = (booking && typeof booking === 'object' && 'service_usages' in booking && Array.isArray((booking as { service_usages?: unknown[] }).service_usages)) ? (booking as { service_usages: unknown[] }).service_usages : ((booking && typeof booking === 'object' && 'services' in booking && Array.isArray((booking as { services?: unknown[] }).services)) ? (booking as { services: unknown[] }).services : []);
if (Array.isArray(serviceUsages) && serviceUsages.length > 0) {
return serviceUsages.reduce((sum: number, su: { total_price?: number }) => {
return sum + (su.total_price || 0);
return serviceUsages.reduce((sum: number, su: unknown) => {
const serviceUsage = su as { total_price?: number };
return sum + (serviceUsage.total_price || 0);
}, 0);
}
return 0;
@@ -192,7 +205,7 @@ const BookingDetailPage: React.FC = () => {
const roomPricePerNight = useMemo(() => {
if (!booking) return 0;
const roomTotal = booking.total_price - servicesTotal;
const roomTotal = booking.total_price - (servicesTotal as number);
return nights > 0 ? roomTotal / nights : roomTotal;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [booking?.total_price, servicesTotal, nights]);
@@ -476,8 +489,8 @@ const BookingDetailPage: React.FC = () => {
</p>
<p className="text-sm text-gray-700">
<span className="font-medium">Payment Type:</span>{' '}
{payment.payment_type === 'deposit' ? 'Deposit (20%)' :
payment.payment_type === 'remaining' ? 'Remaining Payment' :
{(payment as { payment_type?: string }).payment_type === 'deposit' ? 'Deposit (20%)' :
(payment as { payment_type?: string }).payment_type === 'remaining' ? 'Remaining Payment' :
'Full Payment'}
</p>
{payment.transaction_id && (
@@ -530,7 +543,7 @@ const BookingDetailPage: React.FC = () => {
{}
{serviceUsages.length > 0 && (
<>
{serviceUsages.map((serviceUsage: { id?: number; service_name?: string; name?: string; quantity?: number; price?: number; total?: number; total_price?: number }, index: number) => (
{(serviceUsages as Array<{ id?: number; service_name?: string; name?: string; quantity?: number; price?: number; unit_price?: number; total?: number; total_price?: number }>).map((serviceUsage, index: number) => (
<div key={serviceUsage.id || index} className="flex justify-between items-center">
<div>
<p className="text-sm font-medium text-gray-900">

View File

@@ -131,7 +131,7 @@ const BookingSuccessPage: React.FC = () => {
// If Stripe payment is pending, redirect to payment completion page
if (bookingData.payment_method === PAYMENT_METHOD.STRIPE && bookingData.payments) {
// Find any pending Stripe payment that needs completion
const pendingStripePayment = bookingData.payments.find(
const pendingStripePayment = (bookingData.payments as unknown as Payment[] | undefined)?.find(
(p: Payment) =>
p.payment_method === PAYMENT_METHOD.STRIPE &&
p.payment_status === 'pending' // Payment record status (not booking payment_status)
@@ -334,13 +334,13 @@ const BookingSuccessPage: React.FC = () => {
// Check individual payment records for more granular status
if (booking.payments && Array.isArray(booking.payments)) {
// Calculate total from completed payment records
const totalPaid = booking.payments
const totalPaid = ((booking.payments as unknown as Payment[] | undefined) || [])
.filter((p: Payment) => p.payment_status === 'completed') // Payment record status
.reduce((sum: number, p: Payment) => sum + parseFloat(p.amount?.toString() || '0'), 0);
.reduce((sum: number, p: Payment) => sum + (typeof p.amount === 'number' ? p.amount : parseFloat(String(p.amount || '0'))), 0);
// For deposit bookings, check if deposit payment is completed
if (booking.requires_deposit) {
const depositPayment = booking.payments.find(
const depositPayment = ((booking.payments as unknown as Payment[] | undefined))?.find(
(p: Payment) => p.payment_type === PAYMENT_TYPE.DEPOSIT && p.payment_status === 'completed'
);
if (depositPayment) {
@@ -349,7 +349,7 @@ const BookingSuccessPage: React.FC = () => {
} else {
// For full payment bookings, check if total paid meets or exceeds booking price
// Allow small floating point differences (0.01) for currency calculations
return totalPaid >= booking.total_price - 0.01;
return (totalPaid as number) >= booking.total_price - 0.01;
}
}
@@ -663,7 +663,7 @@ const BookingSuccessPage: React.FC = () => {
)}
{}
{(booking.payment_method === PAYMENT_METHOD.CASH || booking.payment_method === PAYMENT_METHOD.BANK_TRANSFER) && (
{((booking.payment_method as string) === PAYMENT_METHOD.CASH || (booking.payment_method as string) === PAYMENT_METHOD.BANK_TRANSFER) && (
<div
className="bg-blue-50 border border-blue-200
rounded-lg p-6 mb-6"
@@ -876,7 +876,7 @@ const BookingSuccessPage: React.FC = () => {
If you cancel the booking, 20% of
the total order value will be charged
</li>
{(booking.payment_method === PAYMENT_METHOD.CASH || booking.payment_method === PAYMENT_METHOD.BANK_TRANSFER) && (
{((booking.payment_method as string) === PAYMENT_METHOD.CASH || (booking.payment_method as string) === PAYMENT_METHOD.BANK_TRANSFER) && (
<li>
Please transfer within 24 hours
to secure your room

View File

@@ -3,6 +3,7 @@ import { useSearchParams, useNavigate } from 'react-router-dom';
import { confirmBoricaPayment } from '../../features/payments/services/paymentService';
import { toast } from 'react-toastify';
import { CheckCircle, XCircle, Loader2 } from 'lucide-react';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const BoricaReturnPage: React.FC = () => {
const [searchParams] = useSearchParams();

View File

@@ -5,9 +5,10 @@ import Loading from '../../shared/components/Loading';
import EmptyState from '../../shared/components/EmptyState';
import Pagination from '../../shared/components/Pagination';
import complaintService, { Complaint } from '../../features/guest_management/services/complaintService';
import bookingService from '../../features/bookings/services/bookingService';
import bookingService, { Booking } from '../../features/bookings/services/bookingService';
import { formatDate } from '../../shared/utils/format';
import { logger } from '../../shared/utils/logger';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const ComplaintPage: React.FC = () => {
const [complaints, setComplaints] = useState<Complaint[]>([]);
@@ -88,7 +89,7 @@ const ComplaintPage: React.FC = () => {
return;
}
// Silently fail - bookings are optional, but log for debugging
logger.debug('Failed to fetch bookings for complaint form', error);
logger.debug('Failed to fetch bookings for complaint form', error as Record<string, unknown> | undefined);
}
};
@@ -242,7 +243,7 @@ const ComplaintPage: React.FC = () => {
const CreateComplaintModal: React.FC<{
bookings: Booking[];
onClose: () => void;
onSubmit: (data: { booking_id?: number; category: string; priority: string; title: string; description: string; attachments?: string[] }) => void;
onSubmit: (data: { booking_id?: number; room_id?: number; category: string; priority: string; title: string; description: string; attachments?: string[] }) => void;
}> = ({ bookings, onClose, onSubmit }) => {
const [formData, setFormData] = useState({
booking_id: '',

View File

@@ -170,13 +170,13 @@ const DashboardPage: React.FC = () => {
const fetchLoyalty = async () => {
try {
setLoadingLoyalty(true);
const response = await loyaltyService.getMyLoyalty();
const response = await loyaltyService.getMyStatus();
if (response.status === 'success' && response.data) {
setLoyaltyInfo({
available_points: response.data.available_points || 0,
tier_name: response.data.tier?.name || 'Bronze',
lifetime_points: response.data.lifetime_points || 0,
next_tier_points_needed: response.data.next_tier_points_needed,
next_tier_points_needed: response.data.points_needed_for_next_tier || response.data.next_tier?.points_needed || 0,
});
}
} catch (err: unknown) {
@@ -189,7 +189,7 @@ const DashboardPage: React.FC = () => {
};
if (!isAbortError(err)) {
// Loyalty is optional, don't show error
logger.debug('Loyalty info not available', err);
logger.debug('Loyalty info not available', err as Record<string, unknown> | undefined);
}
} finally {
setLoadingLoyalty(false);

View File

@@ -180,7 +180,7 @@ const FullPaymentPage: React.FC = () => {
} else {
// Fallback to payments from booking data
if (bookingData.payments && bookingData.payments.length > 0) {
const stripePaymentFromBooking = bookingData.payments.find(
const stripePaymentFromBooking = (bookingData.payments as unknown as Payment[] | undefined)?.find(
(p: Payment) =>
(p.payment_method === PAYMENT_METHOD.STRIPE || p.payment_method === PAYMENT_METHOD.CREDIT_CARD) &&
p.payment_status === 'pending'

View File

@@ -3,6 +3,7 @@ import { useSearchParams, useNavigate, Link } from 'react-router-dom';
import { CheckCircle, XCircle, AlertCircle, Loader2, Home, Shield } from 'lucide-react';
import { toast } from 'react-toastify';
import gdprService from '../../features/compliance/services/gdprService';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const GDPRDeletionConfirmPage: React.FC = () => {
const [searchParams] = useSearchParams();

View File

@@ -4,6 +4,7 @@ import gdprService, { GDPRRequest } from '../../features/compliance/services/gdp
import { toast } from 'react-toastify';
import Loading from '../../shared/components/Loading';
import { formatDate } from '../../shared/utils/format';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const GDPRPage: React.FC = () => {
const [requests, setRequests] = useState<GDPRRequest[]>([]);

View File

@@ -7,6 +7,7 @@ import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurren
import { formatDate } from '../../shared/utils/format';
import CreateGroupBookingModal from '../../features/hotel_services/components/CreateGroupBookingModal';
import { useNavigate } from 'react-router-dom';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const GroupBookingPage: React.FC = () => {
const navigate = useNavigate();

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
// import { useNavigate } from 'react-router-dom'; // Unused
import {
Plus,
Clock,
@@ -22,10 +22,11 @@ import { logger } from '../../shared/utils/logger';
import Loading from '../../shared/components/Loading';
import EmptyState from '../../shared/components/EmptyState';
import useAuthStore from '../../store/useAuthStore';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const GuestRequestsPage: React.FC = () => {
const { userInfo: _userInfo } = useAuthStore();
const _navigate = useNavigate();
// const _navigate = useNavigate(); // Unused
const [loading, setLoading] = useState(true);
const [requests, setRequests] = useState<GuestRequest[]>([]);
const [bookings, setBookings] = useState<Booking[]>([]);
@@ -253,8 +254,10 @@ const GuestRequestsPage: React.FC = () => {
? "You need to be checked in to submit service requests. Please check in first or contact reception."
: "Submit your first request to get started"
}
actionLabel={bookings.length > 0 ? "Create Request" : undefined}
onAction={bookings.length > 0 ? () => setShowCreateModal(true) : undefined}
action={bookings.length > 0 ? {
label: "Create Request",
onClick: () => setShowCreateModal(true)
} : undefined}
/>
) : (
<div className="grid gap-6">
@@ -343,7 +346,7 @@ const GuestRequestsPage: React.FC = () => {
booking_id: e.target.value,
room_id: booking?.room_id?.toString() || '',
});
setSelectedBooking(booking);
setSelectedBooking(booking || null);
}}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]"
required

View File

@@ -8,6 +8,8 @@ import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurren
import { formatDate } from '../../shared/utils/format';
import useAuthStore from '../../store/useAuthStore';
import { validateInvoiceId } from '../../shared/utils/routeValidation';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
import { isAxiosError } from 'axios';
const InvoicePage: React.FC = () => {
const { id } = useParams<{ id: string }>();
@@ -65,7 +67,7 @@ const InvoicePage: React.FC = () => {
const invoiceData = response.data.invoice;
// Validate ownership for customer role
if (userInfo?.role === 'customer' && invoiceData.booking?.user_id !== userInfo.id) {
if (userInfo?.role === 'customer' && invoiceData.user_id !== userInfo.id) {
toast.error('You do not have permission to view this invoice');
navigate('/bookings');
setLoading(false);
@@ -110,7 +112,7 @@ const InvoicePage: React.FC = () => {
// If it's a 404 and we haven't retried yet, retry multiple times with increasing delays
// This handles cases where invoice was just created and might not be immediately available
if (getUserFriendlyError(error) === 404 && retryCount < 3) {
if (isAxiosError(error) && error.response?.status === 404 && retryCount < 3) {
// Wait with increasing delay (500ms, 1000ms, 2000ms)
const delay = 500 * Math.pow(2, retryCount);
setTimeout(() => {
@@ -120,7 +122,7 @@ const InvoicePage: React.FC = () => {
}
// Handle invoice not found (404) after retries - show appropriate message
if (getUserFriendlyError(error) === 404) {
if (isAxiosError(error) && error.response?.status === 404) {
handleInvoiceNotFound();
} else {
// Other errors (network, server errors, etc.) - only show toast if not a validation error
@@ -187,7 +189,7 @@ const InvoicePage: React.FC = () => {
return <Loading fullScreen text="Loading invoice..." />;
}
if (!invoice && !loading) {
if (!invoice) {
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-4xl mx-auto px-4">
@@ -222,6 +224,18 @@ const InvoicePage: React.FC = () => {
);
}
if (!invoice) {
return (
<div className="min-h-screen bg-gray-50 py-8 print:bg-white">
<div className="max-w-4xl mx-auto px-4">
<div className="bg-white rounded-lg shadow-lg p-8 text-center">
<p className="text-gray-600">Loading invoice...</p>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 py-8 print:bg-white">
<div className="max-w-4xl mx-auto px-4">

View File

@@ -22,6 +22,8 @@ import loyaltyService, {
Referral
} from '../../features/loyalty/services/loyaltyService';
import { formatDate } from '../../shared/utils/format';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
import { isAxiosError } from 'axios';
type Tab = 'overview' | 'rewards' | 'history' | 'referrals';
@@ -100,16 +102,25 @@ const LoyaltyPage: React.FC = () => {
setAnniversaryDate(response.data.anniversary_date);
}
} catch (error: unknown) {
const isAbortError = (e: unknown): boolean => {
return typeof e === 'object' && e !== null && (e as { name?: string; code?: string }).name === 'AbortError';
return (e as { name?: string; code?: string }).code === 'ERR_CANCELED';
const isAbortErrorFunc = (e: unknown): boolean => {
if (typeof e === 'object' && e !== null) {
const err = e as { name?: string; code?: string };
return err.name === 'AbortError' || err.code === 'ERR_CANCELED';
}
return false;
};
if (isAbortErrorFunc(error)) {
return;
}
// Check if the error is about loyalty program being disabled
// FastAPI returns detail in error.response.data.detail
// The apiClient might transform it, so check both locations
const statusCode = getUserFriendlyError(error) || error.status;
const errorData = error.response?.data || {};
const errorDetail = errorData.detail || errorData.message || '';
const errorObj = error as { status?: number; response?: { data?: { detail?: unknown; message?: unknown } } };
const statusCode = isAxiosError(error) ? error.response?.status : (errorObj.status || getUserFriendlyError(error));
const errorData = errorObj.response?.data || {};
const errorDetail = (typeof errorData === 'object' && errorData !== null && 'detail' in errorData ? errorData.detail : null) || (typeof errorData === 'object' && errorData !== null && 'message' in errorData ? errorData.message : null);
const errorMessage = getUserFriendlyError(error) || '';
// Check if it's a 503 error (service unavailable) which indicates disabled

View File

@@ -23,6 +23,7 @@ import EmptyState from '../../shared/components/EmptyState';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { parseDateLocal } from '../../shared/utils/format';
import { getBookingStatusConfig, canCancelBooking } from '../../shared/utils/bookingUtils';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const MyBookingsPage: React.FC = () => {
const { isAuthenticated } = useAuthStore();

View File

@@ -30,6 +30,7 @@ import { validateBookingId } from '../../shared/utils/routeValidation';
import { validateAndHandleBookingOwnership } from '../../shared/utils/ownershipValidation';
import { PAYMENT_METHOD, PAYMENT_STATUS } from '../../shared/constants/bookingConstants';
import { useCompanySettings } from '../../shared/contexts/CompanySettingsContext';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const PaymentConfirmationPage: React.FC = () => {
const { id } = useParams<{ id: string }>();

View File

@@ -24,7 +24,9 @@ import {
Trash2,
Database,
Smartphone,
Tablet
Tablet,
Eye,
EyeOff
} from 'lucide-react';
import { toast } from 'react-toastify';
import authService from '../../features/auth/services/authService';
@@ -38,7 +40,9 @@ import { useGlobalLoading } from '../../shared/contexts/LoadingContext';
import { normalizeImageUrl } from '../../shared/utils/imageUtils';
import { formatDate } from '../../shared/utils/format';
import { UserSession } from '../../features/auth/services/sessionService';
import { useNavigate } from 'react-router-dom';
// import { useNavigate } from 'react-router-dom'; // Unused
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
import { isAxiosError } from 'axios';
const profileValidationSchema = yup.object().shape({
name: yup
@@ -83,7 +87,7 @@ type PasswordFormData = yup.InferType<typeof passwordValidationSchema>;
const ProfilePage: React.FC = () => {
const { userInfo, setUser } = useAuthStore();
const { setLoading } = useGlobalLoading();
const _navigate = useNavigate();
// const _navigate = useNavigate(); // Unused
const [activeTab, setActiveTab] = useState<'profile' | 'password' | 'mfa' | 'sessions' | 'gdpr'>('profile');
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
const [avatarError, setAvatarError] = useState<boolean>(false);
@@ -105,8 +109,8 @@ const ProfilePage: React.FC = () => {
const [showBackupCodes, setShowBackupCodes] = useState<string[] | null>(null);
const [showMfaSecret, setShowMfaSecret] = useState<boolean>(false);
const mfaAbortControllerRef = useRef<AbortController | null>(null);
const _sessionsAbortControllerRef = useRef<AbortController | null>(null);
const _gdprAbortControllerRef = useRef<AbortController | null>(null);
// const _sessionsAbortControllerRef = useRef<AbortController | null>(null); // Unused
// const _gdprAbortControllerRef = useRef<AbortController | null>(null); // Unused
const fetchProfile = async () => {
@@ -241,8 +245,16 @@ const ProfilePage: React.FC = () => {
if (response.status === 'success' || response.success) {
const updatedUser = response.data?.user || response.data;
if (updatedUser) {
setUser(updatedUser);
if (updatedUser && typeof updatedUser === 'object' && 'id' in updatedUser) {
setUser({
id: (updatedUser as { id: number }).id,
name: ((updatedUser as { name?: string; full_name?: string }).name || (updatedUser as { name?: string; full_name?: string }).full_name || '') as string,
email: (updatedUser as { email: string }).email,
phone: ((updatedUser as { phone?: string; phone_number?: string }).phone || (updatedUser as { phone?: string; phone_number?: string }).phone_number) as string | undefined,
avatar: (updatedUser as { avatar?: string }).avatar,
role: (updatedUser as { role: string }).role,
createdAt: ((updatedUser as { createdAt?: string; created_at?: string }).createdAt || (updatedUser as { createdAt?: string; created_at?: string }).created_at) as string | undefined,
});
toast.success('Profile updated successfully!');
refetchProfile();
}
@@ -1158,7 +1170,7 @@ const SessionsTab: React.FC = () => {
fetchSessions();
}
} catch (error: unknown) {
if (getUserFriendlyError(error) === 401) {
if (isAxiosError(error) && error.response?.status === 401) {
toast.warning('Your session has been revoked. You will be logged out.');
setTimeout(() => {
window.location.href = '/';
@@ -1184,7 +1196,7 @@ const SessionsTab: React.FC = () => {
fetchSessions();
}
} catch (error: unknown) {
if (getUserFriendlyError(error) === 401) {
if (isAxiosError(error) && error.response?.status === 401) {
toast.warning('All sessions have been revoked. You will be logged out.');
setTimeout(() => {
window.location.href = '/';

Some files were not shown because too many files have changed in this diff Show More