Compare commits

...

4 Commits

Author SHA1 Message Date
Ivan
a713369e03 fix: Settings courts list not loading and status display
- Handle Courts API returning array directly (not wrapped in data property)
- Map pricePerHour to hourlyRate for frontend compatibility
- Handle uppercase DB status values (AVAILABLE, MAINTENANCE, CLOSED)
- Send pricePerHour field when creating/updating courts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 03:52:08 +00:00
Ivan
7d0d6d32f1 fix: Live Courts data structure and status naming
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:26:21 +00:00
Ivan
da8a730867 fix: handle isOpenPlay and map form values to Prisma enums in court API
- Add isOpenPlay field to POST and PUT routes
- Accept both hourlyRate and pricePerHour (form vs API naming)
- Map lowercase type/status from form to uppercase Prisma enums

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:21:36 +00:00
Ivan
296491d0b9 feat: add production start script with tunnel URL config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 23:07:14 +00:00
6 changed files with 94 additions and 22 deletions

View File

@@ -135,7 +135,7 @@ export default function LiveCourtsPage() {
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) throw new Error("Failed to load court data"); if (!response.ok) throw new Error("Failed to load court data");
const data = await response.json(); const data = await response.json();
setCourts(data.courts ?? data.data ?? []); setCourts(Array.isArray(data) ? data : data.courts ?? data.data ?? []);
setLastUpdated(new Date()); setLastUpdated(new Date());
} catch (err) { } catch (err) {
console.error("Live courts fetch error:", err); console.error("Live courts fetch error:", err);

View File

@@ -103,7 +103,12 @@ export default function SettingsPage() {
const res = await fetch("/api/courts"); const res = await fetch("/api/courts");
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
setCourts(data.data || []); // Courts API returns array directly, map pricePerHour to hourlyRate for frontend
const courtsArray = Array.isArray(data) ? data : data.data || [];
setCourts(courtsArray.map((c: Record<string, unknown>) => ({
...c,
hourlyRate: c.pricePerHour ?? c.hourlyRate,
})));
} }
} catch (error) { } catch (error) {
console.error("Error fetching courts:", error); console.error("Error fetching courts:", error);
@@ -494,14 +499,14 @@ export default function SettingsPage() {
<td className="px-4 py-3"> <td className="px-4 py-3">
<span <span
className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${ className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
court.status === "active" ["active", "AVAILABLE"].includes(court.status)
? "bg-accent/10 text-accent-700" ? "bg-accent/10 text-accent-700"
: court.status === "maintenance" : ["maintenance", "MAINTENANCE"].includes(court.status)
? "bg-amber-100 text-amber-700" ? "bg-amber-100 text-amber-700"
: "bg-gray-100 text-gray-600" : "bg-gray-100 text-gray-600"
}`} }`}
> >
{court.status === "active" ? "Active" : court.status === "maintenance" ? "Maintenance" : "Inactive"} {["active", "AVAILABLE"].includes(court.status) ? "Active" : ["maintenance", "MAINTENANCE"].includes(court.status) ? "Maintenance" : "Inactive"}
</span> </span>
</td> </td>
<td className="px-4 py-3 text-right"> <td className="px-4 py-3 text-right">
@@ -726,10 +731,11 @@ function CourtFormModal({
siteId, siteId,
type, type,
hourlyRate: parseFloat(hourlyRate), hourlyRate: parseFloat(hourlyRate),
pricePerHour: parseFloat(hourlyRate),
peakHourlyRate: peakHourlyRate ? parseFloat(peakHourlyRate) : null, peakHourlyRate: peakHourlyRate ? parseFloat(peakHourlyRate) : null,
status, status,
isOpenPlay, isOpenPlay,
}); } as Partial<Court> & { pricePerHour?: number });
}; };
return ( return (

View File

@@ -125,23 +125,34 @@ export async function PUT(
type, type,
status, status,
pricePerHour, pricePerHour,
hourlyRate,
description, description,
features, features,
displayOrder, displayOrder,
isActive, isActive,
isOpenPlay,
} = body; } = body;
const price = pricePerHour ?? hourlyRate;
// Map lowercase form values to Prisma enum values
const typeMap: Record<string, string> = { indoor: 'INDOOR', outdoor: 'OUTDOOR', covered: 'COVERED' };
const statusMap: Record<string, string> = { active: 'AVAILABLE', maintenance: 'MAINTENANCE', inactive: 'CLOSED' };
const mappedType = type ? (typeMap[type.toLowerCase()] || type) : undefined;
const mappedStatus = status ? (statusMap[status.toLowerCase()] || status) : undefined;
const court = await db.court.update({ const court = await db.court.update({
where: { id }, where: { id },
data: { data: {
...(name !== undefined && { name }), ...(name !== undefined && { name }),
...(type !== undefined && { type }), ...(mappedType !== undefined && { type: mappedType }),
...(status !== undefined && { status }), ...(mappedStatus !== undefined && { status: mappedStatus }),
...(pricePerHour !== undefined && { pricePerHour }), ...(price !== undefined && { pricePerHour: price }),
...(description !== undefined && { description }), ...(description !== undefined && { description }),
...(features !== undefined && { features }), ...(features !== undefined && { features }),
...(displayOrder !== undefined && { displayOrder }), ...(displayOrder !== undefined && { displayOrder }),
...(isActive !== undefined && { isActive }), ...(isActive !== undefined && { isActive }),
...(isOpenPlay !== undefined && { isOpenPlay }),
}, },
include: { include: {
site: { site: {

View File

@@ -97,14 +97,24 @@ export async function POST(request: NextRequest) {
type, type,
status, status,
pricePerHour, pricePerHour,
hourlyRate,
description, description,
features, features,
displayOrder, displayOrder,
isActive, isActive,
isOpenPlay,
} = body; } = body;
const price = pricePerHour ?? hourlyRate;
// Map lowercase form values to Prisma enum values
const typeMap: Record<string, string> = { indoor: 'INDOOR', outdoor: 'OUTDOOR', covered: 'COVERED' };
const statusMap: Record<string, string> = { active: 'AVAILABLE', maintenance: 'MAINTENANCE', inactive: 'CLOSED' };
const mappedType = typeMap[type?.toLowerCase()] || type || 'INDOOR';
const mappedStatus = statusMap[status?.toLowerCase()] || status || 'AVAILABLE';
// Validate required fields // Validate required fields
if (!siteId || !name || pricePerHour === undefined) { if (!siteId || !name || price === undefined) {
return NextResponse.json( return NextResponse.json(
{ error: 'Missing required fields: siteId, name, pricePerHour' }, { error: 'Missing required fields: siteId, name, pricePerHour' },
{ status: 400 } { status: 400 }
@@ -138,12 +148,13 @@ export async function POST(request: NextRequest) {
data: { data: {
siteId, siteId,
name, name,
type: type || 'INDOOR', type: mappedType,
status: status || 'AVAILABLE', status: mappedStatus,
pricePerHour, pricePerHour: price,
description: description || null, description: description || null,
features: features || [], features: features || [],
displayOrder: displayOrder ?? 0, displayOrder: displayOrder ?? 0,
isOpenPlay: isOpenPlay ?? false,
isActive: isActive ?? true, isActive: isActive ?? true,
}, },
include: { include: {

View File

@@ -63,26 +63,46 @@ export async function GET(request: NextRequest) {
orderBy: { displayOrder: 'asc' }, orderBy: { displayOrder: 'asc' },
}); });
// Compute status for each court // Compute status for each court and transform to frontend shape
const courtsWithStatus = courts.map((court) => { const courtsWithStatus = courts.map((court) => {
let status: 'available' | 'active' | 'open-play' | 'booked'; let status: 'available' | 'active' | 'open_play' | 'booked';
if (court.sessions.length > 0) { if (court.sessions.length > 0) {
// Court has active sessions status = court.isOpenPlay ? 'open_play' : 'active';
if (court.isOpenPlay) { } else if (court.isOpenPlay) {
status = 'open-play'; status = 'open_play';
} else {
status = 'active';
}
} else if (court.bookings.length > 0) { } else if (court.bookings.length > 0) {
status = 'booked'; status = 'booked';
} else { } else {
status = 'available'; status = 'available';
} }
// Transform sessions to players array for frontend
const players = court.sessions.map((session) => ({
id: session.client?.id || session.id,
firstName: session.client?.firstName,
lastName: session.client?.lastName,
walkInName: session.walkInName,
checkedInAt: session.startTime.toISOString(),
sessionId: session.id,
}));
// Get upcoming booking info
const upcomingBooking = court.bookings.length > 0 ? {
startTime: court.bookings[0].startTime.toISOString(),
clientName: court.bookings[0].client
? `${court.bookings[0].client.firstName} ${court.bookings[0].client.lastName}`
: 'Walk-in',
} : undefined;
return { return {
...court, id: court.id,
name: court.name,
type: court.type,
isOpenPlay: court.isOpenPlay,
status, status,
players,
upcomingBooking,
}; };
}); });

24
scripts/start.sh Executable file
View File

@@ -0,0 +1,24 @@
#!/bin/bash
# SmashPoint - Cabo Pickleball Club
# Production server start script
set -e
# Load environment variables
export NEXTAUTH_URL="https://smashpoint.consultoria-as.com"
export NEXT_PUBLIC_APP_URL="https://smashpoint.consultoria-as.com"
export NEXTAUTH_SECRET="xApk6WiZYJZwUpKk6ZlyHoseXqsCSnTmRDqzDdmtRVY="
APP_DIR="/root/Padel/apps/web"
PORT=3000
# Kill any existing server on the port
fuser -k $PORT/tcp 2>/dev/null || true
sleep 2
cd "$APP_DIR"
echo "Starting SmashPoint on port $PORT..."
echo "URL: $NEXTAUTH_URL"
npx next start --port $PORT