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) => { allElements.forEach((el) => {
const htmlEl = el as HTMLElement; const htmlEl = el as HTMLElement;
const tagName = htmlEl.tagName.toLowerCase(); 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 // Override inline colors to use theme-aware colors
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) { 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) => { const getIconComponent = (iconName?: string, fallback: React.ComponentType<{ className?: string }> = Mail) => {
if (!iconName) return fallback; 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) // Try direct match first (for PascalCase names)
if (icons[iconName]) { if (icons[iconName]) {

View File

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

View File

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

View File

@@ -3,14 +3,14 @@ import { useParams, Link, useNavigate } from 'react-router-dom';
import { ArrowLeft, Share2, Tag, Star } from 'lucide-react'; import { ArrowLeft, Share2, Tag, Star } from 'lucide-react';
import * as LucideIcons from 'lucide-react'; import * as LucideIcons from 'lucide-react';
import pageContentService, { PageContent } from '../services/pageContentService'; 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 Loading from '../../../shared/components/Loading';
import { createSanitizedHtml } from '../../../shared/utils/htmlSanitizer'; import { createSanitizedHtml } from '../../../shared/utils/htmlSanitizer';
import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency'; import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency';
import { useTheme } from '../../../shared/contexts/ThemeContext'; import { useTheme } from '../../../shared/contexts/ThemeContext';
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils'; import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
interface ServiceSection { export interface ServiceSection {
type: 'hero' | 'text' | 'image' | 'gallery' | 'quote' | 'features' | 'cta' | 'video'; type: 'hero' | 'text' | 'image' | 'gallery' | 'quote' | 'features' | 'cta' | 'video';
title?: string; title?: string;
content?: string; content?: string;
@@ -60,6 +60,14 @@ const ServiceDetailPage: React.FC = () => {
const textClasses = getThemeTextClasses(theme.theme_layout_mode); const textClasses = getThemeTextClasses(theme.theme_layout_mode);
const cardClasses = getThemeCardClasses(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(() => { useEffect(() => {
if (slug) { if (slug) {
fetchService(); fetchService();
@@ -216,7 +224,7 @@ const ServiceDetailPage: React.FC = () => {
}); });
if (servicesResponse.success && servicesResponse.data?.services) { 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 // Skip current service
if (currentService.type === 'hotel' && service.id === currentService.id) { if (currentService.type === 'hotel' && service.id === currentService.id) {
return; return;
@@ -229,7 +237,7 @@ const ServiceDetailPage: React.FC = () => {
const serviceSlug = service.slug || service.name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''); const serviceSlug = service.slug || service.name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
services.push({ services.push({
id: service.id, id: service.id ?? `service-${Date.now()}`,
title: service.name, title: service.name,
slug: serviceSlug, slug: serviceSlug,
description: service.description, description: service.description,
@@ -243,17 +251,17 @@ const ServiceDetailPage: React.FC = () => {
// Add luxury services as fallback // Add luxury services as fallback
if (pageContent?.luxury_services && Array.isArray(pageContent.luxury_services)) { 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 (s.slug && s.slug !== currentService.slug) {
if (!currentService.category || s.category === currentService.category) { if (!currentService.category || s.category === currentService.category) {
// Check if already in services (by slug) // 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); const exists = services.some(serv => serv.slug === serviceSlug);
if (!exists) { if (!exists) {
services.push({ services.push({
id: `luxury-${index}`, id: `luxury-${index}`,
title: s.title || 'Service', title: s.title || s.name || 'Service',
slug: s.slug, slug: s.slug || serviceSlug,
description: s.description, description: s.description,
image: s.image, image: s.image,
icon: s.icon, icon: s.icon,
@@ -412,10 +420,10 @@ const ServiceDetailPage: React.FC = () => {
)} )}
<div className="flex items-start gap-6 mb-6"> <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="relative flex-shrink-0">
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 rounded-full blur-3xl"></div> <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' className: 'w-20 h-20 sm:w-24 sm:h-24 text-[var(--luxury-gold)] relative z-10 drop-shadow-2xl'
})} })}
</div> </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"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{section.features.map((feature, featIndex) => { {section.features.map((feature, featIndex) => {
const IconComponent = feature.icon && (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[feature.icon] const IconComponent = feature.icon ? getIconComponent(feature.icon) : null;
? (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[feature.icon]
: null;
return ( 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 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> <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) => { const getIconComponent = (iconName?: string, fallback: React.ComponentType<{ className?: string }> = Award) => {
if (!iconName) return fallback; if (!iconName) return fallback;
const icons = LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }> | undefined>;
// Try direct match first (for PascalCase names) // Try direct match first (for PascalCase names)
if ((LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[iconName]) { if (icons[iconName]) {
return (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[iconName]; return icons[iconName];
} }
// Convert to PascalCase (capitalize first letter) // Convert to PascalCase (capitalize first letter)
const pascalCaseName = iconName.charAt(0).toUpperCase() + iconName.slice(1).toLowerCase(); const pascalCaseName = iconName.charAt(0).toUpperCase() + iconName.slice(1).toLowerCase();
if ((LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[pascalCaseName]) { if (icons[pascalCaseName]) {
return (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[pascalCaseName]; return icons[pascalCaseName];
} }
return fallback; return fallback;
@@ -67,7 +69,7 @@ const ServicesPage: React.FC = () => {
// Extract categories from luxury services // Extract categories from luxury services
const categories = new Set<string>(); const categories = new Set<string>();
if (Array.isArray(content.luxury_services)) { 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) { if (service.category) {
categories.add(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) // Add luxury services from page content (only if not already in hotel services)
if (pageContent?.luxury_services && Array.isArray(pageContent.luxury_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 // 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 existsInHotel = hotelServices.some((hs: Service) => {
const hotelSlug = hs.slug || hs.name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''); const hotelSlug = hs.slug || hs.name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
return hotelSlug === existingSlug; return hotelSlug === existingSlug;
@@ -145,13 +147,13 @@ const ServicesPage: React.FC = () => {
if (!existsInHotel) { if (!existsInHotel) {
services.push({ services.push({
id: `luxury-${index}`, id: `luxury-${index}`,
title: service.title || 'Service', title: service.title || service.name || 'Service',
description: service.description || '', description: service.description || '',
image: service.image, image: service.image,
icon: service.icon, icon: service.icon,
category: service.category, category: service.category,
type: 'luxury', type: 'luxury',
slug: service.slug, slug: service.slug || existingSlug,
}); });
} }
}); });
@@ -351,10 +353,10 @@ const ServicesPage: React.FC = () => {
</div> </div>
) : ( ) : (
<div className={`h-48 sm:h-56 ${cardClasses} flex items-center justify-center p-8`}> <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="relative">
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 rounded-full blur-2xl"></div> <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' className: 'w-16 h-16 sm:w-20 sm:h-20 text-[var(--luxury-gold)] relative z-10 drop-shadow-lg'
})} })}
</div> </div>

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import roomService, { Room } from '../../rooms/services/roomService';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency'; import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency';
import DatePicker from 'react-datepicker'; import DatePicker from 'react-datepicker';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
import 'react-datepicker/dist/react-datepicker.css'; import 'react-datepicker/dist/react-datepicker.css';
interface RoomBlock { 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 userService, { User as UserType } from '../../auth/services/userService';
import useAuthStore from '../../../store/useAuthStore'; import useAuthStore from '../../../store/useAuthStore';
import DatePicker from 'react-datepicker'; import DatePicker from 'react-datepicker';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
import 'react-datepicker/dist/react-datepicker.css'; import 'react-datepicker/dist/react-datepicker.css';
const HousekeepingManagement: React.FC = () => { const HousekeepingManagement: React.FC = () => {
@@ -82,7 +83,15 @@ const HousekeepingManagement: React.FC = () => {
const fetchTasks = async () => { const fetchTasks = async () => {
try { try {
setLoading(true); 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, page: currentPage,
limit: 10, limit: 10,
include_cleaning_rooms: true // Include rooms in cleaning status include_cleaning_rooms: true // Include rooms in cleaning status
@@ -173,7 +182,7 @@ const HousekeepingManagement: React.FC = () => {
const items = defaultChecklistItems[type] || []; const items = defaultChecklistItems[type] || [];
setFormData({ setFormData({
...formData, ...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: '' })), 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 userService, { User as UserType } from '../../auth/services/userService';
import useAuthStore from '../../../store/useAuthStore'; import useAuthStore from '../../../store/useAuthStore';
import DatePicker from 'react-datepicker'; import DatePicker from 'react-datepicker';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
import 'react-datepicker/dist/react-datepicker.css'; import 'react-datepicker/dist/react-datepicker.css';
const InspectionManagement: React.FC = () => { const InspectionManagement: React.FC = () => {
@@ -88,7 +89,7 @@ const InspectionManagement: React.FC = () => {
const fetchInspections = async () => { const fetchInspections = async () => {
try { try {
setLoading(true); 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.room_id) params.room_id = parseInt(filters.room_id);
if (filters.inspection_type) params.inspection_type = filters.inspection_type; if (filters.inspection_type) params.inspection_type = filters.inspection_type;
if (filters.status) params.status = filters.status; if (filters.status) params.status = filters.status;
@@ -480,7 +481,7 @@ const InspectionManagement: React.FC = () => {
<select <select
required required
value={formData.inspection_type} 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" 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> <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 userService, { User as UserType } from '../../auth/services/userService';
import useAuthStore from '../../../store/useAuthStore'; import useAuthStore from '../../../store/useAuthStore';
import DatePicker from 'react-datepicker'; import DatePicker from 'react-datepicker';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
import 'react-datepicker/dist/react-datepicker.css'; import 'react-datepicker/dist/react-datepicker.css';
const MaintenanceManagement: React.FC = () => { const MaintenanceManagement: React.FC = () => {
@@ -61,7 +62,7 @@ const MaintenanceManagement: React.FC = () => {
const fetchMaintenanceRecords = async () => { const fetchMaintenanceRecords = async () => {
try { try {
setLoading(true); 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.room_id) params.room_id = parseInt(filters.room_id);
if (filters.status) params.status = filters.status; if (filters.status) params.status = filters.status;
if (filters.maintenance_type) params.maintenance_type = filters.maintenance_type; if (filters.maintenance_type) params.maintenance_type = filters.maintenance_type;
@@ -417,7 +418,7 @@ const MaintenanceManagement: React.FC = () => {
<select <select
required required
value={formData.maintenance_type} 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" 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> <option value="preventive">Preventive</option>

View File

@@ -153,7 +153,7 @@ const loyaltyService = {
limit: number = 20, limit: number = 20,
transactionType?: string transactionType?: string
): Promise<PointsHistoryResponse> => { ): Promise<PointsHistoryResponse> => {
const params: { page: number; limit: number } = { page, limit }; const params: { page: number; limit: number; transaction_type?: string } = { page, limit };
if (transactionType) { if (transactionType) {
params.transaction_type = 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 (search) params.search = search;
if (tierId) params.tier_id = tierId; if (tierId) params.tier_id = tierId;
const response = await apiClient.get('/api/loyalty/admin/users', { params }); 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 { toast } from 'react-toastify';
import ConfirmationDialog from '../../../shared/components/ConfirmationDialog'; import ConfirmationDialog from '../../../shared/components/ConfirmationDialog';
import { formatWorkingHours } from '../../../shared/utils/format'; import { formatWorkingHours } from '../../../shared/utils/format';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
interface ChatWidgetProps { interface ChatWidgetProps {
onClose?: () => void; onClose?: () => void;

View File

@@ -4,6 +4,7 @@ import { toast } from 'react-toastify';
import notificationService, { Notification } from '../services/notificationService'; import notificationService, { Notification } from '../services/notificationService';
import { formatDate } from '../../../shared/utils/format'; import { formatDate } from '../../../shared/utils/format';
import useAuthStore from '../../../store/useAuthStore'; import useAuthStore from '../../../store/useAuthStore';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
const InAppNotificationBell: React.FC = () => { const InAppNotificationBell: React.FC = () => {
const { isAuthenticated, token, isLoading } = useAuthStore(); const { isAuthenticated, token, isLoading } = useAuthStore();
@@ -108,7 +109,7 @@ const InAppNotificationBell: React.FC = () => {
try { try {
await notificationService.markAsRead(notificationId); await notificationService.markAsRead(notificationId);
setNotifications(notifications.map(n => 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)); setUnreadCount(Math.max(0, unreadCount - 1));
} catch (error: unknown) { } catch (error: unknown) {
@@ -121,7 +122,7 @@ const InAppNotificationBell: React.FC = () => {
setLoading(true); setLoading(true);
const unread = notifications.filter(n => !n.read_at); const unread = notifications.filter(n => !n.read_at);
await Promise.all(unread.map(n => notificationService.markAsRead(n.id))); 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); setUnreadCount(0);
} catch (error: unknown) { } catch (error: unknown) {
toast.error(getUserFriendlyError(error) || 'Failed to mark all as read'); 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 { toast } from 'react-toastify';
import Loading from '../../../shared/components/Loading'; import Loading from '../../../shared/components/Loading';
import notificationService, { NotificationPreferences as NotificationPreferencesType } from '../services/notificationService'; import notificationService, { NotificationPreferences as NotificationPreferencesType } from '../services/notificationService';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
const NotificationPreferences: React.FC = () => { const NotificationPreferences: React.FC = () => {
const [preferences, setPreferences] = useState<NotificationPreferencesType | null>(null); 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 { X, Plus } from 'lucide-react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import notificationService, { NotificationTemplate } from '../services/notificationService'; import notificationService, { NotificationTemplate } from '../services/notificationService';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
interface NotificationTemplatesModalProps { interface NotificationTemplatesModalProps {
onClose: () => void; onClose: () => void;

View File

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

View File

@@ -3,6 +3,7 @@ import { createBoricaPayment } from '../services/paymentService';
import { X, Loader2, AlertCircle, CreditCard } from 'lucide-react'; import { X, Loader2, AlertCircle, CreditCard } from 'lucide-react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useFormatCurrency } from '../hooks/useFormatCurrency'; import { useFormatCurrency } from '../hooks/useFormatCurrency';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
interface BoricaPaymentModalProps { interface BoricaPaymentModalProps {
isOpen: boolean; isOpen: boolean;
@@ -75,7 +76,7 @@ const BoricaPaymentModal: React.FC<BoricaPaymentModalProps> = ({
// Create a form and submit it to Borica gateway // Create a form and submit it to Borica gateway
const form = document.createElement('form'); const form = document.createElement('form');
form.method = 'POST'; form.method = 'POST';
form.action = paymentRequest.gateway_url; form.action = (paymentRequest.gateway_url as string) || '';
form.style.display = 'none'; form.style.display = 'none';
// Add all form fields // 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="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"> <div className="flex justify-between items-center text-xs sm:text-sm">
<span className="text-gray-400">Order ID:</span> <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>
<div className="flex justify-between items-center text-xs sm:text-sm"> <div className="flex justify-between items-center text-xs sm:text-sm">
<span className="text-gray-400">Amount:</span> <span className="text-gray-400">Amount:</span>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,8 @@ import roomService, { Room } from '../services/roomService';
import advancedRoomService, { RoomStatusBoardItem } from '../services/advancedRoomService'; import advancedRoomService, { RoomStatusBoardItem } from '../services/advancedRoomService';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { logger } from '../../../shared/utils/logger'; import { logger } from '../../../shared/utils/logger';
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
import { isAxiosError } from 'axios';
interface RoomContextType { interface RoomContextType {
// Room list state // 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 // 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 setStatusBoardError(null); // Don't set error for unauthorized access
setStatusBoardRooms([]); // Clear status board if unauthorized setStatusBoardRooms([]); // Clear status board if unauthorized
return; // Silently return without logging return; // Silently return without logging

View File

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

View File

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

View File

@@ -91,7 +91,7 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
continue; 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) // Only include if it's a function (React component)
if (typeof iconComponent === 'function') { if (typeof iconComponent === 'function') {
@@ -127,7 +127,7 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
); );
}, [searchQuery, allIcons]); }, [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) => { const handleIconSelect = (iconName: string) => {
onChange(iconName); 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"> <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) => { {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; if (!IconComponent) return null;
const isSelected = normalizedValue === iconName; const isSelected = normalizedValue === iconName;

View File

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

View File

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

View File

@@ -61,6 +61,7 @@ import analyticsService, {
import { SimpleBarChart, SimpleLineChart, SimplePieChart, KPICard } from '../../features/analytics/components/SimpleChart'; import { SimpleBarChart, SimpleLineChart, SimplePieChart, KPICard } from '../../features/analytics/components/SimpleChart';
import { exportData } from '../../shared/utils/exportUtils'; import { exportData } from '../../shared/utils/exportUtils';
import CustomReportBuilder from '../../features/analytics/components/CustomReportBuilder'; import CustomReportBuilder from '../../features/analytics/components/CustomReportBuilder';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
type AnalyticsTab = 'overview' | 'reports' | 'revenue' | 'operational' | 'guest' | 'financial' | 'audit-logs' | 'reviews'; 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 EmptyState from '../../shared/components/EmptyState';
import { formatDate } from '../../shared/utils/format'; import { formatDate } from '../../shared/utils/format';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency'; import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const ApprovalManagementPage: React.FC = () => { const ApprovalManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency(); const { formatCurrency } = useFormatCurrency();
@@ -23,7 +24,7 @@ const ApprovalManagementPage: React.FC = () => {
const fetchApprovals = async () => { const fetchApprovals = async () => {
try { try {
setLoading(true); setLoading(true);
const params: { status?: string } = {}; const params: { status?: ApprovalStatus } = {};
if (filter !== 'all') params.status = filter; if (filter !== 'all') params.status = filter;
const response = await approvalService.getApprovals(params); const response = await approvalService.getApprovals(params);
setApprovals(response.data || []); setApprovals(response.data || []);
@@ -225,19 +226,29 @@ const ApprovalManagementPage: React.FC = () => {
<p className="text-lg font-bold text-emerald-600">{formatCurrency(selectedApproval.amount)}</p> <p className="text-lg font-bold text-emerald-600">{formatCurrency(selectedApproval.amount)}</p>
</div> </div>
)} )}
{selectedApproval.previous_value && ( {selectedApproval.previous_value != null && (
<div> <div>
<h3 className="font-semibold mb-2">Previous Value</h3> <h3 className="font-semibold mb-2">Previous Value</h3>
<pre className="bg-slate-50 p-3 rounded text-sm overflow-x-auto"> <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> </pre>
</div> </div>
)} )}
{selectedApproval.new_value && ( {selectedApproval.new_value != null && (
<div> <div>
<h3 className="font-semibold mb-2">New Value</h3> <h3 className="font-semibold mb-2">New Value</h3>
<pre className="bg-slate-50 p-3 rounded text-sm overflow-x-auto"> <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> </pre>
</div> </div>
)} )}

View File

@@ -1,17 +1,18 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Download, Filter } from 'lucide-react'; import { Download, Filter } from 'lucide-react';
import { toast } from 'react-toastify'; 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 Loading from '../../shared/components/Loading';
import EmptyState from '../../shared/components/EmptyState'; import EmptyState from '../../shared/components/EmptyState';
import { formatDate } from '../../shared/utils/format'; import { formatDate } from '../../shared/utils/format';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency'; import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const AuditTrailPage: React.FC = () => { const AuditTrailPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency(); const { formatCurrency } = useFormatCurrency();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [records, setRecords] = useState<FinancialAuditRecord[]>([]); 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({ const [filters, setFilters] = useState({
action_type: '', action_type: '',
user_id: '', user_id: '',
@@ -33,7 +34,18 @@ const AuditTrailPage: React.FC = () => {
const fetchAuditTrail = async () => { const fetchAuditTrail = async () => {
try { try {
setLoading(true); 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) { if (response.status === 'success' && response.data) {
setRecords(response.data.audit_trail || []); setRecords(response.data.audit_trail || []);
setPagination(response.data.pagination); setPagination(response.data.pagination);
@@ -47,7 +59,18 @@ const AuditTrailPage: React.FC = () => {
const handleExport = async (format: 'csv' | 'json') => { const handleExport = async (format: 'csv' | 'json') => {
try { 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 url = window.URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -62,6 +62,7 @@ import analyticsService, {
import { SimpleBarChart, SimpleLineChart, SimplePieChart, KPICard } from '../../features/analytics/components/SimpleChart'; import { SimpleBarChart, SimpleLineChart, SimplePieChart, KPICard } from '../../features/analytics/components/SimpleChart';
import { exportData } from '../../shared/utils/exportUtils'; import { exportData } from '../../shared/utils/exportUtils';
import CustomReportBuilder from '../../features/analytics/components/CustomReportBuilder'; import CustomReportBuilder from '../../features/analytics/components/CustomReportBuilder';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
type AnalyticsTab = 'overview' | 'reports' | 'revenue' | 'operational' | 'guest' | 'financial' | 'audit-logs'; 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> <label className="block text-sm font-semibold text-gray-900">Report Type</label>
<select <select
value={reportType} 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" 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> <option value="">All</option>

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import Pagination from '../../shared/components/Pagination';
import ConfirmationDialog from '../../shared/components/ConfirmationDialog'; import ConfirmationDialog from '../../shared/components/ConfirmationDialog';
import bannerServiceModule from '../../features/content/services/bannerService'; import bannerServiceModule from '../../features/content/services/bannerService';
import type { Banner } from '../../features/content/services/bannerService'; import type { Banner } from '../../features/content/services/bannerService';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const { const {
getAllBanners, getAllBanners,
@@ -113,7 +114,7 @@ const BannerManagementPage: React.FC = () => {
try { try {
setUploadingImage(true); setUploadingImage(true);
const response = await uploadBannerImage(file); 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 }); setFormData({ ...formData, image_url: response.data.image_url });
toast.success('Image uploaded successfully'); toast.success('Image uploaded successfully');
} }
@@ -141,7 +142,7 @@ const BannerManagementPage: React.FC = () => {
if (imageFile && !imageUrl) { if (imageFile && !imageUrl) {
setUploadingImage(true); setUploadingImage(true);
const uploadResponse = await uploadBannerImage(imageFile); 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; imageUrl = uploadResponse.data.image_url;
} else { } else {
throw new Error('Failed to upload image'); 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 Pagination from '../../shared/components/Pagination';
import ConfirmationDialog from '../../shared/components/ConfirmationDialog'; import ConfirmationDialog from '../../shared/components/ConfirmationDialog';
import blogService, { BlogPost, BlogPostCreate, BlogPostUpdate, BlogSection } from '../../features/content/services/blogService'; import blogService, { BlogPost, BlogPostCreate, BlogPostUpdate, BlogSection } from '../../features/content/services/blogService';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const BlogManagementPage: React.FC = () => { const BlogManagementPage: React.FC = () => {
const [posts, setPosts] = useState<BlogPost[]>([]); 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 { useNavigate, useSearchParams } from 'react-router-dom';
import CreateBookingModal from '../../features/hotel_services/components/CreateBookingModal'; import CreateBookingModal from '../../features/hotel_services/components/CreateBookingModal';
import { logger } from '../../shared/utils/logger'; import { logger } from '../../shared/utils/logger';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const BookingManagementPage: React.FC = () => { const BookingManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency(); const { formatCurrency } = useFormatCurrency();
@@ -158,7 +159,7 @@ const BookingManagementPage: React.FC = () => {
const handleUpdateStatus = async (id: number, status: string) => { const handleUpdateStatus = async (id: number, status: string) => {
try { try {
setUpdatingBookingId(id); setUpdatingBookingId(id);
await bookingService.updateBooking(id, { status } as { status: string }); await bookingService.updateBooking(id, { status: status as Booking['status'] });
toast.success('Status updated successfully'); toast.success('Status updated successfully');
await fetchBookings(); await fetchBookings();
} catch (error: unknown) { } catch (error: unknown) {
@@ -203,15 +204,15 @@ const BookingManagementPage: React.FC = () => {
logger.info('Invoice creation response', { response }); logger.info('Invoice creation response', { response });
// Check response structure - handle different possible formats // Check response structure - handle different possible formats
let invoice = null; let invoice: { id?: number | string } | null = null;
if (response.status === 'success' && response.data) { if (response.status === 'success' && response.data) {
// Try different possible response structures // Try different possible response structures
const responseData = response.data as { invoice?: unknown; data?: { invoice?: unknown } }; const responseData = response.data as { invoice?: { id?: number | string }; data?: { invoice?: { id?: number | string } } };
invoice = responseData.invoice || responseData.data?.invoice || response.data; invoice = (responseData.invoice || responseData.data?.invoice || response.data) as { id?: number | string } | null;
logger.debug('Extracted invoice', { invoice }); logger.debug('Extracted invoice', { invoice });
} }
if (!invoice) { if (!invoice || !invoice.id) {
logger.error('Failed to create invoice - no invoice in response', { response }); logger.error('Failed to create invoice - no invoice in response', { response });
toast.error(response.message || 'Failed to create invoice - no invoice data received'); toast.error(response.message || 'Failed to create invoice - no invoice data received');
return; return;
@@ -770,7 +771,7 @@ const BookingManagementPage: React.FC = () => {
Additional Services Additional Services
</label> </label>
<div className="space-y-2"> <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 key={service.id || idx} className="flex justify-between items-center py-2 border-b border-purple-100 last:border-0">
<div> <div>
<p className="text-sm font-medium text-slate-900">{service.service_name || service.name || 'Service'}</p> <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> </p>
</div> </div>
<span className={`text-xs px-2 py-1 rounded-full font-medium ${ <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' ? 'bg-green-100 text-green-700'
: payment.payment_status === 'pending' : payment.payment_status === 'pending'
? 'bg-yellow-100 text-yellow-700' ? 'bg-yellow-100 text-yellow-700'
: 'bg-red-100 text-red-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> </span>
</div> </div>
{payment.transaction_id && ( {payment.transaction_id && (

View File

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

View File

@@ -9,6 +9,7 @@ import EmptyState from '../../shared/components/EmptyState';
import Pagination from '../../shared/components/Pagination'; import Pagination from '../../shared/components/Pagination';
import complaintService, { Complaint, ComplaintFilters } from '../../features/guest_management/services/complaintService'; import complaintService, { Complaint, ComplaintFilters } from '../../features/guest_management/services/complaintService';
import { formatDate } from '../../shared/utils/format'; import { formatDate } from '../../shared/utils/format';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const ComplaintManagementPage: React.FC = () => { const ComplaintManagementPage: React.FC = () => {
const [complaints, setComplaints] = useState<Complaint[]>([]); const [complaints, setComplaints] = useState<Complaint[]>([]);
@@ -92,7 +93,7 @@ const ComplaintManagementPage: React.FC = () => {
toast.success('Complaint status updated'); toast.success('Complaint status updated');
fetchComplaints(); fetchComplaints();
if (selectedComplaint?.id === complaintId) { 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) { } catch (error: unknown) {
toast.error(getUserFriendlyError(error) || 'Unable to update complaint'); toast.error(getUserFriendlyError(error) || 'Unable to update complaint');
@@ -305,7 +306,7 @@ const ComplaintDetailModal: React.FC<{
onClose: () => void; onClose: () => void;
onResolve: () => void; onResolve: () => void;
onUpdateStatus: (id: number, status: string) => void; onUpdateStatus: (id: number, status: string) => void;
}> = ({ complaint, onClose, onResolve, _onUpdateStatus }) => { }> = ({ complaint, onClose, onResolve }) => {
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"> <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"> <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; onClose: () => void;
onResolve: (resolution: string, rating?: number, feedback?: string) => void; onResolve: (resolution: string, rating?: number, feedback?: string) => void;
resolving: boolean; resolving: boolean;
}> = ({ _complaint, onClose, onResolve, resolving }) => { }> = ({ onClose, onResolve, resolving }) => {
const [resolution, setResolution] = useState(''); const [resolution, setResolution] = useState('');
const [rating, setRating] = useState<number | undefined>(); const [rating, setRating] = useState<number | undefined>();
const [feedback, setFeedback] = useState(''); const [feedback, setFeedback] = useState('');

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import Loading from '../../shared/components/Loading';
import apiClient from '../../shared/services/apiClient'; import apiClient from '../../shared/services/apiClient';
import { logger } from '../../shared/utils/logger'; import { logger } from '../../shared/utils/logger';
import { useRoomContext } from '../../features/rooms/contexts/RoomContext'; import { useRoomContext } from '../../features/rooms/contexts/RoomContext';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const EditRoomPage: React.FC = () => { const EditRoomPage: React.FC = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
@@ -26,7 +27,7 @@ const EditRoomPage: React.FC = () => {
room_number: '', room_number: '',
floor: 1, floor: 1,
room_type_id: 1, room_type_id: 1,
status: 'available' as 'available' | 'occupied' | 'maintenance', status: 'available' as 'available' | 'occupied' | 'maintenance' | 'cleaning' | 'reserved',
featured: false, featured: false,
price: '', price: '',
description: '', description: '',
@@ -730,7 +731,7 @@ const EditRoomPage: React.FC = () => {
</label> </label>
<select <select
value={formData.status} 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" 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 required
> >

View File

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

View File

@@ -12,6 +12,7 @@ import financialAuditService, { FinancialAuditRecord, FinancialAuditFilters } fr
import { formatDate } from '../../shared/utils/format'; import { formatDate } from '../../shared/utils/format';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency'; import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { exportData } from '../../shared/utils/exportUtils'; import { exportData } from '../../shared/utils/exportUtils';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const FinancialAuditTrailPage: React.FC = () => { const FinancialAuditTrailPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency(); const { formatCurrency } = useFormatCurrency();
@@ -90,7 +91,11 @@ const FinancialAuditTrailPage: React.FC = () => {
'Created At': formatDate(record.created_at), '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'); toast.success('Audit trail exported successfully');
} catch (error: unknown) { } catch (error: unknown) {
toast.error(getUserFriendlyError(error) || 'Unable to export audit trail'); 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 gdprService, { GDPRRequest } from '../../features/compliance/services/gdprService';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import Loading from '../../shared/components/Loading'; import Loading from '../../shared/components/Loading';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
import { formatDate } from '../../shared/utils/format';
const GDPRManagementPage: React.FC = () => { const GDPRManagementPage: React.FC = () => {
const [requests, setRequests] = useState<GDPRRequest[]>([]); 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 { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { formatDate } from '../../shared/utils/format'; import { formatDate } from '../../shared/utils/format';
import CreateGroupBookingModal from '../../features/hotel_services/components/CreateGroupBookingModal'; import CreateGroupBookingModal from '../../features/hotel_services/components/CreateGroupBookingModal';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const GroupBookingManagementPage: React.FC = () => { const GroupBookingManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency(); const { formatCurrency } = useFormatCurrency();

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import { toast } from 'react-toastify';
import Loading from '../../shared/components/Loading'; import Loading from '../../shared/components/Loading';
import { validateInvoiceId } from '../../shared/utils/routeValidation'; import { validateInvoiceId } from '../../shared/utils/routeValidation';
import { logger } from '../../shared/utils/logger'; import { logger } from '../../shared/utils/logger';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const InvoiceEditPage: React.FC = () => { const InvoiceEditPage: React.FC = () => {
const { id } = useParams<{ id: string }>(); 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 { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { formatDate } from '../../shared/utils/format'; import { formatDate } from '../../shared/utils/format';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const InvoiceManagementPage: React.FC = () => { const InvoiceManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency(); 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 loyaltyService, { LoyaltyTier, LoyaltyReward } from '../../features/loyalty/services/loyaltyService';
import Pagination from '../../shared/components/Pagination'; import Pagination from '../../shared/components/Pagination';
import { logger } from '../../shared/utils/logger'; import { logger } from '../../shared/utils/logger';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
type Tab = 'users' | 'tiers' | 'rewards'; type Tab = 'users' | 'tiers' | 'rewards';

View File

@@ -8,6 +8,8 @@ import Loading from '../../shared/components/Loading';
import Pagination from '../../shared/components/Pagination'; import Pagination from '../../shared/components/Pagination';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency'; import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import DatePicker from 'react-datepicker'; import DatePicker from 'react-datepicker';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
import { formatDate } from '../../shared/utils/format';
import 'react-datepicker/dist/react-datepicker.css'; import 'react-datepicker/dist/react-datepicker.css';
const MaintenanceManagementPage: React.FC = () => { const MaintenanceManagementPage: React.FC = () => {
@@ -87,7 +89,7 @@ const MaintenanceManagementPage: React.FC = () => {
const fetchRecords = async () => { const fetchRecords = async () => {
try { try {
setLoading(true); 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, page: currentPage,
limit: itemsPerPage, limit: itemsPerPage,
}; };
@@ -163,7 +165,21 @@ const MaintenanceManagementPage: React.FC = () => {
} }
try { 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, room_id: formData.room_id,
maintenance_type: formData.maintenance_type, maintenance_type: formData.maintenance_type,
title: formData.title, title: formData.title,
@@ -637,7 +653,11 @@ const MaintenanceManagementPage: React.FC = () => {
</label> </label>
<DatePicker <DatePicker
selected={formData.scheduled_start} selected={formData.scheduled_start}
onChange={(date: Date) => setFormData({ ...formData, scheduled_start: date })} onChange={(date: Date | null) => {
if (date) {
setFormData({ ...formData, scheduled_start: date });
}
}}
showTimeSelect showTimeSelect
dateFormat="MMMM d, yyyy h:mm aa" 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" 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 Pagination from '../../shared/components/Pagination';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency'; import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import { logger } from '../../shared/utils/logger'; import { logger } from '../../shared/utils/logger';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const PackageManagementPage: React.FC = () => { const PackageManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency(); const { formatCurrency } = useFormatCurrency();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,13 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Plus, Search, Edit, Trash2, X, Upload } from 'lucide-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 { ServiceSection } from '../../features/content/pages/ServiceDetailPage';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import Loading from '../../shared/components/Loading'; import Loading from '../../shared/components/Loading';
import Pagination from '../../shared/components/Pagination'; import Pagination from '../../shared/components/Pagination';
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency'; import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
import ConfirmationDialog from '../../shared/components/ConfirmationDialog'; import ConfirmationDialog from '../../shared/components/ConfirmationDialog';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const ServiceManagementPage: React.FC = () => { const ServiceManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency(); const { formatCurrency } = useFormatCurrency();
@@ -83,10 +84,10 @@ const ServiceManagementPage: React.FC = () => {
}; };
if (editingService) { if (editingService) {
await serviceService.updateService(editingService.id, dataToSubmit); await serviceService.updateService(editingService.id, dataToSubmit as UpdateServiceData);
toast.success('Service updated successfully'); toast.success('Service updated successfully');
} else { } else {
await serviceService.createService(dataToSubmit); await serviceService.createService(dataToSubmit as CreateServiceData);
toast.success('Service added successfully'); toast.success('Service added successfully');
} }
setShowModal(false); setShowModal(false);
@@ -522,7 +523,7 @@ const ServiceManagementPage: React.FC = () => {
value={section?.type || 'text'} value={section?.type || 'text'}
onChange={(e) => { onChange={(e) => {
const updatedSections = [...formData.sections]; 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 }); setFormData({ ...formData, sections: updatedSections });
}} }}
className="px-3 py-1.5 border border-slate-300 rounded text-sm" 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 { useCurrency } from '../../features/payments/contexts/CurrencyContext';
import Loading from '../../shared/components/Loading'; import Loading from '../../shared/components/Loading';
import { getCurrencySymbol } from '../../shared/utils/format'; import { getCurrencySymbol } from '../../shared/utils/format';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
type SettingsTab = 'general' | 'cookie' | 'currency' | 'payment' | 'smtp' | 'company' | 'recaptcha' | 'theme'; 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 Pagination from '../../shared/components/Pagination';
import { formatDate } from '../../shared/utils/format'; import { formatDate } from '../../shared/utils/format';
import DatePicker from 'react-datepicker'; import DatePicker from 'react-datepicker';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
import 'react-datepicker/dist/react-datepicker.css'; import 'react-datepicker/dist/react-datepicker.css';
// Custom scrollbar styles - Luxury design // Custom scrollbar styles - Luxury design
@@ -148,7 +149,7 @@ const StaffShiftDashboardPage: React.FC = () => {
const fetchShifts = async () => { const fetchShifts = async () => {
try { try {
setLoading(true); 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.status) params.status = filters.status;
if (filters.staff_id) params.staff_id = parseInt(filters.staff_id); if (filters.staff_id) params.staff_id = parseInt(filters.staff_id);
if (filters.shift_date) params.shift_date = filters.shift_date; if (filters.shift_date) params.shift_date = filters.shift_date;

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import webhookService, { Webhook, CreateWebhookData, UpdateWebhookData } from '.
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import Loading from '../../shared/components/Loading'; import Loading from '../../shared/components/Loading';
import { formatDate } from '../../shared/utils/format'; import { formatDate } from '../../shared/utils/format';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const WebhookManagementPage: React.FC = () => { const WebhookManagementPage: React.FC = () => {
const [webhooks, setWebhooks] = useState<Webhook[]>([]); 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 workflowService, { Workflow } from '../../features/system/services/workflowService';
import WorkflowBuilder from '../../features/system/components/WorkflowBuilder'; import WorkflowBuilder from '../../features/system/components/WorkflowBuilder';
import WorkflowDetailModal from '../../features/system/components/WorkflowDetailModal'; import WorkflowDetailModal from '../../features/system/components/WorkflowDetailModal';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const WorkflowManagementPage: React.FC = () => { const WorkflowManagementPage: React.FC = () => {
const [showBuilder, setShowBuilder] = useState(false); 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 UserManagementPage } from './UserManagementPage';
export { default as GuestProfilePage } from './GuestProfilePage'; export { default as GuestProfilePage } from './GuestProfilePage';
export { default as GroupBookingManagementPage } from './GroupBookingManagementPage'; 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 ReceptionDashboardPage } from './ReceptionDashboardPage';
export { default as AdvancedRoomManagementPage } from './AdvancedRoomManagementPage'; export { default as AdvancedRoomManagementPage } from './AdvancedRoomManagementPage';
export { default as PageContentDashboardPage } from './PageContentDashboard'; export { default as PageContentDashboardPage } from './PageContentDashboard';

View File

@@ -16,6 +16,7 @@ import {
FileText, FileText,
Building2, Building2,
AlertCircle, AlertCircle,
XCircle,
} from 'lucide-react'; } from 'lucide-react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { import {
@@ -140,7 +141,18 @@ const BookingDetailPage: React.FC = () => {
const handleCancelSuccess = async () => { const handleCancelSuccess = async () => {
if (booking) { 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); 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 : []); 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) { if (Array.isArray(serviceUsages) && serviceUsages.length > 0) {
return serviceUsages.reduce((sum: number, su: { total_price?: number }) => { return serviceUsages.reduce((sum: number, su: unknown) => {
return sum + (su.total_price || 0); const serviceUsage = su as { total_price?: number };
return sum + (serviceUsage.total_price || 0);
}, 0); }, 0);
} }
return 0; return 0;
@@ -192,7 +205,7 @@ const BookingDetailPage: React.FC = () => {
const roomPricePerNight = useMemo(() => { const roomPricePerNight = useMemo(() => {
if (!booking) return 0; if (!booking) return 0;
const roomTotal = booking.total_price - servicesTotal; const roomTotal = booking.total_price - (servicesTotal as number);
return nights > 0 ? roomTotal / nights : roomTotal; return nights > 0 ? roomTotal / nights : roomTotal;
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [booking?.total_price, servicesTotal, nights]); }, [booking?.total_price, servicesTotal, nights]);
@@ -476,8 +489,8 @@ const BookingDetailPage: React.FC = () => {
</p> </p>
<p className="text-sm text-gray-700"> <p className="text-sm text-gray-700">
<span className="font-medium">Payment Type:</span>{' '} <span className="font-medium">Payment Type:</span>{' '}
{payment.payment_type === 'deposit' ? 'Deposit (20%)' : {(payment as { payment_type?: string }).payment_type === 'deposit' ? 'Deposit (20%)' :
payment.payment_type === 'remaining' ? 'Remaining Payment' : (payment as { payment_type?: string }).payment_type === 'remaining' ? 'Remaining Payment' :
'Full Payment'} 'Full Payment'}
</p> </p>
{payment.transaction_id && ( {payment.transaction_id && (
@@ -530,7 +543,7 @@ const BookingDetailPage: React.FC = () => {
{} {}
{serviceUsages.length > 0 && ( {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 key={serviceUsage.id || index} className="flex justify-between items-center">
<div> <div>
<p className="text-sm font-medium text-gray-900"> <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 Stripe payment is pending, redirect to payment completion page
if (bookingData.payment_method === PAYMENT_METHOD.STRIPE && bookingData.payments) { if (bookingData.payment_method === PAYMENT_METHOD.STRIPE && bookingData.payments) {
// Find any pending Stripe payment that needs completion // 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) =>
p.payment_method === PAYMENT_METHOD.STRIPE && p.payment_method === PAYMENT_METHOD.STRIPE &&
p.payment_status === 'pending' // Payment record status (not booking payment_status) 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 // Check individual payment records for more granular status
if (booking.payments && Array.isArray(booking.payments)) { if (booking.payments && Array.isArray(booking.payments)) {
// Calculate total from completed payment records // 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 .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 // For deposit bookings, check if deposit payment is completed
if (booking.requires_deposit) { 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' (p: Payment) => p.payment_type === PAYMENT_TYPE.DEPOSIT && p.payment_status === 'completed'
); );
if (depositPayment) { if (depositPayment) {
@@ -349,7 +349,7 @@ const BookingSuccessPage: React.FC = () => {
} else { } else {
// For full payment bookings, check if total paid meets or exceeds booking price // For full payment bookings, check if total paid meets or exceeds booking price
// Allow small floating point differences (0.01) for currency calculations // 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 <div
className="bg-blue-50 border border-blue-200 className="bg-blue-50 border border-blue-200
rounded-lg p-6 mb-6" rounded-lg p-6 mb-6"
@@ -876,7 +876,7 @@ const BookingSuccessPage: React.FC = () => {
If you cancel the booking, 20% of If you cancel the booking, 20% of
the total order value will be charged the total order value will be charged
</li> </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> <li>
Please transfer within 24 hours Please transfer within 24 hours
to secure your room 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 { confirmBoricaPayment } from '../../features/payments/services/paymentService';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { CheckCircle, XCircle, Loader2 } from 'lucide-react'; import { CheckCircle, XCircle, Loader2 } from 'lucide-react';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const BoricaReturnPage: React.FC = () => { const BoricaReturnPage: React.FC = () => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();

View File

@@ -5,9 +5,10 @@ import Loading from '../../shared/components/Loading';
import EmptyState from '../../shared/components/EmptyState'; import EmptyState from '../../shared/components/EmptyState';
import Pagination from '../../shared/components/Pagination'; import Pagination from '../../shared/components/Pagination';
import complaintService, { Complaint } from '../../features/guest_management/services/complaintService'; 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 { formatDate } from '../../shared/utils/format';
import { logger } from '../../shared/utils/logger'; import { logger } from '../../shared/utils/logger';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const ComplaintPage: React.FC = () => { const ComplaintPage: React.FC = () => {
const [complaints, setComplaints] = useState<Complaint[]>([]); const [complaints, setComplaints] = useState<Complaint[]>([]);
@@ -88,7 +89,7 @@ const ComplaintPage: React.FC = () => {
return; return;
} }
// Silently fail - bookings are optional, but log for debugging // 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<{ const CreateComplaintModal: React.FC<{
bookings: Booking[]; bookings: Booking[];
onClose: () => void; 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 }) => { }> = ({ bookings, onClose, onSubmit }) => {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
booking_id: '', booking_id: '',

View File

@@ -170,13 +170,13 @@ const DashboardPage: React.FC = () => {
const fetchLoyalty = async () => { const fetchLoyalty = async () => {
try { try {
setLoadingLoyalty(true); setLoadingLoyalty(true);
const response = await loyaltyService.getMyLoyalty(); const response = await loyaltyService.getMyStatus();
if (response.status === 'success' && response.data) { if (response.status === 'success' && response.data) {
setLoyaltyInfo({ setLoyaltyInfo({
available_points: response.data.available_points || 0, available_points: response.data.available_points || 0,
tier_name: response.data.tier?.name || 'Bronze', tier_name: response.data.tier?.name || 'Bronze',
lifetime_points: response.data.lifetime_points || 0, 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) { } catch (err: unknown) {
@@ -189,7 +189,7 @@ const DashboardPage: React.FC = () => {
}; };
if (!isAbortError(err)) { if (!isAbortError(err)) {
// Loyalty is optional, don't show error // 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 { } finally {
setLoadingLoyalty(false); setLoadingLoyalty(false);

View File

@@ -180,7 +180,7 @@ const FullPaymentPage: React.FC = () => {
} else { } else {
// Fallback to payments from booking data // Fallback to payments from booking data
if (bookingData.payments && bookingData.payments.length > 0) { 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) =>
(p.payment_method === PAYMENT_METHOD.STRIPE || p.payment_method === PAYMENT_METHOD.CREDIT_CARD) && (p.payment_method === PAYMENT_METHOD.STRIPE || p.payment_method === PAYMENT_METHOD.CREDIT_CARD) &&
p.payment_status === 'pending' 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 { CheckCircle, XCircle, AlertCircle, Loader2, Home, Shield } from 'lucide-react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import gdprService from '../../features/compliance/services/gdprService'; import gdprService from '../../features/compliance/services/gdprService';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const GDPRDeletionConfirmPage: React.FC = () => { const GDPRDeletionConfirmPage: React.FC = () => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();

View File

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

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; // import { useNavigate } from 'react-router-dom'; // Unused
import { import {
Plus, Plus,
Clock, Clock,
@@ -22,10 +22,11 @@ import { logger } from '../../shared/utils/logger';
import Loading from '../../shared/components/Loading'; import Loading from '../../shared/components/Loading';
import EmptyState from '../../shared/components/EmptyState'; import EmptyState from '../../shared/components/EmptyState';
import useAuthStore from '../../store/useAuthStore'; import useAuthStore from '../../store/useAuthStore';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
const GuestRequestsPage: React.FC = () => { const GuestRequestsPage: React.FC = () => {
const { userInfo: _userInfo } = useAuthStore(); const { userInfo: _userInfo } = useAuthStore();
const _navigate = useNavigate(); // const _navigate = useNavigate(); // Unused
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [requests, setRequests] = useState<GuestRequest[]>([]); const [requests, setRequests] = useState<GuestRequest[]>([]);
const [bookings, setBookings] = useState<Booking[]>([]); 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." ? "You need to be checked in to submit service requests. Please check in first or contact reception."
: "Submit your first request to get started" : "Submit your first request to get started"
} }
actionLabel={bookings.length > 0 ? "Create Request" : undefined} action={bookings.length > 0 ? {
onAction={bookings.length > 0 ? () => setShowCreateModal(true) : undefined} label: "Create Request",
onClick: () => setShowCreateModal(true)
} : undefined}
/> />
) : ( ) : (
<div className="grid gap-6"> <div className="grid gap-6">
@@ -343,7 +346,7 @@ const GuestRequestsPage: React.FC = () => {
booking_id: e.target.value, booking_id: e.target.value,
room_id: booking?.room_id?.toString() || '', 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)]" 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 required

View File

@@ -8,6 +8,8 @@ import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurren
import { formatDate } from '../../shared/utils/format'; import { formatDate } from '../../shared/utils/format';
import useAuthStore from '../../store/useAuthStore'; import useAuthStore from '../../store/useAuthStore';
import { validateInvoiceId } from '../../shared/utils/routeValidation'; import { validateInvoiceId } from '../../shared/utils/routeValidation';
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
import { isAxiosError } from 'axios';
const InvoicePage: React.FC = () => { const InvoicePage: React.FC = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
@@ -65,7 +67,7 @@ const InvoicePage: React.FC = () => {
const invoiceData = response.data.invoice; const invoiceData = response.data.invoice;
// Validate ownership for customer role // 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'); toast.error('You do not have permission to view this invoice');
navigate('/bookings'); navigate('/bookings');
setLoading(false); 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 // 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 // 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) // Wait with increasing delay (500ms, 1000ms, 2000ms)
const delay = 500 * Math.pow(2, retryCount); const delay = 500 * Math.pow(2, retryCount);
setTimeout(() => { setTimeout(() => {
@@ -120,7 +122,7 @@ const InvoicePage: React.FC = () => {
} }
// Handle invoice not found (404) after retries - show appropriate message // Handle invoice not found (404) after retries - show appropriate message
if (getUserFriendlyError(error) === 404) { if (isAxiosError(error) && error.response?.status === 404) {
handleInvoiceNotFound(); handleInvoiceNotFound();
} else { } else {
// Other errors (network, server errors, etc.) - only show toast if not a validation error // 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..." />; return <Loading fullScreen text="Loading invoice..." />;
} }
if (!invoice && !loading) { if (!invoice) {
return ( return (
<div className="min-h-screen bg-gray-50 py-8"> <div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-4xl mx-auto px-4"> <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 ( return (
<div className="min-h-screen bg-gray-50 py-8 print:bg-white"> <div className="min-h-screen bg-gray-50 py-8 print:bg-white">
<div className="max-w-4xl mx-auto px-4"> <div className="max-w-4xl mx-auto px-4">

View File

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

View File

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

View File

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

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