Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
443c920
feat: update product model and repository to support new category and…
esspindola Aug 19, 2025
2a980be
feat: Enhance product management with dynamic product type and unit d…
esspindola Aug 19, 2025
4ffdaa3
Refactor SettingsPage and SideMenu components for improved styling an…
esspindola Aug 20, 2025
10b7f5e
Merge pull request #19 from ZatoBox/fix/new-product
esspindola Aug 20, 2025
c49c33b
fix: remove default frontend URL fallback in RegisterPage component
esspindola Aug 20, 2025
7ace5b0
Merge pull request #20 from ZatoBox/fix/new-product
esspindola Aug 20, 2025
afdad6d
fix: update favicon link in index.html and add vercel.json for routing
esspindola Aug 20, 2025
9b1715f
Merge pull request #21 from ZatoBox/hotfix/vercel-routes
esspindola Aug 20, 2025
a1ee536
feat: add user inventory retrieval and related API integration
esspindola Aug 20, 2025
a14a8ad
feat: update POS integration status message and add maintenance mode …
esspindola Aug 20, 2025
2fe2c20
feat: update SalesDrawer component to disable payment button and indi…
esspindola Aug 20, 2025
f6db328
feat: add email existence check during registration and login processes
esspindola Aug 20, 2025
7b57a26
Merge pull request #23 from ZatoBox/fix/hover-and-product-owner
esspindola Aug 20, 2025
32d0393
Refactor code structure for improved readability and maintainability
esspindola Aug 20, 2025
1e12249
Merge pull request #24 from ZatoBox/fix/landing-page-as-home
esspindola Aug 20, 2025
40afd8a
feat: enhance login and registration pages with social login options
esspindola Aug 20, 2025
327d950
Merge pull request #25 from ZatoBox/fix/home-content
esspindola Aug 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions backend/zato-csm-backend/config/init_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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(
Expand Down
77 changes: 50 additions & 27 deletions backend/zato-csm-backend/models/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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]
Expand All @@ -82,4 +106,3 @@ class ProductResponse(BaseModel):

class Config:
from_attributes = True

77 changes: 47 additions & 30 deletions backend/zato-csm-backend/repositories/product_repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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()
8 changes: 8 additions & 0 deletions backend/zato-csm-backend/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
8 changes: 8 additions & 0 deletions backend/zato-csm-backend/routes/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 0 additions & 4 deletions backend/zato-csm-backend/routes/products.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
51 changes: 30 additions & 21 deletions backend/zato-csm-backend/services/inventory_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
]
Expand All @@ -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,
Expand Down
Loading