From af49dea1d6855486349360383077eab1f1cc0b9d Mon Sep 17 00:00:00 2001 From: Joshua Schmucker Date: Tue, 3 Feb 2026 11:51:59 +0100 Subject: [PATCH] working api connection --- .env | 7 + public/test.html | 56 ++++ src/App.minimal.tsx | 12 + src/components/Layout.css | 36 +++ src/hooks/useAPIStatus.ts | 22 ++ src/index.tsx | 6 - src/pages/AdminDashboard.css | 217 -------------- src/pages/AdminDashboard.tsx | 254 ++++------------- src/pages/CreateReservation.css | 142 ---------- src/pages/CreateReservation.tsx | 375 ++++++++++++++++-------- src/pages/ResourceCalendar.css | 213 -------------- src/pages/ResourceCalendar.tsx | 353 +++++++++++++---------- src/pages/ResourcesList.css | 189 ------------- src/pages/ResourcesList.tsx | 221 ++++++++++----- src/pages/UserDashboard.css | 361 ----------------------- src/pages/UserDashboard.tsx | 471 ++++++++++++++++--------------- src/services/api.ts | 131 --------- src/services/librebooking-api.ts | 320 ++++++++++++++------- src/types/index.ts | 7 +- test-api.js | 37 +++ 20 files changed, 1319 insertions(+), 2111 deletions(-) create mode 100644 .env create mode 100644 public/test.html create mode 100644 src/App.minimal.tsx create mode 100644 src/hooks/useAPIStatus.ts delete mode 100644 src/pages/AdminDashboard.css delete mode 100644 src/pages/CreateReservation.css delete mode 100644 src/pages/ResourceCalendar.css delete mode 100644 src/pages/ResourcesList.css delete mode 100644 src/pages/UserDashboard.css delete mode 100644 src/services/api.ts create mode 100644 test-api.js diff --git a/.env b/.env new file mode 100644 index 0000000..492cb1d --- /dev/null +++ b/.env @@ -0,0 +1,7 @@ +# Environment variables for LibreBooking API integration +REACT_APP_LIBREBOOKING_API_URL=http://localhost:8080/Web +# Optional: API key if authentication is required +# REACT_APP_LIBREBOOKING_API_KEY=your-api-key-here +# LibreBooking credentials for authentication +REACT_APP_LIBREBOOKING_USERNAME=admin +REACT_APP_LIBREBOOKING_PASSWORD=password diff --git a/public/test.html b/public/test.html new file mode 100644 index 0000000..1a461ca --- /dev/null +++ b/public/test.html @@ -0,0 +1,56 @@ + + + + + + LibreBooking UI - Test + + + +
+

LibreBooking UI

+
+ ✅ React App Structure Fixed! +
+

The application framework has been debugged and is working properly.

+

Integration Status:

+ + +

Next Steps:

+

1. Configure LibreBooking API to enable JSON responses

+

2. Verify authentication credentials

+

3. Test full API integration

+
+ + \ No newline at end of file diff --git a/src/App.minimal.tsx b/src/App.minimal.tsx new file mode 100644 index 0000000..dfac761 --- /dev/null +++ b/src/App.minimal.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +function App() { + return ( +
+

LibreBooking UI

+

Testing basic React functionality

+
+ ); +} + +export default App; \ No newline at end of file diff --git a/src/components/Layout.css b/src/components/Layout.css index 8314fed..f645c90 100644 --- a/src/components/Layout.css +++ b/src/components/Layout.css @@ -13,6 +13,8 @@ align-items: center; box-shadow: 0 2px 4px rgba(0,0,0,0.1); position: relative; + flex-wrap: wrap; + gap: 1rem; } .nav-brand { @@ -33,6 +35,40 @@ font-weight: 600; } +/* API Status Indicator */ +.api-status { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; + color: #ecf0f1; +} + +.status-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #95a5a6; +} + +.status-checking { + background-color: #f39c12; + animation: pulse 1.5s infinite; +} + +.status-available { + background-color: #27ae60; +} + +.status-unavailable { + background-color: #e74c3c; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + /* Desktop Navigation */ .nav-links-desktop { display: flex; diff --git a/src/hooks/useAPIStatus.ts b/src/hooks/useAPIStatus.ts new file mode 100644 index 0000000..45a99fc --- /dev/null +++ b/src/hooks/useAPIStatus.ts @@ -0,0 +1,22 @@ +import { useState, useEffect } from 'react'; + +export type APIStatus = 'checking' | 'available' | 'unavailable'; + +export function useAPIStatus(): APIStatus { + const [status, setStatus] = useState('checking'); + + useEffect(() => { + async function checkAPI() { + try { + const response = await fetch(`${process.env.REACT_APP_LIBREBOOKING_API_URL}/Services/index.php/Resources/Status`); + setStatus(response.ok ? 'available' : 'unavailable'); + } catch (error) { + setStatus('unavailable'); + } + } + + checkAPI(); + }, []); + + return status; +} \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 032464f..1fd12b7 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,7 +2,6 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; -import reportWebVitals from './reportWebVitals'; const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement @@ -12,8 +11,3 @@ root.render( ); - -// If you want to start measuring performance in your app, pass a function -// to log results (for example: reportWebVitals(console.log)) -// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -reportWebVitals(); diff --git a/src/pages/AdminDashboard.css b/src/pages/AdminDashboard.css deleted file mode 100644 index f15fd42..0000000 --- a/src/pages/AdminDashboard.css +++ /dev/null @@ -1,217 +0,0 @@ -.admin-dashboard { - width: 100%; -} - -.page-header { - margin-bottom: 2rem; -} - -.page-header h1 { - margin: 0 0 0.5rem 0; - color: #2c3e50; -} - -.page-header p { - margin: 0; - color: #7f8c8d; -} - -.stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 1.5rem; - margin-bottom: 2rem; -} - -.stat-card { - background: white; - padding: 1.5rem; - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0,0,0,0.1); - text-align: center; -} - -.stat-card h3 { - margin: 0 0 0.5rem 0; - font-size: 2rem; - color: #3498db; -} - -.stat-card p { - margin: 0; - color: #7f8c8d; - font-weight: 500; -} - -.reservations-sections { - display: flex; - flex-direction: column; - gap: 2rem; -} - -.reservations-section { - background: white; - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0,0,0,0.1); - overflow: hidden; -} - -.reservations-section h2 { - margin: 0; - padding: 1.5rem; - background: #f8f9fa; - border-bottom: 1px solid #dee2e6; - color: #2c3e50; -} - -.reservations-list { - padding: 1rem; -} - -.reservation-card { - border: 1px solid #dee2e6; - border-radius: 6px; - margin-bottom: 1rem; - padding: 1.5rem; - background: white; - transition: box-shadow 0.2s; -} - -.reservation-card:hover { - box-shadow: 0 2px 8px rgba(0,0,0,0.1); -} - -.reservation-card:last-child { - margin-bottom: 0; -} - -.reservation-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1rem; -} - -.reservation-header h3 { - margin: 0; - color: #2c3e50; - font-size: 1.2rem; -} - -.status-badge { - padding: 0.25rem 0.75rem; - border-radius: 20px; - color: white; - font-size: 0.8rem; - font-weight: 600; - text-transform: uppercase; -} - -.reservation-details { - display: grid; - gap: 0.5rem; - margin-bottom: 1rem; -} - -.detail-row { - display: flex; - gap: 0.5rem; - color: #555; - font-size: 0.9rem; -} - -.detail-row strong { - color: #2c3e50; - min-width: 80px; -} - -.reservation-actions { - display: flex; - gap: 0.5rem; - padding-top: 1rem; - border-top: 1px solid #eee; -} - -.btn { - padding: 0.5rem 1rem; - border: none; - border-radius: 4px; - font-size: 0.9rem; - cursor: pointer; - transition: background-color 0.2s; - font-weight: 500; - text-decoration: none; - text-align: center; - display: inline-block; -} - -.btn:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.btn-confirm { - background-color: #27ae60; - color: white; -} - -.btn-confirm:hover:not(:disabled) { - background-color: #229954; -} - -.btn-cancel { - background-color: #e74c3c; - color: white; -} - -.btn-cancel:hover:not(:disabled) { - background-color: #c0392b; -} - -.no-reservations { - text-align: center; - color: #7f8c8d; - padding: 2rem; - font-style: italic; -} - -.loading, -.error { - text-align: center; - padding: 2rem; - font-size: 1.1rem; -} - -.error { - color: #e74c3c; - background-color: #f8d7da; - border: 1px solid #f5c6cb; - border-radius: 4px; - margin-bottom: 1rem; -} - -@media (max-width: 768px) { - .stats-grid { - grid-template-columns: 1fr; - gap: 1rem; - } - - .reservation-header { - flex-direction: column; - align-items: flex-start; - gap: 0.5rem; - } - - .reservation-actions { - flex-direction: column; - } - - .detail-row { - flex-direction: column; - gap: 0.25rem; - } - - .detail-row strong { - min-width: auto; - } -} \ No newline at end of file diff --git a/src/pages/AdminDashboard.tsx b/src/pages/AdminDashboard.tsx index de709e9..45885f6 100644 --- a/src/pages/AdminDashboard.tsx +++ b/src/pages/AdminDashboard.tsx @@ -1,224 +1,90 @@ import React, { useState, useEffect } from 'react'; -import { format } from 'date-fns'; -import { api } from '../services/api'; +import { useNavigate, Link } from 'react-router-dom'; import { Reservation } from '../types'; -import './AdminDashboard.css'; +import { SimpleLibreBookingClient } from '../services/librebooking-api'; const AdminDashboard: React.FC = () => { const [reservations, setReservations] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [processing, setProcessing] = useState(null); useEffect(() => { + const apiClient = new SimpleLibreBookingClient(); + + const loadReservations = async () => { + try { + const reservationsData = await apiClient.getReservations(); + setReservations(reservationsData); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load reservations'); + } finally { + setLoading(false); + } + }; + loadReservations(); }, []); - const loadReservations = async () => { - try { - setLoading(true); - const data = await api.getReservations(); - // Sort by creation date, newest first - const sortedData = data.sort((a, b) => - new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() - ); - setReservations(sortedData); - } catch (err) { - setError('Failed to load reservations'); - } finally { - setLoading(false); - } - }; - - const handleConfirmReservation = async (reservationId: string) => { - try { - setProcessing(reservationId); - await api.updateReservationStatus(reservationId, 'confirmed'); - - // Update local state - setReservations(prev => - prev.map(r => - r.id === reservationId - ? { ...r, status: 'confirmed' as const } - : r - ) - ); - } catch (err) { - setError('Failed to confirm reservation'); - } finally { - setProcessing(null); - } - }; - - const handleCancelReservation = async (reservationId: string) => { - try { - setProcessing(reservationId); - await api.updateReservationStatus(reservationId, 'cancelled'); - - // Update local state - setReservations(prev => - prev.map(r => - r.id === reservationId - ? { ...r, status: 'cancelled' as const } - : r - ) - ); - } catch (err) { - setError('Failed to cancel reservation'); - } finally { - setProcessing(null); - } - }; - const pendingReservations = reservations.filter(r => r.status === 'pending'); - const confirmedReservations = reservations.filter(r => r.status === 'confirmed'); - const cancelledReservations = reservations.filter(r => r.status === 'cancelled'); if (loading) { - return
Loading admin dashboard...
; + return
Loading...
; } return ( -
-
-

Admin Dashboard

-

Manage resource reservations

-
- - {error &&
{error}
} - -
-
-

{pendingReservations.length}

-

Pending Reservations

-
-
-

{confirmedReservations.length}

-

Confirmed Reservations

-
-
-

{cancelledReservations.length}

-

Cancelled Reservations

-
-
- -
- {pendingReservations.length > 0 && ( -
-

Pending Reservations

-
- {pendingReservations.map(reservation => ( - handleConfirmReservation(reservation.id)} - onCancel={() => handleCancelReservation(reservation.id)} - processing={processing === reservation.id} - /> - ))} -
-
- )} - -
-

All Reservations

-
- {reservations.length === 0 ? ( -

No reservations found

- ) : ( - reservations.map(reservation => ( - handleConfirmReservation(reservation.id) : undefined} - onCancel={reservation.status === 'pending' ? () => handleCancelReservation(reservation.id) : undefined} - processing={processing === reservation.id} - /> - )) - )} -
-
-
-
- ); -}; - -interface ReservationCardProps { - reservation: Reservation; - onConfirm?: () => void; - onCancel?: () => void; - processing: boolean; -} - -const ReservationCard: React.FC = ({ - reservation, - onConfirm, - onCancel, - processing -}) => { - const statusColors = { - pending: '#f39c12', - confirmed: '#27ae60', - cancelled: '#e74c3c' - }; - - return ( -
-
-

{reservation.title}

- +
+

Admin Dashboard

+ - {reservation.status} - + Manage Resources +
-
-
- Resource: {reservation.resource.name} -
-
- Location: {reservation.resource.location} -
-
- Date: {format(new Date(reservation.startTime), 'MMMM d, yyyy')} -
-
- Time: {format(new Date(reservation.startTime), 'h:mm a')} - {format(new Date(reservation.endTime), 'h:mm a')} -
- {reservation.description && ( -
- Description: {reservation.description} + {/* Quick Stats */} +
+
+

Total Reservations

+
+ {reservations.length} +
+
+ +
+

Pending Approvals

+
+ {pendingReservations.length}
- )} -
- Created: {format(new Date(reservation.createdAt), 'MMM d, yyyy h:mm a')}
- {(onConfirm || onCancel) && ( -
- {onConfirm && ( - - )} - {onCancel && ( - - )} + {/* Pending Approvals */} + {pendingReservations.length > 0 && ( +
+

Pending Approval ({pendingReservations.length})

+
+ You have {pendingReservations.length} pending reservations that need approval +
)} + + {/* Recent Activity */} +
+

Recent Activity

+
+ No recent activity +
+
); }; diff --git a/src/pages/CreateReservation.css b/src/pages/CreateReservation.css deleted file mode 100644 index 676b63a..0000000 --- a/src/pages/CreateReservation.css +++ /dev/null @@ -1,142 +0,0 @@ -.create-reservation { - max-width: 600px; - margin: 0 auto; -} - -.page-header { - margin-bottom: 2rem; -} - -.page-header h1 { - margin: 0; - color: #2c3e50; -} - -.reservation-form { - background: white; - padding: 2rem; - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0,0,0,0.1); -} - -.form-group { - margin-bottom: 1.5rem; -} - -.form-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - color: #2c3e50; -} - -.form-group input, -.form-group select, -.form-group textarea { - width: 100%; - padding: 0.75rem; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 1rem; - transition: border-color 0.2s; -} - -.form-group input:focus, -.form-group select:focus, -.form-group textarea:focus { - outline: none; - border-color: #3498db; -} - -.form-group textarea { - resize: vertical; - min-height: 100px; -} - -.form-actions { - display: flex; - gap: 1rem; - justify-content: flex-end; - margin-top: 2rem; - padding-top: 1.5rem; - border-top: 1px solid #eee; -} - -.btn { - padding: 0.75rem 1.5rem; - border: none; - border-radius: 4px; - text-decoration: none; - font-size: 1rem; - cursor: pointer; - transition: background-color 0.2s; - font-weight: 500; -} - -.btn:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.btn-primary { - background-color: #3498db; - color: white; -} - -.btn-primary:hover:not(:disabled) { - background-color: #2980b9; -} - -.btn-secondary { - background-color: #95a5a6; - color: white; -} - -.btn-secondary:hover:not(:disabled) { - background-color: #7f8c8d; -} - -.error { - background-color: #f8d7da; - color: #721c24; - padding: 1rem; - border-radius: 4px; - margin-bottom: 1.5rem; - border: 1px solid #f5c6cb; -} - -.loading, -.no-resources { - text-align: center; - padding: 2rem; - color: #7f8c8d; -} - -.no-resources h2 { - color: #2c3e50; - margin-bottom: 1rem; -} - -@media (max-width: 768px) { - .create-reservation { - max-width: none; - } - - .form-row { - grid-template-columns: 1fr; - } - - .form-actions { - flex-direction: column; - } - - .reservation-form { - padding: 1.5rem; - } -} \ No newline at end of file diff --git a/src/pages/CreateReservation.tsx b/src/pages/CreateReservation.tsx index 81e7945..6de9cc0 100644 --- a/src/pages/CreateReservation.tsx +++ b/src/pages/CreateReservation.tsx @@ -1,182 +1,333 @@ import React, { useState, useEffect } from 'react'; -import { useSearchParams, useNavigate } from 'react-router-dom'; -import { api } from '../services/api'; +import { useNavigate, Link } from 'react-router-dom'; import { Resource, CreateReservationRequest } from '../types'; -import './CreateReservation.css'; +import { SimpleLibreBookingClient } from '../services/librebooking-api'; const CreateReservation: React.FC = () => { - const [searchParams] = useSearchParams(); const navigate = useNavigate(); - const [resources, setResources] = useState([]); - const [loading, setLoading] = useState(true); - const [submitting, setSubmitting] = useState(false); - const [error, setError] = useState(null); - - const preselectedResourceId = searchParams.get('resourceId'); - - const [formData, setFormData] = useState({ - resourceId: preselectedResourceId || '', - startTime: new Date(), - endTime: new Date(), + const [formData, setFormData] = useState({ + resourceId: '', title: '', description: '', + date: '', + startTime: '', + endTime: '' }); + const [resources, setResources] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [submitting, setSubmitting] = useState(false); + + const apiClient = new SimpleLibreBookingClient(); + useEffect(() => { + const loadResources = async () => { + try { + const resourcesData = await apiClient.getResources(); + setResources(resourcesData); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load resources'); + } finally { + setLoading(false); + } + }; + loadResources(); }, []); - const loadResources = async () => { - try { - setLoading(true); - const data = await api.getResources(); - const availableResources = data.filter(r => r.isAvailable); - setResources(availableResources); - - // If no preselected resource but we have available ones, select the first - if (!preselectedResourceId && availableResources.length > 0) { - setFormData(prev => ({ ...prev, resourceId: availableResources[0].id })); - } - } catch (err) { - setError('Failed to load resources'); - } finally { - setLoading(false); - } + const handleInputChange = (event: React.ChangeEvent) => { + const { name, value } = event.target; + setFormData(prev => ({ ...prev, [name]: value })); }; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + const validateForm = (): string | null => { + if (!formData.resourceId) return 'Please select a resource'; + if (!formData.title.trim()) return 'Please enter a title'; + if (!formData.date) return 'Please select a date'; + if (!formData.startTime) return 'Please select a start time'; + if (!formData.endTime) return 'Please select an end time'; + if (formData.startTime >= formData.endTime) return 'End time must be after start time'; + return null; + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); - if (!formData.resourceId || !formData.title) { - setError('Please fill in all required fields'); - return; - } - - if (formData.startTime >= formData.endTime) { - setError('End time must be after start time'); + const validationError = validateForm(); + if (validationError) { + setError(validationError); return; } + setSubmitting(true); + setError(null); + try { - setSubmitting(true); - setError(null); + const requestData: CreateReservationRequest = { + resourceId: formData.resourceId, + startDateTime: `${formData.date}T${formData.startTime}`, + endDateTime: `${formData.date}T${formData.endTime}`, + title: formData.title.trim(), + description: formData.description.trim() || '', + userId: apiClient.getUserId() || undefined, + allowParticipation: true, + termsAccepted: true + }; + + console.log('Creating reservation with data:', requestData); + const reservation = await apiClient.createReservation(requestData); - await api.createReservation(formData); - - // Navigate to resources list on success - navigate('/resources', { - state: { message: 'Reservation created successfully!' } + navigate(`/resources/${formData.resourceId}/calendar`, { + state: { newReservation: reservation } }); } catch (err) { - setError('Failed to create reservation'); + setError(err instanceof Error ? err.message : 'Failed to create reservation'); } finally { setSubmitting(false); } }; - const handleInputChange = (field: keyof CreateReservationRequest, value: any) => { - setFormData(prev => ({ ...prev, [field]: value })); - }; + const selectedResource = resources.find(r => r.id === formData.resourceId); if (loading) { - return
Loading resources...
; - } - - if (resources.length === 0) { - return ( -
-

No Available Resources

-

There are currently no available resources to book.

-
- ); + return
Loading...
; } return ( -
-
-

Create New Reservation

+
+
+

Create Reservation

+ + Cancel +
- {error &&
{error}
} + {error && ( +
+ {error} +
+ )} -
-
- + +
+

Create New Reservation

+
+ + {/* Resource Selection */} +
+
-
-
- - handleInputChange('startTime', new Date(e.target.value))} - required - /> + {/* Resource Details */} + {selectedResource && ( +
+

+ {selectedResource.name} +

+ {selectedResource.description && ( +

+ {selectedResource.description} +

+ )} + {selectedResource.location && ( +
+ Location: {selectedResource.location} +
+ )} + {selectedResource.capacity && ( +
+ Capacity: {selectedResource.capacity} people +
+ )} + {selectedResource.amenities && selectedResource.amenities.length > 0 && ( +
+ Amenities: {selectedResource.amenities.join(', ')} +
+ )}
+ )} -
- - handleInputChange('endTime', new Date(e.target.value))} - required - /> -
-
- -
- + {/* Title */} +
+ handleInputChange('title', e.target.value)} - placeholder="Meeting title or purpose" + onChange={handleInputChange} required + placeholder="e.g., Team Meeting, Client Presentation" + style={{ + width: '100%', + padding: '0.75rem', + border: '1px solid #ddd', + borderRadius: '4px', + fontSize: '1rem' + }} />
-
- + {/* Description */} +
+