diff --git a/backend/zato-csm-backend/config/init_database.py b/backend/zato-csm-backend/config/init_database.py
index d0606725..92b00312 100644
--- a/backend/zato-csm-backend/config/init_database.py
+++ b/backend/zato-csm-backend/config/init_database.py
@@ -28,7 +28,7 @@ def create_tables_sql():
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL,
min_stock INT DEFAULT 0,
- category_id INT,
+ category VARCHAR(255) NOT NULL,
images JSONB,
status VARCHAR(20) DEFAULT 'active',
weight DECIMAL(10,2),
@@ -39,8 +39,7 @@ def create_tables_sql():
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_updated TIMESTAMP DEFAULT NOW(),
localization TEXT,
- FOREIGN KEY (creator_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE RESTRICT,
- FOREIGN KEY (category_id) REFERENCES categories(id) ON UPDATE CASCADE ON DELETE RESTRICT
+ FOREIGN KEY (creator_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS inventory_movements(
diff --git a/backend/zato-csm-backend/models/product.py b/backend/zato-csm-backend/models/product.py
index c77224f8..f295d346 100644
--- a/backend/zato-csm-backend/models/product.py
+++ b/backend/zato-csm-backend/models/product.py
@@ -3,62 +3,86 @@
from datetime import datetime
from enum import Enum
+
class ProductType(str, Enum):
PHYSICAL_PRODUCT = "Physical Product"
SERVICE = "Service"
DIGITAL = "Digital"
+
class ProductStatus(str, Enum):
ACTIVE = "active"
INACTIVE = "inactive"
+
class ProductUnity(str, Enum):
PER_ITEM = "Per item"
PER_KILOGRAM = "Per kilogram"
PER_LITER = "Per liter"
+ PER_METRO = "Per metro"
+
+
+class ProductCategory(str, Enum):
+ FURNITURE = "Furniture"
+ TEXTILES = "Textiles"
+ LIGHTING = "Lighting"
+ ELECTRONICS = "Electronics"
+ DECORATION = "Decoration"
+ OFFICE = "Office"
+ GAMING = "Gaming"
+
class CreateProductRequest(BaseModel):
- # Default fields
- name: str = Field(..., min_length=1, description="Product name")
- price: float = Field(..., gt=0, description="Product price")
- stock: int = Field(..., gt=0, description="Product stock")
+ name: str = Field(..., min_length=1)
+ price: float = Field(..., gt=0)
+ stock: int = Field(..., ge=0)
unit: ProductUnity = Field(...)
product_type: ProductType = Field(...)
+ category: str = Field(...)
- # Optional fields
- description: Optional[str] = Field(None, description="Product description")
- category_id: Optional[str] = Field(None, description="Product category", gt=0)
- sku: Optional[str] = Field(None, max_length=255, description="Product SKU")
- weight: Optional[float] = Field(None, gt=0, description="Product weight")
- localization: Optional[str] = Field(None, description="Product localization")
- min_stock: Optional[int] = Field(..., gt=0, description="Product minimum stock")
+ description: Optional[str] = None
+ sku: Optional[str] = Field(None, max_length=255)
+ weight: Optional[float] = Field(None, gt=0)
+ localization: Optional[str] = None
+ min_stock: int = Field(0, ge=0)
status: ProductStatus = Field(ProductStatus.ACTIVE)
- @validator("name")
- def validate_name(cls, v):
- if not v:
- raise ValueError("Name is mandatory")
- return v
-
@validator("sku")
- def validate_price(cls, v):
- if v and len(v.strip()) == 0:
+ def _normalize_sku(cls, v):
+ if v is not None and v.strip() == "":
return None
return v
+ @validator("category")
+ def _validate_category(cls, v):
+ valid = {c.value for c in ProductCategory}
+ if v not in valid:
+ raise ValueError(f"Invalid category. Allowed: {', '.join(sorted(valid))}")
+ return v
+
+
class UpdateProductRequest(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=255)
- description: Optional[str] = Field(None)
+ description: Optional[str] = None
price: Optional[float] = Field(None, gt=0)
stock: Optional[int] = Field(None, ge=0)
- category_id: Optional[str] = Field(None, gt=0)
+ category: Optional[str] = None
sku: Optional[str] = Field(None, max_length=255)
weight: Optional[float] = Field(None, ge=0)
- localization: Optional[str] = Field(None)
+ localization: Optional[str] = None
min_stock: Optional[int] = Field(None, ge=0)
- status: Optional[ProductStatus] = Field(None)
- product_type: Optional[ProductType] = Field(None)
- unit: Optional[ProductUnity] = Field(None)
+ status: Optional[ProductStatus] = None
+ product_type: Optional[ProductType] = None
+ unit: Optional[ProductUnity] = None
+
+ @validator("category")
+ def _validate_category_update(cls, v):
+ if v is None:
+ return v
+ valid = {c.value for c in ProductCategory}
+ if v not in valid:
+ raise ValueError(f"Invalid category. Allowed: {', '.join(sorted(valid))}")
+ return v
class ProductResponse(BaseModel):
@@ -68,7 +92,7 @@ class ProductResponse(BaseModel):
price: float
stock: int
min_stock: int
- category_id: Optional[int]
+ category: str
images: Optional[List[str]] = []
status: str
weight: Optional[float]
@@ -82,4 +106,3 @@ class ProductResponse(BaseModel):
class Config:
from_attributes = True
-
diff --git a/backend/zato-csm-backend/repositories/product_repositories.py b/backend/zato-csm-backend/repositories/product_repositories.py
index e0ad5ee2..9d221704 100644
--- a/backend/zato-csm-backend/repositories/product_repositories.py
+++ b/backend/zato-csm-backend/repositories/product_repositories.py
@@ -5,46 +5,56 @@
from utils.timezone_utils import get_current_time_with_timezone
+
class ProductRepository(BaseRepository):
def create_product(
self,
name: str,
+ description: str | None,
price: float,
stock: int,
unit: str,
product_type: str,
+ category: str,
+ sku: str | None,
+ min_stock: int,
+ status: str,
+ weight: float | None,
+ localization: str | None,
creator_id: int,
- description: str = None,
- category_id: int = None,
- images: list = None,
- min_stock: int=0,
- sku: str=None,
- status: str="active",
- weight: float=0.0,
- localization: str=None,
- user_timezone: str = "UTC",
):
- last_updated = get_current_time_with_timezone(user_timezone)
- created_at = get_current_time_with_timezone(user_timezone)
-
- images_json = json.dumps(images) if images else json.dumps([])
-
- with self._get_cursor() as cursor:
- cursor.execute(
- "INSERT INTO products ("
- "name, description, price, stock, category_id, images, "
- "min_stock, status, weight, sku, creator_id, unit,"
- "product_type, created_at, last_updated, localization)"
- "VALUES ( %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING *",
- (
- name, description, price, stock, category_id, images_json,
- min_stock, status, weight, sku, creator_id, unit, product_type,
- created_at, last_updated, localization
- ),
- )
- self.db.commit()
- return cursor.fetchone()
+ query = """
+ INSERT INTO products
+ (name, description, price, stock, min_stock, category, images, status, weight, sku, creator_id, unit, product_type, localization)
+ VALUES
+ (%s, %s, %s, %s, %s, %s, '[]'::jsonb, %s, %s, %s, %s, %s, %s, %s)
+ RETURNING id, name, description, price, stock, min_stock, category, images, status, weight, sku, creator_id, unit, product_type, created_at, last_updated, localization
+ """
+ cur = self.db.cursor()
+ cur.execute(
+ query,
+ (
+ name,
+ description,
+ price,
+ stock,
+ min_stock,
+ category,
+ status,
+ weight,
+ sku,
+ creator_id,
+ unit,
+ product_type,
+ localization,
+ ),
+ )
+ row = cur.fetchone()
+ self.db.commit()
+ colnames = [desc[0] for desc in cur.description]
+ cur.close()
+ return dict(zip(colnames, row))
def update_product(self, product_id, updates: dict, user_timezone: str = "UTC"):
# Protecting the created_at and id Update field
@@ -83,7 +93,9 @@ def find_by_id(self, product_id: int):
def find_by_category(self, category_id: int):
with self._get_cursor() as cursor:
- cursor.execute("SELECT * FROM products WHERE category_id=%s", (category_id,))
+ cursor.execute(
+ "SELECT * FROM products WHERE category_id=%s", (category_id,)
+ )
return cursor.fetchall()
def find_by_name(self, name: str):
@@ -101,3 +113,8 @@ def delete_product(self, product_id: int):
if not product:
raise HTTPException(status_code=404, detail="Product not found")
return product
+
+ def find_by_creator(self, creator_id: int):
+ with self._get_cursor() as cursor:
+ cursor.execute("SELECT * FROM products WHERE creator_id=%s", (creator_id,))
+ return cursor.fetchall()
diff --git a/backend/zato-csm-backend/routes/auth.py b/backend/zato-csm-backend/routes/auth.py
index e3d1b3f8..f25835d7 100644
--- a/backend/zato-csm-backend/routes/auth.py
+++ b/backend/zato-csm-backend/routes/auth.py
@@ -147,3 +147,11 @@ def update_profile(
if not current_user.get("admin"):
raise HTTPException(status_code=403, detail="Acess denied")
return auth_service.update_profile(user_id, updates)
+
+
+@router.get("/check-email")
+def check_email(email: str, db=Depends(get_db_connection)):
+ with db.cursor() as cursor:
+ cursor.execute("SELECT id FROM users WHERE email=%s", (email,))
+ row = cursor.fetchone()
+ return {"exists": bool(row)}
diff --git a/backend/zato-csm-backend/routes/inventory.py b/backend/zato-csm-backend/routes/inventory.py
index 5c0a12be..b9e612ae 100644
--- a/backend/zato-csm-backend/routes/inventory.py
+++ b/backend/zato-csm-backend/routes/inventory.py
@@ -21,6 +21,14 @@ def get_inventory(
return {"success": True, "inventory": inventory}
+@router.get("/user")
+def get_user_inventory(
+ user=Depends(get_current_user), inventory_service=Depends(_get_inventory_service)
+):
+ inventory = inventory_service.get_inventory_by_user(user["id"])
+ return {"success": True, "inventory": inventory}
+
+
@router.put("/{product_id}")
def update_inventory(
product_id: int,
diff --git a/backend/zato-csm-backend/routes/products.py b/backend/zato-csm-backend/routes/products.py
index 646974b1..6f5c8141 100644
--- a/backend/zato-csm-backend/routes/products.py
+++ b/backend/zato-csm-backend/routes/products.py
@@ -34,16 +34,12 @@ def _get_product_service(db=Depends(get_db_connection)) -> ProductService:
@router.post("/", response_model=ProductResponse)
def create_product(
product_data: CreateProductRequest,
- images: List[UploadFile] = File(None),
- # request: Request,
current_user=Depends(get_current_user),
product_service=Depends(_get_product_service),
):
- # user_timezone = get_user_timezone_from_request(request)
product = product_service.create_product(
product_data,
creator_id=current_user['id'],
- images=images
)
return ProductResponse(**product)
diff --git a/backend/zato-csm-backend/services/inventory_service.py b/backend/zato-csm-backend/services/inventory_service.py
index e3cfc273..790fed14 100644
--- a/backend/zato-csm-backend/services/inventory_service.py
+++ b/backend/zato-csm-backend/services/inventory_service.py
@@ -10,15 +10,28 @@ def __init__(self, product_repo: ProductRepository):
def get_inventory(self):
products = self.product_repo.find_all()
- inventory = []
+ return [
+ {
+ "id": p["id"],
+ "product_id": p["id"],
+ "quantity": p.get("stock", 0),
+ "location": p.get("location"),
+ "updated_at": p.get("last_updated", datetime.now().isoformat() + "Z"),
+ "product": p,
+ }
+ for p in products
+ ]
+ def get_inventory_by_user(self, user_id: int):
+ products = self.product_repo.find_by_creator(user_id)
return [
{
"id": p["id"],
- "productId": p["id"],
- "quantity": p["stock"],
- "minStock": p.get("min_stock", 0),
- "lastUpdated": p.get("last_updated", datetime.now().isoformat() + "Z"),
+ "product_id": p["id"],
+ "quantity": p.get("stock", 0),
+ "location": p.get("location"),
+ "updated_at": p.get("last_updated", datetime.now().isoformat() + "Z"),
+ "product": p,
}
for p in products
]
@@ -36,36 +49,32 @@ def update_stock(self, product_id: int, quantity: int, user_timezone: str = "UTC
return {
"id": updated_product["id"],
- "productId": updated_product["id"],
- "quantity": updated_product["stock"],
- "lastUpdated": updated_product.get(
- "last_updated", datetime.now().isoformat() + "Z"
- ),
+ "product_id": updated_product["id"],
+ "quantity": updated_product.get("stock", 0),
+ "updated_at": updated_product.get("last_updated", datetime.now().isoformat() + "Z"),
+ "product": updated_product,
}
def check_low_stock(self, min_threshold: int = 0):
- """Function to check low stock products"""
products = self.product_repo.find_all()
-
return [
{
"id": p["id"],
- "currentStock": p["stock"],
- "minStock": min_threshold,
- "needRestock": True,
+ "product_id": p["id"],
+ "quantity": p.get("stock", 0),
+ "min_threshold": min_threshold,
+ "need_restock": True,
+ "product": p,
}
for p in products
- if p["stock"] <= min_threshold
+ if p.get("stock", 0) <= min_threshold
]
def get_inventory_summary(self):
- """Inventory summary functionality"""
products = self.product_repo.find_all()
-
total_products = len(products)
- total_stock = sum(p["stock"] for p in products)
- low_stock_count = len([p for p in products if p["stock"] <= 0])
-
+ total_stock = sum(p.get("stock", 0) for p in products)
+ low_stock_count = len([p for p in products if p.get("stock", 0) <= 0])
return {
"totalProducts": total_products,
"totalStock": total_stock,
diff --git a/backend/zato-csm-backend/services/product_service.py b/backend/zato-csm-backend/services/product_service.py
index f5de23cb..67224f7a 100644
--- a/backend/zato-csm-backend/services/product_service.py
+++ b/backend/zato-csm-backend/services/product_service.py
@@ -3,122 +3,136 @@
from models.product import CreateProductRequest
from repositories.product_repositories import ProductRepository
-from PIL import Image
-import os
-import io
+
class ProductService:
- def __init__(self, product_repo: ProductRepository):
- self.product_repo = product_repo
-
- def create_product(
- self,
- product_data: CreateProductRequest,
- creator_id: int,
- images: List = None
- ):
- # Processar upload de imagens
- images_paths = self._process_images(images) if images else []
+ def __init__(self, repo: ProductRepository):
+ self.repo = repo
+
+ def create_product(self, product_data: CreateProductRequest, creator_id: int):
+ unit_val = getattr(product_data.unit, "value", product_data.unit)
+ type_val = getattr(
+ product_data.product_type, "value", product_data.product_type
+ )
+ category_val = getattr(product_data.category, "value", product_data.category)
- # Criar produto
- product = self.product_repo.create_product(
+ return self.repo.create_product(
name=product_data.name,
description=product_data.description,
- price=product_data.price,
- stock=product_data.stock,
- category_id=product_data.category_id,
- images=images_paths,
+ price=float(product_data.price),
+ stock=int(product_data.stock),
+ unit=unit_val,
+ product_type=type_val,
+ category=category_val,
sku=product_data.sku,
+ min_stock=int(getattr(product_data, "min_stock", 0)),
+ status=getattr(
+ getattr(product_data, "status", "active"),
+ "value",
+ getattr(product_data, "status", "active"),
+ ),
+ weight=getattr(product_data, "weight", None),
+ localization=getattr(product_data, "localization", None),
creator_id=creator_id,
- unit=product_data.unit.value,
- product_type=product_data.product_type.value,
- weight=product_data.weight,
- localization=product_data.localization,
- min_stock=product_data.min_stock or 0,
- status=product_data.status.value
)
- return product
-
- def _process_images(self, images: List):
- if not images:
- return []
-
- ALLOWED_EXTENSIONS = {"jpg", "jpeg", "png", "gif", "webp"}
- MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB em bytes
-
- image_paths = []
- upload_dir = "uploads/products/"
- os.makedirs(upload_dir, exist_ok=True)
-
- for image in images:
- ext = os.path.splitext(image.filename)[1].lower()
-
- # validação de formatos
- if ext not in ALLOWED_EXTENSIONS:
- raise HTTPException(status_code=400, detail=f"Invalid image format. Allowed: {'.'.join(ALLOWED_EXTENSIONS)}")
-
- image_content = image.file.read()
-
- # validação de tamanho
- file_size = len(image_content)
- if file_size > MAX_FILE_SIZE:
- size_mb = file_size / (1024 * 1024)
- raise HTTPException(status_code=400, detail=f"Image too large: {size_mb:.1f}MB. Maximum allowed: 5MB")
-
- # processamento da imagem
- try:
- img = Image.open(io.BytesIO(image_content))
- img.verify() # prevenção de uploads maliciosos
- except Exception:
- raise HTTPException(status_code=400, detail=f"Invalid image file: {image.filename}")
-
- filename = f"product-{int(os.times()[4] * 1000)}-{os.getpid()}{ext}"
- filepath = os.path.join(upload_dir, filename)
- with open(filepath, "wb") as f:
- f.write(image.file.read())
- image_paths.append(f"/uploads/products/{filename}")
-
- # resert file pointer para as próximas operações
- # image.file.seek(0)
- return image_paths
def list_products(self):
- # Buscar todos os produtos
- return self.product_repo.find_all()
+ return self.repo.find_all()
def search_by_category(self, category: str):
- return self.product_repo.find_by_category(category)
+ return self.repo.find_by_category(category)
def search_by_name(self, name: str):
- return self.product_repo.find_by_name(name)
+ return self.repo.find_by_name(name)
def get_product(self, product_id):
if not product_id or product_id <= 0:
raise HTTPException(status_code=400, detail="Invalid product ID")
- product = self.product_repo.find_by_id(product_id)
+ product = self.repo.find_by_id(product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
return product
- def update_product(self, product_id: int, updates: dict, user_timezone: str = "UTC"):
- # Validation allowed fields
- allowed_fields = ["name", "description", "price", "stock", "category", "images"]
+ def update_product(
+ self, product_id: int, updates: dict, user_timezone: str = "UTC"
+ ):
+ allowed_fields = [
+ "name",
+ "description",
+ "price",
+ "stock",
+ "category",
+ "images",
+ "sku",
+ "weight",
+ "localization",
+ "min_stock",
+ "status",
+ "product_type",
+ "unit",
+ ]
for field in list(updates.keys()):
if field not in allowed_fields:
raise HTTPException(status_code=400, detail=f"Invalid field: {field}")
- if "price" in updates and updates["price"] <= 0:
- raise HTTPException(status_code=400, detail="Price must be positive")
- if "stock" in updates and updates["stock"] < 0:
- raise HTTPException(status_code=400, detail="Stock cannot be negative")
+ for fld in (
+ "product_type",
+ "unit",
+ "category",
+ "description",
+ "localization",
+ "sku",
+ "status",
+ ):
+ val = updates.get(fld)
+ if val == "" or val is None:
+ updates.pop(fld, None)
+
+ if "price" in updates:
+ try:
+ updates["price"] = float(updates["price"])
+ if updates["price"] <= 0:
+ raise HTTPException(
+ status_code=400, detail="Price must be positive"
+ )
+ except (ValueError, TypeError):
+ raise HTTPException(status_code=400, detail="Invalid price value")
+
+ if "stock" in updates:
+ try:
+ updates["stock"] = int(updates["stock"])
+ if updates["stock"] < 0:
+ raise HTTPException(
+ status_code=400, detail="Stock cannot be negative"
+ )
+ except (ValueError, TypeError):
+ raise HTTPException(status_code=400, detail="Invalid stock value")
+
+ if "min_stock" in updates:
+ try:
+ updates["min_stock"] = int(updates["min_stock"])
+ if updates["min_stock"] < 0:
+ raise HTTPException(
+ status_code=400, detail="min_stock cannot be negative"
+ )
+ except (ValueError, TypeError):
+ raise HTTPException(status_code=400, detail="Invalid min_stock value")
+
+ if "weight" in updates:
+ try:
+ updates["weight"] = float(updates["weight"])
+ if updates["weight"] < 0:
+ raise HTTPException(status_code=400, detail="Invalid weight value")
+ except (ValueError, TypeError):
+ raise HTTPException(status_code=400, detail="Invalid weight value")
images = updates.get("images")
if images and isinstance(images, list):
updates["images"] = self._process_images(images)
- return self.product_repo.update_product(product_id, updates, user_timezone)
+ return self.repo.update_product(product_id, updates, user_timezone)
def delete_product(self, product_id):
- return self.product_repo.delete_product(product_id)
+ return self.repo.delete_product(product_id)
diff --git a/frontend/index.html b/frontend/index.html
index 4a403743..0912411a 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1,10 +1,10 @@
-
+
-
+
- FrontPOSw - Inventory Management System
+ ZatoBox
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index d8e16ff9..cd4012a2 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -6051,7 +6051,7 @@
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
- "devOptional": true,
+ "dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 76a14122..1504e84d 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -5,7 +5,7 @@ import {
Navigate,
Outlet,
} from 'react-router-dom';
-import { AuthProvider } from './contexts/AuthContext';
+import { AuthProvider, useAuth } from './contexts/AuthContext';
import { PluginProvider } from './contexts/PluginContext';
import ProtectedRoute from './components/ProtectedRoute';
import SideMenu from './components/SideMenu';
@@ -34,6 +34,18 @@ function AppLayout() {
);
}
+function RootElement() {
+ const { isAuthenticated } = useAuth();
+ if (isAuthenticated) {
+ return (
+
+
+
+ );
+ }
+ return ;
+}
+
function App() {
return (
@@ -48,11 +60,7 @@ function App() {
-
-
- }
+ element={ }
>
} />
} />
@@ -66,7 +74,7 @@ function App() {
POS Integration
- POS system integration module is active and ready to use.
+ POS system integration module is currently under development. This feature will allow you to connect ZatoBox with your existing POS system for seamless inventory management and sales tracking.
}
diff --git a/frontend/src/components/EditProductPage.tsx b/frontend/src/components/EditProductPage.tsx
index c980e0dc..a6b48df6 100644
--- a/frontend/src/components/EditProductPage.tsx
+++ b/frontend/src/components/EditProductPage.tsx
@@ -1,36 +1,20 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
-import { ArrowLeft, Upload, Plus, X } from 'lucide-react';
+import { ArrowLeft, Upload, Plus, Check } from 'lucide-react';
import { productsAPI, Product } from '../services/api';
import { useAuth } from '../contexts/AuthContext';
-interface VariantValue {
- id: string;
- value: string;
- selected: boolean;
-}
-
-interface VariantData {
- [key: string]: {
- isActive: boolean;
- values: VariantValue[];
- newValue: string;
- showPanel: boolean;
- };
-}
-
const EditProductPage: React.FC = () => {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const { isAuthenticated } = useAuth();
const [formData, setFormData] = useState({
- productType: 'Physical Product',
+ productType: '',
name: '',
description: '',
- location: 'main-warehouse',
- createCategory: false,
- unit: 'Per item',
+ location: '',
+ unit: '',
weight: '',
price: '',
inventoryQuantity: '',
@@ -38,11 +22,12 @@ const EditProductPage: React.FC = () => {
sku: '',
});
const [selectedCategories, setSelectedCategories] = useState([]);
- const [showMoreVariantsPanel, setShowMoreVariantsPanel] = useState(false);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState(null);
const [originalProduct, setOriginalProduct] = useState(null);
+ const [status, setStatus] = useState<'active' | 'inactive' | ''>('');
+ const [togglingStatus, setTogglingStatus] = useState(false);
const existingCategories = [
'Furniture',
@@ -51,113 +36,64 @@ const EditProductPage: React.FC = () => {
'Electronics',
'Decoration',
'Office',
+ 'Gaming',
];
- const variantTypes = ['Color', 'Size', 'Material', 'Style', 'Finish'];
+ const unitOptions = [
+ { label: 'Per item', value: 'Per item' },
+ { label: 'Per kilogram', value: 'Per kilogram' },
+ { label: 'Per liter', value: 'Per liter' },
+ { label: 'Per metro', value: 'Per metro' },
+ ];
- const [variants, setVariants] = useState({
- Color: {
- isActive: true,
- values: [
- { id: '1', value: 'Red', selected: false },
- { id: '2', value: 'Blue', selected: true },
- { id: '3', value: 'Green', selected: false },
- { id: '4', value: 'Black', selected: true },
- ],
- newValue: '',
- showPanel: false,
- },
- Size: {
- isActive: true,
- values: [
- { id: '1', value: 'Small', selected: false },
- { id: '2', value: 'Medium', selected: true },
- { id: '3', value: 'Large', selected: false },
- { id: '4', value: 'Extra Large', selected: false },
- ],
- newValue: '',
- showPanel: false,
- },
- Material: {
- isActive: false,
- values: [
- { id: '1', value: 'Wood', selected: false },
- { id: '2', value: 'Metal', selected: false },
- { id: '3', value: 'Plastic', selected: false },
- { id: '4', value: 'Glass', selected: false },
- ],
- newValue: '',
- showPanel: false,
- },
- Style: {
- isActive: false,
- values: [
- { id: '1', value: 'Modern', selected: false },
- { id: '2', value: 'Classic', selected: false },
- { id: '3', value: 'Minimalist', selected: false },
- { id: '4', value: 'Industrial', selected: false },
- ],
- newValue: '',
- showPanel: false,
- },
- Finish: {
- isActive: false,
- values: [
- { id: '1', value: 'Matte', selected: false },
- { id: '2', value: 'Glossy', selected: false },
- { id: '3', value: 'Satin', selected: false },
- { id: '4', value: 'Textured', selected: false },
- ],
- newValue: '',
- showPanel: false,
- },
- });
+ const productTypeOptions = [
+ { label: 'Physical Product', value: 'Physical Product' },
+ { label: 'Service', value: 'Service' },
+ { label: 'Digital', value: 'Digital' },
+ ];
- // Load product data when component mounts
useEffect(() => {
const fetchProduct = async () => {
- if (!id || !isAuthenticated) {
- setError('ID de producto inválido o no autenticado');
+ if (!id) {
+ setError('ID de producto inválido');
setLoading(false);
return;
}
-
try {
setLoading(true);
setError(null);
-
- const response = await productsAPI.getById(parseInt(id));
-
+ const response = await productsAPI.getById(parseInt(id, 10));
if (response.success) {
- const product = response.product;
- setFormData({
- productType: 'Physical Product',
- name: product.name,
+ const product = response.product as Product;
+ setOriginalProduct(product);
+ setStatus(product.status ?? 'inactive');
+ setFormData((prev) => ({
+ ...prev,
+ productType: '',
+ name: product.name || '',
description: product.description || '',
- location: 'main-warehouse',
- createCategory: false,
- unit: 'Per item',
- weight: '',
- price: product.price.toString(),
- inventoryQuantity: product.stock.toString(),
- lowStockAlert: '',
+ location: product.localization || '',
+ unit: '',
+ weight: product.weight != null ? String(product.weight) : '',
+ price: product.price != null ? String(product.price) : '',
+ inventoryQuantity:
+ product.stock != null ? String(product.stock) : '',
+ lowStockAlert:
+ product.min_stock != null ? String(product.min_stock) : '',
sku: product.sku || '',
- });
- setSelectedCategories([product.category]);
- setOriginalProduct(product);
+ }));
+ setSelectedCategories(product.category ? [product.category] : []);
} else {
setError('Error al cargar el producto');
}
} catch (err) {
- console.error('Error fetching product:', err);
setError('Error al cargar el producto');
} finally {
setLoading(false);
}
};
-
fetchProduct();
- }, [id, isAuthenticated]);
+ }, [id]);
const handleInputChange = (field: string, value: string | boolean) => {
setFormData((prev) => ({ ...prev, [field]: value }));
@@ -171,153 +107,98 @@ const EditProductPage: React.FC = () => {
);
};
- const handleVariantToggle = (variantType: string) => {
- setVariants((prev) => ({
- ...prev,
- [variantType]: {
- ...prev[variantType],
- isActive: !prev[variantType].isActive,
- showPanel: !prev[variantType].isActive ? true : false,
- },
- }));
- };
-
- const handleVariantValueToggle = (variantType: string, valueId: string) => {
- setVariants((prev) => ({
- ...prev,
- [variantType]: {
- ...prev[variantType],
- values: prev[variantType].values.map((value) =>
- value.id === valueId ? { ...value, selected: !value.selected } : value
- ),
- },
- }));
- };
-
- const handleAddVariantValue = (variantType: string) => {
- const newValue = variants[variantType].newValue.trim();
- if (newValue) {
- const newId = Date.now().toString();
- setVariants((prev) => ({
- ...prev,
- [variantType]: {
- ...prev[variantType],
- values: [
- ...prev[variantType].values,
- { id: newId, value: newValue, selected: true },
- ],
- newValue: '',
- },
- }));
- }
- };
-
- const handleNewValueChange = (variantType: string, value: string) => {
- setVariants((prev) => ({
- ...prev,
- [variantType]: {
- ...prev[variantType],
- newValue: value,
- },
- }));
- };
-
- const handleSaveVariant = (variantType: string) => {
- setVariants((prev) => ({
- ...prev,
- [variantType]: {
- ...prev[variantType],
- showPanel: false,
- },
- }));
- };
-
- const handleCancelVariant = (variantType: string) => {
- setVariants((prev) => ({
- ...prev,
- [variantType]: {
- ...prev[variantType],
- showPanel: false,
- isActive: false,
- values: prev[variantType].values.map((value) => ({
- ...value,
- selected: false,
- })),
- newValue: '',
- },
- }));
- };
-
- const getSelectedValues = (variantType: string) => {
- return variants[variantType].values
- .filter((value) => value.selected)
- .map((value) => value.value)
- .join(', ');
- };
-
- const handleMoreVariantsToggle = (variantType: string) => {
- setVariants((prev) => ({
- ...prev,
- [variantType]: {
- ...prev[variantType],
- isActive: !prev[variantType].isActive,
- },
- }));
- };
-
- const handleSaveMoreVariants = () => {
- setShowMoreVariantsPanel(false);
- };
-
const handleSave = async () => {
if (!isAuthenticated) {
- setError('You must log in to edit products');
+ setError('Debes iniciar sesión para editar productos');
return;
}
-
if (!id) {
- setError('Invalid product ID or not authenticated');
+ setError('ID de producto inválido');
return;
}
-
- if (!formData.name || !formData.price) {
- setError('Name and price are required');
+ if (!formData.name || formData.name.trim() === '') {
+ setError('El nombre es requerido');
+ return;
+ }
+ const priceNum = Number(formData.price);
+ if (
+ formData.price !== '' &&
+ (!Number.isFinite(priceNum) || priceNum <= 0)
+ ) {
+ setError('Precio inválido');
+ return;
+ }
+ const stockNum =
+ formData.inventoryQuantity === ''
+ ? null
+ : parseInt(formData.inventoryQuantity, 10);
+ if (
+ stockNum !== null &&
+ (!Number.isFinite(stockNum) || Number.isNaN(stockNum) || stockNum < 0)
+ ) {
+ setError('Cantidad de inventario inválida');
return;
}
-
try {
setSaving(true);
setError(null);
-
- const priceNum = parseFloat(formData.price);
- const stockNum = parseInt(formData.inventoryQuantity) || 0;
- const category = selectedCategories[0] || 'General';
-
- const payload: Partial = {};
+ const payload: Record = {};
+ const category = selectedCategories[0];
if (!originalProduct) {
payload.name = formData.name;
- payload.description = formData.description || undefined;
- payload.price = priceNum;
- payload.stock = stockNum;
- payload.category = category;
- if (formData.sku !== undefined)
- payload.sku = formData.sku === '' ? undefined : formData.sku;
+ payload.description = formData.description || null;
+ if (formData.price !== '') payload.price = priceNum;
+ if (stockNum !== null) payload.stock = stockNum;
+ if (category) payload.category = category;
+ if (formData.unit !== '') payload.unit = formData.unit;
+ if (formData.productType !== '')
+ payload.product_type = formData.productType;
+ if (formData.location !== '') payload.localization = formData.location;
+ if (formData.weight !== '') payload.weight = Number(formData.weight);
+ if (formData.sku !== '') payload.sku = formData.sku;
+ if (formData.lowStockAlert !== '')
+ payload.min_stock = Number(formData.lowStockAlert);
} else {
if (formData.name !== originalProduct.name)
payload.name = formData.name;
if (
(formData.description || '') !== (originalProduct.description || '')
)
- payload.description = formData.description || undefined;
- if (!Number.isNaN(priceNum) && priceNum !== originalProduct.price)
+ payload.description = formData.description || null;
+ if (formData.price !== '' && priceNum !== originalProduct.price)
payload.price = priceNum;
- if (stockNum !== originalProduct.stock) payload.stock = stockNum;
- if (category !== originalProduct.category) payload.category = category;
- if ((formData.sku || '') !== (originalProduct.sku || '')) {
- if (formData.sku) payload.sku = formData.sku;
- else payload.sku = undefined;
- }
+ if (stockNum !== null && stockNum !== originalProduct.stock)
+ payload.stock = stockNum;
+ if (category && category !== originalProduct.category)
+ payload.category = category;
+ if (
+ formData.unit !== '' &&
+ formData.unit !== ((originalProduct as any).unit || '')
+ )
+ payload.unit = formData.unit;
+ if (
+ formData.productType !== '' &&
+ formData.productType !== (originalProduct.product_type || '')
+ )
+ payload.product_type = formData.productType;
+ if (
+ formData.location !== '' &&
+ formData.location !== (originalProduct.localization || '')
+ )
+ payload.localization = formData.location;
+ if (
+ formData.weight !== '' &&
+ Number(formData.weight) !== (originalProduct.weight || 0)
+ )
+ payload.weight = Number(formData.weight);
+ if (formData.sku !== '' && formData.sku !== (originalProduct.sku || ''))
+ payload.sku = formData.sku;
+ if (
+ formData.lowStockAlert !== '' &&
+ Number(formData.lowStockAlert) !== (originalProduct.min_stock || 0)
+ )
+ payload.min_stock = Number(formData.lowStockAlert);
}
if (Object.keys(payload).length === 0) {
@@ -326,16 +207,16 @@ const EditProductPage: React.FC = () => {
return;
}
- const response = await productsAPI.update(parseInt(id), payload);
+ const response = await productsAPI.update(parseInt(id, 10), payload);
if (response.success) {
setOriginalProduct(response.product);
+ setStatus(response.product.status ?? 'inactive');
navigate('/inventory');
} else {
setError('Error updating product');
}
} catch (err) {
- console.error('Error updating product:', err);
setError('Error updating product');
} finally {
setSaving(false);
@@ -344,44 +225,56 @@ const EditProductPage: React.FC = () => {
const handleDelete = async () => {
if (!isAuthenticated) {
- setError('You must log in to delete products');
+ setError('Debes iniciar sesión para eliminar productos');
return;
}
-
if (!id) {
- setError('Invalid product ID or not authenticated');
+ setError('ID de producto inválido');
return;
}
+ if (!window.confirm('¿Seguro que querés eliminar este producto?')) return;
+ try {
+ setSaving(true);
+ setError(null);
+ const response = await productsAPI.delete(parseInt(id, 10));
+ if (response.success) navigate('/inventory');
+ else setError('Error deleting product');
+ } catch (err) {
+ setError('Error deleting product');
+ } finally {
+ setSaving(false);
+ }
+ };
- if (
- !window.confirm(
- 'Are you sure you want to delete this product? This action cannot be undone.'
- )
- ) {
+ const handleToggleStatus = async () => {
+ if (!isAuthenticated) {
+ setError('Debes iniciar sesión para cambiar el estado');
return;
}
-
+ if (!id) {
+ setError('ID de producto inválido');
+ return;
+ }
+ const newStatus = status === 'active' ? 'inactive' : 'active';
try {
- setSaving(true);
+ setTogglingStatus(true);
setError(null);
-
- const response = await productsAPI.delete(parseInt(id));
-
+ const response = await productsAPI.update(parseInt(id, 10), {
+ status: newStatus,
+ });
if (response.success) {
- console.log('Product deleted successfully');
- navigate('/inventory');
+ setStatus(newStatus);
+ setOriginalProduct(response.product);
} else {
- setError('Error deleting product');
+ setError('Error al cambiar estado');
}
} catch (err) {
- console.error('Error deleting product:', err);
- setError('Error deleting product');
+ setError('Error al cambiar estado');
} finally {
- setSaving(false);
+ setTogglingStatus(false);
}
};
- // Show loading state
if (loading) {
return (
@@ -393,7 +286,6 @@ const EditProductPage: React.FC = () => {
);
}
- // Show error state
if (error) {
return (
@@ -427,532 +319,299 @@ const EditProductPage: React.FC = () => {
return (
- {loading ? (
-
-
-
Loading product...
+
+
+
+
+
navigate('/inventory')}
+ className='p-2 transition-colors rounded-full hover:bg-gray-50 md:hidden'
+ >
+
+
+
+ Edit Product
+
+
+
+ {error &&
{error}
}
+
navigate('/inventory')}
+ className='px-4 py-2 font-medium text-black transition-colors bg-gray-100 rounded-lg hover:bg-gray-200'
+ >
+ Go back
+
+
+ Delete
+
+
+
+ {status === 'active' ? 'Active' : 'Inactive'}
+
+
+ {saving ? (
+ <>
+
+ Saving...
+ >
+ ) : (
+ Save
+ )}
+
+
+
- ) : (
- <>
- {/* Sub-header */}
-
-
-
-
-
navigate('/inventory')}
- className='p-2 transition-colors rounded-full hover:bg-gray-50 md:hidden'
- >
-
-
-
- Edit Product
-
-
+
-
- {error &&
{error}
}
-
- Delete
-
-
- {saving ? (
- <>
-
- Saving...
- >
- ) : (
- Save
- )}
-
-
+
+
+
+
+
+
+ Product Name *
+
+ handleInputChange('name', e.target.value)}
+ placeholder='Product name'
+ className='w-full p-3 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary'
+ required
+ />
+
+
+
+ Description
+
+
-
- {/* Content */}
-
- {/* Desktop Two Column Layout */}
-
- {/* Left Column */}
-
- {/* Product Type */}
-
-
-
-
- Tipo de artículo
-
-
- handleInputChange('productType', e.target.value)
- }
- className='w-full p-3 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary'
- >
-
- Physical Product
-
- Service
- Digital
-
-
-
- Cambiar
-
-
-
+
+
+ Product Images
+
+
+
+
+ Drag and drop images here
+
+
+ or click to select files
+
+
+
- {/* Basic Information */}
-
-
-
- Product Name *
-
-
- handleInputChange('name', e.target.value)
- }
- placeholder='Product name'
- className='w-full p-3 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary'
- required
- />
-
+
+
+ Locations
+
+ handleInputChange('location', e.target.value)}
+ className='w-full p-3 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary'
+ placeholder='Optional physical location'
+ />
+
-
- {/* Image Upload */}
-
-
- Product Images
+
+
+
+ Units
+
+
+
+
+ Unit
-
-
-
- Drag and drop images here
-
-
- or click to select files
-
-
+
handleInputChange('unit', e.target.value)}
+ className='w-full p-3 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary'
+ >
+ -- Leave blank to keep current --
+ {unitOptions.map((opt) => (
+
+ {opt.label}
+
+ ))}
+
- {/* Location */}
-
+
- Locations
+ Product Type
- handleInputChange('location', e.target.value)
+ handleInputChange('productType', e.target.value)
}
className='w-full p-3 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary'
>
- Select warehouse
- Main Warehouse
-
- Secondary Warehouse
-
- Physical Store
+ -- Leave blank to keep current --
+ {productTypeOptions.map((opt) => (
+
+ {opt.label}
+
+ ))}
- {/* Categorization */}
-
-
- Categorization
-
-
-
-
-
- handleInputChange('createCategory', e.target.checked)
- }
- className='w-4 h-4 border-gray-300 rounded text-complement focus:ring-complement'
- />
-
- Create category
-
-
-
-
-
- Existing categories
-
-
- {existingCategories.map((category) => (
-
- handleCategoryToggle(category)}
- className='w-4 h-4 border-gray-300 rounded text-complement focus:ring-complement'
- />
-
- {category}
-
-
- ))}
-
-
-
-
-
-
- {/* Right Column */}
-
- {/* Units Section */}
-
-
- Units
-
-
-
-
-
- Unit
-
-
- handleInputChange('unit', e.target.value)
- }
- className='w-full p-3 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary'
- >
- Per item
- Per kilogram
- Per meter
- Per liter
-
-
-
-
-
- Weight (kg)
-
-
- handleInputChange('weight', e.target.value)
- }
- className='w-full p-3 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary'
- placeholder='0.00'
- step='0.01'
- />
-
-
-
-
- Price (required)
-
-
-
- $
-
-
- handleInputChange('price', e.target.value)
- }
- className='w-full py-3 pl-8 pr-3 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary'
- placeholder='0.00'
- step='0.01'
- />
-
-
-
-
-
- Add additional unit
-
-
+
+
+ Weight (kg)
+
+
+ handleInputChange('weight', e.target.value)
+ }
+ className='w-full p-3 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary'
+ placeholder='0.00'
+ step='0.01'
+ />
- {/* Inventory Section */}
-
-
- Inventory
-
-
-
-
-
- Inventory quantity
-
-
- handleInputChange('inventoryQuantity', e.target.value)
- }
- className='w-full p-3 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary'
- placeholder='0'
- />
-
-
-
-
- Low stock alert
-
-
- handleInputChange('lowStockAlert', e.target.value)
- }
- className='w-full p-3 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary'
- placeholder='5'
- />
-
-
-
-
- SKU
-
-
- handleInputChange('sku', e.target.value)
- }
- className='w-full p-3 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary'
- placeholder='SKU-001'
- />
-
+
+
+ Price (required)
+
+
+
+ $
+
+
+ handleInputChange('price', e.target.value)
+ }
+ className='w-full py-3 pl-8 pr-3 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary'
+ placeholder='0.00'
+ step='0.01'
+ />
- {/* Variants Section */}
-
-
- Variants
-
-
-
- {variantTypes.map((variant) => (
-
-
- handleVariantToggle(variant)}
- className='w-4 h-4 border-gray-300 rounded text-complement focus:ring-complement'
- />
-
- {variant}
-
-
-
- {/* Show selected values summary */}
- {variants[variant].isActive &&
- !variants[variant].showPanel &&
- getSelectedValues(variant) && (
-
- {variant}: {getSelectedValues(variant)}
-
- )}
-
- {/* Inline Panel */}
- {variants[variant].showPanel && (
-
-
-
- {variant}
-
- handleCancelVariant(variant)}
- className='p-1 transition-colors rounded hover:bg-gray-200'
- >
-
-
-
-
- {/* Add new value */}
-
-
- handleNewValueChange(variant, e.target.value)
- }
- placeholder='Add value'
- className='flex-1 p-2 text-sm border rounded border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary'
- />
-
handleAddVariantValue(variant)}
- className='p-2 text-white transition-colors rounded bg-complement hover:bg-complement-600'
- >
-
-
-
-
- {/* Existing values */}
-
- {variants[variant].values.map((value) => (
-
-
- handleVariantValueToggle(
- variant,
- value.id
- )
- }
- className='w-4 h-4 border-gray-300 rounded text-complement focus:ring-complement'
- />
-
- {value.value}
-
-
- ))}
-
-
- {/* Panel actions */}
-
- handleSaveVariant(variant)}
- className='px-3 py-1 text-sm text-white transition-colors rounded bg-success hover:bg-success-600'
- >
- Save
-
- handleCancelVariant(variant)}
- className='px-3 py-1 text-sm transition-colors bg-gray-300 rounded hover:bg-gray-400 text-text-primary'
- >
- Cancel
-
-
-
- )}
-
- ))}
-
-
setShowMoreVariantsPanel(true)}
- className='flex items-center justify-center w-full p-3 mt-4 space-x-2 transition-colors border border-dashed rounded-lg border-divider text-text-secondary hover:bg-gray-50'
- >
-
- Add more variants
-
-
-
+
+
+ Add additional unit
+
-
- >
- )}
- {/* More Variants Floating Panel */}
- {showMoreVariantsPanel && (
-
-
-
-
- Add Variants
+
+
+ Inventory
- setShowMoreVariantsPanel(false)}
- className='p-1 transition-colors rounded hover:bg-gray-50'
- >
-
-
-
-
-
- {variantTypes.map((variant) => (
-
+
+
+
+ Inventory quantity
+
handleMoreVariantsToggle(variant)}
- className='w-4 h-4 border-gray-300 rounded text-complement focus:ring-complement'
+ type='number'
+ value={formData.inventoryQuantity}
+ onChange={(e) =>
+ handleInputChange('inventoryQuantity', e.target.value)
+ }
+ className='w-full p-3 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary'
+ placeholder='0'
/>
-
- {variant}
-
- ))}
-
-
-
- Save
-
-
setShowMoreVariantsPanel(false)}
- className='flex-1 py-2 font-medium transition-colors bg-gray-300 rounded-lg hover:bg-gray-400 text-text-primary'
- >
- Cancel
-
+
+
+ Low stock alert
+
+
+ handleInputChange('lowStockAlert', e.target.value)
+ }
+ className='w-full p-3 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary'
+ placeholder='5'
+ />
+
+
- )}
+
);
};
diff --git a/frontend/src/components/HomePage.tsx b/frontend/src/components/HomePage.tsx
index 5a3c33c8..2c2214fd 100644
--- a/frontend/src/components/HomePage.tsx
+++ b/frontend/src/components/HomePage.tsx
@@ -4,27 +4,18 @@ import ProductCard from './ProductCard';
import SalesDrawer from './SalesDrawer';
import PaymentScreen from './PaymentScreen';
import PaymentSuccessScreen from './PaymentSuccessScreen';
-import { productsAPI, salesAPI } from '../services/api';
+import { inventoryAPI, salesAPI } from '../services/api';
+import type { Product } from '../services/api';
import { useAuth } from '../contexts/AuthContext';
interface HomePageProps {
+ tab?: string;
searchTerm?: string;
}
-interface Product {
- id: number;
- name: string;
- description?: string;
- sku?: string;
- category: string;
- stock: number;
- price: number;
- status: 'active' | 'inactive';
- image?: string;
- images?: string[];
-}
-
-const HomePage: React.FC
= ({ searchTerm: externalSearchTerm = '' }) => {
+const HomePage: React.FC = ({
+ searchTerm: externalSearchTerm = '',
+}) => {
const { isAuthenticated } = useAuth();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [isPaymentOpen, setIsPaymentOpen] = useState(false);
@@ -53,7 +44,7 @@ const HomePage: React.FC = ({ searchTerm: externalSearchTerm = ''
// Fetch products from backend
useEffect(() => {
- const fetchProducts = async() => {
+ const fetchProducts = async () => {
if (!isAuthenticated) {
setLoading(false);
return;
@@ -61,21 +52,25 @@ const HomePage: React.FC = ({ searchTerm: externalSearchTerm = ''
try {
setLoading(true);
- const response = await productsAPI.getAll();
- if (response.success) {
- // Show active products (allow stock 0 to see all products)
- const availableProducts = response.products.filter(
- (product: Product) => product.status === 'active',
- );
- console.log('Products loaded:', response.products);
- console.log('Available products:', availableProducts);
+ const response = await inventoryAPI.getActive();
+ if (response && response.success && Array.isArray(response.inventory)) {
+ const availableProducts = response.inventory
+ .filter((item: any) => item.product)
+ .map((item: any) => {
+ const product = item.product as Product;
+ return {
+ ...product,
+ id: product.id ?? item.product_id ?? 0,
+ stock: Number(item.quantity ?? product.stock ?? 0),
+ } as Product;
+ });
setProducts(availableProducts);
} else {
- setError('Error loading products');
+ setProducts([]);
}
} catch (err) {
console.error('Error fetching products:', err);
- setError('Error loading products');
+ setProducts([]);
} finally {
setLoading(false);
}
@@ -93,7 +88,8 @@ const HomePage: React.FC = ({ searchTerm: externalSearchTerm = ''
};
document.addEventListener('visibilitychange', handleVisibilityChange);
- return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
+ return () =>
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
}, []);
// Filter products based on search term
@@ -102,8 +98,8 @@ const HomePage: React.FC = ({ searchTerm: externalSearchTerm = ''
return products;
}
- return products.filter(product =>
- product.name.toLowerCase().includes(activeSearchTerm.toLowerCase()),
+ return products.filter((product) =>
+ product.name.toLowerCase().includes(activeSearchTerm.toLowerCase())
);
}, [activeSearchTerm, products]);
@@ -111,14 +107,14 @@ const HomePage: React.FC = ({ searchTerm: externalSearchTerm = ''
const handleProductClick = (product: Product) => {
setSelectedProduct(product);
setIsDrawerOpen(true);
- setCartItems(prevCart => {
- const existing = prevCart.find(item => item.id === product.id);
+ setCartItems((prevCart) => {
+ const existing = prevCart.find((item) => item.id === product.id);
if (existing) {
// Add quantity, respecting stock
- return prevCart.map(item =>
+ return prevCart.map((item) =>
item.id === product.id
? { ...item, quantity: Math.min(item.quantity + 1, product.stock) }
- : item,
+ : item
);
} else {
return [
@@ -148,11 +144,11 @@ const HomePage: React.FC = ({ searchTerm: externalSearchTerm = ''
setIsPaymentOpen(false);
};
- const handlePaymentSuccess = async(method: string) => {
+ const handlePaymentSuccess = async (method: string) => {
try {
// Prepare sale data
const saleData = {
- items: cartItems.map(item => ({
+ items: cartItems.map((item) => ({
product_id: item.id,
quantity: item.quantity,
price: item.price,
@@ -168,9 +164,9 @@ const HomePage: React.FC = ({ searchTerm: externalSearchTerm = ''
console.log('Sale created successfully:', saleResponse);
// Update local inventory immediately
- setProducts(prevProducts =>
- prevProducts.map(product => {
- const cartItem = cartItems.find(item => item.id === product.id);
+ setProducts((prevProducts) =>
+ prevProducts.map((product) => {
+ const cartItem = cartItems.find((item) => item.id === product.id);
if (cartItem) {
return {
...product,
@@ -178,7 +174,7 @@ const HomePage: React.FC = ({ searchTerm: externalSearchTerm = ''
};
}
return product;
- }),
+ })
);
// Show success message
@@ -213,19 +209,19 @@ const HomePage: React.FC = ({ searchTerm: externalSearchTerm = ''
// Modify quantity of a product in cart
const updateCartItemQuantity = (productId: number, change: number) => {
- setCartItems(prev => {
- const updatedItems = prev.map(item =>
+ setCartItems((prev) => {
+ const updatedItems = prev.map((item) =>
item.id === productId
? { ...item, quantity: Math.max(0, item.quantity + change) }
- : item,
+ : item
);
- return updatedItems.filter(item => item.quantity > 0);
+ return updatedItems.filter((item) => item.quantity > 0);
});
};
// Remove product from cart
const removeFromCart = (productId: number) => {
- setCartItems(prev => prev.filter(item => item.id !== productId));
+ setCartItems((prev) => prev.filter((item) => item.id !== productId));
};
// Clear cart
@@ -234,23 +230,31 @@ const HomePage: React.FC = ({ searchTerm: externalSearchTerm = ''
};
// Function to reload products
- const reloadProducts = async() => {
+ const reloadProducts = async () => {
try {
setLoading(true);
setError(null);
- const response = await productsAPI.getAll();
-
- if (response.success) {
- // Show active products (allow stock 0 to see all products)
- const availableProducts = response.products.filter(product => product.status === 'active');
+ const response = await inventoryAPI.getActive();
+
+ if (response && response.success && Array.isArray(response.inventory)) {
+ const availableProducts = response.inventory
+ .filter((item: any) => item.product)
+ .map((item: any) => {
+ const product = item.product as Product;
+ return {
+ ...product,
+ id: product.id ?? item.product_id ?? 0,
+ stock: Number(item.quantity ?? product.stock ?? 0),
+ } as Product;
+ });
setProducts(availableProducts);
- console.log('Products reloaded:', response.products);
- console.log('Available products:', availableProducts);
} else {
+ setProducts([]);
setError('Error reloading products');
}
} catch (err) {
console.error('Error reloading products:', err);
+ setProducts([]);
setError('Error reloading products');
} finally {
setLoading(false);
@@ -260,10 +264,12 @@ const HomePage: React.FC = ({ searchTerm: externalSearchTerm = ''
// Show loading state
if (loading) {
return (
-
-
-
-
Loading products...
+
+
+
+
+ Loading products...
+
);
@@ -272,17 +278,29 @@ const HomePage: React.FC
= ({ searchTerm: externalSearchTerm = ''
// Show error state
if (error) {
return (
-
-
-
-
-
+
+
+
-
{error}
+
+ {error}
+
window.location.reload()}
- className="px-4 py-2 font-medium text-black transition-all duration-300 rounded-lg bg-primary hover:bg-primary-600 hover:scale-105 hover:shadow-lg btn-animate"
+ className='px-4 py-2 font-medium text-black transition-all duration-300 rounded-lg bg-primary hover:bg-primary-600 hover:scale-105 hover:shadow-lg btn-animate'
>
Reintentar
@@ -293,28 +311,35 @@ const HomePage: React.FC
= ({ searchTerm: externalSearchTerm = ''
return (
<>
-
-
-
+
+
+
{/* Title and Search Row */}
-
-
+
+
Sales Dashboard
{/* Search and Refresh Row */}
-
+
{/* Search Bar */}
-
-
+
+
handleLocalSearchChange(e.target.value)}
- placeholder="Search products..."
- className="w-full py-2 pl-10 pr-4 text-sm transition-all duration-300 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary placeholder-text-secondary hover:border-complement/50"
+ placeholder='Search products...'
+ className='w-full py-2 pl-10 pr-4 text-sm transition-all duration-300 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary placeholder-text-secondary hover:border-complement/50'
/>
@@ -322,19 +347,24 @@ const HomePage: React.FC
= ({ searchTerm: externalSearchTerm = ''
-
+
{/* Description */}
-
+
{activeSearchTerm ? (
<>
- Showing {filteredProducts.length} result{filteredProducts.length !== 1 ? 's' : ''} for "{activeSearchTerm}"
+ Showing {filteredProducts.length} result
+ {filteredProducts.length !== 1 ? 's' : ''} for "
+ {activeSearchTerm}"
>
) : (
'Select products to create sales orders quickly'
@@ -343,9 +373,9 @@ const HomePage: React.FC = ({ searchTerm: externalSearchTerm = ''
{/* Responsive Grid with white background */}
-
+
{filteredProducts.length > 0 ? (
-
+
{filteredProducts.map((product) => (
= ({ searchTerm: externalSearchTerm = ''
))}
) : (
-
-
-
-
+
+
-
+
No products found
-
+
{activeSearchTerm
? `No products match "${activeSearchTerm}". Try different search terms.`
- : 'No products available for sale.'
- }
+ : 'No products available for sale.'}
)}
diff --git a/frontend/src/components/InventoryPage.tsx b/frontend/src/components/InventoryPage.tsx
index c0f7eb50..6855700d 100644
--- a/frontend/src/components/InventoryPage.tsx
+++ b/frontend/src/components/InventoryPage.tsx
@@ -1,22 +1,17 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
-import { ArrowLeft, Search, Printer, Plus, Package, ChevronDown } from 'lucide-react';
+import {
+ ArrowLeft,
+ Search,
+ Printer,
+ Plus,
+ Package,
+ ChevronDown,
+} from 'lucide-react';
import { productsAPI } from '../services/api';
+import type { Product } from '../services/api';
import { useAuth } from '../contexts/AuthContext';
-interface InventoryItem {
- id: number;
- name: string;
- description?: string;
- sku?: string;
- category: string;
- stock: number;
- price: number;
- status: 'active' | 'inactive';
- image?: string;
- images?: string[];
-}
-
const InventoryPage: React.FC = () => {
const navigate = useNavigate();
const { isAuthenticated } = useAuth();
@@ -24,7 +19,7 @@ const InventoryPage: React.FC = () => {
const [categoryFilter, setCategoryFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
- const [inventoryItems, setInventoryItems] = useState([]);
+ const [inventoryItems, setInventoryItems] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [deleteConfirmId, setDeleteConfirmId] = useState(null);
@@ -32,7 +27,7 @@ const InventoryPage: React.FC = () => {
// Fetch products from backend
useEffect(() => {
- const fetchProducts = async() => {
+ const fetchInventory = async () => {
if (!isAuthenticated) {
setError('You must log in to view inventory');
setLoading(false);
@@ -42,27 +37,35 @@ const InventoryPage: React.FC = () => {
try {
setLoading(true);
const response = await productsAPI.getAll();
- if (response.success) {
- setInventoryItems(response.products);
- } else {
- setError('Error loading products');
+ if (!response || !response.products) {
+ setError('Error loading inventory');
+ return;
}
+
+ setInventoryItems(response.products);
+ setError(null);
} catch (err) {
- console.error('Error fetching products:', err);
- setError('Error loading products');
+ console.error('Error fetching inventory:', err);
+ setError('Error loading inventory');
} finally {
setLoading(false);
}
};
- fetchProducts();
+ fetchInventory();
}, [isAuthenticated]);
- const categories = ['all', 'Furniture', 'Textiles', 'Lighting', 'Electronics'];
+ const categories = [
+ 'all',
+ 'Furniture',
+ 'Textiles',
+ 'Lighting',
+ 'Electronics',
+ ];
const handleSelectAll = (checked: boolean) => {
if (checked) {
- setSelectedItems(inventoryItems.map(item => item.id));
+ setSelectedItems(inventoryItems.map((item) => item.id));
} else {
setSelectedItems([]);
}
@@ -72,7 +75,7 @@ const InventoryPage: React.FC = () => {
if (checked) {
setSelectedItems([...selectedItems, id]);
} else {
- setSelectedItems(selectedItems.filter(itemId => itemId !== id));
+ setSelectedItems(selectedItems.filter((itemId) => itemId !== id));
}
};
@@ -95,8 +98,10 @@ const InventoryPage: React.FC = () => {
setDeleteConfirmId(id);
};
- const handleDeleteConfirm = async() => {
- if (!deleteConfirmId) {return;}
+ const handleDeleteConfirm = async () => {
+ if (!deleteConfirmId) {
+ return;
+ }
setIsDeleting(true);
setError(null);
@@ -109,9 +114,13 @@ const InventoryPage: React.FC = () => {
if (response.success) {
console.log('Product deleted successfully, updating UI');
// Remove the product from the local state
- setInventoryItems(prevItems => prevItems.filter(item => item.id !== deleteConfirmId));
+ setInventoryItems((prevItems) =>
+ prevItems.filter((item) => item.id !== deleteConfirmId)
+ );
// Remove from selected items if it was selected
- setSelectedItems(prevSelected => prevSelected.filter(itemId => itemId !== deleteConfirmId));
+ setSelectedItems((prevSelected) =>
+ prevSelected.filter((itemId) => itemId !== deleteConfirmId)
+ );
setError(null); // Clear any previous errors
} else {
console.error('Delete failed:', response.message);
@@ -119,7 +128,10 @@ const InventoryPage: React.FC = () => {
}
} catch (err) {
console.error('Error deleting product:', err);
- setError('Error deleting product: ' + (err instanceof Error ? err.message : 'Unknown error'));
+ setError(
+ 'Error deleting product: ' +
+ (err instanceof Error ? err.message : 'Unknown error')
+ );
} finally {
setIsDeleting(false);
setDeleteConfirmId(null);
@@ -131,20 +143,24 @@ const InventoryPage: React.FC = () => {
setIsDeleting(false);
};
- const filteredItems = inventoryItems.filter(item => {
- const matchesCategory = categoryFilter === 'all' || item.category === categoryFilter;
- const matchesStatus = statusFilter === 'all' || item.status === statusFilter;
- const matchesSearch = item.name.toLowerCase().includes(searchTerm.toLowerCase());
+ const filteredItems = inventoryItems.filter((item) => {
+ const matchesCategory =
+ categoryFilter === 'all' || item.category === categoryFilter;
+ const matchesStatus =
+ statusFilter === 'all' || item.status === statusFilter;
+ const matchesSearch = item.name
+ .toLowerCase()
+ .includes(searchTerm.toLowerCase());
return matchesCategory && matchesStatus && matchesSearch;
});
// Show loading state
if (loading) {
return (
-
-
-
-
Loading products...
+
);
@@ -153,17 +169,27 @@ const InventoryPage: React.FC = () => {
// Show error state
if (error) {
return (
-
-
-
-
-
+
+
+
-
{error}
+
{error}
window.location.reload()}
- className="bg-primary hover:bg-primary-600 text-black font-medium px-4 py-2 rounded-lg transition-colors"
+ className='px-4 py-2 font-medium text-black transition-colors rounded-lg bg-primary hover:bg-primary-600'
>
Retry
@@ -173,51 +199,58 @@ const InventoryPage: React.FC = () => {
}
return (
-
+
{/* Sub-header with filters */}
-
-
+
+
{/* Top Row */}
-
-
+
+
navigate('/')}
- className="p-2 hover:bg-gray-50 rounded-full transition-colors md:hidden"
+ className='p-2 transition-colors rounded-full hover:bg-gray-50 md:hidden'
>
-
+
-
Inventory
+
+ Inventory
+
navigate('/new-product')}
- className="bg-primary hover:bg-primary-600 text-black font-medium px-4 py-2 rounded-lg transition-colors flex items-center space-x-2"
+ className='flex items-center px-4 py-2 space-x-2 font-medium text-black transition-colors rounded-lg bg-primary hover:bg-primary-600'
>
- Create Item
+ Create Item
{/* Filters Row */}
-
-
+
+
{/* Category Filter */}
-
+
setCategoryFilter(e.target.value)}
- className="appearance-none bg-bg-surface border border-divider rounded-lg px-4 py-2 pr-8 text-sm focus:ring-2 focus:ring-complement focus:border-transparent text-text-primary"
+ className='px-4 py-2 pr-8 text-sm border rounded-lg appearance-none bg-bg-surface border-divider focus:ring-2 focus:ring-complement focus:border-transparent text-text-primary'
>
- All Categories
- {categories.slice(1).map(category => (
- {category}
+ All Categories
+ {categories.slice(1).map((category) => (
+
+ {category}
+
))}
-
+
{/* Status Toggle */}
-
+
setStatusFilter('all')}
className={`px-3 py-1 rounded-md text-sm font-medium transition-colors ${
@@ -251,27 +284,30 @@ const InventoryPage: React.FC = () => {
{/* All Filters Dropdown */}
-
-
+
+
All Filters
-
+
{/* Search and Actions */}
-
-
-
+
+
+
setSearchTerm(e.target.value)}
- placeholder="Search..."
- className="pl-10 pr-4 py-2 border border-divider rounded-lg text-sm focus:ring-2 focus:ring-complement focus:border-transparent w-48 bg-bg-surface text-text-primary"
+ placeholder='Search...'
+ className='w-48 py-2 pl-10 pr-4 text-sm border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary'
/>
-
-
+
+
@@ -280,27 +316,32 @@ const InventoryPage: React.FC = () => {
{/* Content */}
-
-
+
{/* Error Message */}
{error && (
-
-
-
-
-
+
+
+
-
-
Error
-
- {error}
-
-
+
+
Error
+
{error}
+
setError(null)}
- className="bg-red-50 px-2 py-1.5 rounded-md text-sm font-medium text-red-800 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-red-50 focus:ring-red-600"
+ className='bg-red-50 px-2 py-1.5 rounded-md text-sm font-medium text-red-800 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-red-50 focus:ring-red-600'
>
Dismiss
@@ -311,100 +352,130 @@ const InventoryPage: React.FC = () => {
)}
{/* Desktop/Tablet Table */}
-
-
-
+
+
+
-
+
handleSelectAll(e.target.checked)}
- className="w-4 h-4 text-complement border-gray-300 rounded focus:ring-complement"
+ className='w-4 h-4 border-gray-300 rounded text-complement focus:ring-complement'
/>
-
+
Image
-
+
Item
-
+
Category
-
+
Stock
-
+
Price
-
+
-
+
{filteredItems.map((item) => (
-
+
handleSelectItem(item.id, e.target.checked)}
- className="w-4 h-4 text-complement border-gray-300 rounded focus:ring-complement"
+ onChange={(e) =>
+ handleSelectItem(item.id, e.target.checked)
+ }
+ className='w-4 h-4 border-gray-300 rounded text-complement focus:ring-complement'
/>
-
-
-
+
+
-
-
-
{item.name}
-
+
+
+
+ {item.name}
+
+
{item.status === 'active' ? 'Active' : 'Inactive'}
-
+
{item.category}
-
- 10
- ? 'bg-success-100 text-success-800'
- : item.stock > 0
+
+ 10
+ ? 'bg-success-100 text-success-800'
+ : item.stock > 0
? 'bg-warning-100 text-warning-800'
: 'bg-error-100 text-error-800'
- }`}>
+ }`}
+ >
{item.stock} units
-
+
${item.price.toFixed(2)}
-
-
+
+
handleEditProduct(item.id)}
- className="p-1 hover:bg-gray-50 rounded transition-colors"
- title="Edit"
+ className='p-1 transition-colors rounded hover:bg-gray-50'
+ title='Edit'
>
-
-
+
+
handleDeleteClick(item.id, e)}
- className="p-1 hover:bg-red-50 rounded transition-colors"
- title="Delete"
+ className='p-1 transition-colors rounded hover:bg-red-50'
+ title='Delete'
>
-
-
+
+
@@ -416,72 +487,100 @@ const InventoryPage: React.FC = () => {
{/* Mobile Card Layout */}
-
+
{filteredItems.map((item) => (
-
+
handleSelectItem(item.id, e.target.checked)}
- className="w-4 h-4 text-complement border-gray-300 rounded focus:ring-complement mt-1"
+ className='w-4 h-4 mt-1 border-gray-300 rounded text-complement focus:ring-complement'
/>
-
-
+
-
-
-
-
+
+
+
+
{item.name}
-
{item.category}
-
-
-
10
- ? 'bg-success-100 text-success-800'
- : item.stock > 0
+
+ {item.category}
+
+
+
+ 10
+ ? 'bg-success-100 text-success-800'
+ : item.stock > 0
? 'bg-warning-100 text-warning-800'
: 'bg-error-100 text-error-800'
- }`}>
+ }`}
+ >
{item.stock} units
-
+
${item.price.toFixed(2)}
-
+
{item.status === 'active' ? 'Active' : 'Inactive'}
-
+
handleEditProduct(item.id)}
- className="p-1 hover:bg-gray-50 rounded transition-colors"
- title="Edit"
+ className='p-1 transition-colors rounded hover:bg-gray-50'
+ title='Edit'
>
-
-
+
+
handleDeleteClick(item.id, e)}
- className="p-1 hover:bg-red-50 rounded transition-colors"
- title="Delete"
+ className='p-1 transition-colors rounded hover:bg-red-50'
+ title='Delete'
>
-
-
+
+
@@ -494,46 +593,63 @@ const InventoryPage: React.FC = () => {
{/* Empty State */}
{filteredItems.length === 0 && (
-
-
-
No items found
-
Try adjusting the filters or create a new item.
+
+
+
+ No items found
+
+
+ Try adjusting the filters or create a new item.
+
)}
{/* Delete Confirmation Modal */}
{deleteConfirmId && (
-
-
-
-
-
-
+
+
+
+
-
Confirm Delete
+
+ Confirm Delete
+
-
- Are you sure you want to delete this product? This action cannot be undone.
+
+ Are you sure you want to delete this product? This action cannot
+ be undone.
-
+
Cancel
{isDeleting ? (
<>
-
+
Deleting...
>
) : (
diff --git a/frontend/src/components/LandingPage.tsx b/frontend/src/components/LandingPage.tsx
index 24f52786..96150308 100644
--- a/frontend/src/components/LandingPage.tsx
+++ b/frontend/src/components/LandingPage.tsx
@@ -6,717 +6,732 @@ import BetaModal from './landing/BetaModal';
import BetaCounter from './landing/BetaCounter';
export default function ZatoBoxBitcoiners() {
- const [isModalOpen, setIsModalOpen] = useState(false);
- const [referralCode, setReferralCode] = useState('');
-
- const generateReferralCode = () => {
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
- let result = '';
- for (let i = 0; i < 8; i++) {
- result += chars.charAt(Math.floor(Math.random() * chars.length));
- }
- return result;
- };
-
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault();
- const newReferralCode = generateReferralCode();
- setReferralCode(newReferralCode);
- setIsModalOpen(true);
- };
-
- return (
-
-
-
-
-
-
-
-
- ACCEPT
-
- BITCOIN
-
- WITHOUT NODES OR COMPLICATIONS.
-
-
- CONNECT YOUR WALLET AND START ACCEPTING PAYMENTS IN SECONDS.
-
-
-
- {'>'} NO NEED TO RUN A NODE OR MANAGE COMPLEX APIs
-
-
- {'>'} LIGHTNING AND ON-CHAIN PAYMENTS INTEGRATED
-
-
- {'>'} COMPATIBLE WITH YOUR FAVORITE WALLETS
-
-
- {'>'} TOTAL PRIVACY AND SELF-CUSTODY
-
-
-
- {/* Main CTA Button */}
-
-
-
- ⚡ ACCESS BETA
-
-
-
- {'>'} NO CREDIT CARD. FIRST MONTH FREE. SLOTS BY REGISTRATION
- ORDER.
-
-
-
-
-
-
-
-
- {/* YouTube Video instead of The Mask MP4 */}
-
- VIDEO
-
-
-
- {/* Beta Slots Counter below the video */}
-
-
-
-
-
-
-
-
- {/* Main Benefits Section - Fused from Why ZatoBox and Key Features */}
-
-
-
- MAIN BENEFITS
-
-
- "EVERYTHING YOU NEED TO ACCEPT BITCOIN, WITHOUT THE TECHNICAL
- HEADACHES."
-
-
-
-
-
- [01] NO NODES OR COMPLEX SETUP
-
-
- {'>'} NO NEED TO RUN A NODE
-
- {'>'} NO COMPLEX APIs TO MANAGE
-
- {'>'} EVERYTHING WORKS OUT OF THE BOX
-
-
- SAVE TIME AND AVOID TECHNICAL RISKS
-
-
-
-
-
- [02] LIGHTNING + ON-CHAIN INTEGRATION
-
-
- {'>'} INSTANT PAYMENTS
-
- {'>'} AUTOMATIC QR GENERATION
-
- {'>'} OPTIONAL FIAT CONVERSION
-
-
- RECEIVE PAYMENTS AND MAINTAIN CONTROL OF YOUR BTC
-
-
-
-
-
- [03] COMPATIBLE WALLETS
-
-
- {'>'} BLUEWALLET, MUUN, PHOENIX
-
- {'>'} BREEZ AND MORE
-
- {'>'} USE YOUR FAVORITE WALLET
-
-
- NO NEED TO CHANGE YOUR EXISTING SETUP
-
-
-
-
-
- [04] SELF-CUSTODY & PRIVACY
-
-
- {'>'} YOU CONTROL YOUR KEYS
-
- {'>'} ZATOBOX DOESN'T HOLD FUNDS
-
- {'>'} TOTAL PRIVACY GUARANTEED
-
-
- YOUR BITCOIN, YOUR CONTROL, YOUR PRIVACY
-
-
-
-
-
- [05] WEB + PHYSICAL POS
-
-
- {'>'} WORKS ON ANY DEVICE
-
- {'>'} TABLET, LAPTOP OR PHONE
-
- {'>'} HOSTING AND SUPPORT INCLUDED
-
-
- FOCUS ON YOUR BUSINESS, NOT ON TECHNICAL ISSUES
-
-
-
-
-
- [06] TRANSPARENT PRICING
-
-
- {'>'} $29.99/MONTH OR $200/YEAR
-
- {'>'} NO HIDDEN FEES
-
- {' '}
-
- SEE WHAT'S INCLUDED
-
-
-
- CLEAR PRICING, NO SURPRISES
-
-
-
-
-
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [referralCode, setReferralCode] = useState('');
+
+ const generateReferralCode = () => {
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
+ let result = '';
+ for (let i = 0; i < 8; i++) {
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
+ }
+ return result;
+ };
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ const newReferralCode = generateReferralCode();
+ setReferralCode(newReferralCode);
+ setIsModalOpen(true);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ ACCEPT
+
+
+ BITCOIN
+
+
+ WITHOUT NODES OR COMPLICATIONS.
+
+
+ CONNECT YOUR WALLET AND START ACCEPTING PAYMENTS IN SECONDS.
+
+
+
+ {'>'} NO NEED TO RUN A NODE OR MANAGE COMPLEX APIs
-
-
- {/* Demo */}
-
-
-
- SEE HOW
-
-
- SIMPLE IT IS
-
-
-
- WATCH THE DEMO AND SEE HOW EASY IT IS TO START ACCEPTING BITCOIN.
-
-
- {/* YouTube Video */}
-
-
-
- INVOICE SCAN → LIGHTNING QR GENERATION → DASHBOARD WITH BALANCE
-
-
-
-
- JOIN AND TRY THE BETA FOR FREE
-
-
+
+ {'>'} LIGHTNING AND ON-CHAIN PAYMENTS INTEGRATED
-
-
- {/* Plans and Pricing */}
-
-
-
- TRANSPARENT PRICING
-
-
-
-
-
- [MONTHLY PLAN]
-
-
- $29.99
-
-
- PER MONTH
-
-
- ✓ COMPLETE POS SYSTEM
-
- ✓ LIGHTNING & ON-CHAIN PAYMENTS
-
-
- ✓ INVENTORY MANAGEMENT
-
-
- ✓ OCR INVOICE SCANNING
-
-
- ✓ HOSTING INCLUDED
-
- ✓ REAL SUPPORT
-
- ✓ CANCEL ANYTIME
-
-
-
- START MONTHLY PLAN
-
-
- ✓ NO CREDIT CARD REQUIRED • ✓ CANCEL ANYTIME
-
-
-
-
-
- BEST VALUE
-
-
- [YEARLY PLAN]
-
-
- $200
-
-
- PER YEAR
-
-
- SAVE $159.88
-
-
-
- ✓ EVERYTHING FROM MONTHLY PLAN
-
-
- ✓ IMMEDIATE ACCESS
-
-
- ✓ PRIORITY SUPPORT
-
-
- ✓ CUSTOM INTEGRATIONS
-
-
- ✓ FEATURE DEVELOPMENT INPUT
-
-
- ✓ NO CREDIT CARD REQUIRED
-
-
-
- ⚡ START YEARLY PLAN
-
-
- ✓ NO CREDIT CARD REQUIRED • ✓ CANCEL ANYTIME
-
-
-
-
- {/* Collaborative Development Section */}
-
-
-
- [COLLABORATIVE DEVELOPMENT]
-
-
- YEARLY PLAN MEMBERS DEVELOP CUSTOM TOOLS TOGETHER WITH OUR TEAM
-
-
-
-
- [STEP-BY-STEP PROCESS]
-
-
- • 1. DEFINE YOUR NEEDS
- • 2. DESIGN THE SOLUTION
- • 3. DEVELOP TOGETHER
- • 4. TEST & DEPLOY
-
-
-
-
- [CUSTOM INTEGRATIONS]
-
-
- • YOUR SPECIFIC WALLET
- • YOUR INVENTORY SYSTEM
- • YOUR PAYMENT PROCESSORS
- • YOUR ACCOUNTING TOOLS
-
-
-
-
-
- YOUR BUSINESS PRIORITIES ARE OUR DEVELOPMENT PRIORITIES
-
-
- GET YEARLY PLAN FOR COLLABORATIVE DEVELOPMENT
-
-
-
-
-
- {/* Additional Information */}
-
-
-
- [NEED HELP?]
-
-
- IF YOU WANT TO TRY BEFORE BUYING, YOU CAN:
-
- • REQUEST A CUSTOM DEMO
-
- • CONSULT WITH OUR TEAM
- • WATCH DEMONSTRATION VIDEOS
-
-
-
- REQUEST DEMO
-
-
- CONTACT SUPPORT
-
-
-
-
+
+ {'>'} COMPATIBLE WITH YOUR FAVORITE WALLETS
-
-
- {/* Beta Access Form - Simplified */}
-
-
-
- [ACCESS BETA]
-
-
-
-
- {'>'} NO CREDIT CARD. FIRST MONTH FREE. SLOTS BY REGISTRATION
- ORDER.
-
-
- {'>'} WE'LL CONTACT YOU FOR ADDITIONAL DETAILS AFTER REGISTRATION.
-
-
+
+ {'>'} TOTAL PRIVACY AND SELF-CUSTODY
-
-
- {/* Social Proof Section */}
-
-
-
- TRUSTED BY BITCOINERS
-
-
-
-
-
-
-
- [COMMUNITY TESTED]
-
-
- JOIN OUR GROWING COMMUNITY OF BITCOIN BUSINESSES
-
-
- JOIN DISCORD →
-
-
-
+
+
+ {/* Main CTA Button */}
+
+
+
+ ⚡ ACCESS BETA
+
+
+
+ {'>'} NO CREDIT CARD. FIRST MONTH FREE. SLOTS BY REGISTRATION
+ ORDER.
+
+
+
+
+
+
+
+
+ {/* YouTube Video instead of The Mask MP4 */}
+
+ VIDEO
-
+
- {/* FAQ */}
-
-
-
- [FAQ]
-
-
-
-
- DO I NEED TO RUN A NODE?
-
-
- NO, ZATOBOX HANDLES THE BACKEND AND HOSTING. YOU JUST CONNECT
- YOUR WALLET.
-
-
-
-
- CAN I USE LIGHTNING AND ON-CHAIN?
-
-
- YES, BOTH ARE INTEGRATED. CHOOSE THE ONE YOU PREFER.
-
-
-
-
- CAN I WITHDRAW TO MY WALLET?
-
-
- YES, ZATOBOX DOESN'T HOLD FUNDS, IT JUST FACILITATES THE
- PROCESS.
-
-
-
-
- WHAT WALLETS ARE SUPPORTED?
-
-
- BLUEWALLET, MUUN, PHOENIX, BREEZ AND OTHER LIGHTNING-COMPATIBLE
- ONES.
-
-
-
-
- WHAT HAPPENS AFTER THE FREE MONTH?
-
-
- YOU CAN CONTINUE FOR $29.99/MONTH OR $200/YEAR, OR CANCEL
- WITHOUT PENALTY.
-
-
-
-
- IS IT OPEN SOURCE?
-
-
- YES, YOU CAN VIEW THE CODE ON{' '}
-
- GITHUB
-
- .
-
-
-
+ {/* Beta Slots Counter below the video */}
+
+
+
+
+
+
+
+
+ {/* Main Benefits Section - Fused from Why ZatoBox and Key Features */}
+
+
+
+ MAIN BENEFITS
+
+
+ "EVERYTHING YOU NEED TO ACCEPT BITCOIN, WITHOUT THE TECHNICAL
+ HEADACHES."
+
+
+
+
+
+ [01] NO NODES OR COMPLEX SETUP
+
+
+ {'>'} NO NEED TO RUN A NODE
+
+ {'>'} NO COMPLEX APIs TO MANAGE
+
+ {'>'} EVERYTHING WORKS OUT OF THE BOX
+
+
+ SAVE TIME AND AVOID TECHNICAL RISKS
+
+
+
+
+
+ [02] LIGHTNING + ON-CHAIN INTEGRATION
+
+
+ {'>'} INSTANT PAYMENTS
+
+ {'>'} AUTOMATIC QR GENERATION
+
+ {'>'} OPTIONAL FIAT CONVERSION
+
+
+ RECEIVE PAYMENTS AND MAINTAIN CONTROL OF YOUR BTC
+
+
+
+
+
+ [03] COMPATIBLE WALLETS
+
+
+ {'>'} BLUEWALLET, MUUN, PHOENIX
+
+ {'>'} BREEZ AND MORE
+
+ {'>'} USE YOUR FAVORITE WALLET
+
+
+ NO NEED TO CHANGE YOUR EXISTING SETUP
+
+
+
+
+
+ [04] SELF-CUSTODY & PRIVACY
+
+
+ {'>'} YOU CONTROL YOUR KEYS
+
+ {'>'} ZATOBOX DOESN'T HOLD FUNDS
+
+ {'>'} TOTAL PRIVACY GUARANTEED
+
+
+ YOUR BITCOIN, YOUR CONTROL, YOUR PRIVACY
+
+
+
+
+
+ [05] WEB + PHYSICAL POS
+
+
+ {'>'} WORKS ON ANY DEVICE
+
+ {'>'} TABLET, LAPTOP OR PHONE
+
+ {'>'} HOSTING AND SUPPORT INCLUDED
+
+
+ FOCUS ON YOUR BUSINESS, NOT ON TECHNICAL ISSUES
+
+
+
+
+
+ [06] TRANSPARENT PRICING
+
+
+ {'>'} $29.99/MONTH OR $200/YEAR
+
+ {'>'} NO HIDDEN FEES
+ {' '}
+
+ SEE WHAT'S INCLUDED
+
+
+
+ CLEAR PRICING, NO SURPRISES
+
+
+
+
+
+
+
+
+ {/* Demo */}
+
+
+
+ SEE HOW
+
+
+ SIMPLE IT IS
+
+
+
+ WATCH THE DEMO AND SEE HOW EASY IT IS TO START ACCEPTING BITCOIN.
+
+
+ {/* YouTube Video */}
+
+
+
+ INVOICE SCAN → LIGHTNING QR GENERATION → DASHBOARD WITH BALANCE
+
+
+
+
+ JOIN AND TRY THE BETA FOR FREE
+
+
+
+
+
+ {/* Plans and Pricing */}
+
+
+
+ TRANSPARENT PRICING
+
+
+
+
+
+ [MONTHLY PLAN]
+
+
+ $29.99
+
+
+ PER MONTH
+
+
+ ✓ COMPLETE POS SYSTEM
+
+ ✓ LIGHTNING & ON-CHAIN PAYMENTS
+
+
+ ✓ INVENTORY MANAGEMENT
+
+
+ ✓ OCR INVOICE SCANNING
+
+
+ ✓ HOSTING INCLUDED
+
+ ✓ REAL SUPPORT
+
+ ✓ CANCEL ANYTIME
+
+
+
+ START MONTHLY PLAN
+
+
+ ✓ NO CREDIT CARD REQUIRED • ✓ CANCEL ANYTIME
+
+
+
+
+
+ BEST VALUE
+
+
+ [YEARLY PLAN]
+
+
+ $200
+
+
+ PER YEAR
+
+
+ SAVE $159.88
+
+
+
+ ✓ EVERYTHING FROM MONTHLY PLAN
+
+
+ ✓ IMMEDIATE ACCESS
+
+
+ ✓ PRIORITY SUPPORT
+
+
+ ✓ CUSTOM INTEGRATIONS
+
+
+ ✓ FEATURE DEVELOPMENT INPUT
+
+
+ ✓ NO CREDIT CARD REQUIRED
+
+
+
+ ⚡ START YEARLY PLAN
+
+
+ ✓ NO CREDIT CARD REQUIRED • ✓ CANCEL ANYTIME
+
+
+
+
+ {/* Collaborative Development Section */}
+
+
+
+ [COLLABORATIVE DEVELOPMENT]
+
+
+ YEARLY PLAN MEMBERS DEVELOP CUSTOM TOOLS TOGETHER WITH OUR TEAM
+
+
+
+
+ [STEP-BY-STEP PROCESS]
+
+
+ • 1. DEFINE YOUR NEEDS
+ • 2. DESIGN THE SOLUTION
+ • 3. DEVELOP TOGETHER
+ • 4. TEST & DEPLOY
+
-
-
- {/* Footer */}
-
+
+ {/* Additional Information */}
+
+
+
+ [NEED HELP?]
+
+
+ IF YOU WANT TO TRY BEFORE BUYING, YOU CAN:
+
+ • REQUEST A CUSTOM DEMO
+
+ • CONSULT WITH OUR TEAM
+ • WATCH DEMONSTRATION VIDEOS
+
+
+
+
+
+
+
+ {/* Beta Access Form - Simplified */}
+
+
+
+ [ACCESS BETA]
+
+
+
+
+ {'>'} NO CREDIT CARD. FIRST MONTH FREE. SLOTS BY REGISTRATION
+ ORDER.
+
+
+ {'>'} WE'LL CONTACT YOU FOR ADDITIONAL DETAILS AFTER REGISTRATION.
+
+
+
+
+
+ {/* Social Proof Section */}
+
+
+
+ TRUSTED BY BITCOINERS
+
+
+
+
- {/* Beta Confirmation Modal */}
-
setIsModalOpen(false)}
- referralCode={referralCode}
- />
+
+
+ [COMMUNITY TESTED]
+
+
+ JOIN OUR GROWING COMMUNITY OF BITCOIN BUSINESSES
+
+
+ JOIN DISCORD →
+
+
+
+
+
+
+ {/* FAQ */}
+
+
+
+ [FAQ]
+
+
+
+
+ DO I NEED TO RUN A NODE?
+
+
+ NO, ZATOBOX HANDLES THE BACKEND AND HOSTING. YOU JUST CONNECT
+ YOUR WALLET.
+
+
+
+
+ CAN I USE LIGHTNING AND ON-CHAIN?
+
+
+ YES, BOTH ARE INTEGRATED. CHOOSE THE ONE YOU PREFER.
+
+
+
+
+ CAN I WITHDRAW TO MY WALLET?
+
+
+ YES, ZATOBOX DOESN'T HOLD FUNDS, IT JUST FACILITATES THE
+ PROCESS.
+
+
+
+
+ WHAT WALLETS ARE SUPPORTED?
+
+
+ BLUEWALLET, MUUN, PHOENIX, BREEZ AND OTHER LIGHTNING-COMPATIBLE
+ ONES.
+
+
+
+
+ WHAT HAPPENS AFTER THE FREE MONTH?
+
+
+ YOU CAN CONTINUE FOR $29.99/MONTH OR $200/YEAR, OR CANCEL
+ WITHOUT PENALTY.
+
+
+
+
+ IS IT OPEN SOURCE?
+
+
+ YES, YOU CAN VIEW THE CODE ON{' '}
+
+ GITHUB
+
+ .
+
+
+
- );
+
+
+ {/* Footer */}
+
+
+
+
+
+
+
+
+ MODULAR POS FOR BITCOIN-FRIENDLY BUSINESSES.
+
+ NO COMPLICATIONS. NO NODES.
+
+
+
+
+
+
+
+ © 2025 ZATOBOX - MODULAR POS FOR BITCOINERS
+
+
+
+
+
+ {/* Minimalist Floating CTA */}
+
+
+ {/* Beta Confirmation Modal */}
+
setIsModalOpen(false)}
+ referralCode={referralCode}
+ />
+
+ );
}
diff --git a/frontend/src/components/LoginPage.tsx b/frontend/src/components/LoginPage.tsx
index c9afa22d..cca5657c 100644
--- a/frontend/src/components/LoginPage.tsx
+++ b/frontend/src/components/LoginPage.tsx
@@ -160,8 +160,7 @@ const LoginPage: React.FC = () => {
- {/* Divider */}
-
+
@@ -213,8 +212,7 @@ const LoginPage: React.FC = () => {
- {/* Sign Up Link */}
-
+
Don't have an account?{' '}
{
- {/* Sign Up Link */}
-
+
+
+
+
+ Or continue with
+
+
+
+
+
+
handleSocialLogin('google')}
+ className='flex items-center justify-center h-12 px-4 transition-colors border rounded-lg border-divider hover:bg-gray-50'
+ >
+
+
+
+
+
+
+ Google
+
+
handleSocialLogin('github')}
+ className='flex items-center justify-center h-12 px-4 transition-colors border rounded-lg border-divider hover:bg-gray-50'
+ >
+
+
+
+ GitHub
+
+
+
+
Don't have an account?{' '}
{
'Electronics',
'Decoration',
'Office',
+ 'Gaming',
+ ];
+
+ const unitOptions = [
+ { label: 'Per item', value: 'Per item' },
+ { label: 'Per kilogram', value: 'Per kilogram' },
+ { label: 'Per liter', value: 'Per liter' },
+ { label: 'Per metro', value: 'Per metro' },
+ ];
+
+ const productTypeOptions = [
+ { label: 'Physical Product', value: 'Physical Product' },
+ { label: 'Service', value: 'Service' },
+ { label: 'Digital', value: 'Digital' },
];
const handleInputChange = (field: string, value: string | boolean) => {
@@ -103,42 +117,40 @@ const NewProductPage: React.FC = () => {
setLoading(false);
return;
}
+ if (selectedCategories.length === 0) {
+ setError('Category is required');
+ setLoading(false);
+ return;
+ }
+ const priceNum = Number(formData.price);
+ if (!Number.isFinite(priceNum) || priceNum <= 0) {
+ setError('Price must be greater than 0');
+ setLoading(false);
+ return;
+ }
+ const stockNum = parseInt(formData.inventoryQuantity, 10);
+ if (!Number.isFinite(stockNum) || Number.isNaN(stockNum) || stockNum < 0) {
+ setError('Inventory quantity must be 0 or more');
+ setLoading(false);
+ return;
+ }
+
try {
- const productPayload: any = {
+ const productPayload: Record = {
name: formData.name,
description: formData.description || null,
- price: parseFloat(formData.price) || 0,
- stock: parseInt(formData.inventoryQuantity) || 0,
- min_stock: parseInt(formData.lowStockAlert) || 0,
- weight: formData.weight ? parseFloat(formData.weight) : null,
+ price: priceNum,
+ stock: stockNum,
+ unit: formData.unit,
+ category: selectedCategories[0],
sku: formData.sku || null,
- unit_name: formData.unit,
- product_type: formData.productType || 'Physical Product',
- localization: formData.location || null,
- status: 'active',
};
- if (selectedCategories.length > 0) {
- productPayload.category = selectedCategories[0];
- }
- console.log('DEBUG create payload:', productPayload);
- const createResult = await productsAPI.create(productPayload);
- if (!createResult || !createResult.success) {
- setError(
- (createResult && createResult.message) || 'Error creating product'
- );
- setLoading(false);
- return;
- }
- const createdProduct = createResult.product;
- if (selectedFiles.length > 0 && createdProduct && createdProduct.id) {
- const imagesForm = new FormData();
- selectedFiles.forEach((file) => {
- imagesForm.append('images', file);
- });
- if (productsAPI.uploadImages) {
- await productsAPI.uploadImages(createdProduct.id, imagesForm);
- }
+
+ if (formData.productType && formData.productType !== '') {
+ productPayload.product_type = formData.productType;
}
+
+ await productsAPI.create(productPayload as any);
navigate('/inventory');
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Error creating product');
@@ -335,10 +347,29 @@ const NewProductPage: React.FC = () => {
className='w-full p-3 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary'
required
>
- Per item
- Per kilogramo
- Per metro
- Per litro
+ {unitOptions.map((opt) => (
+
+ {opt.label}
+
+ ))}
+
+
+
+
+ Product Type
+
+
+ handleInputChange('productType', e.target.value)
+ }
+ className='w-full p-3 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary'
+ >
+ {productTypeOptions.map((opt) => (
+
+ {opt.label}
+
+ ))}
diff --git a/frontend/src/components/OCRResultPage.tsx b/frontend/src/components/OCRResultPage.tsx
index 5d975300..aa39d7cc 100644
--- a/frontend/src/components/OCRResultPage.tsx
+++ b/frontend/src/components/OCRResultPage.tsx
@@ -21,6 +21,27 @@ const OCRResultPage: React.FC = () => {
});
const [systemStatus, setSystemStatus] = useState
(null);
+ const maintenanceMode = true;
+
+ if (maintenanceMode) {
+ return (
+
+
+
In maintenance
+
+ The OCR feature is temporarily unavailable.
+
+
+ In maintenance
+
+
+
+ );
+ }
+
const handleFileChange = (e: React.ChangeEvent) => {
if (e.target.files && e.target.files[0]) {
setFile(e.target.files[0]);
diff --git a/frontend/src/components/PluginStorePage.tsx b/frontend/src/components/PluginStorePage.tsx
index f962813e..334e093e 100644
--- a/frontend/src/components/PluginStorePage.tsx
+++ b/frontend/src/components/PluginStorePage.tsx
@@ -10,7 +10,7 @@ interface IPlugin {
description: string;
category: string;
icon: string;
- status: 'active' | 'inactive' | 'coming-soon';
+ status: 'active' | 'inactive' | 'coming-soon' | 'maintenance';
version: string;
author: string;
rating: number;
@@ -34,7 +34,9 @@ const PluginStorePage: React.FC = () => {
const categoriesRef = useRef(null);
const [showNotification, setShowNotification] = useState(false);
const [notificationMessage, setNotificationMessage] = useState('');
- const [notificationType, setNotificationType] = useState<'success' | 'info'>('info');
+ const [notificationType, setNotificationType] = useState<'success' | 'info'>(
+ 'info'
+ );
const categories = [
{ id: 'all', name: 'Discover', icon: '🌟' },
@@ -52,22 +54,30 @@ const PluginStorePage: React.FC = () => {
{
id: 'ocr-module',
name: 'OCR Document Scanner',
- description: 'Scan and extract data from invoices, receipts, and documents automatically',
+ description:
+ 'Scan and extract data from invoices, receipts, and documents automatically',
category: 'productivity',
icon: '🔍',
- status: 'active',
+ status: 'maintenance',
version: '1.2.0',
author: 'ZatoBox Team',
rating: 4.8,
installs: 1250,
price: 'free',
- features: ['Document scanning', 'Data extraction', 'Invoice processing', 'Receipt management'],
- screenshot: 'https://images.unsplash.com/photo-1554224155-6726b3ff858f?w=400&h=300&fit=crop',
+ features: [
+ 'Document scanning',
+ 'Data extraction',
+ 'Invoice processing',
+ 'Receipt management',
+ ],
+ screenshot:
+ 'https://images.unsplash.com/photo-1554224155-6726b3ff858f?w=400&h=300&fit=crop',
},
{
id: 'smart-inventory',
name: 'Smart Inventory Manager',
- description: 'Advanced inventory tracking with AI-powered stock predictions and alerts',
+ description:
+ 'Advanced inventory tracking with AI-powered stock predictions and alerts',
category: 'inventory',
icon: '🧠',
status: 'coming-soon',
@@ -76,13 +86,20 @@ const PluginStorePage: React.FC = () => {
rating: 0,
installs: 0,
price: 'premium',
- features: ['AI predictions', 'Low stock alerts', 'Demand forecasting', 'Automated reordering'],
- screenshot: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=300&fit=crop',
+ features: [
+ 'AI predictions',
+ 'Low stock alerts',
+ 'Demand forecasting',
+ 'Automated reordering',
+ ],
+ screenshot:
+ 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=300&fit=crop',
},
{
id: 'advanced-analytics',
name: 'Advanced Analytics Dashboard',
- description: 'Comprehensive business analytics with real-time insights and reporting',
+ description:
+ 'Comprehensive business analytics with real-time insights and reporting',
category: 'analytics',
icon: '📈',
status: 'coming-soon',
@@ -91,12 +108,18 @@ const PluginStorePage: React.FC = () => {
rating: 0,
installs: 0,
price: 'premium',
- features: ['Real-time dashboards', 'Custom reports', 'Data visualization', 'Export capabilities'],
+ features: [
+ 'Real-time dashboards',
+ 'Custom reports',
+ 'Data visualization',
+ 'Export capabilities',
+ ],
},
{
id: 'pos-integration',
name: 'POS System Integration',
- description: 'Connect with popular POS systems for seamless data synchronization',
+ description:
+ 'Connect with popular POS systems for seamless data synchronization',
category: 'integrations',
icon: '💳',
status: 'active',
@@ -105,7 +128,12 @@ const PluginStorePage: React.FC = () => {
rating: 4.7,
installs: 850,
price: 'free',
- features: ['Multi-POS support', 'Real-time sync', 'Payment processing', 'Receipt printing'],
+ features: [
+ 'Multi-POS support',
+ 'Real-time sync',
+ 'Payment processing',
+ 'Receipt printing',
+ ],
},
{
id: 'email-automation',
@@ -119,7 +147,12 @@ const PluginStorePage: React.FC = () => {
rating: 0,
installs: 0,
price: 'premium',
- features: ['Email templates', 'Automated campaigns', 'Customer segmentation', 'Performance tracking'],
+ features: [
+ 'Email templates',
+ 'Automated campaigns',
+ 'Customer segmentation',
+ 'Performance tracking',
+ ],
},
{
id: 'mobile-app',
@@ -133,12 +166,18 @@ const PluginStorePage: React.FC = () => {
rating: 0,
installs: 0,
price: 'free',
- features: ['Offline mode', 'Push notifications', 'Barcode scanning', 'Quick actions'],
+ features: [
+ 'Offline mode',
+ 'Push notifications',
+ 'Barcode scanning',
+ 'Quick actions',
+ ],
},
{
id: 'api-gateway',
name: 'API Gateway',
- description: 'Developer tools for custom integrations and third-party connections',
+ description:
+ 'Developer tools for custom integrations and third-party connections',
category: 'developer',
icon: '🔌',
status: 'coming-soon',
@@ -161,23 +200,32 @@ const PluginStorePage: React.FC = () => {
rating: 0,
installs: 0,
price: 'premium',
- features: ['Store management', 'Inventory sync', 'Centralized reporting', 'Role-based access'],
+ features: [
+ 'Store management',
+ 'Inventory sync',
+ 'Centralized reporting',
+ 'Role-based access',
+ ],
},
];
useEffect(() => {
// Simulate loading
setTimeout(() => {
- // Sync plugin status with context
- const syncedPlugins = mockPlugins.map(plugin => {
- // Keep coming-soon plugins as coming-soon
- if (plugin.status === 'coming-soon') {
+ const syncedPlugins = mockPlugins.map((plugin) => {
+ if (
+ plugin.status === 'coming-soon' ||
+ plugin.status === 'maintenance'
+ ) {
return plugin;
}
- // For other plugins, sync with context
return {
...plugin,
- status: (isPluginActive(plugin.id) ? 'active' : 'inactive') as 'active' | 'inactive' | 'coming-soon',
+ status: (isPluginActive(plugin.id) ? 'active' : 'inactive') as
+ | 'active'
+ | 'inactive'
+ | 'coming-soon'
+ | 'maintenance',
};
});
@@ -192,14 +240,17 @@ const PluginStorePage: React.FC = () => {
// Filter by category
if (selectedCategory !== 'all') {
- filtered = filtered.filter(plugin => plugin.category === selectedCategory);
+ filtered = filtered.filter(
+ (plugin) => plugin.category === selectedCategory
+ );
}
// Filter by search query
if (searchQuery) {
- filtered = filtered.filter(plugin =>
- plugin.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
- plugin.description.toLowerCase().includes(searchQuery.toLowerCase()),
+ filtered = filtered.filter(
+ (plugin) =>
+ plugin.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ plugin.description.toLowerCase().includes(searchQuery.toLowerCase())
);
}
@@ -248,7 +299,10 @@ const PluginStorePage: React.FC = () => {
}, [filteredPlugins]);
// Function to show notification
- const showPluginNotification = (message: string, type: 'success' | 'info') => {
+ const showPluginNotification = (
+ message: string,
+ type: 'success' | 'info'
+ ) => {
// Only show notifications when explicitly called, not automatically
setNotificationMessage(message);
setNotificationType(type);
@@ -259,106 +313,142 @@ const PluginStorePage: React.FC = () => {
}, 3000);
};
- const handlePluginToggle = async(pluginId: string) => {
+ const handlePluginToggle = async (pluginId: string) => {
if (!token) {
alert('Please log in to manage plugins');
return;
}
// Get current plugin info
- const plugin = plugins.find(p => p.id === pluginId);
- if (!plugin) {return;}
+ const plugin = plugins.find((p) => p.id === pluginId);
+ if (!plugin) {
+ return;
+ }
// Toggle plugin status using context
togglePlugin(pluginId);
// Update local state to reflect the change
- setPlugins(prev => prev.map(p => {
- if (p.id === pluginId) {
- const newStatus = isPluginActive(pluginId) ? 'active' : 'inactive';
- return { ...p, status: newStatus as 'active' | 'inactive' | 'coming-soon' };
- }
- return p;
- }));
+ setPlugins((prev) =>
+ prev.map((p) => {
+ if (p.id === pluginId) {
+ const newStatus = isPluginActive(pluginId) ? 'active' : 'inactive';
+ return {
+ ...p,
+ status: newStatus as 'active' | 'inactive' | 'coming-soon',
+ };
+ }
+ return p;
+ })
+ );
// Plugin status updated successfully
};
const getStatusBadge = (status: string) => {
switch (status) {
- case 'active':
- return Active ;
- case 'inactive':
- return Inactive ;
- case 'coming-soon':
- return Coming Soon ;
- default:
- return null;
+ case 'active':
+ return (
+
+ Active
+
+ );
+ case 'inactive':
+ return (
+
+ Inactive
+
+ );
+ case 'coming-soon':
+ return (
+
+ Coming Soon
+
+ );
+ case 'maintenance':
+ return (
+
+ En mantenimiento
+
+ );
+ default:
+ return null;
}
};
const getPriceBadge = (price: string) => {
- return price === 'free'
- ? Free
- : Premium ;
+ return price === 'free' ? (
+
+ Free
+
+ ) : (
+
+ Premium
+
+ );
};
if (loading) {
return (
-
-
-
-
Loading Plugin Store...
+
+
+
+
Loading Plugin Store...
);
}
return (
-
+
{/* Header */}
-
-
-
-
-
Plugin Store
-
- Browse the ever-growing collection of business modules on ZatoBox
+
+
+
+
+
+ Plugin Store
+
+
+ Browse the ever-growing collection of business modules on
+ ZatoBox
-
-
+
{/* Search and Filters */}
-
-
+
+
{/* Search Section */}
-
-
-
+
{/* Category Tabs with Horizontal Scroll */}
-
-
+
+
{/* Left Arrow */}
{canScrollLeft && (
-
+
)}
@@ -366,9 +456,9 @@ const PluginStorePage: React.FC = () => {
{canScrollRight && (
-
+
)}
@@ -376,7 +466,7 @@ const PluginStorePage: React.FC = () => {
{
: 'bg-gray-100 text-text-secondary hover:bg-gray-200 hover:shadow-sm'
}`}
>
- {category.icon}
+ {category.icon}
{category.name}
))}
{/* Gradient Overlay for Right Edge */}
-
+
@@ -407,62 +497,82 @@ const PluginStorePage: React.FC = () => {
{/* Featured Section */}
{selectedCategory === 'all' && (
-
-
🔥 MOST INSTALLS By popular demand
-
+
+
+ 🔥 MOST INSTALLS By popular demand
+
+
{filteredPlugins.slice(0, 3).map((plugin) => (
-
+
{plugin.screenshot && (
-
+
)}
-
-
-
-
{plugin.icon}
+
+
+
+
{plugin.icon}
-
{plugin.name}
-
{plugin.description}
+
+ {plugin.name}
+
+
+ {plugin.description}
+
-
-
+
+
{getStatusBadge(plugin.status)}
{getPriceBadge(plugin.price)}
-
-
⭐
-
{plugin.rating}
-
({plugin.installs})
+
+ ⭐
+
+ {plugin.rating}
+
+
+ ({plugin.installs})
+
{plugin.status === 'active' ? (
handlePluginToggle(plugin.id)}
- className="w-full bg-red-100 text-red-800 py-2 px-4 rounded-lg font-medium hover:bg-red-200 transition-colors"
+ className='w-full px-4 py-2 font-medium text-red-800 transition-colors bg-red-100 rounded-lg hover:bg-red-200'
>
Deactivate
) : plugin.status === 'inactive' ? (
handlePluginToggle(plugin.id)}
- className="w-full bg-green-100 text-green-800 py-2 px-4 rounded-lg font-medium hover:bg-green-200 transition-colors"
+ className='w-full px-4 py-2 font-medium text-green-800 transition-colors bg-green-100 rounded-lg hover:bg-green-200'
>
Activate
- ) : (
+ ) : plugin.status === 'coming-soon' ? (
Coming Soon
+ ) : (
+
+ En mantenimiento
+
)}
@@ -472,42 +582,56 @@ const PluginStorePage: React.FC = () => {
)}
{/* All Plugins Grid */}
-
+
{filteredPlugins.map((plugin) => (
-
-
-
-
-
{plugin.icon}
+
+
+
+
+
{plugin.icon}
-
{plugin.name}
-
{plugin.description}
+
+ {plugin.name}
+
+
+ {plugin.description}
+
-
-
+
+
{getStatusBadge(plugin.status)}
{getPriceBadge(plugin.price)}
-
-
⭐
-
{plugin.rating}
-
({plugin.installs})
+
+ ⭐
+
+ {plugin.rating}
+
+
+ ({plugin.installs})
+
{/* Features */}
-
-
+
+
{plugin.features.slice(0, 2).map((feature, index) => (
-
+
{feature}
))}
{plugin.features.length > 2 && (
-
+
+{plugin.features.length - 2} more
)}
@@ -515,26 +639,19 @@ const PluginStorePage: React.FC = () => {
{/* Action Button */}
- {plugin.status === 'active' ? (
+ {plugin.status === 'coming-soon' ? (
handlePluginToggle(plugin.id)}
- className="w-full bg-red-100 text-red-800 py-2 px-4 rounded-lg font-medium hover:bg-red-200 transition-colors"
- >
- Deactivate
-
- ) : plugin.status === 'inactive' ? (
-
handlePluginToggle(plugin.id)}
- className="w-full bg-green-100 text-green-800 py-2 px-4 rounded-lg font-medium hover:bg-green-200 transition-colors"
+ disabled
+ className='w-full px-4 py-2 font-medium text-gray-400 bg-gray-100 rounded-lg cursor-not-allowed'
>
- Activate
+ Coming Soon
) : (
- Coming Soon
+ En mantenimiento
)}
@@ -544,39 +661,47 @@ const PluginStorePage: React.FC = () => {
{/* Empty State */}
{filteredPlugins.length === 0 && (
-
-
🔍
-
No plugins found
-
Try adjusting your search or filter criteria
+
+
🔍
+
+ No plugins found
+
+
+ Try adjusting your search or filter criteria
+
)}
{/* Plugin Change Notification */}
{showNotification && (
-
-
-
-
{notificationMessage}
+
+
+
+
{notificationMessage}
)}
{/* Example usage buttons for navigate and showPluginNotification */}
-
+
navigate('/')}
- className="bg-blue-100 text-blue-800 py-2 px-4 rounded-lg font-medium hover:bg-blue-200 transition-colors"
+ className='px-4 py-2 font-medium text-blue-800 transition-colors bg-blue-100 rounded-lg hover:bg-blue-200'
>
- Go Home (navigate)
+ Go Home (navigate)
showPluginNotification('This is a test notification!', 'info')}
- className="bg-green-100 text-green-800 py-2 px-4 rounded-lg font-medium hover:bg-green-200 transition-colors"
+ onClick={() =>
+ showPluginNotification('This is a test notification!', 'info')
+ }
+ className='px-4 py-2 font-medium text-green-800 transition-colors bg-green-100 rounded-lg hover:bg-green-200'
>
- Show Notification
+ Show Notification
diff --git a/frontend/src/components/ProductCard.tsx b/frontend/src/components/ProductCard.tsx
index d4b5569c..998a65ca 100644
--- a/frontend/src/components/ProductCard.tsx
+++ b/frontend/src/components/ProductCard.tsx
@@ -1,18 +1,6 @@
import React from 'react';
import { Package } from 'lucide-react';
-
-interface Product {
- id: number;
- name: string;
- description?: string;
- sku?: string;
- category: string;
- stock: number;
- price: number;
- status: 'active' | 'inactive';
- image?: string;
- images?: string[];
-}
+import type { Product } from '../services/api';
interface ProductCardProps {
product: Product;
@@ -24,65 +12,65 @@ const ProductCard: React.FC
= ({ product, onClick }) => {
onClick(product);
};
- // Obtener la imagen a mostrar (prioridad: image > images[0] > placeholder)
const getImageUrl = () => {
- if (product.image) {
- // Si la imagen ya tiene http, usarla tal como está
+ if (product.image && typeof product.image === 'string') {
if (product.image.startsWith('http')) {
return product.image;
}
- // Si es una URL relativa, construir la URL completa
return `http://localhost:4444${product.image}`;
}
- if (product.images && product.images.length > 0) {
+ if (Array.isArray(product.images) && product.images.length > 0) {
const imageUrl = product.images[0];
- // Si la imagen ya tiene http, usarla tal como está
- if (imageUrl.startsWith('http')) {
+ if (typeof imageUrl === 'string' && imageUrl.startsWith('http')) {
return imageUrl;
}
- // Si es una URL relativa, construir la URL completa
return `http://localhost:4444${imageUrl}`;
}
return null;
};
const imageUrl = getImageUrl();
+ const unitLabel = (product as any).unit_name ?? 'per unit';
return (
{/* Stock Badge */}
-
-
10
- ? 'bg-success-100 text-success-800 group-hover:bg-success-200'
- : product.stock > 0
- ? 'bg-warning-100 text-warning-800 group-hover:bg-warning-200'
- : 'bg-error-100 text-error-800 group-hover:bg-error-200'
- }`}>
-
+
10
- ? 'bg-success-500'
+ ? 'bg-success-100 text-success-800 group-hover:bg-success-200'
: product.stock > 0
+ ? 'bg-warning-100 text-warning-800 group-hover:bg-warning-200'
+ : 'bg-error-100 text-error-800 group-hover:bg-error-200'
+ }`}
+ >
+ 10
+ ? 'bg-success-500'
+ : product.stock > 0
? 'bg-warning-500'
: 'bg-error-500'
- }`}>
+ }`}
+ >
{product.stock} in stock
{/* Product Image */}
-
+
{imageUrl ? (
{
// Si la imagen falla, mostrar el placeholder
const target = e.target as HTMLImageElement;
@@ -93,62 +81,66 @@ const ProductCard: React.FC
= ({ product, onClick }) => {
) : null}
{/* Fallback placeholder */}
-
-
-
+
{/* Overlay on hover */}
-
+
{/* Product Info */}
-
+
{/* Category */}
-
-
- {product.category}
+
+
+ {product.category ?? ''}
{product.sku && (
-
+
{product.sku}
)}
{/* Product Name */}
-
+
{product.name}
{/* Description */}
- {product.description && (
-
+ {product.description ? (
+
{product.description}
- )}
+ ) : null}
{/* Price and Action */}
-
-
-
+
+
+
${product.price.toFixed(2)}
-
- per unit
+
+ {unitLabel}
{/* Add to Cart Button */}
-
+
Add
{/* Click indicator */}
-
+
);
};
diff --git a/frontend/src/components/ProfilePage.tsx b/frontend/src/components/ProfilePage.tsx
index 7c19719e..16af1409 100644
--- a/frontend/src/components/ProfilePage.tsx
+++ b/frontend/src/components/ProfilePage.tsx
@@ -1,23 +1,24 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
+import { authAPI } from '../services/api';
import {
- ArrowLeft,
- User,
- Shield,
- Eye,
- EyeOff,
- Globe,
- Bell,
- CreditCard,
- Download,
- HelpCircle,
- MessageSquare,
- Activity,
- Save,
- Camera,
- Smartphone,
- Monitor,
- X,
+ ArrowLeft,
+ User,
+ Shield,
+ Eye,
+ EyeOff,
+ Globe,
+ Bell,
+ CreditCard,
+ Download,
+ HelpCircle,
+ MessageSquare,
+ Activity,
+ Save,
+ Camera,
+ Smartphone,
+ Monitor,
+ X,
} from 'lucide-react';
interface Session {
@@ -52,14 +53,16 @@ const ProfilePage: React.FC = () => {
const [showAddCardForm, setShowAddCardForm] = useState(false);
const [editingCard, setEditingCard] = useState(null);
const [showPassword, setShowPassword] = useState({
- current: false,
- new: false,
- confirm: false,
- });
+ current: false,
+ new: false,
+ confirm: false,
+ });
+ // @ts-ignore
+ const [loadingProfile, setLoadingProfile] = useState(false);
// Profile data state
const [profileData, setProfileData] = useState({
- name: 'John Doe',
+ full_name: 'John Doe',
email: 'john.doe@company.com',
phone: '+1 (555) 123-4567',
address: '123 Main St, City, Country',
@@ -322,23 +325,23 @@ const ProfilePage: React.FC = () => {
};
const renderProfileHeader = () => (
-
-
+
+
{/* Avatar */}
-
{/* User Info */}
-
{profileData.name}
+
{profileData.full_name}
{profileData.email}
-
+
{profileData.role}
@@ -353,21 +356,21 @@ const ProfilePage: React.FC = () => {
const renderPersonalData = () => (
-
+
-
+
Full Name
handleInputChange('name', e.target.value)}
- className="w-full p-3 border border-divider rounded-lg focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary"
+ value={profileData.full_name}
+ onChange={(e) => handleInputChange('full_name', e.target.value)}
+ className="w-full p-3 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary"
/>
-
+
Email
@@ -375,7 +378,7 @@ const ProfilePage: React.FC = () => {
type="email"
value={profileData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
- className="flex-1 p-3 border border-divider rounded-lg focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary"
+ className="flex-1 p-3 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary"
/>
{
-
+
Phone
@@ -396,7 +399,7 @@ const ProfilePage: React.FC = () => {
type="tel"
value={profileData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
- className="flex-1 p-3 border border-divider rounded-lg focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary"
+ className="flex-1 p-3 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary"
/>
{
-
+
Full Address
handleInputChange('address', e.target.value)}
- className="w-full p-3 border border-divider rounded-lg focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary"
+ className="w-full p-3 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary"
/>
@@ -427,10 +430,10 @@ const ProfilePage: React.FC = () => {
{/* Password Change */}
-
Change Password
+
Change Password
-
+
Current Password
@@ -438,12 +441,12 @@ const ProfilePage: React.FC = () => {
type={showPassword.current ? 'text' : 'password'}
value={passwordData.current}
onChange={(e) => handlePasswordChange('current', e.target.value)}
- className="w-full p-3 pr-12 border border-divider rounded-lg focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary"
+ className="w-full p-3 pr-12 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary"
/>
togglePasswordVisibility('current')}
- className="absolute right-3 top-1/2 transform -translate-y-1/2 text-text-secondary hover:text-text-primary"
+ className="absolute transform -translate-y-1/2 right-3 top-1/2 text-text-secondary hover:text-text-primary"
>
{showPassword.current ? : }
@@ -451,7 +454,7 @@ const ProfilePage: React.FC = () => {
-
+
New Password
@@ -459,12 +462,12 @@ const ProfilePage: React.FC = () => {
type={showPassword.new ? 'text' : 'password'}
value={passwordData.new}
onChange={(e) => handlePasswordChange('new', e.target.value)}
- className="w-full p-3 pr-12 border border-divider rounded-lg focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary"
+ className="w-full p-3 pr-12 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary"
/>
togglePasswordVisibility('new')}
- className="absolute right-3 top-1/2 transform -translate-y-1/2 text-text-secondary hover:text-text-primary"
+ className="absolute transform -translate-y-1/2 right-3 top-1/2 text-text-secondary hover:text-text-primary"
>
{showPassword.new ? : }
@@ -472,7 +475,7 @@ const ProfilePage: React.FC = () => {
-
+
Confirm New Password
@@ -480,12 +483,12 @@ const ProfilePage: React.FC = () => {
type={showPassword.confirm ? 'text' : 'password'}
value={passwordData.confirm}
onChange={(e) => handlePasswordChange('confirm', e.target.value)}
- className="w-full p-3 pr-12 border border-divider rounded-lg focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary"
+ className="w-full p-3 pr-12 border rounded-lg border-divider focus:ring-2 focus:ring-complement focus:border-transparent bg-bg-surface text-text-primary"
/>
togglePasswordVisibility('confirm')}
- className="absolute right-3 top-1/2 transform -translate-y-1/2 text-text-secondary hover:text-text-primary"
+ className="absolute transform -translate-y-1/2 right-3 top-1/2 text-text-secondary hover:text-text-primary"
>
{showPassword.confirm ? : }
@@ -495,7 +498,7 @@ const ProfilePage: React.FC = () => {
{/* Two Factor Authentication */}
-
+
Two-Factor Authentication
@@ -514,13 +517,13 @@ const ProfilePage: React.FC = () => {
{/* Active Sessions */}
-
-
Active Sessions
+
+
Active Sessions
{sessions.map((session) => (
-
+
-