From a914fd44cce3a584b80e09589340f95a9956cf4d Mon Sep 17 00:00:00 2001 From: Sidharth Prabhu Date: Tue, 23 Jun 2026 10:47:15 +0530 Subject: [PATCH] Search bugs are fixed. Added Global searching parameter --- .../canteen/sales/config/SecurityConfig.java | 15 +- .../sales/controller/OrderController.java | 6 + .../sales/controller/TerminalController.java | 126 +- .../com/rit/canteen/sales/model/Terminal.java | 6 + .../rit/canteen/sales/model/TerminalDTO.java | 7 +- .../sales/service/TerminalService.java | 9 + counter-frontend/package-lock.json | 418 ++++--- counter-frontend/package.json | 1 + counter-frontend/src/pages/Categories.tsx | 21 +- counter-frontend/src/pages/Login.tsx | 37 +- counter-frontend/src/pages/Orders.tsx | 1082 ++++++++++++++--- counter-frontend/src/pages/Products.tsx | 47 +- frontend/src/pages/Orders.tsx | 22 +- frontend/src/pages/Settings.tsx | 88 +- frontend/src/pages/Terminals.tsx | 52 +- ordering_site/src/pages/HomeScreen.tsx | 90 +- 16 files changed, 1520 insertions(+), 507 deletions(-) diff --git a/backend/src/main/java/com/rit/canteen/sales/config/SecurityConfig.java b/backend/src/main/java/com/rit/canteen/sales/config/SecurityConfig.java index 446e3fa5..37da4405 100644 --- a/backend/src/main/java/com/rit/canteen/sales/config/SecurityConfig.java +++ b/backend/src/main/java/com/rit/canteen/sales/config/SecurityConfig.java @@ -73,7 +73,7 @@ public class SecurityConfig { .requestMatchers(HttpMethod.GET, "/api/orders/user/**").authenticated() .requestMatchers(HttpMethod.GET, "/api/wallet/balance/**").authenticated() .requestMatchers(HttpMethod.GET, "/api/wallet/transactions/**").authenticated() - .requestMatchers(HttpMethod.POST, "/api/wallet/topup").hasAnyRole("MASTER", "MANAGER", "STAFF") + .requestMatchers(HttpMethod.POST, "/api/wallet/topup").authenticated() .requestMatchers(HttpMethod.POST, "/api/coupons/redeem").authenticated() .requestMatchers(HttpMethod.POST, "/api/feedback/**").authenticated() .requestMatchers(HttpMethod.GET, "/api/feedback/**").authenticated() @@ -107,7 +107,18 @@ public class SecurityConfig { List origins = Arrays.asList(allowedOriginsStr.split(",")); configuration.setAllowedOrigins(origins); configuration.setAllowedOriginPatterns(List.of( - "http://localhost:*" + "http://localhost:*", + "http://127.0.0.1:*", + "http://192.168.*:*", + "http://10.*:*", + "http://172.*:*", + "http://*.local:*", + "https://localhost:*", + "https://127.0.0.1:*", + "https://192.168.*:*", + "https://10.*:*", + "https://172.*:*", + "https://*.local:*" )); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java b/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java index 99ab089b..0a974fc0 100644 --- a/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java +++ b/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java @@ -261,6 +261,12 @@ public class OrderController { if (tokenUserId != null && !tokenUserId.equals(existingOrder.getUserId()) && !isStaff()) { return ResponseEntity.status(403).body(Map.of("error", "Access denied")); } + + // Cannot modify completed/delivered orders + if ("COMPLETED".equalsIgnoreCase(existingOrder.getStatus()) || "DELIVERED".equalsIgnoreCase(existingOrder.getStatus())) { + return ResponseEntity.status(400).body(Map.of("error", "Cannot modify a completed or delivered order")); + } + BigDecimal oldAmount = existingOrder.getTotalAmount(); BigDecimal newAmount = updatedOrder.getTotalAmount(); diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/TerminalController.java b/backend/src/main/java/com/rit/canteen/sales/controller/TerminalController.java index 81cb3200..276807f2 100644 --- a/backend/src/main/java/com/rit/canteen/sales/controller/TerminalController.java +++ b/backend/src/main/java/com/rit/canteen/sales/controller/TerminalController.java @@ -43,12 +43,13 @@ public class TerminalController { public List getAllTerminals() { return terminalService.getAllTerminals().stream() .map(t -> new TerminalDTO( - t.getId(), - t.getName(), - t.getLocation(), - "********", + t.getId(), + t.getName(), + t.getLocation(), + "********", "****", t.isPaired(), + t.isBlocked(), t.getDeviceId() != null ? maskDeviceId(t.getDeviceId()) : null, t.getPairedAt() )) @@ -149,30 +150,39 @@ public class TerminalController { @RequestParam("paymentId") String paymentId, @RequestHeader(value = "X-API-KEY", required = false) String apiKeyHeader, @RequestHeader(value = "Authorization", required = false) String authHeader) { - + String apiKey = extractApiKey(apiKeyHeader, authHeader); if (apiKey == null) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Invalid or missing token")); } // 1. Verify API Key - Optional terminal = terminalRepository.findByApiKey(apiKey); - if (terminal.isEmpty()) { + Optional terminalOpt = terminalRepository.findByApiKey(apiKey); + if (terminalOpt.isEmpty()) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Invalid API Key")); } + Terminal terminal = terminalOpt.get(); - // 2. Parse and Clean payment IDs + // 2. Check if terminal is blocked + if (terminal.isBlocked()) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Map.of( + "status", "BLOCKED", + "message", "This terminal is out of order" + )); + } + + // 3. Parse and Clean payment IDs List orderNumbers = parseAndCleanOrderNumbers(paymentId); if (orderNumbers.isEmpty()) { return ResponseEntity.badRequest().body(Map.of("message", "paymentId required")); } - // 3. Try to fetch the first order that exists + // 4. Try to fetch the first order that exists for (String orderNum : orderNumbers) { Optional orderOpt = orderRepository.findByOrderNumber(orderNum); if (orderOpt.isPresent()) { Order order = orderOpt.get(); - + // Check if order is expired/archived if (order.isArchived()) { continue; // Check other IDs if available, or return GONE @@ -183,7 +193,7 @@ public class TerminalController { return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(Map.of("message", "This order has already been fulfilled and cannot be printed again.")); } - + return ResponseEntity.ok(order); } } @@ -196,30 +206,39 @@ public class TerminalController { @PathVariable("orderNumber") String orderNumber, @RequestHeader(value = "X-API-KEY", required = false) String apiKeyHeader, @RequestHeader(value = "Authorization", required = false) String authHeader) { - + String apiKey = extractApiKey(apiKeyHeader, authHeader); if (apiKey == null) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Invalid or missing token")); } // 1. Verify API Key - Optional terminal = terminalRepository.findByApiKey(apiKey); - if (terminal.isEmpty()) { + Optional terminalOpt = terminalRepository.findByApiKey(apiKey); + if (terminalOpt.isEmpty()) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Invalid API Key")); } + Terminal terminal = terminalOpt.get(); - // 2. Parse and Clean order numbers + // 2. Check if terminal is blocked + if (terminal.isBlocked()) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Map.of( + "status", "BLOCKED", + "message", "This terminal is out of order" + )); + } + + // 3. Parse and Clean order numbers List orderNumbers = parseAndCleanOrderNumbers(orderNumber); if (orderNumbers.isEmpty()) { return ResponseEntity.badRequest().body(Map.of("message", "orderNumber required")); } - // 3. Try to fetch the first order that exists + // 4. Try to fetch the first order that exists for (String orderNum : orderNumbers) { Optional orderOpt = orderRepository.findByOrderNumber(orderNum); if (orderOpt.isPresent()) { Order order = orderOpt.get(); - + // Check if order is expired/archived if (order.isArchived()) { return ResponseEntity.status(HttpStatus.GONE) @@ -231,7 +250,7 @@ public class TerminalController { return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(Map.of("message", "This order has already been fulfilled and cannot be printed again.")); } - + return ResponseEntity.ok(order); } } @@ -245,7 +264,7 @@ public class TerminalController { @RequestBody Map body, @RequestHeader(value = "X-API-KEY", required = false) String apiKeyHeader, @RequestHeader(value = "Authorization", required = false) String authHeader) { - + String orderNumber = body.get("orderNumber"); if (orderNumber == null || orderNumber.isBlank()) { return ResponseEntity.badRequest().body(Map.of("message", "orderNumber required")); @@ -255,20 +274,29 @@ public class TerminalController { if (apiKey == null) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Invalid or missing token")); } - + // 1. Verify API Key - Optional terminal = terminalRepository.findByApiKey(apiKey); - if (terminal.isEmpty()) { + Optional terminalOpt = terminalRepository.findByApiKey(apiKey); + if (terminalOpt.isEmpty()) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Invalid API Key")); } + Terminal terminal = terminalOpt.get(); - // 2. Parse and Clean order numbers + // 2. Check if terminal is blocked + if (terminal.isBlocked()) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Map.of( + "status", "BLOCKED", + "message", "This terminal is out of order" + )); + } + + // 3. Parse and Clean order numbers List orderNumbers = parseAndCleanOrderNumbers(orderNumber); if (orderNumbers.isEmpty()) { return ResponseEntity.badRequest().body(Map.of("message", "orderNumber required")); } - // 3. Process all found orders + // 4. Process all found orders List successfulOrders = new ArrayList<>(); boolean alreadyCompleted = false; boolean archivedFound = false; @@ -277,7 +305,7 @@ public class TerminalController { Optional orderOpt = orderRepository.findByOrderNumber(orderNum); if (orderOpt.isPresent()) { Order order = orderOpt.get(); - + if (order.isArchived()) { archivedFound = true; continue; @@ -297,8 +325,8 @@ public class TerminalController { } if (!successfulOrders.isEmpty()) { - String msg = alreadyCompleted && successfulOrders.size() == 1 - ? "Order was already marked as delivered." + String msg = alreadyCompleted && successfulOrders.size() == 1 + ? "Order was already marked as delivered." : "Order(s) marked as delivered successfully: " + String.join(", ", successfulOrders); return ResponseEntity.ok(Map.of("success", true, "message", msg)); } @@ -317,7 +345,7 @@ public class TerminalController { @PathVariable("orderNumber") String orderNumber, @RequestHeader(value = "X-API-KEY", required = false) String apiKeyHeader, @RequestHeader(value = "Authorization", required = false) String authHeader) { - + if (orderNumber == null || orderNumber.isBlank()) { return ResponseEntity.badRequest().body(Map.of("message", "orderNumber required")); } @@ -326,20 +354,29 @@ public class TerminalController { if (apiKey == null) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Invalid or missing token")); } - + // 1. Verify API Key - Optional terminal = terminalRepository.findByApiKey(apiKey); - if (terminal.isEmpty()) { + Optional terminalOpt = terminalRepository.findByApiKey(apiKey); + if (terminalOpt.isEmpty()) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Invalid API Key")); } + Terminal terminal = terminalOpt.get(); - // 2. Parse and Clean order numbers + // 2. Check if terminal is blocked + if (terminal.isBlocked()) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Map.of( + "status", "BLOCKED", + "message", "This terminal is out of order" + )); + } + + // 3. Parse and Clean order numbers List orderNumbers = parseAndCleanOrderNumbers(orderNumber); if (orderNumbers.isEmpty()) { return ResponseEntity.badRequest().body(Map.of("message", "orderNumber required")); } - // 3. Process all found orders + // 4. Process all found orders List successfulOrders = new ArrayList<>(); boolean alreadyCompleted = false; boolean archivedFound = false; @@ -348,7 +385,7 @@ public class TerminalController { Optional orderOpt = orderRepository.findByOrderNumber(orderNum); if (orderOpt.isPresent()) { Order order = orderOpt.get(); - + if (order.isArchived()) { archivedFound = true; continue; @@ -368,8 +405,8 @@ public class TerminalController { } if (!successfulOrders.isEmpty()) { - String msg = alreadyCompleted && successfulOrders.size() == 1 - ? "Order was already marked as delivered." + String msg = alreadyCompleted && successfulOrders.size() == 1 + ? "Order was already marked as delivered." : "Order(s) marked as delivered successfully: " + String.join(", ", successfulOrders); return ResponseEntity.ok(Map.of("success", true, "message", msg)); } @@ -394,7 +431,8 @@ public class TerminalController { "status", "VALID", "terminalId", t.getId(), "name", t.getName(), - "location", t.getLocation() + "location", t.getLocation(), + "blocked", t.isBlocked() )); } @@ -493,6 +531,20 @@ public class TerminalController { } } + @PutMapping("/{id}/block") + public ResponseEntity toggleBlockStatus(@PathVariable Long id) { + boolean newStatus = terminalService.toggleBlockStatus(id); + if (terminalService.getTerminalById(id).isPresent()) { + return ResponseEntity.ok(Map.of( + "success", true, + "blocked", newStatus, + "message", newStatus ? "Terminal blocked" : "Terminal unblocked" + )); + } else { + return ResponseEntity.notFound().build(); + } + } + // ────────────────────────────────────────────────────────────── // Helpers // ────────────────────────────────────────────────────────────── diff --git a/backend/src/main/java/com/rit/canteen/sales/model/Terminal.java b/backend/src/main/java/com/rit/canteen/sales/model/Terminal.java index e7fc7786..b32ddfcb 100644 --- a/backend/src/main/java/com/rit/canteen/sales/model/Terminal.java +++ b/backend/src/main/java/com/rit/canteen/sales/model/Terminal.java @@ -30,6 +30,9 @@ public class Terminal { @Column(nullable = false, columnDefinition = "boolean default false") private boolean paired = false; + @Column(nullable = false, columnDefinition = "boolean default false") + private boolean blocked = false; + @Column(name = "paired_at") private LocalDateTime pairedAt; @@ -64,6 +67,9 @@ public class Terminal { public boolean isPaired() { return paired; } public void setPaired(boolean paired) { this.paired = paired; } + public boolean isBlocked() { return blocked; } + public void setBlocked(boolean blocked) { this.blocked = blocked; } + public LocalDateTime getPairedAt() { return pairedAt; } public void setPairedAt(LocalDateTime pairedAt) { this.pairedAt = pairedAt; } } diff --git a/backend/src/main/java/com/rit/canteen/sales/model/TerminalDTO.java b/backend/src/main/java/com/rit/canteen/sales/model/TerminalDTO.java index 925494a7..8b8d86d4 100644 --- a/backend/src/main/java/com/rit/canteen/sales/model/TerminalDTO.java +++ b/backend/src/main/java/com/rit/canteen/sales/model/TerminalDTO.java @@ -9,19 +9,21 @@ public class TerminalDTO { private String apiKey; private String pin; private boolean paired; + private boolean blocked; private String deviceId; private LocalDateTime pairedAt; public TerminalDTO() {} public TerminalDTO(Long id, String name, String location, String apiKey, String pin, - boolean paired, String deviceId, LocalDateTime pairedAt) { + boolean paired, boolean blocked, String deviceId, LocalDateTime pairedAt) { this.id = id; this.name = name; this.location = location; this.apiKey = apiKey; this.pin = pin; this.paired = paired; + this.blocked = blocked; this.deviceId = deviceId; this.pairedAt = pairedAt; } @@ -45,6 +47,9 @@ public class TerminalDTO { public boolean isPaired() { return paired; } public void setPaired(boolean paired) { this.paired = paired; } + public boolean isBlocked() { return blocked; } + public void setBlocked(boolean blocked) { this.blocked = blocked; } + public String getDeviceId() { return deviceId; } public void setDeviceId(String deviceId) { this.deviceId = deviceId; } diff --git a/backend/src/main/java/com/rit/canteen/sales/service/TerminalService.java b/backend/src/main/java/com/rit/canteen/sales/service/TerminalService.java index 445d2b37..197a6b27 100644 --- a/backend/src/main/java/com/rit/canteen/sales/service/TerminalService.java +++ b/backend/src/main/java/com/rit/canteen/sales/service/TerminalService.java @@ -44,6 +44,15 @@ public class TerminalService { } } + public boolean toggleBlockStatus(Long id) { + if (id == null) return false; + return terminalRepository.findById(id).map(terminal -> { + terminal.setBlocked(!terminal.isBlocked()); + terminalRepository.save(terminal); + return terminal.isBlocked(); + }).orElse(false); + } + public Terminal updateTerminal(Long id, Terminal details) { if (id == null) return null; return terminalRepository.findById(id).map(existing -> { diff --git a/counter-frontend/package-lock.json b/counter-frontend/package-lock.json index 0bc55158..e64bf595 100644 --- a/counter-frontend/package-lock.json +++ b/counter-frontend/package-lock.json @@ -12,6 +12,7 @@ "date-fns": "^4.1.0", "framer-motion": "^12.38.0", "lucide-react": "^1.7.0", + "qrcode.react": "^4.2.0", "react": "^19.2.4", "react-dom": "^19.2.4", "react-router-dom": "^7.14.0", @@ -51,13 +52,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -66,9 +67,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -76,21 +77,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -107,14 +108,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -124,14 +125,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -141,9 +142,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -151,29 +152,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -183,9 +184,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -193,9 +194,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -203,9 +204,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -213,27 +214,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -243,33 +244,33 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -277,23 +278,23 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dev": true, "license": "MIT", "optional": true, @@ -303,9 +304,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "dev": true, "license": "MIT", "optional": true, @@ -584,14 +585,14 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", - "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.2" }, "funding": { "type": "github", @@ -603,9 +604,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.124.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", - "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", "dev": true, "license": "MIT", "funding": { @@ -613,9 +614,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", - "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", "cpu": [ "arm64" ], @@ -630,9 +631,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", - "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", "cpu": [ "arm64" ], @@ -647,9 +648,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", - "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", "cpu": [ "x64" ], @@ -664,9 +665,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", - "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", "cpu": [ "x64" ], @@ -681,9 +682,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", - "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", "cpu": [ "arm" ], @@ -698,9 +699,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", "cpu": [ "arm64" ], @@ -718,9 +719,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", - "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", "cpu": [ "arm64" ], @@ -738,9 +739,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", "cpu": [ "ppc64" ], @@ -758,9 +759,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", "cpu": [ "s390x" ], @@ -778,9 +779,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", "cpu": [ "x64" ], @@ -798,9 +799,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", - "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", "cpu": [ "x64" ], @@ -818,9 +819,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", - "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", "cpu": [ "arm64" ], @@ -835,9 +836,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", - "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", "cpu": [ "wasm32" ], @@ -845,18 +846,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.9.2", - "@emnapi/runtime": "1.9.2", - "@napi-rs/wasm-runtime": "^1.1.3" + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" }, "engines": { - "node": ">=14.0.0" + "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", - "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", "cpu": [ "arm64" ], @@ -871,9 +872,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", - "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", "cpu": [ "x64" ], @@ -1193,9 +1194,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "dev": true, "license": "MIT", "optional": true, @@ -1446,9 +1447,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -2433,10 +2434,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -2877,9 +2888,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "dev": true, "funding": [ { @@ -3013,9 +3024,9 @@ } }, "node_modules/postcss": { - "version": "8.5.9", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", - "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -3033,7 +3044,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -3068,6 +3079,15 @@ "node": ">=6" } }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react": { "version": "19.2.5", "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", @@ -3090,9 +3110,9 @@ } }, "node_modules/react-router": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.1.tgz", - "integrity": "sha512-5BCvFskyAAVumqhEKh/iPhLOIkfxcEUz8WqFIARCkMg8hZZzDYX9CtwxXA0e+qT8zAxmMC0x3Ckb9iMONwc5jg==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz", + "integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -3112,12 +3132,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.1.tgz", - "integrity": "sha512-ZkrQuwwhGibjQLqH1eCdyiZyLWglPxzxdl5tgwgKEyCSGC76vmAjleGocRe3J/MLfzMUIKwaFJWpFVJhK3d2xA==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.0.tgz", + "integrity": "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==", "license": "MIT", "dependencies": { - "react-router": "7.14.1" + "react-router": "7.18.0" }, "engines": { "node": ">=20.0.0" @@ -3138,14 +3158,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", - "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.124.0", - "@rolldown/pluginutils": "1.0.0-rc.15" + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" @@ -3154,27 +3174,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.15", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", - "@rolldown/binding-darwin-x64": "1.0.0-rc.15", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" } }, "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", - "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT" }, @@ -3291,9 +3311,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -3426,17 +3446,17 @@ } }, "node_modules/vite": { - "version": "8.0.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", - "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.15", - "tinyglobby": "^0.2.15" + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -3452,7 +3472,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.0", + "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", diff --git a/counter-frontend/package.json b/counter-frontend/package.json index b1671b17..82b075fe 100644 --- a/counter-frontend/package.json +++ b/counter-frontend/package.json @@ -14,6 +14,7 @@ "date-fns": "^4.1.0", "framer-motion": "^12.38.0", "lucide-react": "^1.7.0", + "qrcode.react": "^4.2.0", "react": "^19.2.4", "react-dom": "^19.2.4", "react-router-dom": "^7.14.0", diff --git a/counter-frontend/src/pages/Categories.tsx b/counter-frontend/src/pages/Categories.tsx index c34fb891..d725bd4a 100644 --- a/counter-frontend/src/pages/Categories.tsx +++ b/counter-frontend/src/pages/Categories.tsx @@ -11,6 +11,23 @@ import { } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; +// Local authenticated fetch wrapper +const apiFetch = async (url: string, options: RequestInit = {}) => { + const token = sessionStorage.getItem('counterToken'); + const headers = { + 'Content-Type': 'application/json', + ...(options.headers || {}), + ...(token ? { 'Authorization': `Bearer ${token}` } : {}) + }; + const response = await fetch(url, { ...options, headers }); + if (response.status === 401 || response.status === 403) { + sessionStorage.removeItem('isCounterLoggedIn'); + sessionStorage.removeItem('counterToken'); + window.location.href = '/login'; + } + return response; +}; + const Categories = () => { const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); @@ -22,7 +39,7 @@ const Categories = () => { setLoading(true); try { const host = window.location.hostname; - const response = await fetch(`http://${host}:8080/api/base-items`); + const response = await apiFetch(`http://${host}:8080/api/base-items`); const data = await response.json(); setCategories(Array.isArray(data) ? data : (data.content || [])); } catch (error) { @@ -46,7 +63,7 @@ const Categories = () => { const method = currentCategory?.id ? 'PUT' : 'POST'; try { - const response = await fetch(url, { + const response = await apiFetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(currentCategory) diff --git a/counter-frontend/src/pages/Login.tsx b/counter-frontend/src/pages/Login.tsx index 76bf71df..5b16edda 100644 --- a/counter-frontend/src/pages/Login.tsx +++ b/counter-frontend/src/pages/Login.tsx @@ -22,15 +22,38 @@ const Login = () => { sessionStorage.removeItem('counterUserName'); }, []); - const handleLogin = (e: React.FormEvent) => { + const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); - if (username === 'krishna' && password === '12345678') { - sessionStorage.setItem('isCounterLoggedIn', 'true'); - sessionStorage.setItem('counterUserName', 'Krishna'); - navigate('/pos'); - } else { - alert('Invalid credentials'); + try { + const host = window.location.hostname; + const response = await fetch(`http://${host}:8080/api/system/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: username, password }) + }); + const data = await response.json(); + if (response.ok && data.token && (data.role === 'MANAGER' || data.role === 'MASTER')) { + sessionStorage.setItem('isCounterLoggedIn', 'true'); + sessionStorage.setItem('counterUserName', data.name); + sessionStorage.setItem('counterToken', data.token); + navigate('/pos'); + } else if (username === 'krishna' && password === '12345678') { + sessionStorage.setItem('isCounterLoggedIn', 'true'); + sessionStorage.setItem('counterUserName', 'Krishna'); + navigate('/pos'); + } else { + alert(data.error || 'Invalid credentials or access denied'); + } + } catch (err) { + if (username === 'krishna' && password === '12345678') { + sessionStorage.setItem('isCounterLoggedIn', 'true'); + sessionStorage.setItem('counterUserName', 'Krishna'); + navigate('/pos'); + } else { + console.error(err); + alert('Connection error or invalid credentials'); + } } }; diff --git a/counter-frontend/src/pages/Orders.tsx b/counter-frontend/src/pages/Orders.tsx index a7aff2bf..04b42667 100644 --- a/counter-frontend/src/pages/Orders.tsx +++ b/counter-frontend/src/pages/Orders.tsx @@ -1,249 +1,953 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { Search, Calendar, - RefreshCw, - ShoppingBag, + Filter, CreditCard, User, - CheckCircle2, - Clock, - ChevronRight + ShoppingBag, + MoreVertical, + RefreshCw, + Trash2, + Plus, + Minus, + Check, + X as XIcon, + RotateCcw, + Edit2, + Printer } from 'lucide-react'; -import { motion } from 'framer-motion'; +import { format } from 'date-fns'; +import { QRCodeCanvas } from 'qrcode.react'; +import Pagination from '../components/Pagination'; -const MOCK_ORDERS = [ - { id: 13, displayId: '#13', date: '2026-04-08', amount: 320, status: 'PAID', items: 3 }, - { id: 12, displayId: '#12', date: '2026-04-08', amount: 1000, status: 'PAID', items: 1 }, - { id: 11, displayId: '#11', date: '2026-04-08', amount: 60, status: 'PAID', items: 2 }, - { id: 10, displayId: '#10', date: '2026-04-08', amount: 50, status: 'PAID', items: 1 }, - { id: 9, displayId: '#9', date: '2026-04-08', amount: 50, status: 'PAID', items: 1 }, - { id: 8, displayId: '#8', date: '2026-04-08', amount: 20, status: 'PAID', items: 1 }, -]; +interface OrderItem { + id?: number; + productName: string; + quantity: number; + price: number; + productId: number; +} -const Orders = () => { - const [orders, setOrders] = useState([]); +interface Product { + id: number; + name: string; + price: number; + category: string; +} + +interface UserData { + name: string; + mobileNumber: string; +} + +interface Order { + id: number; + orderNumber: string; + displayOrderId: string; + totalAmount: number; + status: string; + paymentMethod: string; + createdAt: string; + user: UserData; + items: OrderItem[]; +} + +// Local authenticated fetch wrapper +const apiFetch = async (url: string, options: RequestInit = {}) => { + const token = sessionStorage.getItem('counterToken'); + const headers = { + 'Content-Type': 'application/json', + ...(options.headers || {}), + ...(token ? { 'Authorization': `Bearer ${token}` } : {}) + }; + const response = await fetch(url, { ...options, headers }); + if (response.status === 401 || response.status === 403) { + sessionStorage.removeItem('isCounterLoggedIn'); + sessionStorage.removeItem('counterToken'); + window.location.href = '/login'; + } + return response; +}; + +const Orders: React.FC = () => { + const [orders, setOrders] = useState([]); + const [selectedOrder, setSelectedOrder] = useState(null); const [loading, setLoading] = useState(true); - const [selectedOrder, setSelectedOrder] = useState(null); - const [activeTab, setActiveTab] = useState('ALL'); - const [searchTerm, setSearchTerm] = useState(''); + const [searchQuery, setSearchQuery] = useState(''); + const [statusFilter, setStatusFilter] = useState(''); + const [paymentFilter, setPaymentFilter] = useState(''); + const [startDate, setStartDate] = useState(format(new Date(), 'yyyy-MM-dd')); + const [endDate, setEndDate] = useState(format(new Date(), 'yyyy-MM-dd')); + + // Pagination States + const [currentPage, setCurrentPage] = useState(0); + const [pageSize, setPageSize] = useState(10); + const [totalElements, setTotalElements] = useState(0); + const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(''); + + // Action Menu & Edit Modal States + const [showActionMenu, setShowActionMenu] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [allProducts, setAllProducts] = useState([]); + const [editSearchQuery, setEditSearchQuery] = useState(''); + const [editingItems, setEditingItems] = useState([]); + const [isUpdatingOrder, setIsUpdatingOrder] = useState(false); + const [showQRMenu, setShowQRMenu] = useState(false); + const [isRegenerating, setIsRegenerating] = useState(false); + + // Debounce search query + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearchQuery(searchQuery); + }, 500); + return () => clearTimeout(timer); + }, [searchQuery]); + + // Reset to first page when search changes + useEffect(() => { + setCurrentPage(0); + }, [debouncedSearchQuery]); + + useEffect(() => { + fetchOrders(); + }, [startDate, endDate, statusFilter, paymentFilter, currentPage, pageSize, debouncedSearchQuery]); + + useEffect(() => { + fetchProducts(); + }, []); + + const fetchProducts = async () => { + try { + const response = await apiFetch(`http://${window.location.hostname}:8080/api/products?size=1000`); + if (response.ok) { + const data = await response.json(); + setAllProducts(Array.isArray(data) ? data : (data.content || [])); + } + } catch (error) { + console.error('Error fetching products:', error); + } + }; const fetchOrders = async () => { setLoading(true); try { - const host = window.location.hostname; - const paymentType = activeTab === 'ALL' ? '' : activeTab; - - let url = `http://${host}:8080/api/orders/all?archived=false&paymentType=${paymentType}`; - - if (searchTerm) { - url += `&search=${encodeURIComponent(searchTerm)}`; - } + // Build query params + const params = new URLSearchParams(); + params.append('archived', 'false'); // Only show live orders + if (startDate) params.append('startDate', `${startDate}T00:00:00`); + if (endDate) params.append('endDate', `${endDate}T23:59:59`); + if (statusFilter) params.append('status', statusFilter); + if (paymentFilter) params.append('paymentType', paymentFilter); + if (debouncedSearchQuery) params.append('search', debouncedSearchQuery); + params.append('page', currentPage.toString()); + params.append('size', pageSize.toString()); - const response = await fetch(url); + const response = await apiFetch(`http://${window.location.hostname}:8080/api/orders/all?${params.toString()}`); const data = await response.json(); - const orderList = Array.isArray(data) ? data : (data.content || []); - setOrders(orderList); - - if (orderList.length > 0 && !selectedOrder) { - setSelectedOrder(orderList[0]); + if (data && data.content && Array.isArray(data.content)) { + setOrders(data.content); + setTotalElements(data.totalElements); + + // If we have a selected order, try to refresh it from the data list + if (selectedOrder) { + const updated = data.content.find((o: Order) => o.id === selectedOrder.id); + if (updated) setSelectedOrder(updated); + } else if (data.content.length > 0) { + setSelectedOrder(data.content[0]); + } + } else { + console.error('API did not return a valid Page object:', data); + setOrders([]); + setTotalElements(0); } } catch (error) { console.error('Error fetching orders:', error); + setOrders([]); + setTotalElements(0); } finally { setLoading(false); } }; - React.useEffect(() => { + const handleApproveOrder = async (orderId: number) => { + try { + const response = await apiFetch(`http://${window.location.hostname}:8080/api/orders/${orderId}/status`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: 'COMPLETED' }) + }); + + if (response.ok) { + fetchOrders(); + } else { + alert('Failed to update order status'); + } + } catch (error) { + console.error('Error updating order:', error); + } + }; + + const handleMarkUndelivered = async (orderId: number) => { + try { + const response = await apiFetch(`http://${window.location.hostname}:8080/api/orders/${orderId}/status`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: 'PAID' }) + }); + + if (response.ok) { + setShowActionMenu(false); + fetchOrders(); + } + } catch (error) { + console.error('Error marking undelivered:', error); + } + }; + + const handleEditOrder = () => { + if (!selectedOrder) return; + setEditingItems([...selectedOrder.items]); + setIsEditModalOpen(true); + setShowActionMenu(false); + }; + + const updateItemQuantity = (index: number, delta: number) => { + const newItems = [...editingItems]; + newItems[index].quantity = Math.max(1, newItems[index].quantity + delta); + setEditingItems(newItems); + }; + + const removeItem = (index: number) => { + setEditingItems(editingItems.filter((_, i) => i !== index)); + }; + + const addProductToOrder = (product: Product) => { + // Check if product already in list + const existing = editingItems.findIndex(item => item.productId === product.id); + if (existing !== -1) { + updateItemQuantity(existing, 1); + } else { + setEditingItems([...editingItems, { + productId: product.id, + productName: product.name, + price: product.price, + quantity: 1 + }]); + } + setEditSearchQuery(''); + }; + + const editTotal = useMemo(() => { + return editingItems.reduce((sum, item) => sum + (item.price * item.quantity), 0); + }, [editingItems]); + + const saveOrderEdits = async () => { + if (!selectedOrder) return; + setIsUpdatingOrder(true); + try { + const response = await apiFetch(`http://${window.location.hostname}:8080/api/orders/${selectedOrder.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...selectedOrder, + items: editingItems, + totalAmount: editTotal + }) + }); + + if (response.ok) { + setIsEditModalOpen(false); + fetchOrders(); + } else { + alert('Failed to update order details'); + } + } catch (error) { + console.error('Error saving edits:', error); + } finally { + setIsUpdatingOrder(false); + } + }; + + const handlePrintQR = () => { + const canvas = document.querySelector('.qr-canvas canvas') as HTMLCanvasElement; + if (!canvas || !selectedOrder) return; + + const qrDataURL = canvas.toDataURL('image/png'); + const printWindow = window.open('', '_blank'); + if (printWindow) { + printWindow.document.write(` + + + Print QR Code - Order #${selectedOrder.displayOrderId} + + + + +
+

Order #${selectedOrder.displayOrderId}

+

Scan to verify at terminal

+

Customer: ${selectedOrder.user?.name || 'Walk-in'}

+
+ + + + `); + printWindow.document.close(); + } + }; + + const handleRegenerateQR = async () => { + if (!selectedOrder) return; + setIsRegenerating(true); + try { + const newOrderNumber = `ORD-${Math.random().toString(36).substring(2, 10).toUpperCase()}`; + const response = await apiFetch(`http://${window.location.hostname}:8080/api/orders/${selectedOrder.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...selectedOrder, + orderNumber: newOrderNumber + }) + }); + + if (response.ok) { + fetchOrders(); // This will refresh the whole list and the selected order + setShowQRMenu(false); + } else { + alert('Failed to regenerate sync ID'); + } + } catch (error) { + console.error('Error regenerating QR:', error); + } finally { + setIsRegenerating(false); + } + }; + + const filteredProducts = useMemo(() => { + if (!editSearchQuery.trim()) return []; + return allProducts.filter(p => + p.name.toLowerCase().includes(editSearchQuery.toLowerCase()) || + p.category.toLowerCase().includes(editSearchQuery.toLowerCase()) + ).slice(0, 5); // Limit results + }, [allProducts, editSearchQuery]); + + const handleSearchSubmit = (e: React.FormEvent) => { + e.preventDefault(); fetchOrders(); - }, [activeTab, searchTerm]); + }; + + const getStatusColor = (status: string) => { + switch (status.toUpperCase()) { + case 'COMPLETED': return 'bg-emerald-50 text-emerald-600 border-emerald-100'; + case 'PAID': return 'bg-indigo-50 text-indigo-600 border-indigo-100'; + case 'PENDING': return 'bg-amber-50 text-amber-600 border-amber-100'; + case 'CANCELLED': return 'bg-rose-50 text-rose-600 border-rose-100'; + default: return 'bg-slate-50 text-slate-600 border-slate-100'; + } + }; return ( -
- {/* Sub Header / Tabs */} -
-
- {['ALL', 'CASH', 'UPI', 'CARD'].map((tab) => ( - + + +
- {/* Main Content */} -
- {/* Left Sidebar: Order List */} -
-
-

Order Feed

- -
- -
- {loading ? ( -
-
-
- ) : orders.length > 0 ? ( - orders.map((order) => ( - setSelectedOrder(order)} - className={`p-6 rounded-[2rem] border-2 transition-all cursor-pointer group ${ - selectedOrder?.id === order.id - ? 'bg-primary/5 border-primary shadow-xl shadow-primary/5 scale-[1.02]' - : 'bg-white border-slate-100/50 hover:border-primary/20 hover:shadow-lg' - }`} - > -
-
-

#{order.displayOrderId || order.id}

-

{new Date(order.createdAt).toLocaleTimeString()}

-
- ₹{order.totalAmount} + {/* Main Content Area */} +
+ {/* Left Column: Orders List */} +
+ {loading ? ( +
+ + Loading orders... +
+ ) : orders.length === 0 ? ( +
+ +

No orders found

+

Try adjusting your filters

+
+ ) : ( + orders.map((order) => ( +
setSelectedOrder(order)} + className={`group relative bg-white border rounded-xl p-4 cursor-pointer transition-all hover:shadow-lg active:scale-[0.98] ${ + selectedOrder?.id === order.id + ? 'border-primary ring-1 ring-primary shadow-md' + : 'border-slate-200 hover:border-primary/50' + }`} + > +
+
+ {order.status}
-
- - {order.status} - -
- -
+
+
Grand Total
+
₹{order.totalAmount.toFixed(2)}
- - )) - ) : ( -
- -

No orders found in {activeTab}

-
- )} -
-
+
- {/* Right Section: Order Detail */} -
- {selectedOrder ? ( - -
-
-
- -
-
-
-

Order #{selectedOrder.displayOrderId || selectedOrder.id}

- {selectedOrder.status} -
-

Reference ID: {selectedOrder.orderNumber}

+
+
+
Order #
+
#{order.displayOrderId || order.id}
+
+ + {order.paymentMethod}
-

Created At

-

{new Date(selectedOrder.createdAt).toLocaleString()}

+
+ {order.user?.name || 'Anonymous User'} +
+
+ {format(new Date(order.createdAt), 'dd/MM/yy HH:mm')} +
+
+ + {order.user?.name ? 'REGISTERED' : 'WALK-IN'} +
-
+
+ + {selectedOrder?.id === order.id && ( +
+ )} +
+ )) + )} + + { + setPageSize(newSize); + setCurrentPage(0); + }} + /> +
-
-
-

Payment Type

-

- {selectedOrder.paymentMethod} -

+ {/* Right Column: Order Details */} +
+ {selectedOrder ? ( + <> + {/* Order Detail Header */} +
+
+
+
-
-

Customer

-

- {selectedOrder.user?.name || 'Walk-in Customer'} -

+
+

+ Active Order #{selectedOrder.displayOrderId || selectedOrder.id} +

+

Session ID: {selectedOrder.orderNumber}

-
-

Order Type

-

- {selectedOrder.orderType.replace('_', ' ')} -

+
+
+
+
Creation Time
+
{format(new Date(selectedOrder.createdAt), 'HH:mm:ss')}
-
+
+ {selectedOrder.status} +
+
+
-
-
-

Item List

- {selectedOrder.items?.length || 0} ITEMS + {/* Tabs Container */} +
+ {['Orders', 'Payment Information', 'Customer Details'].map((tab, idx) => ( + + ))} +
+ + {/* Content Scroll Area */} +
+
+
+
+ +
+
+
Customer Info
+
{selectedOrder.user?.name || 'Walk-in Customer'}
+
{selectedOrder.user?.mobileNumber || 'No Contact Data'}
+
+
+
+
+ +
+
+
Payment Method
+
{selectedOrder.paymentMethod.toUpperCase()}
+
Secure Sync ID: {selectedOrder.orderNumber.split('-')[1] || '000'}
+
+ + {/* Interactive Controls Overlay for active orders */} +
+ {selectedOrder.status.toUpperCase() !== 'COMPLETED' && selectedOrder.status.toUpperCase() !== 'DELIVERED' && ( + + )} +
+ + {showActionMenu && ( +
+ +
+ )} +
+
+
+
+ +
+
+

Items

+

Price

-
- {selectedOrder.items?.map((item: any, idx: number) => ( -
-
-
- {item.productName?.substring(0, 2).toUpperCase()} -
-
-

{item.productName}

-

{item.stallName}

-
+ +
+ {selectedOrder.items.map((item, idx) => ( +
+
+
+ {item.productName.substring(0, 2)}
+
+
{item.productName}
+
Standard Portion
+
+
+
-

₹{item.price * item.quantity}

-

{item.quantity}x @ ₹{item.price}

+
₹{(item.price * item.quantity).toLocaleString()}
+
+ {item.quantity} x ₹{item.price} +
- ))} +
+ ))}
-
+
+
-
-
-
-
- Subtotal - ₹{selectedOrder.totalAmount} + {/* Bottom Summary Panel */} +
+
+ +
+
+
+
Unique Items
+
{selectedOrder.items.length} Items
+
+ + {/* QR Verification Section */} +
+
+
setShowQRMenu(!showQRMenu)} + className="p-1.5 bg-white rounded-lg shadow-sm transition-all hover:scale-105 hover:ring-2 hover:ring-white/50 cursor-pointer qr-canvas relative group" + > + +
+ +
+
+ + {showQRMenu && ( +
+
Sync Actions
+ + +
+ )}
-
-
- Order Total - ₹{selectedOrder.totalAmount} +
+
Verification
+
Manual Sync ID
-
-
-
- +
+
+ +
+
Active Grand Total
+
₹{selectedOrder.totalAmount.toLocaleString()}
+
{selectedOrder.status === 'COMPLETED' ? 'Transaction Completed' : 'Transaction Pending Approval'}
+
+
+ +
+ + +
+
+ ) : ( -
-
- -
-

Select an order to view details

+
+
+ +
+

Select an order to view details

+

Pick an order from the list on the left to see full transaction history, items, and customer info.

)}
-
); }; diff --git a/counter-frontend/src/pages/Products.tsx b/counter-frontend/src/pages/Products.tsx index 4269cfbf..3b974e49 100644 --- a/counter-frontend/src/pages/Products.tsx +++ b/counter-frontend/src/pages/Products.tsx @@ -84,6 +84,23 @@ const emptyProduct: Product = { stock: 0 }; +// Local authenticated fetch wrapper +const apiFetch = async (url: string, options: RequestInit = {}) => { + const token = sessionStorage.getItem('counterToken'); + const headers = { + 'Content-Type': 'application/json', + ...(options.headers || {}), + ...(token ? { 'Authorization': `Bearer ${token}` } : {}) + }; + const response = await fetch(url, { ...options, headers }); + if (response.status === 401 || response.status === 403) { + sessionStorage.removeItem('isCounterLoggedIn'); + sessionStorage.removeItem('counterToken'); + window.location.href = '/login'; + } + return response; +}; + const Products = () => { const [products, setProducts] = useState([]); const [categories, setCategories] = useState([]); @@ -92,6 +109,7 @@ const Products = () => { const [editingProduct, setEditingProduct] = useState(null); const [formData, setFormData] = useState(emptyProduct); const [searchTerm, setSearchTerm] = useState(''); + const [debouncedSearch, setDebouncedSearch] = useState(''); const [currentPage, setCurrentPage] = useState(0); const [pageSize, setPageSize] = useState(10); const [totalElements, setTotalElements] = useState(0); @@ -106,16 +124,28 @@ const Products = () => { } }, []); + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearch(searchTerm); + setCurrentPage(0); // Reset to page 0 on search + }, 500); + return () => clearTimeout(timer); + }, [searchTerm]); + useEffect(() => { fetchProducts(); + }, [currentPage, pageSize, debouncedSearch]); + + useEffect(() => { fetchCategories(); - }, [currentPage, pageSize]); + }, []); const fetchProducts = async () => { setLoading(true); try { const host = window.location.hostname; - const response = await fetch(`http://${host}:8080/api/products?page=${currentPage}&size=${pageSize}`); + const searchParam = debouncedSearch ? `&search=${encodeURIComponent(debouncedSearch)}` : ''; + const response = await apiFetch(`http://${host}:8080/api/products?page=${currentPage}&size=${pageSize}${searchParam}`); const data = await response.json(); if (data && data.content) { @@ -135,7 +165,7 @@ const Products = () => { const fetchCategories = async () => { try { const host = window.location.hostname; - const response = await fetch(`http://${host}:8080/api/base-items`); + const response = await apiFetch(`http://${host}:8080/api/base-items`); const data = await response.json(); setCategories(Array.isArray(data) ? data : (data.content || [])); } catch (error) { @@ -152,7 +182,7 @@ const Products = () => { const method = editingProduct?.id ? 'PUT' : 'POST'; try { - const response = await fetch(url, { + const response = await apiFetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData), @@ -170,7 +200,7 @@ const Products = () => { if (!window.confirm(`Delete ${product.name}?`)) return; try { const host = window.location.hostname; - const response = await fetch(`http://${host}:8080/api/products/${product.id}`, { method: 'DELETE' }); + const response = await apiFetch(`http://${host}:8080/api/products/${product.id}`, { method: 'DELETE' }); if (response.ok) fetchProducts(); } catch (error) { console.error('Error deleting:', error); @@ -180,7 +210,7 @@ const Products = () => { const handleToggleStock = async (product: Product) => { try { const host = window.location.hostname; - const response = await fetch(`http://${host}:8080/api/products/${product.id}/toggle-stock`, { method: 'PATCH' }); + const response = await apiFetch(`http://${host}:8080/api/products/${product.id}/toggle-stock`, { method: 'PATCH' }); if (response.ok) fetchProducts(); } catch (error) { console.error('Error toggling stock:', error); @@ -196,10 +226,7 @@ const Products = () => { } }; - const filteredProducts = products.filter(p => - (p.name || '').toLowerCase().includes(searchTerm.toLowerCase()) || - (p.category || '').toLowerCase().includes(searchTerm.toLowerCase()) - ); + const filteredProducts = products; return (
diff --git a/frontend/src/pages/Orders.tsx b/frontend/src/pages/Orders.tsx index a4c0d52e..a07be2eb 100644 --- a/frontend/src/pages/Orders.tsx +++ b/frontend/src/pages/Orders.tsx @@ -1,4 +1,4 @@ -import { apiFetch } from '../api'; +import { apiFetch } from '../api'; import React, { useState, useEffect, useMemo } from 'react'; import { Search, @@ -104,10 +104,10 @@ const Orders: React.FC = () => { const fetchProducts = async () => { try { - const response = await apiFetch(`http://${window.location.hostname}:8080/api/products`); + const response = await apiFetch(`http://${window.location.hostname}:8080/api/products?size=1000`); if (response.ok) { const data = await response.json(); - setAllProducts(data); + setAllProducts(Array.isArray(data) ? data : (data.content || [])); } } catch (error) { console.error('Error fetching products:', error); @@ -580,13 +580,15 @@ const Orders: React.FC = () => { {/* Interactive Controls Overlay for active orders */}
- + {selectedOrder.status.toUpperCase() !== 'COMPLETED' && selectedOrder.status.toUpperCase() !== 'DELIVERED' && ( + + )}
@@ -317,7 +339,7 @@ const Settings = () => {
- {admin.role} ACCESS + {admin.role || 'MASTER'} ACCESS
@@ -337,10 +359,21 @@ const Settings = () => {
-

New Administrator

-

Grant full system access to a team member

+

New Team Member

+

Grant system access to a team member

+
+ + +
{ />
-
- -

- CRITICAL: This user will have full MASTER privileges. They can manage sales, tokens, and other administrators. -

-
+ {newAdmin.role === 'MASTER' ? ( +
+ +

+ CRITICAL: This user will have full MASTER privileges. They can manage sales, tokens, and other administrators. +

+
+ ) : ( +
+ +

+ INFO: This user will have MANAGER privileges. They will have access to the Counter Dashboard and other management tasks. +

+
+ )}
diff --git a/frontend/src/pages/Terminals.tsx b/frontend/src/pages/Terminals.tsx index d03a9c56..2ff03bed 100644 --- a/frontend/src/pages/Terminals.tsx +++ b/frontend/src/pages/Terminals.tsx @@ -2,11 +2,11 @@ import { apiFetch } from '../api'; import React, { useState, useEffect } from 'react'; import { motion } from 'framer-motion'; import { - Monitor, - Plus, - MapPin, - ShieldCheck, - Trash2, + Monitor, + Plus, + MapPin, + ShieldCheck, + Trash2, Pencil, Search, ExternalLink, @@ -16,7 +16,8 @@ import { WifiOff, LinkIcon, Unlink, - Smartphone + Smartphone, + Ban } from 'lucide-react'; import AddTerminalModal from '../components/AddTerminalModal.tsx'; import PinVerificationModal from '../components/PinVerificationModal.tsx'; @@ -29,6 +30,7 @@ interface Terminal { location: string; apiKey: string; paired: boolean; + blocked: boolean; deviceId: string | null; pairedAt: string | null; } @@ -96,6 +98,24 @@ const Terminals = () => { } }; + const handleBlock = async (id: number, currentStatus: boolean) => { + const action = currentStatus ? 'unblock' : 'block'; + if (window.confirm(`Are you sure you want to ${action} this terminal?`)) { + try { + const response = await apiFetch(`/api/terminals/${id}/block`, { method: 'PUT' }); + if (response.ok) { + fetchTerminals(); + } else { + const data = await response.json().catch(() => ({})); + alert(data.message || `Failed to ${action} terminal (HTTP ${response.status})`); + } + } catch (error) { + console.error(`Failed to ${action} terminal:`, error); + alert(`Network error: Could not ${action} terminal`); + } + } + }; + const pairedCount = terminals.filter(t => t.paired).length; const filteredTerminals = terminals.filter(t => { @@ -204,6 +224,20 @@ const Terminals = () => {
{/* Action buttons — completely separate from card click */}
+ {terminal.paired && ( + + )} {terminal.paired && (