User setup added
This commit is contained in:
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
239
create_master_user.py
Normal 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()
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user