Complete Phase 5: Update EventDetail and GroupDetail pages with Bootstrap

This commit is contained in:
steinhelge
2025-11-23 21:38:17 +01:00
parent 965c4136fc
commit 4b61bd7c29
2 changed files with 290 additions and 250 deletions
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Card, Button, Row, Col, Form, Modal } from 'react-bootstrap';
import { useEvent } from '../../hooks/useEvents';
import { useGroups, useCreateGroup } from '../../hooks/useGroups';
import { useProducts, useCreateProduct } from '../../hooks/useProducts';
@@ -14,8 +15,8 @@ export default function EventDetailPage() {
const createGroup = useCreateGroup(id!);
const createProduct = useCreateProduct(id!);
const [showGroupForm, setShowGroupForm] = useState(false);
const [showProductForm, setShowProductForm] = useState(false);
const [showGroupModal, setShowGroupModal] = useState(false);
const [showProductModal, setShowProductModal] = useState(false);
const [groupForm, setGroupForm] = useState<CreateGroupRequest>({ name: '' });
const [productForm, setProductForm] = useState<CreateProductRequest>({ name: '', type: ProductType.Drink });
@@ -23,143 +24,173 @@ export default function EventDetailPage() {
e.preventDefault();
await createGroup.mutateAsync(groupForm);
setGroupForm({ name: '' });
setShowGroupForm(false);
setShowGroupModal(false);
};
const handleCreateProduct = async (e: React.FormEvent) => {
e.preventDefault();
await createProduct.mutateAsync(productForm);
setProductForm({ name: '', type: ProductType.Drink });
setShowProductForm(false);
setShowProductModal(false);
};
if (!event) return <div className="p-8">Loading...</div>;
if (!event) return <div className="text-center py-5"><div className="spinner-border" role="status"><span className="visually-hidden">Loading...</span></div></div>;
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">{event.name}</h1>
<p className="text-gray-600 mt-2">{event.location}</p>
<>
<div className="mb-4">
<h1>{event.name}</h1>
<p className="text-muted">{event.location}</p>
<p className="text-muted">
{new Date(event.startDate).toLocaleDateString()} - {new Date(event.endDate).toLocaleDateString()}
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Groups Section */}
<div>
<div className="flex justify-between items-center mb-4">
<h2 className="text-2xl font-semibold">Groups</h2>
<button
onClick={() => setShowGroupForm(!showGroupForm)}
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 text-sm"
>
{showGroupForm ? 'Cancel' : 'Add Group'}
</button>
</div>
{showGroupForm && (
<div className="bg-white p-4 rounded-lg shadow mb-4">
<form onSubmit={handleCreateGroup} className="space-y-3">
<input
type="text"
placeholder="Group Name"
required
value={groupForm.name}
onChange={(e) => setGroupForm({ ...groupForm, name: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
/>
<input
type="text"
placeholder="Contact Person (optional)"
value={groupForm.contactPersonName || ''}
onChange={(e) => setGroupForm({ ...groupForm, contactPersonName: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
/>
<input
type="email"
placeholder="Contact Email (optional)"
value={groupForm.contactEmail || ''}
onChange={(e) => setGroupForm({ ...groupForm, contactEmail: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
/>
<button
type="submit"
className="w-full bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700"
>
Create Group
</button>
</form>
</div>
)}
<div className="space-y-3">
{groups?.map((group) => (
<div
key={group.id}
onClick={() => navigate(`/groups/${group.id}`)}
className="bg-white p-4 rounded-lg shadow cursor-pointer hover:shadow-lg transition-shadow"
>
<h3 className="font-semibold text-lg">{group.name}</h3>
{group.contactPersonName && (
<p className="text-sm text-gray-600">{group.contactPersonName}</p>
<Row>
<Col lg={6} className="mb-4">
<Card className="shadow-sm h-100">
<Card.Header className="bg-primary text-white d-flex justify-content-between align-items-center">
<h5 className="mb-0">Groups</h5>
<Button variant="light" size="sm" onClick={() => setShowGroupModal(true)}>
Add Group
</Button>
</Card.Header>
<Card.Body>
<div className="d-grid gap-3">
{groups?.map((group) => (
<Card
key={group.id}
className="border-0 shadow-sm"
style={{ cursor: 'pointer' }}
onClick={() => navigate(`/groups/${group.id}`)}
>
<Card.Body>
<h6 className="mb-1">{group.name}</h6>
{group.contactPersonName && (
<small className="text-muted d-block">{group.contactPersonName}</small>
)}
<span className="badge bg-secondary mt-2">{group.peopleCount || 0} people</span>
</Card.Body>
</Card>
))}
{(!groups || groups.length === 0) && (
<p className="text-muted text-center py-3">No groups yet</p>
)}
<p className="text-sm text-gray-500 mt-2">{group.peopleCount || 0} people</p>
</div>
))}
</div>
</div>
</Card.Body>
</Card>
</Col>
{/* Products Section */}
<div>
<div className="flex justify-between items-center mb-4">
<h2 className="text-2xl font-semibold">Products</h2>
<button
onClick={() => setShowProductForm(!showProductForm)}
className="bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 text-sm"
>
{showProductForm ? 'Cancel' : 'Add Product'}
</button>
</div>
<Col lg={6} className="mb-4">
<Card className="shadow-sm h-100">
<Card.Header className="bg-success text-white d-flex justify-content-between align-items-center">
<h5 className="mb-0">Products</h5>
<Button variant="light" size="sm" onClick={() => setShowProductModal(true)}>
Add Product
</Button>
</Card.Header>
<Card.Body>
<div className="d-grid gap-3">
{products?.map((product) => (
<Card key={product.id} className="border-0 shadow-sm">
<Card.Body>
<h6 className="mb-1">{product.name}</h6>
<span className="badge bg-info">{ProductType[product.type]}</span>
</Card.Body>
</Card>
))}
{(!products || products.length === 0) && (
<p className="text-muted text-center py-3">No products yet</p>
)}
</div>
</Card.Body>
</Card>
</Col>
</Row>
{showProductForm && (
<div className="bg-white p-4 rounded-lg shadow mb-4">
<form onSubmit={handleCreateProduct} className="space-y-3">
<input
type="text"
placeholder="Product Name"
required
value={productForm.name}
onChange={(e) => setProductForm({ ...productForm, name: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
/>
<select
value={productForm.type}
onChange={(e) => setProductForm({ ...productForm, type: parseInt(e.target.value) })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
>
<option value={ProductType.Access}>Access</option>
<option value={ProductType.Drink}>Drink</option>
<option value={ProductType.Meal}>Meal</option>
<option value={ProductType.Special}>Special</option>
</select>
<button
type="submit"
className="w-full bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700"
>
Create Product
</button>
</form>
{/* Group Modal */}
<Modal show={showGroupModal} onHide={() => setShowGroupModal(false)}>
<Modal.Header closeButton>
<Modal.Title>Add Group</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form onSubmit={handleCreateGroup}>
<Form.Group className="mb-3">
<Form.Label>Group Name</Form.Label>
<Form.Control
type="text"
required
value={groupForm.name}
onChange={(e) => setGroupForm({ ...groupForm, name: e.target.value })}
/>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Contact Person (optional)</Form.Label>
<Form.Control
type="text"
value={groupForm.contactPersonName || ''}
onChange={(e) => setGroupForm({ ...groupForm, contactPersonName: e.target.value })}
/>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Contact Email (optional)</Form.Label>
<Form.Control
type="email"
value={groupForm.contactEmail || ''}
onChange={(e) => setGroupForm({ ...groupForm, contactEmail: e.target.value })}
/>
</Form.Group>
<div className="d-flex justify-content-end gap-2">
<Button variant="secondary" onClick={() => setShowGroupModal(false)}>
Cancel
</Button>
<Button variant="primary" type="submit">
Create Group
</Button>
</div>
)}
</Form>
</Modal.Body>
</Modal>
<div className="space-y-3">
{products?.map((product) => (
<div key={product.id} className="bg-white p-4 rounded-lg shadow">
<h3 className="font-semibold text-lg">{product.name}</h3>
<p className="text-sm text-gray-500">{ProductType[product.type]}</p>
</div>
))}
</div>
</div>
</div>
</div>
{/* Product Modal */}
<Modal show={showProductModal} onHide={() => setShowProductModal(false)}>
<Modal.Header closeButton>
<Modal.Title>Add Product</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form onSubmit={handleCreateProduct}>
<Form.Group className="mb-3">
<Form.Label>Product Name</Form.Label>
<Form.Control
type="text"
required
value={productForm.name}
onChange={(e) => setProductForm({ ...productForm, name: e.target.value })}
/>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Product Type</Form.Label>
<Form.Select
value={productForm.type}
onChange={(e) => setProductForm({ ...productForm, type: parseInt(e.target.value) })}
>
<option value={ProductType.Access}>Access</option>
<option value={ProductType.Drink}>Drink</option>
<option value={ProductType.Meal}>Meal</option>
<option value={ProductType.Special}>Special</option>
</Form.Select>
</Form.Group>
<div className="d-flex justify-content-end gap-2">
<Button variant="secondary" onClick={() => setShowProductModal(false)}>
Cancel
</Button>
<Button variant="success" type="submit">
Create Product
</Button>
</div>
</Form>
</Modal.Body>
</Modal>
</>
);
}
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { useParams } from 'react-router-dom';
import { Card, Button, Form, Modal, ListGroup, Badge, Row, Col } from 'react-bootstrap';
import { useGroup, useCreatePerson, useAssignQuota } from '../../hooks/useGroups';
import { useProducts } from '../../hooks/useProducts';
import type { CreatePersonRequest, Person } from '../../lib/types';
@@ -10,13 +11,12 @@ export default function GroupDetailPage() {
const createPerson = useCreatePerson(id!);
const assignQuota = useAssignQuota();
const [showPersonForm, setShowPersonForm] = useState(false);
const [showPersonModal, setShowPersonModal] = useState(false);
const [personForm, setPersonForm] = useState<CreatePersonRequest>({ name: '' });
const [selectedPerson, setSelectedPerson] = useState<Person | null>(null);
const [quotaProductId, setQuotaProductId] = useState('');
const [quotaAmount, setQuotaAmount] = useState(1);
// Get event ID from group data
const eventId = group?.eventId || '';
const { data: products } = useProducts(eventId);
@@ -24,7 +24,7 @@ export default function GroupDetailPage() {
e.preventDefault();
await createPerson.mutateAsync(personForm);
setPersonForm({ name: '' });
setShowPersonForm(false);
setShowPersonModal(false);
refetch();
};
@@ -42,160 +42,169 @@ export default function GroupDetailPage() {
alert('Quota assigned successfully!');
setQuotaProductId('');
setQuotaAmount(1);
setSelectedPerson(null);
refetch();
} catch (error) {
alert('Failed to assign quota');
}
};
if (!group) return <div className="p-8">Loading...</div>;
if (!group) return <div className="text-center py-5"><div className="spinner-border" role="status"><span className="visually-hidden">Loading...</span></div></div>;
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">{group.name}</h1>
<>
<div className="mb-4">
<h1>{group.name}</h1>
{group.contactPersonName && (
<p className="text-gray-600 mt-2">Contact: {group.contactPersonName}</p>
<p className="text-muted mb-1">Contact: {group.contactPersonName}</p>
)}
{group.contactEmail && (
<p className="text-gray-600">{group.contactEmail}</p>
<p className="text-muted">{group.contactEmail}</p>
)}
</div>
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-semibold">People ({group.people?.length || 0})</h2>
<button
onClick={() => setShowPersonForm(!showPersonForm)}
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700"
>
{showPersonForm ? 'Cancel' : 'Add Person'}
</button>
<div className="d-flex justify-content-between align-items-center mb-3">
<h4>People ({group.people?.length || 0})</h4>
<Button variant="primary" onClick={() => setShowPersonModal(true)}>
Add Person
</Button>
</div>
{showPersonForm && (
<div className="bg-white p-6 rounded-lg shadow mb-6">
<h3 className="text-lg font-semibold mb-4">Add New Person</h3>
<form onSubmit={handleCreatePerson} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Name *</label>
<input
<Row className="g-4">
{group.people?.map((person) => (
<Col key={person.id} lg={6}>
<Card className="shadow-sm h-100">
<Card.Body>
<div className="d-flex justify-content-between align-items-start mb-3">
<div>
<h5 className="mb-1">{person.name}</h5>
{person.email && <small className="text-muted d-block">{person.email}</small>}
{person.phoneNumber && <small className="text-muted d-block">{person.phoneNumber}</small>}
<small className="font-monospace text-muted d-block mt-2">
QR: {person.qrCode.substring(0, 8)}...
</small>
</div>
<Button
variant={selectedPerson?.id === person.id ? 'secondary' : 'outline-primary'}
size="sm"
onClick={() => setSelectedPerson(selectedPerson?.id === person.id ? null : person)}
>
{selectedPerson?.id === person.id ? 'Close' : 'Assign Quota'}
</Button>
</div>
{person.quotas && person.quotas.length > 0 && (
<div className="border-top pt-3">
<h6 className="mb-2">Current Quotas</h6>
<ListGroup variant="flush">
{person.quotas.map((quota) => (
<ListGroup.Item key={quota.productId} className="px-0 d-flex justify-content-between">
<span>{quota.productName}</span>
<Badge bg="primary">{quota.remainingAmount}/{quota.initialAmount}</Badge>
</ListGroup.Item>
))}
</ListGroup>
</div>
)}
{selectedPerson?.id === person.id && (
<div className="border-top pt-3 mt-3">
<h6 className="mb-3">Assign New Quota</h6>
<Form.Group className="mb-2">
<Form.Select
size="sm"
value={quotaProductId}
onChange={(e) => setQuotaProductId(e.target.value)}
>
<option value="">Select Product</option>
{products?.map((product) => (
<option key={product.id} value={product.id}>
{product.name}
</option>
))}
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Control
type="number"
size="sm"
min="1"
value={quotaAmount}
onChange={(e) => setQuotaAmount(parseInt(e.target.value))}
placeholder="Amount"
/>
</Form.Group>
<Button
variant="success"
size="sm"
className="w-100"
onClick={() => handleAssignQuota(person.id)}
disabled={!quotaProductId || assignQuota.isPending}
>
{assignQuota.isPending ? 'Assigning...' : 'Assign Quota'}
</Button>
</div>
)}
</Card.Body>
</Card>
</Col>
))}
</Row>
{(!group.people || group.people.length === 0) && (
<Card className="text-center py-5 shadow-sm">
<Card.Body>
<p className="text-muted mb-3">No people in this group yet.</p>
<Button variant="primary" onClick={() => setShowPersonModal(true)}>
Add the first person
</Button>
</Card.Body>
</Card>
)}
{/* Add Person Modal */}
<Modal show={showPersonModal} onHide={() => setShowPersonModal(false)}>
<Modal.Header closeButton>
<Modal.Title>Add Person</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form onSubmit={handleCreatePerson}>
<Form.Group className="mb-3">
<Form.Label>Name *</Form.Label>
<Form.Control
type="text"
required
value={personForm.name}
onChange={(e) => setPersonForm({ ...personForm, name: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Email</label>
<input
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Email</Form.Label>
<Form.Control
type="email"
value={personForm.email || ''}
onChange={(e) => setPersonForm({ ...personForm, email: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Phone Number</label>
<input
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Phone Number</Form.Label>
<Form.Control
type="tel"
value={personForm.phoneNumber || ''}
onChange={(e) => setPersonForm({ ...personForm, phoneNumber: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
/>
</Form.Group>
<div className="d-flex justify-content-end gap-2">
<Button variant="secondary" onClick={() => setShowPersonModal(false)}>
Cancel
</Button>
<Button variant="primary" type="submit" disabled={createPerson.isPending}>
{createPerson.isPending ? 'Adding...' : 'Add Person'}
</Button>
</div>
<button
type="submit"
disabled={createPerson.isPending}
className="w-full bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{createPerson.isPending ? 'Adding...' : 'Add Person'}
</button>
</form>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{group.people?.map((person) => (
<div key={person.id} className="bg-white p-6 rounded-lg shadow">
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="text-lg font-semibold text-gray-900">{person.name}</h3>
{person.email && <p className="text-sm text-gray-600">{person.email}</p>}
{person.phoneNumber && <p className="text-sm text-gray-600">{person.phoneNumber}</p>}
<p className="text-xs text-gray-500 mt-2 font-mono">QR: {person.qrCode}</p>
</div>
<button
onClick={() => setSelectedPerson(selectedPerson?.id === person.id ? null : person)}
className="text-blue-600 hover:text-blue-700 text-sm font-medium"
>
{selectedPerson?.id === person.id ? 'Close' : 'Assign Quota'}
</button>
</div>
{person.quotas && person.quotas.length > 0 && (
<div className="border-t pt-4 mt-4">
<h4 className="text-sm font-semibold text-gray-700 mb-2">Current Quotas</h4>
<div className="space-y-2">
{person.quotas.map((quota) => (
<div key={quota.productId} className="flex justify-between text-sm">
<span>{quota.productName}</span>
<span className="font-semibold">{quota.remainingAmount}/{quota.initialAmount}</span>
</div>
))}
</div>
</div>
)}
{selectedPerson?.id === person.id && (
<div className="border-t pt-4 mt-4">
<h4 className="text-sm font-semibold text-gray-700 mb-3">Assign New Quota</h4>
<div className="space-y-3">
<select
value={quotaProductId}
onChange={(e) => setQuotaProductId(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
>
<option value="">Select Product</option>
{products?.map((product) => (
<option key={product.id} value={product.id}>
{product.name}
</option>
))}
</select>
<input
type="number"
min="1"
value={quotaAmount}
onChange={(e) => setQuotaAmount(parseInt(e.target.value))}
placeholder="Amount"
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
/>
<button
onClick={() => handleAssignQuota(person.id)}
disabled={!quotaProductId || assignQuota.isPending}
className="w-full bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 disabled:opacity-50 text-sm"
>
{assignQuota.isPending ? 'Assigning...' : 'Assign Quota'}
</button>
</div>
</div>
)}
</div>
))}
</div>
{(!group.people || group.people.length === 0) && !showPersonForm && (
<div className="text-center py-12 bg-gray-50 rounded-lg">
<p className="text-gray-500">No people in this group yet.</p>
<button
onClick={() => setShowPersonForm(true)}
className="mt-4 text-blue-600 hover:text-blue-700 font-medium"
>
Add the first person
</button>
</div>
)}
</div>
</Form>
</Modal.Body>
</Modal>
</>
);
}