updates
This commit is contained in:
@@ -43,7 +43,7 @@ const CancellationPolicyPage: React.FC = () => {
|
||||
allElements.forEach((el) => {
|
||||
const htmlEl = el as HTMLElement;
|
||||
const tagName = htmlEl.tagName.toLowerCase();
|
||||
const _currentColor = htmlEl.style.color;
|
||||
// const _currentColor = htmlEl.style.color; // Unused variable
|
||||
|
||||
// Override inline colors to use theme-aware colors
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
|
||||
@@ -19,7 +19,7 @@ import { getThemeBackgroundClasses, getThemeHeroBackgroundClasses, getThemeTextC
|
||||
const getIconComponent = (iconName?: string, fallback: React.ComponentType<{ className?: string }> = Mail) => {
|
||||
if (!iconName) return fallback;
|
||||
|
||||
const icons = LucideIcons as Record<string, React.ComponentType<{ className?: string }> | undefined>;
|
||||
const icons = LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }> | undefined>;
|
||||
|
||||
// Try direct match first (for PascalCase names)
|
||||
if (icons[iconName]) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||
import Loading from '../../../shared/components/Loading';
|
||||
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
|
||||
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
|
||||
import { isAxiosError } from 'axios';
|
||||
|
||||
const PrivacyPolicyPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
@@ -80,7 +81,7 @@ const PrivacyPolicyPage: React.FC = () => {
|
||||
} catch (err: unknown) {
|
||||
console.error('Error fetching page content:', err);
|
||||
// If page is disabled (404), set pageContent to null to show disabled message
|
||||
if (getUserFriendlyError(err) === 404) {
|
||||
if (isAxiosError(err) && err.response?.status === 404) {
|
||||
setPageContent(null);
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||
import Loading from '../../../shared/components/Loading';
|
||||
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
|
||||
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
|
||||
import { isAxiosError } from 'axios';
|
||||
|
||||
const RefundsPolicyPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
@@ -80,7 +81,7 @@ const RefundsPolicyPage: React.FC = () => {
|
||||
} catch (err: unknown) {
|
||||
console.error('Error fetching page content:', err);
|
||||
// If page is disabled (404), set pageContent to null to show disabled message
|
||||
if (getUserFriendlyError(err) === 404) {
|
||||
if (isAxiosError(err) && err.response?.status === 404) {
|
||||
setPageContent(null);
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -3,14 +3,14 @@ import { useParams, Link, useNavigate } from 'react-router-dom';
|
||||
import { ArrowLeft, Share2, Tag, Star } from 'lucide-react';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import pageContentService, { PageContent } from '../services/pageContentService';
|
||||
import serviceService from '../../hotel_services/services/serviceService';
|
||||
import serviceService, { Service } from '../../hotel_services/services/serviceService';
|
||||
import Loading from '../../../shared/components/Loading';
|
||||
import { createSanitizedHtml } from '../../../shared/utils/htmlSanitizer';
|
||||
import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency';
|
||||
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
|
||||
|
||||
interface ServiceSection {
|
||||
export interface ServiceSection {
|
||||
type: 'hero' | 'text' | 'image' | 'gallery' | 'quote' | 'features' | 'cta' | 'video';
|
||||
title?: string;
|
||||
content?: string;
|
||||
@@ -60,6 +60,14 @@ const ServiceDetailPage: React.FC = () => {
|
||||
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||
|
||||
// Helper function to get icon component from icon name
|
||||
const getIconComponent = (iconName?: string, fallback: React.ComponentType<{ className?: string }> = Star) => {
|
||||
if (!iconName) return fallback;
|
||||
const icons = LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }> | undefined>;
|
||||
const IconComponent = icons[iconName] || fallback;
|
||||
return IconComponent;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (slug) {
|
||||
fetchService();
|
||||
@@ -216,7 +224,7 @@ const ServiceDetailPage: React.FC = () => {
|
||||
});
|
||||
|
||||
if (servicesResponse.success && servicesResponse.data?.services) {
|
||||
servicesResponse.data.services.forEach((service: { slug?: string; category?: string; id?: number | string }) => {
|
||||
servicesResponse.data.services.forEach((service: Service) => {
|
||||
// Skip current service
|
||||
if (currentService.type === 'hotel' && service.id === currentService.id) {
|
||||
return;
|
||||
@@ -229,7 +237,7 @@ const ServiceDetailPage: React.FC = () => {
|
||||
|
||||
const serviceSlug = service.slug || service.name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
||||
services.push({
|
||||
id: service.id,
|
||||
id: service.id ?? `service-${Date.now()}`,
|
||||
title: service.name,
|
||||
slug: serviceSlug,
|
||||
description: service.description,
|
||||
@@ -243,17 +251,17 @@ const ServiceDetailPage: React.FC = () => {
|
||||
|
||||
// Add luxury services as fallback
|
||||
if (pageContent?.luxury_services && Array.isArray(pageContent.luxury_services)) {
|
||||
pageContent.luxury_services.forEach((s: { slug?: string; category?: string }, index: number) => {
|
||||
pageContent.luxury_services.forEach((s: { slug?: string; category?: string; title?: string; name?: string; description?: string; image?: string; icon?: string }, index: number) => {
|
||||
if (s.slug && s.slug !== currentService.slug) {
|
||||
if (!currentService.category || s.category === currentService.category) {
|
||||
// Check if already in services (by slug)
|
||||
const serviceSlug = s.slug || s.title?.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
||||
const serviceSlug = s.slug || (s.title || s.name || 'service').toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
||||
const exists = services.some(serv => serv.slug === serviceSlug);
|
||||
if (!exists) {
|
||||
services.push({
|
||||
id: `luxury-${index}`,
|
||||
title: s.title || 'Service',
|
||||
slug: s.slug,
|
||||
title: s.title || s.name || 'Service',
|
||||
slug: s.slug || serviceSlug,
|
||||
description: s.description,
|
||||
image: s.image,
|
||||
icon: s.icon,
|
||||
@@ -412,10 +420,10 @@ const ServiceDetailPage: React.FC = () => {
|
||||
)}
|
||||
|
||||
<div className="flex items-start gap-6 mb-6">
|
||||
{service.icon && (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[service.icon] && (
|
||||
{service.icon && (
|
||||
<div className="relative flex-shrink-0">
|
||||
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 rounded-full blur-3xl"></div>
|
||||
{React.createElement((LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[service.icon], {
|
||||
{React.createElement(getIconComponent(service.icon), {
|
||||
className: 'w-20 h-20 sm:w-24 sm:h-24 text-[var(--luxury-gold)] relative z-10 drop-shadow-2xl'
|
||||
})}
|
||||
</div>
|
||||
@@ -604,9 +612,7 @@ const ServiceDetailPage: React.FC = () => {
|
||||
)}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{section.features.map((feature, featIndex) => {
|
||||
const IconComponent = feature.icon && (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[feature.icon]
|
||||
? (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[feature.icon]
|
||||
: null;
|
||||
const IconComponent = feature.icon ? getIconComponent(feature.icon) : null;
|
||||
return (
|
||||
<div key={featIndex} className="group relative bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a] rounded-3xl border-2 border-[var(--luxury-gold)]/20 p-8 shadow-2xl hover:border-[var(--luxury-gold)]/60 hover:shadow-[var(--luxury-gold)]/20 transition-all duration-500 hover:-translate-y-2">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[var(--luxury-gold)]/0 to-[var(--luxury-gold)]/0 group-hover:from-[var(--luxury-gold)]/5 group-hover:to-transparent rounded-3xl transition-all duration-500"></div>
|
||||
|
||||
@@ -13,15 +13,17 @@ import { getThemeBackgroundClasses, getThemeHeroBackgroundClasses, getThemeTextC
|
||||
const getIconComponent = (iconName?: string, fallback: React.ComponentType<{ className?: string }> = Award) => {
|
||||
if (!iconName) return fallback;
|
||||
|
||||
const icons = LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }> | undefined>;
|
||||
|
||||
// Try direct match first (for PascalCase names)
|
||||
if ((LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[iconName]) {
|
||||
return (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[iconName];
|
||||
if (icons[iconName]) {
|
||||
return icons[iconName];
|
||||
}
|
||||
|
||||
// Convert to PascalCase (capitalize first letter)
|
||||
const pascalCaseName = iconName.charAt(0).toUpperCase() + iconName.slice(1).toLowerCase();
|
||||
if ((LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[pascalCaseName]) {
|
||||
return (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[pascalCaseName];
|
||||
if (icons[pascalCaseName]) {
|
||||
return icons[pascalCaseName];
|
||||
}
|
||||
|
||||
return fallback;
|
||||
@@ -67,7 +69,7 @@ const ServicesPage: React.FC = () => {
|
||||
// Extract categories from luxury services
|
||||
const categories = new Set<string>();
|
||||
if (Array.isArray(content.luxury_services)) {
|
||||
content.luxury_services.forEach((service: { icon?: string; name?: string; description?: string }) => {
|
||||
content.luxury_services.forEach((service: { icon?: string; name?: string; description?: string; category?: string; title?: string }) => {
|
||||
if (service.category) {
|
||||
categories.add(service.category);
|
||||
}
|
||||
@@ -133,9 +135,9 @@ const ServicesPage: React.FC = () => {
|
||||
|
||||
// Add luxury services from page content (only if not already in hotel services)
|
||||
if (pageContent?.luxury_services && Array.isArray(pageContent.luxury_services)) {
|
||||
pageContent.luxury_services.forEach((service: { icon?: string; name?: string; description?: string }, index: number) => {
|
||||
pageContent.luxury_services.forEach((service: { icon?: string; name?: string; description?: string; category?: string; title?: string; slug?: string; image?: string }, index: number) => {
|
||||
// Check if this service already exists in hotel services by slug
|
||||
const existingSlug = service.slug || service.title?.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
||||
const existingSlug = service.slug || (service.title || service.name || 'service').toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
||||
const existsInHotel = hotelServices.some((hs: Service) => {
|
||||
const hotelSlug = hs.slug || hs.name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
||||
return hotelSlug === existingSlug;
|
||||
@@ -145,13 +147,13 @@ const ServicesPage: React.FC = () => {
|
||||
if (!existsInHotel) {
|
||||
services.push({
|
||||
id: `luxury-${index}`,
|
||||
title: service.title || 'Service',
|
||||
title: service.title || service.name || 'Service',
|
||||
description: service.description || '',
|
||||
image: service.image,
|
||||
icon: service.icon,
|
||||
category: service.category,
|
||||
type: 'luxury',
|
||||
slug: service.slug,
|
||||
slug: service.slug || existingSlug,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -351,10 +353,10 @@ const ServicesPage: React.FC = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className={`h-48 sm:h-56 ${cardClasses} flex items-center justify-center p-8`}>
|
||||
{service.icon && (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[service.icon] ? (
|
||||
{service.icon ? (
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 rounded-full blur-2xl"></div>
|
||||
{React.createElement((LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[service.icon], {
|
||||
{React.createElement(getIconComponent(service.icon), {
|
||||
className: 'w-16 h-16 sm:w-20 sm:h-20 text-[var(--luxury-gold)] relative z-10 drop-shadow-lg'
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||
import Loading from '../../../shared/components/Loading';
|
||||
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
|
||||
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
|
||||
import { isAxiosError } from 'axios';
|
||||
|
||||
const TermsPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
@@ -80,7 +81,7 @@ const TermsPage: React.FC = () => {
|
||||
} catch (err: unknown) {
|
||||
console.error('Error fetching page content:', err);
|
||||
// If page is disabled (404), set pageContent to null to show disabled message
|
||||
if (getUserFriendlyError(err) === 404) {
|
||||
if (isAxiosError(err) && err.response?.status === 404) {
|
||||
setPageContent(null);
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { toast } from 'react-toastify';
|
||||
import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
|
||||
|
||||
interface CreateBookingModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -170,7 +171,7 @@ const CreateBookingModal: React.FC<CreateBookingModalProps> = ({
|
||||
handleClose();
|
||||
onSuccess();
|
||||
} catch (error: unknown) {
|
||||
toast.error(getUserFriendlyError(error) || getUserFriendlyError(error) || 'Failed to create booking');
|
||||
toast.error(getUserFriendlyError(error) || 'Failed to create booking');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import roomService, { Room } from '../../rooms/services/roomService';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
|
||||
interface RoomBlock {
|
||||
|
||||
@@ -18,6 +18,7 @@ import roomService, { Room } from '../../rooms/services/roomService';
|
||||
import userService, { User as UserType } from '../../auth/services/userService';
|
||||
import useAuthStore from '../../../store/useAuthStore';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
|
||||
const HousekeepingManagement: React.FC = () => {
|
||||
@@ -82,7 +83,15 @@ const HousekeepingManagement: React.FC = () => {
|
||||
const fetchTasks = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const params: { page: number; limit: number; room_id?: number; status?: string } = {
|
||||
const params: {
|
||||
page: number;
|
||||
limit: number;
|
||||
room_id?: number;
|
||||
status?: string;
|
||||
task_type?: string;
|
||||
date?: string;
|
||||
include_cleaning_rooms?: boolean;
|
||||
} = {
|
||||
page: currentPage,
|
||||
limit: 10,
|
||||
include_cleaning_rooms: true // Include rooms in cleaning status
|
||||
@@ -173,7 +182,7 @@ const HousekeepingManagement: React.FC = () => {
|
||||
const items = defaultChecklistItems[type] || [];
|
||||
setFormData({
|
||||
...formData,
|
||||
task_type: type as 'cleaning' | 'inspection' | 'maintenance',
|
||||
task_type: type as 'checkout' | 'stayover' | 'vacant' | 'inspection' | 'turndown',
|
||||
checklist_items: items.map(item => ({ item, completed: false, notes: '' })),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ import roomService, { Room } from '../../rooms/services/roomService';
|
||||
import userService, { User as UserType } from '../../auth/services/userService';
|
||||
import useAuthStore from '../../../store/useAuthStore';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
|
||||
const InspectionManagement: React.FC = () => {
|
||||
@@ -88,7 +89,7 @@ const InspectionManagement: React.FC = () => {
|
||||
const fetchInspections = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const params: { page: number; limit: number; room_id?: number; inspection_type?: string } = { page: currentPage, limit: 10 };
|
||||
const params: { page: number; limit: number; room_id?: number; inspection_type?: string; status?: string } = { page: currentPage, limit: 10 };
|
||||
if (filters.room_id) params.room_id = parseInt(filters.room_id);
|
||||
if (filters.inspection_type) params.inspection_type = filters.inspection_type;
|
||||
if (filters.status) params.status = filters.status;
|
||||
@@ -480,7 +481,7 @@ const InspectionManagement: React.FC = () => {
|
||||
<select
|
||||
required
|
||||
value={formData.inspection_type}
|
||||
onChange={(e) => setFormData({ ...formData, inspection_type: e.target.value as 'routine' | 'deep' | 'pre_checkin' | 'post_checkout' | 'maintenance' })}
|
||||
onChange={(e) => setFormData({ ...formData, inspection_type: e.target.value as 'pre_checkin' | 'post_checkout' | 'routine' | 'maintenance' | 'damage' })}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="pre_checkin">Pre Check-in</option>
|
||||
|
||||
@@ -15,6 +15,7 @@ import roomService, { Room } from '../../rooms/services/roomService';
|
||||
import userService, { User as UserType } from '../../auth/services/userService';
|
||||
import useAuthStore from '../../../store/useAuthStore';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
|
||||
const MaintenanceManagement: React.FC = () => {
|
||||
@@ -61,7 +62,7 @@ const MaintenanceManagement: React.FC = () => {
|
||||
const fetchMaintenanceRecords = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const params: { page: number; limit: number; room_id?: number; status?: string } = { page: currentPage, limit: 10 };
|
||||
const params: { page: number; limit: number; room_id?: number; status?: string; maintenance_type?: string } = { page: currentPage, limit: 10 };
|
||||
if (filters.room_id) params.room_id = parseInt(filters.room_id);
|
||||
if (filters.status) params.status = filters.status;
|
||||
if (filters.maintenance_type) params.maintenance_type = filters.maintenance_type;
|
||||
@@ -417,7 +418,7 @@ const MaintenanceManagement: React.FC = () => {
|
||||
<select
|
||||
required
|
||||
value={formData.maintenance_type}
|
||||
onChange={(e) => setFormData({ ...formData, maintenance_type: e.target.value as 'repair' | 'cleaning' | 'inspection' | 'upgrade' | 'other' })}
|
||||
onChange={(e) => setFormData({ ...formData, maintenance_type: e.target.value as 'preventive' | 'corrective' | 'emergency' | 'upgrade' | 'inspection' })}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="preventive">Preventive</option>
|
||||
|
||||
@@ -153,7 +153,7 @@ const loyaltyService = {
|
||||
limit: number = 20,
|
||||
transactionType?: string
|
||||
): Promise<PointsHistoryResponse> => {
|
||||
const params: { page: number; limit: number } = { page, limit };
|
||||
const params: { page: number; limit: number; transaction_type?: string } = { page, limit };
|
||||
if (transactionType) {
|
||||
params.transaction_type = transactionType;
|
||||
}
|
||||
@@ -232,7 +232,7 @@ const loyaltyService = {
|
||||
};
|
||||
};
|
||||
}> => {
|
||||
const params: { page: number; limit: number } = { page, limit };
|
||||
const params: { page: number; limit: number; search?: string; tier_id?: number } = { page, limit };
|
||||
if (search) params.search = search;
|
||||
if (tierId) params.tier_id = tierId;
|
||||
const response = await apiClient.get('/api/loyalty/admin/users', { params });
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useCompanySettings } from '../../../shared/contexts/CompanySettingsCont
|
||||
import { toast } from 'react-toastify';
|
||||
import ConfirmationDialog from '../../../shared/components/ConfirmationDialog';
|
||||
import { formatWorkingHours } from '../../../shared/utils/format';
|
||||
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
|
||||
|
||||
interface ChatWidgetProps {
|
||||
onClose?: () => void;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { toast } from 'react-toastify';
|
||||
import notificationService, { Notification } from '../services/notificationService';
|
||||
import { formatDate } from '../../../shared/utils/format';
|
||||
import useAuthStore from '../../../store/useAuthStore';
|
||||
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
|
||||
|
||||
const InAppNotificationBell: React.FC = () => {
|
||||
const { isAuthenticated, token, isLoading } = useAuthStore();
|
||||
@@ -108,7 +109,7 @@ const InAppNotificationBell: React.FC = () => {
|
||||
try {
|
||||
await notificationService.markAsRead(notificationId);
|
||||
setNotifications(notifications.map(n =>
|
||||
n.id === notificationId ? { ...n, status: 'read' as 'unread' | 'read', read_at: new Date().toISOString() } : n
|
||||
n.id === notificationId ? { ...n, status: 'read' as Notification['status'], read_at: new Date().toISOString() } : n
|
||||
));
|
||||
setUnreadCount(Math.max(0, unreadCount - 1));
|
||||
} catch (error: unknown) {
|
||||
@@ -121,7 +122,7 @@ const InAppNotificationBell: React.FC = () => {
|
||||
setLoading(true);
|
||||
const unread = notifications.filter(n => !n.read_at);
|
||||
await Promise.all(unread.map(n => notificationService.markAsRead(n.id)));
|
||||
setNotifications(notifications.map(n => ({ ...n, status: 'read' as 'read' | 'unread', read_at: new Date().toISOString() })));
|
||||
setNotifications(notifications.map(n => ({ ...n, status: 'read' as Notification['status'], read_at: new Date().toISOString() })));
|
||||
setUnreadCount(0);
|
||||
} catch (error: unknown) {
|
||||
toast.error(getUserFriendlyError(error) || 'Failed to mark all as read');
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Bell, Mail, MessageSquare, Smartphone, Save } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../../shared/components/Loading';
|
||||
import notificationService, { NotificationPreferences as NotificationPreferencesType } from '../services/notificationService';
|
||||
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
|
||||
|
||||
const NotificationPreferences: React.FC = () => {
|
||||
const [preferences, setPreferences] = useState<NotificationPreferencesType | null>(null);
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { X, Plus } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import notificationService, { NotificationTemplate } from '../services/notificationService';
|
||||
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
|
||||
|
||||
interface NotificationTemplatesModalProps {
|
||||
onClose: () => void;
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import notificationService from '../services/notificationService';
|
||||
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
|
||||
|
||||
interface SendNotificationModalProps {
|
||||
onClose: () => void;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createBoricaPayment } from '../services/paymentService';
|
||||
import { X, Loader2, AlertCircle, CreditCard } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useFormatCurrency } from '../hooks/useFormatCurrency';
|
||||
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
|
||||
|
||||
interface BoricaPaymentModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -75,7 +76,7 @@ const BoricaPaymentModal: React.FC<BoricaPaymentModalProps> = ({
|
||||
// Create a form and submit it to Borica gateway
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = paymentRequest.gateway_url;
|
||||
form.action = (paymentRequest.gateway_url as string) || '';
|
||||
form.style.display = 'none';
|
||||
|
||||
// Add all form fields
|
||||
@@ -172,7 +173,7 @@ const BoricaPaymentModal: React.FC<BoricaPaymentModalProps> = ({
|
||||
<div className="bg-[#1a1a1a]/50 border border-[var(--luxury-gold)]/20 rounded-lg p-3 sm:p-4 space-y-2 sm:space-y-3">
|
||||
<div className="flex justify-between items-center text-xs sm:text-sm">
|
||||
<span className="text-gray-400">Order ID:</span>
|
||||
<span className="text-white font-mono">{paymentRequest.order_id}</span>
|
||||
<span className="text-white font-mono">{String(paymentRequest.order_id || '')}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-xs sm:text-sm">
|
||||
<span className="text-gray-400">Amount:</span>
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { useFormatCurrency } from '../hooks/useFormatCurrency';
|
||||
import StripePaymentModal from './StripePaymentModal';
|
||||
import PayPalPaymentModal from './PayPalPaymentModal';
|
||||
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
|
||||
|
||||
interface DepositPaymentModalProps {
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from '@stripe/react-stripe-js';
|
||||
import { CreditCard, Loader2, AlertCircle, CheckCircle } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
|
||||
|
||||
interface StripePaymentFormProps {
|
||||
clientSecret: string;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { loadStripe, StripeElementsOptions } from '@stripe/stripe-js';
|
||||
import { loadStripe, StripeElementsOptions, Stripe } from '@stripe/stripe-js';
|
||||
import { Elements } from '@stripe/react-stripe-js';
|
||||
import StripePaymentForm from './StripePaymentForm';
|
||||
import { createStripePaymentIntent, confirmStripePayment } from '../services/paymentService';
|
||||
import { X, Loader2, AlertCircle } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
|
||||
|
||||
interface StripePaymentModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -23,7 +24,7 @@ const StripePaymentModal: React.FC<StripePaymentModalProps> = ({
|
||||
onSuccess,
|
||||
onClose,
|
||||
}) => {
|
||||
const [stripePromise, setStripePromise] = useState<Promise<unknown> | null>(null);
|
||||
const [stripePromise, setStripePromise] = useState<Promise<Stripe | null> | null>(null);
|
||||
const [clientSecret, setClientSecret] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { loadStripe, StripeElementsOptions } from '@stripe/stripe-js';
|
||||
import { loadStripe, StripeElementsOptions, Stripe } from '@stripe/stripe-js';
|
||||
import { Elements } from '@stripe/react-stripe-js';
|
||||
import StripePaymentForm from './StripePaymentForm';
|
||||
import { createStripePaymentIntent, confirmStripePayment } from '../services/paymentService';
|
||||
@@ -20,7 +20,7 @@ const StripePaymentWrapper: React.FC<StripePaymentWrapperProps> = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
}) => {
|
||||
const [stripePromise, setStripePromise] = useState<Promise<unknown> | null>(null);
|
||||
const [stripePromise, setStripePromise] = useState<Promise<Stripe | null> | null>(null);
|
||||
const [clientSecret, setClientSecret] = useState<string | null>(null);
|
||||
const [, setPublishableKey] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -61,7 +61,7 @@ export const createPayment = async (
|
||||
// Handle both 'status: success' and 'success: true' formats
|
||||
return {
|
||||
success: data.status === 'success' || data.success === true,
|
||||
data: data.data || {},
|
||||
data: (data.data as { payment: Payment }) || { payment: {} as Payment },
|
||||
message: data.message,
|
||||
};
|
||||
};
|
||||
@@ -76,7 +76,7 @@ export const getPaymentByBookingId = async (
|
||||
// Handle both 'status: success' and 'success: true' formats
|
||||
return {
|
||||
success: data.status === 'success' || data.success === true,
|
||||
data: data.data || {},
|
||||
data: (data.data as { payment: Payment }) || { payment: {} as Payment },
|
||||
message: data.message,
|
||||
};
|
||||
};
|
||||
@@ -125,7 +125,7 @@ export const getBankTransferInfo = async (
|
||||
// Handle both 'status: success' and 'success: true' formats
|
||||
return {
|
||||
success: data.status === 'success' || data.success === true,
|
||||
data: data.data || {},
|
||||
data: (data.data as { payment: Payment; bank_info: BankInfo }) || { payment: {} as Payment, bank_info: {} as BankInfo },
|
||||
message: data.message,
|
||||
};
|
||||
};
|
||||
@@ -149,7 +149,7 @@ export const confirmDepositPayment = async (
|
||||
// Handle both 'status: success' and 'success: true' formats
|
||||
return {
|
||||
success: data.status === 'success' || data.success === true,
|
||||
data: data.data || {},
|
||||
data: (data.data as { payment: Payment; booking: Booking }) || { payment: {} as Payment, booking: {} as Booking },
|
||||
message: data.message,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useAntibotForm } from '../../auth/hooks/useAntibotForm';
|
||||
import HoneypotField from '../../../shared/components/HoneypotField';
|
||||
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||
import { getThemeTextClasses, getThemeCardClasses, getThemeInputClasses } from '../../../shared/utils/themeUtils';
|
||||
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
|
||||
|
||||
interface ReviewSectionProps {
|
||||
roomId: number;
|
||||
|
||||
@@ -3,6 +3,8 @@ import roomService, { Room } from '../services/roomService';
|
||||
import advancedRoomService, { RoomStatusBoardItem } from '../services/advancedRoomService';
|
||||
import { toast } from 'react-toastify';
|
||||
import { logger } from '../../../shared/utils/logger';
|
||||
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
|
||||
import { isAxiosError } from 'axios';
|
||||
|
||||
interface RoomContextType {
|
||||
// Room list state
|
||||
@@ -175,7 +177,7 @@ export const RoomProvider: React.FC<RoomProviderProps> = ({ children }) => {
|
||||
}
|
||||
|
||||
// Handle 401 Unauthorized gracefully - user may not have admin/staff role
|
||||
if (getUserFriendlyError(error) === 401) {
|
||||
if (isAxiosError(error) && error.response?.status === 401) {
|
||||
setStatusBoardError(null); // Don't set error for unauthorized access
|
||||
setStatusBoardRooms([]); // Clear status board if unauthorized
|
||||
return; // Silently return without logging
|
||||
|
||||
@@ -5,7 +5,7 @@ export interface Room {
|
||||
room_type_id: number;
|
||||
room_number: string;
|
||||
floor: number;
|
||||
status: 'available' | 'occupied' | 'maintenance';
|
||||
status: 'available' | 'occupied' | 'maintenance' | 'cleaning' | 'reserved';
|
||||
featured: boolean;
|
||||
price?: number;
|
||||
description?: string;
|
||||
@@ -295,7 +295,7 @@ export interface CreateRoomData {
|
||||
room_number: string;
|
||||
floor: number;
|
||||
room_type_id: number;
|
||||
status: 'available' | 'occupied' | 'maintenance';
|
||||
status: 'available' | 'occupied' | 'maintenance' | 'cleaning' | 'reserved';
|
||||
featured?: boolean;
|
||||
price?: number;
|
||||
description?: string;
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import taskService from '../services/taskService';
|
||||
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
|
||||
|
||||
interface CreateTaskModalProps {
|
||||
onClose: () => void;
|
||||
|
||||
@@ -91,7 +91,7 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
|
||||
continue;
|
||||
}
|
||||
|
||||
const iconComponent = (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[iconName];
|
||||
const iconComponent = (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }>>)[iconName];
|
||||
|
||||
// Only include if it's a function (React component)
|
||||
if (typeof iconComponent === 'function') {
|
||||
@@ -127,7 +127,7 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
|
||||
);
|
||||
}, [searchQuery, allIcons]);
|
||||
|
||||
const selectedIcon = normalizedValue && (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[normalizedValue] ? (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[normalizedValue] : null;
|
||||
const selectedIcon = normalizedValue && (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }>>)[normalizedValue] ? (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }>>)[normalizedValue] : null;
|
||||
|
||||
const handleIconSelect = (iconName: string) => {
|
||||
onChange(iconName);
|
||||
@@ -202,7 +202,7 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
|
||||
)}
|
||||
<div className="grid grid-cols-6 sm:grid-cols-8 md:grid-cols-10 gap-2">
|
||||
{filteredIcons.slice(0, searchQuery.trim() ? 500 : 300).map((iconName) => {
|
||||
const IconComponent = (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[iconName];
|
||||
const IconComponent = (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }>>)[iconName];
|
||||
if (!IconComponent) return null;
|
||||
|
||||
const isSelected = normalizedValue === iconName;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { toast } from 'react-toastify';
|
||||
import { Task } from '../services/taskService';
|
||||
import taskService from '../services/taskService';
|
||||
import { formatDate } from '../../../shared/utils/format';
|
||||
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
|
||||
|
||||
interface TaskDetailModalProps {
|
||||
task: Task;
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
||||
import { X, Plus, Trash2, GripVertical, Save } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import workflowService, { Workflow, WorkflowStep } from '../services/workflowService';
|
||||
import { getUserFriendlyError } from '../../../shared/utils/errorSanitizer';
|
||||
|
||||
interface WorkflowBuilderProps {
|
||||
workflow?: Workflow | null;
|
||||
|
||||
@@ -61,6 +61,7 @@ import analyticsService, {
|
||||
import { SimpleBarChart, SimpleLineChart, SimplePieChart, KPICard } from '../../features/analytics/components/SimpleChart';
|
||||
import { exportData } from '../../shared/utils/exportUtils';
|
||||
import CustomReportBuilder from '../../features/analytics/components/CustomReportBuilder';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
type AnalyticsTab = 'overview' | 'reports' | 'revenue' | 'operational' | 'guest' | 'financial' | 'audit-logs' | 'reviews';
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const ApprovalManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
@@ -23,7 +24,7 @@ const ApprovalManagementPage: React.FC = () => {
|
||||
const fetchApprovals = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const params: { status?: string } = {};
|
||||
const params: { status?: ApprovalStatus } = {};
|
||||
if (filter !== 'all') params.status = filter;
|
||||
const response = await approvalService.getApprovals(params);
|
||||
setApprovals(response.data || []);
|
||||
@@ -225,19 +226,29 @@ const ApprovalManagementPage: React.FC = () => {
|
||||
<p className="text-lg font-bold text-emerald-600">{formatCurrency(selectedApproval.amount)}</p>
|
||||
</div>
|
||||
)}
|
||||
{selectedApproval.previous_value && (
|
||||
{selectedApproval.previous_value != null && (
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">Previous Value</h3>
|
||||
<pre className="bg-slate-50 p-3 rounded text-sm overflow-x-auto">
|
||||
{JSON.stringify(selectedApproval.previous_value, null, 2)}
|
||||
{(() => {
|
||||
const value = selectedApproval.previous_value;
|
||||
return typeof value === 'string'
|
||||
? value
|
||||
: JSON.stringify(value, null, 2);
|
||||
})()}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{selectedApproval.new_value && (
|
||||
{selectedApproval.new_value != null && (
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">New Value</h3>
|
||||
<pre className="bg-slate-50 p-3 rounded text-sm overflow-x-auto">
|
||||
{JSON.stringify(selectedApproval.new_value, null, 2)}
|
||||
{(() => {
|
||||
const value = selectedApproval.new_value;
|
||||
return typeof value === 'string'
|
||||
? value
|
||||
: JSON.stringify(value, null, 2);
|
||||
})()}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Download, Filter } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import financialAuditService, { FinancialAuditRecord } from '../../features/payments/services/financialAuditService';
|
||||
import financialAuditService, { FinancialAuditRecord, FinancialAuditFilters } from '../../features/payments/services/financialAuditService';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const AuditTrailPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [records, setRecords] = useState<FinancialAuditRecord[]>([]);
|
||||
const [pagination, setPagination] = useState<unknown>(null);
|
||||
const [pagination, setPagination] = useState<{ page: number; limit: number; total: number; total_pages: number } | null>(null);
|
||||
const [filters, setFilters] = useState({
|
||||
action_type: '',
|
||||
user_id: '',
|
||||
@@ -33,7 +34,18 @@ const AuditTrailPage: React.FC = () => {
|
||||
const fetchAuditTrail = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await financialAuditService.getAuditTrail(filters);
|
||||
const auditFilters: FinancialAuditFilters = {
|
||||
action_type: filters.action_type || undefined,
|
||||
user_id: filters.user_id ? parseInt(filters.user_id) : undefined,
|
||||
start_date: filters.start_date || undefined,
|
||||
end_date: filters.end_date || undefined,
|
||||
payment_id: filters.payment_id ? parseInt(filters.payment_id) : undefined,
|
||||
invoice_id: filters.invoice_id ? parseInt(filters.invoice_id) : undefined,
|
||||
booking_id: filters.booking_id ? parseInt(filters.booking_id) : undefined,
|
||||
page: filters.page,
|
||||
limit: filters.limit,
|
||||
};
|
||||
const response = await financialAuditService.getAuditTrail(auditFilters);
|
||||
if (response.status === 'success' && response.data) {
|
||||
setRecords(response.data.audit_trail || []);
|
||||
setPagination(response.data.pagination);
|
||||
@@ -47,7 +59,18 @@ const AuditTrailPage: React.FC = () => {
|
||||
|
||||
const handleExport = async (format: 'csv' | 'json') => {
|
||||
try {
|
||||
const blob = await financialAuditService.exportAuditTrail(filters, format);
|
||||
const auditFilters: FinancialAuditFilters = {
|
||||
action_type: filters.action_type || undefined,
|
||||
user_id: filters.user_id ? parseInt(filters.user_id) : undefined,
|
||||
start_date: filters.start_date || undefined,
|
||||
end_date: filters.end_date || undefined,
|
||||
payment_id: filters.payment_id ? parseInt(filters.payment_id) : undefined,
|
||||
invoice_id: filters.invoice_id ? parseInt(filters.invoice_id) : undefined,
|
||||
booking_id: filters.booking_id ? parseInt(filters.booking_id) : undefined,
|
||||
page: filters.page,
|
||||
limit: filters.limit,
|
||||
};
|
||||
const blob = await financialAuditService.exportAuditTrail(auditFilters, format);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
|
||||
@@ -6,6 +6,7 @@ import glService from '../../features/payments/services/glService';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const FinancialReportsPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
|
||||
@@ -6,6 +6,7 @@ import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const GLManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurren
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { logger } from '../../shared/utils/logger';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const InvoiceManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
|
||||
@@ -9,6 +9,7 @@ import ExportButton from '../../shared/components/ExportButton';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { getPaymentStatusColor, getPaymentMethodLabel } from '../../shared/utils/paymentUtils';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const PaymentManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
|
||||
@@ -24,7 +24,9 @@ import {
|
||||
Trash2,
|
||||
Database,
|
||||
Smartphone,
|
||||
Tablet
|
||||
Tablet,
|
||||
Eye,
|
||||
EyeOff
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import authService from '../../features/auth/services/authService';
|
||||
@@ -38,7 +40,8 @@ import { useGlobalLoading } from '../../shared/contexts/LoadingContext';
|
||||
import { normalizeImageUrl } from '../../shared/utils/imageUtils';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { UserSession } from '../../features/auth/services/sessionService';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
import { isAxiosError } from 'axios';
|
||||
|
||||
const profileValidationSchema = yup.object().shape({
|
||||
name: yup
|
||||
@@ -83,7 +86,7 @@ type PasswordFormData = yup.InferType<typeof passwordValidationSchema>;
|
||||
const ProfilePage: React.FC = () => {
|
||||
const { userInfo, setUser } = useAuthStore();
|
||||
const { setLoading } = useGlobalLoading();
|
||||
const _navigate = useNavigate();
|
||||
// const _navigate = useNavigate(); // Reserved for future use
|
||||
const [activeTab, setActiveTab] = useState<'profile' | 'password' | 'mfa' | 'sessions' | 'gdpr'>('profile');
|
||||
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
|
||||
const [avatarError, setAvatarError] = useState<boolean>(false);
|
||||
@@ -105,8 +108,8 @@ const ProfilePage: React.FC = () => {
|
||||
const [showBackupCodes, setShowBackupCodes] = useState<string[] | null>(null);
|
||||
const [showMfaSecret, setShowMfaSecret] = useState<boolean>(false);
|
||||
const mfaAbortControllerRef = useRef<AbortController | null>(null);
|
||||
const _sessionsAbortControllerRef = useRef<AbortController | null>(null);
|
||||
const _gdprAbortControllerRef = useRef<AbortController | null>(null);
|
||||
// const _sessionsAbortControllerRef = useRef<AbortController | null>(null); // Reserved for future use
|
||||
// const _gdprAbortControllerRef = useRef<AbortController | null>(null); // Reserved for future use
|
||||
|
||||
|
||||
const fetchProfile = async () => {
|
||||
@@ -241,8 +244,16 @@ const ProfilePage: React.FC = () => {
|
||||
|
||||
if (response.status === 'success' || response.success) {
|
||||
const updatedUser = response.data?.user || response.data;
|
||||
if (updatedUser) {
|
||||
setUser(updatedUser);
|
||||
if (updatedUser && typeof updatedUser === 'object' && 'id' in updatedUser) {
|
||||
setUser({
|
||||
id: (updatedUser as { id: number }).id,
|
||||
name: ((updatedUser as { name?: string; full_name?: string }).name || (updatedUser as { name?: string; full_name?: string }).full_name || '') as string,
|
||||
email: (updatedUser as { email: string }).email,
|
||||
phone: ((updatedUser as { phone?: string; phone_number?: string }).phone || (updatedUser as { phone?: string; phone_number?: string }).phone_number) as string | undefined,
|
||||
avatar: (updatedUser as { avatar?: string }).avatar,
|
||||
role: (updatedUser as { role: string }).role,
|
||||
createdAt: ((updatedUser as { createdAt?: string; created_at?: string }).createdAt || (updatedUser as { createdAt?: string; created_at?: string }).created_at) as string | undefined,
|
||||
});
|
||||
toast.success('Profile updated successfully!');
|
||||
refetchProfile();
|
||||
}
|
||||
@@ -1158,7 +1169,7 @@ const SessionsTab: React.FC = () => {
|
||||
fetchSessions();
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (getUserFriendlyError(error) === 401) {
|
||||
if (isAxiosError(error) && error.response?.status === 401) {
|
||||
toast.warning('Your session has been revoked. You will be logged out.');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
@@ -1184,7 +1195,7 @@ const SessionsTab: React.FC = () => {
|
||||
fetchSessions();
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (getUserFriendlyError(error) === 401) {
|
||||
if (isAxiosError(error) && error.response?.status === 401) {
|
||||
toast.warning('All sessions have been revoked. You will be logged out.');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Play } from 'lucide-react';
|
||||
import { Play, AlertTriangle } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import reconciliationService, { ReconciliationException, ExceptionStatus } from '../../features/payments/services/reconciliationService';
|
||||
import reconciliationService, { ReconciliationException, ExceptionStatus, ExceptionStats } from '../../features/payments/services/reconciliationService';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const ReconciliationPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [exceptions, setExceptions] = useState<ReconciliationException[]>([]);
|
||||
const [stats, setStats] = useState<Record<string, unknown> | null>(null);
|
||||
const [stats, setStats] = useState<ExceptionStats | null>(null);
|
||||
const [selectedException, setSelectedException] = useState<ReconciliationException | null>(null);
|
||||
const [filter, setFilter] = useState<ExceptionStatus | 'all'>('all');
|
||||
const [comment, setComment] = useState('');
|
||||
|
||||
@@ -6,6 +6,7 @@ import accountantSecurityService, { AccountantSession, AccountantActivityLog, MF
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const SecurityManagementPage: React.FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
@@ -4,6 +4,7 @@ import apiKeyService, { APIKey, CreateAPIKeyData, UpdateAPIKeyData } from '../..
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const APIKeyManagementPage: React.FC = () => {
|
||||
const [apiKeys, setApiKeys] = useState<APIKey[]>([]);
|
||||
|
||||
@@ -40,6 +40,7 @@ import analyticsService, {
|
||||
import { SimpleBarChart, SimpleLineChart, SimplePieChart, KPICard } from '../../features/analytics/components/SimpleChart';
|
||||
import { exportData } from '../../shared/utils/exportUtils';
|
||||
import CustomReportBuilder from '../../features/analytics/components/CustomReportBuilder';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
type AnalyticsCategory = 'revenue' | 'operational' | 'guest' | 'financial' | 'comprehensive';
|
||||
|
||||
@@ -211,7 +212,7 @@ const AdvancedAnalyticsPage: React.FC = () => {
|
||||
title = 'Financial Analytics Report';
|
||||
} else if (comprehensiveData) {
|
||||
// Export comprehensive data
|
||||
exportDataArray = [comprehensiveData];
|
||||
exportDataArray = [comprehensiveData as unknown as Record<string, unknown>];
|
||||
filename = 'comprehensive-analytics';
|
||||
title = 'Comprehensive Analytics Report';
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import apiClient from '../../shared/services/apiClient';
|
||||
import { logger } from '../../shared/utils/logger';
|
||||
import { useRoomContext } from '../../features/rooms/contexts/RoomContext';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
type Tab = 'status-board' | 'maintenance' | 'housekeeping' | 'inspections' | 'room-types';
|
||||
|
||||
@@ -80,8 +81,8 @@ const AdvancedRoomManagementPage: React.FC = () => {
|
||||
const [roomTypes, setRoomTypes] = useState<Array<{ id: number; name: string }>>([]);
|
||||
const [uploadingImages, setUploadingImages] = useState(false);
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||
const [customAmenityInput, setCustomAmenityInput] = useState('');
|
||||
const [editingAmenity, setEditingAmenity] = useState<{ name: string; newName: string } | null>(null);
|
||||
const [_customAmenityInput, _setCustomAmenityInput] = useState('');
|
||||
const [_editingAmenity, _setEditingAmenity] = useState<{ name: string; newName: string } | null>(null);
|
||||
|
||||
// Room Types management state
|
||||
const [roomTypesList, setRoomTypesList] = useState<RoomType[]>([]);
|
||||
@@ -671,92 +672,93 @@ const AdvancedRoomManagementPage: React.FC = () => {
|
||||
});
|
||||
setSelectedFiles([]);
|
||||
setUploadingImages(false);
|
||||
setCustomAmenityInput('');
|
||||
setEditingAmenity(null);
|
||||
_setCustomAmenityInput('');
|
||||
_setEditingAmenity(null);
|
||||
};
|
||||
|
||||
// Amenities are now managed only at the room type level
|
||||
// Rooms automatically inherit amenities from their room type
|
||||
|
||||
const _handleAddCustomAmenity = () => {
|
||||
const trimmed = customAmenityInput.trim();
|
||||
if (trimmed && !roomFormData.amenities.includes(trimmed)) {
|
||||
setRoomFormData(prev => ({
|
||||
...prev,
|
||||
amenities: [...prev.amenities, trimmed]
|
||||
}));
|
||||
setCustomAmenityInput('');
|
||||
}
|
||||
};
|
||||
// Unused functions - commented out to fix TypeScript errors
|
||||
// const _handleAddCustomAmenity = () => {
|
||||
// const trimmed = customAmenityInput.trim();
|
||||
// if (trimmed && !roomFormData.amenities.includes(trimmed)) {
|
||||
// setRoomFormData(prev => ({
|
||||
// ...prev,
|
||||
// amenities: [...prev.amenities, trimmed]
|
||||
// }));
|
||||
// setCustomAmenityInput('');
|
||||
// }
|
||||
// };
|
||||
|
||||
const _handleRemoveAmenity = (amenity: string) => {
|
||||
setRoomFormData(prev => ({
|
||||
...prev,
|
||||
amenities: prev.amenities.filter(a => a !== amenity)
|
||||
}));
|
||||
};
|
||||
// const _handleRemoveAmenity = (amenity: string) => {
|
||||
// setRoomFormData(prev => ({
|
||||
// ...prev,
|
||||
// amenities: prev.amenities.filter(a => a !== amenity)
|
||||
// }));
|
||||
// };
|
||||
|
||||
const _handleEditAmenity = (amenity: string) => {
|
||||
setEditingAmenity({ name: amenity, newName: amenity });
|
||||
};
|
||||
// const _handleEditAmenity = (amenity: string) => {
|
||||
// setEditingAmenity({ name: amenity, newName: amenity });
|
||||
// };
|
||||
|
||||
const _handleSaveAmenityEdit = async () => {
|
||||
if (!editingAmenity || editingAmenity.name === editingAmenity.newName.trim()) {
|
||||
setEditingAmenity(null);
|
||||
return;
|
||||
}
|
||||
// const _handleSaveAmenityEdit = async () => {
|
||||
// if (!editingAmenity || editingAmenity.name === editingAmenity.newName.trim()) {
|
||||
// setEditingAmenity(null);
|
||||
// return;
|
||||
// }
|
||||
|
||||
const newName = editingAmenity.newName.trim();
|
||||
if (!newName) {
|
||||
toast.error('Amenity name cannot be empty');
|
||||
return;
|
||||
}
|
||||
// const newName = editingAmenity.newName.trim();
|
||||
// if (!newName) {
|
||||
// toast.error('Amenity name cannot be empty');
|
||||
// return;
|
||||
// }
|
||||
|
||||
try {
|
||||
await roomService.updateAmenity(editingAmenity.name, newName);
|
||||
toast.success(`Amenity "${editingAmenity.name}" updated to "${newName}"`);
|
||||
// try {
|
||||
// await roomService.updateAmenity(editingAmenity.name, newName);
|
||||
// toast.success(`Amenity "${editingAmenity.name}" updated to "${newName}"`);
|
||||
|
||||
setAvailableAmenities(prev => {
|
||||
const updated = prev.map(a => a === editingAmenity.name ? newName : a);
|
||||
return updated.sort();
|
||||
});
|
||||
// setAvailableAmenities(prev => {
|
||||
// const updated = prev.map(a => a === editingAmenity.name ? newName : a);
|
||||
// return updated.sort();
|
||||
// });
|
||||
|
||||
setRoomFormData(prev => ({
|
||||
...prev,
|
||||
amenities: prev.amenities.map(a => a === editingAmenity.name ? newName : a)
|
||||
}));
|
||||
// setRoomFormData(prev => ({
|
||||
// ...prev,
|
||||
// amenities: prev.amenities.map(a => a === editingAmenity.name ? newName : a)
|
||||
// }));
|
||||
|
||||
setEditingAmenity(null);
|
||||
await fetchAvailableAmenities();
|
||||
await refreshRooms();
|
||||
await refreshStatusBoard();
|
||||
} catch (error: unknown) {
|
||||
toast.error(getUserFriendlyError(error) || getUserFriendlyError(error) || 'Failed to update amenity');
|
||||
}
|
||||
};
|
||||
// setEditingAmenity(null);
|
||||
// await fetchAvailableAmenities();
|
||||
// await refreshRooms();
|
||||
// await refreshStatusBoard();
|
||||
// } catch (error: unknown) {
|
||||
// toast.error(getUserFriendlyError(error) || 'Failed to update amenity');
|
||||
// }
|
||||
// };
|
||||
|
||||
const _handleDeleteAmenity = async (amenity: string) => {
|
||||
if (!window.confirm(`Are you sure you want to delete "${amenity}"? This will remove it from all rooms and room types.`)) {
|
||||
return;
|
||||
}
|
||||
// const _handleDeleteAmenity = async (amenity: string) => {
|
||||
// if (!window.confirm(`Are you sure you want to delete "${amenity}"? This will remove it from all rooms and room types.`)) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
try {
|
||||
await roomService.deleteAmenity(amenity);
|
||||
toast.success(`Amenity "${amenity}" deleted successfully`);
|
||||
// try {
|
||||
// await roomService.deleteAmenity(amenity);
|
||||
// toast.success(`Amenity "${amenity}" deleted successfully`);
|
||||
|
||||
setAvailableAmenities(prev => prev.filter(a => a !== amenity));
|
||||
setRoomFormData(prev => ({
|
||||
...prev,
|
||||
amenities: prev.amenities.filter(a => a !== amenity)
|
||||
}));
|
||||
// setAvailableAmenities(prev => prev.filter(a => a !== amenity));
|
||||
// setRoomFormData(prev => ({
|
||||
// ...prev,
|
||||
// amenities: prev.amenities.filter(a => a !== amenity)
|
||||
// }));
|
||||
|
||||
await fetchAvailableAmenities();
|
||||
await refreshRooms();
|
||||
await refreshStatusBoard();
|
||||
} catch (error: unknown) {
|
||||
toast.error(getUserFriendlyError(error) || getUserFriendlyError(error) || 'Failed to delete amenity');
|
||||
}
|
||||
};
|
||||
// await fetchAvailableAmenities();
|
||||
// await refreshRooms();
|
||||
// await refreshStatusBoard();
|
||||
// } catch (error: unknown) {
|
||||
// toast.error(getUserFriendlyError(error) || 'Failed to delete amenity');
|
||||
// }
|
||||
// };
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.files || e.target.files.length === 0) {
|
||||
@@ -896,38 +898,38 @@ const AdvancedRoomManagementPage: React.FC = () => {
|
||||
setEditingRoom(response.data.room);
|
||||
} catch (error: unknown) {
|
||||
logger.error('Error deleting image', error);
|
||||
toast.error(getUserFriendlyError(error) || getUserFriendlyError(error) || 'Unable to delete image');
|
||||
toast.error(getUserFriendlyError(error) || 'Unable to delete image');
|
||||
}
|
||||
};
|
||||
|
||||
const _getRoomStatusBadge = (status: string) => {
|
||||
const badges: Record<string, { bg: string; text: string; label: string; border: string }> = {
|
||||
available: {
|
||||
bg: 'bg-gradient-to-r from-emerald-50 to-green-50',
|
||||
text: 'text-emerald-800',
|
||||
label: 'Available',
|
||||
border: 'border-emerald-200'
|
||||
},
|
||||
occupied: {
|
||||
bg: 'bg-gradient-to-r from-blue-50 to-indigo-50',
|
||||
text: 'text-blue-800',
|
||||
label: 'Occupied',
|
||||
border: 'border-blue-200'
|
||||
},
|
||||
maintenance: {
|
||||
bg: 'bg-gradient-to-r from-amber-50 to-yellow-50',
|
||||
text: 'text-amber-800',
|
||||
label: 'Maintenance',
|
||||
border: 'border-amber-200'
|
||||
},
|
||||
};
|
||||
const badge = badges[status] || badges.available;
|
||||
return (
|
||||
<span className={`px-4 py-1.5 rounded-full text-xs font-semibold border shadow-sm ${badge.bg} ${badge.text} ${badge.border}`}>
|
||||
{badge.label}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
// const _getRoomStatusBadge = (status: string) => {
|
||||
// const badges: Record<string, { bg: string; text: string; label: string; border: string }> = {
|
||||
// available: {
|
||||
// bg: 'bg-gradient-to-r from-emerald-50 to-green-50',
|
||||
// text: 'text-emerald-800',
|
||||
// label: 'Available',
|
||||
// border: 'border-emerald-200'
|
||||
// },
|
||||
// occupied: {
|
||||
// bg: 'bg-gradient-to-r from-blue-50 to-indigo-50',
|
||||
// text: 'text-blue-800',
|
||||
// label: 'Occupied',
|
||||
// border: 'border-blue-200'
|
||||
// },
|
||||
// maintenance: {
|
||||
// bg: 'bg-gradient-to-r from-amber-50 to-yellow-50',
|
||||
// text: 'text-amber-800',
|
||||
// label: 'Maintenance',
|
||||
// border: 'border-amber-200'
|
||||
// },
|
||||
// };
|
||||
// const badge = badges[status] || badges.available;
|
||||
// return (
|
||||
// <span className={`px-4 py-1.5 rounded-full text-xs font-semibold border shadow-sm ${badge.bg} ${badge.text} ${badge.border}`}>
|
||||
// {badge.label}
|
||||
// </span>
|
||||
// );
|
||||
// };
|
||||
|
||||
if (statusBoardLoading && statusBoardRooms.length === 0 && activeTab === 'status-board') {
|
||||
return <Loading />;
|
||||
|
||||
@@ -62,6 +62,7 @@ import analyticsService, {
|
||||
import { SimpleBarChart, SimpleLineChart, SimplePieChart, KPICard } from '../../features/analytics/components/SimpleChart';
|
||||
import { exportData } from '../../shared/utils/exportUtils';
|
||||
import CustomReportBuilder from '../../features/analytics/components/CustomReportBuilder';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
type AnalyticsTab = 'overview' | 'reports' | 'revenue' | 'operational' | 'guest' | 'financial' | 'audit-logs';
|
||||
|
||||
@@ -835,7 +836,7 @@ const AnalyticsDashboardPage: React.FC = () => {
|
||||
<label className="block text-sm font-semibold text-gray-900">Report Type</label>
|
||||
<select
|
||||
value={reportType}
|
||||
onChange={(e) => setReportType(e.target.value as 'revenue' | 'occupancy' | 'guest' | 'financial')}
|
||||
onChange={(e) => setReportType(e.target.value as 'daily' | 'weekly' | 'monthly' | 'yearly' | '')}
|
||||
className="w-full px-4 py-3 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition-all duration-200"
|
||||
>
|
||||
<option value="">All</option>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { toast } from 'react-toastify';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const ApprovalManagementPage: React.FC = () => {
|
||||
const [approvals, setApprovals] = useState<ApprovalRequest[]>([]);
|
||||
@@ -72,20 +73,20 @@ const ApprovalManagementPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const _handleCancel = async (id: number) => {
|
||||
if (!confirm('Are you sure you want to cancel this approval request?')) return;
|
||||
// const _handleCancel = async (id: number) => {
|
||||
// if (!confirm('Are you sure you want to cancel this approval request?')) return;
|
||||
|
||||
try {
|
||||
setProcessingId(id);
|
||||
await approvalService.cancelRequest(id);
|
||||
toast.success('Approval request cancelled');
|
||||
fetchApprovals();
|
||||
} catch (error: unknown) {
|
||||
toast.error(getUserFriendlyError(error) || 'Unable to cancel request');
|
||||
} finally {
|
||||
setProcessingId(null);
|
||||
}
|
||||
};
|
||||
// try {
|
||||
// setProcessingId(id);
|
||||
// await approvalService.cancelRequest(id);
|
||||
// toast.success('Approval request cancelled');
|
||||
// fetchApprovals();
|
||||
// } catch (error: unknown) {
|
||||
// toast.error(getUserFriendlyError(error) || 'Unable to cancel request');
|
||||
// } finally {
|
||||
// setProcessingId(null);
|
||||
// }
|
||||
// };
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
|
||||
@@ -5,6 +5,8 @@ import { toast } from 'react-toastify';
|
||||
import { logger } from '../../shared/utils/logger';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
import { isAxiosError } from 'axios';
|
||||
|
||||
const BackupManagementPage: React.FC = () => {
|
||||
const [backups, setBackups] = useState<Backup[]>([]);
|
||||
@@ -51,18 +53,22 @@ const BackupManagementPage: React.FC = () => {
|
||||
toast.success('Backup created successfully');
|
||||
fetchBackups();
|
||||
} catch (error: unknown) {
|
||||
const errorDetail = getUserFriendlyError(error);
|
||||
if (getUserFriendlyError(error) === 503 && errorDetail?.requires_installation) {
|
||||
// Show a more detailed error for missing mysqldump
|
||||
toast.error(
|
||||
<div>
|
||||
<p className="font-semibold mb-1">Backup service unavailable</p>
|
||||
<p className="text-sm">{errorDetail.message || errorDetail}</p>
|
||||
</div>,
|
||||
if (isAxiosError(error) && error.response?.status === 503) {
|
||||
const errorDetail = error.response?.data as { requires_installation?: boolean; message?: string } | undefined;
|
||||
if (errorDetail?.requires_installation) {
|
||||
// Show a more detailed error for missing mysqldump
|
||||
toast.error(
|
||||
<div>
|
||||
<p className="font-semibold mb-1">Backup service unavailable</p>
|
||||
<p className="text-sm">{errorDetail.message || getUserFriendlyError(error)}</p>
|
||||
</div>,
|
||||
{ autoClose: 10000 }
|
||||
);
|
||||
} else {
|
||||
toast.error(getUserFriendlyError(error) || 'Unable to create backup');
|
||||
}
|
||||
} else {
|
||||
toast.error(getUserFriendlyError(error)?.message || getUserFriendlyError(error) || getUserFriendlyError(error) || 'Unable to create backup');
|
||||
toast.error(getUserFriendlyError(error) || 'Unable to create backup');
|
||||
}
|
||||
} finally {
|
||||
setCreating(false);
|
||||
|
||||
@@ -6,6 +6,7 @@ import Pagination from '../../shared/components/Pagination';
|
||||
import ConfirmationDialog from '../../shared/components/ConfirmationDialog';
|
||||
import bannerServiceModule from '../../features/content/services/bannerService';
|
||||
import type { Banner } from '../../features/content/services/bannerService';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const {
|
||||
getAllBanners,
|
||||
@@ -113,7 +114,7 @@ const BannerManagementPage: React.FC = () => {
|
||||
try {
|
||||
setUploadingImage(true);
|
||||
const response = await uploadBannerImage(file);
|
||||
if (response.status === 'success' || response.success) {
|
||||
if ((response as { status?: string; success?: boolean }).status === 'success' || response.success) {
|
||||
setFormData({ ...formData, image_url: response.data.image_url });
|
||||
toast.success('Image uploaded successfully');
|
||||
}
|
||||
@@ -141,7 +142,7 @@ const BannerManagementPage: React.FC = () => {
|
||||
if (imageFile && !imageUrl) {
|
||||
setUploadingImage(true);
|
||||
const uploadResponse = await uploadBannerImage(imageFile);
|
||||
if (uploadResponse.status === 'success' || uploadResponse.success) {
|
||||
if ((uploadResponse as { status?: string; success?: boolean }).status === 'success' || uploadResponse.success) {
|
||||
imageUrl = uploadResponse.data.image_url;
|
||||
} else {
|
||||
throw new Error('Failed to upload image');
|
||||
|
||||
@@ -5,6 +5,7 @@ import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import ConfirmationDialog from '../../shared/components/ConfirmationDialog';
|
||||
import blogService, { BlogPost, BlogPostCreate, BlogPostUpdate, BlogSection } from '../../features/content/services/blogService';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const BlogManagementPage: React.FC = () => {
|
||||
const [posts, setPosts] = useState<BlogPost[]>([]);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { parseDateLocal } from '../../shared/utils/format';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import CreateBookingModal from '../../features/hotel_services/components/CreateBookingModal';
|
||||
import { logger } from '../../shared/utils/logger';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const BookingManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
@@ -158,7 +159,7 @@ const BookingManagementPage: React.FC = () => {
|
||||
const handleUpdateStatus = async (id: number, status: string) => {
|
||||
try {
|
||||
setUpdatingBookingId(id);
|
||||
await bookingService.updateBooking(id, { status } as { status: string });
|
||||
await bookingService.updateBooking(id, { status: status as Booking['status'] });
|
||||
toast.success('Status updated successfully');
|
||||
await fetchBookings();
|
||||
} catch (error: unknown) {
|
||||
@@ -203,15 +204,15 @@ const BookingManagementPage: React.FC = () => {
|
||||
logger.info('Invoice creation response', { response });
|
||||
|
||||
// Check response structure - handle different possible formats
|
||||
let invoice = null;
|
||||
let invoice: { id?: number | string } | null = null;
|
||||
if (response.status === 'success' && response.data) {
|
||||
// Try different possible response structures
|
||||
const responseData = response.data as { invoice?: unknown; data?: { invoice?: unknown } };
|
||||
invoice = responseData.invoice || responseData.data?.invoice || response.data;
|
||||
const responseData = response.data as { invoice?: { id?: number | string }; data?: { invoice?: { id?: number | string } } };
|
||||
invoice = (responseData.invoice || responseData.data?.invoice || response.data) as { id?: number | string } | null;
|
||||
logger.debug('Extracted invoice', { invoice });
|
||||
}
|
||||
|
||||
if (!invoice) {
|
||||
if (!invoice || !invoice.id) {
|
||||
logger.error('Failed to create invoice - no invoice in response', { response });
|
||||
toast.error(response.message || 'Failed to create invoice - no invoice data received');
|
||||
return;
|
||||
@@ -770,7 +771,7 @@ const BookingManagementPage: React.FC = () => {
|
||||
Additional Services
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{((selectedBooking as Booking & { service_usages: Array<{ id?: number; service_name?: string; name?: string; quantity?: number; price?: number; total?: number }> }).service_usages).map((service, idx: number) => (
|
||||
{((selectedBooking as Booking & { service_usages: Array<{ id?: number; service_name?: string; name?: string; quantity?: number; price?: number; unit_price?: number; total?: number; total_price?: number }> }).service_usages).map((service, idx: number) => (
|
||||
<div key={service.id || idx} className="flex justify-between items-center py-2 border-b border-purple-100 last:border-0">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-900">{service.service_name || service.name || 'Service'}</p>
|
||||
@@ -823,13 +824,13 @@ const BookingManagementPage: React.FC = () => {
|
||||
</p>
|
||||
</div>
|
||||
<span className={`text-xs px-2 py-1 rounded-full font-medium ${
|
||||
payment.payment_status === 'completed' || payment.payment_status === 'paid'
|
||||
payment.payment_status === 'completed'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: payment.payment_status === 'pending'
|
||||
? 'bg-yellow-100 text-yellow-700'
|
||||
: 'bg-red-100 text-red-700'
|
||||
}`}>
|
||||
{payment.payment_status === 'completed' || payment.payment_status === 'paid' ? 'Paid' : payment.payment_status || 'Pending'}
|
||||
{payment.payment_status === 'completed' ? 'Paid' : payment.payment_status || 'Pending'}
|
||||
</span>
|
||||
</div>
|
||||
{payment.transaction_id && (
|
||||
|
||||
@@ -6,6 +6,7 @@ import Loading from '../../shared/components/Loading';
|
||||
import CurrencyIcon from '../../shared/components/CurrencyIcon';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../shared/utils/format';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
interface ServiceItem {
|
||||
service_name: string;
|
||||
|
||||
@@ -9,6 +9,7 @@ import EmptyState from '../../shared/components/EmptyState';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import complaintService, { Complaint, ComplaintFilters } from '../../features/guest_management/services/complaintService';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const ComplaintManagementPage: React.FC = () => {
|
||||
const [complaints, setComplaints] = useState<Complaint[]>([]);
|
||||
@@ -92,7 +93,7 @@ const ComplaintManagementPage: React.FC = () => {
|
||||
toast.success('Complaint status updated');
|
||||
fetchComplaints();
|
||||
if (selectedComplaint?.id === complaintId) {
|
||||
setSelectedComplaint({ ...selectedComplaint, status: status as 'pending' | 'in_progress' | 'resolved' | 'closed' });
|
||||
setSelectedComplaint({ ...selectedComplaint, status: status as 'open' | 'in_progress' | 'resolved' | 'closed' | 'escalated' });
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
toast.error(getUserFriendlyError(error) || 'Unable to update complaint');
|
||||
@@ -305,7 +306,7 @@ const ComplaintDetailModal: React.FC<{
|
||||
onClose: () => void;
|
||||
onResolve: () => void;
|
||||
onUpdateStatus: (id: number, status: string) => void;
|
||||
}> = ({ complaint, onClose, onResolve, _onUpdateStatus }) => {
|
||||
}> = ({ complaint, onClose, onResolve }) => {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
||||
@@ -385,7 +386,7 @@ const ResolveComplaintModal: React.FC<{
|
||||
onClose: () => void;
|
||||
onResolve: (resolution: string, rating?: number, feedback?: string) => void;
|
||||
resolving: boolean;
|
||||
}> = ({ _complaint, onClose, onResolve, resolving }) => {
|
||||
}> = ({ onClose, onResolve, resolving }) => {
|
||||
const [resolution, setResolution] = useState('');
|
||||
const [rating, setRating] = useState<number | undefined>();
|
||||
const [feedback, setFeedback] = useState('');
|
||||
|
||||
@@ -2,12 +2,14 @@ import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Download,
|
||||
Shield,
|
||||
Activity
|
||||
Activity,
|
||||
DollarSign
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import complianceService, { ComplianceReport, GDPRSummary } from '../../features/security/services/complianceService';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const ComplianceReportingPage: React.FC = () => {
|
||||
const [report, setReport] = useState<ComplianceReport | null>(null);
|
||||
|
||||
@@ -8,6 +8,7 @@ import adminPrivacyService, {
|
||||
CookiePolicySettingsResponse,
|
||||
} from '../../features/content/services/adminPrivacyService';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const CookieSettingsPage: React.FC = () => {
|
||||
const [policy, setPolicy] = useState<CookiePolicySettings>({
|
||||
|
||||
@@ -5,6 +5,7 @@ import systemSettingsService, { PlatformCurrencyResponse } from '../../features/
|
||||
import { useCurrency } from '../../features/payments/contexts/CurrencyContext';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import { getCurrencySymbol } from '../../shared/utils/format';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const CurrencySettingsPage: React.FC = () => {
|
||||
const { currency, supportedCurrencies, refreshCurrency } = useCurrency();
|
||||
|
||||
@@ -7,6 +7,7 @@ import Loading from '../../shared/components/Loading';
|
||||
import apiClient from '../../shared/services/apiClient';
|
||||
import { logger } from '../../shared/utils/logger';
|
||||
import { useRoomContext } from '../../features/rooms/contexts/RoomContext';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const EditRoomPage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -26,7 +27,7 @@ const EditRoomPage: React.FC = () => {
|
||||
room_number: '',
|
||||
floor: 1,
|
||||
room_type_id: 1,
|
||||
status: 'available' as 'available' | 'occupied' | 'maintenance',
|
||||
status: 'available' as 'available' | 'occupied' | 'maintenance' | 'cleaning' | 'reserved',
|
||||
featured: false,
|
||||
price: '',
|
||||
description: '',
|
||||
@@ -730,7 +731,7 @@ const EditRoomPage: React.FC = () => {
|
||||
</label>
|
||||
<select
|
||||
value={formData.status}
|
||||
onChange={(e) => setFormData({ ...formData, status: e.target.value as 'available' | 'occupied' | 'maintenance' | 'reserved' | 'out_of_order' })}
|
||||
onChange={(e) => setFormData({ ...formData, status: e.target.value as 'available' | 'occupied' | 'maintenance' | 'cleaning' | 'reserved' })}
|
||||
className="w-full px-5 py-3.5 bg-gradient-to-br from-white to-slate-50/50 border-2 border-slate-200 rounded-2xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100/50 transition-all duration-300 text-slate-800 font-medium shadow-sm hover:shadow-md focus:shadow-lg cursor-pointer appearance-none bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNiA5TDEyIDE1TDE4IDkiIHN0cm9rZT0iIzkyOUE1IiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvc3ZnPg==')] bg-[length:20px] bg-[right_1rem_center] bg-no-repeat pr-12"
|
||||
required
|
||||
>
|
||||
|
||||
@@ -15,9 +15,51 @@ import { toast } from 'react-toastify';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
type CampaignTab = 'campaigns' | 'segments' | 'templates' | 'drip-sequences' | 'analytics';
|
||||
|
||||
type CampaignFormData = {
|
||||
name: string;
|
||||
subject: string;
|
||||
html_content: string;
|
||||
text_content: string;
|
||||
campaign_type: string;
|
||||
segment_id: number | undefined;
|
||||
scheduled_at: string;
|
||||
template_id: number | undefined;
|
||||
from_name: string;
|
||||
from_email: string;
|
||||
track_opens: boolean;
|
||||
track_clicks: boolean;
|
||||
recipient_type: 'users' | 'subscribers' | 'both';
|
||||
};
|
||||
|
||||
type SegmentFormData = {
|
||||
name: string;
|
||||
description: string;
|
||||
criteria: {
|
||||
role: string;
|
||||
has_bookings: boolean | undefined;
|
||||
is_vip: boolean | undefined;
|
||||
last_booking_days: number | undefined;
|
||||
};
|
||||
};
|
||||
|
||||
type TemplateFormData = {
|
||||
name: string;
|
||||
subject: string;
|
||||
html_content: string;
|
||||
text_content: string;
|
||||
category: string;
|
||||
};
|
||||
|
||||
type DripFormData = {
|
||||
name: string;
|
||||
description: string;
|
||||
trigger_event: string;
|
||||
};
|
||||
|
||||
const EmailCampaignManagementPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<CampaignTab>('campaigns');
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -238,9 +280,10 @@ const EmailCampaignManagementPage: React.FC = () => {
|
||||
html_content: '',
|
||||
text_content: '',
|
||||
campaign_type: 'newsletter',
|
||||
segment_id: undefined,
|
||||
segment_id: undefined as number | undefined,
|
||||
scheduled_at: '',
|
||||
template_id: undefined,
|
||||
template_id: undefined as number | undefined,
|
||||
recipient_type: 'users' as 'users' | 'subscribers' | 'both',
|
||||
from_name: '',
|
||||
from_email: '',
|
||||
track_opens: true,
|
||||
@@ -703,8 +746,8 @@ const AnalyticsTab: React.FC<{ analytics: CampaignAnalytics }> = ({ analytics })
|
||||
);
|
||||
|
||||
const CampaignModal: React.FC<{
|
||||
form: typeof campaignForm;
|
||||
setForm: (form: typeof campaignForm) => void;
|
||||
form: CampaignFormData;
|
||||
setForm: (form: CampaignFormData) => void;
|
||||
segments: CampaignSegment[];
|
||||
templates: EmailTemplate[];
|
||||
onSave: () => void;
|
||||
@@ -800,8 +843,8 @@ const CampaignModal: React.FC<{
|
||||
);
|
||||
|
||||
const SegmentModal: React.FC<{
|
||||
form: typeof segmentForm;
|
||||
setForm: (form: typeof segmentForm) => void;
|
||||
form: SegmentFormData;
|
||||
setForm: (form: SegmentFormData) => void;
|
||||
onSave: () => void;
|
||||
onClose: () => void;
|
||||
}> = ({ form, setForm, onSave, onClose }) => (
|
||||
@@ -865,8 +908,8 @@ const SegmentModal: React.FC<{
|
||||
);
|
||||
|
||||
const TemplateModal: React.FC<{
|
||||
form: typeof templateForm;
|
||||
setForm: (form: typeof templateForm) => void;
|
||||
form: TemplateFormData;
|
||||
setForm: (form: TemplateFormData) => void;
|
||||
onSave: () => void;
|
||||
onClose: () => void;
|
||||
}> = ({ form, setForm, onSave, onClose }) => (
|
||||
@@ -927,8 +970,8 @@ const TemplateModal: React.FC<{
|
||||
);
|
||||
|
||||
const DripSequenceModal: React.FC<{
|
||||
form: typeof dripForm;
|
||||
setForm: (form: typeof dripForm) => void;
|
||||
form: DripFormData;
|
||||
setForm: (form: DripFormData) => void;
|
||||
onSave: () => void;
|
||||
onClose: () => void;
|
||||
}> = ({ form, setForm, onSave, onClose }) => (
|
||||
|
||||
@@ -12,6 +12,7 @@ import financialAuditService, { FinancialAuditRecord, FinancialAuditFilters } fr
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { exportData } from '../../shared/utils/exportUtils';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const FinancialAuditTrailPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
@@ -90,7 +91,11 @@ const FinancialAuditTrailPage: React.FC = () => {
|
||||
'Created At': formatDate(record.created_at),
|
||||
}));
|
||||
|
||||
exportData(csvData, 'financial_audit_trail', 'csv');
|
||||
exportData({
|
||||
data: csvData,
|
||||
filename: 'financial_audit_trail',
|
||||
format: 'csv',
|
||||
});
|
||||
toast.success('Audit trail exported successfully');
|
||||
} catch (error: unknown) {
|
||||
toast.error(getUserFriendlyError(error) || 'Unable to export audit trail');
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Download, Trash2 } from 'lucide-react';
|
||||
import gdprService, { GDPRRequest } from '../../features/compliance/services/gdprService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
|
||||
const GDPRManagementPage: React.FC = () => {
|
||||
const [requests, setRequests] = useState<GDPRRequest[]>([]);
|
||||
|
||||
@@ -7,6 +7,7 @@ import Pagination from '../../shared/components/Pagination';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import CreateGroupBookingModal from '../../features/hotel_services/components/CreateGroupBookingModal';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const GroupBookingManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
|
||||
@@ -22,6 +22,7 @@ import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { logger } from '../../shared/utils/logger';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
type TabType = 'list' | 'profile';
|
||||
|
||||
@@ -108,7 +109,7 @@ const GuestProfilePage: React.FC = () => {
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const handleFilterChange = (key: keyof GuestSearchParams, value: string | number | undefined) => {
|
||||
const handleFilterChange = (key: keyof GuestSearchParams, value: string | number | boolean | undefined) => {
|
||||
setFilters({ ...filters, [key]: value, page: 1 });
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Search, Plus, Edit, Eye, CheckCircle, Clock, X, FileCheck, AlertCircle } from 'lucide-react';
|
||||
import advancedRoomService, { RoomInspection } from '../../features/rooms/services/advancedRoomService';
|
||||
import advancedRoomService, { RoomInspection, InspectionChecklistItem } from '../../features/rooms/services/advancedRoomService';
|
||||
import roomService, { Room } from '../../features/rooms/services/roomService';
|
||||
import userService, { User } from '../../features/auth/services/userService';
|
||||
import { toast } from 'react-toastify';
|
||||
@@ -8,6 +8,7 @@ import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
|
||||
const InspectionManagementPage: React.FC = () => {
|
||||
@@ -37,7 +38,7 @@ const InspectionManagementPage: React.FC = () => {
|
||||
inspection_type: 'routine',
|
||||
scheduled_at: new Date(),
|
||||
inspected_by: 0,
|
||||
checklist_items: [] as Array<{ id?: number; item: string; checked: boolean; notes?: string }>,
|
||||
checklist_items: [] as InspectionChecklistItem[],
|
||||
});
|
||||
|
||||
const inspectionTypes = [
|
||||
@@ -147,7 +148,14 @@ const InspectionManagementPage: React.FC = () => {
|
||||
toast.success('Inspection updated successfully');
|
||||
} else {
|
||||
// Create new inspection
|
||||
const dataToSubmit: Record<string, unknown> = {
|
||||
const dataToSubmit: {
|
||||
room_id: number;
|
||||
inspection_type: string;
|
||||
scheduled_at: string;
|
||||
checklist_items: InspectionChecklistItem[];
|
||||
booking_id?: number;
|
||||
inspected_by?: number;
|
||||
} = {
|
||||
room_id: formData.room_id,
|
||||
inspection_type: formData.inspection_type,
|
||||
scheduled_at: formData.scheduled_at.toISOString(),
|
||||
@@ -531,7 +539,11 @@ const InspectionManagementPage: React.FC = () => {
|
||||
<label className="block text-sm font-semibold text-slate-700 mb-2">Scheduled At *</label>
|
||||
<DatePicker
|
||||
selected={formData.scheduled_at}
|
||||
onChange={(date: Date) => setFormData({ ...formData, scheduled_at: date })}
|
||||
onChange={(date: Date | null) => {
|
||||
if (date) {
|
||||
setFormData({ ...formData, scheduled_at: date });
|
||||
}
|
||||
}}
|
||||
showTimeSelect
|
||||
dateFormat="MMMM d, yyyy h:mm aa"
|
||||
className="w-full px-4 py-2 border-2 border-slate-200 rounded-xl focus:border-amber-400"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { toast } from 'react-toastify';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const InventoryManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
|
||||
@@ -6,6 +6,7 @@ import { toast } from 'react-toastify';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import { validateInvoiceId } from '../../shared/utils/routeValidation';
|
||||
import { logger } from '../../shared/utils/logger';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const InvoiceEditPage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
@@ -8,6 +8,7 @@ import ExportButton from '../../shared/components/ExportButton';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const InvoiceManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
|
||||
@@ -20,6 +20,7 @@ import ConfirmationDialog from '../../shared/components/ConfirmationDialog';
|
||||
import loyaltyService, { LoyaltyTier, LoyaltyReward } from '../../features/loyalty/services/loyaltyService';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { logger } from '../../shared/utils/logger';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
type Tab = 'users' | 'tiers' | 'rewards';
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
|
||||
const MaintenanceManagementPage: React.FC = () => {
|
||||
@@ -87,7 +89,7 @@ const MaintenanceManagementPage: React.FC = () => {
|
||||
const fetchRecords = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const params: { page: number; limit: number; room_id?: number; status?: string } = {
|
||||
const params: { page: number; limit: number; room_id?: number; status?: string; maintenance_type?: string; priority?: string } = {
|
||||
page: currentPage,
|
||||
limit: itemsPerPage,
|
||||
};
|
||||
@@ -163,7 +165,21 @@ const MaintenanceManagementPage: React.FC = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const dataToSubmit: Record<string, unknown> = {
|
||||
const dataToSubmit: {
|
||||
room_id: number;
|
||||
maintenance_type: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
scheduled_start: string;
|
||||
scheduled_end?: string;
|
||||
assigned_to?: number;
|
||||
estimated_cost?: number;
|
||||
blocks_room: boolean;
|
||||
block_start?: string;
|
||||
block_end?: string;
|
||||
priority: string;
|
||||
notes?: string;
|
||||
} = {
|
||||
room_id: formData.room_id,
|
||||
maintenance_type: formData.maintenance_type,
|
||||
title: formData.title,
|
||||
@@ -637,7 +653,11 @@ const MaintenanceManagementPage: React.FC = () => {
|
||||
</label>
|
||||
<DatePicker
|
||||
selected={formData.scheduled_start}
|
||||
onChange={(date: Date) => setFormData({ ...formData, scheduled_start: date })}
|
||||
onChange={(date: Date | null) => {
|
||||
if (date) {
|
||||
setFormData({ ...formData, scheduled_start: date });
|
||||
}
|
||||
}}
|
||||
showTimeSelect
|
||||
dateFormat="MMMM d, yyyy h:mm aa"
|
||||
className="w-full px-4 py-2 border-2 border-slate-200 rounded-xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100"
|
||||
|
||||
@@ -8,6 +8,7 @@ import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { logger } from '../../shared/utils/logger';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const PackageManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
|
||||
@@ -9,6 +9,8 @@ import ExportButton from '../../shared/components/ExportButton';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { PAYMENT_STATUS } from '../../shared/constants/bookingConstants';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
import { logger } from '../../shared/utils/logger';
|
||||
|
||||
const PaymentManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
@@ -17,6 +19,7 @@ const PaymentManagementPage: React.FC = () => {
|
||||
const [filters, setFilters] = useState({
|
||||
search: '',
|
||||
method: '',
|
||||
status: '',
|
||||
from: '',
|
||||
to: '',
|
||||
});
|
||||
@@ -388,15 +391,15 @@ const PaymentManagementPage: React.FC = () => {
|
||||
<h3 className="text-lg font-semibold mb-2 text-amber-100">Total Revenue</h3>
|
||||
<p className="text-4xl font-bold">
|
||||
{formatCurrency(payments
|
||||
.filter(p => p.payment_status === PAYMENT_STATUS.PAID || p.payment_status === 'completed')
|
||||
.filter(p => p.payment_status === 'completed')
|
||||
.reduce((sum, p) => sum + p.amount, 0))}
|
||||
</p>
|
||||
<p className="text-sm mt-3 text-amber-100/90">
|
||||
Total {payments.filter(p => p.payment_status === PAYMENT_STATUS.PAID || p.payment_status === 'completed').length} paid transaction{payments.filter(p => p.payment_status === PAYMENT_STATUS.PAID || p.payment_status === 'completed').length !== 1 ? 's' : ''}
|
||||
Total {payments.filter(p => p.payment_status === 'completed').length} paid transaction{payments.filter(p => p.payment_status === 'completed').length !== 1 ? 's' : ''}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white/20 backdrop-blur-sm p-6 rounded-2xl">
|
||||
<div className="text-5xl font-bold text-white/80">{payments.filter(p => p.payment_status === PAYMENT_STATUS.PAID || p.payment_status === 'completed').length}</div>
|
||||
<div className="text-5xl font-bold text-white/80">{payments.filter(p => p.payment_status === 'completed').length}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,9 @@ import { useGlobalLoading } from '../../shared/contexts/LoadingContext';
|
||||
import { normalizeImageUrl } from '../../shared/utils/imageUtils';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { UserSession } from '../../features/auth/services/sessionService';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
// import { useNavigate } from 'react-router-dom'; // Unused
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
import { isAxiosError } from 'axios';
|
||||
|
||||
const profileValidationSchema = yup.object().shape({
|
||||
name: yup
|
||||
@@ -85,7 +87,7 @@ type PasswordFormData = yup.InferType<typeof passwordValidationSchema>;
|
||||
const ProfilePage: React.FC = () => {
|
||||
const { userInfo, setUser } = useAuthStore();
|
||||
const { setLoading } = useGlobalLoading();
|
||||
const _navigate = useNavigate();
|
||||
// const _navigate = useNavigate(); // Unused
|
||||
const [activeTab, setActiveTab] = useState<'profile' | 'password' | 'mfa' | 'sessions' | 'gdpr'>('profile');
|
||||
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
|
||||
const [avatarError, setAvatarError] = useState<boolean>(false);
|
||||
@@ -107,8 +109,8 @@ const ProfilePage: React.FC = () => {
|
||||
const [showBackupCodes, setShowBackupCodes] = useState<string[] | null>(null);
|
||||
const [showMfaSecret, setShowMfaSecret] = useState<boolean>(false);
|
||||
const mfaAbortControllerRef = useRef<AbortController | null>(null);
|
||||
const _sessionsAbortControllerRef = useRef<AbortController | null>(null);
|
||||
const _gdprAbortControllerRef = useRef<AbortController | null>(null);
|
||||
// const _sessionsAbortControllerRef = useRef<AbortController | null>(null); // Reserved for future use
|
||||
// const _gdprAbortControllerRef = useRef<AbortController | null>(null); // Reserved for future use
|
||||
|
||||
|
||||
const fetchProfile = async () => {
|
||||
@@ -244,8 +246,16 @@ const ProfilePage: React.FC = () => {
|
||||
|
||||
if (response.status === 'success' || response.success) {
|
||||
const updatedUser = response.data?.user || response.data;
|
||||
if (updatedUser) {
|
||||
setUser(updatedUser);
|
||||
if (updatedUser && typeof updatedUser === 'object' && 'id' in updatedUser) {
|
||||
setUser({
|
||||
id: (updatedUser as { id: number }).id,
|
||||
name: ((updatedUser as { name?: string; full_name?: string }).name || (updatedUser as { name?: string; full_name?: string }).full_name || '') as string,
|
||||
email: (updatedUser as { email: string }).email,
|
||||
phone: ((updatedUser as { phone?: string; phone_number?: string }).phone || (updatedUser as { phone?: string; phone_number?: string }).phone_number) as string | undefined,
|
||||
avatar: (updatedUser as { avatar?: string }).avatar,
|
||||
role: (updatedUser as { role: string }).role,
|
||||
createdAt: ((updatedUser as { createdAt?: string; created_at?: string }).createdAt || (updatedUser as { createdAt?: string; created_at?: string }).created_at) as string | undefined,
|
||||
});
|
||||
toast.success('Profile updated successfully!');
|
||||
refetchProfile();
|
||||
}
|
||||
@@ -1161,7 +1171,7 @@ const SessionsTab: React.FC = () => {
|
||||
fetchSessions();
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (getUserFriendlyError(error) === 401) {
|
||||
if (isAxiosError(error) && error.response?.status === 401) {
|
||||
toast.warning('Your session has been revoked. You will be logged out.');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
@@ -1187,7 +1197,7 @@ const SessionsTab: React.FC = () => {
|
||||
fetchSessions();
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (getUserFriendlyError(error) === 401) {
|
||||
if (isAxiosError(error) && error.response?.status === 401) {
|
||||
toast.warning('All sessions have been revoked. You will be logged out.');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
|
||||
@@ -134,7 +134,7 @@ const PromotionManagementPage: React.FC = () => {
|
||||
await promotionService.updatePromotion(editingPromotion.id, submitData);
|
||||
toast.success('Promotion updated successfully');
|
||||
} else {
|
||||
await promotionService.createPromotion(submitData);
|
||||
await promotionService.createPromotion(submitData as CreatePromotionData);
|
||||
toast.success('Promotion added successfully');
|
||||
}
|
||||
setShowModal(false);
|
||||
|
||||
@@ -13,8 +13,9 @@ import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { useCurrency } from '../../features/payments/contexts/CurrencyContext';
|
||||
import promotionService, { Promotion } from '../../features/loyalty/services/promotionService';
|
||||
import promotionService, { Promotion, CreatePromotionData } from '../../features/loyalty/services/promotionService';
|
||||
import { getRoomTypes } from '../../features/rooms/services/roomService';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
interface RoomType {
|
||||
id: number;
|
||||
@@ -137,7 +138,7 @@ const PromotionsManagementPage: React.FC = () => {
|
||||
await promotionService.updatePromotion(editingPromotion.id, submitData);
|
||||
toast.success('Promotion updated successfully');
|
||||
} else {
|
||||
await promotionService.createPromotion(submitData);
|
||||
await promotionService.createPromotion(submitData as unknown as CreatePromotionData);
|
||||
toast.success('Promotion added successfully');
|
||||
}
|
||||
setShowPromotionModal(false);
|
||||
|
||||
@@ -7,6 +7,7 @@ import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { logger } from '../../shared/utils/logger';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const RatePlanManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
LogIn,
|
||||
LogOut,
|
||||
@@ -26,6 +27,7 @@ import Pagination from '../../shared/components/Pagination';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../shared/utils/format';
|
||||
import CreateBookingModal from '../../features/hotel_services/components/CreateBookingModal';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
type ReceptionTab = 'overview' | 'check-in' | 'check-out' | 'bookings';
|
||||
|
||||
@@ -43,6 +45,7 @@ interface ServiceItem {
|
||||
}
|
||||
|
||||
const ReceptionDashboardPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
const [activeTab, setActiveTab] = useState<ReceptionTab>('overview');
|
||||
|
||||
@@ -313,7 +316,7 @@ const ReceptionDashboardPage: React.FC = () => {
|
||||
const handleUpdateBookingStatus = async (id: number, status: string) => {
|
||||
try {
|
||||
setUpdatingBookingId(id);
|
||||
await bookingService.updateBooking(id, { status } as { status: string });
|
||||
await bookingService.updateBooking(id, { status: status as Booking['status'] });
|
||||
toast.success('Status updated successfully');
|
||||
await fetchBookings();
|
||||
} catch (error: unknown) {
|
||||
@@ -1599,13 +1602,13 @@ const ReceptionDashboardPage: React.FC = () => {
|
||||
</p>
|
||||
</div>
|
||||
<span className={`text-xs px-2 py-1 rounded-full font-medium ${
|
||||
payment.payment_status === 'completed' || payment.payment_status === 'paid'
|
||||
payment.payment_status === 'completed'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: payment.payment_status === 'pending'
|
||||
? 'bg-yellow-100 text-yellow-700'
|
||||
: 'bg-red-100 text-red-700'
|
||||
}`}>
|
||||
{payment.payment_status === 'completed' || payment.payment_status === 'paid' ? 'Paid' : payment.payment_status || 'Pending'}
|
||||
{payment.payment_status === 'completed' ? 'Paid' : payment.payment_status || 'Pending'}
|
||||
</span>
|
||||
</div>
|
||||
{payment.transaction_id && (
|
||||
|
||||
@@ -3,6 +3,7 @@ import reviewService, { Review } from '../../features/reviews/services/reviewSer
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const ReviewManagementPage: React.FC = () => {
|
||||
const [reviews, setReviews] = useState<Review[]>([]);
|
||||
|
||||
@@ -14,12 +14,13 @@ import {
|
||||
AlertCircle,
|
||||
Info
|
||||
} from 'lucide-react';
|
||||
import { securityService, SecurityEvent, SecurityStats, OAuthProvider, DataSubjectRequest } from '../../features/security/services/securityService';
|
||||
import { securityService, SecurityEvent, SecurityStats, OAuthProvider, DataSubjectRequest, IPBlacklist } from '../../features/security/services/securityService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { logger } from '../../shared/utils/logger';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
type SecurityTab = 'events' | 'stats' | 'ip-whitelist' | 'ip-blacklist' | 'oauth' | 'gdpr' | 'scan';
|
||||
|
||||
@@ -595,7 +596,7 @@ const IPWhitelistTab: React.FC = () => {
|
||||
|
||||
// IP Blacklist Tab Component
|
||||
const IPBlacklistTab: React.FC = () => {
|
||||
const [ips, setIPs] = useState<Array<{ id: number; ip_address: string; description?: string; created_at?: string }>>([]);
|
||||
const [ips, setIPs] = useState<IPBlacklist[]>([]);
|
||||
const [, setLoading] = useState(false);
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [newIP, setNewIP] = useState({ ip_address: '', reason: '' });
|
||||
@@ -1122,7 +1123,7 @@ const OAuthProvidersTab: React.FC = () => {
|
||||
const GDPRRequestsTab: React.FC = () => {
|
||||
const [requests, setRequests] = useState<DataSubjectRequest[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [_selectedRequest, setSelectedRequest] = useState<DataSubjectRequest | null>(null);
|
||||
const [selectedRequest, setSelectedRequest] = useState<DataSubjectRequest | null>(null);
|
||||
const [filters, setFilters] = useState({
|
||||
status: '',
|
||||
request_type: ''
|
||||
@@ -1419,7 +1420,23 @@ const GDPRRequestsTab: React.FC = () => {
|
||||
// Security Scan Tab Component
|
||||
const SecurityScanTab: React.FC = () => {
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [scanResults, setScanResults] = useState<Record<string, unknown> | null>(null);
|
||||
const [scanResults, setScanResults] = useState<{
|
||||
duration_seconds?: number;
|
||||
critical_issues?: number;
|
||||
high_issues?: number;
|
||||
medium_issues?: number;
|
||||
low_issues?: number;
|
||||
total_issues?: number;
|
||||
checks?: Array<{
|
||||
check_name?: string;
|
||||
name?: string;
|
||||
status?: string;
|
||||
severity?: string;
|
||||
description?: string;
|
||||
recommendation?: string;
|
||||
issue_count?: number;
|
||||
}>;
|
||||
} | null>(null);
|
||||
const [scheduleInterval, setScheduleInterval] = useState(24);
|
||||
const [scheduled, setScheduled] = useState(false);
|
||||
|
||||
@@ -1557,7 +1574,7 @@ const SecurityScanTab: React.FC = () => {
|
||||
|
||||
{/* Check Results */}
|
||||
<div className="space-y-3">
|
||||
{scanResults.checks?.map((check: { name?: string; status?: string; message?: string; severity?: string }, index: number) => (
|
||||
{scanResults.checks?.map((check: { name?: string; check_name?: string; status?: string; message?: string; severity?: string; description?: string; recommendation?: string; issue_count?: number }, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`border rounded-lg sm:rounded-xl p-3 sm:p-4 ${
|
||||
@@ -1570,8 +1587,8 @@ const SecurityScanTab: React.FC = () => {
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2 mb-2">
|
||||
<h5 className="font-semibold text-sm sm:text-base">{check.check_name}</h5>
|
||||
<span className={`px-2 py-1 rounded-full text-xs border flex-shrink-0 ${getSeverityColor(check.severity)}`}>
|
||||
{check.severity}
|
||||
<span className={`px-2 py-1 rounded-full text-xs border flex-shrink-0 ${getSeverityColor(check.severity || 'low')}`}>
|
||||
{check.severity || 'low'}
|
||||
</span>
|
||||
<span className={`px-2 py-1 rounded-full text-xs flex-shrink-0 ${
|
||||
check.status === 'failed' ? 'bg-red-100 text-red-800' :
|
||||
@@ -1587,9 +1604,9 @@ const SecurityScanTab: React.FC = () => {
|
||||
💡 Recommendation: {check.recommendation}
|
||||
</p>
|
||||
)}
|
||||
{check.issue_count > 0 && (
|
||||
{(check.issue_count ?? 0) > 0 && (
|
||||
<p className="text-xs sm:text-sm text-gray-600 mt-2">
|
||||
Issues found: {check.issue_count}
|
||||
Issues found: {check.issue_count ?? 0}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Plus, Search, Edit, Trash2, X, Upload } from 'lucide-react';
|
||||
import serviceService, { Service } from '../../features/hotel_services/services/serviceService';
|
||||
import serviceService, { Service, UpdateServiceData, CreateServiceData } from '../../features/hotel_services/services/serviceService';
|
||||
import { ServiceSection } from '../../features/content/pages/ServiceDetailPage';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import ConfirmationDialog from '../../shared/components/ConfirmationDialog';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const ServiceManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
@@ -83,10 +84,10 @@ const ServiceManagementPage: React.FC = () => {
|
||||
};
|
||||
|
||||
if (editingService) {
|
||||
await serviceService.updateService(editingService.id, dataToSubmit);
|
||||
await serviceService.updateService(editingService.id, dataToSubmit as UpdateServiceData);
|
||||
toast.success('Service updated successfully');
|
||||
} else {
|
||||
await serviceService.createService(dataToSubmit);
|
||||
await serviceService.createService(dataToSubmit as CreateServiceData);
|
||||
toast.success('Service added successfully');
|
||||
}
|
||||
setShowModal(false);
|
||||
@@ -522,7 +523,7 @@ const ServiceManagementPage: React.FC = () => {
|
||||
value={section?.type || 'text'}
|
||||
onChange={(e) => {
|
||||
const updatedSections = [...formData.sections];
|
||||
updatedSections[sectionIndex] = { ...updatedSections[sectionIndex], type: e.target.value };
|
||||
updatedSections[sectionIndex] = { ...updatedSections[sectionIndex], type: e.target.value as ServiceSection['type'] };
|
||||
setFormData({ ...formData, sections: updatedSections });
|
||||
}}
|
||||
className="px-3 py-1.5 border border-slate-300 rounded text-sm"
|
||||
|
||||
@@ -48,6 +48,7 @@ import { recaptchaService, RecaptchaSettingsAdminResponse, UpdateRecaptchaSettin
|
||||
import { useCurrency } from '../../features/payments/contexts/CurrencyContext';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import { getCurrencySymbol } from '../../shared/utils/format';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
type SettingsTab = 'general' | 'cookie' | 'currency' | 'payment' | 'smtp' | 'company' | 'recaptcha' | 'theme';
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
|
||||
// Custom scrollbar styles - Luxury design
|
||||
@@ -148,7 +149,7 @@ const StaffShiftDashboardPage: React.FC = () => {
|
||||
const fetchShifts = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const params: { page: number; limit: number; status?: string; staff_id?: number } = { page: currentPage, limit: itemsPerPage };
|
||||
const params: { page: number; limit: number; status?: string; staff_id?: number; shift_date?: string; department?: string } = { page: currentPage, limit: itemsPerPage };
|
||||
if (filters.status) params.status = filters.status;
|
||||
if (filters.staff_id) params.staff_id = parseInt(filters.staff_id);
|
||||
if (filters.shift_date) params.shift_date = filters.shift_date;
|
||||
|
||||
@@ -6,6 +6,7 @@ import systemSettingsService, {
|
||||
UpdateStripeSettingsRequest,
|
||||
} from '../../features/system/services/systemSettingsService';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const StripeSettingsPage: React.FC = () => {
|
||||
const [settings, setSettings] = useState<StripeSettingsResponse['data'] | null>(null);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Calendar,
|
||||
User,
|
||||
Play,
|
||||
Clock,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
@@ -19,6 +20,7 @@ import TaskDetailModal from '../../features/system/components/TaskDetailModal';
|
||||
import CreateTaskModal from '../../features/system/components/CreateTaskModal';
|
||||
import TaskFilters from '../../features/system/components/TaskFilters';
|
||||
import { logger } from '../../shared/utils/logger';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
type TaskStatus = 'pending' | 'assigned' | 'in_progress' | 'completed' | 'cancelled' | 'overdue';
|
||||
type TaskPriority = 'low' | 'medium' | 'high' | 'urgent';
|
||||
@@ -61,8 +63,8 @@ const TaskManagementPage: React.FC = () => {
|
||||
const { data: statistics, execute: fetchStatistics } = useAsync<TaskStatistics>(
|
||||
async () => {
|
||||
const r = await taskService.getTaskStatistics();
|
||||
const responseData = r as { data?: { data?: Task[] }; status?: string };
|
||||
return responseData.data?.data || (r.data as Task[]);
|
||||
const responseData = r.data as { status?: string; data?: TaskStatistics };
|
||||
return (responseData.data || r.data) as TaskStatistics;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
@@ -10,6 +10,8 @@ import useAuthStore from '../../store/useAuthStore';
|
||||
import { logger } from '../../shared/utils/logger';
|
||||
import { useApiCall } from '../../shared/hooks/useApiCall';
|
||||
import { useStepUpAuth } from '../../features/auth/contexts/StepUpAuthContext';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
import { isAxiosError } from 'axios';
|
||||
|
||||
const UserManagementPage: React.FC = () => {
|
||||
const { userInfo } = useAuthStore();
|
||||
@@ -23,11 +25,13 @@ const UserManagementPage: React.FC = () => {
|
||||
const pendingSubmitDataRef = useRef<{ data: Record<string, unknown>; isEdit: boolean } | null>(null);
|
||||
|
||||
const { execute: executeSubmit, isLoading: isSubmitting } = useApiCall(
|
||||
async (data: Record<string, unknown>, isEdit: boolean) => {
|
||||
async (...args: unknown[]) => {
|
||||
const data = args[0] as Record<string, unknown>;
|
||||
const isEdit = args[1] as boolean;
|
||||
if (isEdit && editingUser) {
|
||||
return await userService.updateUser(editingUser.id, data);
|
||||
return await userService.updateUser(editingUser.id, data as Parameters<typeof userService.updateUser>[1]);
|
||||
} else {
|
||||
return await userService.createUser(data);
|
||||
return await userService.createUser(data as unknown as Parameters<typeof userService.createUser>[0]);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -145,33 +149,33 @@ const UserManagementPage: React.FC = () => {
|
||||
|
||||
// Check if step-up authentication is required
|
||||
// Check both the original response structure and the modified error from API client
|
||||
const errorData = error.response?.data;
|
||||
const errorObj = error as { response?: { data?: { detail?: unknown } }; requiresStepUp?: boolean; stepUpAction?: string };
|
||||
const errorData = errorObj.response?.data;
|
||||
const errorDetail = errorData?.detail;
|
||||
|
||||
// Check for step-up required in multiple ways
|
||||
const isStepUpRequired =
|
||||
error.requiresStepUp === true ||
|
||||
error.stepUpAction !== undefined ||
|
||||
(getUserFriendlyError(error) === 403 &&
|
||||
(errorDetail?.error === 'step_up_required' ||
|
||||
errorData?.error === 'step_up_required' ||
|
||||
(typeof errorDetail === 'object' && errorDetail?.error === 'step_up_required') ||
|
||||
errorObj.requiresStepUp === true ||
|
||||
errorObj.stepUpAction !== undefined ||
|
||||
(isAxiosError(error) && error.response?.status === 403 &&
|
||||
((typeof errorDetail === 'object' && errorDetail !== null && 'error' in errorDetail && (errorDetail as { error?: string }).error === 'step_up_required') ||
|
||||
(typeof errorData === 'object' && errorData !== null && 'error' in errorData && (errorData as { error?: string }).error === 'step_up_required') ||
|
||||
(typeof errorDetail === 'string' && errorDetail.includes('Step-up authentication required'))));
|
||||
|
||||
if (isStepUpRequired) {
|
||||
const actionDescription =
|
||||
error.stepUpAction ||
|
||||
(typeof errorDetail === 'object' ? errorDetail?.action : null) ||
|
||||
errorDetail?.action ||
|
||||
errorObj.stepUpAction ||
|
||||
(typeof errorDetail === 'object' && errorDetail !== null && 'action' in errorDetail ? (errorDetail as { action?: string }).action : null) ||
|
||||
(typeof errorDetail === 'object' && errorDetail !== null && 'action' in errorDetail ? (errorDetail as { action?: string }).action : null) ||
|
||||
(typeof errorDetail === 'string' ? errorDetail : null) ||
|
||||
errorDetail?.message ||
|
||||
(typeof errorDetail === 'object' && errorDetail !== null && 'message' in errorDetail ? (errorDetail as { message?: string }).message : null) ||
|
||||
(editingUser ? 'user update' : 'user creation');
|
||||
|
||||
logger.debug('Step-up required, opening modal', {
|
||||
actionDescription,
|
||||
error: {
|
||||
requiresStepUp: error.requiresStepUp,
|
||||
stepUpAction: error.stepUpAction,
|
||||
requiresStepUp: errorObj.requiresStepUp,
|
||||
stepUpAction: errorObj.stepUpAction,
|
||||
status: getUserFriendlyError(error),
|
||||
detail: errorDetail
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import webhookService, { Webhook, CreateWebhookData, UpdateWebhookData } from '.
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const WebhookManagementPage: React.FC = () => {
|
||||
const [webhooks, setWebhooks] = useState<Webhook[]>([]);
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useAsync } from '../../shared/hooks/useAsync';
|
||||
import workflowService, { Workflow } from '../../features/system/services/workflowService';
|
||||
import WorkflowBuilder from '../../features/system/components/WorkflowBuilder';
|
||||
import WorkflowDetailModal from '../../features/system/components/WorkflowDetailModal';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const WorkflowManagementPage: React.FC = () => {
|
||||
const [showBuilder, setShowBuilder] = useState(false);
|
||||
|
||||
@@ -8,7 +8,7 @@ export { default as AdminDashboardPage } from './DashboardPage';
|
||||
export { default as UserManagementPage } from './UserManagementPage';
|
||||
export { default as GuestProfilePage } from './GuestProfilePage';
|
||||
export { default as GroupBookingManagementPage } from './GroupBookingManagementPage';
|
||||
export { default as BusinessDashboardPage } from './BusinessDashboardPage';
|
||||
// export { default as BusinessDashboardPage } from './BusinessDashboardPage'; // File does not exist
|
||||
export { default as ReceptionDashboardPage } from './ReceptionDashboardPage';
|
||||
export { default as AdvancedRoomManagementPage } from './AdvancedRoomManagementPage';
|
||||
export { default as PageContentDashboardPage } from './PageContentDashboard';
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
FileText,
|
||||
Building2,
|
||||
AlertCircle,
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import {
|
||||
@@ -140,7 +141,18 @@ const BookingDetailPage: React.FC = () => {
|
||||
|
||||
const handleCancelSuccess = async () => {
|
||||
if (booking) {
|
||||
await fetchBookingDetails(booking.id);
|
||||
// Refetch booking details after cancellation
|
||||
const bookingId = validateBookingId(id);
|
||||
if (bookingId) {
|
||||
try {
|
||||
const response = await getBookingById(bookingId);
|
||||
if (response.success && response.data?.booking) {
|
||||
setBooking(response.data.booking);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error refetching booking:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
setShowCancelModal(false);
|
||||
};
|
||||
@@ -170,8 +182,9 @@ const BookingDetailPage: React.FC = () => {
|
||||
|
||||
const serviceUsages = (booking && typeof booking === 'object' && 'service_usages' in booking && Array.isArray((booking as { service_usages?: unknown[] }).service_usages)) ? (booking as { service_usages: unknown[] }).service_usages : ((booking && typeof booking === 'object' && 'services' in booking && Array.isArray((booking as { services?: unknown[] }).services)) ? (booking as { services: unknown[] }).services : []);
|
||||
if (Array.isArray(serviceUsages) && serviceUsages.length > 0) {
|
||||
return serviceUsages.reduce((sum: number, su: { total_price?: number }) => {
|
||||
return sum + (su.total_price || 0);
|
||||
return serviceUsages.reduce((sum: number, su: unknown) => {
|
||||
const serviceUsage = su as { total_price?: number };
|
||||
return sum + (serviceUsage.total_price || 0);
|
||||
}, 0);
|
||||
}
|
||||
return 0;
|
||||
@@ -192,7 +205,7 @@ const BookingDetailPage: React.FC = () => {
|
||||
const roomPricePerNight = useMemo(() => {
|
||||
if (!booking) return 0;
|
||||
|
||||
const roomTotal = booking.total_price - servicesTotal;
|
||||
const roomTotal = booking.total_price - (servicesTotal as number);
|
||||
return nights > 0 ? roomTotal / nights : roomTotal;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [booking?.total_price, servicesTotal, nights]);
|
||||
@@ -476,8 +489,8 @@ const BookingDetailPage: React.FC = () => {
|
||||
</p>
|
||||
<p className="text-sm text-gray-700">
|
||||
<span className="font-medium">Payment Type:</span>{' '}
|
||||
{payment.payment_type === 'deposit' ? 'Deposit (20%)' :
|
||||
payment.payment_type === 'remaining' ? 'Remaining Payment' :
|
||||
{(payment as { payment_type?: string }).payment_type === 'deposit' ? 'Deposit (20%)' :
|
||||
(payment as { payment_type?: string }).payment_type === 'remaining' ? 'Remaining Payment' :
|
||||
'Full Payment'}
|
||||
</p>
|
||||
{payment.transaction_id && (
|
||||
@@ -530,7 +543,7 @@ const BookingDetailPage: React.FC = () => {
|
||||
{}
|
||||
{serviceUsages.length > 0 && (
|
||||
<>
|
||||
{serviceUsages.map((serviceUsage: { id?: number; service_name?: string; name?: string; quantity?: number; price?: number; total?: number; total_price?: number }, index: number) => (
|
||||
{(serviceUsages as Array<{ id?: number; service_name?: string; name?: string; quantity?: number; price?: number; unit_price?: number; total?: number; total_price?: number }>).map((serviceUsage, index: number) => (
|
||||
<div key={serviceUsage.id || index} className="flex justify-between items-center">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
|
||||
@@ -131,7 +131,7 @@ const BookingSuccessPage: React.FC = () => {
|
||||
// If Stripe payment is pending, redirect to payment completion page
|
||||
if (bookingData.payment_method === PAYMENT_METHOD.STRIPE && bookingData.payments) {
|
||||
// Find any pending Stripe payment that needs completion
|
||||
const pendingStripePayment = bookingData.payments.find(
|
||||
const pendingStripePayment = (bookingData.payments as unknown as Payment[] | undefined)?.find(
|
||||
(p: Payment) =>
|
||||
p.payment_method === PAYMENT_METHOD.STRIPE &&
|
||||
p.payment_status === 'pending' // Payment record status (not booking payment_status)
|
||||
@@ -334,13 +334,13 @@ const BookingSuccessPage: React.FC = () => {
|
||||
// Check individual payment records for more granular status
|
||||
if (booking.payments && Array.isArray(booking.payments)) {
|
||||
// Calculate total from completed payment records
|
||||
const totalPaid = booking.payments
|
||||
const totalPaid = ((booking.payments as unknown as Payment[] | undefined) || [])
|
||||
.filter((p: Payment) => p.payment_status === 'completed') // Payment record status
|
||||
.reduce((sum: number, p: Payment) => sum + parseFloat(p.amount?.toString() || '0'), 0);
|
||||
.reduce((sum: number, p: Payment) => sum + (typeof p.amount === 'number' ? p.amount : parseFloat(String(p.amount || '0'))), 0);
|
||||
|
||||
// For deposit bookings, check if deposit payment is completed
|
||||
if (booking.requires_deposit) {
|
||||
const depositPayment = booking.payments.find(
|
||||
const depositPayment = ((booking.payments as unknown as Payment[] | undefined))?.find(
|
||||
(p: Payment) => p.payment_type === PAYMENT_TYPE.DEPOSIT && p.payment_status === 'completed'
|
||||
);
|
||||
if (depositPayment) {
|
||||
@@ -349,7 +349,7 @@ const BookingSuccessPage: React.FC = () => {
|
||||
} else {
|
||||
// For full payment bookings, check if total paid meets or exceeds booking price
|
||||
// Allow small floating point differences (0.01) for currency calculations
|
||||
return totalPaid >= booking.total_price - 0.01;
|
||||
return (totalPaid as number) >= booking.total_price - 0.01;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -663,7 +663,7 @@ const BookingSuccessPage: React.FC = () => {
|
||||
)}
|
||||
|
||||
{}
|
||||
{(booking.payment_method === PAYMENT_METHOD.CASH || booking.payment_method === PAYMENT_METHOD.BANK_TRANSFER) && (
|
||||
{((booking.payment_method as string) === PAYMENT_METHOD.CASH || (booking.payment_method as string) === PAYMENT_METHOD.BANK_TRANSFER) && (
|
||||
<div
|
||||
className="bg-blue-50 border border-blue-200
|
||||
rounded-lg p-6 mb-6"
|
||||
@@ -876,7 +876,7 @@ const BookingSuccessPage: React.FC = () => {
|
||||
If you cancel the booking, 20% of
|
||||
the total order value will be charged
|
||||
</li>
|
||||
{(booking.payment_method === PAYMENT_METHOD.CASH || booking.payment_method === PAYMENT_METHOD.BANK_TRANSFER) && (
|
||||
{((booking.payment_method as string) === PAYMENT_METHOD.CASH || (booking.payment_method as string) === PAYMENT_METHOD.BANK_TRANSFER) && (
|
||||
<li>
|
||||
Please transfer within 24 hours
|
||||
to secure your room
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||
import { confirmBoricaPayment } from '../../features/payments/services/paymentService';
|
||||
import { toast } from 'react-toastify';
|
||||
import { CheckCircle, XCircle, Loader2 } from 'lucide-react';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const BoricaReturnPage: React.FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
@@ -5,9 +5,10 @@ import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import complaintService, { Complaint } from '../../features/guest_management/services/complaintService';
|
||||
import bookingService from '../../features/bookings/services/bookingService';
|
||||
import bookingService, { Booking } from '../../features/bookings/services/bookingService';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { logger } from '../../shared/utils/logger';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const ComplaintPage: React.FC = () => {
|
||||
const [complaints, setComplaints] = useState<Complaint[]>([]);
|
||||
@@ -88,7 +89,7 @@ const ComplaintPage: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
// Silently fail - bookings are optional, but log for debugging
|
||||
logger.debug('Failed to fetch bookings for complaint form', error);
|
||||
logger.debug('Failed to fetch bookings for complaint form', error as Record<string, unknown> | undefined);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -242,7 +243,7 @@ const ComplaintPage: React.FC = () => {
|
||||
const CreateComplaintModal: React.FC<{
|
||||
bookings: Booking[];
|
||||
onClose: () => void;
|
||||
onSubmit: (data: { booking_id?: number; category: string; priority: string; title: string; description: string; attachments?: string[] }) => void;
|
||||
onSubmit: (data: { booking_id?: number; room_id?: number; category: string; priority: string; title: string; description: string; attachments?: string[] }) => void;
|
||||
}> = ({ bookings, onClose, onSubmit }) => {
|
||||
const [formData, setFormData] = useState({
|
||||
booking_id: '',
|
||||
|
||||
@@ -170,13 +170,13 @@ const DashboardPage: React.FC = () => {
|
||||
const fetchLoyalty = async () => {
|
||||
try {
|
||||
setLoadingLoyalty(true);
|
||||
const response = await loyaltyService.getMyLoyalty();
|
||||
const response = await loyaltyService.getMyStatus();
|
||||
if (response.status === 'success' && response.data) {
|
||||
setLoyaltyInfo({
|
||||
available_points: response.data.available_points || 0,
|
||||
tier_name: response.data.tier?.name || 'Bronze',
|
||||
lifetime_points: response.data.lifetime_points || 0,
|
||||
next_tier_points_needed: response.data.next_tier_points_needed,
|
||||
next_tier_points_needed: response.data.points_needed_for_next_tier || response.data.next_tier?.points_needed || 0,
|
||||
});
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
@@ -189,7 +189,7 @@ const DashboardPage: React.FC = () => {
|
||||
};
|
||||
if (!isAbortError(err)) {
|
||||
// Loyalty is optional, don't show error
|
||||
logger.debug('Loyalty info not available', err);
|
||||
logger.debug('Loyalty info not available', err as Record<string, unknown> | undefined);
|
||||
}
|
||||
} finally {
|
||||
setLoadingLoyalty(false);
|
||||
|
||||
@@ -180,7 +180,7 @@ const FullPaymentPage: React.FC = () => {
|
||||
} else {
|
||||
// Fallback to payments from booking data
|
||||
if (bookingData.payments && bookingData.payments.length > 0) {
|
||||
const stripePaymentFromBooking = bookingData.payments.find(
|
||||
const stripePaymentFromBooking = (bookingData.payments as unknown as Payment[] | undefined)?.find(
|
||||
(p: Payment) =>
|
||||
(p.payment_method === PAYMENT_METHOD.STRIPE || p.payment_method === PAYMENT_METHOD.CREDIT_CARD) &&
|
||||
p.payment_status === 'pending'
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useSearchParams, useNavigate, Link } from 'react-router-dom';
|
||||
import { CheckCircle, XCircle, AlertCircle, Loader2, Home, Shield } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import gdprService from '../../features/compliance/services/gdprService';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const GDPRDeletionConfirmPage: React.FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
@@ -4,6 +4,7 @@ import gdprService, { GDPRRequest } from '../../features/compliance/services/gdp
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const GDPRPage: React.FC = () => {
|
||||
const [requests, setRequests] = useState<GDPRRequest[]>([]);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurren
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import CreateGroupBookingModal from '../../features/hotel_services/components/CreateGroupBookingModal';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const GroupBookingPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
// import { useNavigate } from 'react-router-dom'; // Unused
|
||||
import {
|
||||
Plus,
|
||||
Clock,
|
||||
@@ -22,10 +22,11 @@ import { logger } from '../../shared/utils/logger';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import useAuthStore from '../../store/useAuthStore';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const GuestRequestsPage: React.FC = () => {
|
||||
const { userInfo: _userInfo } = useAuthStore();
|
||||
const _navigate = useNavigate();
|
||||
// const _navigate = useNavigate(); // Unused
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [requests, setRequests] = useState<GuestRequest[]>([]);
|
||||
const [bookings, setBookings] = useState<Booking[]>([]);
|
||||
@@ -253,8 +254,10 @@ const GuestRequestsPage: React.FC = () => {
|
||||
? "You need to be checked in to submit service requests. Please check in first or contact reception."
|
||||
: "Submit your first request to get started"
|
||||
}
|
||||
actionLabel={bookings.length > 0 ? "Create Request" : undefined}
|
||||
onAction={bookings.length > 0 ? () => setShowCreateModal(true) : undefined}
|
||||
action={bookings.length > 0 ? {
|
||||
label: "Create Request",
|
||||
onClick: () => setShowCreateModal(true)
|
||||
} : undefined}
|
||||
/>
|
||||
) : (
|
||||
<div className="grid gap-6">
|
||||
@@ -343,7 +346,7 @@ const GuestRequestsPage: React.FC = () => {
|
||||
booking_id: e.target.value,
|
||||
room_id: booking?.room_id?.toString() || '',
|
||||
});
|
||||
setSelectedBooking(booking);
|
||||
setSelectedBooking(booking || null);
|
||||
}}
|
||||
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]"
|
||||
required
|
||||
|
||||
@@ -8,6 +8,8 @@ import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurren
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import useAuthStore from '../../store/useAuthStore';
|
||||
import { validateInvoiceId } from '../../shared/utils/routeValidation';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
import { isAxiosError } from 'axios';
|
||||
|
||||
const InvoicePage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -65,7 +67,7 @@ const InvoicePage: React.FC = () => {
|
||||
const invoiceData = response.data.invoice;
|
||||
|
||||
// Validate ownership for customer role
|
||||
if (userInfo?.role === 'customer' && invoiceData.booking?.user_id !== userInfo.id) {
|
||||
if (userInfo?.role === 'customer' && invoiceData.user_id !== userInfo.id) {
|
||||
toast.error('You do not have permission to view this invoice');
|
||||
navigate('/bookings');
|
||||
setLoading(false);
|
||||
@@ -110,7 +112,7 @@ const InvoicePage: React.FC = () => {
|
||||
|
||||
// If it's a 404 and we haven't retried yet, retry multiple times with increasing delays
|
||||
// This handles cases where invoice was just created and might not be immediately available
|
||||
if (getUserFriendlyError(error) === 404 && retryCount < 3) {
|
||||
if (isAxiosError(error) && error.response?.status === 404 && retryCount < 3) {
|
||||
// Wait with increasing delay (500ms, 1000ms, 2000ms)
|
||||
const delay = 500 * Math.pow(2, retryCount);
|
||||
setTimeout(() => {
|
||||
@@ -120,7 +122,7 @@ const InvoicePage: React.FC = () => {
|
||||
}
|
||||
|
||||
// Handle invoice not found (404) after retries - show appropriate message
|
||||
if (getUserFriendlyError(error) === 404) {
|
||||
if (isAxiosError(error) && error.response?.status === 404) {
|
||||
handleInvoiceNotFound();
|
||||
} else {
|
||||
// Other errors (network, server errors, etc.) - only show toast if not a validation error
|
||||
@@ -187,7 +189,7 @@ const InvoicePage: React.FC = () => {
|
||||
return <Loading fullScreen text="Loading invoice..." />;
|
||||
}
|
||||
|
||||
if (!invoice && !loading) {
|
||||
if (!invoice) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
@@ -222,6 +224,18 @@ const InvoicePage: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (!invoice) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-8 print:bg-white">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<div className="bg-white rounded-lg shadow-lg p-8 text-center">
|
||||
<p className="text-gray-600">Loading invoice...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-8 print:bg-white">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
|
||||
@@ -22,6 +22,8 @@ import loyaltyService, {
|
||||
Referral
|
||||
} from '../../features/loyalty/services/loyaltyService';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
import { isAxiosError } from 'axios';
|
||||
|
||||
type Tab = 'overview' | 'rewards' | 'history' | 'referrals';
|
||||
|
||||
@@ -100,16 +102,25 @@ const LoyaltyPage: React.FC = () => {
|
||||
setAnniversaryDate(response.data.anniversary_date);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const isAbortError = (e: unknown): boolean => {
|
||||
return typeof e === 'object' && e !== null && (e as { name?: string; code?: string }).name === 'AbortError';
|
||||
return (e as { name?: string; code?: string }).code === 'ERR_CANCELED';
|
||||
const isAbortErrorFunc = (e: unknown): boolean => {
|
||||
if (typeof e === 'object' && e !== null) {
|
||||
const err = e as { name?: string; code?: string };
|
||||
return err.name === 'AbortError' || err.code === 'ERR_CANCELED';
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (isAbortErrorFunc(error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the error is about loyalty program being disabled
|
||||
// FastAPI returns detail in error.response.data.detail
|
||||
// The apiClient might transform it, so check both locations
|
||||
const statusCode = getUserFriendlyError(error) || error.status;
|
||||
const errorData = error.response?.data || {};
|
||||
const errorDetail = errorData.detail || errorData.message || '';
|
||||
const errorObj = error as { status?: number; response?: { data?: { detail?: unknown; message?: unknown } } };
|
||||
const statusCode = isAxiosError(error) ? error.response?.status : (errorObj.status || getUserFriendlyError(error));
|
||||
const errorData = errorObj.response?.data || {};
|
||||
const errorDetail = (typeof errorData === 'object' && errorData !== null && 'detail' in errorData ? errorData.detail : null) || (typeof errorData === 'object' && errorData !== null && 'message' in errorData ? errorData.message : null);
|
||||
const errorMessage = getUserFriendlyError(error) || '';
|
||||
|
||||
// Check if it's a 503 error (service unavailable) which indicates disabled
|
||||
|
||||
@@ -23,6 +23,7 @@ import EmptyState from '../../shared/components/EmptyState';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../shared/utils/format';
|
||||
import { getBookingStatusConfig, canCancelBooking } from '../../shared/utils/bookingUtils';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const MyBookingsPage: React.FC = () => {
|
||||
const { isAuthenticated } = useAuthStore();
|
||||
|
||||
@@ -30,6 +30,7 @@ import { validateBookingId } from '../../shared/utils/routeValidation';
|
||||
import { validateAndHandleBookingOwnership } from '../../shared/utils/ownershipValidation';
|
||||
import { PAYMENT_METHOD, PAYMENT_STATUS } from '../../shared/constants/bookingConstants';
|
||||
import { useCompanySettings } from '../../shared/contexts/CompanySettingsContext';
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
|
||||
const PaymentConfirmationPage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
@@ -24,7 +24,9 @@ import {
|
||||
Trash2,
|
||||
Database,
|
||||
Smartphone,
|
||||
Tablet
|
||||
Tablet,
|
||||
Eye,
|
||||
EyeOff
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import authService from '../../features/auth/services/authService';
|
||||
@@ -38,7 +40,9 @@ import { useGlobalLoading } from '../../shared/contexts/LoadingContext';
|
||||
import { normalizeImageUrl } from '../../shared/utils/imageUtils';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { UserSession } from '../../features/auth/services/sessionService';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
// import { useNavigate } from 'react-router-dom'; // Unused
|
||||
import { getUserFriendlyError } from '../../shared/utils/errorSanitizer';
|
||||
import { isAxiosError } from 'axios';
|
||||
|
||||
const profileValidationSchema = yup.object().shape({
|
||||
name: yup
|
||||
@@ -83,7 +87,7 @@ type PasswordFormData = yup.InferType<typeof passwordValidationSchema>;
|
||||
const ProfilePage: React.FC = () => {
|
||||
const { userInfo, setUser } = useAuthStore();
|
||||
const { setLoading } = useGlobalLoading();
|
||||
const _navigate = useNavigate();
|
||||
// const _navigate = useNavigate(); // Unused
|
||||
const [activeTab, setActiveTab] = useState<'profile' | 'password' | 'mfa' | 'sessions' | 'gdpr'>('profile');
|
||||
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
|
||||
const [avatarError, setAvatarError] = useState<boolean>(false);
|
||||
@@ -105,8 +109,8 @@ const ProfilePage: React.FC = () => {
|
||||
const [showBackupCodes, setShowBackupCodes] = useState<string[] | null>(null);
|
||||
const [showMfaSecret, setShowMfaSecret] = useState<boolean>(false);
|
||||
const mfaAbortControllerRef = useRef<AbortController | null>(null);
|
||||
const _sessionsAbortControllerRef = useRef<AbortController | null>(null);
|
||||
const _gdprAbortControllerRef = useRef<AbortController | null>(null);
|
||||
// const _sessionsAbortControllerRef = useRef<AbortController | null>(null); // Unused
|
||||
// const _gdprAbortControllerRef = useRef<AbortController | null>(null); // Unused
|
||||
|
||||
|
||||
const fetchProfile = async () => {
|
||||
@@ -241,8 +245,16 @@ const ProfilePage: React.FC = () => {
|
||||
|
||||
if (response.status === 'success' || response.success) {
|
||||
const updatedUser = response.data?.user || response.data;
|
||||
if (updatedUser) {
|
||||
setUser(updatedUser);
|
||||
if (updatedUser && typeof updatedUser === 'object' && 'id' in updatedUser) {
|
||||
setUser({
|
||||
id: (updatedUser as { id: number }).id,
|
||||
name: ((updatedUser as { name?: string; full_name?: string }).name || (updatedUser as { name?: string; full_name?: string }).full_name || '') as string,
|
||||
email: (updatedUser as { email: string }).email,
|
||||
phone: ((updatedUser as { phone?: string; phone_number?: string }).phone || (updatedUser as { phone?: string; phone_number?: string }).phone_number) as string | undefined,
|
||||
avatar: (updatedUser as { avatar?: string }).avatar,
|
||||
role: (updatedUser as { role: string }).role,
|
||||
createdAt: ((updatedUser as { createdAt?: string; created_at?: string }).createdAt || (updatedUser as { createdAt?: string; created_at?: string }).created_at) as string | undefined,
|
||||
});
|
||||
toast.success('Profile updated successfully!');
|
||||
refetchProfile();
|
||||
}
|
||||
@@ -1158,7 +1170,7 @@ const SessionsTab: React.FC = () => {
|
||||
fetchSessions();
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (getUserFriendlyError(error) === 401) {
|
||||
if (isAxiosError(error) && error.response?.status === 401) {
|
||||
toast.warning('Your session has been revoked. You will be logged out.');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
@@ -1184,7 +1196,7 @@ const SessionsTab: React.FC = () => {
|
||||
fetchSessions();
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (getUserFriendlyError(error) === 401) {
|
||||
if (isAxiosError(error) && error.response?.status === 401) {
|
||||
toast.warning('All sessions have been revoked. You will be logged out.');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user