diff --git a/backend/app/api/clients/endpoints.py b/backend/app/api/clients/endpoints.py deleted file mode 100644 index 6b29bab..0000000 --- a/backend/app/api/clients/endpoints.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -The Clients endpoint -""" -from fastapi import APIRouter, HTTPException -from sqlmodel import select -from app.api.deps import SessionDep, exists -from app.schemas.models import Client -from app.schemas.schemas import ClientCreate, ClientUpdate -from pydantic import ValidationError -from typing import Sequence, List, Optional - -router = APIRouter(prefix="/clients", tags=["clients"]) - - -@router.get("/", response_model=List[Client]) -def fetch_clients(session: SessionDep) -> Sequence[Client]: - """Fetch client list - """ - clients = session.exec(select(Client)).all() - return clients - - -@router.post("/", response_model=ClientCreate) -def create_client(client_data: ClientCreate, session: SessionDep) -> Client: - """Create a client - """ - existing = exists(session, Client, tin_number=client_data.tin_number) - if existing: - raise HTTPException(status_code=400, detail="Client with this tin number already exists") - - try: - client = Client.model_validate(client_data) - except ValidationError as e: - raise HTTPException(status_code=400, detail=e.errors()) - - session.add(client) - session.commit() - session.refresh(client) - return client - - -@router.get("/{client_id}", response_model=Client) -def get_client(client_id: int, session: SessionDep) -> Optional[Client]: - """Returns a client by its ID - """ - stmt = select(Client).where(Client.id == client_id) - result: Optional[Client] = session.exec(stmt).first() - - if not result: - raise HTTPException(status_code=404, detail="Client not found") - - return result - - -@router.patch("/{client_id}", response_model=ClientCreate) -def update_client( - client_id: int, - client_data: ClientUpdate, - session: SessionDep) -> Optional[Client]: - """Updates a client using its ID - """ - client = session.get(Client, client_id) - if not client: - raise HTTPException(status_code=404, detail="Client not found") - - update_fields = client_data.model_dump(exclude_unset=True) - - for key, value in update_fields.items(): - setattr(client, key, value) - - session.add(client) - session.commit() - session.refresh(client) - return client - - -@router.delete("/{client_id}", status_code=204) -def delete_client(client_id: int, session: SessionDep): - """Deletes a client - """ - client = session.get(Client, client_id) - - if not client: - raise HTTPException(status_code=404, detail="Client not found") - - session.delete(client) - session.commit() diff --git a/backend/app/api/clients/__init__.py b/backend/app/api/endpoints/__init__.py similarity index 100% rename from backend/app/api/clients/__init__.py rename to backend/app/api/endpoints/__init__.py diff --git a/backend/app/api/products/__init__.py b/backend/app/api/endpoints/auth.py similarity index 100% rename from backend/app/api/products/__init__.py rename to backend/app/api/endpoints/auth.py diff --git a/backend/app/api/products/endpoints.py b/backend/app/api/products/endpoints.py deleted file mode 100644 index 18ace6c..0000000 --- a/backend/app/api/products/endpoints.py +++ /dev/null @@ -1,99 +0,0 @@ -""" -The products endpoint -""" -from fastapi import APIRouter, HTTPException -from sqlmodel import select -from app.api.deps import SessionDep, exists -from app.schemas.models import Product -from app.schemas.schemas import ProductCreate, ProductUpdate -from pydantic import ValidationError -from typing import Sequence, List, Optional - - -router = APIRouter(prefix="/products", tags=["products"]) - - -@router.get("/", response_model=List[Product]) -def fetch_products(session: SessionDep) -> Sequence[Product]: - """Fetch product list - """ - products = session.exec(select(Product)).all() - return products - - -@router.post("/", response_model=ProductCreate) -def create_product(product_data: ProductCreate, session: SessionDep) -> Product: - """Create a product - """ - name_exists = exists(session, Product, product_name=product_data.product_name) - if name_exists: - raise HTTPException( - status_code=400, - detail="Product with this product_name exists" - ) - - existing = exists(session, Product, product_code=product_data.product_code) - if existing: - raise HTTPException( - status_code=400, - detail="Product with this product_code exists" - ) - - try: - product = Product.model_validate(product_data) - except ValidationError as e: - raise HTTPException(status_code=400, detail=e.errors()) - - session.add(product) - session.commit() - session.refresh(product) - return product - - -@router.get("/{product_id}", response_model=Product) -def get_product(product_id: int, session: SessionDep) -> Optional[Product]: - """Returns a product by its id - """ - stmt = select(Product).where(Product.id == product_id) - result: Optional[Product] = session.exec(stmt).first() - - if not result: - raise HTTPException(status_code=404, detail="Product not found") - - return result - - -@router.patch("/{product_id}",response_model=ProductUpdate) -def update_product( - product_id: int, - product_data: ProductUpdate, - session: SessionDep -) -> Optional[Product]: - """Updates a product - """ - product = session.get(Product, product_id) - if not product: - raise HTTPException(status_code=404, detail="Product not found") - - updated_fields = product_data.model_dump(exclude_unset=True) - - for key, value in updated_fields.items(): - setattr(product, key, value) - - session.add(product) - session.commit() - session.refresh(product) - return product - - -@router.delete("/{product_id}", status_code=204) -def delete_product(product_id: int, session: SessionDep): - """Deletes a product - """ - product = session.get(Product, product_id) - - if not product: - raise HTTPException(status_code=404, detail="Product not found") - - session.delete(product) - session.commit() diff --git a/backend/app/api/suppliers/__init__.py b/backend/app/api/suppliers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/app/api/suppliers/endpoints.py b/backend/app/api/suppliers/endpoints.py deleted file mode 100644 index cef2aac..0000000 --- a/backend/app/api/suppliers/endpoints.py +++ /dev/null @@ -1,88 +0,0 @@ -"""The suppliers endpoint -""" -from fastapi import APIRouter, HTTPException -from sqlmodel import select -from app.api.deps import SessionDep, exists -from app.schemas.models import Supplier -from app.schemas.schemas import SupplierCreate, SupplierUpdate -from pydantic import ValidationError -from typing import Sequence, List, Optional - - -router = APIRouter(prefix="/suppliers", tags=["suppliers"]) - - -@router.get("/", response_model=List[Supplier]) -def fetch_suppliers(session: SessionDep) -> Sequence[Supplier]: - """Fetch supplier list - """ - suppliers = session.exec(select(Supplier)).all() - return suppliers - - -@router.post("/", response_model=SupplierCreate) -def create_supplier(supplier_data: SupplierCreate, session: SessionDep) -> Supplier: - """Create a supplier - """ - existing = exists(session, Supplier, tin_number=supplier_data.tin_number) - if existing: - raise HTTPException(status_code=400, detail="Supplier with this tin_number already exists") - - try: - supplier = Supplier.model_validate(supplier_data) - except ValidationError as e: - raise HTTPException(status_code=400, detail=e.errors()) - - session.add(supplier) - session.commit() - session.refresh(supplier) - return supplier - - -@router.get("/{supplier_id}", response_model=Supplier) -def get_supplier(supplier_id: int, session: SessionDep) -> Optional[Supplier]: - """Returns a supplier by its ID - """ - stmt = select(Supplier).where(Supplier.id == supplier_id) - result: Optional[Supplier] = session.exec(stmt).first() - - if not result: - raise HTTPException(status_code=404, detail="Supplier not found") - - return result - - -@router.patch("/{supplier_id}", response_model=SupplierUpdate) -def update_supplier( - supplier_id: int, - supplier_data: SupplierUpdate, - session: SessionDep -) -> Optional[Supplier]: - """Updates a supplier's details - """ - supplier = session.get(Supplier, supplier_id) - if not supplier: - raise HTTPException(status_code=404, detail="Supplier not found") - - updated_fields = supplier_data.model_dump(exclude_unset=True) - - for key, value in updated_fields.items(): - setattr(supplier, key, value) - - session.add(supplier) - session.commit() - session.refresh(supplier) - return supplier - - -@router.delete("/{supplier_id}", status_code=204) -def delete_supplier(supplier_id: int, session: SessionDep): - """Deletes a supplier - """ - supplier = session.get(Supplier, supplier_id) - - if not supplier: - raise HTTPException(status_code=404, detail="Supplier not found") - - session.delete(supplier) - session.commit() diff --git a/backend/app/main.py b/backend/app/main.py index 1d205d5..f5bc501 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -7,9 +7,9 @@ NOTE: from app.core.config import settings from typing import Union from fastapi import FastAPI -from app.api.clients.endpoints import router as clients_router -from app.api.suppliers.endpoints import router as supplier_router -from app.api.products.endpoints import router as product_router +from backend.app.api.endpoints.clients import router as clients_router +from backend.app.api.endpoints.suppliers import router as supplier_router +from backend.app.api.endpoints.products import router as product_router app = FastAPI( diff --git a/backend/app/schemas/base.py b/backend/app/schemas/base.py index 6ae9c95..55121bf 100644 --- a/backend/app/schemas/base.py +++ b/backend/app/schemas/base.py @@ -1,26 +1,62 @@ from sqlmodel import SQLModel from enum import Enum - class UserRole(str, Enum): + """User roles for system access. + + Attributes: + ADMIN (str): Administrator with full access. + WRITE (str): User with write permissions. + READ_ONLY (str): User with read-only permissions. + """ ADMIN = "admin" WRITE = "write" READ_ONLY = "read_only" - class TransactionType(str, Enum): + """Types of financial transactions. + + Attributes: + SALE (str): Sale transaction. + PURCHASE (str): Purchase transaction. + CREDIT (str): Credit transaction. + """ SALE = "sell" PURCHASE = "buy" CREDIT = "credit" - class TransactionStatus(str, Enum): + """Possible statuses of a transaction. + + Attributes: + UNPAID (str): Transaction not paid. + PARTIALLY_PAID (str): Transaction partially paid. + PAID (str): Transaction fully paid. + CANCELLED (str): Transaction cancelled. + """ UNPAID = "unpaid" PARTIALLY_PAID = "partially_paid" PAID = "paid" CANCELLED = 'cancelled' - class PartnerType(str, Enum): + """Types of business partners. + + Attributes: + CLIENT (str): Client partner. + SUPPLIER (str): Supplier partner. + """ CLIENT = "client" SUPPLIER = "supplier" + +class PaymentMethod(str, Enum): + """Payment methods available. + + Attributes: + MOMO (str): Mobile money. + BANK (str): Bank transfer. + CASH (str): Cash payment. + """ + MOMO = "momo" + BANK = "bank" + CASH = "cash" diff --git a/backend/app/schemas/models.py b/backend/app/schemas/models.py index 729131c..f00044e 100644 --- a/backend/app/schemas/models.py +++ b/backend/app/schemas/models.py @@ -1,56 +1,81 @@ """ -This module contains Pydantic/Database Models that map database tables validate -and serialize api responses. +Models module. -If the logic is identical -> SQLModel is used to do both. -Otherwise pydantic - for api responses -And SQLAlchemy is used for db data validation. +This module contains Pydantic and SQLModel classes for database table mapping, +API request/response validation, and serialization. -TODO: -Mapping & validation for: -- Clients, Suppliers, Products, payments - -Done: -* Table mappings +The models include: +- User +- Partner +- Product +- Transaction and its details +- Payment +- Credit account +- Inventory """ + from sqlmodel import SQLModel, Field, UniqueConstraint -from datetime import datetime +from datetime import datetime, date from sqlalchemy import Column, DateTime, func, Enum as SQLEnum from enum import Enum from typing import Optional -from base import UserRole, PartnerType, TransactionType, TransactionStatus +from base import UserRole, PartnerType, TransactionType, TransactionStatus, PaymentMethod class User(SQLModel, table=True): + """User table mapping, API request/response validation, and serialization. + + Attributes: + id (int, optional): Primary key. + username (str): Unique user name (max 100 chars). + role (UserRole): User role (default READ_ONLY). + password_hash (str): Hashed password. """ - User table mapping, api response validation and serialisation - """ + id: Optional[int] = Field(default=None, primary_key=True) username: str = Field(nullable=False,unique=True, max_length=100) - role: UserRole = Field(nullable=True, default=PartnerType.CLIENT) + role: UserRole = Field(nullable=False, max_length= 10, default=UserRole.READ_ONLY) password_hash: str = Field(nullable=False) + class Partner(SQLModel, table=True): - """Clients table mapping, api response validation and serialisation""" + """Partner (client or supplier) mapping, API request/response validation, and serialization. + + Attributes: + id (int, optional): Primary key. + tin_number (int): Tax identification number. + names (str): Full name. + type (PartnerType): Partner type (CLIENT or SUPPLIER). + phone_number (str, optional): Phone number. + """ + id: Optional[int] = Field(default=None, primary_key=True) tin_number: int = Field(nullable=False, unique=True) names: str = Field(max_length=100, nullable=False) - type: PartnerType = Field(nullable=False, default=PartnerType.CLIENT) + type: PartnerType = Field(nullable=False, max_length=10, default=PartnerType.CLIENT) phone_number: str = Field(max_length=10, nullable=True) class Product(SQLModel, table=True): - """Products table mapping, api response validation and serialisation + """Products table mapping, API request/response validation, and serialization. - NOTE: Every time a product's purchase price changes, it should be updated - here as well + Every time a product's purchase price changes, update here. + selling_price is referential: defaults but can be overridden. + + Attributes: + id (int, optional): Primary key. + product_code (str): Unique product code (max 10 chars). + product_name (str): Unique product name (max 20 chars). + purchase_price (int): Last purchase price. + selling_price (int): Reference selling price. + date_modified (datetime): Last modified timestamp. """ - __table_args__ = (UniqueConstraint("product_code")) id: Optional[int] = Field(default=None, primary_key=True) product_code: str = Field(max_length=10, unique=True, nullable=False) product_name: str = Field(max_length=20, nullable=False, unique=True) purchase_price: int = Field(nullable=False) + selling_price: int = Field(nullable=False) date_modified: datetime = Field( default=None, sa_column=Column(DateTime(timezone=True), @@ -60,18 +85,44 @@ class Product(SQLModel, table=True): class Transaction(SQLModel, table=True): - """ - Transaction table mapping, api response validation and serialisation + """Transaction table mapping, API request/response validation, and serialization. - Include both business events to/from suppliers and to/from clients + Includes both business events to/from suppliers and clients. + + Attributes: + id (int, optional): Primary key. + partner_id (int): Related partner ID. + transcation_type (TransactionType): Type of transaction. + transaction_status (TransactionStatus): Current status. + total_amount (int): Total transaction amount. + created_by (int): User ID who created. + updated_by (int): User ID who last updated. + created_on (datetime): Creation timestamp. + updated_on (datetime): Last update timestamp. """ + __tablename__: str = "transactions" id: Optional[int] = Field(default=None, primary_key=True) partner_id: Optional[int] = Field(nullable=False, foreign_key="partner.id") transcation_type: TransactionType = Field( - sa_column=Column(SQLEnum(TransactionType), nullable=False) + sa_column=Column( + SQLEnum(TransactionType), + nullable=False, + default=TransactionType.SALE + ) ) - transaction_status: TransactionStatus + transaction_status: TransactionStatus = Field( + sa_column=Column( + SQLEnum(TransactionStatus), + nullable=False, + default=TransactionStatus.UNPAID + ) + ) + total_amount: int = Field(nullable=False, default=0) + + created_by: int = Field(nullable=False, foreign_key="user.id") + updated_by: int = Field(nullable=False, foreign_key="user.id") + created_on: datetime = Field( default=None, sa_column=Column(DateTime(timezone=True), server_default=func.now()) @@ -86,32 +137,133 @@ class Transaction(SQLModel, table=True): ) -class Transaction_items(SQLModel, table=True): - """ - Transaction table mapping, api response validation and serialisation - Includes transactions details from transactions +class Transaction_details(SQLModel, table=True): + """Transaction details mapping, API request/response validation, and serialization. + + Attributes: + id (int, optional): Primary key. + partner_id (int): Related partner ID. + product_id (str): Product ID. + qty (int): Quantity. + selling_price (int): Unit price. + total_value (int): qty * selling_price. + created_by (int): User ID who created. + updated_by (int): User ID who last updated. + created_at (datetime): Creation timestamp. + updated_at (datetime): Last update timestamp. """ - -class Payment(SQLModel, table=True): - """ - - """ - -class Credit_accounts(SQLModel, table=True): - """Credit table mapping, api response validation and serialisation - - Include both credit from suppliers and to clients - """ - __tablename__: str = "credit_accounts" + __tablename__: str = "transaction_details" id: Optional[int] = Field(default=None, primary_key=True) - product_code: str = Field(nullable=False, foreign_key="product.product_code") - client_id: Optional[int] = Field(nullable=True, foreign_key="client.id") - supplier_id: Optional[int] = Field(nullable=True, foreign_key="supplier.id") + partner_id: int = Field(nullable=False, foreign_key="partner.id") + product_id: str = Field(nullable=False, foreign_key="product.id") qty: int = Field(nullable=False) - amount: int = Field(nullable=False) - date: datetime = Field( + selling_price: int = Field(nullable=False) + + # qty * selling_price + total_value: int = Field(nullable=False, default=0) # per items + + created_by: int = Field(nullable=False, foreign_key="user.id") + updated_by: int = Field(nullable=False, foreign_key="user.id") + created_at: datetime = Field( default=None, sa_column=Column(DateTime(timezone=True), server_default=func.now()) ) + updated_at: datetime = Field( + default=None, + sa_column=Column(DateTime(timezone=True), server_default=func.now()) + ) + + +class Payment(SQLModel, table=True): + """Payment table mapping, API request/response validation, and serialization. + + Attributes: + id (int, optional): Primary key. + transaction_id (int): Related transaction ID. + payment_method (PaymentMethod): Method of payment. + paid_amount (int): Amount paid. + payment_date (date): Date of payment. + created_by (int): User ID who created. + updated_by (int): User ID who last updated. + created_at (datetime): Creation timestamp. + updated_at (datetime): Last update timestamp. + """ + + id: Optional[int] = Field(default=None, primary_key=True) + transaction_id: int = Field(nullable=False, foreign_key="transactions.id") + payment_method: PaymentMethod = Field( + sa_column=Column( + SQLEnum(PaymentMethod), + nullable=False, + default=PaymentMethod.CASH + ) + ) + paid_amount: int = Field(nullable=False) + payment_date: date = Field(nullable=False) + created_by: int = Field(nullable=False, foreign_key="user.id") + updated_by: int = Field(nullable=False, foreign_key="user.id") + created_at: datetime = Field( + default=None, + sa_column=Column(DateTime(timezone=True), server_default=func.now()) + ) + updated_at: datetime = Field( + default=None, + sa_column=Column(DateTime(timezone=True), server_default=func.now()) + ) + + +class Credit(SQLModel, table=True): + """Credit account mapping, API request/response validation, and serialization. + + Includes both supplier and client credit events. + + Attributes: + id (int, optional): Primary key. + partner_id (int): Related partner ID. + transaction_id (int): Related transaction ID. + credit_amount (int): Credit amount. + credit_limit (int): Credit limit. + balance (int): Current balance. + created_by (int): User ID who created. + updated_by (int): User ID who last updated. + created_at (datetime): Creation timestamp. + updated_at (datetime): Last update timestamp. + """ + + __tablename__: str = "credit_accounts" + + id: Optional[int] = Field(default=None, primary_key=True) + partner_id: int = Field(nullable=False, unique=True, foreign_key="partner.id") + transaction_id: int = Field(nullable=False, foreign_key="transactions.id") + + credit_amount: int = Field(nullable=False) + credit_limit: int = Field(nullable=False) + balance: int = Field(nullable=False) + + created_by: int = Field(nullable=False, foreign_key="user.id") + updated_by: int = Field(nullable=False, foreign_key="user.id") + created_at: datetime = Field( + default=None, + sa_column=Column(DateTime(timezone=True), server_default=func.now()) + ) + updated_at: datetime = Field( + default=None, + sa_column=Column(DateTime(timezone=True), server_default=func.now()) + ) + + +class Inventory(SQLModel, table=True): + """Inventory mapping, API request/response validation, and serialization. + + Attributes: + id (int, optional): Primary key. + product_id (int): Related product ID. + total_qty (int): Total quantity in inventory. + """ + + id: Optional[int] = Field(default=None, primary_key=True) + + product_id: int = Field(nullable=False, unique=True, foreign_key="product.id") + total_qty: int = Field(nullable=False, default=0) diff --git a/backend/app/schemas/schemas.py b/backend/app/schemas/schemas.py index 4ac4d30..4c79ffb 100644 --- a/backend/app/schemas/schemas.py +++ b/backend/app/schemas/schemas.py @@ -1,3 +1,6 @@ +""" +Custom validation schema +""" from sqlmodel import SQLModel from typing import Optional diff --git a/backend/app/test.py b/backend/app/test.py deleted file mode 100644 index 7dc6798..0000000 --- a/backend/app/test.py +++ /dev/null @@ -1,37 +0,0 @@ -import pytest -from sqlmodel import SQLModel, Session, create_engine - -from app.core.config import settings -from app.api.deps import SessionDep -from app.schemas.schemas import ClientCreate -from app.api.clients.endpoints import create_client -from fastapi import HTTPException - - -# Fixture for a temporary in-memory database session -@pytest.fixture -def session(): - engine = create_engine(str(settings.database_uri), echo=False) - SQLModel.metadata.create_all(engine) - with Session(engine) as session: - yield session - - -def test_create_client_success(session: SessionDep): - client_data = ClientCreate(tin_number=781410046, names="Lin", phone_number="0781410046") - client = create_client(client_data, session) - - assert client.id is not None - assert client.names == "Lin" - assert client.tin_number == 781410046 - assert client.phone_number == "0781410046" - - -def test_create_client_duplicate_name(session): - client_data = ClientCreate(tin_number=781410045, names="John Doe", phone_number="0781410045") - - # Try to create another client with the same name - with pytest.raises(HTTPException) as exc_info: - create_client(client_data, session) - assert exc_info.value.status_code == 400 - assert "already exists" in exc_info.value.detail diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index f650315..0000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,27 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules - -# next.js -/.next/ -/out/ - -# production -/build - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# env files -.env* - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..66d6542 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,2 @@ +### Stack +HTMX + Tailwind + Alpine.js/Hyperscript diff --git a/frontend/app/globals.css b/frontend/app/globals.css deleted file mode 100644 index 02687bd..0000000 --- a/frontend/app/globals.css +++ /dev/null @@ -1,123 +0,0 @@ -@import 'tailwindcss'; -@import 'tw-animate-css'; - -@custom-variant dark (&:is(.dark *)); - -:root { - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --destructive-foreground: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --radius: 0.625rem; - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); -} - -.dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.145 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.145 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.985 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.396 0.141 25.723); - --destructive-foreground: oklch(0.637 0.237 25.331); - --border: oklch(0.269 0 0); - --input: oklch(0.269 0 0); - --ring: oklch(0.439 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(0.269 0 0); - --sidebar-ring: oklch(0.439 0 0); -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); -} - -@layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; - } -} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx deleted file mode 100644 index a262171..0000000 --- a/frontend/app/layout.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { Metadata } from 'next' -import { GeistSans } from 'geist/font/sans' -import { GeistMono } from 'geist/font/mono' -import './globals.css' - -export const metadata: Metadata = { - title: 'v0 App', - description: 'Created with v0', - generator: 'v0.app', -} - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode -}>) { - return ( - - - - - {children} - - ) -} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx deleted file mode 100644 index 64085a2..0000000 --- a/frontend/app/page.tsx +++ /dev/null @@ -1,561 +0,0 @@ -"use client" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { - DollarSign, - Package, - Users, - AlertTriangle, - ShoppingCart, - CreditCard, - BarChart3, - Plus, - Building2, -} from "lucide-react" -import { useBusinessData } from "@/hooks/use-business-data" -import { useProducts } from "@/hooks/use-products" -import { useClients } from "@/hooks/use-clients" -import { useSuppliers } from "@/hooks/use-suppliers" -import { ProductForm } from "@/components/products/product-form" -import { ProductsTable } from "@/components/products/products-table" -import { ClientForm } from "@/components/clients/client-form" -import { ClientsTable } from "@/components/clients/clients-table" -import { PaymentForm } from "@/components/clients/payment-form" -import { SupplierForm } from "@/components/suppliers/supplier-form" -import { SuppliersTable } from "@/components/suppliers/suppliers-table" -import { PurchaseForm } from "@/components/suppliers/purchase-form" -import { SupplierPaymentForm } from "@/components/suppliers/supplier-payment-form" -import { CreditOverview } from "@/components/credit/credit-overview" -import { TransactionHistory } from "@/components/credit/transaction-history" -import { useState } from "react" -import type { Product, Client, Supplier } from "@/types/business" - -export default function Dashboard() { - const { kpis, recentTransactions, topProducts, clientsWithCredit, suppliersWithCredit, loading } = useBusinessData() - const { - products, - loading: productsLoading, - addProduct, - updateProduct, - deleteProduct, - getLowStockProducts, - getOutOfStockProducts, - } = useProducts() - - const { - clients, - loading: clientsLoading, - addClient, - updateClient, - deleteClient, - addPayment, - getClientById, - getClientsWithOutstandingCredit, - getClientsOverCreditLimit, - getTotalOutstandingAmount, - } = useClients() - - const { - suppliers, - loading: suppliersLoading, - addSupplier, - updateSupplier, - deleteSupplier, - addPurchase, - recordPayment, - getSupplierById, - getSuppliersWithOutstandingBalance, - getTotalAmountOwed, - } = useSuppliers() - - const [showProductForm, setShowProductForm] = useState(false) - const [editingProduct, setEditingProduct] = useState() - const [showClientForm, setShowClientForm] = useState(false) - const [editingClient, setEditingClient] = useState() - const [showPaymentForm, setShowPaymentForm] = useState(false) - const [paymentClientId, setPaymentClientId] = useState("") - const [showSupplierForm, setShowSupplierForm] = useState(false) - const [editingSupplier, setEditingSupplier] = useState() - const [showPurchaseForm, setShowPurchaseForm] = useState(false) - const [purchaseSupplierId, setPurchaseSupplierId] = useState("") - const [showSupplierPaymentForm, setShowSupplierPaymentForm] = useState(false) - const [supplierPaymentId, setSupplierPaymentId] = useState("") - - const handleEditProduct = (product: Product) => { - setEditingProduct(product) - setShowProductForm(true) - } - - const handleProductSubmit = (productData: Omit) => { - if (editingProduct) { - updateProduct(editingProduct.id, productData) - setEditingProduct(undefined) - } else { - addProduct(productData) - } - } - - const handleCloseProductForm = () => { - setShowProductForm(false) - setEditingProduct(undefined) - } - - const handleEditClient = (client: Client) => { - setEditingClient(client) - setShowClientForm(true) - } - - const handleClientSubmit = (clientData: Omit) => { - if (editingClient) { - updateClient(editingClient.id, clientData) - setEditingClient(undefined) - } else { - addClient(clientData) - } - } - - const handleCloseClientForm = () => { - setShowClientForm(false) - setEditingClient(undefined) - } - - const handleAddPayment = (clientId: string) => { - setPaymentClientId(clientId) - setShowPaymentForm(true) - } - - const handlePaymentSubmit = (payment: { amount: number; notes: string; date: string }) => { - addPayment(paymentClientId, payment.amount, payment.notes, payment.date) - setShowPaymentForm(false) - setPaymentClientId("") - } - - const handleEditSupplier = (supplier: Supplier) => { - setEditingSupplier(supplier) - setShowSupplierForm(true) - } - - const handleSupplierSubmit = (supplierData: Omit) => { - if (editingSupplier) { - updateSupplier(editingSupplier.id, supplierData) - setEditingSupplier(undefined) - } else { - addSupplier(supplierData) - } - } - - const handleCloseSupplierForm = () => { - setShowSupplierForm(false) - setEditingSupplier(undefined) - } - - const handleAddPurchase = (supplierId: string) => { - setPurchaseSupplierId(supplierId) - setShowPurchaseForm(true) - } - - const handlePurchaseSubmit = (purchase: { - amount: number - description: string - date: string - invoiceNumber: string - }) => { - addPurchase(purchaseSupplierId, purchase.amount, purchase.description, purchase.date, purchase.invoiceNumber) - setShowPurchaseForm(false) - setPurchaseSupplierId("") - } - - const handleRecordPayment = (supplierId: string) => { - setSupplierPaymentId(supplierId) - setShowSupplierPaymentForm(true) - } - - const handleSupplierPaymentSubmit = (payment: { - amount: number - notes: string - date: string - paymentMethod: string - }) => { - recordPayment(supplierPaymentId, payment.amount, payment.notes, payment.date, payment.paymentMethod) - setShowSupplierPaymentForm(false) - setSupplierPaymentId("") - } - - if (loading || productsLoading || clientsLoading || suppliersLoading) { - return ( -
-
-
-

Loading dashboard...

-
-
- ) - } - - const lowStockProducts = getLowStockProducts() - const outOfStockProducts = getOutOfStockProducts() - const clientsWithOutstanding = getClientsWithOutstandingCredit() - const clientsOverLimit = getClientsOverCreditLimit() - const totalOutstanding = getTotalOutstandingAmount() - const suppliersWithBalance = getSuppliersWithOutstandingBalance() - const totalOwed = getTotalAmountOwed() - const paymentClient = getClientById(paymentClientId) - const purchaseSupplier = getSupplierById(purchaseSupplierId) - const paymentSupplier = getSupplierById(supplierPaymentId) - - return ( -
- {/* Header */} -
-
-
-
-

Wholesale Manager

-

Business Dashboard

-
- -
-
-
- -
- {/* KPI Cards */} -
- - - Total Revenue - - - -
${kpis.totalRevenue.toLocaleString()}
-

- +{kpis.revenueGrowth}% from last month -

-
-
- - - - Active Products - - - -
{products.length}
-

- {lowStockProducts.length} low stock, {outOfStockProducts.length} out of stock -

-
-
- - - - Active Clients - - - -
{clients.length}
-

${totalOutstanding.toLocaleString()} outstanding

-
-
- - - - Suppliers - - - -
{suppliers.length}
-

${totalOwed.toLocaleString()} owed

-
-
-
- - {/* Main Content Tabs */} - - - Overview - Products - Clients - Suppliers - Credit Tracking - - - -
- {/* Recent Transactions */} - - - - - Recent Transactions - - Latest business activities - - -
- {recentTransactions.slice(0, 5).map((transaction) => ( -
-
-

{transaction.client}

-

{transaction.product}

-
-
-

${transaction.amount}

- - {transaction.status} - -
-
- ))} -
-
-
- - {/* Top Products */} - - - - - Top Selling Products - - Best performers this month - - -
- {topProducts.map((product, index) => ( -
-
-
- #{index + 1} -
-
-

{product.name}

-

{product.category}

-
-
-
-

{product.unitsSold} sold

-

${product.revenue}

-
-
- ))} -
-
-
-
-
- - - - -
-
- - - Products Management - - Manage your product inventory and pricing -
- -
-
- - {/* Stock Alerts */} - {(lowStockProducts.length > 0 || outOfStockProducts.length > 0) && ( -
-
- -

Stock Alerts

-
- {outOfStockProducts.length > 0 && ( -

- {outOfStockProducts.length} products are out of stock -

- )} - {lowStockProducts.length > 0 && ( -

- {lowStockProducts.length} products are running low -

- )} -
- )} - - -
-
-
- - - - -
-
- - - Clients Management - - Manage your clients and track credit status -
- -
-
- - {/* Credit Alerts */} - {(clientsWithOutstanding.length > 0 || clientsOverLimit.length > 0) && ( -
-
- -

Credit Alerts

-
- {clientsOverLimit.length > 0 && ( -

- {clientsOverLimit.length} clients are over their credit limit -

- )} -

- Total outstanding: ${totalOutstanding.toLocaleString()} -

-
- )} - - -
-
-
- - - - -
-
- - - Suppliers Management - - Manage your suppliers and track payment obligations -
- -
-
- - {/* Payment Alerts */} - {suppliersWithBalance.length > 0 && ( -
-
- -

Payment Obligations

-
-

- {suppliersWithBalance.length} suppliers have outstanding balances -

-

- Total amount owed: ${totalOwed.toLocaleString()} -

-
- )} - - -
-
-
- - {/* Added new credit tracking tab */} - - - - -
-
- - {/* Product Form Dialog */} - - - {/* Client Form Dialog */} - - - {/* Payment Form Dialog */} - {paymentClient && ( - { - setShowPaymentForm(open) - if (!open) setPaymentClientId("") - }} - onSubmit={handlePaymentSubmit} - /> - )} - - {/* Supplier Form Dialog */} - - - {/* Purchase Form Dialog */} - {purchaseSupplier && ( - { - setShowPurchaseForm(open) - if (!open) setPurchaseSupplierId("") - }} - onSubmit={handlePurchaseSubmit} - /> - )} - - {/* Supplier Payment Form Dialog */} - {paymentSupplier && ( - { - setShowSupplierPaymentForm(open) - if (!open) setSupplierPaymentId("") - }} - onSubmit={handleSupplierPaymentSubmit} - /> - )} -
- ) -} diff --git a/frontend/components.json b/frontend/components.json deleted file mode 100644 index 335484f..0000000 --- a/frontend/components.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "", - "css": "app/globals.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "iconLibrary": "lucide" -} \ No newline at end of file diff --git a/frontend/components/clients/client-form.tsx b/frontend/components/clients/client-form.tsx deleted file mode 100644 index 9dcfb5c..0000000 --- a/frontend/components/clients/client-form.tsx +++ /dev/null @@ -1,200 +0,0 @@ -"use client" - -import type React from "react" - -import { useState } from "react" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Textarea } from "@/components/ui/textarea" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" -import type { Client } from "@/types/business" - -interface ClientFormProps { - client?: Client - open: boolean - onOpenChange: (open: boolean) => void - onSubmit: (client: Omit) => void -} - -const paymentTerms = ["Net 15", "Net 30", "Net 45", "Net 60", "COD", "Prepaid", "Custom"] - -export function ClientForm({ client, open, onOpenChange, onSubmit }: ClientFormProps) { - const [formData, setFormData] = useState({ - name: client?.name || "", - email: client?.email || "", - phone: client?.phone || "", - address: client?.address || "", - creditLimit: client?.creditLimit || 0, - paymentTerms: client?.paymentTerms || "Net 30", - notes: client?.notes || "", - contactPerson: client?.contactPerson || "", - businessType: client?.businessType || "", - }) - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() - onSubmit({ - ...formData, - outstandingAmount: client?.outstandingAmount || 0, - }) - onOpenChange(false) - // Reset form if it's a new client - if (!client) { - setFormData({ - name: "", - email: "", - phone: "", - address: "", - creditLimit: 0, - paymentTerms: "Net 30", - notes: "", - contactPerson: "", - businessType: "", - }) - } - } - - return ( - - - - {client ? "Edit Client" : "Add New Client"} - - {client ? "Update client information" : "Enter the details for the new client"} - - - -
-
-
- - setFormData({ ...formData, name: e.target.value })} - placeholder="Enter business name" - required - /> -
- -
- - setFormData({ ...formData, contactPerson: e.target.value })} - placeholder="Primary contact name" - /> -
-
- -
-
- - setFormData({ ...formData, email: e.target.value })} - placeholder="client@example.com" - /> -
- -
- - setFormData({ ...formData, phone: e.target.value })} - placeholder="(555) 123-4567" - /> -
-
- -
- -