Ritz Token Initialized
This commit is contained in:
BIN
Ritz/.DS_Store
vendored
Normal file
BIN
Ritz/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
Ritz/Ritz-Stack.png
Normal file
BIN
Ritz/Ritz-Stack.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
Ritz/Ritz.psd
Normal file
BIN
Ritz/Ritz.psd
Normal file
Binary file not shown.
BIN
Ritz/image-removebg-preview (6) 2.png
Normal file
BIN
Ritz/image-removebg-preview (6) 2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 266 KiB |
BIN
Ritz/image-removebg-preview (6).png
Normal file
BIN
Ritz/image-removebg-preview (6).png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 348 KiB |
@@ -14,6 +14,9 @@ import org.springframework.data.domain.PageRequest;
|
|||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
import org.springframework.data.jpa.domain.Specification;
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
|
import com.rit.canteen.sales.repository.UserRepository;
|
||||||
|
import com.rit.canteen.sales.service.OrderArchiverService;
|
||||||
|
import com.rit.canteen.sales.service.TokenService;
|
||||||
import org.springframework.format.annotation.DateTimeFormat;
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@@ -32,7 +35,13 @@ public class OrderController {
|
|||||||
private ProductRepository productRepository;
|
private ProductRepository productRepository;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private com.rit.canteen.sales.service.OrderArchiverService orderArchiverService;
|
private UserRepository userRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private OrderArchiverService orderArchiverService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TokenService tokenService;
|
||||||
|
|
||||||
// Use ThreadLocal to safely store conflicts for the current request context
|
// Use ThreadLocal to safely store conflicts for the current request context
|
||||||
private static final ThreadLocal<List<Map<String, Object>>> requestConflicts = new ThreadLocal<>();
|
private static final ThreadLocal<List<Map<String, Object>>> requestConflicts = new ThreadLocal<>();
|
||||||
@@ -163,7 +172,19 @@ public class OrderController {
|
|||||||
String displayId = String.format("%03d", todaysOrderCount + 1);
|
String displayId = String.format("%03d", todaysOrderCount + 1);
|
||||||
order.setDisplayOrderId(displayId);
|
order.setDisplayOrderId(displayId);
|
||||||
|
|
||||||
// 4. Final Save
|
// 4. Token Payment Check
|
||||||
|
if ("RITZ_TOKEN".equals(order.getPaymentMethod())) {
|
||||||
|
try {
|
||||||
|
tokenService.spend(order.getUser().getId(), order.getTotalAmount(), "ORD-" + displayId);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
if ("INSUFFICIENT_TOKENS".equals(e.getMessage())) {
|
||||||
|
throw new RuntimeException("INSUFFICIENT_TOKENS");
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Final Save
|
||||||
Order savedOrder = orderRepository.save(order);
|
Order savedOrder = orderRepository.save(order);
|
||||||
|
|
||||||
System.out.println("Placed Order: " + savedOrder.getId() + " -> Display ID: #" + displayId);
|
System.out.println("Placed Order: " + savedOrder.getId() + " -> Display ID: #" + displayId);
|
||||||
@@ -188,6 +209,13 @@ public class OrderController {
|
|||||||
"conflicts", conflicts != null ? conflicts : new ArrayList<>()
|
"conflicts", conflicts != null ? conflicts : new ArrayList<>()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if ("INSUFFICIENT_TOKENS".equals(e.getMessage())) {
|
||||||
|
return ResponseEntity.status(400).body(Map.of(
|
||||||
|
"success", false,
|
||||||
|
"errorType", "TOKEN_ERROR",
|
||||||
|
"message", "Insufficient Ritz Tokens. Please top up your wallet."
|
||||||
|
));
|
||||||
|
}
|
||||||
return ResponseEntity.status(500).body(Map.of("success", false, "message", e.getMessage() != null ? e.getMessage() : "Internal Server Error"));
|
return ResponseEntity.status(500).body(Map.of("success", false, "message", e.getMessage() != null ? e.getMessage() : "Internal Server Error"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.rit.canteen.sales.controller;
|
||||||
|
|
||||||
|
import com.rit.canteen.sales.model.TokenTransaction;
|
||||||
|
import com.rit.canteen.sales.model.User;
|
||||||
|
import com.rit.canteen.sales.service.TokenService;
|
||||||
|
import com.rit.canteen.sales.service.UserService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/wallet")
|
||||||
|
public class WalletController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private com.rit.canteen.sales.repository.UserRepository userRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private com.rit.canteen.sales.service.TokenService tokenService;
|
||||||
|
|
||||||
|
@GetMapping("/balance/{userId}")
|
||||||
|
public ResponseEntity<?> getBalance(@PathVariable Long userId) {
|
||||||
|
try {
|
||||||
|
User user = userRepository.findById(userId).orElse(null);
|
||||||
|
|
||||||
|
if (user == null) return ResponseEntity.notFound().build();
|
||||||
|
return ResponseEntity.ok(Map.of("balance", user.getRitzTokenBalance()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.status(500).body(Map.of("error", e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/transactions/{userId}")
|
||||||
|
public ResponseEntity<List<TokenTransaction>> getTransactions(@PathVariable Long userId) {
|
||||||
|
return ResponseEntity.ok(tokenService.getTransactions(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/topup")
|
||||||
|
public ResponseEntity<?> topUp(@RequestBody Map<String, Object> request) {
|
||||||
|
try {
|
||||||
|
Long userId = Long.valueOf(request.get("userId").toString());
|
||||||
|
BigDecimal amount = new BigDecimal(request.get("amount").toString());
|
||||||
|
String ref = request.getOrDefault("referenceId", "TOPUP-" + System.currentTimeMillis()).toString();
|
||||||
|
|
||||||
|
User updatedUser = tokenService.topUp(userId, amount, ref);
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"success", true,
|
||||||
|
"newBalance", updatedUser.getRitzTokenBalance(),
|
||||||
|
"message", "Successfully added " + amount + " Ritz Tokens"
|
||||||
|
));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.status(500).body(Map.of("error", e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,14 +43,16 @@ public class LoginResponse {
|
|||||||
private String mobileNumber;
|
private String mobileNumber;
|
||||||
private String name;
|
private String name;
|
||||||
private boolean isLoggedIn;
|
private boolean isLoggedIn;
|
||||||
|
private java.math.BigDecimal ritzTokenBalance;
|
||||||
|
|
||||||
public UserDto() {}
|
public UserDto() {}
|
||||||
|
|
||||||
public UserDto(Long id, String mobileNumber, String name, boolean isLoggedIn) {
|
public UserDto(Long id, String mobileNumber, String name, boolean isLoggedIn, java.math.BigDecimal ritzTokenBalance) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.mobileNumber = mobileNumber;
|
this.mobileNumber = mobileNumber;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.isLoggedIn = isLoggedIn;
|
this.isLoggedIn = isLoggedIn;
|
||||||
|
this.ritzTokenBalance = ritzTokenBalance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getId() { return id; }
|
public Long getId() { return id; }
|
||||||
@@ -63,6 +65,9 @@ public class LoginResponse {
|
|||||||
public void setName(String name) { this.name = name; }
|
public void setName(String name) { this.name = name; }
|
||||||
|
|
||||||
public boolean isLoggedIn() { return isLoggedIn; }
|
public boolean isLoggedIn() { return isLoggedIn; }
|
||||||
public void setLoggedIn(boolean loggedIn) { isLoggedIn = loggedIn; }
|
public void setLoggedIn(boolean loggedIn) { this.isLoggedIn = loggedIn; }
|
||||||
|
|
||||||
|
public java.math.BigDecimal getRitzTokenBalance() { return ritzTokenBalance; }
|
||||||
|
public void setRitzTokenBalance(java.math.BigDecimal ritzTokenBalance) { this.ritzTokenBalance = ritzTokenBalance; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.rit.canteen.sales.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "token_transactions")
|
||||||
|
public class TokenTransaction {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "user_id", nullable = false)
|
||||||
|
private User user;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private BigDecimal amount;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private TransactionType type;
|
||||||
|
|
||||||
|
@Column(length = 255)
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private LocalDateTime timestamp;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private String referenceId; // e.g. Order Display ID or Payment ID
|
||||||
|
|
||||||
|
public enum TransactionType {
|
||||||
|
TOPUP,
|
||||||
|
SPEND,
|
||||||
|
REFUND
|
||||||
|
}
|
||||||
|
|
||||||
|
public TokenTransaction() {}
|
||||||
|
|
||||||
|
public TokenTransaction(User user, BigDecimal amount, TransactionType type, String description, String referenceId) {
|
||||||
|
this.user = user;
|
||||||
|
this.amount = amount;
|
||||||
|
this.type = type;
|
||||||
|
this.description = description;
|
||||||
|
this.referenceId = referenceId;
|
||||||
|
this.timestamp = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public User getUser() { return user; }
|
||||||
|
public void setUser(User user) { this.user = user; }
|
||||||
|
|
||||||
|
public BigDecimal getAmount() { return amount; }
|
||||||
|
public void setAmount(BigDecimal amount) { this.amount = amount; }
|
||||||
|
|
||||||
|
public TransactionType getType() { return type; }
|
||||||
|
public void setType(TransactionType type) { this.type = type; }
|
||||||
|
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
|
||||||
|
public LocalDateTime getTimestamp() { return timestamp; }
|
||||||
|
public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }
|
||||||
|
|
||||||
|
public String getReferenceId() { return referenceId; }
|
||||||
|
public void setReferenceId(String referenceId) { this.referenceId = referenceId; }
|
||||||
|
}
|
||||||
@@ -34,6 +34,9 @@ public class User {
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private java.math.BigDecimal ritzTokenBalance = java.math.BigDecimal.ZERO;
|
||||||
|
|
||||||
@Column(nullable = true)
|
@Column(nullable = true)
|
||||||
private LocalDateTime lastLoginAt;
|
private LocalDateTime lastLoginAt;
|
||||||
|
|
||||||
@@ -48,6 +51,9 @@ public class User {
|
|||||||
public String getName() { return name; }
|
public String getName() { return name; }
|
||||||
public void setName(String name) { this.name = name; }
|
public void setName(String name) { this.name = name; }
|
||||||
|
|
||||||
|
public java.math.BigDecimal getRitzTokenBalance() { return ritzTokenBalance; }
|
||||||
|
public void setRitzTokenBalance(java.math.BigDecimal ritzTokenBalance) { this.ritzTokenBalance = ritzTokenBalance; }
|
||||||
|
|
||||||
public String getPinHash() { return pinHash; }
|
public String getPinHash() { return pinHash; }
|
||||||
public void setPinHash(String pinHash) { this.pinHash = pinHash; }
|
public void setPinHash(String pinHash) { this.pinHash = pinHash; }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.rit.canteen.sales.repository;
|
||||||
|
|
||||||
|
import com.rit.canteen.sales.model.TokenTransaction;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface TokenTransactionRepository extends JpaRepository<TokenTransaction, Long> {
|
||||||
|
List<TokenTransaction> findByUserIdOrderByTimestampDesc(Long userId);
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ public class DatabaseSeeder implements CommandLineRunner {
|
|||||||
public void run(String... args) throws Exception {
|
public void run(String... args) throws Exception {
|
||||||
repairStallsSchema();
|
repairStallsSchema();
|
||||||
repairOrdersSchema();
|
repairOrdersSchema();
|
||||||
|
repairUsersSchema();
|
||||||
repairFeedbackSchema();
|
repairFeedbackSchema();
|
||||||
repairLobColumns();
|
repairLobColumns();
|
||||||
seedCategories();
|
seedCategories();
|
||||||
@@ -54,6 +55,17 @@ public class DatabaseSeeder implements CommandLineRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void repairUsersSchema() {
|
||||||
|
System.out.println("Checking schema consistency for 'app_users' table...");
|
||||||
|
try {
|
||||||
|
// Add ritz_token_balance if it doesn't exist
|
||||||
|
jdbcTemplate.execute("ALTER TABLE app_users ADD COLUMN IF NOT EXISTS ritz_token_balance NUMERIC(19, 2) DEFAULT 0 NOT NULL");
|
||||||
|
System.out.println("Users schema consistency confirmed.");
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Users schema repair notice: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void repairStallsSchema() {
|
private void repairStallsSchema() {
|
||||||
System.out.println("Checking schema consistency for 'stalls' table...");
|
System.out.println("Checking schema consistency for 'stalls' table...");
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.rit.canteen.sales.service;
|
||||||
|
|
||||||
|
import com.rit.canteen.sales.model.TokenTransaction;
|
||||||
|
import com.rit.canteen.sales.model.User;
|
||||||
|
import com.rit.canteen.sales.repository.TokenTransactionRepository;
|
||||||
|
import com.rit.canteen.sales.repository.UserRepository;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class TokenService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserRepository userRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TokenTransactionRepository transactionRepository;
|
||||||
|
|
||||||
|
public List<TokenTransaction> getTransactions(Long userId) {
|
||||||
|
return transactionRepository.findByUserIdOrderByTimestampDesc(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public User topUp(Long userId, BigDecimal amount, String paymentRef) {
|
||||||
|
User user = userRepository.findById(userId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||||
|
|
||||||
|
BigDecimal currentBalance = user.getRitzTokenBalance() != null ? user.getRitzTokenBalance() : BigDecimal.ZERO;
|
||||||
|
user.setRitzTokenBalance(currentBalance.add(amount));
|
||||||
|
User savedUser = userRepository.save(user);
|
||||||
|
|
||||||
|
TokenTransaction transaction = new TokenTransaction(
|
||||||
|
user,
|
||||||
|
amount,
|
||||||
|
TokenTransaction.TransactionType.TOPUP,
|
||||||
|
"Wallet Top Up via UPI/Card",
|
||||||
|
paymentRef
|
||||||
|
);
|
||||||
|
transactionRepository.save(transaction);
|
||||||
|
|
||||||
|
return savedUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void spend(Long userId, BigDecimal amount, String orderRef) {
|
||||||
|
User user = userRepository.findById(userId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||||
|
|
||||||
|
BigDecimal currentBalance = user.getRitzTokenBalance() != null ? user.getRitzTokenBalance() : BigDecimal.ZERO;
|
||||||
|
|
||||||
|
if (currentBalance.compareTo(amount) < 0) {
|
||||||
|
throw new RuntimeException("INSUFFICIENT_TOKENS");
|
||||||
|
}
|
||||||
|
|
||||||
|
user.setRitzTokenBalance(currentBalance.subtract(amount));
|
||||||
|
userRepository.save(user);
|
||||||
|
|
||||||
|
TokenTransaction transaction = new TokenTransaction(
|
||||||
|
user,
|
||||||
|
amount,
|
||||||
|
TokenTransaction.TransactionType.SPEND,
|
||||||
|
"Food Order Payment",
|
||||||
|
orderRef
|
||||||
|
);
|
||||||
|
transactionRepository.save(transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,7 +61,7 @@ public class UserService {
|
|||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
|
|
||||||
LoginResponse.UserDto userDto = new LoginResponse.UserDto(
|
LoginResponse.UserDto userDto = new LoginResponse.UserDto(
|
||||||
user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn()
|
user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.getRitzTokenBalance()
|
||||||
);
|
);
|
||||||
|
|
||||||
return new LoginResponse(true, "Registration successful. You are now logged in.", userDto);
|
return new LoginResponse(true, "Registration successful. You are now logged in.", userDto);
|
||||||
@@ -88,7 +88,7 @@ public class UserService {
|
|||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
|
|
||||||
LoginResponse.UserDto userDto = new LoginResponse.UserDto(
|
LoginResponse.UserDto userDto = new LoginResponse.UserDto(
|
||||||
user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn()
|
user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.getRitzTokenBalance()
|
||||||
);
|
);
|
||||||
|
|
||||||
return new LoginResponse(true, "Login successful.", userDto);
|
return new LoginResponse(true, "Login successful.", userDto);
|
||||||
@@ -154,7 +154,7 @@ public class UserService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
return new LoginResponse.UserDto(user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn());
|
return new LoginResponse.UserDto(user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.getRitzTokenBalance());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,7 +172,8 @@ public class UserService {
|
|||||||
user.getId(),
|
user.getId(),
|
||||||
user.getMobileNumber(),
|
user.getMobileNumber(),
|
||||||
user.getName(),
|
user.getName(),
|
||||||
user.isLoggedIn()
|
user.isLoggedIn(),
|
||||||
|
user.getRitzTokenBalance()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +212,7 @@ public class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
return new LoginResponse.UserDto(user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn());
|
return new LoginResponse.UserDto(user.getId(), user.getMobileNumber(), user.getName(), user.isLoggedIn(), user.getRitzTokenBalance());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Search, User, Phone, Tag, ChevronRight, UserCheck, UserMinus, MoreVertical, LayoutGrid, List, Edit2, Trash2, Shield, X, Eye, EyeOff, Loader2, AlertCircle, CheckCircle } from 'lucide-react';
|
import { Search, User, Phone, Tag, ChevronRight, UserCheck, UserMinus, MoreVertical, LayoutGrid, List, Edit2, Trash2, Shield, X, Eye, EyeOff, Loader2, AlertCircle, CheckCircle, CircleDollarSign } from 'lucide-react';
|
||||||
import Pagination from '../components/Pagination';
|
import Pagination from '../components/Pagination';
|
||||||
|
|
||||||
interface UserDto {
|
interface UserDto {
|
||||||
@@ -7,6 +7,7 @@ interface UserDto {
|
|||||||
mobileNumber: string;
|
mobileNumber: string;
|
||||||
name: string;
|
name: string;
|
||||||
loggedIn: boolean;
|
loggedIn: boolean;
|
||||||
|
ritzTokenBalance: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Customers: React.FC = () => {
|
const Customers: React.FC = () => {
|
||||||
@@ -241,6 +242,7 @@ const Customers: React.FC = () => {
|
|||||||
<th className="px-6 py-4 text-xs font-bold text-[#64748b] uppercase tracking-wider">Customer Info</th>
|
<th className="px-6 py-4 text-xs font-bold text-[#64748b] uppercase tracking-wider">Customer Info</th>
|
||||||
<th className="px-6 py-4 text-xs font-bold text-[#64748b] uppercase tracking-wider">Contact Info</th>
|
<th className="px-6 py-4 text-xs font-bold text-[#64748b] uppercase tracking-wider">Contact Info</th>
|
||||||
<th className="px-6 py-4 text-xs font-bold text-[#64748b] uppercase tracking-wider">Membership</th>
|
<th className="px-6 py-4 text-xs font-bold text-[#64748b] uppercase tracking-wider">Membership</th>
|
||||||
|
<th className="px-6 py-4 text-xs font-bold text-[#64748b] uppercase tracking-wider">Wallet Balance</th>
|
||||||
<th className="px-6 py-4 text-xs font-bold text-[#64748b] uppercase tracking-wider">Status</th>
|
<th className="px-6 py-4 text-xs font-bold text-[#64748b] uppercase tracking-wider">Status</th>
|
||||||
<th className="px-6 py-4 text-xs font-bold text-[#64748b] uppercase tracking-wider text-right">Actions</th>
|
<th className="px-6 py-4 text-xs font-bold text-[#64748b] uppercase tracking-wider text-right">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -271,6 +273,21 @@ const Customers: React.FC = () => {
|
|||||||
REGULAR CUSTOMER
|
REGULAR CUSTOMER
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-8 h-8 bg-indigo-50 rounded-lg flex items-center justify-center text-indigo-600">
|
||||||
|
<CircleDollarSign size={16} />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-black text-[#231651]">
|
||||||
|
R{user.ritzTokenBalance?.toLocaleString() || '0'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<span className="text-sm font-bold text-[#231651]">
|
||||||
|
R{user.ritzTokenBalance?.toLocaleString() || '0'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-bold ${
|
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-bold ${
|
||||||
user.loggedIn
|
user.loggedIn
|
||||||
@@ -301,7 +318,7 @@ const Customers: React.FC = () => {
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={5} className="px-6 py-12 text-center text-[#64748b]">
|
<td colSpan={6} className="px-6 py-12 text-center text-[#64748b]">
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
<User size={40} className="mb-2 opacity-20" />
|
<User size={40} className="mb-2 opacity-20" />
|
||||||
<p className="font-medium">No customers found</p>
|
<p className="font-medium">No customers found</p>
|
||||||
@@ -346,6 +363,9 @@ const Customers: React.FC = () => {
|
|||||||
<span className="px-2 py-0.5 bg-blue-50 text-blue-600 text-[10px] font-bold rounded-lg border border-blue-100 uppercase tracking-wider">
|
<span className="px-2 py-0.5 bg-blue-50 text-blue-600 text-[10px] font-bold rounded-lg border border-blue-100 uppercase tracking-wider">
|
||||||
RIT STUDENT
|
RIT STUDENT
|
||||||
</span>
|
</span>
|
||||||
|
<span className="px-2 py-0.5 bg-indigo-50 text-indigo-600 text-[10px] font-bold rounded-lg border border-indigo-100 uppercase tracking-wider">
|
||||||
|
R{user.ritzTokenBalance?.toLocaleString() || '0'} TOKENS
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button onClick={() => handleEditClick(user)} className="w-full py-2.5 bg-gray-50 border border-[#e2e8f0] text-[#1e293b] hover:bg-[#231651] hover:text-white rounded-xl flex items-center justify-center gap-2 text-sm font-bold transition-all">
|
<button onClick={() => handleEditClick(user)} className="w-full py-2.5 bg-gray-50 border border-[#e2e8f0] text-[#1e293b] hover:bg-[#231651] hover:text-white rounded-xl flex items-center justify-center gap-2 text-sm font-bold transition-all">
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import LoginScreen from './pages/LoginScreen';
|
|||||||
import ProfileScreen from './pages/ProfileScreen';
|
import ProfileScreen from './pages/ProfileScreen';
|
||||||
import ChangePinScreen from './pages/ChangePinScreen';
|
import ChangePinScreen from './pages/ChangePinScreen';
|
||||||
import StallDetailScreen from './pages/StallDetailScreen';
|
import StallDetailScreen from './pages/StallDetailScreen';
|
||||||
|
import WalletScreen from './pages/WalletScreen';
|
||||||
|
import TopUpScreen from './pages/TopUpScreen';
|
||||||
import StockAlert from './components/StockAlert';
|
import StockAlert from './components/StockAlert';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
@@ -78,6 +80,16 @@ function App() {
|
|||||||
<ChangePinScreen />
|
<ChangePinScreen />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
|
<Route path="/wallet" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<WalletScreen />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path="/topup" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<TopUpScreen />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
</CartProvider>
|
</CartProvider>
|
||||||
|
|||||||
@@ -122,7 +122,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
id: updatedUserDto.id,
|
id: updatedUserDto.id,
|
||||||
name: updatedUserDto.name,
|
name: updatedUserDto.name,
|
||||||
mobileNumber: updatedUserDto.mobileNumber,
|
mobileNumber: updatedUserDto.mobileNumber,
|
||||||
isLoggedIn: updatedUserDto.loggedIn
|
isLoggedIn: updatedUserDto.loggedIn,
|
||||||
|
ritzTokenBalance: updatedUserDto.ritzTokenBalance
|
||||||
};
|
};
|
||||||
setUser(updatedUser);
|
setUser(updatedUser);
|
||||||
localStorage.setItem('user', JSON.stringify(updatedUser));
|
localStorage.setItem('user', JSON.stringify(updatedUser));
|
||||||
|
|||||||
@@ -28,8 +28,139 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.address-card.selected, .payment-card.selected {
|
.address-card.selected, .payment-card.selected {
|
||||||
border-color: var(--primary);
|
border-color: #6366f1;
|
||||||
background-color: var(--primary-light);
|
background: rgba(99, 102, 241, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ritz-payment-card {
|
||||||
|
padding: 1.2rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ritz-payment-card.insufficient {
|
||||||
|
opacity: 0.8;
|
||||||
|
border-style: dashed;
|
||||||
|
background: rgba(239, 68, 68, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ritz-icon {
|
||||||
|
background: #6366f1;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-name-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 800;
|
||||||
|
padding: 0.2rem 0.6rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.error {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-balance-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-val {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-tokens-checkout-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 1rem;
|
||||||
|
background: white;
|
||||||
|
border: 1px dashed #6366f1;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: #6366f1;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-tokens-checkout-btn:active {
|
||||||
|
background: rgba(99, 102, 241, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-tokens-checkout-btn .btn-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-security-note {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ritz-text {
|
||||||
|
color: #6366f1 !important;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insufficient-warning {
|
||||||
|
margin: 1.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: rgba(255, 149, 0, 0.1);
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
color: #cc7700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-text h3 {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-text p {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ritz-order-btn {
|
||||||
|
background: #6366f1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ritz-order-btn:disabled {
|
||||||
|
background: var(--bg-secondary) !important;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner-small {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top-color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.address-icon, .payment-icon {
|
.address-icon, .payment-icon {
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Smartphone } from 'lucide-react';
|
import {
|
||||||
|
CircleDollarSign,
|
||||||
|
Wallet,
|
||||||
|
ChevronRight,
|
||||||
|
AlertCircle,
|
||||||
|
ShieldCheck,
|
||||||
|
PlusCircle
|
||||||
|
} from 'lucide-react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import Header from '../components/Header';
|
import Header from '../components/Header';
|
||||||
import StockConflictModal from '../components/StockConflictModal';
|
import StockConflictModal from '../components/StockConflictModal';
|
||||||
@@ -9,34 +16,52 @@ import { useAuth } from '../contexts/AuthContext';
|
|||||||
import { useFood } from '../contexts/FoodContext';
|
import { useFood } from '../contexts/FoodContext';
|
||||||
import './CheckoutScreen.css';
|
import './CheckoutScreen.css';
|
||||||
|
|
||||||
const UPI_APPS = [
|
|
||||||
{ id: 'gpay', name: 'Google Pay', icon: 'https://cdn.iconscout.com/icon/free/png-256/free-google-pay-logo-icon-download-in-svg-png-gif-file-formats--technology-social-media-vol-3-pack-logos-icons-2944849.png' },
|
|
||||||
{ id: 'phonepe', name: 'PhonePe', icon: 'https://cdn.iconscout.com/icon/free/png-256/free-phonepe-logo-icon-download-in-svg-png-gif-file-formats--technology-social-media-vol-5-pack-logos-icons-2945037.png' },
|
|
||||||
{ id: 'paytm', name: 'Paytm', icon: 'https://cdn.iconscout.com/icon/free/png-256/free-paytm-logo-icon-download-in-svg-png-gif-file-formats--technology-social-media-vol-5-pack-logos-icons-2945031.png' },
|
|
||||||
{ id: 'bhim', name: 'BHIM UPI', icon: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcT6-6R-pUu-Wj4V_vO8t-qXz9-7vjJ7XvK36A&s' },
|
|
||||||
{ id: 'other', name: 'Others', icon: null }
|
|
||||||
];
|
|
||||||
|
|
||||||
const CheckoutScreen: React.FC = () => {
|
const CheckoutScreen: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { cart, totalPrice, clearCart, removeFromCart, updateQuantity } = useCart();
|
const { cart, totalPrice, clearCart, removeFromCart, updateQuantity } = useCart();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { refreshData } = useFood();
|
const { refreshData } = useFood();
|
||||||
const [selectedApp, setSelectedApp] = useState('gpay');
|
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const [currentBalance, setCurrentBalance] = useState<number>(user?.ritzTokenBalance || 0);
|
||||||
|
const [isLoadingBalance, setIsLoadingBalance] = useState(true);
|
||||||
|
|
||||||
// Conflict state
|
// Conflict state
|
||||||
const [stockConflicts, setStockConflicts] = useState<any[]>([]);
|
const [stockConflicts, setStockConflicts] = useState<any[]>([]);
|
||||||
const [showConflictModal, setShowConflictModal] = useState(false);
|
const [showConflictModal, setShowConflictModal] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchBalance();
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const fetchBalance = async () => {
|
||||||
|
if (!user) return;
|
||||||
|
try {
|
||||||
|
setIsLoadingBalance(true);
|
||||||
|
const response = await fetch(`http://${window.location.hostname}:8080/api/wallet/balance/${user.id}`);
|
||||||
|
const data = await response.json();
|
||||||
|
setCurrentBalance(data.balance || 0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching balance:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingBalance(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isInsufficient = currentBalance < totalPrice;
|
||||||
|
|
||||||
const handlePlaceOrder = async () => {
|
const handlePlaceOrder = async () => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
if (isInsufficient) {
|
||||||
|
alert('Insufficient Ritz Tokens. Please top up your wallet.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
|
|
||||||
const orderData = {
|
const orderData = {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
totalAmount: totalPrice,
|
totalAmount: totalPrice,
|
||||||
paymentMethod: UPI_APPS.find(a => a.id === selectedApp)?.name || 'UPI',
|
paymentMethod: 'RITZ_TOKEN',
|
||||||
orderType: 'MY_ORDER',
|
orderType: 'MY_ORDER',
|
||||||
items: cart.map(item => ({
|
items: cart.map(item => ({
|
||||||
productId: Number(item.id),
|
productId: Number(item.id),
|
||||||
@@ -45,7 +70,8 @@ const CheckoutScreen: React.FC = () => {
|
|||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
stallId: item.stallId ? Number(item.stallId) : null,
|
stallId: item.stallId ? Number(item.stallId) : null,
|
||||||
stallName: item.stallName || null
|
stallName: item.stallName || null
|
||||||
}))
|
})),
|
||||||
|
user: { id: user.id } // Backend needs user object for token deduction
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -66,10 +92,12 @@ const CheckoutScreen: React.FC = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (data.errorType === 'STOCK_ERROR') {
|
} else if (data.errorType === 'STOCK_ERROR') {
|
||||||
console.error('Final Step Stock Conflict:', data.conflicts);
|
|
||||||
setStockConflicts(data.conflicts || []);
|
setStockConflicts(data.conflicts || []);
|
||||||
setShowConflictModal(true);
|
setShowConflictModal(true);
|
||||||
await refreshData(true); // Sync background stock
|
await refreshData(true);
|
||||||
|
} else if (data.errorType === 'TOKEN_ERROR') {
|
||||||
|
alert(data.message || 'Insufficient Tokens');
|
||||||
|
fetchBalance(); // Sync balance
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || 'Failed to place order');
|
alert(data.message || 'Failed to place order');
|
||||||
}
|
}
|
||||||
@@ -106,56 +134,89 @@ const CheckoutScreen: React.FC = () => {
|
|||||||
<main className="safe-area-bottom">
|
<main className="safe-area-bottom">
|
||||||
<section className="checkout-section">
|
<section className="checkout-section">
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
<h2 className="section-title">Pay using UPI</h2>
|
<h2 className="section-title">Payment Method</h2>
|
||||||
<p className="section-subtitle">Select your preferred UPI app</p>
|
<p className="section-subtitle">Food orders are paid using Ritz Tokens</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="payment-options">
|
<div className="payment-options">
|
||||||
{UPI_APPS.map((app) => (
|
<div className={`payment-card ritz-payment-card ${isInsufficient ? 'insufficient' : 'selected'}`}>
|
||||||
<div
|
<div className="payment-icon ritz-icon">
|
||||||
key={app.id}
|
<CircleDollarSign size={24} />
|
||||||
className={`payment-card ${selectedApp === app.id ? 'selected' : ''}`}
|
</div>
|
||||||
onClick={() => setSelectedApp(app.id)}
|
<div className="payment-info">
|
||||||
>
|
<div className="payment-name-row">
|
||||||
<div className="payment-icon">
|
<span className="payment-name">Pay with Ritz Tokens</span>
|
||||||
{app.icon ? (
|
{isInsufficient && (
|
||||||
<img src={app.icon} alt={app.name} className="upi-app-icon" />
|
<span className="status-badge error">Insufficient Balance</span>
|
||||||
) : (
|
|
||||||
<div className="upi-placeholder">
|
|
||||||
<Smartphone size={20} />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="payment-info">
|
<div className="wallet-balance-info">
|
||||||
<span className="payment-name">{app.name}</span>
|
<Wallet size={14} />
|
||||||
<span className="payment-sub">
|
<span>Current Balance: </span>
|
||||||
{app.id === 'other' ? 'Pay via any installed UPI app' : `Fast & secure payments via ${app.name}`}
|
<span className="balance-val">R{currentBalance.toLocaleString()}</span>
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{!isInsufficient && (
|
||||||
<div className="selection-radio">
|
<div className="selection-radio">
|
||||||
<div className="radio-inner" />
|
<div className="radio-inner" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
))}
|
</div>
|
||||||
|
|
||||||
|
{isInsufficient && (
|
||||||
|
<motion.button
|
||||||
|
className="add-tokens-checkout-btn"
|
||||||
|
onClick={() => navigate('/topup')}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
>
|
||||||
|
<div className="btn-content">
|
||||||
|
<PlusCircle size={20} />
|
||||||
|
<span>Top up Ritz Tokens</span>
|
||||||
|
</div>
|
||||||
|
<ChevronRight size={18} />
|
||||||
|
</motion.button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="payment-security-note">
|
||||||
|
<ShieldCheck size={14} />
|
||||||
|
<span>Secured by Ritz Token Protocol. 1 Token = ₹1.00</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="order-summary-mini">
|
<div className="order-summary-mini">
|
||||||
<div className="summary-row">
|
<div className="summary-row">
|
||||||
<span>Amount to pay</span>
|
<span>Tokens to be deducted</span>
|
||||||
<span className="summary-price">₹{(totalPrice + 0).toFixed(2)}</span>
|
<span className="summary-price ritz-text">R{totalPrice.toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="tax-info">Inclusive of all taxes and charges</p>
|
<p className="tax-info">Exclusive of any platform bonuses</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isInsufficient && (
|
||||||
|
<div className="insufficient-warning">
|
||||||
|
<AlertCircle size={20} />
|
||||||
|
<div className="warning-text">
|
||||||
|
<strong>Short by R{(totalPrice - currentBalance).toLocaleString()}</strong>
|
||||||
|
<p>Add more tokens to complete your order.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div className="checkout-footer">
|
<div className="checkout-footer">
|
||||||
<button
|
<button
|
||||||
className="place-order-button"
|
className="place-order-button ritz-order-btn"
|
||||||
onClick={handlePlaceOrder}
|
onClick={handlePlaceOrder}
|
||||||
disabled={isProcessing}
|
disabled={isProcessing || isInsufficient}
|
||||||
>
|
>
|
||||||
{isProcessing ? 'Processing...' : `Pay ₹${totalPrice.toFixed(2)} & Place Order`}
|
{isProcessing ? (
|
||||||
|
<div className="loading-spinner-small" />
|
||||||
|
) : isInsufficient ? (
|
||||||
|
'Insufficient Tokens'
|
||||||
|
) : (
|
||||||
|
`Pay R${totalPrice.toLocaleString()} & Place Order`
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
Phone,
|
Phone,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
User,
|
|
||||||
X,
|
X,
|
||||||
User as UserIcon
|
User as UserIcon,
|
||||||
|
Wallet
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
@@ -77,6 +77,7 @@ const ProfileScreen: React.FC = () => {
|
|||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ icon: ShoppingBag, label: 'My Orders', sub: 'View order history', path: '/orders' },
|
{ icon: ShoppingBag, label: 'My Orders', sub: 'View order history', path: '/orders' },
|
||||||
|
{ icon: Wallet, label: 'My Wallet', sub: 'Ritz Tokens & History', path: '/wallet' },
|
||||||
{ icon: ShieldCheck, label: 'Account Security', sub: 'Change Security PIN', path: '/change-pin' },
|
{ icon: ShieldCheck, label: 'Account Security', sub: 'Change Security PIN', path: '/change-pin' },
|
||||||
{ icon: Settings, label: 'Preferences', sub: 'Notifications, Language', path: '#' },
|
{ icon: Settings, label: 'Preferences', sub: 'Notifications, Language', path: '#' },
|
||||||
{ icon: HelpCircle, label: 'Help & Support', sub: 'FAQs, Contact Us', path: '#' }
|
{ icon: HelpCircle, label: 'Help & Support', sub: 'FAQs, Contact Us', path: '#' }
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export interface User {
|
|||||||
mobileNumber: string;
|
mobileNumber: string;
|
||||||
name: string;
|
name: string;
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
|
ritzTokenBalance: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Stall {
|
export interface Stall {
|
||||||
|
|||||||
Reference in New Issue
Block a user