This commit is contained in:
Iliyan Angelov
2025-11-28 02:40:05 +02:00
parent 627959f52b
commit 312f85530c
246 changed files with 23535 additions and 3428 deletions

View File

@@ -13,6 +13,7 @@ import { Link } from 'react-router-dom';
import { pageContentService } from '../services/api';
import type { PageContent } from '../services/api/pageContentService';
import { useCompanySettings } from '../contexts/CompanySettingsContext';
import { createSanitizedHtml } from '../utils/htmlSanitizer';
const AboutPage: React.FC = () => {
const { settings } = useCompanySettings();
@@ -206,7 +207,7 @@ const AboutPage: React.FC = () => {
{pageContent?.story_content ? (
<div
className="text-lg md:text-xl leading-relaxed font-light tracking-wide"
dangerouslySetInnerHTML={{ __html: pageContent.story_content.replace(/\n/g, '<br />') }}
dangerouslySetInnerHTML={createSanitizedHtml(pageContent.story_content.replace(/\n/g, '<br />'))}
/>
) : (
<>

View File

@@ -5,6 +5,7 @@ import { pageContentService } from '../services/api';
import type { PageContent } from '../services/api/pageContentService';
import { useCompanySettings } from '../contexts/CompanySettingsContext';
import Loading from '../components/common/Loading';
import { createSanitizedHtml } from '../utils/htmlSanitizer';
const AccessibilityPage: React.FC = () => {
const { settings } = useCompanySettings();
@@ -153,9 +154,9 @@ const AccessibilityPage: React.FC = () => {
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
[&_strong]:text-[#d4af37] [&_b]:text-[#d4af37] [&_a]:text-[#d4af37]"
style={{ color: '#d1d5db' }}
dangerouslySetInnerHTML={{
__html: pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
}}
dangerouslySetInnerHTML={createSanitizedHtml(
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
)}
/>
</div>

View File

@@ -5,6 +5,7 @@ import { pageContentService } from '../services/api';
import type { PageContent } from '../services/api/pageContentService';
import { useCompanySettings } from '../contexts/CompanySettingsContext';
import Loading from '../components/common/Loading';
import { createSanitizedHtml } from '../utils/htmlSanitizer';
const CancellationPolicyPage: React.FC = () => {
const { settings } = useCompanySettings();
@@ -154,9 +155,9 @@ const CancellationPolicyPage: React.FC = () => {
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
[&_strong]:text-[#d4af37] [&_b]:text-[#d4af37] [&_a]:text-[#d4af37]"
style={{ color: '#d1d5db' }}
dangerouslySetInnerHTML={{
__html: pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
}}
dangerouslySetInnerHTML={createSanitizedHtml(
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
)}
/>
</div>

View File

@@ -5,6 +5,7 @@ import { pageContentService } from '../services/api';
import type { PageContent } from '../services/api/pageContentService';
import { useCompanySettings } from '../contexts/CompanySettingsContext';
import Loading from '../components/common/Loading';
import { createSanitizedHtml } from '../utils/htmlSanitizer';
const FAQPage: React.FC = () => {
const { settings } = useCompanySettings();
@@ -153,9 +154,9 @@ const FAQPage: React.FC = () => {
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
[&_strong]:text-[#d4af37] [&_b]:text-[#d4af37] [&_a]:text-[#d4af37]"
style={{ color: '#d1d5db' }}
dangerouslySetInnerHTML={{
__html: pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
}}
dangerouslySetInnerHTML={createSanitizedHtml(
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
)}
/>
</div>

View File

@@ -5,6 +5,7 @@ import { pageContentService } from '../services/api';
import type { PageContent } from '../services/api/pageContentService';
import { useCompanySettings } from '../contexts/CompanySettingsContext';
import Loading from '../components/common/Loading';
import { createSanitizedHtml } from '../utils/htmlSanitizer';
const PrivacyPolicyPage: React.FC = () => {
const { settings } = useCompanySettings();
@@ -161,9 +162,9 @@ const PrivacyPolicyPage: React.FC = () => {
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
[&_strong]:text-[#d4af37] [&_b]:text-[#d4af37] [&_a]:text-[#d4af37]"
style={{ color: '#d1d5db' }}
dangerouslySetInnerHTML={{
__html: pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
}}
dangerouslySetInnerHTML={createSanitizedHtml(
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
)}
/>
</div>

View File

@@ -5,6 +5,7 @@ import { pageContentService } from '../services/api';
import type { PageContent } from '../services/api/pageContentService';
import { useCompanySettings } from '../contexts/CompanySettingsContext';
import Loading from '../components/common/Loading';
import { createSanitizedHtml } from '../utils/htmlSanitizer';
const RefundsPolicyPage: React.FC = () => {
const { settings } = useCompanySettings();
@@ -161,9 +162,9 @@ const RefundsPolicyPage: React.FC = () => {
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
[&_strong]:text-[#d4af37] [&_b]:text-[#d4af37] [&_a]:text-[#d4af37]"
style={{ color: '#d1d5db' }}
dangerouslySetInnerHTML={{
__html: pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
}}
dangerouslySetInnerHTML={createSanitizedHtml(
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
)}
/>
</div>

View File

@@ -5,6 +5,7 @@ import { pageContentService } from '../services/api';
import type { PageContent } from '../services/api/pageContentService';
import { useCompanySettings } from '../contexts/CompanySettingsContext';
import Loading from '../components/common/Loading';
import { createSanitizedHtml } from '../utils/htmlSanitizer';
const TermsPage: React.FC = () => {
const { settings } = useCompanySettings();
@@ -161,9 +162,9 @@ const TermsPage: React.FC = () => {
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
[&_strong]:text-[#d4af37] [&_b]:text-[#d4af37] [&_a]:text-[#d4af37]"
style={{ color: '#d1d5db' }}
dangerouslySetInnerHTML={{
__html: pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
}}
dangerouslySetInnerHTML={createSanitizedHtml(
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
)}
/>
</div>

View File

@@ -33,6 +33,7 @@ import InspectionManagement from '../../components/shared/InspectionManagement';
import Pagination from '../../components/common/Pagination';
import apiClient from '../../services/api/apiClient';
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
import { logger } from '../../utils/logger';
type Tab = 'status-board' | 'maintenance' | 'housekeeping' | 'inspections' | 'rooms';
@@ -107,7 +108,7 @@ const AdvancedRoomManagementPage: React.FC = () => {
setFloors(uniqueFloors);
}
} catch (error) {
console.error('Failed to fetch floors:', error);
logger.error('Failed to fetch floors', error);
}
};
@@ -217,7 +218,7 @@ const AdvancedRoomManagementPage: React.FC = () => {
setAvailableAmenities(response.data.amenities);
}
} catch (error) {
console.error('Failed to fetch amenities:', error);
logger.error('Failed to fetch amenities', error);
}
}, []);
@@ -294,7 +295,7 @@ const AdvancedRoomManagementPage: React.FC = () => {
}
});
} catch (err) {
console.error(`Failed to fetch page ${page}:`, err);
logger.error(`Failed to fetch page ${page}`, err);
}
}
}
@@ -308,7 +309,7 @@ const AdvancedRoomManagementPage: React.FC = () => {
return prev;
});
} catch (error) {
console.error('Failed to fetch room types:', error);
logger.error('Failed to fetch room types', error);
}
};
@@ -335,7 +336,7 @@ const AdvancedRoomManagementPage: React.FC = () => {
const updatedRoom = await roomService.getRoomByNumber(editingRoom.room_number);
setEditingRoom(updatedRoom.data.room);
} catch (err) {
console.error('Failed to refresh room data:', err);
logger.error('Failed to refresh room data', err);
}
} else {
const createData = {
@@ -476,7 +477,7 @@ const AdvancedRoomManagementPage: React.FC = () => {
setEditingRoom(roomData);
} catch (error) {
console.error('Failed to fetch full room details:', error);
logger.error('Failed to fetch full room details', error);
}
};
@@ -617,7 +618,7 @@ const AdvancedRoomManagementPage: React.FC = () => {
const response = await roomService.getRoomByNumber(editingRoom.room_number);
setEditingRoom(response.data.room);
} catch (error: any) {
console.error('Error deleting image:', error);
logger.error('Error deleting image', error);
toast.error(error.response?.data?.message || error.response?.data?.detail || 'Unable to delete image');
}
};

View File

@@ -38,6 +38,7 @@ import { reportService, ReportData, reviewService, Review } from '../../services
import { auditService, AuditLog, AuditLogFilters } from '../../services/api/auditService';
import { formatDate } from '../../utils/format';
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
import { logger } from '../../utils/logger';
import analyticsService, {
ComprehensiveAnalyticsData,
RevPARData,
@@ -250,7 +251,7 @@ const AnalyticsDashboardPage: React.FC = () => {
setTotalItems(response.data.pagination.total);
}
} catch (error: any) {
console.error('Error fetching audit logs:', error);
logger.error('Error fetching audit logs', error);
toast.error(error.response?.data?.message || 'Unable to load audit logs');
} finally {
setAuditLoading(false);

View File

@@ -8,6 +8,7 @@ import { useFormatCurrency } from '../../hooks/useFormatCurrency';
import { parseDateLocal } from '../../utils/format';
import { useNavigate } from 'react-router-dom';
import CreateBookingModal from '../../components/shared/CreateBookingModal';
import { logger } from '../../utils/logger';
const BookingManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency();
@@ -105,7 +106,7 @@ const BookingManagementPage: React.FC = () => {
} catch (error: any) {
const errorMessage = error.response?.data?.detail || error.response?.data?.message || error.message || 'Unable to create invoice';
toast.error(errorMessage);
console.error('Invoice creation error:', error);
logger.error('Invoice creation error', error);
} finally {
setCreatingInvoice(false);
}

View File

@@ -19,6 +19,7 @@ import { useFormatCurrency } from '../../hooks/useFormatCurrency';
import { useAsync } from '../../hooks/useAsync';
import { useNavigate } from 'react-router-dom';
import useAuthStore from '../../store/useAuthStore';
import { logger } from '../../utils/logger';
const DashboardPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency();
@@ -36,7 +37,7 @@ const DashboardPage: React.FC = () => {
await logout();
navigate('/');
} catch (error) {
console.error('Logout error:', error);
logger.error('Logout error', error);
}
};
@@ -71,7 +72,7 @@ const DashboardPage: React.FC = () => {
setRecentPayments(response.data.payments);
}
} catch (err: any) {
console.error('Error fetching payments:', err);
logger.error('Error fetching payments', err);
} finally {
setLoadingPayments(false);
}

View File

@@ -28,6 +28,7 @@ import { formatDate } from '../../utils/format';
import TaskDetailModal from '../../components/tasks/TaskDetailModal';
import CreateTaskModal from '../../components/tasks/CreateTaskModal';
import TaskFilters from '../../components/tasks/TaskFilters';
import { logger } from '../../utils/logger';
type TaskStatus = 'pending' | 'assigned' | 'in_progress' | 'completed' | 'cancelled' | 'overdue';
type TaskPriority = 'low' | 'medium' | 'high' | 'urgent';
@@ -61,7 +62,7 @@ const TaskManagementPage: React.FC = () => {
const tasksArray = responseData?.data || responseData || [];
return Array.isArray(tasksArray) ? tasksArray : [];
}).catch(error => {
console.error('Error fetching tasks:', error);
logger.error('Error fetching tasks', error);
return [];
}),
{ immediate: true }

View File

@@ -3,15 +3,40 @@ import { Plus, Search, Edit, Trash2, X } from 'lucide-react';
import { userService, User } from '../../services/api';
import { toast } from 'react-toastify';
import Loading from '../../components/common/Loading';
import LoadingButton from '../../components/common/LoadingButton';
import ErrorMessage from '../../components/common/ErrorMessage';
import Pagination from '../../components/common/Pagination';
import useAuthStore from '../../store/useAuthStore';
import { logger } from '../../utils/logger';
import { useApiCall } from '../../hooks/useApiCall';
const UserManagementPage: React.FC = () => {
const { userInfo } = useAuthStore();
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [showModal, setShowModal] = useState(false);
const [editingUser, setEditingUser] = useState<User | null>(null);
const [deletingUserId, setDeletingUserId] = useState<number | null>(null);
const { execute: executeSubmit, isLoading: isSubmitting } = useApiCall(
async (data: any, isEdit: boolean) => {
if (isEdit && editingUser) {
return await userService.updateUser(editingUser.id, data);
} else {
return await userService.createUser(data);
}
},
{
showSuccessToast: true,
successMessage: (editingUser ? 'User updated' : 'User added') + ' successfully',
onSuccess: () => {
setShowModal(false);
resetForm();
setTimeout(() => fetchUsers(), 300);
},
}
);
const [filters, setFilters] = useState({
search: '',
role: '',
@@ -42,21 +67,23 @@ const UserManagementPage: React.FC = () => {
const fetchUsers = async () => {
try {
setLoading(true);
console.log('Fetching users with filters:', filters, 'page:', currentPage);
setError(null);
logger.debug('Fetching users', { filters, page: currentPage });
const response = await userService.getUsers({
...filters,
page: currentPage,
limit: itemsPerPage,
});
console.log('Users response:', response);
setUsers(response.data.users);
if (response.data.pagination) {
setTotalPages(response.data.pagination.totalPages);
setTotalItems(response.data.pagination.total);
}
} catch (error: any) {
console.error('Error fetching users:', error);
toast.error(error.response?.data?.message || 'Unable to load users list');
logger.error('Error fetching users', error);
const errorMessage = error.response?.data?.message || 'Unable to load users list';
setError(errorMessage);
toast.error(errorMessage);
} finally {
setLoading(false);
}
@@ -64,49 +91,34 @@ const UserManagementPage: React.FC = () => {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!editingUser && (!formData.password || formData.password.trim() === '')) {
toast.error('Please enter password');
return;
}
try {
const submitData: any = {
full_name: formData.full_name,
email: formData.email,
phone_number: formData.phone_number,
role: formData.role,
status: formData.status,
};
if (editingUser) {
const updateData: any = {
full_name: formData.full_name,
email: formData.email,
phone_number: formData.phone_number,
role: formData.role,
status: formData.status,
};
if (formData.password && formData.password.trim() !== '') {
updateData.password = formData.password;
submitData.password = formData.password;
}
console.log('Updating user:', editingUser.id, 'with data:', updateData);
const response = await userService.updateUser(editingUser.id, updateData);
console.log('Update response:', response);
toast.success('User updated successfully');
logger.debug('Updating user', { userId: editingUser.id, updateData: submitData });
} else {
if (!formData.password || formData.password.trim() === '') {
toast.error('Please enter password');
return;
}
console.log('Creating user with data:', formData);
const response = await userService.createUser(formData);
console.log('Create response:', response);
toast.success('User added successfully');
submitData.password = formData.password;
logger.debug('Creating user', { formData: submitData });
}
setShowModal(false);
resetForm();
setTimeout(() => {
fetchUsers();
}, 300);
await executeSubmit(submitData, !!editingUser);
} catch (error: any) {
console.error('Error submitting user:', error);
toast.error(error.response?.data?.message || 'An error occurred');
logger.error('Error submitting user', error);
}
};
@@ -124,7 +136,6 @@ const UserManagementPage: React.FC = () => {
};
const handleDelete = async (id: number) => {
if (userInfo?.id === id) {
toast.error('You cannot delete your own account');
return;
@@ -133,13 +144,16 @@ const UserManagementPage: React.FC = () => {
if (!window.confirm('Are you sure you want to delete this user?')) return;
try {
console.log('Deleting user:', id);
setDeletingUserId(id);
logger.debug('Deleting user', { userId: id });
await userService.deleteUser(id);
toast.success('User deleted successfully');
fetchUsers();
} catch (error: any) {
console.error('Error deleting user:', error);
logger.error('Error deleting user', error);
toast.error(error.response?.data?.message || 'Unable to delete user');
} finally {
setDeletingUserId(null);
}
};
@@ -248,6 +262,14 @@ const UserManagementPage: React.FC = () => {
</div>
</div>
{error && (
<ErrorMessage
message={error}
onDismiss={() => setError(null)}
className="animate-fade-in"
/>
)}
{}
<div className="bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl border border-slate-200/60 overflow-hidden animate-fade-in" style={{ animationDelay: '0.2s' }}>
<div className="overflow-x-auto">
@@ -307,14 +329,17 @@ const UserManagementPage: React.FC = () => {
>
<Edit className="w-5 h-5" />
</button>
<button
<LoadingButton
onClick={() => handleDelete(user.id)}
className="p-2 rounded-lg text-rose-600 hover:text-rose-700 hover:bg-rose-50 transition-all duration-200 shadow-sm hover:shadow-md border border-rose-200 hover:border-rose-300 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={userInfo?.id === user.id}
isLoading={deletingUserId === user.id}
disabled={userInfo?.id === user.id || deletingUserId === user.id}
variant="danger"
size="sm"
className="p-2"
title="Delete"
>
<Trash2 className="w-5 h-5" />
</button>
</LoadingButton>
</div>
</td>
</tr>
@@ -439,16 +464,20 @@ const UserManagementPage: React.FC = () => {
<button
type="button"
onClick={() => setShowModal(false)}
className="flex-1 px-6 py-3 border-2 border-slate-300 rounded-xl text-slate-700 font-semibold hover:bg-slate-50 transition-all duration-200 shadow-sm hover:shadow-md"
disabled={isSubmitting}
className="flex-1 px-6 py-3 border-2 border-slate-300 rounded-xl text-slate-700 font-semibold hover:bg-slate-50 transition-all duration-200 shadow-sm hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed"
>
Cancel
</button>
<button
<LoadingButton
type="submit"
className="flex-1 px-6 py-3 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-xl font-semibold hover:from-amber-600 hover:to-amber-700 transition-all duration-200 shadow-lg hover:shadow-xl"
isLoading={isSubmitting}
loadingText={editingUser ? 'Updating...' : 'Creating...'}
variant="primary"
className="flex-1"
>
{editingUser ? 'Update' : 'Create'}
</button>
</LoadingButton>
</div>
</form>
</div>