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) && (
-
-
- {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 (
-
- )
-}
diff --git a/frontend/components/clients/clients-table.tsx b/frontend/components/clients/clients-table.tsx
deleted file mode 100644
index 68e4e2f..0000000
--- a/frontend/components/clients/clients-table.tsx
+++ /dev/null
@@ -1,201 +0,0 @@
-"use client"
-
-import { useState } from "react"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import { Badge } from "@/components/ui/badge"
-import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
-import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
-} from "@/components/ui/alert-dialog"
-import { Search, MoreHorizontal, Edit, Trash2, Mail, Phone, CreditCard } from "lucide-react"
-import type { Client } from "@/types/business"
-
-interface ClientsTableProps {
- clients: Client[]
- onEdit: (client: Client) => void
- onDelete: (clientId: string) => void
- onAddPayment: (clientId: string) => void
-}
-
-export function ClientsTable({ clients, onEdit, onDelete, onAddPayment }: ClientsTableProps) {
- const [searchTerm, setSearchTerm] = useState("")
- const [deleteClient, setDeleteClient] = useState(null)
-
- const filteredClients = clients.filter(
- (client) =>
- client.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
- client.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
- client.contactPerson.toLowerCase().includes(searchTerm.toLowerCase()) ||
- client.businessType.toLowerCase().includes(searchTerm.toLowerCase()),
- )
-
- const getCreditStatus = (outstandingAmount: number, creditLimit: number) => {
- if (outstandingAmount === 0) return { label: "Good Standing", variant: "default" as const, color: "text-green-600" }
- if (outstandingAmount >= creditLimit * 0.9)
- return { label: "Near Limit", variant: "secondary" as const, color: "text-yellow-600" }
- if (outstandingAmount > creditLimit)
- return { label: "Over Limit", variant: "destructive" as const, color: "text-red-600" }
- return { label: "Active Credit", variant: "secondary" as const, color: "text-blue-600" }
- }
-
- const handleDelete = (client: Client) => {
- onDelete(client.id)
- setDeleteClient(null)
- }
-
- return (
-
- {/* Search */}
-
-
- setSearchTerm(e.target.value)}
- className="pl-10"
- />
-
-
- {/* Clients Table */}
-
-
-
-
- Client
- Contact
- Business Type
- Payment Terms
- Credit Status
- Outstanding
-
-
-
-
- {filteredClients.length === 0 ? (
-
-
- {searchTerm ? "No clients found matching your search." : "No clients added yet."}
-
-
- ) : (
- filteredClients.map((client) => {
- const creditStatus = getCreditStatus(client.outstandingAmount, client.creditLimit)
- const creditUtilization =
- client.creditLimit > 0 ? ((client.outstandingAmount / client.creditLimit) * 100).toFixed(1) : "0"
-
- return (
-
-
-
-
{client.name}
- {client.contactPerson && (
-
{client.contactPerson}
- )}
-
-
-
-
- {client.email && (
-
-
- {client.email}
-
- )}
- {client.phone && (
-
- )}
-
-
-
- {client.businessType || "N/A"}
-
-
-
- {client.paymentTerms}
-
-
-
-
-
- {creditStatus.label}
-
- {client.creditLimit > 0 && (
-
{creditUtilization}% utilized
- )}
-
-
-
-
-
0 ? creditStatus.color : ""}`}>
- ${client.outstandingAmount.toFixed(2)}
-
-
/ ${client.creditLimit.toFixed(2)} limit
-
-
-
-
-
-
-
-
- onEdit(client)}>
-
- Edit
-
- onAddPayment(client.id)}>
-
- Add Payment
-
- setDeleteClient(client)} className="text-red-600">
-
- Delete
-
-
-
-
-
- )
- })
- )}
-
-
-
-
- {/* Delete Confirmation Dialog */}
-
setDeleteClient(null)}>
-
-
- Delete Client
-
- Are you sure you want to delete "{deleteClient?.name}"? This action cannot be undone and will remove all
- associated transaction history.
-
-
-
- Cancel
- deleteClient && handleDelete(deleteClient)}
- className="bg-red-600 hover:bg-red-700"
- >
- Delete
-
-
-
-
-
- )
-}
diff --git a/frontend/components/clients/payment-form.tsx b/frontend/components/clients/payment-form.tsx
deleted file mode 100644
index 5514cdf..0000000
--- a/frontend/components/clients/payment-form.tsx
+++ /dev/null
@@ -1,144 +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 {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog"
-
-interface PaymentFormProps {
- clientName: string
- outstandingAmount: number
- open: boolean
- onOpenChange: (open: boolean) => void
- onSubmit: (payment: { amount: number; notes: string; date: string }) => void
-}
-
-export function PaymentForm({ clientName, outstandingAmount, open, onOpenChange, onSubmit }: PaymentFormProps) {
- const [formData, setFormData] = useState({
- amount: 0,
- notes: "",
- date: new Date().toISOString().split("T")[0],
- })
-
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault()
- onSubmit(formData)
- onOpenChange(false)
- setFormData({
- amount: 0,
- notes: "",
- date: new Date().toISOString().split("T")[0],
- })
- }
-
- const maxPayment = outstandingAmount
-
- return (
-
- )
-}
diff --git a/frontend/components/credit/credit-overview.tsx b/frontend/components/credit/credit-overview.tsx
deleted file mode 100644
index aca1e24..0000000
--- a/frontend/components/credit/credit-overview.tsx
+++ /dev/null
@@ -1,229 +0,0 @@
-"use client"
-
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
-import { Badge } from "@/components/ui/badge"
-import { Progress } from "@/components/ui/progress"
-import { TrendingUp, TrendingDown, DollarSign, AlertTriangle, Clock, CheckCircle } from "lucide-react"
-import type { Client, Supplier } from "@/types/business"
-
-interface CreditOverviewProps {
- clients: Client[]
- suppliers: Supplier[]
-}
-
-export function CreditOverview({ clients, suppliers }: CreditOverviewProps) {
- // Calculate receivables (money owed to us by clients)
- const totalReceivables = clients.reduce((sum, client) => sum + client.outstandingAmount, 0)
- const clientsWithCredit = clients.filter((client) => client.outstandingAmount > 0)
- const overLimitClients = clients.filter((client) => client.outstandingAmount > client.creditLimit)
- const totalCreditLimit = clients.reduce((sum, client) => sum + client.creditLimit, 0)
- const creditUtilization = totalCreditLimit > 0 ? (totalReceivables / totalCreditLimit) * 100 : 0
-
- // Calculate payables (money we owe to suppliers)
- const totalPayables = suppliers.reduce((sum, supplier) => sum + supplier.amountOwed, 0)
- const suppliersWithBalance = suppliers.filter((supplier) => supplier.amountOwed > 0)
-
- // Net credit position (positive means we're owed more than we owe)
- const netCreditPosition = totalReceivables - totalPayables
-
- // Aging analysis (simplified - in real app would use actual dates)
- const currentReceivables = totalReceivables * 0.6
- const thirtyDayReceivables = totalReceivables * 0.25
- const sixtyDayReceivables = totalReceivables * 0.1
- const ninetyPlusReceivables = totalReceivables * 0.05
-
- return (
-
- {/* Summary Cards */}
-
-
-
- Total Receivables
-
-
-
- ${totalReceivables.toLocaleString()}
-
- {clientsWithCredit.length} clients with outstanding balances
-
-
-
-
-
-
- Total Payables
-
-
-
- ${totalPayables.toLocaleString()}
-
- {suppliersWithBalance.length} suppliers with outstanding balances
-
-
-
-
-
-
- Net Position
- = 0 ? "text-green-600" : "text-red-600"}`} />
-
-
- = 0 ? "text-green-600" : "text-red-600"}`}>
- ${Math.abs(netCreditPosition).toLocaleString()}
-
-
- {netCreditPosition >= 0 ? "Net receivable position" : "Net payable position"}
-
-
-
-
-
-
- Credit Utilization
- 80 ? "text-red-600" : "text-yellow-600"}`} />
-
-
- {creditUtilization.toFixed(1)}%
-
-
- ${totalReceivables.toLocaleString()} / ${totalCreditLimit.toLocaleString()} limit
-
-
-
-
-
- {/* Alerts */}
- {(overLimitClients.length > 0 || creditUtilization > 90) && (
-
-
-
-
- Credit Alerts
-
-
-
- {overLimitClients.length > 0 && (
-
- {overLimitClients.length} clients are over their credit limit
-
- )}
- {creditUtilization > 90 && (
-
- Overall credit utilization is critically high at {creditUtilization.toFixed(1)}%
-
- )}
-
-
- )}
-
- {/* Aging Analysis */}
-
-
-
-
-
- Receivables Aging
-
- Breakdown of outstanding receivables by age
-
-
-
-
-
-
- Current (0-30 days)
-
-
-
${currentReceivables.toFixed(0)}
-
- 60%
-
-
-
-
-
-
-
- 31-60 days
-
-
-
${thirtyDayReceivables.toFixed(0)}
-
- 25%
-
-
-
-
-
-
-
-
${sixtyDayReceivables.toFixed(0)}
-
- 10%
-
-
-
-
-
-
-
-
${ninetyPlusReceivables.toFixed(0)}
-
- 5%
-
-
-
-
-
-
-
-
-
-
-
- Cash Flow Impact
-
- Credit impact on cash flow
-
-
-
-
-
-
Expected Inflow
-
From client payments
-
-
${totalReceivables.toLocaleString()}
-
-
-
-
-
Expected Outflow
-
To supplier payments
-
-
${totalPayables.toLocaleString()}
-
-
-
-
-
Net Cash Impact
-
- {netCreditPosition >= 0 ? "Positive impact" : "Negative impact"}
-
-
-
= 0 ? "text-green-600" : "text-red-600"}`}>
- {netCreditPosition >= 0 ? "+" : "-"}${Math.abs(netCreditPosition).toLocaleString()}
-
-
-
-
-
-
-
- )
-}
diff --git a/frontend/components/credit/transaction-history.tsx b/frontend/components/credit/transaction-history.tsx
deleted file mode 100644
index 834305a..0000000
--- a/frontend/components/credit/transaction-history.tsx
+++ /dev/null
@@ -1,272 +0,0 @@
-"use client"
-
-import { useState } from "react"
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
-import { Badge } from "@/components/ui/badge"
-import { Input } from "@/components/ui/input"
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
-import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
-import { Search, ArrowUpRight, ArrowDownLeft, Calendar } from "lucide-react"
-
-interface Transaction {
- id: string
- date: string
- type: "receivable" | "payable" | "payment_received" | "payment_made"
- entity: string
- description: string
- amount: number
- status: "pending" | "completed" | "overdue"
- reference?: string
-}
-
-// Mock transaction data
-const mockTransactions: Transaction[] = [
- {
- id: "1",
- date: "2024-01-15",
- type: "receivable",
- entity: "ABC Electronics",
- description: "Invoice #INV-001 - Wireless Headphones",
- amount: 2500,
- status: "completed",
- reference: "INV-001",
- },
- {
- id: "2",
- date: "2024-01-14",
- type: "payment_received",
- entity: "Tech Solutions Inc",
- description: "Payment received for Invoice #INV-002",
- amount: 1800,
- status: "completed",
- reference: "PAY-001",
- },
- {
- id: "3",
- date: "2024-01-13",
- type: "payable",
- entity: "Electronics Wholesale Co",
- description: "Purchase Order #PO-001 - Components",
- amount: 3200,
- status: "pending",
- reference: "PO-001",
- },
- {
- id: "4",
- date: "2024-01-12",
- type: "payment_made",
- entity: "Global Tech Supplies",
- description: "Payment for Invoice #SUP-001",
- amount: 1500,
- status: "completed",
- reference: "PAY-002",
- },
- {
- id: "5",
- date: "2024-01-11",
- type: "receivable",
- entity: "Mobile World",
- description: "Invoice #INV-003 - Phone Cases",
- amount: 950,
- status: "overdue",
- reference: "INV-003",
- },
- {
- id: "6",
- date: "2024-01-10",
- type: "payable",
- entity: "Premium Components Ltd",
- description: "Purchase Order #PO-002 - Premium Parts",
- amount: 4500,
- status: "pending",
- reference: "PO-002",
- },
-]
-
-export function TransactionHistory() {
- const [searchTerm, setSearchTerm] = useState("")
- const [typeFilter, setTypeFilter] = useState("all")
- const [statusFilter, setStatusFilter] = useState("all")
-
- const filteredTransactions = mockTransactions.filter((transaction) => {
- const matchesSearch =
- transaction.entity.toLowerCase().includes(searchTerm.toLowerCase()) ||
- transaction.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
- (transaction.reference && transaction.reference.toLowerCase().includes(searchTerm.toLowerCase()))
-
- const matchesType = typeFilter === "all" || transaction.type === typeFilter
- const matchesStatus = statusFilter === "all" || transaction.status === statusFilter
-
- return matchesSearch && matchesType && matchesStatus
- })
-
- const getTransactionIcon = (type: string) => {
- switch (type) {
- case "receivable":
- case "payment_received":
- return
- case "payable":
- case "payment_made":
- return
- default:
- return
- }
- }
-
- const getTransactionColor = (type: string) => {
- switch (type) {
- case "receivable":
- case "payment_received":
- return "text-green-600"
- case "payable":
- case "payment_made":
- return "text-red-600"
- default:
- return "text-foreground"
- }
- }
-
- const getStatusBadge = (status: string) => {
- switch (status) {
- case "completed":
- return (
-
- Completed
-
- )
- case "pending":
- return (
-
- Pending
-
- )
- case "overdue":
- return (
-
- Overdue
-
- )
- default:
- return (
-
- {status}
-
- )
- }
- }
-
- return (
-
-
-
-
- Transaction History
-
- Complete history of all credit-related transactions
-
-
- {/* Filters */}
-
-
-
- setSearchTerm(e.target.value)}
- className="pl-10"
- />
-
-
-
-
-
-
-
- {/* Transaction Table */}
-
-
-
-
- Date
- Type
- Entity
- Description
- Amount
- Status
- Reference
-
-
-
- {filteredTransactions.length === 0 ? (
-
-
- {searchTerm || typeFilter !== "all" || statusFilter !== "all"
- ? "No transactions found matching your filters."
- : "No transactions recorded yet."}
-
-
- ) : (
- filteredTransactions.map((transaction) => (
-
-
- {new Date(transaction.date).toLocaleDateString()}
-
-
-
- {getTransactionIcon(transaction.type)}
- {transaction.type.replace("_", " ")}
-
-
-
- {transaction.entity}
-
-
-
- {transaction.description}
-
-
-
-
- {transaction.type.includes("receivable") || transaction.type.includes("payment_received")
- ? "+"
- : "-"}
- ${transaction.amount.toLocaleString()}
-
-
- {getStatusBadge(transaction.status)}
-
- {transaction.reference && (
- {transaction.reference}
- )}
-
-
- ))
- )}
-
-
-
-
-
- )
-}
diff --git a/frontend/components/products/product-form.tsx b/frontend/components/products/product-form.tsx
deleted file mode 100644
index 9cd451b..0000000
--- a/frontend/components/products/product-form.tsx
+++ /dev/null
@@ -1,230 +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 { Product } from "@/types/business"
-
-interface ProductFormProps {
- product?: Product
- open: boolean
- onOpenChange: (open: boolean) => void
- onSubmit: (product: Omit) => void
-}
-
-const categories = [
- "Electronics",
- "Audio",
- "Accessories",
- "Mobile",
- "Computing",
- "Gaming",
- "Home & Garden",
- "Sports",
- "Other",
-]
-
-export function ProductForm({ product, open, onOpenChange, onSubmit }: ProductFormProps) {
- const [formData, setFormData] = useState({
- name: product?.name || "",
- category: product?.category || "",
- description: product?.description || "",
- buyPrice: product?.buyPrice || 0,
- sellPrice: product?.sellPrice || 0,
- stock: product?.stock || 0,
- minStock: product?.minStock || 5,
- supplier: product?.supplier || "",
- sku: product?.sku || "",
- })
-
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault()
- onSubmit(formData)
- onOpenChange(false)
- // Reset form if it's a new product
- if (!product) {
- setFormData({
- name: "",
- category: "",
- description: "",
- buyPrice: 0,
- sellPrice: 0,
- stock: 0,
- minStock: 5,
- supplier: "",
- sku: "",
- })
- }
- }
-
- const profitMargin =
- formData.sellPrice > 0 && formData.buyPrice > 0
- ? (((formData.sellPrice - formData.buyPrice) / formData.sellPrice) * 100).toFixed(1)
- : "0"
-
- return (
-
- )
-}
diff --git a/frontend/components/products/products-table.tsx b/frontend/components/products/products-table.tsx
deleted file mode 100644
index 3391737..0000000
--- a/frontend/components/products/products-table.tsx
+++ /dev/null
@@ -1,171 +0,0 @@
-"use client"
-
-import { useState } from "react"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import { Badge } from "@/components/ui/badge"
-import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
-import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
-} from "@/components/ui/alert-dialog"
-import { Search, MoreHorizontal, Edit, Trash2, AlertTriangle } from "lucide-react"
-import type { Product } from "@/types/business"
-
-interface ProductsTableProps {
- products: Product[]
- onEdit: (product: Product) => void
- onDelete: (productId: string) => void
-}
-
-export function ProductsTable({ products, onEdit, onDelete }: ProductsTableProps) {
- const [searchTerm, setSearchTerm] = useState("")
- const [deleteProduct, setDeleteProduct] = useState(null)
-
- const filteredProducts = products.filter(
- (product) =>
- product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
- product.category.toLowerCase().includes(searchTerm.toLowerCase()) ||
- product.sku.toLowerCase().includes(searchTerm.toLowerCase()),
- )
-
- const getStockStatus = (stock: number, minStock: number) => {
- if (stock === 0) return { label: "Out of Stock", variant: "destructive" as const }
- if (stock <= minStock) return { label: "Low Stock", variant: "secondary" as const }
- return { label: "In Stock", variant: "default" as const }
- }
-
- const handleDelete = (product: Product) => {
- onDelete(product.id)
- setDeleteProduct(null)
- }
-
- return (
-
- {/* Search */}
-
-
- setSearchTerm(e.target.value)}
- className="pl-10"
- />
-
-
- {/* Products Table */}
-
-
-
-
- Product
- Category
- SKU
- Buy Price
- Sell Price
- Stock
- Status
-
-
-
-
- {filteredProducts.length === 0 ? (
-
-
- {searchTerm ? "No products found matching your search." : "No products added yet."}
-
-
- ) : (
- filteredProducts.map((product) => {
- const stockStatus = getStockStatus(product.stock, product.minStock)
- const profitMargin = (((product.sellPrice - product.buyPrice) / product.sellPrice) * 100).toFixed(1)
-
- return (
-
-
-
-
{product.name}
- {product.description && (
-
{product.description}
- )}
-
-
- {product.category}
-
- {product.sku || "N/A"}
-
- ${product.buyPrice.toFixed(2)}
-
-
-
${product.sellPrice.toFixed(2)}
-
{profitMargin}% margin
-
-
-
-
- {product.stock <= product.minStock && product.stock > 0 && (
-
- )}
-
{product.stock}
-
-
-
- {stockStatus.label}
-
-
-
-
-
-
-
- onEdit(product)}>
-
- Edit
-
- setDeleteProduct(product)} className="text-red-600">
-
- Delete
-
-
-
-
-
- )
- })
- )}
-
-
-
-
- {/* Delete Confirmation Dialog */}
-
setDeleteProduct(null)}>
-
-
- Delete Product
-
- Are you sure you want to delete "{deleteProduct?.name}"? This action cannot be undone.
-
-
-
- Cancel
- deleteProduct && handleDelete(deleteProduct)}
- className="bg-red-600 hover:bg-red-700"
- >
- Delete
-
-
-
-
-
- )
-}
diff --git a/frontend/components/suppliers/purchase-form.tsx b/frontend/components/suppliers/purchase-form.tsx
deleted file mode 100644
index 50bdc68..0000000
--- a/frontend/components/suppliers/purchase-form.tsx
+++ /dev/null
@@ -1,114 +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 {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog"
-
-interface PurchaseFormProps {
- supplierName: string
- open: boolean
- onOpenChange: (open: boolean) => void
- onSubmit: (purchase: { amount: number; description: string; date: string; invoiceNumber: string }) => void
-}
-
-export function PurchaseForm({ supplierName, open, onOpenChange, onSubmit }: PurchaseFormProps) {
- const [formData, setFormData] = useState({
- amount: 0,
- description: "",
- date: new Date().toISOString().split("T")[0],
- invoiceNumber: "",
- })
-
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault()
- onSubmit(formData)
- onOpenChange(false)
- setFormData({
- amount: 0,
- description: "",
- date: new Date().toISOString().split("T")[0],
- invoiceNumber: "",
- })
- }
-
- return (
-
- )
-}
diff --git a/frontend/components/suppliers/supplier-form.tsx b/frontend/components/suppliers/supplier-form.tsx
deleted file mode 100644
index da414db..0000000
--- a/frontend/components/suppliers/supplier-form.tsx
+++ /dev/null
@@ -1,196 +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 { Supplier } from "@/types/business"
-
-interface SupplierFormProps {
- supplier?: Supplier
- open: boolean
- onOpenChange: (open: boolean) => void
- onSubmit: (supplier: Omit) => void
-}
-
-const paymentTerms = ["Net 15", "Net 30", "Net 45", "Net 60", "COD", "Prepaid", "Custom"]
-
-export function SupplierForm({ supplier, open, onOpenChange, onSubmit }: SupplierFormProps) {
- const [formData, setFormData] = useState({
- name: supplier?.name || "",
- email: supplier?.email || "",
- phone: supplier?.phone || "",
- address: supplier?.address || "",
- paymentTerms: supplier?.paymentTerms || "Net 30",
- notes: supplier?.notes || "",
- contactPerson: supplier?.contactPerson || "",
- businessType: supplier?.businessType || "",
- taxId: supplier?.taxId || "",
- })
-
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault()
- onSubmit({
- ...formData,
- amountOwed: supplier?.amountOwed || 0,
- })
- onOpenChange(false)
- // Reset form if it's a new supplier
- if (!supplier) {
- setFormData({
- name: "",
- email: "",
- phone: "",
- address: "",
- paymentTerms: "Net 30",
- notes: "",
- contactPerson: "",
- businessType: "",
- taxId: "",
- })
- }
- }
-
- return (
-
- )
-}
diff --git a/frontend/components/suppliers/supplier-payment-form.tsx b/frontend/components/suppliers/supplier-payment-form.tsx
deleted file mode 100644
index bd62c84..0000000
--- a/frontend/components/suppliers/supplier-payment-form.tsx
+++ /dev/null
@@ -1,162 +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 {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog"
-
-interface SupplierPaymentFormProps {
- supplierName: string
- amountOwed: number
- open: boolean
- onOpenChange: (open: boolean) => void
- onSubmit: (payment: { amount: number; notes: string; date: string; paymentMethod: string }) => void
-}
-
-export function SupplierPaymentForm({
- supplierName,
- amountOwed,
- open,
- onOpenChange,
- onSubmit,
-}: SupplierPaymentFormProps) {
- const [formData, setFormData] = useState({
- amount: 0,
- notes: "",
- date: new Date().toISOString().split("T")[0],
- paymentMethod: "",
- })
-
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault()
- onSubmit(formData)
- onOpenChange(false)
- setFormData({
- amount: 0,
- notes: "",
- date: new Date().toISOString().split("T")[0],
- paymentMethod: "",
- })
- }
-
- const maxPayment = amountOwed
-
- return (
-
- )
-}
diff --git a/frontend/components/suppliers/suppliers-table.tsx b/frontend/components/suppliers/suppliers-table.tsx
deleted file mode 100644
index f32fa28..0000000
--- a/frontend/components/suppliers/suppliers-table.tsx
+++ /dev/null
@@ -1,197 +0,0 @@
-"use client"
-
-import { useState } from "react"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import { Badge } from "@/components/ui/badge"
-import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
-import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
-} from "@/components/ui/alert-dialog"
-import { Search, MoreHorizontal, Edit, Trash2, Mail, Phone, CreditCard, Plus } from "lucide-react"
-import type { Supplier } from "@/types/business"
-
-interface SuppliersTableProps {
- suppliers: Supplier[]
- onEdit: (supplier: Supplier) => void
- onDelete: (supplierId: string) => void
- onAddPurchase: (supplierId: string) => void
- onRecordPayment: (supplierId: string) => void
-}
-
-export function SuppliersTable({ suppliers, onEdit, onDelete, onAddPurchase, onRecordPayment }: SuppliersTableProps) {
- const [searchTerm, setSearchTerm] = useState("")
- const [deleteSupplier, setDeleteSupplier] = useState(null)
-
- const filteredSuppliers = suppliers.filter(
- (supplier) =>
- supplier.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
- supplier.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
- supplier.contactPerson.toLowerCase().includes(searchTerm.toLowerCase()) ||
- supplier.businessType.toLowerCase().includes(searchTerm.toLowerCase()),
- )
-
- const getPaymentStatus = (amountOwed: number) => {
- if (amountOwed === 0) return { label: "Paid Up", variant: "default" as const, color: "text-green-600" }
- if (amountOwed > 5000) return { label: "High Balance", variant: "secondary" as const, color: "text-red-600" }
- return { label: "Outstanding", variant: "secondary" as const, color: "text-yellow-600" }
- }
-
- const handleDelete = (supplier: Supplier) => {
- onDelete(supplier.id)
- setDeleteSupplier(null)
- }
-
- return (
-
- {/* Search */}
-
-
- setSearchTerm(e.target.value)}
- className="pl-10"
- />
-
-
- {/* Suppliers Table */}
-
-
-
-
- Supplier
- Contact
- Business Type
- Payment Terms
- Status
- Amount Owed
-
-
-
-
- {filteredSuppliers.length === 0 ? (
-
-
- {searchTerm ? "No suppliers found matching your search." : "No suppliers added yet."}
-
-
- ) : (
- filteredSuppliers.map((supplier) => {
- const paymentStatus = getPaymentStatus(supplier.amountOwed)
-
- return (
-
-
-
-
{supplier.name}
- {supplier.contactPerson && (
-
{supplier.contactPerson}
- )}
-
-
-
-
- {supplier.email && (
-
-
- {supplier.email}
-
- )}
- {supplier.phone && (
-
- )}
-
-
-
- {supplier.businessType || "N/A"}
-
-
-
- {supplier.paymentTerms}
-
-
-
-
- {paymentStatus.label}
-
-
-
-
-
0 ? paymentStatus.color : ""}`}>
- ${supplier.amountOwed.toFixed(2)}
-
-
-
-
-
-
-
-
-
- onEdit(supplier)}>
-
- Edit
-
- onAddPurchase(supplier.id)}>
-
- Add Purchase
-
- {supplier.amountOwed > 0 && (
- onRecordPayment(supplier.id)}>
-
- Record Payment
-
- )}
- setDeleteSupplier(supplier)} className="text-red-600">
-
- Delete
-
-
-
-
-
- )
- })
- )}
-
-
-
-
- {/* Delete Confirmation Dialog */}
-
setDeleteSupplier(null)}>
-
-
- Delete Supplier
-
- Are you sure you want to delete "{deleteSupplier?.name}"? This action cannot be undone and will remove all
- associated purchase history.
-
-
-
- Cancel
- deleteSupplier && handleDelete(deleteSupplier)}
- className="bg-red-600 hover:bg-red-700"
- >
- Delete
-
-
-
-
-
- )
-}
diff --git a/frontend/components/theme-provider.tsx b/frontend/components/theme-provider.tsx
deleted file mode 100644
index 55c2f6e..0000000
--- a/frontend/components/theme-provider.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-'use client'
-
-import * as React from 'react'
-import {
- ThemeProvider as NextThemesProvider,
- type ThemeProviderProps,
-} from 'next-themes'
-
-export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
- return {children}
-}
diff --git a/frontend/components/ui/accordion.tsx b/frontend/components/ui/accordion.tsx
deleted file mode 100644
index 4a8cca4..0000000
--- a/frontend/components/ui/accordion.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-"use client"
-
-import * as React from "react"
-import * as AccordionPrimitive from "@radix-ui/react-accordion"
-import { ChevronDownIcon } from "lucide-react"
-
-import { cn } from "@/lib/utils"
-
-function Accordion({
- ...props
-}: React.ComponentProps) {
- return
-}
-
-function AccordionItem({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-function AccordionTrigger({
- className,
- children,
- ...props
-}: React.ComponentProps) {
- return (
-
- svg]:rotate-180",
- className
- )}
- {...props}
- >
- {children}
-
-
-
- )
-}
-
-function AccordionContent({
- className,
- children,
- ...props
-}: React.ComponentProps) {
- return (
-
- {children}
-
- )
-}
-
-export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/frontend/components/ui/alert-dialog.tsx b/frontend/components/ui/alert-dialog.tsx
deleted file mode 100644
index 0863e40..0000000
--- a/frontend/components/ui/alert-dialog.tsx
+++ /dev/null
@@ -1,157 +0,0 @@
-"use client"
-
-import * as React from "react"
-import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
-
-import { cn } from "@/lib/utils"
-import { buttonVariants } from "@/components/ui/button"
-
-function AlertDialog({
- ...props
-}: React.ComponentProps) {
- return
-}
-
-function AlertDialogTrigger({
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-function AlertDialogPortal({
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-function AlertDialogOverlay({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-function AlertDialogContent({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
-
-
-
- )
-}
-
-function AlertDialogHeader({
- className,
- ...props
-}: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function AlertDialogFooter({
- className,
- ...props
-}: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function AlertDialogTitle({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-function AlertDialogDescription({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-function AlertDialogAction({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-function AlertDialogCancel({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-export {
- AlertDialog,
- AlertDialogPortal,
- AlertDialogOverlay,
- AlertDialogTrigger,
- AlertDialogContent,
- AlertDialogHeader,
- AlertDialogFooter,
- AlertDialogTitle,
- AlertDialogDescription,
- AlertDialogAction,
- AlertDialogCancel,
-}
diff --git a/frontend/components/ui/alert.tsx b/frontend/components/ui/alert.tsx
deleted file mode 100644
index 1421354..0000000
--- a/frontend/components/ui/alert.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import * as React from "react"
-import { cva, type VariantProps } from "class-variance-authority"
-
-import { cn } from "@/lib/utils"
-
-const alertVariants = cva(
- "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
- {
- variants: {
- variant: {
- default: "bg-card text-card-foreground",
- destructive:
- "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- }
-)
-
-function Alert({
- className,
- variant,
- ...props
-}: React.ComponentProps<"div"> & VariantProps) {
- return (
-
- )
-}
-
-function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function AlertDescription({
- className,
- ...props
-}: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-export { Alert, AlertTitle, AlertDescription }
diff --git a/frontend/components/ui/aspect-ratio.tsx b/frontend/components/ui/aspect-ratio.tsx
deleted file mode 100644
index 3df3fd0..0000000
--- a/frontend/components/ui/aspect-ratio.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-"use client"
-
-import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
-
-function AspectRatio({
- ...props
-}: React.ComponentProps) {
- return
-}
-
-export { AspectRatio }
diff --git a/frontend/components/ui/avatar.tsx b/frontend/components/ui/avatar.tsx
deleted file mode 100644
index 71e428b..0000000
--- a/frontend/components/ui/avatar.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-"use client"
-
-import * as React from "react"
-import * as AvatarPrimitive from "@radix-ui/react-avatar"
-
-import { cn } from "@/lib/utils"
-
-function Avatar({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-function AvatarImage({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-function AvatarFallback({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-export { Avatar, AvatarImage, AvatarFallback }
diff --git a/frontend/components/ui/badge.tsx b/frontend/components/ui/badge.tsx
deleted file mode 100644
index 0205413..0000000
--- a/frontend/components/ui/badge.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
-import { cva, type VariantProps } from "class-variance-authority"
-
-import { cn } from "@/lib/utils"
-
-const badgeVariants = cva(
- "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
- {
- variants: {
- variant: {
- default:
- "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
- secondary:
- "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
- destructive:
- "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
- outline:
- "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- }
-)
-
-function Badge({
- className,
- variant,
- asChild = false,
- ...props
-}: React.ComponentProps<"span"> &
- VariantProps & { asChild?: boolean }) {
- const Comp = asChild ? Slot : "span"
-
- return (
-
- )
-}
-
-export { Badge, badgeVariants }
diff --git a/frontend/components/ui/breadcrumb.tsx b/frontend/components/ui/breadcrumb.tsx
deleted file mode 100644
index eb88f32..0000000
--- a/frontend/components/ui/breadcrumb.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
-import { ChevronRight, MoreHorizontal } from "lucide-react"
-
-import { cn } from "@/lib/utils"
-
-function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
- return
-}
-
-function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
- return (
-
- )
-}
-
-function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
- return (
-
- )
-}
-
-function BreadcrumbLink({
- asChild,
- className,
- ...props
-}: React.ComponentProps<"a"> & {
- asChild?: boolean
-}) {
- const Comp = asChild ? Slot : "a"
-
- return (
-
- )
-}
-
-function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
- return (
-
- )
-}
-
-function BreadcrumbSeparator({
- children,
- className,
- ...props
-}: React.ComponentProps<"li">) {
- return (
- svg]:size-3.5", className)}
- {...props}
- >
- {children ?? }
-
- )
-}
-
-function BreadcrumbEllipsis({
- className,
- ...props
-}: React.ComponentProps<"span">) {
- return (
-
-
- More
-
- )
-}
-
-export {
- Breadcrumb,
- BreadcrumbList,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbPage,
- BreadcrumbSeparator,
- BreadcrumbEllipsis,
-}
diff --git a/frontend/components/ui/button.tsx b/frontend/components/ui/button.tsx
deleted file mode 100644
index a2df8dc..0000000
--- a/frontend/components/ui/button.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
-import { cva, type VariantProps } from "class-variance-authority"
-
-import { cn } from "@/lib/utils"
-
-const buttonVariants = cva(
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
- {
- variants: {
- variant: {
- default:
- "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
- destructive:
- "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
- outline:
- "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
- secondary:
- "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
- ghost:
- "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
- link: "text-primary underline-offset-4 hover:underline",
- },
- size: {
- default: "h-9 px-4 py-2 has-[>svg]:px-3",
- sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
- lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
- icon: "size-9",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- }
-)
-
-function Button({
- className,
- variant,
- size,
- asChild = false,
- ...props
-}: React.ComponentProps<"button"> &
- VariantProps & {
- asChild?: boolean
- }) {
- const Comp = asChild ? Slot : "button"
-
- return (
-
- )
-}
-
-export { Button, buttonVariants }
diff --git a/frontend/components/ui/calendar.tsx b/frontend/components/ui/calendar.tsx
deleted file mode 100644
index 4d7c46a..0000000
--- a/frontend/components/ui/calendar.tsx
+++ /dev/null
@@ -1,213 +0,0 @@
-"use client"
-
-import * as React from "react"
-import {
- ChevronDownIcon,
- ChevronLeftIcon,
- ChevronRightIcon,
-} from "lucide-react"
-import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
-
-import { cn } from "@/lib/utils"
-import { Button, buttonVariants } from "@/components/ui/button"
-
-function Calendar({
- className,
- classNames,
- showOutsideDays = true,
- captionLayout = "label",
- buttonVariant = "ghost",
- formatters,
- components,
- ...props
-}: React.ComponentProps & {
- buttonVariant?: React.ComponentProps["variant"]
-}) {
- const defaultClassNames = getDefaultClassNames()
-
- return (
- svg]:rotate-180`,
- String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
- className
- )}
- captionLayout={captionLayout}
- formatters={{
- formatMonthDropdown: (date) =>
- date.toLocaleString("default", { month: "short" }),
- ...formatters,
- }}
- classNames={{
- root: cn("w-fit", defaultClassNames.root),
- months: cn(
- "flex gap-4 flex-col md:flex-row relative",
- defaultClassNames.months
- ),
- month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
- nav: cn(
- "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
- defaultClassNames.nav
- ),
- button_previous: cn(
- buttonVariants({ variant: buttonVariant }),
- "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
- defaultClassNames.button_previous
- ),
- button_next: cn(
- buttonVariants({ variant: buttonVariant }),
- "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
- defaultClassNames.button_next
- ),
- month_caption: cn(
- "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
- defaultClassNames.month_caption
- ),
- dropdowns: cn(
- "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
- defaultClassNames.dropdowns
- ),
- dropdown_root: cn(
- "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
- defaultClassNames.dropdown_root
- ),
- dropdown: cn(
- "absolute bg-popover inset-0 opacity-0",
- defaultClassNames.dropdown
- ),
- caption_label: cn(
- "select-none font-medium",
- captionLayout === "label"
- ? "text-sm"
- : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
- defaultClassNames.caption_label
- ),
- table: "w-full border-collapse",
- weekdays: cn("flex", defaultClassNames.weekdays),
- weekday: cn(
- "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
- defaultClassNames.weekday
- ),
- week: cn("flex w-full mt-2", defaultClassNames.week),
- week_number_header: cn(
- "select-none w-(--cell-size)",
- defaultClassNames.week_number_header
- ),
- week_number: cn(
- "text-[0.8rem] select-none text-muted-foreground",
- defaultClassNames.week_number
- ),
- day: cn(
- "relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
- defaultClassNames.day
- ),
- range_start: cn(
- "rounded-l-md bg-accent",
- defaultClassNames.range_start
- ),
- range_middle: cn("rounded-none", defaultClassNames.range_middle),
- range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
- today: cn(
- "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
- defaultClassNames.today
- ),
- outside: cn(
- "text-muted-foreground aria-selected:text-muted-foreground",
- defaultClassNames.outside
- ),
- disabled: cn(
- "text-muted-foreground opacity-50",
- defaultClassNames.disabled
- ),
- hidden: cn("invisible", defaultClassNames.hidden),
- ...classNames,
- }}
- components={{
- Root: ({ className, rootRef, ...props }) => {
- return (
-
- )
- },
- Chevron: ({ className, orientation, ...props }) => {
- if (orientation === "left") {
- return (
-
- )
- }
-
- if (orientation === "right") {
- return (
-
- )
- }
-
- return (
-
- )
- },
- DayButton: CalendarDayButton,
- WeekNumber: ({ children, ...props }) => {
- return (
-
-
- {children}
-
- |
- )
- },
- ...components,
- }}
- {...props}
- />
- )
-}
-
-function CalendarDayButton({
- className,
- day,
- modifiers,
- ...props
-}: React.ComponentProps) {
- const defaultClassNames = getDefaultClassNames()
-
- const ref = React.useRef(null)
- React.useEffect(() => {
- if (modifiers.focused) ref.current?.focus()
- }, [modifiers.focused])
-
- return (
-