Compare commits
4 Commits
a882c8698d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a713369e03 | ||
|
|
7d0d6d32f1 | ||
|
|
da8a730867 | ||
|
|
296491d0b9 |
@@ -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);
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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
24
scripts/start.sh
Executable 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
|
||||||
Reference in New Issue
Block a user