Search bugs are fixed. Added Global searching parameter

This commit is contained in:
Sidharth Prabhu
2026-06-23 10:47:15 +05:30
parent 217f523059
commit a914fd44cc
16 changed files with 1520 additions and 507 deletions

View File

@@ -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<String> 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"));

View File

@@ -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();

View File

@@ -43,12 +43,13 @@ public class TerminalController {
public List<TerminalDTO> 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> terminal = terminalRepository.findByApiKey(apiKey);
if (terminal.isEmpty()) {
Optional<Terminal> 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<String> 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<Order> 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> terminal = terminalRepository.findByApiKey(apiKey);
if (terminal.isEmpty()) {
Optional<Terminal> 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<String> 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<Order> 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<String, String> 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> terminal = terminalRepository.findByApiKey(apiKey);
if (terminal.isEmpty()) {
Optional<Terminal> 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<String> 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<String> successfulOrders = new ArrayList<>();
boolean alreadyCompleted = false;
boolean archivedFound = false;
@@ -277,7 +305,7 @@ public class TerminalController {
Optional<Order> 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> terminal = terminalRepository.findByApiKey(apiKey);
if (terminal.isEmpty()) {
Optional<Terminal> 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<String> 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<String> successfulOrders = new ArrayList<>();
boolean alreadyCompleted = false;
boolean archivedFound = false;
@@ -348,7 +385,7 @@ public class TerminalController {
Optional<Order> 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
// ──────────────────────────────────────────────────────────────

View File

@@ -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; }
}

View File

@@ -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; }

View File

@@ -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 -> {

View File

@@ -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",

View File

@@ -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",

View File

@@ -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<any[]>([]);
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)

View File

@@ -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');
}
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -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<Product[]>([]);
const [categories, setCategories] = useState<any[]>([]);
@@ -92,6 +109,7 @@ const Products = () => {
const [editingProduct, setEditingProduct] = useState<Product | null>(null);
const [formData, setFormData] = useState<Product>(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 (
<div className="h-full flex flex-col bg-slate-50 overflow-hidden font-inter">

View File

@@ -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 */}
<div className="absolute right-6 top-1/2 -translate-y-1/2 flex items-center gap-2">
<button
onClick={handleEditOrder}
className="p-2 bg-white rounded-lg border border-slate-200 text-indigo-500 shadow-sm hover:border-indigo-400 transition-all active:scale-95"
title="Edit Order"
>
<Edit2 size={16} />
</button>
{selectedOrder.status.toUpperCase() !== 'COMPLETED' && selectedOrder.status.toUpperCase() !== 'DELIVERED' && (
<button
onClick={handleEditOrder}
className="p-2 bg-white rounded-lg border border-slate-200 text-indigo-500 shadow-sm hover:border-indigo-400 transition-all active:scale-95 cursor-pointer"
title="Edit Order"
>
<Edit2 size={16} />
</button>
)}
<div className="relative action-menu-container">
<button
onClick={() => setShowActionMenu(!showActionMenu)}

View File

@@ -1,4 +1,4 @@
import { apiFetch } from '../api';
import { apiFetch } from '../api';
import React, { useState, useEffect } from 'react';
import {
User,
@@ -34,7 +34,7 @@ const Settings = () => {
// Team State
const [admins, setAdmins] = useState<any[]>([]);
const [newAdmin, setNewAdmin] = useState({ name: '', email: '', password: '' });
const [newAdmin, setNewAdmin] = useState({ name: '', email: '', password: '', role: 'MASTER' });
const [showAddModal, setShowAddModal] = useState(false);
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
@@ -48,11 +48,19 @@ const Settings = () => {
const fetchAdmins = async () => {
try {
const response = await apiFetch('/api/system/admins');
const data = await response.json();
setAdmins(data);
const [adminsRes, managersRes] = await Promise.all([
apiFetch('/api/system/admins'),
apiFetch('/api/system/managers')
]);
const adminsData = await adminsRes.json();
const managersData = await managersRes.json();
const merged = [
...adminsData.map((a: any) => ({ ...a, role: 'MASTER' })),
...managersData.map((m: any) => ({ ...m, role: 'MANAGER' }))
];
setAdmins(merged);
} catch (err) {
console.error('Failed to fetch admins');
console.error('Failed to fetch team members:', err);
}
};
@@ -103,22 +111,36 @@ const Settings = () => {
e.preventDefault();
setStatus('loading');
try {
const response = await apiFetch('/api/system/admins', {
const endpoint = newAdmin.role === 'MANAGER' ? '/api/system/managers' : '/api/system/admins';
const payload = newAdmin.role === 'MANAGER'
? {
name: newAdmin.name,
email: newAdmin.email,
password: newAdmin.password,
viewOnly: false,
permissions: ["dashboard", "sale", "customers", "purchases", "inventory", "expense", "reports", "stores", "table", "wallet", "promotions", "feedback"]
}
: {
name: newAdmin.name,
email: newAdmin.email,
password: newAdmin.password
};
const response = await apiFetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newAdmin)
body: JSON.stringify(payload)
});
if (response.ok) {
setStatus('success');
setMessage('New administrator added');
setNewAdmin({ name: '', email: '', password: '' });
setMessage(newAdmin.role === 'MANAGER' ? 'New manager added' : 'New administrator added');
setNewAdmin({ name: '', email: '', password: '', role: 'MASTER' });
setShowAddModal(false);
fetchAdmins();
setTimeout(() => setStatus('idle'), 3000);
} else {
setStatus('error');
setMessage('Failed to add administrator');
setMessage('Failed to add user');
}
} catch (err) {
setStatus('error');
@@ -286,15 +308,15 @@ const Settings = () => {
>
<div className="flex justify-between items-center bg-white p-6 rounded-3xl border border-[#e2e8f0] shadow-sm">
<div>
<h3 className="text-xl font-bold text-[#001828]">Administrator Team</h3>
<p className="text-sm text-[#64748b]">Found {admins.length} active system administrators</p>
<h3 className="text-xl font-bold text-[#001828]">Team Directory</h3>
<p className="text-sm text-[#64748b]">Found {admins.length} active system members</p>
</div>
<button
onClick={() => setShowAddModal(true)}
className="bg-gradient-to-r from-[#001828] to-[#6366f1] text-white px-6 py-3 rounded-xl font-bold flex items-center gap-2 shadow-lg shadow-indigo-100 hover:scale-[1.02] transition-all"
>
<UserPlus size={18} />
Add New Admin
Add Team Member
</button>
</div>
@@ -317,7 +339,7 @@ const Settings = () => {
</div>
<div className="flex items-center gap-2 text-xs text-[#64748b]">
<ShieldCheck size={14} className="text-green-500" />
<span>{admin.role} ACCESS</span>
<span>{admin.role || 'MASTER'} ACCESS</span>
</div>
</div>
</div>
@@ -337,10 +359,21 @@ const Settings = () => {
<div className="w-16 h-16 bg-white/10 rounded-2xl flex items-center justify-center mx-auto mb-4">
<UserPlus size={32} />
</div>
<h3 className="text-xl font-bold">New Administrator</h3>
<p className="text-sm text-indigo-200">Grant full system access to a team member</p>
<h3 className="text-xl font-bold">New Team Member</h3>
<p className="text-sm text-indigo-200">Grant system access to a team member</p>
</div>
<form onSubmit={handleAddAdmin} className="p-8 space-y-5">
<div>
<label className="block text-xs font-black text-[#64748b] mb-2 uppercase tracking-widest">Role</label>
<select
value={newAdmin.role}
onChange={(e) => setNewAdmin({...newAdmin, role: e.target.value})}
className="w-full bg-gray-50 border border-gray-100 rounded-xl py-3 px-4 outline-none focus:border-indigo-500 transition-all font-semibold text-sm text-[#1e293b] appearance-none"
>
<option value="MASTER">Administrator (Full Access)</option>
<option value="MANAGER">Manager (Counter & Management Access)</option>
</select>
</div>
<div>
<label className="block text-xs font-black text-[#64748b] mb-2 uppercase tracking-widest">Full Name</label>
<input
@@ -375,12 +408,21 @@ const Settings = () => {
/>
</div>
<div className="bg-amber-50 border border-amber-100 p-4 rounded-2xl flex gap-3 text-amber-700">
<ShieldAlert size={20} className="shrink-0" />
<p className="text-[10px] leading-relaxed font-bold">
CRITICAL: This user will have full MASTER privileges. They can manage sales, tokens, and other administrators.
</p>
</div>
{newAdmin.role === 'MASTER' ? (
<div className="bg-amber-50 border border-amber-100 p-4 rounded-2xl flex gap-3 text-amber-700">
<ShieldAlert size={20} className="shrink-0" />
<p className="text-[10px] leading-relaxed font-bold">
CRITICAL: This user will have full MASTER privileges. They can manage sales, tokens, and other administrators.
</p>
</div>
) : (
<div className="bg-indigo-50 border border-indigo-100 p-4 rounded-2xl flex gap-3 text-indigo-700">
<ShieldCheck size={20} className="shrink-0" />
<p className="text-[10px] leading-relaxed font-bold">
INFO: This user will have MANAGER privileges. They will have access to the Counter Dashboard and other management tasks.
</p>
</div>
)}
<div className="flex gap-3 pt-2">
<button type="button" onClick={() => setShowAddModal(false)} className="flex-1 py-3 rounded-xl font-bold text-gray-500 hover:bg-gray-50 transition-all">Cancel</button>

View File

@@ -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 = () => {
</div>
{/* Action buttons — completely separate from card click */}
<div className="flex items-center gap-1 relative z-20">
{terminal.paired && (
<button
type="button"
onClick={(e) => { e.preventDefault(); e.stopPropagation(); handleBlock(terminal.id, terminal.blocked); }}
title={terminal.blocked ? "Unblock terminal" : "Block terminal"}
className={`p-2 rounded-xl transition-all ${
terminal.blocked
? 'text-red-600 bg-red-50 hover:bg-red-100'
: 'text-gray-400 hover:text-red-500 hover:bg-red-50'
}`}
>
<Ban size={16} />
</button>
)}
{terminal.paired && (
<button
type="button"
@@ -263,8 +297,10 @@ const Terminals = () => {
<div className="flex items-center gap-2">
{terminal.paired ? (
<>
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
<span className="text-[12px] font-bold text-green-600 uppercase tracking-wider">Paired</span>
<div className={`w-2 h-2 rounded-full animate-pulse ${terminal.blocked ? 'bg-red-500' : 'bg-green-500'}`} />
<span className={`text-[12px] font-bold uppercase tracking-wider ${terminal.blocked ? 'text-red-600' : 'text-green-600'}`}>
{terminal.blocked ? 'Out of Order' : 'Paired'}
</span>
</>
) : (
<>

View File

@@ -102,15 +102,59 @@ const HomeScreen: React.FC = () => {
const popularItems = useMemo(() => foodItems.filter(item => item.isPopular), [foodItems]);
const filteredItems = useMemo(() => {
if (!searchQuery.trim()) return [];
const query = searchQuery.toLowerCase().trim();
return foodItems.filter(item =>
item.name.toLowerCase().includes(query) ||
item.category.toLowerCase().includes(query) ||
(item.description && item.description.toLowerCase().includes(query))
);
}, [foodItems, searchQuery]);
const [searchResults, setSearchResults] = useState<any[]>([]);
const [isSearching, setIsSearching] = useState(false);
const [debouncedSearch, setDebouncedSearch] = useState('');
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearch(searchQuery);
}, 500);
return () => clearTimeout(timer);
}, [searchQuery]);
useEffect(() => {
if (!debouncedSearch.trim()) {
setSearchResults([]);
return;
}
const fetchSearchResults = async () => {
setIsSearching(true);
try {
const response = await fetch(`http://${window.location.hostname}:8080/api/products?search=${encodeURIComponent(debouncedSearch)}&size=100`);
if (response.ok) {
const data = await response.json();
const products = data.content || data || [];
const mapped = products.map((item: any) => {
const rawImg = item.imageData?.trim();
const finalImage = rawImg ? (rawImg.startsWith('data:') ? rawImg : `data:image/png;base64,${rawImg}`) : '';
const stallFromBackend = item.stalls && item.stalls.length > 0 ? { id: item.stalls[0].id.toString(), name: item.stalls[0].name } : null;
return {
id: item.id.toString(),
name: item.name,
description: item.description || 'No description available',
price: item.price || item.basePrice || 0,
category: item.category,
image: finalImage,
isVeg: item.veg,
isPopular: item.active,
stock: item.stock,
stallId: stallFromBackend?.id,
stallName: stallFromBackend?.name
};
});
setSearchResults(mapped);
}
} catch (err) {
console.error('Error fetching search results:', err);
} finally {
setIsSearching(false);
}
};
fetchSearchResults();
}, [debouncedSearch]);
if (isLoading && categories.length === 0) {
return (
@@ -173,17 +217,25 @@ const HomeScreen: React.FC = () => {
<h2 className="section-title">Search Results for "{searchQuery}"</h2>
</div>
<div className="items-list">
{filteredItems.map((item, index) => (
<ItemCard
key={item.id}
item={item}
isLast={index === filteredItems.length - 1}
/>
))}
{filteredItems.length === 0 && (
<div className="no-results">
<p>No items found matching your search.</p>
{isSearching ? (
<div style={{ padding: 24, textAlign: 'center', color: 'var(--text-light)', width: '100%' }}>
<div className="loading-spinner" style={{ margin: '0 auto' }}>Searching...</div>
</div>
) : (
<>
{searchResults.map((item, index) => (
<ItemCard
key={item.id}
item={item}
isLast={index === searchResults.length - 1}
/>
))}
{searchResults.length === 0 && (
<div className="no-results" style={{ width: '100%' }}>
<p>No items found matching your search.</p>
</div>
)}
</>
)}
</div>
</section>