This commit is contained in:
Iliyan Angelov
2025-12-02 10:42:35 +02:00
parent 4b053ce703
commit 2d770dd27b
24 changed files with 1766 additions and 1402 deletions

View File

@@ -93,6 +93,7 @@ const NotificationManagementPage = lazy(() => import('./pages/admin/Notification
const ReceptionDashboardPage = lazy(() => import('./pages/admin/ReceptionDashboardPage'));
const LoyaltyManagementPage = lazy(() => import('./pages/admin/LoyaltyManagementPage'));
const AdvancedRoomManagementPage = lazy(() => import('./pages/admin/AdvancedRoomManagementPage'));
const EditRoomPage = lazy(() => import('./pages/admin/EditRoomPage'));
const RatePlanManagementPage = lazy(() => import('./pages/admin/RatePlanManagementPage'));
const PackageManagementPage = lazy(() => import('./pages/admin/PackageManagementPage'));
const SecurityManagementPage = lazy(() => import('./pages/admin/SecurityManagementPage'));
@@ -543,6 +544,10 @@ function App() {
path="advanced-rooms"
element={<AdvancedRoomManagementPage />}
/>
<Route
path="rooms/:id/edit"
element={<EditRoomPage />}
/>
<Route
path="page-content"
element={<PageContentDashboardPage />}

View File

@@ -143,6 +143,26 @@ class BlogService {
});
return response.data;
}
// Tag management endpoints
async getAllTags(): Promise<{ status: string; data: { tags: Array<{ name: string; usage_count: number }>; total: number } }> {
const response = await apiClient.get('/blog/admin/tags');
return response.data;
}
async renameTag(oldTag: string, newTag: string): Promise<{ status: string; data: { old_tag: string; new_tag: string; updated_posts: number }; message?: string }> {
const response = await apiClient.put('/blog/admin/tags/rename', null, {
params: { old_tag: oldTag, new_tag: newTag },
});
return response.data;
}
async deleteTag(tag: string): Promise<{ status: string; data: { deleted_tag: string; updated_posts: number }; message?: string }> {
const response = await apiClient.delete('/blog/admin/tags', {
params: { tag },
});
return response.data;
}
}
export const blogService = new BlogService();

View File

@@ -169,6 +169,33 @@ export const getAmenities = async (): Promise<{
};
};
export const updateAmenity = async (
oldName: string,
newName: string
): Promise<{ success: boolean; message: string; data: { updated_count: number; old_name: string; new_name: string } }> => {
const response = await apiClient.put(`/rooms/amenities/${encodeURIComponent(oldName)}`, {
new_name: newName,
});
const responseData = response.data;
return {
success: responseData.status === 'success' || responseData.success === true,
message: responseData.message || '',
data: responseData.data || { updated_count: 0, old_name: oldName, new_name: newName },
};
};
export const deleteAmenity = async (
amenityName: string
): Promise<{ success: boolean; message: string; data: { updated_count: number; amenity_name: string } }> => {
const response = await apiClient.delete(`/rooms/amenities/${encodeURIComponent(amenityName)}`);
const data = response.data;
return {
success: data.status === 'success' || data.success === true,
message: data.message || '',
data: data.data || { updated_count: 0, amenity_name: amenityName },
};
};
export interface RoomType {
id: number;
name: string;
@@ -264,6 +291,8 @@ export default {
getRoomByNumber,
searchAvailableRooms,
getAmenities,
updateAmenity,
deleteAmenity,
getRoomTypes,
createRoom,
updateRoom,

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Hotel,
Wrench,
@@ -15,6 +16,7 @@ import {
MapPin,
Plus,
Edit,
Trash2,
X,
Image as ImageIcon,
Check,
@@ -35,6 +37,7 @@ import { useRoomContext } from '../../features/rooms/contexts/RoomContext';
type Tab = 'status-board' | 'maintenance' | 'housekeeping' | 'inspections';
const AdvancedRoomManagementPage: React.FC = () => {
const navigate = useNavigate();
const {
statusBoardRooms,
statusBoardLoading,
@@ -73,6 +76,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);
// Define fetchFloors before using it in useEffect
const fetchFloors = useCallback(async () => {
@@ -451,87 +456,31 @@ const AdvancedRoomManagementPage: React.FC = () => {
}
};
const handleEditRoom = async (room: Room) => {
setEditingRoom(room);
let amenitiesArray: string[] = [];
if (room.amenities) {
if (Array.isArray(room.amenities)) {
amenitiesArray = room.amenities;
} else if (typeof room.amenities === 'string') {
try {
const parsed = JSON.parse(room.amenities);
amenitiesArray = Array.isArray(parsed) ? parsed : [];
} catch {
const amenitiesStr: string = room.amenities;
amenitiesArray = amenitiesStr.split(',').map((a: string) => a.trim()).filter(Boolean);
}
}
}
setRoomFormData({
room_number: room.room_number,
floor: room.floor,
room_type_id: room.room_type_id,
status: room.status,
featured: room.featured,
price: room.price?.toString() || '',
description: room.description || '',
capacity: room.capacity?.toString() || '',
room_size: room.room_size || '',
view: room.view || '',
amenities: amenitiesArray,
});
setShowRoomModal(true);
try {
const fullRoom = await roomService.getRoomByNumber(room.room_number);
const roomData = fullRoom.data.room;
let updatedAmenitiesArray: string[] = [];
if (roomData.amenities) {
if (Array.isArray(roomData.amenities)) {
updatedAmenitiesArray = roomData.amenities;
} else if (typeof roomData.amenities === 'string') {
try {
const parsed = JSON.parse(roomData.amenities);
updatedAmenitiesArray = Array.isArray(parsed) ? parsed : [];
} catch {
const amenitiesStr: string = roomData.amenities;
updatedAmenitiesArray = amenitiesStr.split(',').map((a: string) => a.trim()).filter(Boolean);
}
}
}
setRoomFormData({
room_number: roomData.room_number,
floor: roomData.floor,
room_type_id: roomData.room_type_id,
status: roomData.status,
featured: roomData.featured,
price: roomData.price?.toString() || '',
description: roomData.description || '',
capacity: roomData.capacity?.toString() || '',
room_size: roomData.room_size || '',
view: roomData.view || '',
amenities: updatedAmenitiesArray,
});
setEditingRoom(roomData);
} catch (error) {
logger.error('Failed to fetch full room details', error);
toast.error('Failed to load complete room details');
}
const handleEditRoom = (room: Room) => {
navigate(`/admin/rooms/${room.id}/edit`);
};
const handleDeleteRoom = async (id: number) => {
if (!window.confirm('Are you sure you want to delete this room?')) return;
const room = contextRooms.find(r => r.id === id) || statusBoardRooms.find(r => r.id === id);
const roomNumber = room?.room_number || 'this room';
if (!window.confirm(`Are you sure you want to delete room ${roomNumber}? This action cannot be undone.`)) {
return;
}
try {
await contextDeleteRoom(id);
toast.success(`Room ${roomNumber} deleted successfully`);
await refreshStatusBoard();
await refreshRooms();
// Remove from expanded rooms if it was expanded
setExpandedRooms(prev => {
const newSet = new Set(prev);
newSet.delete(id);
return newSet;
});
} catch (error: any) {
// Error already handled in context
toast.error(error.response?.data?.message || error.response?.data?.detail || 'Failed to delete room');
}
};
@@ -552,6 +501,8 @@ const AdvancedRoomManagementPage: React.FC = () => {
});
setSelectedFiles([]);
setUploadingImages(false);
setCustomAmenityInput('');
setEditingAmenity(null);
};
const toggleAmenity = (amenity: string) => {
@@ -563,6 +514,86 @@ const AdvancedRoomManagementPage: React.FC = () => {
}));
};
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 handleEditAmenity = (amenity: string) => {
setEditingAmenity({ name: amenity, newName: amenity });
};
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;
}
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();
});
setRoomFormData(prev => ({
...prev,
amenities: prev.amenities.map(a => a === editingAmenity.name ? newName : a)
}));
setEditingAmenity(null);
await fetchAvailableAmenities();
await refreshRooms();
await refreshStatusBoard();
} catch (error: any) {
toast.error(error.response?.data?.message || error.response?.data?.detail || '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;
}
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)
}));
await fetchAvailableAmenities();
await refreshRooms();
await refreshStatusBoard();
} catch (error: any) {
toast.error(error.response?.data?.message || error.response?.data?.detail || 'Failed to delete amenity');
}
};
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
const files = Array.from(e.target.files);
@@ -806,17 +837,29 @@ const AdvancedRoomManagementPage: React.FC = () => {
<span>{getStatusLabel(effectiveStatus)}</span>
</div>
{/* Edit Button */}
<button
onClick={(e) => {
e.stopPropagation();
handleEditRoomFromStatusBoard(room.id);
}}
className="absolute top-3 left-3 p-2 bg-white/90 backdrop-blur-sm rounded-lg text-blue-600 hover:text-blue-700 hover:bg-white transition-all duration-200 shadow-md hover:shadow-lg border border-blue-200 hover:border-blue-300 z-20"
title="Edit Room"
>
<Edit className="w-4 h-4" />
</button>
{/* Action Buttons */}
<div className="absolute top-3 left-3 flex gap-2 z-20">
<button
onClick={(e) => {
e.stopPropagation();
handleEditRoomFromStatusBoard(room.id);
}}
className="p-2 bg-white/90 backdrop-blur-sm rounded-lg text-blue-600 hover:text-blue-700 hover:bg-white transition-all duration-200 shadow-md hover:shadow-lg border border-blue-200 hover:border-blue-300"
title="Edit Room"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteRoom(room.id);
}}
className="p-2 bg-white/90 backdrop-blur-sm rounded-lg text-rose-600 hover:text-rose-700 hover:bg-white transition-all duration-200 shadow-md hover:shadow-lg border border-rose-200 hover:border-rose-300 hover:scale-105"
title={`Delete Room ${room.room_number}`}
>
<Trash2 className="w-4 h-4" />
</button>
</div>
{/* Room Content */}
<div className="p-5 pt-16 cursor-pointer" onClick={() => toggleRoomExpansion(room.id)}>

View File

@@ -353,8 +353,9 @@ const AuditLogsPage: React.FC = () => {
{}
{showDetails && selectedLog && (
<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-3xl w-full max-h-[90vh] overflow-y-auto">
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 overflow-y-auto p-4">
<div className="min-h-full flex items-start justify-center py-8">
<div className="bg-white rounded-lg max-w-3xl w-full my-8 max-h-[calc(100vh-4rem)] overflow-y-auto">
<div className="p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-2xl font-bold text-gray-900">Audit Log Details</h2>
@@ -460,6 +461,7 @@ const AuditLogsPage: React.FC = () => {
</div>
</div>
</div>
</div>
</div>
)}
</div>

View File

@@ -417,8 +417,9 @@ const BannerManagementPage: React.FC = () => {
{}
{showModal && (
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-3 sm:p-4 animate-fade-in">
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-2xl w-full max-h-[95vh] sm:max-h-[90vh] overflow-hidden border border-slate-200/50 animate-scale-in">
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-3 sm:p-4 animate-fade-in">
<div className="min-h-full flex items-start justify-center py-8">
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-2xl w-full my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-slate-200/50 animate-scale-in">
<div className="sticky top-0 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-4 sm:px-6 py-4 sm:py-5 border-b border-slate-700 z-10">
<div className="flex justify-between items-center">
<div>
@@ -440,7 +441,7 @@ const BannerManagementPage: React.FC = () => {
</button>
</div>
</div>
<div className="overflow-y-auto max-h-[calc(95vh-120px)] sm:max-h-[calc(90vh-120px)]">
<div className="overflow-y-auto max-h-[calc(100vh-12rem)]">
<form onSubmit={handleSubmit} className="p-4 sm:p-6 space-y-4 sm:space-y-5">
<div>
<label className="block text-xs sm:text-sm font-semibold text-slate-600 uppercase tracking-wider mb-2">
@@ -679,6 +680,8 @@ const BannerManagementPage: React.FC = () => {
</form>
</div>
</div>
</div>
</div>
</div>
)}

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState, useRef } from 'react';
import { Plus, Search, Edit, Trash2, X, Eye, EyeOff, Loader2, Calendar, User, Tag, GripVertical, Image as ImageIcon, Type, Quote, List, Video, ArrowRight, MoveUp, MoveDown, Sparkles, Upload } from 'lucide-react';
import { Plus, Search, Edit, Trash2, X, Eye, EyeOff, Loader2, Tag, GripVertical, Image as ImageIcon, Type, Quote, List, Video, ArrowRight, MoveUp, MoveDown, Sparkles, Upload } from 'lucide-react';
import { toast } from 'react-toastify';
import Loading from '../../shared/components/Loading';
import Pagination from '../../shared/components/Pagination';
@@ -42,6 +42,16 @@ const BlogManagementPage: React.FC = () => {
const [saving, setSaving] = useState(false);
const [showSectionBuilder, setShowSectionBuilder] = useState(false);
const [uploadingImages, setUploadingImages] = useState<{ [key: string]: boolean }>({});
// Tag management state
const [showTagController, setShowTagController] = useState(false);
const [tags, setTags] = useState<Array<{ name: string; usage_count: number }>>([]);
const [tagsLoading, setTagsLoading] = useState(false);
const [editingTag, setEditingTag] = useState<{ old: string; new: string } | null>(null);
const [deleteTagConfirm, setDeleteTagConfirm] = useState<{ show: boolean; tag: string | null }>({
show: false,
tag: null,
});
useEffect(() => {
setCurrentPage(1);
@@ -51,6 +61,12 @@ const BlogManagementPage: React.FC = () => {
fetchPosts();
}, [filters, currentPage]);
useEffect(() => {
if (showTagController) {
fetchTags();
}
}, [showTagController]);
const fetchPosts = async () => {
try {
setLoading(true);
@@ -275,6 +291,50 @@ const BlogManagementPage: React.FC = () => {
}
};
// Tag management functions
const fetchTags = async () => {
try {
setTagsLoading(true);
const response = await blogService.getAllTags();
if (response.status === 'success' && response.data) {
setTags(response.data.tags);
}
} catch (error: any) {
toast.error(error.response?.data?.detail || 'Failed to load tags');
} finally {
setTagsLoading(false);
}
};
const handleRenameTag = async () => {
if (!editingTag || !editingTag.new.trim() || editingTag.old === editingTag.new.trim()) {
setEditingTag(null);
return;
}
try {
const response = await blogService.renameTag(editingTag.old, editingTag.new.trim());
toast.success(response.message || 'Tag renamed successfully');
setEditingTag(null);
fetchTags();
fetchPosts(); // Refresh posts to show updated tags
} catch (error: any) {
toast.error(error.response?.data?.detail || 'Failed to rename tag');
}
};
const handleDeleteTag = async () => {
if (!deleteTagConfirm.tag) return;
try {
const response = await blogService.deleteTag(deleteTagConfirm.tag);
toast.success(response.message || 'Tag deleted successfully');
setDeleteTagConfirm({ show: false, tag: null });
fetchTags();
fetchPosts(); // Refresh posts to show updated tags
} catch (error: any) {
toast.error(error.response?.data?.detail || 'Failed to delete tag');
}
};
const formatDate = (dateString?: string) => {
if (!dateString) return 'Not published';
return new Date(dateString).toLocaleDateString('en-US', {
@@ -398,13 +458,22 @@ const BlogManagementPage: React.FC = () => {
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900">Blog Management</h1>
<p className="text-gray-600 mt-1">Manage your blog posts and content</p>
</div>
<button
onClick={() => handleOpenModal()}
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-white rounded-lg hover:from-[#f5d76e] hover:to-[#d4af37] transition-all shadow-lg"
>
<Plus className="w-5 h-5" />
<span>New Post</span>
</button>
<div className="flex items-center gap-3">
<button
onClick={() => setShowTagController(true)}
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-lg hover:from-blue-700 hover:to-blue-800 transition-all shadow-lg"
>
<Tag className="w-5 h-5" />
<span>Manage Tags</span>
</button>
<button
onClick={() => handleOpenModal()}
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-white rounded-lg hover:from-[#f5d76e] hover:to-[#d4af37] transition-all shadow-lg"
>
<Plus className="w-5 h-5" />
<span>New Post</span>
</button>
</div>
</div>
</div>
@@ -530,8 +599,9 @@ const BlogManagementPage: React.FC = () => {
{/* Create/Edit Modal */}
{showModal && (
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-3 sm:p-4 animate-fade-in">
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-4xl w-full max-h-[95vh] sm:max-h-[90vh] overflow-hidden border border-slate-200/50 animate-scale-in">
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-3 sm:p-4 animate-fade-in">
<div className="min-h-full flex items-start justify-center py-8">
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-4xl w-full my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-slate-200/50 animate-scale-in">
<div className="sticky top-0 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-4 sm:px-6 py-4 sm:py-5 border-b border-slate-700 z-10">
<div className="flex justify-between items-center">
<div>
@@ -551,7 +621,7 @@ const BlogManagementPage: React.FC = () => {
</div>
</div>
<div className="overflow-y-auto max-h-[calc(95vh-120px)] sm:max-h-[calc(90vh-120px)]">
<div className="overflow-y-auto max-h-[calc(100vh-12rem)]">
<form onSubmit={handleSubmit} className="p-4 sm:p-6 space-y-5 sm:space-y-6">
<div>
<label className="block text-xs sm:text-sm font-semibold text-slate-600 uppercase tracking-wider mb-2">Title *</label>
@@ -1177,6 +1247,7 @@ const BlogManagementPage: React.FC = () => {
</div>
</div>
</div>
</div>
)}
{/* Delete Confirmation */}
@@ -1187,6 +1258,131 @@ const BlogManagementPage: React.FC = () => {
title="Delete Blog Post"
message="Are you sure you want to delete this blog post? This action cannot be undone."
/>
{/* Tag Controller Modal */}
{showTagController && (
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-3 sm:p-4 animate-fade-in">
<div className="min-h-full flex items-start justify-center py-8">
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-4xl w-full my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-slate-200/50 animate-scale-in">
<div className="sticky top-0 bg-gradient-to-r from-blue-600 via-blue-700 to-blue-600 px-4 sm:px-6 py-4 sm:py-5 border-b border-blue-800 z-10">
<div className="flex justify-between items-center">
<div>
<h2 className="text-lg sm:text-xl md:text-2xl font-bold text-white">
Tag Controller
</h2>
<p className="text-blue-100/80 text-xs sm:text-sm font-light mt-1">
Manage all blog tags - rename or delete tags across all posts
</p>
</div>
<button
onClick={() => {
setShowTagController(false);
setEditingTag(null);
}}
className="w-9 h-9 sm:w-10 sm:h-10 flex items-center justify-center rounded-xl text-white hover:bg-blue-800/50 transition-all duration-200 border border-blue-500 hover:border-blue-300"
>
<X className="w-5 h-5 sm:w-6 sm:h-6" />
</button>
</div>
</div>
<div className="overflow-y-auto max-h-[calc(100vh-12rem)]">
<div className="p-4 sm:p-6">
{tagsLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
</div>
) : tags.length === 0 ? (
<div className="text-center py-12">
<Tag className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<p className="text-gray-500 text-lg">No tags found</p>
<p className="text-gray-400 text-sm mt-2">Tags will appear here when you add them to blog posts</p>
</div>
) : (
<div className="space-y-3">
{tags.map((tag) => (
<div
key={tag.name}
className="bg-gradient-to-br from-gray-50 to-white border-2 border-gray-200 rounded-xl p-4 hover:border-blue-300 transition-all"
>
{editingTag && editingTag.old === tag.name ? (
<div className="flex items-center gap-3">
<input
type="text"
value={editingTag.new}
onChange={(e) => setEditingTag({ ...editingTag, new: e.target.value })}
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleRenameTag();
} else if (e.key === 'Escape') {
setEditingTag(null);
}
}}
autoFocus
className="flex-1 px-4 py-2 border-2 border-blue-400 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Enter new tag name"
/>
<button
onClick={handleRenameTag}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
Save
</button>
<button
onClick={() => setEditingTag(null)}
className="px-4 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400 transition-colors"
>
Cancel
</button>
</div>
) : (
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-1">
<div className="flex items-center gap-2">
<Tag className="w-5 h-5 text-blue-600" />
<span className="text-lg font-semibold text-gray-900">{tag.name}</span>
</div>
<span className="px-3 py-1 bg-blue-100 text-blue-700 rounded-full text-sm font-medium">
Used in {tag.usage_count} {tag.usage_count === 1 ? 'post' : 'posts'}
</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setEditingTag({ old: tag.name, new: tag.name })}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center gap-2"
>
<Edit className="w-4 h-4" />
<span>Rename</span>
</button>
<button
onClick={() => setDeleteTagConfirm({ show: true, tag: tag.name })}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors flex items-center gap-2"
>
<Trash2 className="w-4 h-4" />
<span>Delete</span>
</button>
</div>
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
</div>
)}
{/* Delete Tag Confirmation */}
<ConfirmationDialog
isOpen={deleteTagConfirm.show}
onClose={() => setDeleteTagConfirm({ show: false, tag: null })}
onConfirm={handleDeleteTag}
title="Delete Tag"
message={`Are you sure you want to delete the tag "${deleteTagConfirm.tag}"? This will remove it from all blog posts. This action cannot be undone.`}
/>
</div>
</div>
);

View File

@@ -115,7 +115,7 @@ const BookingManagementPage: React.FC = () => {
const response = await invoiceService.getInvoicesByBooking(booking.id);
// Check response structure - handle both possible formats
const invoices = response.data?.invoices || response.data?.data?.invoices || [];
const invoices = response.data?.invoices || (response.data as any)?.data?.invoices || [];
const hasInvoice = Array.isArray(invoices) && invoices.length > 0;
return {
@@ -197,7 +197,7 @@ const BookingManagementPage: React.FC = () => {
let invoice = null;
if (response.status === 'success' && response.data) {
// Try different possible response structures
invoice = response.data.invoice || response.data.data?.invoice || response.data;
invoice = response.data.invoice || (response.data as any).data?.invoice || response.data;
logger.debug('Extracted invoice', { invoice });
}
@@ -637,8 +637,9 @@ const BookingManagementPage: React.FC = () => {
{}
{showDetailModal && selectedBooking && (
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-3 sm:p-4 animate-fade-in">
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl w-full max-w-3xl max-h-[95vh] sm:max-h-[90vh] overflow-hidden animate-scale-in border border-slate-200">
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-3 sm:p-4 animate-fade-in">
<div className="min-h-full flex items-start justify-center py-8">
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl w-full max-w-3xl my-8 max-h-[calc(100vh-4rem)] overflow-hidden animate-scale-in border border-slate-200">
{}
<div className="bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-8 py-6 border-b border-slate-700">
<div className="flex justify-between items-center">
@@ -655,7 +656,7 @@ const BookingManagementPage: React.FC = () => {
</div>
</div>
<div className="overflow-y-auto max-h-[calc(95vh-120px)] sm:max-h-[calc(90vh-120px)]">
<div className="overflow-y-auto max-h-[calc(100vh-12rem)]">
<div className="p-4 sm:p-6 md:p-8 space-y-5 sm:space-y-6">
{}
<div className="grid grid-cols-2 gap-6">
@@ -985,6 +986,7 @@ const BookingManagementPage: React.FC = () => {
</button>
</div>
</div>
</div>
</div>
</div>
)}

View File

@@ -1044,8 +1044,9 @@ const BusinessDashboardPage: React.FC = () => {
{}
{showPromotionModal && (
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-3xl max-h-[90vh] overflow-hidden border border-gray-200">
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-4">
<div className="min-h-full flex items-start justify-center py-8">
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-3xl my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-gray-200">
{}
<div className="bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-8 py-6 border-b border-slate-700">
<div className="flex justify-between items-center">
@@ -1067,7 +1068,7 @@ const BusinessDashboardPage: React.FC = () => {
</div>
{}
<div className="p-8 overflow-y-auto max-h-[calc(90vh-120px)] custom-scrollbar">
<div className="p-8 overflow-y-auto max-h-[calc(100vh-12rem)] custom-scrollbar">
<form onSubmit={handlePromotionSubmit} className="space-y-6">
<div className="grid grid-cols-2 gap-6">
<div>
@@ -1241,7 +1242,8 @@ const BusinessDashboardPage: React.FC = () => {
</form>
</div>
</div>
</div>
</div>
</div>
)}
</div>
)}

File diff suppressed because it is too large Load Diff

View File

@@ -753,8 +753,9 @@ const LoyaltyManagementPage: React.FC = () => {
{/* Tier Modal */}
{showTierModal && (
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-3 sm:p-4 animate-fade-in">
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-2xl w-full max-h-[95vh] sm:max-h-[90vh] overflow-hidden border border-slate-200/50 animate-scale-in">
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-3 sm:p-4 animate-fade-in">
<div className="min-h-full flex items-start justify-center py-8">
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-2xl w-full my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-slate-200/50 animate-scale-in">
<div className="sticky top-0 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-4 sm:px-6 py-4 sm:py-5 border-b border-slate-700 z-10">
<div className="flex justify-between items-center">
<div>
@@ -776,7 +777,7 @@ const LoyaltyManagementPage: React.FC = () => {
</button>
</div>
</div>
<div className="overflow-y-auto max-h-[calc(95vh-120px)] sm:max-h-[calc(90vh-120px)]">
<div className="overflow-y-auto max-h-[calc(100vh-12rem)]">
<form onSubmit={handleTierSubmit} className="p-4 sm:p-6 space-y-4 sm:space-y-5">
<div className="grid grid-cols-2 gap-4">
<div>
@@ -919,13 +920,15 @@ const LoyaltyManagementPage: React.FC = () => {
</form>
</div>
</div>
</div>
</div>
)}
{/* Reward Modal */}
{showRewardModal && (
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-3 sm:p-4 animate-fade-in">
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-3xl w-full max-h-[95vh] sm:max-h-[90vh] overflow-hidden border border-slate-200/50 animate-scale-in">
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-3 sm:p-4 animate-fade-in">
<div className="min-h-full flex items-start justify-center py-8">
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-3xl w-full my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-slate-200/50 animate-scale-in">
<div className="sticky top-0 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-4 sm:px-6 py-4 sm:py-5 border-b border-slate-700 z-10">
<div className="flex justify-between items-center">
<div>
@@ -947,7 +950,7 @@ const LoyaltyManagementPage: React.FC = () => {
</button>
</div>
</div>
<div className="overflow-y-auto max-h-[calc(95vh-120px)] sm:max-h-[calc(90vh-120px)]">
<div className="overflow-y-auto max-h-[calc(100vh-12rem)]">
<form onSubmit={handleRewardSubmit} className="p-4 sm:p-6 space-y-4 sm:space-y-5">
<div className="grid grid-cols-2 gap-4">
<div>
@@ -1149,6 +1152,7 @@ const LoyaltyManagementPage: React.FC = () => {
</form>
</div>
</div>
</div>
</div>
)}

View File

@@ -315,8 +315,9 @@ const PromotionManagementPage: React.FC = () => {
{}
{showModal && (
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-4 animate-fade-in">
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-3xl max-h-[90vh] overflow-hidden border border-slate-200 animate-scale-in">
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-4 animate-fade-in">
<div className="min-h-full flex items-start justify-center py-8">
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-3xl my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-slate-200 animate-scale-in">
{}
<div className="bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-8 py-6 border-b border-slate-700">
<div className="flex justify-between items-center">
@@ -338,7 +339,7 @@ const PromotionManagementPage: React.FC = () => {
</div>
{}
<div className="p-8 overflow-y-auto max-h-[calc(90vh-120px)] custom-scrollbar">
<div className="p-8 overflow-y-auto max-h-[calc(100vh-12rem)] custom-scrollbar">
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-2 gap-6">
<div>
@@ -512,6 +513,8 @@ const PromotionManagementPage: React.FC = () => {
</form>
</div>
</div>
</div>
</div>
</div>
)}
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -475,8 +475,9 @@ const IPWhitelistTab: React.FC = () => {
</div>
{showAddModal && (
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-3 sm:p-4 animate-fade-in">
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-md w-full max-h-[95vh] sm:max-h-[90vh] overflow-hidden border border-slate-200/50 animate-scale-in">
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-3 sm:p-4 animate-fade-in">
<div className="min-h-full flex items-start justify-center py-8">
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-md w-full my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-slate-200/50 animate-scale-in">
<div className="sticky top-0 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-4 sm:px-6 py-4 sm:py-5 border-b border-slate-700 z-10">
<div className="flex justify-between items-center">
<div>
@@ -491,7 +492,7 @@ const IPWhitelistTab: React.FC = () => {
</button>
</div>
</div>
<div className="overflow-y-auto max-h-[calc(95vh-120px)] sm:max-h-[calc(90vh-120px)]">
<div className="overflow-y-auto max-h-[calc(100vh-12rem)]">
<div className="p-4 sm:p-6 space-y-4 sm:space-y-5">
<input
type="text"
@@ -649,8 +650,9 @@ const IPBlacklistTab: React.FC = () => {
</div>
{showAddModal && (
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-3 sm:p-4 animate-fade-in">
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-md w-full max-h-[95vh] sm:max-h-[90vh] overflow-hidden border border-slate-200/50 animate-scale-in">
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-3 sm:p-4 animate-fade-in">
<div className="min-h-full flex items-start justify-center py-8">
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-md w-full my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-slate-200/50 animate-scale-in">
<div className="sticky top-0 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-4 sm:px-6 py-4 sm:py-5 border-b border-slate-700 z-10">
<div className="flex justify-between items-center">
<div>
@@ -665,7 +667,7 @@ const IPBlacklistTab: React.FC = () => {
</button>
</div>
</div>
<div className="overflow-y-auto max-h-[calc(95vh-120px)] sm:max-h-[calc(90vh-120px)]">
<div className="overflow-y-auto max-h-[calc(100vh-12rem)]">
<div className="p-4 sm:p-6 space-y-4 sm:space-y-5">
<input
type="text"
@@ -888,8 +890,9 @@ const OAuthProvidersTab: React.FC = () => {
</div>
{showAddModal && (
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-3 sm:p-4 animate-fade-in">
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-2xl w-full max-h-[95vh] sm:max-h-[90vh] overflow-hidden border border-slate-200/50 animate-scale-in">
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-3 sm:p-4 animate-fade-in">
<div className="min-h-full flex items-start justify-center py-8">
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-2xl w-full my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-slate-200/50 animate-scale-in">
<div className="sticky top-0 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-4 sm:px-6 py-4 sm:py-5 border-b border-slate-700 z-10">
<div className="flex justify-between items-center">
<div>
@@ -908,7 +911,7 @@ const OAuthProvidersTab: React.FC = () => {
</button>
</div>
</div>
<div className="overflow-y-auto max-h-[calc(95vh-120px)] sm:max-h-[calc(90vh-120px)]">
<div className="overflow-y-auto max-h-[calc(100vh-12rem)]">
<div className="p-4 sm:p-6 space-y-3 sm:space-y-4">
<input
type="text"
@@ -1352,8 +1355,9 @@ const GDPRRequestsTab: React.FC = () => {
{/* Request Details Modal */}
{selectedRequest && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl p-4 sm:p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="fixed inset-0 bg-black/50 z-50 overflow-y-auto p-4">
<div className="min-h-full flex items-start justify-center py-8">
<div className="bg-white rounded-xl p-4 sm:p-6 max-w-2xl w-full my-8 max-h-[calc(100vh-4rem)] overflow-y-auto">
<div className="flex justify-between items-start mb-4">
<h4 className="text-base sm:text-lg font-semibold">GDPR Request Details</h4>
<button
@@ -1399,6 +1403,7 @@ const GDPRRequestsTab: React.FC = () => {
</button>
)}
</div>
</div>
</div>
</div>
)}

View File

@@ -240,8 +240,9 @@ const ServiceManagementPage: React.FC = () => {
{}
{showModal && (
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-4 animate-fade-in">
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-md max-h-[90vh] overflow-hidden border border-slate-200 animate-scale-in">
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-4 animate-fade-in">
<div className="min-h-full flex items-start justify-center py-8">
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-md my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-slate-200 animate-scale-in">
{}
<div className="bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-6 py-5 border-b border-slate-700">
<div className="flex justify-between items-center">
@@ -344,6 +345,7 @@ const ServiceManagementPage: React.FC = () => {
</form>
</div>
</div>
</div>
</div>
)}
</div>

View File

@@ -388,8 +388,9 @@ const UserManagementPage: React.FC = () => {
{}
{showModal && (
<div className="fixed inset-0 bg-black/70 backdrop-blur-md flex items-center justify-center z-50 p-3 sm:p-4 animate-fade-in">
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl w-full max-w-md max-h-[95vh] sm:max-h-[90vh] overflow-hidden border border-slate-200 animate-scale-in">
<div className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 overflow-y-auto p-3 sm:p-4 animate-fade-in">
<div className="min-h-full flex items-start justify-center py-8">
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl w-full max-w-md my-8 max-h-[calc(100vh-4rem)] overflow-hidden border border-slate-200 animate-scale-in">
{}
<div className="bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-6 py-5 border-b border-slate-700">
<div className="flex justify-between items-center">
@@ -410,7 +411,7 @@ const UserManagementPage: React.FC = () => {
</div>
</div>
<div className="overflow-y-auto max-h-[calc(95vh-120px)] sm:max-h-[calc(90vh-120px)]">
<div className="overflow-y-auto max-h-[calc(100vh-12rem)]">
<form onSubmit={handleSubmit} className="p-4 sm:p-6 space-y-4 sm:space-y-5">
<div>
<label className="block text-xs font-semibold text-slate-600 uppercase tracking-wider mb-2">
@@ -512,6 +513,7 @@ const UserManagementPage: React.FC = () => {
</form>
</div>
</div>
</div>
</div>
)}
</div>