Compare commits

...

10 Commits

Author SHA1 Message Date
bd69dbceaf Merge pull request 'update Dockerfile' (#5) from dockerfile-update into dev
Some checks failed
Docker Build / docker (push) Failing after 2m0s
Reviewed-on: #5
2025-09-16 15:17:46 +02:00
5a890ac253 2025-09-16 17:12:14+04:00
Some checks failed
Docker Build / docker (push) Failing after 1m16s
Docker Build / docker (pull_request) Failing after 1m22s
2025-09-16 17:12:14 +04:00
b94e205787 Merge pull request 'feature/add-docker-build' (#4) from feature/add-docker-build into dev
Some checks failed
Docker Build / docker (push) Failing after 40s
Reviewed-on: #4
2025-09-16 14:14:37 +02:00
7ff599aae0 2025-09-16 16:10:10+04:00 2025-09-16 14:14:37 +02:00
9d8ef03fea 2025-09-16 15:51:23+04:00 2025-09-16 14:14:37 +02:00
a4a95e8649 2025-09-16 13:59:28+04:00 2025-09-16 14:14:37 +02:00
c7f6ad43c4 2025-09-16 10:35:19+04:00 2025-09-16 14:14:37 +02:00
a4ad1d8645 add dockerfile 2025-09-16 14:14:37 +02:00
Asya Vee
ff6de00988 fix the capacity and NaN participants issue 2025-08-27 02:50:13 +04:00
Asya Vee
459289df2d booking capacity check for time slots 2025-08-27 02:08:23 +04:00
22 changed files with 627 additions and 45 deletions

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
node_modules
.git
.next
dist
*.log
.env.local

View File

@@ -0,0 +1,64 @@
# docker-build.yaml
name: Docker Build
on:
push:
branches:
- "**"
tags:
- "**"
pull_request:
workflow_dispatch:
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract tag name
if: startsWith(github.ref, 'refs/tags/')
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Log in to registry
if: secrets.REGISTRY != '' && secrets.REGISTRY_USER != '' && secrets.REGISTRY_PASSWORD != ''
uses: docker/login-action@v3
with:
registry: ${{ secrets.REGISTRY }}
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Check if SHA image exists
if: env.GIT_TAG != '' && secrets.REGISTRY != ''
id: check_image
run: |
if docker manifest inspect ${{ secrets.REGISTRY }}5mdt/vitrify-me:${{ github.sha }} > /dev/null 2>&1; then
echo "image_exists=true" >> $GITHUB_OUTPUT
else
echo "image_exists=false" >> $GITHUB_OUTPUT
fi
- name: Pull existing image
if: env.GIT_TAG != '' && steps.check_image.outputs.image_exists == 'true'
run: docker pull ${{ secrets.REGISTRY }}5mdt/vitrify-me:${{ github.sha }}
- name: Build Docker image
if: env.GIT_TAG == '' || steps.check_image.outputs.image_exists == 'false'
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ secrets.REGISTRY != '' }}
tags: ${{ secrets.REGISTRY }}5mdt/vitrify-me:${{ github.sha }}
- name: Tag and push with git tag
if: env.GIT_TAG != '' && secrets.REGISTRY != ''
run: |
docker tag ${{ secrets.REGISTRY }}5mdt/vitrify-me:${{ github.sha }} ${{ secrets.REGISTRY }}5mdt/vitrify-me:${{ env.GIT_TAG }}
docker push ${{ secrets.REGISTRY }}5mdt/vitrify-me:${{ env.GIT_TAG }}

16
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,16 @@
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-merge-conflict
- id: detect-private-key
- id: check-added-large-files
- id: check-ast
- id: check-json
- id: check-toml
- id: check-xml

70
Dockerfile Normal file
View File

@@ -0,0 +1,70 @@
# Stage 1: Dependencies
FROM node:20-alpine AS deps
WORKDIR /app
RUN npm install -g pnpm@9.0.0
# Copy all package.json files for proper dependency resolution
COPY package.json pnpm-lock.yaml turbo.json pnpm-workspace.yaml ./
COPY apps/web/package.json ./apps/web/
COPY apps/pocketbase/package.json ./apps/pocketbase/
COPY packages/*/package.json ./packages/
# Create the directory structure that might be missing
RUN mkdir -p packages/ui packages/eslint-config packages/typescript-config
# Copy package.json files to their correct locations
COPY packages/ui/package.json ./packages/ui/
COPY packages/eslint-config/package.json ./packages/eslint-config/
COPY packages/typescript-config/package.json ./packages/typescript-config/
RUN pnpm install --frozen-lockfile
# Stage 2: Builder
FROM node:20-alpine AS builder
WORKDIR /app
RUN npm install -g pnpm@9.0.0
# Copy dependencies and source
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/pnpm-lock.yaml ./
COPY . .
# Build only the web app
RUN pnpm turbo build --filter=web
# Stage 3: Runtime
FROM node:20-alpine AS runtime
WORKDIR /app
RUN npm install -g pnpm@9.0.0
# Copy package files for production install
COPY package.json pnpm-lock.yaml turbo.json ./
COPY apps/web/package.json ./apps/web/
COPY packages/*/package.json ./packages/
# Install only production dependencies
RUN pnpm install --prod --frozen-lockfile
# Copy built Next.js application
COPY --from=builder /app/apps/web/.next ./apps/web/.next
COPY --from=builder /app/apps/web/public ./apps/web/public
COPY --from=builder /app/apps/web/next.config.js ./apps/web/
COPY --from=builder /app/apps/web/package.json ./apps/web/
# Copy other necessary files
COPY --from=builder /app/apps/web/lib ./apps/web/lib
COPY --from=builder /app/apps/web/messages ./apps/web/messages
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
# Change ownership of the app directory
RUN chown -R nextjs:nodejs /app
USER nextjs
EXPOSE 3000
# Use Next.js start command
CMD ["pnpm", "--filter=web", "start"]

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,38 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_586073990")
// update field
collection.fields.addAt(7, new Field({
"hidden": false,
"id": "number3301820327",
"max": null,
"min": 1,
"name": "max_booking_capacity",
"onlyInt": false,
"presentable": false,
"required": true,
"system": false,
"type": "number"
}))
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_586073990")
// update field
collection.fields.addAt(7, new Field({
"hidden": false,
"id": "number3301820327",
"max": null,
"min": 1,
"name": "max_capacity",
"onlyInt": false,
"presentable": false,
"required": true,
"system": false,
"type": "number"
}))
return app.save(collection)
})

View File

@@ -0,0 +1,66 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_43114331")
// update field
collection.fields.addAt(9, new Field({
"hidden": false,
"id": "number1421793101",
"max": null,
"min": null,
"name": "min_participants_capacity",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
}))
// update field
collection.fields.addAt(10, new Field({
"hidden": false,
"id": "number3301820327",
"max": null,
"min": null,
"name": "max_participants_capacity",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
}))
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_43114331")
// update field
collection.fields.addAt(9, new Field({
"hidden": false,
"id": "number1421793101",
"max": null,
"min": null,
"name": "min_capacity",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
}))
// update field
collection.fields.addAt(10, new Field({
"hidden": false,
"id": "number3301820327",
"max": null,
"min": null,
"name": "max_capacity",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
}))
return app.save(collection)
})

View File

@@ -0,0 +1,27 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_586073990")
// remove field
collection.fields.removeById("number3301820327")
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_586073990")
// add field
collection.fields.addAt(7, new Field({
"hidden": false,
"id": "number3301820327",
"max": null,
"min": 1,
"name": "max_booking_capacity",
"onlyInt": false,
"presentable": false,
"required": true,
"system": false,
"type": "number"
}))
return app.save(collection)
})

View File

@@ -0,0 +1,27 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_43114331")
// add field
collection.fields.addAt(12, new Field({
"hidden": false,
"id": "number2396794873",
"max": null,
"min": null,
"name": "booking_capacity",
"onlyInt": false,
"presentable": false,
"required": true,
"system": false,
"type": "number"
}))
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_43114331")
// remove field
collection.fields.removeById("number2396794873")
return app.save(collection)
})

View File

@@ -0,0 +1,22 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_986407980")
// update collection data
unmarshal({
"listRule": "",
"viewRule": ""
}, collection)
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_986407980")
// update collection data
unmarshal({
"listRule": null,
"viewRule": null
}, collection)
return app.save(collection)
})

View File

@@ -0,0 +1,29 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_43114331")
// add field
collection.fields.addAt(13, new Field({
"hidden": false,
"id": "select2363381545",
"maxSelect": 1,
"name": "type",
"presentable": false,
"required": false,
"system": false,
"type": "select",
"values": [
"workshop",
"rental"
]
}))
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_43114331")
// remove field
collection.fields.removeById("select2363381545")
return app.save(collection)
})

View File

@@ -0,0 +1,70 @@
import { NextRequest, NextResponse } from 'next/server';
import { bookingApi } from '@/lib/pocketbase';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id: bookingTypeId } = await params;
// Get booking type details to get the actual capacity
const bookingTypes = await bookingApi.getBookingTypes();
const bookingType = bookingTypes.find(bt => bt.id === bookingTypeId);
if (!bookingType) {
return NextResponse.json({ error: 'Booking type not found' }, { status: 404 });
}
const bookingCapacity = bookingType.booking_capacity;
// Get time slots for this booking type
const timeSlots = await bookingApi.getTimeSlotsForBookingType(bookingTypeId);
// Generate all available dates based on recurrence patterns
const availableSlotsByDate = await bookingApi.generateAvailableTimeSlots(timeSlots);
// Get all dates and check capacity for each
const availableDates: string[] = [];
for (const date of Object.keys(availableSlotsByDate)) {
// Get bookings for this date
const bookings = await bookingApi.getBookingsForDate(date, [bookingTypeId]);
// Check if any time slots have capacity available
const slotsForDate = availableSlotsByDate[date];
const hasAvailableSlots = slotsForDate.some(slot => {
const slotStart = new Date(slot.start_time);
const slotEnd = new Date(slot.end_time);
const overlappingBookings = bookings.filter(booking => {
const bookingStart = new Date(booking.start_time);
const bookingEnd = new Date(booking.end_time);
return bookingStart < slotEnd && bookingEnd > slotStart;
});
const totalParticipants = overlappingBookings.reduce((sum, booking) =>
sum + (booking.participants_count || 0), 0
);
return totalParticipants < bookingCapacity;
});
if (hasAvailableSlots) {
availableDates.push(date);
}
}
return NextResponse.json({
bookingTypeId,
availableDates: availableDates.sort()
});
} catch (error) {
console.error('Error fetching available dates:', error);
return NextResponse.json(
{ error: 'Failed to fetch available dates' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,102 @@
import { NextRequest, NextResponse } from 'next/server';
import { bookingApi } from '@/lib/pocketbase';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id: bookingTypeId } = await params;
const { searchParams } = new URL(request.url);
const date = searchParams.get('date');
if (!date) {
return NextResponse.json({ error: 'Date parameter required' }, { status: 400 });
}
// Get booking type details to get the actual capacity
const bookingTypes = await bookingApi.getBookingTypes();
const bookingType = bookingTypes.find(bt => bt.id === bookingTypeId);
if (!bookingType) {
return NextResponse.json({ error: 'Booking type not found' }, { status: 404 });
}
const bookingCapacity = bookingType.booking_capacity;
// Get time slots for this booking type
const timeSlots = await bookingApi.getTimeSlotsForBookingType(bookingTypeId);
// Generate available slots for all dates
const availableSlotsByDate = await bookingApi.generateAvailableTimeSlots(timeSlots);
const slotsForDate = availableSlotsByDate[date] || [];
if (slotsForDate.length === 0) {
return NextResponse.json({
date,
bookingTypeId,
timeSlots: []
});
}
// Get existing bookings for this date
const bookings = await bookingApi.getBookingsForDate(date, [bookingTypeId]);
console.log(`\n=== Bookings for ${date} and booking type ${bookingTypeId} ===`);
console.log('Number of bookings found:', bookings.length);
bookings.forEach((booking, i) => {
console.log(`Booking ${i+1}: ${booking.start_time} - ${booking.end_time}, participants: ${booking.participants_count}`);
});
// Calculate capacity for each slot
const slotsWithCapacity = slotsForDate.map((slot, index) => {
const slotStart = new Date(slot.start_time);
const slotEnd = new Date(slot.end_time);
console.log(`\n=== Checking slot ${slot.start_time} - ${slot.end_time} ===`);
console.log('Available bookings for date:', bookings.length);
const overlappingBookings = bookings.filter(booking => {
const bookingStart = new Date(booking.start_time);
const bookingEnd = new Date(booking.end_time);
const overlaps = bookingStart < slotEnd && bookingEnd > slotStart;
console.log(`Booking ${booking.start_time} - ${booking.end_time}: overlaps = ${overlaps}`);
return overlaps;
});
const totalParticipants = overlappingBookings.reduce((sum, booking) =>
sum + (booking.participants_count || 0), 0
);
const availableCapacity = Math.max(0, bookingCapacity - totalParticipants);
console.log(`Total participants: ${totalParticipants}, Capacity: ${bookingCapacity}, Available: ${availableCapacity}`);
return {
id: `slot-${date}-${index}`,
start_time: slot.start_time,
end_time: slot.end_time,
availableCapacity,
totalBookings: totalParticipants,
maxCapacity: bookingCapacity,
is_active: availableCapacity > 0
};
});
// Filter out fully booked slots
const availableTimeSlots = slotsWithCapacity.filter(slot => slot.availableCapacity > 0);
return NextResponse.json({
date,
bookingTypeId,
timeSlots: availableTimeSlots
});
} catch (error) {
console.error('Error fetching time slots:', error);
return NextResponse.json(
{ error: 'Failed to fetch time slots' },
{ status: 500 }
);
}
}

View File

@@ -4,7 +4,6 @@ import React, { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { BookingService } from '@/lib/bookingService'; import { BookingService } from '@/lib/bookingService';
import { bookingApi } from '@/lib/pocketbase';
import { BookingType, TimeSlot } from '@/types/bookings'; import { BookingType, TimeSlot } from '@/types/bookings';
import BookingTypeSelector from './BookingTypeSelector'; import BookingTypeSelector from './BookingTypeSelector';
import DateSelector from './DateSelector'; import DateSelector from './DateSelector';
@@ -27,7 +26,6 @@ const BookingInterface = () => {
const [bookingTypes, setBookingTypes] = useState<BookingType[]>([]); const [bookingTypes, setBookingTypes] = useState<BookingType[]>([]);
const [timeSlots, setTimeSlots] = useState<TimeSlot[]>([]); const [timeSlots, setTimeSlots] = useState<TimeSlot[]>([]);
const [availableDates, setAvailableDates] = useState<string[]>([]); const [availableDates, setAvailableDates] = useState<string[]>([]);
const [availableSlotsByDate, setAvailableSlotsByDate] = useState<{ [date: string]: { start_time: string; end_time: string }[] }>({});
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -100,49 +98,61 @@ const BookingInterface = () => {
// Set participants count to booking type's minimum capacity // Set participants count to booking type's minimum capacity
const bookingType = bookingTypes.find(bt => bt.id === typeId); const bookingType = bookingTypes.find(bt => bt.id === typeId);
if (bookingType) { if (bookingType) {
setParticipantsCount(bookingType.min_capacity); setParticipantsCount(bookingType.min_participants_capacity);
} }
// Fetch time slots for the selected booking type // Fetch available dates from server API (with capacity pre-calculated)
try { try {
const timeSlots = await bookingApi.getTimeSlotsForBookingType(typeId); const response = await fetch(`/api/booking-types/${typeId}/available-dates`);
const data = await response.json();
// Generate available time slots grouped by date if (response.ok) {
const generatedSlotsByDate = bookingApi.generateAvailableTimeSlots(timeSlots); setAvailableDates(data.availableDates);
const availableDatesFromAPI = Object.keys(generatedSlotsByDate).sort(); } else {
console.error('Error fetching available dates:', data.error);
// Update available dates and slots data for the date selector setAvailableDates([]);
setAvailableDates(availableDatesFromAPI); }
setAvailableSlotsByDate(generatedSlotsByDate);
} catch (error) { } catch (error) {
console.error('Error fetching time slots:', error); console.error('Error fetching available dates:', error);
setAvailableDates([]);
} }
}; };
const handleDateChange = (date: string) => { const handleDateChange = async (date: string) => {
setSelectedDate(date); setSelectedDate(date);
setSelectedTimeSlot(''); setSelectedTimeSlot('');
// Get time slots for the selected date from availableSlotsByDate // Fetch time slots with capacity from server API
const slotsForDate = availableSlotsByDate[date] || []; try {
console.log(`Time slots for ${date}:`, slotsForDate); const response = await fetch(`/api/booking-types/${selectedBookingType}/time-slots?date=${date}`);
const data = await response.json();
// Convert to TimeSlot format for TimeSlotSelector if (response.ok) {
const formattedTimeSlots: TimeSlot[] = slotsForDate.map((slot, index) => ({ // Convert server response to TimeSlot format
id: `slot-${date}-${index}`, const formattedTimeSlots: TimeSlot[] = data.timeSlots.map((slot: any) => ({
start_time: slot.start_time, id: slot.id,
end_time: slot.end_time, start_time: slot.start_time,
is_active: true, end_time: slot.end_time,
max_capacity: 8, is_active: slot.is_active,
booking_types: [selectedBookingType], booking_capacity: slot.maxCapacity,
is_reccuring: false, booking_types: [selectedBookingType],
recurrence_pattern: undefined, is_reccuring: false,
resources: [], recurrence_pattern: undefined,
created: new Date().toISOString(), resources: [],
updated: new Date().toISOString() created: new Date().toISOString(),
})); updated: new Date().toISOString(),
availableCapacity: slot.availableCapacity // Add capacity info for UI
}));
setTimeSlots(formattedTimeSlots); setTimeSlots(formattedTimeSlots);
} else {
console.error('Error fetching time slots:', data.error);
setTimeSlots([]);
}
} catch (error) {
console.error('Error fetching time slots:', error);
setTimeSlots([]);
}
}; };
return ( return (

View File

@@ -28,8 +28,8 @@ const CustomerDetails: React.FC<CustomerDetailsProps> = ({
const t = useTranslations('bookingForm'); const t = useTranslations('bookingForm');
// Calculate min and max based on booking type // Calculate min and max based on booking type
const minParticipants = bookingType.min_capacity; const minParticipants = bookingType.min_participants_capacity;
const maxParticipants = bookingType.max_capacity; const maxParticipants = bookingType.max_participants_capacity;
return ( return (
<div className="bg-white rounded-xl p-6 shadow-sm"> <div className="bg-white rounded-xl p-6 shadow-sm">
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center gap-2"> <h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center gap-2">

View File

@@ -35,11 +35,14 @@ const DateSelector: React.FC<DateSelectorProps> = ({
date.setDate(today.getDate() + i); date.setDate(today.getDate() + i);
const dateString = date.toISOString().split('T')[0] || ''; const dateString = date.toISOString().split('T')[0] || '';
// Create date object from the ISO string to avoid timezone issues
const displayDate = new Date(dateString + 'T12:00:00.000Z');
days.push({ days.push({
date: dateString, date: dateString,
day: date.getDate(), day: parseInt(dateString.split('-')[2]),
month: date.toLocaleDateString('en', { month: 'short' }), month: displayDate.toLocaleDateString('en', { month: 'short', timeZone: 'UTC' }),
dayName: date.toLocaleDateString('en', { weekday: 'short' }), dayName: displayDate.toLocaleDateString('en', { weekday: 'short', timeZone: 'UTC' }),
available: availableDates.includes(dateString) available: availableDates.includes(dateString)
}); });
} }

View File

@@ -4,7 +4,7 @@ import { useTranslations } from 'next-intl';
import { TimeSlot } from '@/types/bookings'; import { TimeSlot } from '@/types/bookings';
interface TimeSlotSelectorProps { interface TimeSlotSelectorProps {
timeSlots: TimeSlot[]; timeSlots: (TimeSlot & { availableCapacity?: number })[];
selectedTimeSlot: string; selectedTimeSlot: string;
onTimeSlotChange: (slotId: string) => void; onTimeSlotChange: (slotId: string) => void;
loading?: boolean; loading?: boolean;
@@ -85,6 +85,11 @@ const TimeSlotSelector: React.FC<TimeSlotSelectorProps> = ({
<div className="font-medium"> <div className="font-medium">
{formattedTime} {formattedTime}
</div> </div>
{slot.availableCapacity !== undefined && slot.availableCapacity > 0 && (
<div className="text-xs mt-1 text-green-600">
{slot.availableCapacity} spots left
</div>
)}
{!isAvailable && ( {!isAvailable && (
<div className="text-xs mt-1">Unavailable</div> <div className="text-xs mt-1">Unavailable</div>
)} )}

View File

@@ -1,5 +1,5 @@
import PocketBase from 'pocketbase'; import PocketBase from 'pocketbase';
import { BookingType, TimeSlot } from '@/types/bookings'; import { BookingType, TimeSlot, Booking } from '@/types/bookings';
// Initialize PocketBase client // Initialize PocketBase client
export const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL || 'http://127.0.0.1:8090'); export const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL || 'http://127.0.0.1:8090');
@@ -122,4 +122,26 @@ export const bookingApi = {
return availableSlotsByDate; return availableSlotsByDate;
}, },
// Get all bookings for a specific date filtered by booking type IDs
async getBookingsForDate(
date: string,
bookingTypeIds: string[]
): Promise<Booking[]> {
try {
// Create filter for booking type IDs
const bookingTypeFilter = bookingTypeIds.map(id => `booking_type = "${id}"`).join(' || ');
const bookings = await pb.collection('bookings').getFullList<Booking>({
filter: `status = "confirmed" && start_time ~ "${date}" && (${bookingTypeFilter})`,
sort: 'start_time'
});
console.log(`Bookings for ${date}:`, bookings);
return bookings;
} catch (error) {
console.error('Error fetching bookings for date:', error);
return [];
}
},
}; };

View File

@@ -9,13 +9,14 @@ export interface BaseRecord {
export interface Resource extends BaseRecord { export interface Resource extends BaseRecord {
name: string; name: string;
type: 'wheel' | 'workstation'; type: 'wheel' | 'workstation';
capacity: number; usage_capacity: number;
is_active: boolean; is_active: boolean;
} }
// Booking Type entity // Booking Type entity
export interface BookingType extends BaseRecord { export interface BookingType extends BaseRecord {
name: string; name: string;
type: 'workshop' | 'rental';
display_name: string; display_name: string;
description?: string; description?: string;
requires_payment?: boolean; requires_payment?: boolean;
@@ -23,9 +24,10 @@ export interface BookingType extends BaseRecord {
min_duration: number; min_duration: number;
price_per_person: number; price_per_person: number;
resources: string; // relation to Resource resources: string; // relation to Resource
min_capacity: number; min_participants_capacity: number;
max_capacity: number; max_participants_capacity: number;
is_active: boolean; is_active: boolean;
booking_capacity: number;
} }
// Time Slot entity // Time Slot entity
@@ -36,7 +38,6 @@ export interface TimeSlot extends BaseRecord {
is_active: boolean; is_active: boolean;
is_reccuring?: boolean; is_reccuring?: boolean;
recurrence_pattern?: RecurrencePattern; recurrence_pattern?: RecurrencePattern;
max_capacity: number;
} }
// Recurrence pattern structure // Recurrence pattern structure

2
pnpm-lock.yaml generated
View File

@@ -1,3 +1,5 @@
# pnpm-lock.yaml
lockfileVersion: '9.0' lockfileVersion: '9.0'
settings: settings:

View File

@@ -1,3 +1,5 @@
# pnpm-workspace.yaml
packages: packages:
- "apps/*" - "apps/*"
- "packages/*" - "packages/*"