User setup added

This commit is contained in:
Sidharth Prabhu
2026-08-03 10:41:57 +05:30
parent 28a13d619c
commit 6b3714f2bc
8 changed files with 692 additions and 54 deletions

View File

@@ -843,7 +843,7 @@ function App() {
<div className="demo-credentials-box"> <div className="demo-credentials-box">
<h4>💡 Dev Sandbox Access:</h4> <h4>💡 Dev Sandbox Access:</h4>
{loginType === 'SYSTEM' ? ( {loginType === 'SYSTEM' ? (
<p>Default Admin: <strong>admin</strong> / Password: <strong>admin123</strong></p> <p>Default Admin: <strong>admin</strong> / Password: <strong>admin</strong></p>
) : ( ) : (
<p>Sign in with your Google account. First-time users will be asked for a mobile number to complete registration.</p> <p>Sign in with your Google account. First-time users will be asked for a mobile number to complete registration.</p>
)} )}

View File

@@ -2,15 +2,21 @@ package com.rit.canteen.sales.service;
import com.rit.canteen.sales.model.BaseItem; import com.rit.canteen.sales.model.BaseItem;
import com.rit.canteen.sales.model.Product; import com.rit.canteen.sales.model.Product;
import com.rit.canteen.sales.model.Stall;
import com.rit.canteen.sales.model.SystemUser;
import com.rit.canteen.sales.repository.BaseItemRepository; import com.rit.canteen.sales.repository.BaseItemRepository;
import com.rit.canteen.sales.repository.ProductRepository; import com.rit.canteen.sales.repository.ProductRepository;
import com.rit.canteen.sales.repository.StallRepository;
import com.rit.canteen.sales.repository.SystemUserRepository;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner; import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import java.util.List; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
@Component @Component
public class DatabaseSeeder implements CommandLineRunner { public class DatabaseSeeder implements CommandLineRunner {
@@ -21,6 +27,15 @@ public class DatabaseSeeder implements CommandLineRunner {
@Autowired @Autowired
private ProductRepository productRepository; private ProductRepository productRepository;
@Autowired
private StallRepository stallRepository;
@Autowired
private SystemUserRepository systemUserRepository;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Autowired @Autowired
private JdbcTemplate jdbcTemplate; private JdbcTemplate jdbcTemplate;
@@ -34,6 +49,8 @@ public class DatabaseSeeder implements CommandLineRunner {
repairLobColumns(); repairLobColumns();
seedCategories(); seedCategories();
seedProducts(); seedProducts();
seedStalls();
seedMasterUser();
} }
private void repairFeedbackSchema() { private void repairFeedbackSchema() {
@@ -254,4 +271,42 @@ public class DatabaseSeeder implements CommandLineRunner {
p.setActive(true); p.setActive(true);
return p; return p;
} }
private void seedStalls() {
if (stallRepository.count() == 0) {
List<Product> allProducts = productRepository.findAll();
Stall s1 = new Stall("Main Canteen Stall", "Primary food counter serving meals, fast food, and beverages", null);
Stall s2 = new Stall("Bakery & Juice Counter", "Fresh bakery items, pastries, snacks, and fresh fruit juices", null);
Stall s3 = new Stall("South Indian Express", "Authentic dosas, idlis, vadas, and South Indian breakfast specials", null);
s1.setProducts(allProducts);
s2.setProducts(allProducts.stream()
.filter(p -> "Bakery & Sweets".equalsIgnoreCase(p.getCategory()) || "Beverages & Drinks".equalsIgnoreCase(p.getCategory()) || "Snacks & Quick Bites".equalsIgnoreCase(p.getCategory()))
.toList());
s3.setProducts(allProducts.stream()
.filter(p -> "Snacks & Quick Bites".equalsIgnoreCase(p.getCategory()) || "Indian Main Course".equalsIgnoreCase(p.getCategory()))
.toList());
stallRepository.saveAll(List.of(s1, s2, s3));
System.out.println(">>> SEEDED DEFAULT STALLS (Main Canteen, Bakery & Juice Counter, South Indian Express)");
}
}
private void seedMasterUser() {
Optional<SystemUser> existingAdmin = systemUserRepository.findByEmail("admin");
if (existingAdmin.isEmpty()) {
SystemUser admin = new SystemUser();
admin.setName("Admin Master");
admin.setEmail("admin");
admin.setPassword(passwordEncoder.encode("admin"));
admin.setRole("MASTER");
admin.setPermissions(List.of("dashboard", "sale", "customers", "purchases", "inventory", "expense", "reports", "stores", "table", "wallet", "promotions", "feedback"));
admin.setViewOnly(false);
systemUserRepository.save(admin);
System.out.println(">>> SEEDED ADMIN MASTER USER (username: admin, password: admin)");
} else {
System.out.println(">>> ADMIN MASTER USER EXISTS. Skipping admin user creation.");
}
}
} }

View File

@@ -27,7 +27,7 @@ app.jwt.expiration-ms=86400000
# Master Account — REQUIRED environment variables # Master Account — REQUIRED environment variables
# ============================================================ # ============================================================
app.master.username=${MASTER_USER:admin} app.master.username=${MASTER_USER:admin}
app.master.password=${MASTER_PASSWORD:admin123} app.master.password=${MASTER_PASSWORD:admin}
# File upload configuration # File upload configuration
spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-file-size=10MB

239
create_master_user.py Normal file
View File

@@ -0,0 +1,239 @@
#!/usr/bin/env python3
"""
Master User Creation Script for Positeasy Canteen Automation System.
This script creates or updates a master administrator user in the database
with username 'admin' and password 'admin', and synchronizes the application configuration.
"""
import sys
import os
import re
import argparse
import bcrypt
import psycopg2
# Default Configuration
DEFAULT_USERNAME = "admin"
DEFAULT_PASSWORD = "admin"
DEFAULT_NAME = "Admin Master"
DEFAULT_ROLE = "MASTER"
DEFAULT_DB_HOST = "localhost"
DEFAULT_DB_PORT = 5432
DEFAULT_DB_NAME = "positeasy"
DEFAULT_DB_USER = "postgres"
DEFAULT_DB_PASS = "sidharth"
ALL_PERMISSIONS = [
"dashboard",
"sale",
"customers",
"purchases",
"inventory",
"expense",
"reports",
"stores",
"table",
"wallet",
"promotions",
"feedback"
]
def hash_password(password: str) -> str:
"""Hash password using bcrypt compatible with Spring Security BCryptPasswordEncoder."""
salt = bcrypt.gensalt(rounds=10)
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
return hashed.decode('utf-8')
def parse_application_properties(prop_path: str) -> dict:
"""Parse db credentials from backend application.properties if available."""
config = {}
if not os.path.exists(prop_path):
return config
with open(prop_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
config[key.strip()] = value.strip()
return config
def update_application_properties(prop_path: str, username: str, password: str) -> bool:
"""Update app.master.username and app.master.password in application.properties."""
if not os.path.exists(prop_path):
print(f"⚠️ [Config] File not found: {prop_path}")
return False
with open(prop_path, 'r', encoding='utf-8') as f:
content = f.read()
# Update or add app.master.username and app.master.password
new_user_line = f"app.master.username=${{MASTER_USER:{username}}}"
new_pass_line = f"app.master.password=${{MASTER_PASSWORD:{password}}}"
if re.search(r'^app\.master\.username=.*$', content, flags=re.MULTILINE):
content = re.sub(r'^app\.master\.username=.*$', new_user_line, content, flags=re.MULTILINE)
else:
content += f"\n{new_user_line}"
if re.search(r'^app\.master\.password=.*$', content, flags=re.MULTILINE):
content = re.sub(r'^app\.master\.password=.*$', new_pass_line, content, flags=re.MULTILINE)
else:
content += f"\n{new_pass_line}"
with open(prop_path, 'w', encoding='utf-8') as f:
f.write(content)
print(f"✅ [Config] Updated {prop_path} with default master credentials ({username}/{password}).")
return True
def update_frontend_demo_credentials(app_tsx_path: str, username: str, password: str) -> bool:
"""Update UI demo credentials displayed in api-dashboard if file exists."""
if not os.path.exists(app_tsx_path):
return False
with open(app_tsx_path, 'r', encoding='utf-8') as f:
content = f.read()
old_pattern = r'Default Admin:\s*<strong>[^<]*</strong>\s*/\s*Password:\s*<strong>[^<]*</strong>'
new_text = f'Default Admin: <strong>{username}</strong> / Password: <strong>{password}</strong>'
if re.search(old_pattern, content):
content = re.sub(old_pattern, new_text, content)
with open(app_tsx_path, 'w', encoding='utf-8') as f:
f.write(content)
print(f"✅ [UI] Updated demo credentials in {app_tsx_path}.")
return True
return False
def create_or_update_master_user(db_params: dict, username: str, password: str, name: str) -> bool:
"""Insert or update master user in PostgreSQL database."""
print(f"\n🔌 Connecting to PostgreSQL database '{db_params['dbname']}' on {db_params['host']}:{db_params['port']}...")
try:
conn = psycopg2.connect(**db_params)
conn.autocommit = False
cur = conn.cursor()
# Check if system_users table exists
cur.execute("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'system_users'
);
""")
table_exists = cur.fetchone()[0]
if not table_exists:
print("❌ Table 'system_users' does not exist in the database. Has backend schema initialized?")
conn.close()
return False
# Check if master user with this username (stored in email column) exists
cur.execute("SELECT id, name, role FROM system_users WHERE email = %s;", (username,))
row = cur.fetchone()
hashed_pass = hash_password(password)
if row:
user_id = row[0]
print(f"🔄 Existing master user found (ID: {user_id}). Updating credentials...")
cur.execute("""
UPDATE system_users
SET name = %s, password = %s, role = %s, view_only = false
WHERE id = %s;
""", (name, hashed_pass, DEFAULT_ROLE, user_id))
else:
print(f" Creating new master user '{username}'...")
cur.execute("""
INSERT INTO system_users (name, email, password, role, view_only)
VALUES (%s, %s, %s, %s, false)
RETURNING id;
""", (name, username, hashed_pass, DEFAULT_ROLE))
user_id = cur.fetchone()[0]
# Reset permissions for this user
cur.execute("DELETE FROM system_user_permissions WHERE user_id = %s;", (user_id,))
for perm in ALL_PERMISSIONS:
cur.execute("""
INSERT INTO system_user_permissions (user_id, permission)
VALUES (%s, %s);
""", (user_id, perm))
conn.commit()
# Verification query
cur.execute("""
SELECT u.id, u.name, u.email, u.role, count(p.permission)
FROM system_users u
LEFT JOIN system_user_permissions p ON u.id = p.user_id
WHERE u.id = %s
GROUP BY u.id, u.name, u.email, u.role;
""", (user_id,))
ver = cur.fetchone()
cur.close()
conn.close()
print("\n" + "=" * 50)
print("🎉 MASTER USER CREATED / UPDATED SUCCESSFULLY")
print("=" * 50)
print(f" User ID: {ver[0]}")
print(f" Name: {ver[1]}")
print(f" Username: {ver[2]}")
print(f" Password: {password}")
print(f" Role: {ver[3]}")
print(f" Permissions: {ver[4]} granted ({', '.join(ALL_PERMISSIONS[:4])}...)")
print("=" * 50 + "\n")
return True
except Exception as e:
print(f"❌ Database error: {e}")
return False
def main():
parser = argparse.ArgumentParser(description="Create or update master user for Positeasy Canteen Automation.")
parser.add_argument("-u", "--username", default=DEFAULT_USERNAME, help="Master username (default: admin)")
parser.add_argument("-p", "--password", default=DEFAULT_PASSWORD, help="Master password (default: admin)")
parser.add_argument("-n", "--name", default=DEFAULT_NAME, help="Display name for master user")
parser.add_argument("--host", default=DEFAULT_DB_HOST, help="PostgreSQL host (default: localhost)")
parser.add_argument("--port", type=int, default=DEFAULT_DB_PORT, help="PostgreSQL port (default: 5432)")
parser.add_argument("--dbname", default=DEFAULT_DB_NAME, help="PostgreSQL database name (default: positeasy)")
parser.add_argument("--dbuser", default=DEFAULT_DB_USER, help="PostgreSQL database user (default: postgres)")
parser.add_argument("--dbpass", default=DEFAULT_DB_PASS, help="PostgreSQL database password")
args = parser.parse_args()
project_root = os.path.dirname(os.path.abspath(__file__))
prop_path = os.path.join(project_root, "backend", "src", "main", "resources", "application.properties")
app_tsx_path = os.path.join(project_root, "api-dashboard", "src", "App.tsx")
db_params = {
"host": args.host,
"port": args.port,
"dbname": args.dbname,
"user": args.dbuser,
"password": args.dbpass
}
print("🚀 Starting Master User setup script...")
# 1. Database creation / update
db_success = create_or_update_master_user(db_params, args.username, args.password, args.name)
# 2. Sync application.properties
update_application_properties(prop_path, args.username, args.password)
# 3. Sync frontend dashboard UI
update_frontend_demo_credentials(app_tsx_path, args.username, args.password)
if db_success:
print("✨ All steps completed successfully! You can now log in with:")
print(f" Username: {args.username}")
print(f" Password: {args.password}")
else:
print("⚠️ Database operation failed or incomplete. Check database connectivity.")
if __name__ == "__main__":
main()

View File

@@ -97,7 +97,7 @@ const ItemCard: React.FC<ItemCardProps> = ({ item, isLast, variant = 'list' }) =
) : ( ) : (
<div className="list-placeholder">🍲</div> <div className="list-placeholder">🍲</div>
)} )}
{item.isPopular && <span className="bestseller-badge">Bestseller</span>} {item.isPopular && item.stock !== 0 && <span className="bestseller-badge">Bestseller</span>}
{item.stock === 0 && <span className="soldout-badge">Sold Out</span>} {item.stock === 0 && <span className="soldout-badge">Sold Out</span>}
</div> </div>

View File

@@ -267,3 +267,328 @@ main::-webkit-scrollbar {
.veg-nonveg-indicator.non-veg .dot { .veg-nonveg-indicator.non-veg .dot {
background-color: #ef4444; background-color: #ef4444;
} }
/* DESKTOP SCREEN OPTIMIZATIONS */
@media (min-width: 769px) {
body {
display: block;
background-color: var(--surface);
}
.container {
max-width: 100% !important;
width: 100% !important;
margin: 0 !important;
height: auto !important;
min-height: 100vh !important;
border-radius: 0 !important;
border: none !important;
box-shadow: none !important;
background-color: var(--surface) !important;
}
main {
overflow-y: visible !important;
}
/* Home Screen layout adjustments */
.welcome-section {
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
padding: 32px 60px 16px !important;
}
.search-bar-container {
padding: 16px 60px 24px !important;
}
.search-bar {
max-width: 550px !important;
}
/* Grid listings on desktop - Full Width */
.categories-grid {
grid-template-columns: repeat(10, 1fr) !important;
gap: 20px !important;
padding: 0 60px 24px !important;
}
.category-image-wrapper {
max-width: 90px !important;
max-height: 90px !important;
}
.stalls-carousel {
display: grid !important;
grid-template-columns: repeat(4, 1fr) !important;
gap: 24px !important;
padding: 16px 60px 24px !important;
overflow-x: visible !important;
}
.stall-card {
min-width: 100% !important;
max-width: 100% !important;
height: 200px !important;
}
.popular-carousel {
display: grid !important;
grid-template-columns: repeat(5, 1fr) !important;
gap: 24px !important;
padding: 16px 60px 24px !important;
overflow-x: visible !important;
}
/* Multi-column items listing */
.recommended-section .items-list,
.search-results-section .items-list,
.stall-items-section .items-list {
display: grid !important;
grid-template-columns: repeat(3, 1fr) !important;
gap: 24px !important;
padding: 16px 60px 24px !important;
}
.section-header {
padding: 24px 60px 12px !important;
}
/* Floating Pill Bottom Navigation Bar */
.bottom-nav {
position: fixed !important;
bottom: 24px !important;
left: 50% !important;
transform: translateX(-50%) !important;
width: 500px !important;
border-radius: 100px !important;
box-shadow: 0 10px 40px rgba(44, 58, 42, 0.15) !important;
border: 1px solid var(--border) !important;
height: 64px !important;
padding-bottom: 0 !important;
z-index: 1001 !important;
background: var(--surface) !important;
}
/* Redesigned Item Detail Page for Desktop */
.item-detail-page {
display: flex !important;
flex-direction: column !important;
padding: 0 !important;
height: auto !important;
}
.item-detail-page .detail-header-overlay {
position: static !important;
width: 100% !important;
border-bottom: 1px solid var(--border) !important;
}
.item-detail-page main {
display: flex !important;
flex-direction: row !important;
gap: 40px !important;
padding: 40px 60px !important;
align-items: flex-start !important;
}
.item-detail-page .item-hero {
flex: 1 !important;
height: 480px !important;
border-radius: 20px !important;
box-shadow: 0 10px 30px rgba(44, 58, 42, 0.08) !important;
overflow: hidden !important;
}
.item-detail-page .item-hero-image {
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
}
.item-detail-page .item-details-content {
flex: 1.2 !important;
margin-top: 0 !important;
padding: 0 !important;
background: transparent !important;
box-shadow: none !important;
border-radius: 0 !important;
}
.item-detail-page .item-name-large {
font-size: 2.2rem !important;
font-weight: 850 !important;
color: var(--text-dark) !important;
}
.item-detail-page .item-subtitle {
font-size: 1rem !important;
color: var(--primary) !important;
font-weight: 700 !important;
}
.item-detail-page .item-long-description {
font-size: 0.95rem !important;
line-height: 1.6 !important;
color: var(--text-mid) !important;
margin-bottom: 24px !important;
}
.item-detail-page .item-extra-info {
background: var(--primary-light) !important;
border-radius: 16px !important;
padding: 20px !important;
border: 1px solid var(--border) !important;
margin-bottom: 24px !important;
}
.item-detail-page .item-footer {
position: static !important;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
background: var(--surface) !important;
border: 1px solid var(--border) !important;
border-radius: 20px !important;
padding: 20px 24px !important;
box-shadow: var(--shadow) !important;
margin-top: 24px !important;
}
.item-detail-page .footer-price-info {
display: flex !important;
flex-direction: column !important;
}
.item-detail-page .total-label {
font-size: 0.8rem !important;
text-transform: uppercase !important;
letter-spacing: 0.5px !important;
}
.item-detail-page .total-value {
font-size: 1.8rem !important;
font-weight: 850 !important;
color: var(--primary) !important;
}
.item-detail-page .primary-action-button {
height: 48px !important;
padding: 0 32px !important;
font-size: 0.95rem !important;
border-radius: 100px !important;
width: auto !important;
min-width: 160px !important;
}
/* Stall Detail Page Split Flow */
.stall-detail-page main.detail-content {
display: flex !important;
gap: 40px !important;
padding: 24px 60px !important;
align-items: flex-start !important;
}
.stall-main-card {
flex: 1 !important;
max-width: 320px !important;
margin: 0 !important;
position: sticky !important;
top: 24px !important;
background: var(--surface) !important;
padding: 24px !important;
border-radius: 20px !important;
border: 1px solid var(--border) !important;
box-shadow: var(--shadow) !important;
z-index: 100 !important;
}
.stall-items-section {
flex: 2.2 !important;
padding: 0 !important;
}
.stall-items-section .section-title {
padding: 0 0 16px 0 !important;
}
.stall-items-section .items-list {
display: grid !important;
grid-template-columns: repeat(2, 1fr) !important;
gap: 20px !important;
padding: 0 !important;
}
/* Cart split flow */
.cart-page main {
display: flex !important;
gap: 40px !important;
padding: 24px 60px !important;
align-items: flex-start !important;
}
.cart-items-section {
flex: 1.6 !important;
}
.bill-details {
flex: 1 !important;
margin-top: 0 !important;
background: var(--surface) !important;
padding: 24px !important;
border-radius: 16px !important;
border: 1px solid var(--border) !important;
box-shadow: var(--shadow) !important;
}
.cart-footer {
padding: 20px 60px !important;
border-top: 1px solid var(--border) !important;
}
/* Checkout split flow */
.checkout-page main {
display: flex !important;
gap: 40px !important;
padding: 24px 60px !important;
align-items: flex-start !important;
}
.checkout-section {
flex: 1.6 !important;
margin-bottom: 0 !important;
background: transparent !important;
}
.checkout-summary-container-desktop {
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
gap: 24px !important;
}
.checkout-page .order-summary-mini,
.checkout-page .insufficient-warning-premium {
margin: 0 !important;
background: var(--surface) !important;
padding: 24px !important;
border-radius: 16px !important;
border: 1px solid var(--border) !important;
box-shadow: var(--shadow) !important;
}
/* Handle footer buttons in desktop splits */
.checkout-footer {
display: none !important;
}
.desktop-checkout-action {
display: block !important;
}
}
/* Hide desktop checkout actions on mobile */
.desktop-checkout-action {
display: none;
}

View File

@@ -336,52 +336,64 @@ const CheckoutScreen: React.FC = () => {
</div> </div>
</section> </section>
<div className="order-summary-mini"> <div className="checkout-summary-container-desktop">
<div className="summary-row"> <div className="order-summary-mini">
<span>{paymentMethod === 'RAZORPAY' ? 'Amount payable' : 'Tokens to be deducted'}</span> <div className="summary-row">
<span className="summary-price ritz-text"> <span>{paymentMethod === 'RAZORPAY' ? 'Amount payable' : 'Tokens to be deducted'}</span>
{paymentMethod === 'RAZORPAY' ? '₹' : '🅡'}{totalPrice.toLocaleString()} <span className="summary-price ritz-text">
</span> {paymentMethod === 'RAZORPAY' ? '₹' : '🅡'}{totalPrice.toLocaleString()}
</span>
</div>
<p className="tax-info">Inclusive of applicable item prices · No extra platform fee</p>
</div>
{statusMessage && (
<motion.div
className="insufficient-warning-premium"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
style={{ margin: '0 16px 12px' }}
>
<div className="warning-icon-wrapper">
<AlertCircle size={18} />
</div>
<div className="warning-content">
<p className="warning-instruction" style={{ margin: 0 }}>{statusMessage}</p>
</div>
</motion.div>
)}
{isInsufficient && paymentMethod === 'RITZ_TOKEN' && (
<motion.div
className="insufficient-warning-premium"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<div className="warning-icon-wrapper token-warning-visual">
<img src={tokenImage} alt="" className="token-image-mini" />
<div className="warning-icon-overlay">
<AlertCircle size={16} />
</div>
</div>
<div className="warning-content">
<div className="shortfall-amount">
Short by <span className="highlight">🅡{(totalPrice - currentBalance).toLocaleString()}</span>
</div>
<p className="warning-instruction">Top up tokens, or pay online with Razorpay instead.</p>
</div>
</motion.div>
)}
<div className="desktop-checkout-action">
<button
className="place-order-button ritz-order-btn"
onClick={handlePlaceOrder}
disabled={isProcessing || cart.length === 0 || (paymentMethod === 'RITZ_TOKEN' && isInsufficient)}
>
{payLabel()}
</button>
</div> </div>
<p className="tax-info">Inclusive of applicable item prices · No extra platform fee</p>
</div> </div>
{statusMessage && (
<motion.div
className="insufficient-warning-premium"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
style={{ margin: '0 16px 12px' }}
>
<div className="warning-icon-wrapper">
<AlertCircle size={18} />
</div>
<div className="warning-content">
<p className="warning-instruction" style={{ margin: 0 }}>{statusMessage}</p>
</div>
</motion.div>
)}
{isInsufficient && paymentMethod === 'RITZ_TOKEN' && (
<motion.div
className="insufficient-warning-premium"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<div className="warning-icon-wrapper token-warning-visual">
<img src={tokenImage} alt="" className="token-image-mini" />
<div className="warning-icon-overlay">
<AlertCircle size={16} />
</div>
</div>
<div className="warning-content">
<div className="shortfall-amount">
Short by <span className="highlight">🅡{(totalPrice - currentBalance).toLocaleString()}</span>
</div>
<p className="warning-instruction">Top up tokens, or pay online with Razorpay instead.</p>
</div>
</motion.div>
)}
</main> </main>
<div className="checkout-footer"> <div className="checkout-footer">

View File

@@ -7,16 +7,23 @@
position: relative; position: relative;
height: 220px; height: 220px;
width: 100%; width: 100%;
z-index: 1 !important;
} }
.detail-header.sticky { .detail-header.sticky {
height: auto; height: auto;
padding: 12px 16px; padding: 12px 16px;
background: var(--surface); background: var(--surface);
z-index: 100; z-index: 100 !important;
box-shadow: var(--shadow); box-shadow: var(--shadow);
} }
.detail-content {
position: relative;
z-index: 50 !important;
margin-top: -40px !important;
}
.stall-detail-page .add-to-cart-container { .stall-detail-page .add-to-cart-container {
position: absolute; position: absolute;
bottom: -12px; bottom: -12px;
@@ -77,13 +84,13 @@
} }
.stall-main-card { .stall-main-card {
margin: -40px 16px 20px; margin: 0 16px 20px;
background: var(--surface); background: var(--surface) !important;
border-radius: 24px; border-radius: 24px;
padding: 24px; padding: 20px;
position: relative; position: relative;
box-shadow: var(--shadow); box-shadow: var(--shadow);
z-index: 5; z-index: 100 !important;
} }
.stall-header-info { .stall-header-info {