You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

547 lines
17 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { Loader2, AlertCircle, Plus, Edit, Trash2, AlertTriangle, X } from 'lucide-react';
import {
getCourts,
createCourt,
updateCourt,
deleteCourt,
getCourtDependencies,
} from '@/src/lib/api/courts';
import type { Court, CourtRequest, CourtDependencies } from '@/src/types/courts';
import { formatTimestamp } from '@/src/types/courts';
interface ClubCourtsTabProps {
clubId: number;
}
export default function ClubCourtsTab({ clubId }: ClubCourtsTabProps) {
const [courts, setCourts] = useState<Court[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Modal state
const [showAddModal, setShowAddModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showDependenciesModal, setShowDependenciesModal] = useState(false);
const [selectedCourt, setSelectedCourt] = useState<Court | null>(null);
const [dependencies, setDependencies] = useState<CourtDependencies | null>(null);
useEffect(() => {
loadCourts();
}, [clubId]);
async function loadCourts() {
setLoading(true);
const result = await getCourts(clubId);
if (result.success) {
setCourts(result.data);
setError(null);
} else {
setError(result.error.detail);
}
setLoading(false);
}
function handleAdd() {
setShowAddModal(true);
}
function handleEdit(court: Court) {
setSelectedCourt(court);
setShowEditModal(true);
}
async function handleDeleteClick(court: Court) {
setSelectedCourt(court);
// Check dependencies first
const result = await getCourtDependencies(clubId, court.court_id);
if (result.success) {
setDependencies(result.data);
if (result.data.can_delete) {
// No dependencies, show delete confirmation
setShowDeleteModal(true);
} else {
// Has dependencies, show blocking modal
setShowDependenciesModal(true);
}
} else {
setError(result.error.detail);
}
}
function closeModals() {
setShowAddModal(false);
setShowEditModal(false);
setShowDeleteModal(false);
setShowDependenciesModal(false);
setSelectedCourt(null);
setDependencies(null);
}
async function handleSuccess() {
await loadCourts();
closeModals();
}
// Loading state
if (loading) {
return (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-8 h-8 text-slate-600 animate-spin" />
</div>
);
}
// Error state
if (error && courts.length === 0) {
return (
<div className="bg-red-50 border-2 border-red-200 rounded-2xl p-6">
<div className="flex items-start space-x-3">
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
<p className="text-red-700">{error}</p>
</div>
</div>
);
}
return (
<div>
{/* Header with Add button */}
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-2xl font-bold text-slate-900">Courts</h2>
<p className="text-slate-600">Manage court inventory for this club</p>
</div>
<button
onClick={handleAdd}
className="inline-flex items-center px-6 py-3 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors"
>
<Plus className="w-5 h-5 mr-2" />
Add Court
</button>
</div>
{/* Empty state */}
{courts.length === 0 ? (
<div className="bg-slate-50 border-2 border-slate-200 rounded-2xl p-12">
<div className="flex flex-col items-center text-center space-y-6">
<div className="text-6xl">🎾</div>
<h3 className="text-2xl font-bold text-slate-900">
No Courts Yet
</h3>
<p className="text-slate-600 max-w-md leading-relaxed">
Add your first court to start managing slot definitions and bookings for this club.
</p>
<button
onClick={handleAdd}
className="inline-flex items-center px-6 py-3 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors"
>
<Plus className="w-5 h-5 mr-2" />
Add First Court
</button>
</div>
</div>
) : (
/* Court list */
<div className="space-y-4">
{courts.map((court) => (
<div
key={court.court_id}
className="bg-white border-2 border-slate-200 rounded-2xl p-6 hover:border-slate-300 transition-colors"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className="text-xl font-bold text-slate-900 mb-2">
{court.name}
</h3>
<p className="text-sm text-slate-600">
ID: {court.court_id} · Created {formatTimestamp(court.created_at)}
</p>
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => handleEdit(court)}
className="p-2 text-slate-600 hover:text-slate-900 hover:bg-slate-100 rounded-lg transition-colors"
title="Edit"
>
<Edit className="w-5 h-5" />
</button>
<button
onClick={() => handleDeleteClick(court)}
className="p-2 text-red-600 hover:text-red-700 hover:bg-red-50 rounded-lg transition-colors"
title="Delete"
>
<Trash2 className="w-5 h-5" />
</button>
</div>
</div>
</div>
))}
</div>
)}
{/* Add Modal */}
{showAddModal && (
<CourtFormModal
clubId={clubId}
onClose={closeModals}
onSuccess={handleSuccess}
/>
)}
{/* Edit Modal */}
{showEditModal && selectedCourt && (
<CourtFormModal
clubId={clubId}
court={selectedCourt}
onClose={closeModals}
onSuccess={handleSuccess}
/>
)}
{/* Delete Confirmation Modal */}
{showDeleteModal && selectedCourt && (
<DeleteConfirmationModal
clubId={clubId}
court={selectedCourt}
onClose={closeModals}
onSuccess={handleSuccess}
/>
)}
{/* Dependencies Blocking Modal */}
{showDependenciesModal && selectedCourt && dependencies && (
<DependenciesBlockingModal
court={selectedCourt}
dependencies={dependencies}
onClose={closeModals}
/>
)}
</div>
);
}
/**
* Court Form Modal (Add/Edit)
*/
interface CourtFormModalProps {
clubId: number;
court?: Court;
onClose: () => void;
onSuccess: () => void;
}
function CourtFormModal({ clubId, court, onClose, onSuccess }: CourtFormModalProps) {
const isEditing = !!court;
const [name, setName] = useState(court?.name || '');
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const [fieldError, setFieldError] = useState('');
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!name || name.trim().length === 0) {
setFieldError('Court name is required');
return;
}
setSaving(true);
setError('');
setFieldError('');
const request: CourtRequest = {
name: name.trim(),
};
const result = isEditing
? await updateCourt(clubId, court!.court_id, request)
: await createCourt(clubId, request);
if (result.success) {
onSuccess();
} else {
if (result.error.code === 'validation_error' && result.error.errors) {
const nameError = result.error.errors.find(e => e.field === 'name');
if (nameError) {
setFieldError(nameError.message);
}
} else if (result.error.code === 'court_name_duplicate') {
setFieldError(result.error.detail);
} else {
setError(result.error.detail);
}
}
setSaving(false);
}
return (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl p-8 max-w-md w-full">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-slate-900">
{isEditing ? 'Edit Court' : 'Add Court'}
</h2>
<button
onClick={onClose}
className="p-2 hover:bg-slate-100 rounded-lg transition-colors"
disabled={saving}
>
<X className="w-5 h-5 text-slate-600" />
</button>
</div>
{/* Error */}
{error && (
<div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-start space-x-3">
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-700">{error}</p>
</div>
</div>
)}
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-semibold text-slate-900 mb-2">
Court Name <span className="text-red-600">*</span>
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder='e.g., "Court 1", "North Court", "VIP Court"'
maxLength={100}
className={`w-full px-4 py-3 border-2 rounded-lg font-medium transition-colors ${
fieldError
? 'border-red-300 focus:border-red-500'
: 'border-slate-200 focus:border-slate-900'
} focus:outline-none`}
disabled={saving}
autoFocus
/>
{fieldError && (
<p className="mt-1 text-sm text-red-600">{fieldError}</p>
)}
<p className="mt-2 text-xs text-slate-500">
Examples: "Court 1", "North Court", "VIP Court"
</p>
</div>
{/* Actions */}
<div className="flex items-center justify-end space-x-3 pt-4 border-t-2 border-slate-100">
<button
type="button"
onClick={onClose}
className="px-6 py-3 text-slate-700 font-semibold rounded-lg hover:bg-slate-100 transition-colors"
disabled={saving}
>
Cancel
</button>
<button
type="submit"
className="inline-flex items-center px-6 py-3 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors disabled:opacity-50"
disabled={saving}
>
{saving && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
{isEditing ? 'Save' : 'Add'}
</button>
</div>
</form>
</div>
</div>
);
}
/**
* Delete Confirmation Modal
*/
interface DeleteConfirmationModalProps {
clubId: number;
court: Court;
onClose: () => void;
onSuccess: () => void;
}
function DeleteConfirmationModal({ clubId, court, onClose, onSuccess }: DeleteConfirmationModalProps) {
const [deleting, setDeleting] = useState(false);
const [error, setError] = useState('');
async function handleDelete() {
setDeleting(true);
setError('');
const result = await deleteCourt(clubId, court.court_id);
if (result.success) {
onSuccess();
} else {
setError(result.error.detail);
}
setDeleting(false);
}
return (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl p-8 max-w-md w-full">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-slate-900">Delete Court?</h2>
<button
onClick={onClose}
className="p-2 hover:bg-slate-100 rounded-lg transition-colors"
disabled={deleting}
>
<X className="w-5 h-5 text-slate-600" />
</button>
</div>
{/* Error */}
{error && (
<div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-start space-x-3">
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-700">{error}</p>
</div>
</div>
)}
{/* Content */}
<div className="mb-6">
<p className="text-slate-700 leading-relaxed">
Are you sure you want to delete <strong>"{court.name}"</strong>?
</p>
<p className="text-slate-600 mt-2">
This action cannot be undone.
</p>
</div>
{/* Actions */}
<div className="flex items-center justify-end space-x-3">
<button
type="button"
onClick={onClose}
className="px-6 py-3 text-slate-700 font-semibold rounded-lg hover:bg-slate-100 transition-colors"
disabled={deleting}
>
Cancel
</button>
<button
onClick={handleDelete}
className="inline-flex items-center px-6 py-3 bg-red-600 text-white font-semibold rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50"
disabled={deleting}
>
{deleting && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
Delete
</button>
</div>
</div>
</div>
);
}
/**
* Dependencies Blocking Modal
*/
interface DependenciesBlockingModalProps {
court: Court;
dependencies: CourtDependencies;
onClose: () => void;
}
function DependenciesBlockingModal({ court, dependencies, onClose }: DependenciesBlockingModalProps) {
return (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl p-8 max-w-md w-full">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-3">
<AlertTriangle className="w-6 h-6 text-amber-600" />
<h2 className="text-2xl font-bold text-slate-900">Cannot Delete Court</h2>
</div>
<button
onClick={onClose}
className="p-2 hover:bg-slate-100 rounded-lg transition-colors"
>
<X className="w-5 h-5 text-slate-600" />
</button>
</div>
{/* Content */}
<div className="space-y-4">
<p className="text-slate-700 leading-relaxed">
Court <strong>"{court.name}"</strong> cannot be deleted because it is referenced by:
</p>
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 space-y-2">
{dependencies.dependencies.slot_definitions > 0 && (
<div className="flex justify-between items-center">
<span className="text-slate-700">Slot definitions:</span>
<span className="font-bold text-slate-900">
{dependencies.dependencies.slot_definitions}
</span>
</div>
)}
{dependencies.dependencies.slot_instances_future > 0 && (
<div className="flex justify-between items-center">
<span className="text-slate-700">Future slot instances:</span>
<span className="font-bold text-slate-900">
{dependencies.dependencies.slot_instances_future}
</span>
</div>
)}
{dependencies.dependencies.slot_instances_booked > 0 && (
<div className="flex justify-between items-center">
<span className="text-slate-700">Booked slot instances:</span>
<span className="font-bold text-slate-900">
{dependencies.dependencies.slot_instances_booked}
</span>
</div>
)}
</div>
<p className="text-sm text-slate-600 leading-relaxed">
To delete this court, you must first:
</p>
<ol className="list-decimal list-inside space-y-1 text-sm text-slate-700">
{dependencies.dependencies.slot_definitions > 0 && (
<li>Delete or reassign all slot definitions</li>
)}
{dependencies.dependencies.slot_instances_future > 0 && (
<li>Delete future slot instances</li>
)}
{dependencies.dependencies.slot_instances_booked > 0 && (
<li>Cancel or move all booked slot instances</li>
)}
</ol>
</div>
{/* Actions */}
<div className="flex items-center justify-end space-x-3 pt-6 border-t-2 border-slate-100 mt-6">
<button
onClick={onClose}
className="px-6 py-3 bg-slate-900 text-white font-semibold rounded-lg hover:bg-slate-800 transition-colors"
>
Close
</button>
</div>
</div>
</div>
);
}