From 9163cb81914e8067996083f064c8e939704ddf6c Mon Sep 17 00:00:00 2001 From: danielprogram08 Date: Sat, 18 Jul 2026 11:21:19 -0300 Subject: [PATCH] feat:admin screen --- backend/api-mono-flow/Dockerfile | 5 + .../api_mono_flow/config/CorsConfig.java | 9 +- .../api_mono_flow/config/WebConfig.java | 2 +- .../controller/OrderItemController.java | 12 + .../dto/req/UpdateOrderItemStatusRequest.java | 19 ++ .../dto/res/OrderItemResponse.java | 4 + .../api_mono_flow/model/domain/OrderItem.java | 7 + .../model/domain/OrderItemStatus.java | 7 + .../model/mapper/OrderItemMapper.java | 4 + .../model/repository/OrderItemRepository.java | 2 + .../service/OrderItemService.java | 29 +- .../api_mono_flow/service/OrderService.java | 2 + .../migration/V3__add_order_item_status.sql | 6 + docker-compose.yml | 21 +- frontend/.dockerignore | 8 + frontend/Dockerfile | 11 +- frontend/nginx.conf | 7 +- frontend/package-lock.json | 90 ++++-- frontend/package.json | 3 +- frontend/src/contexts/MesaContext.jsx | 45 +-- frontend/src/hooks/useMenu.js | 14 +- frontend/src/index.css | 288 ++++++++++++++++++ frontend/src/main.jsx | 24 +- frontend/src/screens/HomeScreen.jsx | 34 +++ frontend/src/screens/KitchenScreen.jsx | 153 ++++++++++ frontend/src/screens/MenuScreen.jsx | 12 +- frontend/src/services/api.js | 52 +++- 27 files changed, 778 insertions(+), 92 deletions(-) create mode 100644 backend/api-mono-flow/src/main/java/com/example/api_mono_flow/dto/req/UpdateOrderItemStatusRequest.java create mode 100644 backend/api-mono-flow/src/main/java/com/example/api_mono_flow/model/domain/OrderItemStatus.java create mode 100644 backend/api-mono-flow/src/main/resources/db/migration/V3__add_order_item_status.sql create mode 100644 frontend/.dockerignore create mode 100644 frontend/src/screens/HomeScreen.jsx create mode 100644 frontend/src/screens/KitchenScreen.jsx diff --git a/backend/api-mono-flow/Dockerfile b/backend/api-mono-flow/Dockerfile index 643028c..85b9146 100644 --- a/backend/api-mono-flow/Dockerfile +++ b/backend/api-mono-flow/Dockerfile @@ -18,8 +18,13 @@ FROM eclipse-temurin:21-jre-alpine WORKDIR /app +RUN apk add --no-cache wget + COPY --from=build /app/target/*.jar app.jar EXPOSE 8080 +HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=5 \ + CMD wget -qO- http://127.0.0.1:8080/api/items > /dev/null || exit 1 + ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/config/CorsConfig.java b/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/config/CorsConfig.java index 48b73d1..7d08677 100644 --- a/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/config/CorsConfig.java +++ b/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/config/CorsConfig.java @@ -10,8 +10,13 @@ public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") - .allowedOrigins("http://localhost:5173", "http://localhost:80", "http://localhost") - .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedOrigins( + "http://localhost:5173", + "http://localhost:3000", + "http://localhost:80", + "http://localhost" + ) + .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*"); } } \ No newline at end of file diff --git a/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/config/WebConfig.java b/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/config/WebConfig.java index 8d2a0d4..4515c80 100644 --- a/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/config/WebConfig.java +++ b/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/config/WebConfig.java @@ -15,7 +15,7 @@ public void addCorsMappings(CorsRegistry registry) { "http://localhost:4173", "http://localhost:3000" ) - .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(false); } diff --git a/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/controller/OrderItemController.java b/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/controller/OrderItemController.java index 2b0381d..6447b55 100644 --- a/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/controller/OrderItemController.java +++ b/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/controller/OrderItemController.java @@ -1,12 +1,17 @@ package com.example.api_mono_flow.controller; import java.util.List; +import java.util.UUID; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import com.example.api_mono_flow.dto.req.UpdateOrderItemStatusRequest; import com.example.api_mono_flow.dto.res.OrderItemResponse; import com.example.api_mono_flow.service.OrderItemService; @@ -23,4 +28,11 @@ public class OrderItemController { public ResponseEntity> findAll() { return ResponseEntity.ok(orderItemService.findAll()); } + + @PatchMapping("/{orderItemId}/status") + public ResponseEntity updateStatus( + @PathVariable UUID orderItemId, + @RequestBody UpdateOrderItemStatusRequest request) { + return ResponseEntity.ok(orderItemService.updateStatus(orderItemId, request)); + } } diff --git a/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/dto/req/UpdateOrderItemStatusRequest.java b/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/dto/req/UpdateOrderItemStatusRequest.java new file mode 100644 index 0000000..f73d27b --- /dev/null +++ b/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/dto/req/UpdateOrderItemStatusRequest.java @@ -0,0 +1,19 @@ +package com.example.api_mono_flow.dto.req; + +import com.example.api_mono_flow.model.domain.OrderItemStatus; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UpdateOrderItemStatusRequest { + + private OrderItemStatus status; +} diff --git a/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/dto/res/OrderItemResponse.java b/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/dto/res/OrderItemResponse.java index ca34307..ca40e60 100644 --- a/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/dto/res/OrderItemResponse.java +++ b/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/dto/res/OrderItemResponse.java @@ -2,6 +2,8 @@ import java.util.UUID; +import com.example.api_mono_flow.model.domain.OrderItemStatus; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -16,7 +18,9 @@ public class OrderItemResponse { private UUID orderItemId; + private Integer tableNumber; private String customerName; private String itemName; private Integer quantity; + private OrderItemStatus status; } diff --git a/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/model/domain/OrderItem.java b/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/model/domain/OrderItem.java index c8e89ab..6a2aad3 100644 --- a/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/model/domain/OrderItem.java +++ b/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/model/domain/OrderItem.java @@ -4,6 +4,8 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.GenerationType; import jakarta.persistence.GeneratedValue; @@ -41,4 +43,9 @@ public class OrderItem { @Column(name = "quantity", nullable = false) private Integer quantity; + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false, length = 20) + @Builder.Default + private OrderItemStatus status = OrderItemStatus.A_FAZER; } diff --git a/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/model/domain/OrderItemStatus.java b/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/model/domain/OrderItemStatus.java new file mode 100644 index 0000000..f570aac --- /dev/null +++ b/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/model/domain/OrderItemStatus.java @@ -0,0 +1,7 @@ +package com.example.api_mono_flow.model.domain; + +public enum OrderItemStatus { + A_FAZER, + FAZENDO, + CONCLUIDO +} diff --git a/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/model/mapper/OrderItemMapper.java b/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/model/mapper/OrderItemMapper.java index 88fa735..0f854c9 100644 --- a/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/model/mapper/OrderItemMapper.java +++ b/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/model/mapper/OrderItemMapper.java @@ -5,6 +5,7 @@ import com.example.api_mono_flow.model.domain.Item; import com.example.api_mono_flow.model.domain.Order; import com.example.api_mono_flow.model.domain.OrderItem; +import com.example.api_mono_flow.model.domain.OrderItemStatus; public final class OrderItemMapper { @@ -20,6 +21,7 @@ public static OrderItem toEntity(OrderItemRequest request) { .item(request.getItemId() == null ? null : Item.builder().itemId(request.getItemId()).build()) .order(request.getOrderId() == null ? null : Order.builder().orderId(request.getOrderId()).build()) .quantity(request.getQuantity()) + .status(OrderItemStatus.A_FAZER) .build(); } @@ -30,9 +32,11 @@ public static OrderItemResponse toResponse(OrderItem orderItem) { return OrderItemResponse.builder() .orderItemId(orderItem.getOrderItemId()) + .tableNumber(orderItem.getOrder().getTableNumber()) .customerName(orderItem.getOrder().getCustomerName()) .itemName(orderItem.getItem().getName()) .quantity(orderItem.getQuantity()) + .status(orderItem.getStatus()) .build(); } } diff --git a/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/model/repository/OrderItemRepository.java b/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/model/repository/OrderItemRepository.java index a7fb60a..00831c7 100644 --- a/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/model/repository/OrderItemRepository.java +++ b/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/model/repository/OrderItemRepository.java @@ -7,9 +7,11 @@ import org.springframework.stereotype.Repository; import com.example.api_mono_flow.model.domain.OrderItem; +import com.example.api_mono_flow.model.domain.OrderItemStatus; @Repository public interface OrderItemRepository extends JpaRepository { List findByOrder_OrderId(UUID orderId); List findByItem_ItemId(UUID itemId); + List findByStatusNot(OrderItemStatus status); } diff --git a/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/service/OrderItemService.java b/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/service/OrderItemService.java index db40036..5a196cc 100644 --- a/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/service/OrderItemService.java +++ b/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/service/OrderItemService.java @@ -7,10 +7,12 @@ import org.springframework.transaction.annotation.Transactional; import com.example.api_mono_flow.dto.req.OrderItemRequest; +import com.example.api_mono_flow.dto.req.UpdateOrderItemStatusRequest; import com.example.api_mono_flow.dto.res.OrderItemResponse; import com.example.api_mono_flow.model.domain.Item; import com.example.api_mono_flow.model.domain.Order; import com.example.api_mono_flow.model.domain.OrderItem; +import com.example.api_mono_flow.model.domain.OrderItemStatus; import com.example.api_mono_flow.model.mapper.OrderItemMapper; import com.example.api_mono_flow.model.repository.ItemRepository; import com.example.api_mono_flow.model.repository.OrderItemRepository; @@ -29,11 +31,36 @@ public class OrderItemService { private final OrderRepository orderRepository; public List findAll() { - return orderItemRepository.findAll().stream() + return orderItemRepository.findByStatusNot(OrderItemStatus.CONCLUIDO).stream() .map(OrderItemMapper::toResponse) .toList(); } + public OrderItemResponse updateStatus(UUID orderItemId, UpdateOrderItemStatusRequest request) { + if (request == null || request.getStatus() == null) { + throw new IllegalArgumentException("Status is required"); + } + + OrderItem orderItem = findEntityById(orderItemId); + OrderItemStatus current = orderItem.getStatus() == null + ? OrderItemStatus.A_FAZER + : orderItem.getStatus(); + OrderItemStatus next = request.getStatus(); + + if (!isValidTransition(current, next)) { + throw new IllegalArgumentException( + "Invalid status transition: " + current + " -> " + next); + } + + orderItem.setStatus(next); + return OrderItemMapper.toResponse(orderItemRepository.save(orderItem)); + } + + private boolean isValidTransition(OrderItemStatus current, OrderItemStatus next) { + return (current == OrderItemStatus.A_FAZER && next == OrderItemStatus.FAZENDO) + || (current == OrderItemStatus.FAZENDO && next == OrderItemStatus.CONCLUIDO); + } + public OrderItemResponse findById(UUID orderItemId) { return OrderItemMapper.toResponse(findEntityById(orderItemId)); } diff --git a/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/service/OrderService.java b/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/service/OrderService.java index 5bafb0d..b51e5b5 100644 --- a/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/service/OrderService.java +++ b/backend/api-mono-flow/src/main/java/com/example/api_mono_flow/service/OrderService.java @@ -15,6 +15,7 @@ import com.example.api_mono_flow.model.domain.Item; import com.example.api_mono_flow.model.domain.Order; import com.example.api_mono_flow.model.domain.OrderItem; +import com.example.api_mono_flow.model.domain.OrderItemStatus; import com.example.api_mono_flow.model.mapper.OrderItemMapper; import com.example.api_mono_flow.model.mapper.OrderMapper; import com.example.api_mono_flow.model.repository.ItemRepository; @@ -89,6 +90,7 @@ public CompleteOrderResponse createCompleteOrder(CompleteOrderRequest request) { .item(item) .order(order) .quantity(line.getQuantity()) + .status(OrderItemStatus.A_FAZER) .build(); items.add(orderItemRepository.save(orderItem)); } diff --git a/backend/api-mono-flow/src/main/resources/db/migration/V3__add_order_item_status.sql b/backend/api-mono-flow/src/main/resources/db/migration/V3__add_order_item_status.sql new file mode 100644 index 0000000..21ec9ed --- /dev/null +++ b/backend/api-mono-flow/src/main/resources/db/migration/V3__add_order_item_status.sql @@ -0,0 +1,6 @@ +ALTER TABLE tb_order_item + ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'A_FAZER'; + +ALTER TABLE tb_order_item + ADD CONSTRAINT chk_tb_order_item_status + CHECK (status IN ('A_FAZER', 'FAZENDO', 'CONCLUIDO')); diff --git a/docker-compose.yml b/docker-compose.yml index 6f96ae3..b627b2d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,8 @@ services: interval: 10s timeout: 5s retries: 5 + networks: + - menuflow # ── Backend (Spring Boot) ─────────────────────────────────────────────────── backend: @@ -35,8 +37,14 @@ services: depends_on: postgres: condition: service_healthy + networks: + - menuflow - # ── Frontend (React + nginx) ──────────────────────────────────────────────── + # ── Frontend (React SPA + nginx) ──────────────────────────────────────────── + # URLs (porta 3000 no host → 80 no container): + # Home: http://localhost:3000/ + # Mesa: http://localhost:3000/mesa?id=8 (id obrigatório) + # O nginx faz SPA fallback (/mesa → index.html) e proxy /api → backend:8080 frontend: build: context: ./frontend @@ -46,7 +54,14 @@ services: ports: - "3000:80" depends_on: - - backend + backend: + condition: service_started + networks: + - menuflow + +networks: + menuflow: + driver: bridge volumes: - pg_data: \ No newline at end of file + pg_data: diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..16a5fa7 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,8 @@ +node_modules +dist +.git +.gitignore +npm-debug.log* +*.log +.env +.env.* diff --git a/frontend/Dockerfile b/frontend/Dockerfile index f309ea2..2880947 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,22 +1,25 @@ -# ── Stage 1: build ────────────────────────────────────────────────────────── +# ── Stage 1: build (Vite + React Router) ───────────────────────────────────── +# Gera o bundle estático com as rotas / e /mesa?id= FROM node:22-alpine AS build WORKDIR /app -COPY package*.json ./ +COPY package.json package-lock.json ./ RUN npm ci COPY . . RUN npm run build # ── Stage 2: runtime (nginx) ───────────────────────────────────────────────── +# Serve SPA (fallback index.html) e faz proxy de /api → backend FROM nginx:stable-alpine COPY --from=build /app/dist /usr/share/nginx/html - -# Nginx config: serve SPA and proxy /api → backend COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget -qO- http://127.0.0.1/ > /dev/null || exit 1 + CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 8b3ec88..fd2a0e6 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -5,12 +5,15 @@ server { root /usr/share/nginx/html; index index.html; - # SPA fallback — all unknown paths go to index.html + # React Router SPA: + # / → HomeScreen + # /mesa?id=N → MenuScreen (id obrigatório, tratado no cliente) + # Qualquer path desconhecido cai no index.html para o React Router. location / { try_files $uri $uri/ /index.html; } - # Proxy /api requests to the Spring Boot backend + # Proxy /api → Spring Boot (serviço "backend" na rede do compose) location /api/ { proxy_pass http://backend:8080/api/; proxy_http_version 1.1; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 24b5c51..87b461b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "lucide-react": "^1.25.0", "react": "^19.2.7", - "react-dom": "^19.2.7" + "react-dom": "^19.2.7", + "react-router-dom": "^7.18.1" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -664,9 +665,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -684,9 +682,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -704,9 +699,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -724,9 +716,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -744,9 +733,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -764,9 +750,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1069,6 +1052,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1770,9 +1766,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1794,9 +1787,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1818,9 +1808,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1842,9 +1829,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2154,6 +2138,44 @@ "react": "^19.2.7" } }, + "node_modules/react-router": { + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", + "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz", + "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/rolldown": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", @@ -2204,6 +2226,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index b82178f..b556259 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,7 +12,8 @@ "dependencies": { "lucide-react": "^1.25.0", "react": "^19.2.7", - "react-dom": "^19.2.7" + "react-dom": "^19.2.7", + "react-router-dom": "^7.18.1" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/frontend/src/contexts/MesaContext.jsx b/frontend/src/contexts/MesaContext.jsx index 155c804..4405e22 100644 --- a/frontend/src/contexts/MesaContext.jsx +++ b/frontend/src/contexts/MesaContext.jsx @@ -1,40 +1,25 @@ -import React, { createContext, useContext, useState, useEffect } from "react"; +import React, { createContext, useContext, useMemo } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; -const MesaContext = createContext({}); +const MesaContext = createContext(undefined); +/** + * Lê o número da mesa a partir de /mesa?id=N (obrigatório). + */ export function MesaProvider({ children }) { - const [mesa, setMesa] = useState(null); - const [isReady, setIsReady] = useState(false); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); - useEffect(() => { - // 1. Tenta ler da URL - const params = new URLSearchParams(window.location.search); - const mesaFromUrl = params.get("mesa"); + const mesa = useMemo(() => { + const id = searchParams.get("id"); + if (!id || !/^\d+$/.test(id)) return null; + return id; + }, [searchParams]); - if (mesaFromUrl) { - setMesa(mesaFromUrl); - localStorage.setItem("mesa_id", mesaFromUrl); - - // Limpa a URL visualmente para não ficar suja com o parâmetro - const newUrl = window.location.origin + window.location.pathname; - window.history.replaceState({}, document.title, newUrl); - } else { - // 2. Tenta ler do localStorage - const mesaFromStorage = localStorage.getItem("mesa_id"); - if (mesaFromStorage) { - setMesa(mesaFromStorage); - } - } - setIsReady(true); - }, []); - - const clearMesa = () => { - localStorage.removeItem("mesa_id"); - setMesa(null); - }; + const clearMesa = () => navigate("/"); return ( - + {children} ); diff --git a/frontend/src/hooks/useMenu.js b/frontend/src/hooks/useMenu.js index 8701f5e..881c762 100644 --- a/frontend/src/hooks/useMenu.js +++ b/frontend/src/hooks/useMenu.js @@ -37,14 +37,14 @@ export function useMenu() { // Mapeia os itens do banco para o formato que os componentes esperam const mappedItems = apiItems.map((apiItem) => { const id = apiItem.itemId; - idMap[id] = id; // O ID local passa a ser o próprio UUID do banco - + idMap[id] = id; + return { - id: id, - name: apiItem.nome, - desc: "", // Banco de dados atual não possui descrição - price: parseFloat(apiItem.preco), - img: `https://picsum.photos/seed/${id}/200/200` // Placeholder visual + id, + name: apiItem.name ?? apiItem.nome, + desc: "", + price: parseFloat(apiItem.price ?? apiItem.preco), + img: `https://picsum.photos/seed/${id}/200/200`, }; }); diff --git a/frontend/src/index.css b/frontend/src/index.css index d60db26..6b8b769 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -656,6 +656,36 @@ body { color: #E2634A; } +.home-screen__hint { + margin-top: 16px; +} + +.home-screen__example { + margin-top: 20px; +} + +.home-screen__code { + display: inline-block; + margin-top: 4px; + padding: 4px 8px; + border-radius: 8px; + background: rgba(226, 99, 74, 0.12); + color: #E2634A; + font-size: 13px; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; +} + +.home-screen__link { + color: #E2634A; + font-weight: 600; + text-decoration: underline; + text-underline-offset: 3px; +} + +.home-screen__link:hover { + color: #C94E37; +} + /* ─── Loading Screen ─────────────────────────── */ .loading-screen { min-height: 100vh; @@ -675,3 +705,261 @@ body { transform: none !important; box-shadow: none !important; } + +/* ─── Kitchen Kanban ─────────────────────────── */ +.kitchen { + min-height: 100vh; + background: + radial-gradient(ellipse 80% 50% at 10% 0%, rgba(226, 99, 74, 0.12), transparent 55%), + radial-gradient(ellipse 60% 40% at 90% 10%, rgba(124, 148, 115, 0.1), transparent 50%), + #15171C; + padding: 24px 20px 40px; + color: #F6F1E7; +} + +.kitchen__header { + max-width: 1100px; + margin: 0 auto 24px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; +} + +.kitchen__brand { + display: flex; + align-items: center; + gap: 14px; +} + +.kitchen__icon { + width: 48px; + height: 48px; + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + background: #202B24; + color: #E2634A; + border: 1px solid rgba(246, 241, 231, 0.1); +} + +.kitchen__title { + font-family: 'Bitter', serif; + font-size: clamp(24px, 4vw, 32px); + font-weight: 700; + letter-spacing: -0.5px; + line-height: 1.1; +} + +.kitchen__subtitle { + margin-top: 4px; + font-size: 13px; + color: rgba(246, 241, 231, 0.55); +} + +.kitchen__refresh { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + border-radius: 12px; + border: 1px solid rgba(246, 241, 231, 0.16); + background: rgba(246, 241, 231, 0.06); + color: #EFE8D8; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: background 0.18s ease, border-color 0.18s ease; +} + +.kitchen__refresh:hover:not(:disabled) { + background: rgba(246, 241, 231, 0.1); + border-color: rgba(246, 241, 231, 0.28); +} + +.kitchen__refresh:disabled { + opacity: 0.55; + cursor: not-allowed; +} + +.kitchen__error { + max-width: 1100px; + margin: 0 auto 16px; + padding: 12px 14px; + border-radius: 12px; + background: rgba(226, 99, 74, 0.14); + border: 1px solid rgba(226, 99, 74, 0.35); + color: #E2634A; + font-size: 13px; +} + +.kitchen__empty { + max-width: 1100px; + margin: 48px auto; + text-align: center; + color: rgba(246, 241, 231, 0.5); + font-size: 15px; +} + +.kitchen__board { + max-width: 1100px; + margin: 0 auto; + display: grid; + grid-template-columns: 1fr; + gap: 16px; +} + +@media (min-width: 800px) { + .kitchen__board { + grid-template-columns: 1fr 1fr; + gap: 20px; + align-items: start; + } +} + +.kitchen-col { + background: rgba(32, 43, 36, 0.55); + border: 1px solid rgba(246, 241, 231, 0.08); + border-radius: 20px; + padding: 18px; + min-height: 320px; +} + +.kitchen-col__head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.kitchen-col__title { + font-family: 'Bitter', serif; + font-size: 20px; + font-weight: 700; + letter-spacing: -0.3px; +} + +.kitchen-col__count { + min-width: 28px; + height: 28px; + padding: 0 8px; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 13px; + font-weight: 700; + background: rgba(226, 99, 74, 0.18); + color: #E2634A; +} + +.kitchen-col__hint { + margin-top: 6px; + margin-bottom: 16px; + font-size: 12px; + color: rgba(246, 241, 231, 0.45); +} + +.kitchen-col__list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.kitchen-col__empty { + padding: 28px 12px; + text-align: center; + font-size: 13px; + color: rgba(246, 241, 231, 0.35); + border: 1px dashed rgba(246, 241, 231, 0.12); + border-radius: 14px; +} + +.kitchen-card { + background: #F6F1E7; + color: #15171C; + border-radius: 16px; + padding: 16px; + animation: fadeIn 0.25s ease; +} + +.kitchen-card__meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 8px; +} + +.kitchen-card__mesa { + font-size: 12px; + font-weight: 700; + letter-spacing: 0.2px; + color: #C94E37; + text-transform: uppercase; +} + +.kitchen-card__client { + font-size: 12px; + color: rgba(21, 23, 28, 0.55); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 55%; +} + +.kitchen-card__name { + font-family: 'Bitter', serif; + font-size: 18px; + font-weight: 700; + letter-spacing: -0.3px; + line-height: 1.25; +} + +.kitchen-card__qty { + margin-top: 4px; + margin-bottom: 14px; + font-size: 13px; + color: rgba(21, 23, 28, 0.55); +} + +.kitchen-card__btn { + width: 100%; + padding: 11px 14px; + border: none; + border-radius: 12px; + font-size: 14px; + font-weight: 700; + cursor: pointer; + transition: background 0.18s ease, transform 0.15s ease; +} + +.kitchen-card__btn:hover:not(:disabled) { + transform: translateY(-1px); +} + +.kitchen-card__btn:disabled { + opacity: 0.65; + cursor: not-allowed; + transform: none; +} + +.kitchen-card__btn--a_fazer { + background: #E2634A; + color: #F6F1E7; +} + +.kitchen-card__btn--a_fazer:hover:not(:disabled) { + background: #C94E37; +} + +.kitchen-card__btn--fazendo { + background: #202B24; + color: #F6F1E7; +} + +.kitchen-card__btn--fazendo:hover:not(:disabled) { + background: #15171C; +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 67018c6..daf52bb 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,13 +1,27 @@ import React from "react"; import ReactDOM from "react-dom/client"; -import "./index.css" +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import "./index.css"; +import { HomeScreen } from "./screens/HomeScreen"; import { MenuScreen } from "./screens/MenuScreen"; +import { KitchenScreen } from "./screens/KitchenScreen"; import { MesaProvider } from "./contexts/MesaContext"; ReactDOM.createRoot(document.getElementById("root")).render( - - - + + + } /> + + + + } + /> + } /> + + -);`` \ No newline at end of file +); diff --git a/frontend/src/screens/HomeScreen.jsx b/frontend/src/screens/HomeScreen.jsx new file mode 100644 index 0000000..fb29feb --- /dev/null +++ b/frontend/src/screens/HomeScreen.jsx @@ -0,0 +1,34 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import { QrCode } from "lucide-react"; + +/** + * Página inicial — explica como acessar o cardápio da mesa. + */ +export function HomeScreen() { + return ( +
+
+
+ +
+

MenuFlow

+

+ Para fazer seu pedido, escaneie o QR Code localizado + na sua mesa. Ele abre o cardápio já vinculado ao número da mesa. +

+

+ O link do cardápio tem o formato{" "} + /mesa?id=8, onde{" "} + id é o número da mesa. +

+

+ Exemplo:{" "} + + /mesa?id=8 + +

+
+
+ ); +} diff --git a/frontend/src/screens/KitchenScreen.jsx b/frontend/src/screens/KitchenScreen.jsx new file mode 100644 index 0000000..19baef1 --- /dev/null +++ b/frontend/src/screens/KitchenScreen.jsx @@ -0,0 +1,153 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { ChefHat, RefreshCw } from "lucide-react"; +import { fetchOrderItems, updateOrderItemStatus } from "../services/api"; + +const COLUMNS = [ + { + id: "A_FAZER", + title: "A fazer", + hint: "Aceite para começar o preparo", + actionLabel: "Aceitar", + nextStatus: "FAZENDO", + }, + { + id: "FAZENDO", + title: "Fazendo", + hint: "Conclua quando estiver pronto", + actionLabel: "Concluir", + nextStatus: "CONCLUIDO", + }, +]; + +const POLL_MS = 5000; + +/** + * Painel da cozinha — Kanban de itens de pedido. + */ +export function KitchenScreen() { + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [updatingId, setUpdatingId] = useState(null); + + const load = useCallback(async ({ silent = false } = {}) => { + try { + if (!silent) setLoading(true); + setError(null); + const data = await fetchOrderItems(); + setItems(Array.isArray(data) ? data : []); + } catch (err) { + setError(err.message); + } finally { + if (!silent) setLoading(false); + } + }, []); + + useEffect(() => { + load(); + const id = setInterval(() => load({ silent: true }), POLL_MS); + return () => clearInterval(id); + }, [load]); + + const handleAdvance = async (orderItemId, nextStatus) => { + setUpdatingId(orderItemId); + setError(null); + try { + await updateOrderItemStatus(orderItemId, nextStatus); + if (nextStatus === "CONCLUIDO") { + setItems((prev) => prev.filter((item) => item.orderItemId !== orderItemId)); + } else { + setItems((prev) => + prev.map((item) => + item.orderItemId === orderItemId ? { ...item, status: nextStatus } : item + ) + ); + } + } catch (err) { + setError(err.message); + await load({ silent: true }); + } finally { + setUpdatingId(null); + } + }; + + return ( +
+
+
+
+ +
+
+

Cozinha

+

MenuFlow · fila de preparo

+
+
+ +
+ + {error && ( +
+ {error} +
+ )} + + {loading && items.length === 0 ? ( +
Carregando pedidos…
+ ) : ( +
+ {COLUMNS.map((column) => { + const columnItems = items.filter((item) => item.status === column.id); + return ( +
+
+

{column.title}

+ {columnItems.length} +
+

{column.hint}

+ +
+ {columnItems.length === 0 ? ( +

Nenhum item

+ ) : ( + columnItems.map((item) => ( +
+
+ Mesa {item.tableNumber} + {item.customerName && ( + {item.customerName} + )} +
+

{item.itemName}

+

+ {item.quantity}× unidade{item.quantity === 1 ? "" : "s"} +

+ +
+ )) + )} +
+
+ ); + })} +
+ )} +
+ ); +} diff --git a/frontend/src/screens/MenuScreen.jsx b/frontend/src/screens/MenuScreen.jsx index b14e501..b40f9c6 100644 --- a/frontend/src/screens/MenuScreen.jsx +++ b/frontend/src/screens/MenuScreen.jsx @@ -1,4 +1,5 @@ import React, { useState, useRef, useMemo, useCallback } from "react"; +import { Link } from "react-router-dom"; import { useCart } from "../hooks/useCart"; import { useMenu } from "../hooks/useMenu"; import { Hero } from "../components/Hero/Hero"; @@ -81,9 +82,16 @@ export function MenuScreen() {
-

Bem-vindo(a)!

+

Mesa não informada

- Para iniciar o seu pedido, por favor escaneie o QR Code localizado na sua mesa. + O número da mesa é obrigatório. Escaneie o QR Code da + sua mesa ou acesse no formato{" "} + /mesa?id=8. +

+

+ + Voltar ao início +

diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 0609045..1f15fac 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -46,13 +46,20 @@ export async function createItem(item) { * nomeCliente: string, * items: Array<{ itemId: string, quantidade: number }> * }} payload - * @returns {Promise<{ pedidoId: string, mesa: number, nomeCliente: string, items: any[] }>} + * @returns {Promise<{ orderId: string, tableNumber: number, customerName: string, items: any[] }>} */ export async function criarPedidoCompleto(payload) { - const res = await fetch(`${BASE_URL}/pedidos/complete`, { + const res = await fetch(`${BASE_URL}/orders/complete`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), + body: JSON.stringify({ + tableNumber: payload.mesa, + customerName: payload.nomeCliente, + items: payload.items.map((line) => ({ + itemId: line.itemId, + quantity: line.quantidade, + })), + }), }); if (!res.ok) { const text = await res.text(); @@ -60,3 +67,42 @@ export async function criarPedidoCompleto(payload) { } return res.json(); } + +// -------------------------------------------------------------------------- +// Cozinha (order-items) +// -------------------------------------------------------------------------- + +/** + * Lista itens de pedido ativos (A_FAZER / FAZENDO). + * @returns {Promise>} + */ +export async function fetchOrderItems() { + const res = await fetch(`${BASE_URL}/order-items`); + if (!res.ok) throw new Error(`Erro ao buscar pedidos da cozinha: ${res.status}`); + return res.json(); +} + +/** + * Atualiza o status de um item na cozinha. + * @param {string} orderItemId + * @param {'A_FAZER' | 'FAZENDO' | 'CONCLUIDO'} status + */ +export async function updateOrderItemStatus(orderItemId, status) { + const res = await fetch(`${BASE_URL}/order-items/${orderItemId}/status`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ status }), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`Erro ao atualizar status (${res.status}): ${text}`); + } + return res.json(); +}