feat: keep manage venue as separate column under main menu in admin panel
This commit is contained in:
@@ -25,6 +25,7 @@ import { StudentDashboard } from './components/dashboard/StudentDashboard';
|
|||||||
import { StudentRegistrationsView } from './components/dashboard/StudentRegistrationsView';
|
import { StudentRegistrationsView } from './components/dashboard/StudentRegistrationsView';
|
||||||
import { ManageNoticesView } from './components/dashboard/ManageNoticesView';
|
import { ManageNoticesView } from './components/dashboard/ManageNoticesView';
|
||||||
import { ManageStudentsView } from './components/dashboard/ManageStudentsView';
|
import { ManageStudentsView } from './components/dashboard/ManageStudentsView';
|
||||||
|
import { VenueManagement } from './components/dashboard/VenueManagement';
|
||||||
|
|
||||||
|
|
||||||
const AppContent: React.FC = () => {
|
const AppContent: React.FC = () => {
|
||||||
@@ -156,6 +157,8 @@ const AppContent: React.FC = () => {
|
|||||||
return <EventHistory />;
|
return <EventHistory />;
|
||||||
case 'classes':
|
case 'classes':
|
||||||
return user?.role === 'ADMIN' ? <ClassManagement /> : isDashboard;
|
return user?.role === 'ADMIN' ? <ClassManagement /> : isDashboard;
|
||||||
|
case 'manage-venues':
|
||||||
|
return user?.role === 'ADMIN' ? <VenueManagement /> : isDashboard;
|
||||||
case 'automation':
|
case 'automation':
|
||||||
return (user?.role === 'ADMIN' || user?.role === 'PRINCIPAL' || user?.role === 'HOD') ? <AutomationView /> : isDashboard;
|
return (user?.role === 'ADMIN' || user?.role === 'PRINCIPAL' || user?.role === 'HOD') ? <AutomationView /> : isDashboard;
|
||||||
case 'import-excel':
|
case 'import-excel':
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Sidebar } from './Sidebar';
|
import { Sidebar } from './Sidebar';
|
||||||
import { Search, Bell, Settings, LogOut, User as UserIcon, Plus, Calendar, ClipboardList, CheckCircle, Users, BookOpen, Zap, Clock, History as HistoryIcon, LayoutGrid, FileSpreadsheet } from 'lucide-react';
|
import { Search, Bell, Settings, LogOut, User as UserIcon, Plus, Calendar, ClipboardList, CheckCircle, Users, BookOpen, Zap, Clock, History as HistoryIcon, LayoutGrid, FileSpreadsheet, Building2 } from 'lucide-react';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { API_BASE_URL } from '../lib/config';
|
import { API_BASE_URL } from '../lib/config';
|
||||||
@@ -50,6 +50,7 @@ export const DashboardLayout: React.FC<DashboardLayoutProps> = ({
|
|||||||
{ id: 'history', label: 'History', icon: HistoryIcon, description: 'Event ledger and audit trail' },
|
{ id: 'history', label: 'History', icon: HistoryIcon, description: 'Event ledger and audit trail' },
|
||||||
{ id: 'user-management', label: 'Manage Users', icon: Users, description: 'User roles and access', roles: ['ADMIN'] },
|
{ id: 'user-management', label: 'Manage Users', icon: Users, description: 'User roles and access', roles: ['ADMIN'] },
|
||||||
{ id: 'classes', label: 'Classes', icon: BookOpen, description: 'Academic structure', roles: ['ADMIN'] },
|
{ id: 'classes', label: 'Classes', icon: BookOpen, description: 'Academic structure', roles: ['ADMIN'] },
|
||||||
|
{ id: 'manage-venues', label: 'Manage Venues', icon: Building2, description: 'Configure college venues and capacities', roles: ['ADMIN'] },
|
||||||
{ id: 'import-excel', label: 'Import Excel', icon: FileSpreadsheet, description: 'Bulk upload from response sheets', roles: ['ADMIN', 'PRINCIPAL'] },
|
{ id: 'import-excel', label: 'Import Excel', icon: FileSpreadsheet, description: 'Bulk upload from response sheets', roles: ['ADMIN', 'PRINCIPAL'] },
|
||||||
{ id: 'automation', label: 'Automation', icon: Zap, description: 'Smart scheduling tools', roles: ['ADMIN', 'PRINCIPAL', 'HOD'] },
|
{ id: 'automation', label: 'Automation', icon: Zap, description: 'Smart scheduling tools', roles: ['ADMIN', 'PRINCIPAL', 'HOD'] },
|
||||||
].filter(item => !item.roles || item.roles.includes(user?.role || ''));
|
].filter(item => !item.roles || item.roles.includes(user?.role || ''));
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import {
|
|||||||
HelpCircle,
|
HelpCircle,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
Bell
|
Bell,
|
||||||
|
Building2
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
@@ -39,6 +40,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ activeItem, onItemClick }) =>
|
|||||||
{ icon: HistoryIcon, label: 'History', id: 'history' },
|
{ icon: HistoryIcon, label: 'History', id: 'history' },
|
||||||
...((user?.role === 'ADMIN' || user?.role === 'PRINCIPAL' || user?.role === 'HOD') ? [{ icon: Zap, label: 'Automation', id: 'automation' }] : []),
|
...((user?.role === 'ADMIN' || user?.role === 'PRINCIPAL' || user?.role === 'HOD') ? [{ icon: Zap, label: 'Automation', id: 'automation' }] : []),
|
||||||
...(user?.role === 'ADMIN' ? [{ icon: BookOpen, label: 'Classes', id: 'classes' }] : []),
|
...(user?.role === 'ADMIN' ? [{ icon: BookOpen, label: 'Classes', id: 'classes' }] : []),
|
||||||
|
...(user?.role === 'ADMIN' ? [{ icon: Building2, label: 'Manage Venues', id: 'manage-venues' }] : []),
|
||||||
...(user?.role === 'ADMIN' ? [{ icon: Users, label: 'Manage Users', id: 'user-management' }] : []),
|
...(user?.role === 'ADMIN' ? [{ icon: Users, label: 'Manage Users', id: 'user-management' }] : []),
|
||||||
...(user?.role === 'ADMIN' ? [{ icon: Bell, label: 'Manage Notices', id: 'manage-notices' }] : []),
|
...(user?.role === 'ADMIN' ? [{ icon: Bell, label: 'Manage Notices', id: 'manage-notices' }] : []),
|
||||||
...((user?.role === 'FACULTY' || user?.role === 'HOD' || user?.role === 'ADMIN') ? [{ icon: Users, label: 'Student Hub', id: 'student-registrations' }] : []),
|
...((user?.role === 'FACULTY' || user?.role === 'HOD' || user?.role === 'ADMIN') ? [{ icon: Users, label: 'Student Hub', id: 'student-registrations' }] : []),
|
||||||
|
|||||||
@@ -57,14 +57,6 @@ export const ClassManagement: React.FC = () => {
|
|||||||
const [selectedBatchDept, setSelectedBatchDept] = useState('AI&DS');
|
const [selectedBatchDept, setSelectedBatchDept] = useState('AI&DS');
|
||||||
const [tempBatches, setTempBatches] = useState<any[]>([]);
|
const [tempBatches, setTempBatches] = useState<any[]>([]);
|
||||||
|
|
||||||
// Venue Management State
|
|
||||||
const [venues, setVenues] = useState<any[]>([]);
|
|
||||||
const [newVenueName, setNewVenueName] = useState('');
|
|
||||||
const [newVenueCapacity, setNewVenueCapacity] = useState('');
|
|
||||||
const [editingVenueId, setEditingVenueId] = useState<string | null>(null);
|
|
||||||
const [editVenueName, setEditVenueName] = useState('');
|
|
||||||
const [editVenueCapacity, setEditVenueCapacity] = useState('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const deptBatches = batches.filter(b => b.department === selectedBatchDept);
|
const deptBatches = batches.filter(b => b.department === selectedBatchDept);
|
||||||
setTempBatches(JSON.parse(JSON.stringify(deptBatches)));
|
setTempBatches(JSON.parse(JSON.stringify(deptBatches)));
|
||||||
@@ -81,108 +73,8 @@ export const ClassManagement: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchClasses();
|
fetchClasses();
|
||||||
fetchBatches();
|
fetchBatches();
|
||||||
fetchVenues();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchVenues = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(API_BASE_URL + '/api/venues');
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
setVenues(data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch venues:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateVenue = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!newVenueName.trim()) {
|
|
||||||
showAlert('Required', 'Please enter a venue name', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const capacityVal = newVenueCapacity.trim() ? Number(newVenueCapacity) : null;
|
|
||||||
const response = await fetch(API_BASE_URL + '/api/venues', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
name: newVenueName.trim(),
|
|
||||||
capacity: capacityVal
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
fetchVenues();
|
|
||||||
setNewVenueName('');
|
|
||||||
setNewVenueCapacity('');
|
|
||||||
showAlert('Success', 'Venue added successfully.', 'success');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to add venue:', error);
|
|
||||||
showAlert('Error', 'Failed to add venue.', 'error');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStartEditVenue = (venue: any) => {
|
|
||||||
setEditingVenueId(venue.id);
|
|
||||||
setEditVenueName(venue.name);
|
|
||||||
setEditVenueCapacity(venue.capacity !== null && venue.capacity !== undefined ? String(venue.capacity) : '');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveVenueEdit = async (venueId: string) => {
|
|
||||||
if (!editVenueName.trim()) {
|
|
||||||
showAlert('Required', 'Venue name cannot be empty', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const capacityVal = editVenueCapacity.trim() ? Number(editVenueCapacity) : null;
|
|
||||||
const response = await fetch(API_BASE_URL + '/api/venues', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
id: venueId,
|
|
||||||
name: editVenueName.trim(),
|
|
||||||
capacity: capacityVal
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
fetchVenues();
|
|
||||||
setEditingVenueId(null);
|
|
||||||
showAlert('Success', 'Venue updated successfully.', 'success');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to update venue:', error);
|
|
||||||
showAlert('Error', 'Failed to update venue.', 'error');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteVenue = async (venueId: string) => {
|
|
||||||
showConfirm(
|
|
||||||
'Delete Venue',
|
|
||||||
'Are you sure you want to delete this venue? This cannot be undone.',
|
|
||||||
async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE_URL}/api/venues/${venueId}`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
fetchVenues();
|
|
||||||
showAlert('Success', 'Venue deleted successfully.', 'success');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to delete venue:', error);
|
|
||||||
showAlert('Error', 'Failed to delete venue.', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchBatches = async () => {
|
const fetchBatches = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(API_BASE_URL + '/api/batches');
|
const response = await fetch(API_BASE_URL + '/api/batches');
|
||||||
@@ -748,134 +640,6 @@ export const ClassManagement: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Manage Venues Section */}
|
|
||||||
<div className="bg-white rounded-[2.5rem] border border-slate-100 premium-shadow p-8 space-y-6 mt-10">
|
|
||||||
<div className="flex flex-col lg:flex-row lg:items-center justify-between border-b border-slate-50 pb-6 gap-6">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="w-10 h-10 bg-brand-indigo rounded-xl flex items-center justify-center text-white">
|
|
||||||
<Building2 className="w-5 h-5" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xl font-black text-text-dark tracking-tight">Manage Venues</h3>
|
|
||||||
<p className="text-xs text-text-muted font-bold">Configure college venues, halls, and seating capacities.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={handleCreateVenue} className="flex flex-wrap items-center gap-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Venue Name (e.g. Mini Seminar Hall)"
|
|
||||||
value={newVenueName}
|
|
||||||
onChange={e => setNewVenueName(e.target.value)}
|
|
||||||
className="bg-slate-50 border border-slate-200 rounded-xl py-2 px-4 text-xs font-bold focus:bg-white focus:border-brand-indigo outline-none transition-all text-slate-800"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
placeholder="Capacity (e.g. 120)"
|
|
||||||
value={newVenueCapacity}
|
|
||||||
onChange={e => setNewVenueCapacity(e.target.value)}
|
|
||||||
className="bg-slate-50 border border-slate-200 rounded-xl py-2 px-4 text-xs font-bold focus:bg-white focus:border-brand-indigo outline-none transition-all text-slate-800 w-32"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="px-4 py-2 bg-brand-indigo text-white rounded-xl font-black text-[10px] uppercase tracking-widest hover:scale-[1.02] active:scale-[0.98] transition-all flex items-center gap-1.5"
|
|
||||||
>
|
|
||||||
<Plus className="w-3.5 h-3.5" />
|
|
||||||
Add Venue
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
|
||||||
{venues.map(venue => (
|
|
||||||
<div key={venue.id} className="bg-slate-50 p-6 rounded-3xl border border-slate-100 flex flex-col justify-between gap-4">
|
|
||||||
{editingVenueId === venue.id ? (
|
|
||||||
<div className="space-y-3 w-full">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Venue Name</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={editVenueName}
|
|
||||||
onChange={e => setEditVenueName(e.target.value)}
|
|
||||||
className="w-full bg-white border border-slate-200 rounded-xl py-1.5 px-3 text-xs font-bold focus:border-brand-indigo outline-none transition-all"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Seating Capacity</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={editVenueCapacity}
|
|
||||||
onChange={e => setEditVenueCapacity(e.target.value)}
|
|
||||||
className="w-full bg-white border border-slate-200 rounded-xl py-1.5 px-3 text-xs font-bold focus:border-brand-indigo outline-none transition-all"
|
|
||||||
placeholder="Unlimited / Custom"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 pt-2">
|
|
||||||
<button
|
|
||||||
onClick={() => handleSaveVenueEdit(venue.id)}
|
|
||||||
className="flex-1 py-2 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg font-black text-[9px] uppercase tracking-wider transition-all flex items-center justify-center gap-1"
|
|
||||||
>
|
|
||||||
<Save className="w-3.5 h-3.5" />
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setEditingVenueId(null)}
|
|
||||||
className="flex-1 py-2 bg-slate-200 hover:bg-slate-300 text-slate-700 rounded-lg font-black text-[9px] uppercase tracking-wider transition-all"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="flex items-start justify-between gap-4">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-10 h-10 rounded-xl bg-white border border-slate-200 flex items-center justify-center text-brand-indigo shadow-sm shrink-0">
|
|
||||||
<Building2 className="w-5 h-5" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-black text-brand-navy tracking-tight line-clamp-2" title={venue.name}>
|
|
||||||
{venue.name}
|
|
||||||
</h4>
|
|
||||||
<span className="text-[9px] font-black uppercase tracking-widest text-slate-400">
|
|
||||||
{venue.capacity ? `Capacity: ${venue.capacity} seats` : 'No Limit / Custom'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-2 border-t border-slate-200/50 pt-3">
|
|
||||||
<button
|
|
||||||
onClick={() => handleStartEditVenue(venue)}
|
|
||||||
className="p-2 bg-white border border-slate-200 rounded-lg text-slate-400 hover:text-brand-indigo hover:border-brand-indigo transition-all"
|
|
||||||
title="Edit Venue"
|
|
||||||
>
|
|
||||||
<Edit className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDeleteVenue(venue.id)}
|
|
||||||
className="p-2 bg-white border border-slate-200 rounded-lg text-slate-400 hover:text-status-danger hover:border-status-danger transition-all"
|
|
||||||
title="Delete Venue"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{venues.length === 0 && (
|
|
||||||
<div className="col-span-full py-12 text-center text-slate-400 font-bold uppercase tracking-wider text-xs border-2 border-dashed border-slate-200 rounded-[2rem]">
|
|
||||||
No venues registered. Use the form above to add a new venue.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
271
frontend/src/components/dashboard/VenueManagement.tsx
Normal file
271
frontend/src/components/dashboard/VenueManagement.tsx
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
import { API_BASE_URL } from '../../lib/config';
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
Trash2,
|
||||||
|
Building2,
|
||||||
|
Edit,
|
||||||
|
Save,
|
||||||
|
CheckCircle2
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useDialog } from '../../context/DialogContext';
|
||||||
|
|
||||||
|
export const VenueManagement: React.FC = () => {
|
||||||
|
const { showAlert, showConfirm } = useDialog();
|
||||||
|
const [venues, setVenues] = useState<any[]>([]);
|
||||||
|
const [newVenueName, setNewVenueName] = useState('');
|
||||||
|
const [newVenueCapacity, setNewVenueCapacity] = useState('');
|
||||||
|
const [editingVenueId, setEditingVenueId] = useState<string | null>(null);
|
||||||
|
const [editVenueName, setEditVenueName] = useState('');
|
||||||
|
const [editVenueCapacity, setEditVenueCapacity] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchVenues();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchVenues = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(API_BASE_URL + '/api/venues');
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setVenues(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch venues:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateVenue = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!newVenueName.trim()) {
|
||||||
|
showAlert('Required', 'Please enter a venue name', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const capacityVal = newVenueCapacity.trim() ? Number(newVenueCapacity) : null;
|
||||||
|
const response = await fetch(API_BASE_URL + '/api/venues', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: newVenueName.trim(),
|
||||||
|
capacity: capacityVal
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
fetchVenues();
|
||||||
|
setNewVenueName('');
|
||||||
|
setNewVenueCapacity('');
|
||||||
|
showAlert('Success', 'Venue added successfully.', 'success');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to add venue:', error);
|
||||||
|
showAlert('Error', 'Failed to add venue.', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStartEditVenue = (venue: any) => {
|
||||||
|
setEditingVenueId(venue.id);
|
||||||
|
setEditVenueName(venue.name);
|
||||||
|
setEditVenueCapacity(venue.capacity !== null && venue.capacity !== undefined ? String(venue.capacity) : '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveVenueEdit = async (venueId: string) => {
|
||||||
|
if (!editVenueName.trim()) {
|
||||||
|
showAlert('Required', 'Venue name cannot be empty', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const capacityVal = editVenueCapacity.trim() ? Number(editVenueCapacity) : null;
|
||||||
|
const response = await fetch(API_BASE_URL + '/api/venues', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: venueId,
|
||||||
|
name: editVenueName.trim(),
|
||||||
|
capacity: capacityVal
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
fetchVenues();
|
||||||
|
setEditingVenueId(null);
|
||||||
|
showAlert('Success', 'Venue updated successfully.', 'success');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update venue:', error);
|
||||||
|
showAlert('Error', 'Failed to update venue.', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteVenue = async (venueId: string) => {
|
||||||
|
showConfirm(
|
||||||
|
'Delete Venue',
|
||||||
|
'Are you sure you want to delete this venue? This cannot be undone.',
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/venues/${venueId}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
fetchVenues();
|
||||||
|
showAlert('Success', 'Venue deleted successfully.', 'success');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete venue:', error);
|
||||||
|
showAlert('Error', 'Failed to delete venue.', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-10">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-4xl font-black text-text-dark tracking-tight mb-1">Venue Management</h1>
|
||||||
|
<p className="text-text-muted font-black uppercase tracking-widest text-[10px]">Configure college venues, halls, and seating capacities</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-12 h-12 bg-brand-glow rounded-2xl flex items-center justify-center text-brand-indigo premium-shadow">
|
||||||
|
<Building2 className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Manage Venues Section */}
|
||||||
|
<div className="bg-white rounded-[2.5rem] border border-slate-100 premium-shadow p-8 space-y-6">
|
||||||
|
<div className="flex flex-col lg:flex-row lg:items-center justify-between border-b border-slate-50 pb-6 gap-6">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-10 h-10 bg-brand-indigo rounded-xl flex items-center justify-center text-white">
|
||||||
|
<Building2 className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-black text-text-dark tracking-tight">Registered Venues</h3>
|
||||||
|
<p className="text-xs text-text-muted font-bold">Configure college venues, halls, and seating capacities.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleCreateVenue} className="flex flex-wrap items-center gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Venue Name (e.g. Mini Seminar Hall)"
|
||||||
|
value={newVenueName}
|
||||||
|
onChange={e => setNewVenueName(e.target.value)}
|
||||||
|
className="bg-slate-50 border border-slate-200 rounded-xl py-2 px-4 text-xs font-bold focus:bg-white focus:border-brand-indigo outline-none transition-all text-slate-800"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="Capacity (e.g. 120)"
|
||||||
|
value={newVenueCapacity}
|
||||||
|
onChange={e => setNewVenueCapacity(e.target.value)}
|
||||||
|
className="bg-slate-50 border border-slate-200 rounded-xl py-2 px-4 text-xs font-bold focus:bg-white focus:border-brand-indigo outline-none transition-all text-slate-800 w-32"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-4 py-2 bg-brand-indigo text-white rounded-xl font-black text-[10px] uppercase tracking-widest hover:scale-[1.02] active:scale-[0.98] transition-all flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
<Plus className="w-3.5 h-3.5" />
|
||||||
|
Add Venue
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||||
|
{venues.map(venue => (
|
||||||
|
<div key={venue.id} className="bg-slate-50 p-6 rounded-3xl border border-slate-100 flex flex-col justify-between gap-4">
|
||||||
|
{editingVenueId === venue.id ? (
|
||||||
|
<div className="space-y-3 w-full">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Venue Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editVenueName}
|
||||||
|
onChange={e => setEditVenueName(e.target.value)}
|
||||||
|
className="w-full bg-white border border-slate-200 rounded-xl py-1.5 px-3 text-xs font-bold focus:border-brand-indigo outline-none transition-all"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Seating Capacity</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={editVenueCapacity}
|
||||||
|
onChange={e => setEditVenueCapacity(e.target.value)}
|
||||||
|
className="w-full bg-white border border-slate-200 rounded-xl py-1.5 px-3 text-xs font-bold focus:border-brand-indigo outline-none transition-all"
|
||||||
|
placeholder="Unlimited / Custom"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleSaveVenueEdit(venue.id)}
|
||||||
|
className="flex-1 py-2 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg font-black text-[9px] uppercase tracking-wider transition-all flex items-center justify-center gap-1"
|
||||||
|
>
|
||||||
|
<Save className="w-3.5 h-3.5" />
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setEditingVenueId(null)}
|
||||||
|
className="flex-1 py-2 bg-slate-200 hover:bg-slate-300 text-slate-700 rounded-lg font-black text-[9px] uppercase tracking-wider transition-all"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-xl bg-white border border-slate-200 flex items-center justify-center text-brand-indigo shadow-sm shrink-0">
|
||||||
|
<Building2 className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-black text-brand-navy tracking-tight line-clamp-2" title={venue.name}>
|
||||||
|
{venue.name}
|
||||||
|
</h4>
|
||||||
|
<span className="text-[9px] font-black uppercase tracking-widest text-slate-400">
|
||||||
|
{venue.capacity ? `Capacity: ${venue.capacity} seats` : 'No Limit / Custom'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end gap-2 border-t border-slate-200/50 pt-3">
|
||||||
|
<button
|
||||||
|
onClick={() => handleStartEditVenue(venue)}
|
||||||
|
className="p-2 bg-white border border-slate-200 rounded-lg text-slate-400 hover:text-brand-indigo hover:border-brand-indigo transition-all"
|
||||||
|
title="Edit Venue"
|
||||||
|
>
|
||||||
|
<Edit className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteVenue(venue.id)}
|
||||||
|
className="p-2 bg-white border border-slate-200 rounded-lg text-slate-400 hover:text-status-danger hover:border-status-danger transition-all"
|
||||||
|
title="Delete Venue"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{venues.length === 0 && (
|
||||||
|
<div className="col-span-full py-12 text-center text-slate-400 font-bold uppercase tracking-wider text-xs border-2 border-dashed border-slate-200 rounded-[2rem]">
|
||||||
|
No venues registered. Use the form above to add a new venue.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user