feat(api): add REST controllers, services, entities and 15-min timeline grid
This commit is contained in:
20
frontend/src/api/client.ts
Normal file
20
frontend/src/api/client.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import axios from 'axios';
|
||||
import { Reservation } from '../types';
|
||||
|
||||
const BASE = import.meta.env.VITE_API_URL ?? 'http://localhost:8080/api/v1';
|
||||
|
||||
const http = axios.create({ baseURL: BASE });
|
||||
|
||||
export const api = {
|
||||
getTables: () =>
|
||||
http.get('/tables').then((r) => r.data),
|
||||
|
||||
getReservations: (date?: string) =>
|
||||
http.get('/reservations', { params: { date } }).then((r) => r.data),
|
||||
|
||||
createReservation: (payload: Partial<Reservation>) =>
|
||||
http.post('/reservations', payload).then((r) => r.data),
|
||||
|
||||
updateStatus: (id: string, status: string) =>
|
||||
http.patch(`/reservations/${id}/status`, { status }).then((r) => r.data),
|
||||
};
|
||||
@@ -1,7 +1,14 @@
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import { Reservation, ReservationStatus } from '../types';
|
||||
|
||||
const HOURS = Array.from({ length: 14 }, (_, i) => `${String(i + 10).padStart(2, '0')}:00`);
|
||||
// 15-min slots from 10:00 to 24:00
|
||||
const SLOTS: string[] = [];
|
||||
for (let h = 10; h < 24; h++) {
|
||||
for (const m of [0, 15, 30, 45]) {
|
||||
SLOTS.push(`${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`);
|
||||
}
|
||||
}
|
||||
const HOUR_SLOTS = SLOTS.filter((s) => s.endsWith(':00'));
|
||||
|
||||
const STATUS_DOT: Record<ReservationStatus, string> = {
|
||||
PENDING: 'bg-slate-400',
|
||||
@@ -29,7 +36,8 @@ function timeToMinutes(t: string) {
|
||||
const TIMELINE_START = 10 * 60; // 10:00
|
||||
const TIMELINE_END = 24 * 60; // 24:00
|
||||
const TOTAL_MINUTES = TIMELINE_END - TIMELINE_START;
|
||||
const COL_WIDTH = 72; // px per hour
|
||||
const SLOT_WIDTH = 18; // px per 15-min slot
|
||||
const HOUR_WIDTH = SLOT_WIDTH * 4; // 72px per hour
|
||||
|
||||
export default function Timeline() {
|
||||
const { tables, reservations, openBookingModal } = useAppStore();
|
||||
@@ -46,11 +54,12 @@ export default function Timeline() {
|
||||
<div className="w-[120px] shrink-0 p-3 text-xs font-semibold text-slate-500 uppercase tracking-wider border-r border-slate-800">
|
||||
Tisch
|
||||
</div>
|
||||
{HOURS.map((h) => (
|
||||
{/* Hour labels */}
|
||||
{HOUR_SLOTS.map((h) => (
|
||||
<div
|
||||
key={h}
|
||||
className="text-center text-xs text-slate-500 border-r border-slate-800/50 py-3 font-mono"
|
||||
style={{ width: COL_WIDTH }}
|
||||
style={{ width: HOUR_WIDTH }}
|
||||
>
|
||||
{h}
|
||||
</div>
|
||||
@@ -72,15 +81,16 @@ export default function Timeline() {
|
||||
</div>
|
||||
|
||||
{/* Timeline cells */}
|
||||
<div className="relative flex flex-1" style={{ height: 64 }}>
|
||||
{/* Click grid */}
|
||||
{HOURS.map((_, i) => (
|
||||
<div className="relative flex flex-1" style={{ height: 56 }}>
|
||||
{/* 15-min clickable slots */}
|
||||
{SLOTS.map((slot, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => handleSlotClick(table.id, 10 + i)}
|
||||
className="border-r border-slate-800/30 h-full hover:bg-amber-500/10 transition shrink-0 group-hover:border-slate-700/50"
|
||||
style={{ width: COL_WIDTH }}
|
||||
title={`Neue Reservierung: ${table.number} um ${HOURS[i]}`}
|
||||
onClick={() => handleSlotClick(table.id, parseInt(slot.split(':')[0]))}
|
||||
className={`border-r h-full hover:bg-indigo-500/10 transition shrink-0
|
||||
${slot.endsWith(':00') ? 'border-slate-700/60' : 'border-slate-800/30'}`}
|
||||
style={{ width: SLOT_WIDTH }}
|
||||
title={`${table.number} · ${slot}`}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -88,21 +98,19 @@ export default function Timeline() {
|
||||
{tableReservations.map((res: Reservation) => {
|
||||
const startMin = timeToMinutes(res.startTime) - TIMELINE_START;
|
||||
const endMin = timeToMinutes(res.endTime) - TIMELINE_START;
|
||||
const leftPct = (startMin / TOTAL_MINUTES) * 100;
|
||||
const widthPct = ((endMin - startMin) / TOTAL_MINUTES) * 100;
|
||||
const totalWidth = HOURS.length * COL_WIDTH;
|
||||
const totalWidth = SLOTS.length * SLOT_WIDTH;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={res.id}
|
||||
title={`${res.guest.firstName} ${res.guest.lastName} (${res.guestCount}P) ${res.startTime}–${res.endTime}`}
|
||||
className={`absolute top-2 bottom-2 rounded-lg border px-2 flex items-center overflow-hidden text-xs font-semibold cursor-pointer hover:brightness-110 transition z-10 ${STATUS_BAR[res.status]}`}
|
||||
className={`absolute top-1.5 bottom-1.5 rounded-md border px-2 flex items-center overflow-hidden text-xs font-semibold cursor-pointer hover:brightness-110 transition z-10 ${STATUS_BAR[res.status]}`}
|
||||
style={{
|
||||
left: `${(startMin / TOTAL_MINUTES) * totalWidth}px`,
|
||||
width: `${((endMin - startMin) / TOTAL_MINUTES) * totalWidth - 4}px`,
|
||||
width: `${((endMin - startMin) / TOTAL_MINUTES) * totalWidth - 2}px`,
|
||||
}}
|
||||
>
|
||||
<span className={`w-2 h-2 rounded-full shrink-0 mr-1.5 ${STATUS_DOT[res.status]}`} />
|
||||
<span className={`w-1.5 h-1.5 rounded-full shrink-0 mr-1.5 ${STATUS_DOT[res.status]}`} />
|
||||
<span className="truncate">{res.guest.firstName} {res.guest.lastName}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.caspos.controller;
|
||||
|
||||
import com.caspos.dto.CreateReservationRequest;
|
||||
import com.caspos.dto.ReservationDto;
|
||||
import com.caspos.service.ReservationService;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/reservations")
|
||||
@CrossOrigin(origins = {"http://localhost:5173", "http://localhost:3000"})
|
||||
public class ReservationController {
|
||||
|
||||
private final ReservationService reservationService;
|
||||
|
||||
public ReservationController(ReservationService reservationService) {
|
||||
this.reservationService = reservationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/reservations?date=YYYY-MM-DD
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<List<ReservationDto>> getByDate(
|
||||
@RequestParam(required = false)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date
|
||||
) {
|
||||
LocalDate target = date != null ? date : LocalDate.now();
|
||||
return ResponseEntity.ok(reservationService.getByDate(target));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/reservations
|
||||
*/
|
||||
@PostMapping
|
||||
public ResponseEntity<ReservationDto> create(@RequestBody CreateReservationRequest request) {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(reservationService.create(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /api/v1/reservations/{id}/status
|
||||
* Body: { "status": "CONFIRMED" }
|
||||
*/
|
||||
@PatchMapping("/{id}/status")
|
||||
public ResponseEntity<ReservationDto> updateStatus(
|
||||
@PathVariable UUID id,
|
||||
@RequestBody Map<String, String> body
|
||||
) {
|
||||
String status = body.get("status");
|
||||
if (status == null || status.isBlank()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
return ResponseEntity.ok(reservationService.updateStatus(id, status));
|
||||
}
|
||||
}
|
||||
29
src/main/java/com/caspos/controller/TableController.java
Normal file
29
src/main/java/com/caspos/controller/TableController.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.caspos.controller;
|
||||
|
||||
import com.caspos.dto.TableDto;
|
||||
import com.caspos.service.TableService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/tables")
|
||||
@CrossOrigin(origins = {"http://localhost:5173", "http://localhost:3000"})
|
||||
public class TableController {
|
||||
|
||||
private final TableService tableService;
|
||||
|
||||
public TableController(TableService tableService) {
|
||||
this.tableService = tableService;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/tables
|
||||
* Returns all tables ordered by area and table number.
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<List<TableDto>> getAllTables() {
|
||||
return ResponseEntity.ok(tableService.getAllTables());
|
||||
}
|
||||
}
|
||||
31
src/main/java/com/caspos/domain/DiningArea.java
Normal file
31
src/main/java/com/caspos/domain/DiningArea.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.caspos.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "dining_areas")
|
||||
public class DiningArea {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@Column(nullable = false, length = 100)
|
||||
private String name;
|
||||
|
||||
@Column(name = "is_active")
|
||||
private boolean active = true;
|
||||
|
||||
@Column(name = "display_order")
|
||||
private int displayOrder = 0;
|
||||
|
||||
public UUID getId() { return id; }
|
||||
public String getName() { return name; }
|
||||
public boolean isActive() { return active; }
|
||||
public int getDisplayOrder() { return displayOrder; }
|
||||
public void setId(UUID id) { this.id = id; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public void setActive(boolean active) { this.active = active; }
|
||||
public void setDisplayOrder(int displayOrder) { this.displayOrder = displayOrder; }
|
||||
}
|
||||
53
src/main/java/com/caspos/domain/Guest.java
Normal file
53
src/main/java/com/caspos/domain/Guest.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package com.caspos.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "guests")
|
||||
public class Guest {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@Column(name = "first_name", length = 100)
|
||||
private String firstName;
|
||||
|
||||
@Column(name = "last_name", length = 100)
|
||||
private String lastName;
|
||||
|
||||
@Column(length = 50)
|
||||
private String phone;
|
||||
|
||||
@Column(length = 255)
|
||||
private String email;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String notes;
|
||||
|
||||
@Column(name = "no_show_count")
|
||||
private int noShowCount = 0;
|
||||
|
||||
@Column(name = "created_at")
|
||||
private Instant createdAt = Instant.now();
|
||||
|
||||
public UUID getId() { return id; }
|
||||
public String getFirstName() { return firstName; }
|
||||
public String getLastName() { return lastName; }
|
||||
public String getPhone() { return phone; }
|
||||
public String getEmail() { return email; }
|
||||
public String getNotes() { return notes; }
|
||||
public int getNoShowCount() { return noShowCount; }
|
||||
public Instant getCreatedAt() { return createdAt; }
|
||||
|
||||
public void setId(UUID id) { this.id = id; }
|
||||
public void setFirstName(String firstName) { this.firstName = firstName; }
|
||||
public void setLastName(String lastName) { this.lastName = lastName; }
|
||||
public void setPhone(String phone) { this.phone = phone; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
public void setNotes(String notes) { this.notes = notes; }
|
||||
public void setNoShowCount(int noShowCount) { this.noShowCount = noShowCount; }
|
||||
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
72
src/main/java/com/caspos/domain/Reservation.java
Normal file
72
src/main/java/com/caspos/domain/Reservation.java
Normal file
@@ -0,0 +1,72 @@
|
||||
package com.caspos.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "reservations")
|
||||
public class Reservation {
|
||||
|
||||
public enum Status {
|
||||
PENDING, CONFIRMED, ARRIVED, FINISHED, NO_SHOW, CANCELLED
|
||||
}
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "guest_id")
|
||||
private Guest guest;
|
||||
|
||||
@ManyToMany
|
||||
@JoinTable(
|
||||
name = "reservation_table_assignments",
|
||||
joinColumns = @JoinColumn(name = "reservation_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "table_id")
|
||||
)
|
||||
private List<RestaurantTable> tables = new ArrayList<>();
|
||||
|
||||
@Column(name = "start_time", nullable = false)
|
||||
private OffsetDateTime startTime;
|
||||
|
||||
@Column(name = "end_time", nullable = false)
|
||||
private OffsetDateTime endTime;
|
||||
|
||||
@Column(name = "guest_count", nullable = false)
|
||||
private int guestCount;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", nullable = false, length = 30)
|
||||
private Status status = Status.CONFIRMED;
|
||||
|
||||
@Column(name = "special_requests", columnDefinition = "TEXT")
|
||||
private String specialRequests;
|
||||
|
||||
@Column(name = "created_at")
|
||||
private Instant createdAt = Instant.now();
|
||||
|
||||
public UUID getId() { return id; }
|
||||
public Guest getGuest() { return guest; }
|
||||
public List<RestaurantTable> getTables() { return tables; }
|
||||
public OffsetDateTime getStartTime() { return startTime; }
|
||||
public OffsetDateTime getEndTime() { return endTime; }
|
||||
public int getGuestCount() { return guestCount; }
|
||||
public Status getStatus() { return status; }
|
||||
public String getSpecialRequests() { return specialRequests; }
|
||||
public Instant getCreatedAt() { return createdAt; }
|
||||
|
||||
public void setId(UUID id) { this.id = id; }
|
||||
public void setGuest(Guest guest) { this.guest = guest; }
|
||||
public void setTables(List<RestaurantTable> tables) { this.tables = tables; }
|
||||
public void setStartTime(OffsetDateTime startTime) { this.startTime = startTime; }
|
||||
public void setEndTime(OffsetDateTime endTime) { this.endTime = endTime; }
|
||||
public void setGuestCount(int guestCount) { this.guestCount = guestCount; }
|
||||
public void setStatus(Status status) { this.status = status; }
|
||||
public void setSpecialRequests(String specialRequests) { this.specialRequests = specialRequests; }
|
||||
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
68
src/main/java/com/caspos/domain/RestaurantTable.java
Normal file
68
src/main/java/com/caspos/domain/RestaurantTable.java
Normal file
@@ -0,0 +1,68 @@
|
||||
package com.caspos.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "restaurant_tables")
|
||||
public class RestaurantTable {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "dining_area_id", nullable = false)
|
||||
private DiningArea diningArea;
|
||||
|
||||
@Column(name = "table_number", nullable = false, length = 20)
|
||||
private String tableNumber;
|
||||
|
||||
@Column(name = "min_capacity", nullable = false)
|
||||
private int minCapacity = 1;
|
||||
|
||||
@Column(name = "max_capacity", nullable = false)
|
||||
private int maxCapacity;
|
||||
|
||||
@Column(name = "is_combinable")
|
||||
private boolean combinable = true;
|
||||
|
||||
@Column(name = "pos_x")
|
||||
private Double posX;
|
||||
|
||||
@Column(name = "pos_y")
|
||||
private Double posY;
|
||||
|
||||
@Column(name = "width")
|
||||
private Double width;
|
||||
|
||||
@Column(name = "height")
|
||||
private Double height;
|
||||
|
||||
@Column(name = "shape", length = 20)
|
||||
private String shape;
|
||||
|
||||
public UUID getId() { return id; }
|
||||
public DiningArea getDiningArea() { return diningArea; }
|
||||
public String getTableNumber() { return tableNumber; }
|
||||
public int getMinCapacity() { return minCapacity; }
|
||||
public int getMaxCapacity() { return maxCapacity; }
|
||||
public boolean isCombinable() { return combinable; }
|
||||
public Double getPosX() { return posX; }
|
||||
public Double getPosY() { return posY; }
|
||||
public Double getWidth() { return width; }
|
||||
public Double getHeight() { return height; }
|
||||
public String getShape() { return shape; }
|
||||
|
||||
public void setId(UUID id) { this.id = id; }
|
||||
public void setDiningArea(DiningArea diningArea) { this.diningArea = diningArea; }
|
||||
public void setTableNumber(String tableNumber) { this.tableNumber = tableNumber; }
|
||||
public void setMinCapacity(int minCapacity) { this.minCapacity = minCapacity; }
|
||||
public void setMaxCapacity(int maxCapacity) { this.maxCapacity = maxCapacity; }
|
||||
public void setCombinable(boolean combinable) { this.combinable = combinable; }
|
||||
public void setPosX(Double posX) { this.posX = posX; }
|
||||
public void setPosY(Double posY) { this.posY = posY; }
|
||||
public void setWidth(Double width) { this.width = width; }
|
||||
public void setHeight(Double height) { this.height = height; }
|
||||
public void setShape(String shape) { this.shape = shape; }
|
||||
}
|
||||
18
src/main/java/com/caspos/dto/CreateReservationRequest.java
Normal file
18
src/main/java/com/caspos/dto/CreateReservationRequest.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package com.caspos.dto;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public record CreateReservationRequest(
|
||||
String firstName,
|
||||
String lastName,
|
||||
String phone,
|
||||
String email,
|
||||
List<UUID> tableIds,
|
||||
OffsetDateTime startTime,
|
||||
OffsetDateTime endTime,
|
||||
int guestCount,
|
||||
String specialRequests,
|
||||
List<String> tags
|
||||
) {}
|
||||
25
src/main/java/com/caspos/dto/ReservationDto.java
Normal file
25
src/main/java/com/caspos/dto/ReservationDto.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.caspos.dto;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public record ReservationDto(
|
||||
UUID id,
|
||||
GuestDto guest,
|
||||
List<UUID> tableIds,
|
||||
OffsetDateTime startTime,
|
||||
OffsetDateTime endTime,
|
||||
int guestCount,
|
||||
String status,
|
||||
String specialRequests,
|
||||
String createdAt
|
||||
) {
|
||||
public record GuestDto(
|
||||
UUID id,
|
||||
String firstName,
|
||||
String lastName,
|
||||
String phone,
|
||||
String email
|
||||
) {}
|
||||
}
|
||||
17
src/main/java/com/caspos/dto/TableDto.java
Normal file
17
src/main/java/com/caspos/dto/TableDto.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.caspos.dto;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record TableDto(
|
||||
UUID id,
|
||||
String tableNumber,
|
||||
String area,
|
||||
int minCapacity,
|
||||
int maxCapacity,
|
||||
boolean combinable,
|
||||
Double posX,
|
||||
Double posY,
|
||||
Double width,
|
||||
Double height,
|
||||
String shape
|
||||
) {}
|
||||
11
src/main/java/com/caspos/repository/GuestRepository.java
Normal file
11
src/main/java/com/caspos/repository/GuestRepository.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package com.caspos.repository;
|
||||
|
||||
import com.caspos.domain.Guest;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface GuestRepository extends JpaRepository<Guest, UUID> {
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.caspos.repository;
|
||||
|
||||
import com.caspos.domain.Reservation;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface ReservationRepository extends JpaRepository<Reservation, UUID> {
|
||||
|
||||
@Query("""
|
||||
SELECT r FROM Reservation r
|
||||
WHERE CAST(r.startTime AS date) = :date
|
||||
ORDER BY r.startTime ASC
|
||||
""")
|
||||
List<Reservation> findByDate(@Param("date") LocalDate date);
|
||||
|
||||
@Query("""
|
||||
SELECT r FROM Reservation r
|
||||
WHERE r.startTime < :end AND r.endTime > :start
|
||||
AND r.status NOT IN ('CANCELLED', 'NO_SHOW')
|
||||
""")
|
||||
List<Reservation> findOverlapping(@Param("start") OffsetDateTime start, @Param("end") OffsetDateTime end);
|
||||
}
|
||||
14
src/main/java/com/caspos/repository/TableRepository.java
Normal file
14
src/main/java/com/caspos/repository/TableRepository.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package com.caspos.repository;
|
||||
|
||||
import com.caspos.domain.RestaurantTable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface TableRepository extends JpaRepository<RestaurantTable, UUID> {
|
||||
List<RestaurantTable> findByDiningAreaId(UUID diningAreaId);
|
||||
List<RestaurantTable> findAllByOrderByDiningAreaDisplayOrderAscTableNumberAsc();
|
||||
}
|
||||
101
src/main/java/com/caspos/service/ReservationService.java
Normal file
101
src/main/java/com/caspos/service/ReservationService.java
Normal file
@@ -0,0 +1,101 @@
|
||||
package com.caspos.service;
|
||||
|
||||
import com.caspos.domain.Guest;
|
||||
import com.caspos.domain.Reservation;
|
||||
import com.caspos.domain.RestaurantTable;
|
||||
import com.caspos.dto.CreateReservationRequest;
|
||||
import com.caspos.dto.ReservationDto;
|
||||
import com.caspos.repository.GuestRepository;
|
||||
import com.caspos.repository.ReservationRepository;
|
||||
import com.caspos.repository.TableRepository;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class ReservationService {
|
||||
|
||||
private final ReservationRepository reservationRepository;
|
||||
private final GuestRepository guestRepository;
|
||||
private final TableRepository tableRepository;
|
||||
|
||||
public ReservationService(
|
||||
ReservationRepository reservationRepository,
|
||||
GuestRepository guestRepository,
|
||||
TableRepository tableRepository
|
||||
) {
|
||||
this.reservationRepository = reservationRepository;
|
||||
this.guestRepository = guestRepository;
|
||||
this.tableRepository = tableRepository;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ReservationDto> getByDate(LocalDate date) {
|
||||
return reservationRepository.findByDate(date).stream().map(this::toDto).toList();
|
||||
}
|
||||
|
||||
public ReservationDto create(CreateReservationRequest req) {
|
||||
Guest guest = new Guest();
|
||||
guest.setFirstName(req.firstName());
|
||||
guest.setLastName(req.lastName());
|
||||
guest.setPhone(req.phone());
|
||||
guest.setEmail(req.email());
|
||||
guest = guestRepository.save(guest);
|
||||
|
||||
List<RestaurantTable> tables = req.tableIds() == null ? List.of()
|
||||
: tableRepository.findAllById(req.tableIds());
|
||||
|
||||
Reservation reservation = new Reservation();
|
||||
reservation.setGuest(guest);
|
||||
reservation.setTables(tables);
|
||||
reservation.setStartTime(req.startTime());
|
||||
reservation.setEndTime(req.endTime());
|
||||
reservation.setGuestCount(req.guestCount());
|
||||
reservation.setSpecialRequests(req.specialRequests());
|
||||
reservation.setStatus(Reservation.Status.CONFIRMED);
|
||||
|
||||
return toDto(reservationRepository.save(reservation));
|
||||
}
|
||||
|
||||
public ReservationDto updateStatus(UUID id, String rawStatus) {
|
||||
Reservation reservation = reservationRepository.findById(id)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Reservation not found: " + id));
|
||||
|
||||
Reservation.Status newStatus;
|
||||
try {
|
||||
newStatus = Reservation.Status.valueOf(rawStatus.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid status: " + rawStatus);
|
||||
}
|
||||
|
||||
reservation.setStatus(newStatus);
|
||||
return toDto(reservationRepository.save(reservation));
|
||||
}
|
||||
|
||||
private ReservationDto toDto(Reservation r) {
|
||||
var guestDto = r.getGuest() == null ? null : new ReservationDto.GuestDto(
|
||||
r.getGuest().getId(),
|
||||
r.getGuest().getFirstName(),
|
||||
r.getGuest().getLastName(),
|
||||
r.getGuest().getPhone(),
|
||||
r.getGuest().getEmail()
|
||||
);
|
||||
return new ReservationDto(
|
||||
r.getId(),
|
||||
guestDto,
|
||||
r.getTables().stream().map(RestaurantTable::getId).toList(),
|
||||
r.getStartTime(),
|
||||
r.getEndTime(),
|
||||
r.getGuestCount(),
|
||||
r.getStatus().name(),
|
||||
r.getSpecialRequests(),
|
||||
r.getCreatedAt().toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
43
src/main/java/com/caspos/service/TableService.java
Normal file
43
src/main/java/com/caspos/service/TableService.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package com.caspos.service;
|
||||
|
||||
import com.caspos.domain.RestaurantTable;
|
||||
import com.caspos.dto.TableDto;
|
||||
import com.caspos.repository.TableRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@Transactional(readOnly = true)
|
||||
public class TableService {
|
||||
|
||||
private final TableRepository tableRepository;
|
||||
|
||||
public TableService(TableRepository tableRepository) {
|
||||
this.tableRepository = tableRepository;
|
||||
}
|
||||
|
||||
public List<TableDto> getAllTables() {
|
||||
return tableRepository.findAllByOrderByDiningAreaDisplayOrderAscTableNumberAsc()
|
||||
.stream()
|
||||
.map(this::toDto)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private TableDto toDto(RestaurantTable t) {
|
||||
return new TableDto(
|
||||
t.getId(),
|
||||
t.getTableNumber(),
|
||||
t.getDiningArea() != null ? t.getDiningArea().getName() : null,
|
||||
t.getMinCapacity(),
|
||||
t.getMaxCapacity(),
|
||||
t.isCombinable(),
|
||||
t.getPosX(),
|
||||
t.getPosY(),
|
||||
t.getWidth(),
|
||||
t.getHeight(),
|
||||
t.getShape()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user