updates
This commit is contained in:
@@ -1,25 +1,29 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Search, Eye, XCircle, CheckCircle, Loader2, FileText, Plus } from 'lucide-react';
|
||||
import { Search, Eye, XCircle, CheckCircle, Loader2, FileText, Plus, Mail } from 'lucide-react';
|
||||
import { bookingService, Booking, invoiceService } from '../../services/api';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../utils/format';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import CreateBookingModal from '../../components/shared/CreateBookingModal';
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
const BookingManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [bookings, setBookings] = useState<Booking[]>([]);
|
||||
const [bookingsWithInvoices, setBookingsWithInvoices] = useState<Set<number>>(new Set());
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [checkingInvoices, setCheckingInvoices] = useState(false);
|
||||
const [selectedBooking, setSelectedBooking] = useState<Booking | null>(null);
|
||||
const [showDetailModal, setShowDetailModal] = useState(false);
|
||||
const [updatingBookingId, setUpdatingBookingId] = useState<number | null>(null);
|
||||
const [cancellingBookingId, setCancellingBookingId] = useState<number | null>(null);
|
||||
const [creatingInvoice, setCreatingInvoice] = useState(false);
|
||||
const [sendingEmail, setSendingEmail] = useState(false);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [filters, setFilters] = useState({
|
||||
search: '',
|
||||
@@ -29,6 +33,7 @@ const BookingManagementPage: React.FC = () => {
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [totalItems, setTotalItems] = useState(0);
|
||||
const itemsPerPage = 5;
|
||||
const showOnlyWithoutInvoices = searchParams.get('createInvoice') === 'true';
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
@@ -38,6 +43,16 @@ const BookingManagementPage: React.FC = () => {
|
||||
fetchBookings();
|
||||
}, [filters, currentPage]);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch invoices for all bookings to check which have invoices
|
||||
if (bookings.length > 0) {
|
||||
fetchBookingsInvoices();
|
||||
} else {
|
||||
// Reset invoices set when bookings are cleared
|
||||
setBookingsWithInvoices(new Set());
|
||||
}
|
||||
}, [bookings]);
|
||||
|
||||
const fetchBookings = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -58,6 +73,57 @@ const BookingManagementPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchBookingsInvoices = async () => {
|
||||
try {
|
||||
if (!bookings || bookings.length === 0) {
|
||||
setBookingsWithInvoices(new Set());
|
||||
setCheckingInvoices(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setCheckingInvoices(true);
|
||||
const invoiceChecks = await Promise.all(
|
||||
bookings.map(async (booking) => {
|
||||
try {
|
||||
// Validate booking ID
|
||||
if (!booking || !booking.id || isNaN(booking.id) || booking.id <= 0) {
|
||||
return { bookingId: booking?.id || 0, hasInvoice: false };
|
||||
}
|
||||
|
||||
const response = await invoiceService.getInvoicesByBooking(booking.id);
|
||||
|
||||
// Check response structure - handle both possible formats
|
||||
const invoices = response.data?.invoices || response.data?.data?.invoices || [];
|
||||
const hasInvoice = Array.isArray(invoices) && invoices.length > 0;
|
||||
|
||||
return {
|
||||
bookingId: booking.id,
|
||||
hasInvoice: hasInvoice
|
||||
};
|
||||
} catch (error: any) {
|
||||
// Log error but don't fail the entire operation
|
||||
logger.error(`Error checking invoice for booking ${booking?.id}`, error);
|
||||
return { bookingId: booking?.id || 0, hasInvoice: false };
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const withInvoices = new Set(
|
||||
invoiceChecks
|
||||
.filter(check => check.bookingId > 0 && check.hasInvoice)
|
||||
.map(check => check.bookingId)
|
||||
);
|
||||
|
||||
setBookingsWithInvoices(withInvoices);
|
||||
} catch (error) {
|
||||
logger.error('Error checking invoices', error);
|
||||
// On error, assume no bookings have invoices to avoid blocking the UI
|
||||
setBookingsWithInvoices(new Set());
|
||||
} finally {
|
||||
setCheckingInvoices(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateStatus = async (id: number, status: string) => {
|
||||
try {
|
||||
setUpdatingBookingId(id);
|
||||
@@ -86,26 +152,115 @@ const BookingManagementPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateInvoice = async (bookingId: number) => {
|
||||
const handleCreateInvoice = async (bookingId: number, sendEmail: boolean = false) => {
|
||||
try {
|
||||
// Validate bookingId before proceeding
|
||||
if (!bookingId || isNaN(bookingId) || bookingId <= 0) {
|
||||
toast.error('Invalid booking ID');
|
||||
return;
|
||||
}
|
||||
|
||||
setCreatingInvoice(true);
|
||||
|
||||
const invoiceData = {
|
||||
booking_id: Number(bookingId),
|
||||
booking_id: bookingId,
|
||||
};
|
||||
|
||||
const response = await invoiceService.createInvoice(invoiceData);
|
||||
|
||||
if (response.status === 'success' && response.data?.invoice) {
|
||||
toast.success('Invoice created successfully!');
|
||||
setShowDetailModal(false);
|
||||
navigate(`/admin/invoices/${response.data.invoice.id}`);
|
||||
} else {
|
||||
throw new Error('Failed to create invoice');
|
||||
// Log the full response for debugging
|
||||
console.log('Invoice creation response:', JSON.stringify(response, null, 2));
|
||||
logger.info('Invoice creation response', { response });
|
||||
|
||||
// Check response structure - handle different possible formats
|
||||
let invoice = null;
|
||||
if (response.status === 'success' && response.data) {
|
||||
// Try different possible response structures
|
||||
invoice = response.data.invoice || response.data.data?.invoice || response.data;
|
||||
console.log('Extracted invoice:', invoice);
|
||||
}
|
||||
|
||||
if (!invoice) {
|
||||
console.error('Failed to create invoice - no invoice in response', response);
|
||||
logger.error('Failed to create invoice - no invoice in response', { response });
|
||||
toast.error(response.message || 'Failed to create invoice - no invoice data received');
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract and validate invoice ID - handle both number and string types
|
||||
let invoiceId = invoice.id;
|
||||
|
||||
// Log the invoice ID for debugging
|
||||
console.log('Extracted invoice ID:', { invoiceId, type: typeof invoiceId, invoice });
|
||||
logger.info('Extracted invoice ID', { invoiceId, type: typeof invoiceId, invoice });
|
||||
|
||||
// Convert to number if it's a string
|
||||
if (typeof invoiceId === 'string') {
|
||||
invoiceId = parseInt(invoiceId, 10);
|
||||
}
|
||||
|
||||
// Validate invoice ID before navigation
|
||||
if (!invoiceId || isNaN(invoiceId) || invoiceId <= 0 || !isFinite(invoiceId)) {
|
||||
console.error('Invalid invoice ID received from server', {
|
||||
originalInvoiceId: invoice.id,
|
||||
convertedInvoiceId: invoiceId,
|
||||
type: typeof invoiceId,
|
||||
invoice,
|
||||
response: response.data
|
||||
});
|
||||
logger.error('Invalid invoice ID received from server', {
|
||||
originalInvoiceId: invoice.id,
|
||||
convertedInvoiceId: invoiceId,
|
||||
type: typeof invoiceId,
|
||||
invoice,
|
||||
response: response.data
|
||||
});
|
||||
toast.error(`Failed to create invoice: Invalid invoice ID received (${invoice.id})`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure it's a number
|
||||
invoiceId = Number(invoiceId);
|
||||
|
||||
toast.success('Invoice created successfully!');
|
||||
|
||||
// Send email if requested
|
||||
if (sendEmail) {
|
||||
setSendingEmail(true);
|
||||
try {
|
||||
await invoiceService.sendInvoiceEmail(invoiceId);
|
||||
toast.success('Invoice sent via email successfully!');
|
||||
} catch (emailError: any) {
|
||||
toast.warning('Invoice created but email sending failed: ' + (emailError.response?.data?.message || emailError.message));
|
||||
} finally {
|
||||
setSendingEmail(false);
|
||||
}
|
||||
}
|
||||
|
||||
setShowDetailModal(false);
|
||||
// Update the bookings with invoices set immediately
|
||||
setBookingsWithInvoices(prev => new Set(prev).add(bookingId));
|
||||
// Remove the createInvoice query param if present
|
||||
if (showOnlyWithoutInvoices) {
|
||||
searchParams.delete('createInvoice');
|
||||
setSearchParams(searchParams);
|
||||
}
|
||||
// Delay to ensure invoice is fully committed to database before navigation
|
||||
// Increased delay to prevent race conditions
|
||||
setTimeout(() => {
|
||||
navigate(`/admin/invoices/${invoiceId}`);
|
||||
}, 500);
|
||||
} catch (error: any) {
|
||||
// Don't show "Invalid invoice ID" error if it's from validation - it's already handled above
|
||||
const errorMessage = error.response?.data?.detail || error.response?.data?.message || error.message || 'Unable to create invoice';
|
||||
toast.error(errorMessage);
|
||||
|
||||
// Only show error toast if it's not a validation error we've already handled
|
||||
if (!errorMessage.includes('Invalid invoice ID') && !errorMessage.includes('Invalid Invoice ID')) {
|
||||
toast.error(errorMessage);
|
||||
} else {
|
||||
// Log validation errors but don't show toast (they're handled above)
|
||||
logger.error('Invoice creation validation error', error);
|
||||
}
|
||||
logger.error('Invoice creation error', error);
|
||||
} finally {
|
||||
setCreatingInvoice(false);
|
||||
@@ -182,6 +337,29 @@ const BookingManagementPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{}
|
||||
{showOnlyWithoutInvoices && (
|
||||
<div className="bg-gradient-to-r from-amber-50 to-yellow-50 border-2 border-amber-200 rounded-xl p-4 mb-6 animate-fade-in">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="w-5 h-5 text-amber-600" />
|
||||
<div>
|
||||
<h3 className="font-semibold text-amber-900">Select Booking to Create Invoice</h3>
|
||||
<p className="text-sm text-amber-700 mt-1">Showing only bookings without invoices. Select a booking to create and send an invoice.</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
searchParams.delete('createInvoice');
|
||||
setSearchParams(searchParams);
|
||||
}}
|
||||
className="px-4 py-2 text-sm font-medium text-amber-700 hover:text-amber-900 hover:bg-amber-100 rounded-lg transition-colors"
|
||||
>
|
||||
Show All Bookings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-white/80 backdrop-blur-sm rounded-xl sm:rounded-2xl shadow-xl border border-slate-200/60 p-4 sm:p-5 md:p-6 animate-fade-in" style={{ animationDelay: '0.1s' }}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<div className="relative group">
|
||||
@@ -225,7 +403,76 @@ const BookingManagementPage: React.FC = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-slate-100">
|
||||
{bookings.map((booking, index) => (
|
||||
{loading || checkingInvoices ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-8 py-12 text-center">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<Loader2 className="w-8 h-8 text-amber-600 animate-spin mb-4" />
|
||||
<p className="text-slate-600 font-medium">
|
||||
{checkingInvoices ? 'Checking invoices...' : 'Loading bookings...'}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : bookings.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-8 py-12 text-center">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<FileText className="w-12 h-12 text-slate-400 mb-4" />
|
||||
<p className="text-slate-600 font-medium">
|
||||
{showOnlyWithoutInvoices
|
||||
? 'No bookings without invoices found'
|
||||
: 'No bookings found'}
|
||||
</p>
|
||||
{showOnlyWithoutInvoices && (
|
||||
<button
|
||||
onClick={() => {
|
||||
searchParams.delete('createInvoice');
|
||||
setSearchParams(searchParams);
|
||||
}}
|
||||
className="mt-4 px-4 py-2 text-sm font-medium text-amber-700 hover:text-amber-900 hover:bg-amber-100 rounded-lg transition-colors"
|
||||
>
|
||||
Show All Bookings
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : (() => {
|
||||
const filteredBookings = bookings.filter(booking => {
|
||||
// If showOnlyWithoutInvoices is true, only show bookings without invoices
|
||||
if (showOnlyWithoutInvoices) {
|
||||
return !bookingsWithInvoices.has(booking.id);
|
||||
}
|
||||
// Otherwise, show all bookings
|
||||
return true;
|
||||
});
|
||||
|
||||
if (filteredBookings.length === 0 && showOnlyWithoutInvoices) {
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-8 py-12 text-center">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<FileText className="w-12 h-12 text-slate-400 mb-4" />
|
||||
<p className="text-slate-600 font-medium">
|
||||
All bookings already have invoices
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
searchParams.delete('createInvoice');
|
||||
setSearchParams(searchParams);
|
||||
}}
|
||||
className="mt-4 px-4 py-2 text-sm font-medium text-amber-700 hover:text-amber-900 hover:bg-amber-100 rounded-lg transition-colors"
|
||||
>
|
||||
Show All Bookings
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
return filteredBookings.map((booking, index) => (
|
||||
<tr
|
||||
key={booking.id}
|
||||
className="hover:bg-gradient-to-r hover:from-amber-50/30 hover:to-yellow-50/30 transition-all duration-200 group border-b border-slate-100"
|
||||
@@ -289,7 +536,14 @@ const BookingManagementPage: React.FC = () => {
|
||||
})()}
|
||||
</td>
|
||||
<td className="px-8 py-5 whitespace-nowrap">
|
||||
{getStatusBadge(booking.status)}
|
||||
<div className="flex flex-col gap-1">
|
||||
{getStatusBadge(booking.status)}
|
||||
{!bookingsWithInvoices.has(booking.id) && (
|
||||
<span className="text-xs px-2 py-0.5 bg-blue-100 text-blue-700 rounded-full font-medium">
|
||||
No Invoice
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-8 py-5 whitespace-nowrap text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
@@ -348,7 +602,8 @@ const BookingManagementPage: React.FC = () => {
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
));
|
||||
})()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -363,8 +618,8 @@ 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-4 animate-fade-in">
|
||||
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-3xl max-h-[90vh] overflow-hidden animate-scale-in border border-slate-200">
|
||||
<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="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">
|
||||
@@ -381,9 +636,8 @@ const BookingManagementPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div className="p-8 overflow-y-auto max-h-[calc(90vh-120px)]">
|
||||
<div className="space-y-6">
|
||||
<div className="overflow-y-auto max-h-[calc(95vh-120px)] sm:max-h-[calc(90vh-120px)]">
|
||||
<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">
|
||||
<div className="bg-gradient-to-br from-slate-50 to-white p-5 rounded-xl border border-slate-200">
|
||||
@@ -422,15 +676,14 @@ const BookingManagementPage: React.FC = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="bg-gradient-to-br from-slate-50 to-white p-5 rounded-xl border border-slate-200">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
|
||||
<div className="bg-gradient-to-br from-slate-50 to-white p-4 sm:p-5 rounded-xl border border-slate-200">
|
||||
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2 block">Check-in Date</label>
|
||||
<p className="text-base font-semibold text-slate-900">{parseDateLocal(selectedBooking.check_in_date).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}</p>
|
||||
<p className="text-sm sm:text-base font-semibold text-slate-900">{parseDateLocal(selectedBooking.check_in_date).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}</p>
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-slate-50 to-white p-5 rounded-xl border border-slate-200">
|
||||
<div className="bg-gradient-to-br from-slate-50 to-white p-4 sm:p-5 rounded-xl border border-slate-200">
|
||||
<label className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2 block">Check-out Date</label>
|
||||
<p className="text-base font-semibold text-slate-900">{parseDateLocal(selectedBooking.check_out_date).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}</p>
|
||||
<p className="text-sm sm:text-base font-semibold text-slate-900">{parseDateLocal(selectedBooking.check_out_date).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -661,25 +914,50 @@ const BookingManagementPage: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div className="mt-8 pt-6 border-t border-slate-200 flex justify-between items-center">
|
||||
<button
|
||||
onClick={() => handleCreateInvoice(selectedBooking.id)}
|
||||
disabled={creatingInvoice}
|
||||
className="flex items-center gap-2 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 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-lg hover:shadow-xl"
|
||||
>
|
||||
{creatingInvoice ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
Creating Invoice...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileText className="w-5 h-5" />
|
||||
Create Invoice
|
||||
</>
|
||||
<div className="mt-6 sm:mt-8 pt-4 sm:pt-6 border-t border-slate-200 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-4">
|
||||
<div className="flex flex-col sm:flex-row gap-2 sm:gap-3 w-full sm:w-auto">
|
||||
<button
|
||||
onClick={() => handleCreateInvoice(selectedBooking.id, false)}
|
||||
disabled={creatingInvoice || sendingEmail || bookingsWithInvoices.has(selectedBooking.id)}
|
||||
className="flex items-center gap-2 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 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-lg hover:shadow-xl"
|
||||
>
|
||||
{creatingInvoice ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
Creating Invoice...
|
||||
</>
|
||||
) : bookingsWithInvoices.has(selectedBooking.id) ? (
|
||||
<>
|
||||
<FileText className="w-5 h-5" />
|
||||
Invoice Already Exists
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileText className="w-5 h-5" />
|
||||
Create Invoice
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{!bookingsWithInvoices.has(selectedBooking.id) && (
|
||||
<button
|
||||
onClick={() => handleCreateInvoice(selectedBooking.id, true)}
|
||||
disabled={creatingInvoice || sendingEmail}
|
||||
className="flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl font-semibold hover:from-blue-600 hover:to-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-lg hover:shadow-xl"
|
||||
>
|
||||
{creatingInvoice || sendingEmail ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
{creatingInvoice ? 'Creating...' : 'Sending Email...'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Mail className="w-5 h-5" />
|
||||
Create & Send Invoice
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowDetailModal(false)}
|
||||
className="px-8 py-3 bg-gradient-to-r from-slate-700 to-slate-800 text-white rounded-xl font-semibold hover:from-slate-800 hover:to-slate-900 transition-all duration-200 shadow-lg hover:shadow-xl border border-slate-600"
|
||||
|
||||
Reference in New Issue
Block a user