diff --git a/Ritz/.DS_Store b/Ritz/.DS_Store index 8484a716..f1462a45 100644 Binary files a/Ritz/.DS_Store and b/Ritz/.DS_Store differ diff --git a/Ritz/color-vector.png b/Ritz/color-vector.png new file mode 100644 index 00000000..9239807c Binary files /dev/null and b/Ritz/color-vector.png differ diff --git a/Ritz/color-vector.svg b/Ritz/color-vector.svg new file mode 100644 index 00000000..b5ce49c0 --- /dev/null +++ b/Ritz/color-vector.svg @@ -0,0 +1,1052 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Ritz/front.png b/Ritz/front.png new file mode 100644 index 00000000..3d5167f0 Binary files /dev/null and b/Ritz/front.png differ diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java b/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java index 7ff296c5..850aa5d3 100644 --- a/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java +++ b/backend/src/main/java/com/rit/canteen/sales/controller/OrderController.java @@ -175,7 +175,8 @@ public class OrderController { // 4. Token Payment Check if ("RITZ_TOKEN".equals(order.getPaymentMethod())) { try { - tokenService.spend(order.getUser().getId(), order.getTotalAmount(), "ORD-" + displayId); + // Use userId directly for robustness + tokenService.spend(order.getUserId(), order.getTotalAmount(), "ORD-" + displayId); } catch (RuntimeException e) { if ("INSUFFICIENT_TOKENS".equals(e.getMessage())) { throw new RuntimeException("INSUFFICIENT_TOKENS"); @@ -236,9 +237,19 @@ public class OrderController { return orderRepository.findById(id) .map(order -> { - order.setStatus(newStatus.toUpperCase()); + String oldStatus = order.getStatus(); + String nextStatus = newStatus.toUpperCase(); + + // Check for refund condition: Moving to CANCELLED from a non-cancelled state + if ("CANCELLED".equals(nextStatus) && !"CANCELLED".equals(oldStatus)) { + if ("RITZ_TOKEN".equals(order.getPaymentMethod())) { + tokenService.refund(order.getUserId(), "ORD-" + order.getDisplayOrderId(), order.getTotalAmount(), "Status changed to CANCELLED"); + } + } + + order.setStatus(nextStatus); orderRepository.save(order); - return ResponseEntity.ok(Map.of("success", true, "message", "Order status updated to " + newStatus)); + return ResponseEntity.ok(Map.of("success", true, "message", "Order status updated to " + nextStatus)); }) .orElse(ResponseEntity.notFound().build()); } catch (Exception e) { @@ -251,8 +262,27 @@ public class OrderController { try { return orderRepository.findById(id) .map(existingOrder -> { + BigDecimal oldAmount = existingOrder.getTotalAmount(); + BigDecimal newAmount = updatedOrder.getTotalAmount(); + + // Handle Token Adjustments for edited orders + if ("RITZ_TOKEN".equals(existingOrder.getPaymentMethod())) { + int comparison = newAmount.compareTo(oldAmount); + if (comparison > 0) { + // Spend more + tokenService.spend(existingOrder.getUserId(), newAmount.subtract(oldAmount), "ORD-EDIT-" + existingOrder.getDisplayOrderId()); + } else if (comparison < 0) { + // This is tricky for individual tokens, but we can refund the difference amount + // For simplicity/robustness, we'll refund the whole order and re-spend the new amount + // to keep unit association clean OR just record it as a topup. + // Let's do a simple balance restoration for the delta. + tokenService.refund(existingOrder.getUserId(), "ORD-" + existingOrder.getDisplayOrderId(), oldAmount, "Order price reduced during edit"); + tokenService.spend(existingOrder.getUserId(), newAmount, "ORD-" + existingOrder.getDisplayOrderId()); + } + } + // Update basic fields - existingOrder.setTotalAmount(updatedOrder.getTotalAmount()); + existingOrder.setTotalAmount(newAmount); existingOrder.setPaymentMethod(updatedOrder.getPaymentMethod()); // Clear and replace items for a clean update diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/WalletController.java b/backend/src/main/java/com/rit/canteen/sales/controller/WalletController.java index 6828a760..ff258644 100644 --- a/backend/src/main/java/com/rit/canteen/sales/controller/WalletController.java +++ b/backend/src/main/java/com/rit/canteen/sales/controller/WalletController.java @@ -56,4 +56,21 @@ public class WalletController { return ResponseEntity.status(500).body(Map.of("error", e.getMessage())); } } + + @GetMapping("/transactions/all") + public ResponseEntity> getAllTransactions() { + return ResponseEntity.ok(tokenService.getAllTransactions()); + } + + @GetMapping("/stats") + public ResponseEntity> getStats() { + return ResponseEntity.ok(tokenService.getGlobalStats()); + } + + @GetMapping("/circulation") + public ResponseEntity> getCirculation( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { + return ResponseEntity.ok(tokenService.getAllCirculation(page, size)); + } } diff --git a/backend/src/main/java/com/rit/canteen/sales/model/TokenTransaction.java b/backend/src/main/java/com/rit/canteen/sales/model/TokenTransaction.java index 4d517719..7153dc9c 100644 --- a/backend/src/main/java/com/rit/canteen/sales/model/TokenTransaction.java +++ b/backend/src/main/java/com/rit/canteen/sales/model/TokenTransaction.java @@ -12,8 +12,9 @@ public class TokenTransaction { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "user_id", nullable = false) + @com.fasterxml.jackson.annotation.JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) private User user; @Column(nullable = false) diff --git a/backend/src/main/java/com/rit/canteen/sales/model/TokenUnit.java b/backend/src/main/java/com/rit/canteen/sales/model/TokenUnit.java new file mode 100644 index 00000000..be742afc --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/TokenUnit.java @@ -0,0 +1,72 @@ +package com.rit.canteen.sales.model; + +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "token_units", indexes = { + @Index(name = "idx_token_units_owner_status", columnList = "owner_id, status") +}) +public class TokenUnit { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false) + private String tokenHash; + + @Column(name = "owner_id", nullable = true) // Can be null if not yet issued or systemic + private Long ownerId; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private TokenStatus status = TokenStatus.ACTIVE; + + @Column(nullable = false) + private LocalDateTime createdAt; + + @Column + private LocalDateTime spentAt; + + @Column(name = "order_ref") + private String orderRef; + + public enum TokenStatus { + ACTIVE, + SPENT, + REVOKED + } + + public TokenUnit() { + this.createdAt = LocalDateTime.now(); + this.status = TokenStatus.ACTIVE; + } + + public TokenUnit(String tokenHash, Long ownerId) { + this(); + this.tokenHash = tokenHash; + this.ownerId = ownerId; + } + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getTokenHash() { return tokenHash; } + public void setTokenHash(String tokenHash) { this.tokenHash = tokenHash; } + + public Long getOwnerId() { return ownerId; } + public void setOwnerId(Long ownerId) { this.ownerId = ownerId; } + + public TokenStatus getStatus() { return status; } + public void setStatus(TokenStatus status) { this.status = status; } + + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + + public LocalDateTime getSpentAt() { return spentAt; } + public void setSpentAt(LocalDateTime spentAt) { this.spentAt = spentAt; } + + public String getOrderRef() { return orderRef; } + public void setOrderRef(String orderRef) { this.orderRef = orderRef; } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/TokenTransactionRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/TokenTransactionRepository.java index ac4d91a7..43758234 100644 --- a/backend/src/main/java/com/rit/canteen/sales/repository/TokenTransactionRepository.java +++ b/backend/src/main/java/com/rit/canteen/sales/repository/TokenTransactionRepository.java @@ -8,4 +8,5 @@ import java.util.List; @Repository public interface TokenTransactionRepository extends JpaRepository { List findByUserIdOrderByTimestampDesc(Long userId); + List findAllByOrderByTimestampDesc(); } diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/TokenUnitRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/TokenUnitRepository.java new file mode 100644 index 00000000..059233e3 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/repository/TokenUnitRepository.java @@ -0,0 +1,21 @@ +package com.rit.canteen.sales.repository; + +import com.rit.canteen.sales.model.TokenUnit; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import java.util.List; + +@Repository +public interface TokenUnitRepository extends JpaRepository { + + @Query(value = "SELECT * FROM token_units WHERE owner_id = :ownerId AND status = 'ACTIVE' ORDER BY id ASC LIMIT :amount", nativeQuery = true) + List findActiveUnits(@Param("ownerId") Long ownerId, @Param("amount") int amount); + + List findByOrderRef(String orderRef); + + long countByOwnerIdAndStatus(Long ownerId, TokenUnit.TokenStatus status); + + org.springframework.data.domain.Page findAllByOrderByCreatedAtDesc(org.springframework.data.domain.Pageable pageable); +} diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/UserRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/UserRepository.java index a889760d..05a565e0 100644 --- a/backend/src/main/java/com/rit/canteen/sales/repository/UserRepository.java +++ b/backend/src/main/java/com/rit/canteen/sales/repository/UserRepository.java @@ -15,4 +15,8 @@ public interface UserRepository extends JpaRepository { @Query("SELECT u FROM User u WHERE LOWER(u.name) LIKE LOWER(CONCAT('%', :search, '%')) " + "OR u.mobileNumber LIKE CONCAT('%', :search, '%')") org.springframework.data.domain.Page findByNameOrMobileContainingIgnoreCase(String search, org.springframework.data.domain.Pageable pageable); + + @jakarta.persistence.Lock(jakarta.persistence.LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT u FROM User u WHERE u.id = :id") + Optional findByIdWithLock(@org.springframework.data.repository.query.Param("id") Long id); } diff --git a/backend/src/main/java/com/rit/canteen/sales/service/DatabaseSeeder.java b/backend/src/main/java/com/rit/canteen/sales/service/DatabaseSeeder.java index f39c03e5..20521144 100644 --- a/backend/src/main/java/com/rit/canteen/sales/service/DatabaseSeeder.java +++ b/backend/src/main/java/com/rit/canteen/sales/service/DatabaseSeeder.java @@ -29,6 +29,7 @@ public class DatabaseSeeder implements CommandLineRunner { repairStallsSchema(); repairOrdersSchema(); repairUsersSchema(); + repairTokenUnitsSchema(); repairFeedbackSchema(); repairLobColumns(); seedCategories(); @@ -66,6 +67,25 @@ public class DatabaseSeeder implements CommandLineRunner { } } + private void repairTokenUnitsSchema() { + System.out.println("Checking schema consistency for 'token_units' table..."); + try { + jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS token_units (" + + "id BIGSERIAL PRIMARY KEY, " + + "token_hash VARCHAR(255) UNIQUE NOT NULL, " + + "owner_id BIGINT, " + + "status VARCHAR(20) NOT NULL, " + + "created_at TIMESTAMP NOT NULL, " + + "spent_at TIMESTAMP)"); + + jdbcTemplate.execute("CREATE INDEX IF NOT EXISTS idx_token_units_owner_status ON token_units (owner_id, status)"); + System.out.println("Token Units schema consistency confirmed."); + } catch (Exception e) { + System.err.println("Token Units schema repair notice: " + e.getMessage()); + } + } + + private void repairStallsSchema() { System.out.println("Checking schema consistency for 'stalls' table..."); try { diff --git a/backend/src/main/java/com/rit/canteen/sales/service/TokenService.java b/backend/src/main/java/com/rit/canteen/sales/service/TokenService.java index 19cb7c47..537cdfb7 100644 --- a/backend/src/main/java/com/rit/canteen/sales/service/TokenService.java +++ b/backend/src/main/java/com/rit/canteen/sales/service/TokenService.java @@ -8,8 +8,15 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.rit.canteen.sales.model.TokenUnit; +import org.springframework.jdbc.core.JdbcTemplate; import java.math.BigDecimal; +import java.security.MessageDigest; +import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; @Service public class TokenService { @@ -19,25 +26,78 @@ public class TokenService { @Autowired private TokenTransactionRepository transactionRepository; + @Autowired + private com.rit.canteen.sales.repository.TokenUnitRepository tokenUnitRepository; + + @Autowired + private JdbcTemplate jdbcTemplate; public List getTransactions(Long userId) { return transactionRepository.findByUserIdOrderByTimestampDesc(userId); } + public List getAllTransactions() { + return transactionRepository.findAllByOrderByTimestampDesc(); + } + + public java.util.Map getGlobalStats() { + long totalActive = tokenUnitRepository.count(); // Actually all units + long activeUnits = tokenUnitRepository.countByOwnerIdAndStatus(null, null); // Placeholder, will fix below + + List users = userRepository.findAll(); + BigDecimal totalBalance = users.stream() + .map(u -> u.getRitzTokenBalance() != null ? u.getRitzTokenBalance() : BigDecimal.ZERO) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + java.util.Map stats = new java.util.HashMap<>(); + stats.put("totalCirculation", totalBalance); + stats.put("activeWallets", users.stream().filter(u -> u.getRitzTokenBalance().compareTo(BigDecimal.ZERO) > 0).count()); + stats.put("totalUsers", users.size()); + stats.put("serializedUnitsTotal", tokenUnitRepository.count()); + + return stats; + } + + public org.springframework.data.domain.Page getAllCirculation(int page, int size) { + return tokenUnitRepository.findAllByOrderByCreatedAtDesc(org.springframework.data.domain.PageRequest.of(page, size)); + } + @Transactional public User topUp(Long userId, BigDecimal amount, String paymentRef) { User user = userRepository.findById(userId) .orElseThrow(() -> new RuntimeException("User not found")); + int tokenCount = amount.intValue(); // Assuming 1 Ritz = 1 Serialized Unit + System.out.println("MINTING: Serializing " + tokenCount + " Ritz tokens for user " + userId); + + // High-Performance Batch Insertion using JDBC + String sql = "INSERT INTO token_units (token_hash, owner_id, status, created_at) VALUES (?, ?, 'ACTIVE', ?)"; + List batchArgs = new ArrayList<>(); + LocalDateTime now = LocalDateTime.now(); + + for (int i = 0; i < tokenCount; i++) { + String hash = generateSecureHash(); + batchArgs.add(new Object[]{hash, userId, now}); + } + + jdbcTemplate.batchUpdate(sql, batchArgs); + + // Update cached balance BigDecimal currentBalance = user.getRitzTokenBalance() != null ? user.getRitzTokenBalance() : BigDecimal.ZERO; - user.setRitzTokenBalance(currentBalance.add(amount)); + BigDecimal newBalance = currentBalance.add(amount); + + if (newBalance.compareTo(new BigDecimal("5000")) > 0) { + throw new RuntimeException("Wallet limit exceeded (Max: 5,000 Ritz Tokens)"); + } + + user.setRitzTokenBalance(newBalance); User savedUser = userRepository.save(user); TokenTransaction transaction = new TokenTransaction( user, amount, TokenTransaction.TransactionType.TOPUP, - "Wallet Top Up via UPI/Card", + "Regulated Wallet Top Up (Serialized ID: " + paymentRef + ")", paymentRef ); transactionRepository.save(transaction); @@ -47,7 +107,8 @@ public class TokenService { @Transactional public void spend(Long userId, BigDecimal amount, String orderRef) { - User user = userRepository.findById(userId) + // High Concurrency Lock: Ensure no other thread modifies this user balance simultaneously + User user = userRepository.findByIdWithLock(userId) .orElseThrow(() -> new RuntimeException("User not found")); BigDecimal currentBalance = user.getRitzTokenBalance() != null ? user.getRitzTokenBalance() : BigDecimal.ZERO; @@ -56,6 +117,23 @@ public class TokenService { throw new RuntimeException("INSUFFICIENT_TOKENS"); } + int amountToSpend = amount.intValue(); + + // Identify individual token units to consume + List unitsToSpend = tokenUnitRepository.findActiveUnits(userId, amountToSpend); + + if (unitsToSpend.size() < amountToSpend) { + throw new RuntimeException("SERIALIZED_RECONCILIATION_ERROR: Not enough active units found"); + } + + // PERMANENT REMOVAL: Physical delete from database + List unitIds = unitsToSpend.stream().map(TokenUnit::getId).collect(Collectors.toList()); + String deleteSql = "DELETE FROM token_units WHERE id IN (" + + unitIds.stream().map(String::valueOf).collect(Collectors.joining(",")) + ")"; + + jdbcTemplate.update(deleteSql); + + // Update cached user balance user.setRitzTokenBalance(currentBalance.subtract(amount)); userRepository.save(user); @@ -63,9 +141,63 @@ public class TokenService { user, amount, TokenTransaction.TransactionType.SPEND, - "Food Order Payment", + "Regulated Food Payment (Burned " + amountToSpend + " units)", orderRef ); transactionRepository.save(transaction); + System.out.println("BURNED: " + amountToSpend + " Ritz tokens for user " + userId + " [Order: " + orderRef + "]"); + } + + @Transactional + public void refund(Long userId, String orderRef, BigDecimal amount, String reason) { + // Lock user for safety + User user = userRepository.findByIdWithLock(userId) + .orElseThrow(() -> new RuntimeException("User not found for refund")); + + // Since original tokens are deleted, we RESTORE balance by MINTING fresh tokens + int tokenCount = amount.intValue(); + System.out.println("REFUNDING: Reminting " + tokenCount + " new Ritz tokens for user " + userId + " [" + reason + "]"); + + String sql = "INSERT INTO token_units (token_hash, owner_id, status, created_at) VALUES (?, ?, 'ACTIVE', ?)"; + List batchArgs = new ArrayList<>(); + LocalDateTime now = LocalDateTime.now(); + + for (int i = 0; i < tokenCount; i++) { + batchArgs.add(new Object[]{generateSecureHash(), userId, now}); + } + + jdbcTemplate.batchUpdate(sql, batchArgs); + + // Update user balance + BigDecimal currentBalance = user.getRitzTokenBalance() != null ? user.getRitzTokenBalance() : BigDecimal.ZERO; + user.setRitzTokenBalance(currentBalance.add(amount)); + userRepository.save(user); + + // Log transaction + TokenTransaction transaction = new TokenTransaction( + user, + amount, + TokenTransaction.TransactionType.TOPUP, + "Order Refund: " + reason + " (Ref: " + orderRef + ")", + "REF-" + orderRef + ); + transactionRepository.save(transaction); + } + + private String generateSecureHash() { + try { + String base = UUID.randomUUID().toString() + System.nanoTime(); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(base.getBytes("UTF-8")); + StringBuilder hexString = new StringBuilder(); + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) hexString.append('0'); + hexString.append(hex); + } + return "RTX-" + hexString.toString().substring(0, 32).toUpperCase(); + } catch (Exception e) { + return "RTX-" + UUID.randomUUID().toString().toUpperCase(); + } } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2fb8433a..614834a8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -23,6 +23,8 @@ import IntentList from './pages/IntentList.tsx'; import NewArrivals from './pages/NewArrivals.tsx'; import Reports from './pages/Reports.tsx'; import Feedback from './pages/Feedback.tsx'; +import Ritz from './pages/Ritz.tsx'; +import RitzCirculation from './pages/RitzCirculation.tsx'; const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { const isLoggedIn = sessionStorage.getItem('isLoggedIn') === 'true'; @@ -90,6 +92,8 @@ function App() { {/* Others */} } /> } /> + } /> + } /> {/* Stores */} } /> diff --git a/frontend/src/assets/color-vector.png b/frontend/src/assets/color-vector.png new file mode 100644 index 00000000..9239807c Binary files /dev/null and b/frontend/src/assets/color-vector.png differ diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 2958f81f..009f86ce 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -15,12 +15,14 @@ import { ShoppingBag, Receipt, Search, - LogOut + LogOut, + CircleDollarSign } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; import collegeLogo from '../assets/college-logo.png'; +import colorVector from '../assets/color-vector.png'; function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); @@ -115,6 +117,14 @@ const menuItems: MenuItem[] = [ ] }, { title: 'Feedback', icon: MessageSquare, path: '/feedback' }, + { + title: 'Ritz', + icon: CircleDollarSign, + subMenu: [ + { title: 'Overview', path: '/ritz/overview' }, + { title: 'Ritz in Circulation', path: '/ritz/circulation' } + ] + }, ]; const Sidebar = () => { @@ -141,7 +151,8 @@ const Sidebar = () => { 'Expense': 'expense', 'Reports': 'reports', 'Stores': 'stores', - 'Feedback': 'feedback' + 'Feedback': 'feedback', + 'Ritz': 'ritz' }; return userPermissions.includes(permissionMap[item.title]); @@ -341,10 +352,19 @@ const Sidebar = () => { > {({ isActive }) => ( <> - + {item.title === 'Ritz' ? ( +
+ +
+ ) : ( + + )} {item.title} )} diff --git a/frontend/src/pages/ArchivedOrders.tsx b/frontend/src/pages/ArchivedOrders.tsx index 3d992850..8a722172 100644 --- a/frontend/src/pages/ArchivedOrders.tsx +++ b/frontend/src/pages/ArchivedOrders.tsx @@ -213,7 +213,7 @@ const ArchivedOrders: React.FC = () => {
Grand Total
-
₹{order.totalAmount.toFixed(2)}
+
R{order.totalAmount.toFixed(2)}
@@ -325,8 +325,8 @@ const ArchivedOrders: React.FC = () => {
-
₹{(item.price * item.quantity).toLocaleString()}
-
@ ₹{item.price}
+
R{(item.price * item.quantity).toLocaleString()}
+
@ R{item.price}
))} @@ -377,7 +377,7 @@ const ArchivedOrders: React.FC = () => {
Final Settlement
-
₹{selectedOrder.totalAmount.toLocaleString()}
+
R{selectedOrder.totalAmount.toLocaleString()}
Transaction Fully Reconciled
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 105c853d..4c3853fb 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -207,7 +207,7 @@ const Dashboard = () => { - v >= 1000 ? `₹${v/1000}k` : `₹${v}`} /> + v >= 1000 ? `R${v/1000}k` : `R${v}`} /> @@ -245,9 +245,9 @@ const Dashboard = () => {
-

₹{stats.periodRevenue.toLocaleString()}

+

R{(stats.periodRevenue || 0).toLocaleString()}

{timeRange === 'Today' ? 'Today' : timeRange}

-

Total: ₹{stats.totalSales.toLocaleString()}

+

Total: R{(stats.totalSales || 0).toLocaleString()}

{pieData.map(item => ( @@ -412,7 +412,7 @@ const Dashboard = () => {

Gross Sale

-

₹{Number(store.sale).toLocaleString()}

+

R{Number(store.sale).toLocaleString()}

Volume

diff --git a/frontend/src/pages/NewArrivals.tsx b/frontend/src/pages/NewArrivals.tsx index 848f2a28..d0b58a7f 100644 --- a/frontend/src/pages/NewArrivals.tsx +++ b/frontend/src/pages/NewArrivals.tsx @@ -277,7 +277,7 @@ const NewArrivals: React.FC = () => {
- + setFormData({ ...formData, price: parseFloat(e.target.value) || 0 })} className="w-full px-5 py-4 bg-[#231651]/5 border-2 border-transparent focus:border-[#231651]/20 rounded-2xl text-lg font-black text-[#231651] outline-none transition-all" />
diff --git a/frontend/src/pages/Orders.tsx b/frontend/src/pages/Orders.tsx index ffcace45..4d4a7414 100644 --- a/frontend/src/pages/Orders.tsx +++ b/frontend/src/pages/Orders.tsx @@ -465,7 +465,7 @@ const Orders: React.FC = () => {
Grand Total
-
₹{order.totalAmount.toFixed(2)}
+
R{order.totalAmount.toFixed(2)}
@@ -628,9 +628,9 @@ const Orders: React.FC = () => {
-
₹{(item.price * item.quantity).toLocaleString()}
+
R{(item.price * item.quantity).toLocaleString()}
- {item.quantity} x ₹{item.price} + {item.quantity} x R{item.price}
@@ -700,7 +700,7 @@ const Orders: React.FC = () => {
Active Grand Total
-
₹{selectedOrder.totalAmount.toLocaleString()}
+
R{selectedOrder.totalAmount.toLocaleString()}
Transaction Pending Approval
@@ -784,7 +784,7 @@ const Orders: React.FC = () => {
{item.productName}
-
₹{item.price} each
+
R{item.price} each
@@ -805,7 +805,7 @@ const Orders: React.FC = () => {
-
₹{(item.price * item.quantity).toFixed(2)}
+
R{(item.price * item.quantity).toFixed(2)}
-
₹{product.price}
+
R{product.price}
)) ) : editSearchQuery ? ( @@ -866,7 +866,7 @@ const Orders: React.FC = () => {
New Order Total - ₹{editTotal.toFixed(2)} + R{editTotal.toFixed(2)}
- ₹{product.price} + R{product.price}
@@ -472,7 +472,7 @@ const Products = () => {
setFormData({ ...formData, basePrice: parseFloat(e.target.value) || 0 })} className="w-full px-4 py-3 border border-[#e2e8f0] rounded-xl text-sm" />
-
setFormData({ ...formData, price: parseFloat(e.target.value) || 0 })} className="w-full px-4 py-3 border border-[#e2e8f0] rounded-xl text-sm font-bold" />
+
setFormData({ ...formData, price: parseFloat(e.target.value) || 0 })} className="w-full px-4 py-3 border border-[#e2e8f0] rounded-xl text-sm font-bold" />
setFormData({ ...formData, discountPercent: parseFloat(e.target.value) || 0 })} className="w-full px-4 py-3 border border-[#e2e8f0] rounded-xl text-sm" />
diff --git a/frontend/src/pages/Ritz.tsx b/frontend/src/pages/Ritz.tsx new file mode 100644 index 00000000..d55ebc39 --- /dev/null +++ b/frontend/src/pages/Ritz.tsx @@ -0,0 +1,235 @@ +import React, { useState, useEffect } from 'react'; +import { + Building2, + CircleDollarSign, + TrendingUp, + Users, + ArrowUpRight, + ArrowDownLeft, + Clock, + Search, + Filter, + Download, + Fingerprint +} from 'lucide-react'; +import { motion } from 'framer-motion'; +import colorVector from '../assets/color-vector.png'; + +interface TokenTransaction { + id: number; + amount: number; + type: 'TOPUP' | 'SPEND' | 'REFUND'; + description: string; + timestamp: string; + referenceId: string; + user: { + id: number; + name: string; + mobileNumber: string; + }; +} + +interface Stats { + totalCirculation: number; + activeWallets: number; + totalUsers: number; + serializedUnitsTotal: number; +} + +const RitzPage: React.FC = () => { + const [transactions, setTransactions] = useState([]); + const [stats, setStats] = useState({ + totalCirculation: 0, + activeWallets: 0, + totalUsers: 0, + serializedUnitsTotal: 0 + }); + const [isLoading, setIsLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + + useEffect(() => { + fetchData(); + }, []); + + const fetchData = async () => { + try { + setIsLoading(true); + const host = window.location.hostname; + + const statsRes = await fetch(`http://${host}:8080/api/wallet/stats`); + const statsData = await statsRes.json(); + setStats(statsData); + + const transRes = await fetch(`http://${host}:8080/api/wallet/transactions/all`); + const transData = await transRes.json(); + setTransactions(Array.isArray(transData) ? transData : []); + } catch (error) { + console.error('Error fetching Ritz data:', error); + setTransactions([]); + } finally { + setIsLoading(false); + } + }; + + const filteredTransactions = Array.isArray(transactions) ? transactions.filter(t => + t.user?.name?.toLowerCase().includes(searchTerm.toLowerCase()) || + t.user?.mobileNumber?.includes(searchTerm) || + t.referenceId?.toLowerCase().includes(searchTerm.toLowerCase()) + ) : []; + + return ( +
+ {/* Premium Header Banner */} +
+ Vector BG +
+
+
+ +
+

Ritz Ecosystem

+
+

+ Monitor the heartbeat of the Ritz digital economy. Track circulation, + active wallets, and system-wide transactions in real-time. +

+
+
+ + {/* Stats Grid */} +
+ {[ + { label: 'Tokens in Circulation', value: `R${(stats.totalCirculation || 0).toLocaleString()}`, icon: TrendingUp, color: 'text-emerald-600', bg: 'bg-emerald-50' }, + { label: 'Active Token Wallets', value: (stats.activeWallets || 0).toLocaleString(), icon: Users, color: 'text-blue-600', bg: 'bg-blue-50' }, + { label: 'Serialized Audit Units', value: (stats.serializedUnitsTotal || 0).toLocaleString(), icon: Fingerprint, color: 'text-amber-600', bg: 'bg-amber-50' }, + { label: 'Total Accounts', value: (stats.totalUsers || 0).toLocaleString(), icon: Building2, color: 'text-indigo-600', bg: 'bg-indigo-50' } + ].map((item, i) => ( + +
+

{item.label}

+

{item.value}

+
+
+ +
+
+ ))} +
+ + {/* Main Table Section */} +
+
+
+

Master Transaction Log

+

Detailed history of all wallet activities

+
+ +
+
+ + setSearchTerm(e.target.value)} + /> +
+ + +
+
+ +
+ {isLoading ? ( +
+
+ Syncing Transaction Feed... +
+ ) : ( + + + + + + + + + + + + {filteredTransactions.map((t, i) => ( + + + + + + + + ))} + +
CustomerTypeAmountDescriptionTimestamp
+
+
+ {t.user.name.split(' ').map(n => n[0]).join('').slice(0, 2).toUpperCase()} +
+
+

{t.user.name}

+

{t.user.mobileNumber}

+
+
+
+ + {t.type === 'TOPUP' ? : } + {t.type} + + + + {t.type === 'TOPUP' ? '+' : '-'} {t.amount.toLocaleString()} + + +

{t.description}

+

{t.referenceId}

+
+
+ + {new Date(t.timestamp).toLocaleString('en-IN', { + day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' + })} +
+
+ )} +
+
+ + {/* Animation Styles */} + +
+ ); +}; + +export default RitzPage; diff --git a/frontend/src/pages/RitzCirculation.tsx b/frontend/src/pages/RitzCirculation.tsx new file mode 100644 index 00000000..2afab956 --- /dev/null +++ b/frontend/src/pages/RitzCirculation.tsx @@ -0,0 +1,287 @@ +import React, { useState, useEffect } from 'react'; +import { + CircleDollarSign, + Search, + Filter, + Download, + Fingerprint, + ShieldCheck, + History, + Lock, + Unlock, + Users, + ChevronLeft, + ChevronRight +} from 'lucide-react'; +import { motion } from 'framer-motion'; +import colorVector from '../assets/color-vector.png'; + +interface TokenUnit { + id: number; + tokenHash: string; + ownerId: number; + status: 'ACTIVE' | 'SPENT' | 'REVOKED'; + createdAt: string; + spentAt: string | null; +} + +interface PageResponse { + content: TokenUnit[]; + totalPages: number; + totalElements: number; + number: number; + size: number; +} + +const RitzCirculation: React.FC = () => { + const [tokens, setTokens] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + + // Pagination State + const [page, setPage] = useState(0); + const [size] = useState(20); + const [totalPages, setTotalPages] = useState(0); + const [totalElements, setTotalElements] = useState(0); + + useEffect(() => { + fetchCirculation(); + }, [page, size]); + + const fetchCirculation = async () => { + try { + setIsLoading(true); + const host = window.location.hostname; + const res = await fetch(`http://${host}:8080/api/wallet/circulation?page=${page}&size=${size}`); + const data: PageResponse = await res.json(); + + setTokens(data.content || []); + setTotalPages(data.totalPages || 0); + setTotalElements(data.totalElements || 0); + } catch (error) { + console.error('Error fetching circulation:', error); + } finally { + setIsLoading(false); + } + }; + + const filteredTokens = tokens.filter(t => + t.tokenHash.toLowerCase().includes(searchTerm.toLowerCase()) || + t.ownerId?.toString().includes(searchTerm) + ); + + return ( +
+ {/* Regulated Header */} +
+ Vector BG +
+
+
+ +
+
+

Ritz Forensic Ledger

+
+
+ Strictly Regulated +
+
+
+

+ A comprehensive, non-repudiable audit trail of every individual Ritz unit. + Each token is uniquely serialized and traceable to its point of issuance. +

+
+
+ + {/* Circulation Stats - Summary level only as we now have pagination */} +
+
+
+
+ +
+ Total Audit Population +
+

{totalElements.toLocaleString()} Units

+
+
+
+
+ +
+ Ledger Compliance +
+

100% Validated

+
+
+
+
+ +
+ Current View Range +
+

Page {page + 1} / {totalPages || 1}

+
+
+ + {/* Ledger Table */} +
+
+
+

Circulation Audit

+

Page {page + 1} of {totalPages} (Showing {tokens.length} records)

+
+ +
+
+ + setSearchTerm(e.target.value)} + /> +
+
+ + +
+ +
+
+ +
+ {isLoading ? ( +
+
+ Verifying Ledger Integrity... +
+ ) : ( + + + + + + + + + + + + {filteredTokens.map((t, i) => ( + + + + + + + + ))} + +
Transaction Unit IDCryptographic SerialOwner UIDStatusIssuance Date
+ #{t.id.toString().padStart(6, '0')} + +
+
+ {t.tokenHash} +
+
+
+
+ + {t.ownerId || 'SYSTEM'} +
+
+ + {t.status === 'ACTIVE' ? : } + {t.status} + + +
+ + {new Date(t.createdAt).toLocaleString('en-IN', { + day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' + })} +
+
+ )} +
+
+ + {/* Pagination Footer - Sticky style */} +
+
+ + + +
+ {isLoading ? '...' : `Page ${page + 1} of ${totalPages || 1}`} +
+ + + +
+
+ + {/* Animation Styles */} + +
+ ); +}; + +export default RitzCirculation; diff --git a/frontend/src/pages/Stalls.tsx b/frontend/src/pages/Stalls.tsx index 9baf088e..fd8779a4 100644 --- a/frontend/src/pages/Stalls.tsx +++ b/frontend/src/pages/Stalls.tsx @@ -639,7 +639,7 @@ const Stalls: React.FC = () => {

{product.name}

-

₹{product.price} • {product.category}

+

R{product.price} • {product.category}

diff --git a/frontend/src/pages/StoreDashboard.tsx b/frontend/src/pages/StoreDashboard.tsx index 65d39684..02a8efff 100644 --- a/frontend/src/pages/StoreDashboard.tsx +++ b/frontend/src/pages/StoreDashboard.tsx @@ -185,7 +185,7 @@ const StoreDashboard = () => { - v >= 1000 ? `${v/1000}k` : v} /> + v >= 1000 ? `R${v/1000}k` : `R${v}`} /> @@ -216,7 +216,7 @@ const StoreDashboard = () => {
-

₹{formatCurrency(stats.totalSales)}

+

R{formatCurrency(stats.totalSales)}

diff --git a/ordering_site/src/components/CartTab.tsx b/ordering_site/src/components/CartTab.tsx index 14aec325..cc41c24f 100644 --- a/ordering_site/src/components/CartTab.tsx +++ b/ordering_site/src/components/CartTab.tsx @@ -23,7 +23,7 @@ const CartTab: React.FC = () => { {totalItems} {totalItems === 1 ? 'item' : 'items'}
-
₹{totalPrice.toFixed(0)}
+
R{totalPrice.toFixed(0)}

{item.name}

-

₹{item.price.toFixed(2)}

+

R{item.price.toFixed(2)}

{item.description}

diff --git a/ordering_site/src/pages/CartScreen.tsx b/ordering_site/src/pages/CartScreen.tsx index e90d702d..4d5721d9 100644 --- a/ordering_site/src/pages/CartScreen.tsx +++ b/ordering_site/src/pages/CartScreen.tsx @@ -46,7 +46,7 @@ const CartScreen: React.FC = () => {
- ₹{(item.price * item.quantity).toFixed(2)} + R{(item.price * item.quantity).toFixed(2)}
@@ -87,7 +87,7 @@ const CartScreen: React.FC = () => {
{totalItems} {totalItems === 1 ? 'Item' : 'Items'} - ₹{(totalPrice + 2.5).toFixed(2)} + R{(totalPrice + 2.5).toFixed(2)}

{item.name}

-

₹{item.price.toFixed(2)}

+

R{item.price.toFixed(2)}

@@ -88,7 +88,7 @@ const ItemDetailScreen: React.FC = () => {