From 4bef5974bceae52ec2cc363db01d674e50289d85 Mon Sep 17 00:00:00 2001 From: Pranav Rajesh Waklekar Date: Wed, 5 Mar 2025 23:52:01 +0530 Subject: [PATCH 1/3] FEATURE: Implmented anomaly detection algorithm using isolation tree --- .../supplier_to_warehouse_recp.json | 3 +- Backend/dummy_data/warehouse_to_customer.json | 46 +++++------ Backend/src/Db/database_management.py | 3 - Backend/src/ml/__init__.py | 0 Backend/src/ml/anomaly_detection.py | 34 ++++++++ Backend/src/ml/data_fetcher.py | 78 +++++++++++++++++++ Backend/src/ml/data_simulator.py | 31 ++++++++ Backend/src/ml/db_init.py | 8 ++ Backend/src/ml/main.py | 27 +++++++ Backend/src/ml/test_main.py | 77 ++++++++++++++++++ 10 files changed, 280 insertions(+), 27 deletions(-) create mode 100644 Backend/src/ml/__init__.py create mode 100644 Backend/src/ml/anomaly_detection.py create mode 100644 Backend/src/ml/data_fetcher.py create mode 100644 Backend/src/ml/data_simulator.py create mode 100644 Backend/src/ml/db_init.py create mode 100644 Backend/src/ml/main.py create mode 100644 Backend/src/ml/test_main.py diff --git a/Backend/dummy_data/supplier_to_warehouse_recp.json b/Backend/dummy_data/supplier_to_warehouse_recp.json index 45244b3..83b57db 100644 --- a/Backend/dummy_data/supplier_to_warehouse_recp.json +++ b/Backend/dummy_data/supplier_to_warehouse_recp.json @@ -1,5 +1,6 @@ { - "Supplier": [ + + "Supplier": [ {"supplier_id": "SUP1", "supplier_name": "Supplier A"}, {"supplier_id": "SUP2", "supplier_name": "Supplier B"} ], diff --git a/Backend/dummy_data/warehouse_to_customer.json b/Backend/dummy_data/warehouse_to_customer.json index 0522135..51542bb 100644 --- a/Backend/dummy_data/warehouse_to_customer.json +++ b/Backend/dummy_data/warehouse_to_customer.json @@ -1,24 +1,24 @@ { - "Supplier": [ - {"supplier_id": "SUP1", "supplier_name": "Supplier A"}, - {"supplier_id": "SUP2", "supplier_name": "Supplier B"} - ], - "Product": [ - {"product_id": "PROD1", "rfid_tag": "RFID123", "product_name": "Laptop", "status": "available", "supplier_id": "SUP1"}, - {"product_id": "PROD2", "rfid_tag": "RFID124", "product_name": "Phone", "status": "available", "supplier_id": "SUP1"}, - {"product_id": "PROD3", "rfid_tag": "RFID125", "product_name": "Tablet", "status": "sold", "supplier_id": "SUP2"} - ], - "Shelf": [ - {"shelf_id": "SHELF1", "shelf_location": "Aisle 1, Shelf 2"}, - {"shelf_id": "SHELF2", "shelf_location": "Aisle 3, Shelf 1"} - ], - "ShelfInventory": [ - {"shelf_inventory_id": "SHELF_INV1", "shelf_id": "SHELF1", "product_id": "PROD1", "added_timestamp": "2025-03-03T17:00:00", "removed_timestamp": "2025-03-05T12:30:00"}, - {"shelf_inventory_id": "SHELF_INV2", "shelf_id": "SHELF1", "product_id": "PROD2", "added_timestamp": "2025-03-03T18:00:00", "removed_timestamp": null}, - {"shelf_inventory_id": "SHELF_INV3", "shelf_id": "SHELF2", "product_id": "PROD3", "added_timestamp": "2025-03-04T10:00:00", "removed_timestamp": "2025-03-05T12:00:00"} - ], - "Sale": [ - {"sale_id": "SALE1", "product_id": "PROD1", "sale_timestamp": "2025-03-05T12:30:00"}, - {"sale_id": "SALE2", "product_id": "PROD3", "sale_timestamp": "2025-03-05T12:00:00"} - ] - } \ No newline at end of file + "Supplier": [ + {"supplier_id": "SUP1", "supplier_name": "Supplier A"}, + {"supplier_id": "SUP2", "supplier_name": "Supplier B"} + ], + "Product": [ + {"product_id": "PROD1", "rfid_tag": "RFID123", "product_name": "Laptop", "status": "available", "supplier_id": "SUP1"}, + {"product_id": "PROD2", "rfid_tag": "RFID124", "product_name": "Phone", "status": "available", "supplier_id": "SUP1"}, + {"product_id": "PROD3", "rfid_tag": "RFID125", "product_name": "Tablet", "status": "sold", "supplier_id": "SUP2"} + ], + "Shelf": [ + {"shelf_id": "SHELF1", "shelf_location": "Aisle 1, Shelf 2"}, + {"shelf_id": "SHELF2", "shelf_location": "Aisle flags3, Shelf 1"} + ], + "ShelfInventory": [ + {"shelf_inventory_id": "SHELF_INV1", "shelf_id": "SHELF1", "product_id": "PROD1", "added_timestamp": "2025-03-03T17:00:00", "removed_timestamp": "2025-03-05T12:30:00"}, + {"shelf_inventory_id": "SHELF_INV2", "shelf_id": "SHELF1", "product_id": "PROD2", "added_timestamp": "2025-03-03T18:00:00", "removed_timestamp": "2025-03-05T12:15:00"}, + {"shelf_inventory_id": "SHELF_INV3", "shelf_id": "SHELF2", "product_id": "PROD3", "added_timestamp": "2025-03-04T10:00:00", "removed_timestamp": "2025-03-05T12:00:00"} + ], + "Sale": [ + {"sale_id": "SALE1", "product_id": "PROD1", "sale_timestamp": "2025-03-05T12:30:00"}, + {"sale_id": "SALE2", "product_id": "PROD2", "sale_timestamp": "2025-03-05T12:15:00"} + ] +} \ No newline at end of file diff --git a/Backend/src/Db/database_management.py b/Backend/src/Db/database_management.py index 68dfb44..05bd5ef 100644 --- a/Backend/src/Db/database_management.py +++ b/Backend/src/Db/database_management.py @@ -1,6 +1,3 @@ -from mako.parsetree import Expression -from sqlalchemy import False_ -from sqlalchemy.sql.functions import mode from sqlmodel.ext.asyncio.session import AsyncSession import logging diff --git a/Backend/src/ml/__init__.py b/Backend/src/ml/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/src/ml/anomaly_detection.py b/Backend/src/ml/anomaly_detection.py new file mode 100644 index 0000000..a85194f --- /dev/null +++ b/Backend/src/ml/anomaly_detection.py @@ -0,0 +1,34 @@ +# Backend/src/ml/anomaly_detection.py +import pandas as pd +import numpy as np +from sklearn.ensemble import IsolationForest +from .data_fetcher import fetch_recent_data +from ..Db.database_management import DatabaseManagement + +async def detect_anomalies(db: DatabaseManagement, interval_seconds: int = 60, contamination: float = 0.1): + """Detect anomalies in the recent inventory and sales data using Isolation Forest.""" + time_window_minutes = interval_seconds / 60 + + df = await fetch_recent_data(db, time_window_minutes) + # print("Data fetched for anomaly detection:", df) + + if df.empty: + # print("No data available for anomaly detection.") + return df + + features = ["inventory_count", "inventory_change", "sales_count"] + X = df[features].values + + iso_forest = IsolationForest( + contamination=contamination, + random_state=42, + n_estimators=100 + ) + iso_forest.fit(X) + + df["anomaly_label"] = iso_forest.predict(X) + df["anomaly_score"] = iso_forest.decision_function(X) + df["is_anomaly"] = df["anomaly_label"].apply(lambda x: True if x == -1 else False) + + # print("Data with anomaly labels and scores:", df) + return df \ No newline at end of file diff --git a/Backend/src/ml/data_fetcher.py b/Backend/src/ml/data_fetcher.py new file mode 100644 index 0000000..58ecd5c --- /dev/null +++ b/Backend/src/ml/data_fetcher.py @@ -0,0 +1,78 @@ +# Backend/src/ml/data_fetcher.py +import pandas as pd +import numpy as np +from datetime import datetime, timedelta +from sqlalchemy.sql import text +from Backend.src.Db.database_management import DatabaseManagement +from Backend.src.Db.models import Product, ShelfInventory, Sale, Shelf + +async def fetch_recent_data(db: DatabaseManagement, time_window_minutes: int = 60) -> pd.DataFrame: + """Fetch recent shelf inventory and sales data from the database.""" + cutoff_time = datetime.now() - timedelta(minutes=time_window_minutes) + # print(f"Cutoff Time: {cutoff_time}") + + # Fetch Shelf Inventory + shelf_inv_query = text(""" + SELECT shelf_inventory_id, product_id, shelf_id, added_timestamp, removed_timestamp + FROM ShelfInventory + WHERE added_timestamp >= :cutoff OR removed_timestamp >= :cutoff + ORDER BY added_timestamp, removed_timestamp + """).bindparams(cutoff=cutoff_time) + shelf_inv_results = await db.session.exec(shelf_inv_query) + shelf_inv_df = pd.DataFrame(shelf_inv_results.mappings().all()) if shelf_inv_results else pd.DataFrame() + # print("Raw Shelf Inventory DataFrame:", shelf_inv_df) + + # Fetch Sales + sales_query = text(""" + SELECT product_id, sale_timestamp, COUNT(*) as sales_count + FROM Sale + WHERE sale_timestamp >= :cutoff + GROUP BY product_id, sale_timestamp + """).bindparams(cutoff=cutoff_time) + sales_results = await db.session.exec(sales_query) + sales_df = pd.DataFrame(sales_results.mappings().all()) if sales_results else pd.DataFrame() + # print("Raw Sales DataFrame:", sales_df) + + # Fetch Products + products = await db.search(Product, all_results=True) + products_df = pd.DataFrame([p.dict() for p in products]) + # print("Products DataFrame:", products_df) + + # Fetch Shelves + shelves = await db.search(Shelf, all_results=True) + shelves_df = pd.DataFrame([s.dict() for s in shelves]) + # print("Shelves DataFrame:", shelves_df) + + # Preprocess Shelf Inventory Data + if not shelf_inv_df.empty: + shelf_inv_df["stock_change"] = np.where(shelf_inv_df["added_timestamp"].notnull() & shelf_inv_df["removed_timestamp"].isnull(), 1, + np.where(shelf_inv_df["removed_timestamp"].notnull(), -1, 0)) + shelf_inv_df["timestamp"] = shelf_inv_df.apply( + lambda row: row["removed_timestamp"] if pd.notnull(row["removed_timestamp"]) else row["added_timestamp"], axis=1 + ) + shelf_inv_df = shelf_inv_df.groupby(["product_id", "shelf_id", "timestamp"])["stock_change"].sum().reset_index() + shelf_inv_df["inventory_count"] = shelf_inv_df.groupby("product_id")["stock_change"].cumsum().fillna(0) + shelf_inv_df["inventory_change"] = shelf_inv_df.groupby("product_id")["inventory_count"].diff().fillna(0) + # print("After Preprocessing Shelf Inventory:", shelf_inv_df) + + shelf_inv_df = shelf_inv_df.merge(products_df[["product_id", "product_name"]], on="product_id", how="left") + # print("After Merging Products:", shelf_inv_df) + + shelf_inv_df = shelf_inv_df.merge(shelves_df[["shelf_id", "shelf_location"]], on="shelf_id", how="left") + # print("After Merging Shelves:", shelf_inv_df) + + if not sales_df.empty: + shelf_inv_df = shelf_inv_df.merge(sales_df[["sale_timestamp", "product_id", "sales_count"]], + left_on=["timestamp", "product_id"], + right_on=["sale_timestamp", "product_id"], + how="left") + # print("After Merging Sales:", shelf_inv_df) + + shelf_inv_df["sales_count"] = shelf_inv_df["sales_count"].fillna(0) + # print("Final Shelf Inventory DataFrame Before Return:", shelf_inv_df) + + # Ensure the returned DataFrame has the expected columns, even if empty + expected_columns = ["timestamp", "product_id", "product_name", "shelf_location", "inventory_count", "inventory_change", "sales_count"] + if shelf_inv_df.empty: + return pd.DataFrame(columns=expected_columns) + return shelf_inv_df[expected_columns] \ No newline at end of file diff --git a/Backend/src/ml/data_simulator.py b/Backend/src/ml/data_simulator.py new file mode 100644 index 0000000..683f5d5 --- /dev/null +++ b/Backend/src/ml/data_simulator.py @@ -0,0 +1,31 @@ + +import asyncio +from datetime import datetime, timedelta +from Backend.src.Db.database_management import DatabaseManagement +from Backend.src.Db.models import ShelfInventory, Sale, Product, Supplier, Shelf + +async def simulate_data_insertion(db: DatabaseManagement): + """Simulate real-time data insertion for hackathon demo.""" + base_time = datetime(2025, 3, 5, 10, 0) + for i in range(10): + time = base_time + timedelta(minutes=i * 5) + if i % 3 == 0: + shelf_inv = ShelfInventory( + shelf_inventory_id=f"SI{i+10}", + shelf_id="S1", + product_id="P1", + added_timestamp=base_time, + removed_timestamp=time + ) + else: + shelf_inv = ShelfInventory( + shelf_inventory_id=f"SI{i+10}", + shelf_id="S1", + product_id="P1", + added_timestamp=time, + removed_timestamp=None + ) + sale = Sale(sale_id=f"S{i+10}", product_id="P1", sale_timestamp=time) + await db.insert(sale) + await db.insert(shelf_inv) + await asyncio.sleep(2) \ No newline at end of file diff --git a/Backend/src/ml/db_init.py b/Backend/src/ml/db_init.py new file mode 100644 index 0000000..dfb4553 --- /dev/null +++ b/Backend/src/ml/db_init.py @@ -0,0 +1,8 @@ +# Backend/src/db_init.py +from sqlalchemy.ext.asyncio import AsyncEngine +from sqlmodel import SQLModel + +async def initialize_database(engine: AsyncEngine): + """Create tables if they don’t exist.""" + async with engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.create_all) \ No newline at end of file diff --git a/Backend/src/ml/main.py b/Backend/src/ml/main.py new file mode 100644 index 0000000..f4ab47d --- /dev/null +++ b/Backend/src/ml/main.py @@ -0,0 +1,27 @@ + +import asyncio +from Backend.src.Db.database_management import DatabaseManagement +from Backend.src.Db.db import get_session +from Backend.src.ml.anomaly_detection import detect_anomalies +from Backend.src.ml.data_simulator import simulate_data_insertion +from Backend.src.Db.models import Product, Supplier, Shelf + +async def main(): + + async for session in get_session(): + db = DatabaseManagement(session) + + await db.insert(Product(product_id="P1", rfid_tag="RFID1", product_name="Rice 5kg", status="available", supplier_id="SUP1")) + await db.insert(Product(product_id="P2", rfid_tag="RFID2", product_name="Toothpaste", status="available", supplier_id="SUP1")) + await db.insert(Supplier(supplier_id="SUP1", supplier_name="Supplier A")) + await db.insert(Shelf(shelf_id="S1", shelf_location="Aisle 1, Shelf 2")) + + + sim_task = asyncio.create_task(simulate_data_insertion(db)) + detect_task = asyncio.create_task(detect_anomalies(db, interval_seconds=10)) + + await sim_task + await detect_task + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/Backend/src/ml/test_main.py b/Backend/src/ml/test_main.py new file mode 100644 index 0000000..3adbe0a --- /dev/null +++ b/Backend/src/ml/test_main.py @@ -0,0 +1,77 @@ +# Backend/src/ml/test_main.py +import asyncio +import json +import pandas as pd +from sqlalchemy.ext.asyncio import create_async_engine +from sqlmodel.ext.asyncio.session import AsyncSession +from sqlalchemy.orm import sessionmaker +from .db_init import initialize_database +from ..Db.database_management import DatabaseManagement +from .anomaly_detection import detect_anomalies +from .data_simulator import simulate_data_insertion +from ..Db.models import Product, Supplier, Shelf, ShelfInventory, Sale + +# In-memory SQLite engine for testing +test_engine = create_async_engine("sqlite+aiosqlite:///:memory:") + +async def get_test_session() -> AsyncSession: + """Provide a session for the in-memory SQLite database.""" + async_session = sessionmaker(bind=test_engine, class_=AsyncSession, expire_on_commit=False) + async with async_session() as session: + yield session + +async def load_json_data(db: DatabaseManagement, json_file: str): + """Load dummy data from JSON into the in-memory database.""" + with open(json_file, 'r') as f: + data = json.load(f) + + # Insert Suppliers + for supplier in data.get("Supplier", []): + await db.insert(Supplier(**supplier)) + + # Insert Products + for product in data.get("Product", []): + await db.insert(Product(**product)) + + # Insert Shelves + for shelf in data.get("Shelf", []): + await db.insert(Shelf(**shelf)) + + # Insert Shelf Inventory + for shelf_inv in data.get("ShelfInventory", []): + shelf_inv["added_timestamp"] = pd.to_datetime(shelf_inv["added_timestamp"]) + if shelf_inv["removed_timestamp"]: + shelf_inv["removed_timestamp"] = pd.to_datetime(shelf_inv["removed_timestamp"]) + await db.insert(ShelfInventory(**shelf_inv)) + + # Insert Sales + for sale in data.get("Sale", []): + sale["sale_timestamp"] = pd.to_datetime(sale["sale_timestamp"]) + await db.insert(Sale(**sale)) + + # Commit the session + await db.session.commit() + +async def main(): + # Initialize in-memory database + await initialize_database(test_engine) + + async for session in get_test_session(): + db = DatabaseManagement(session) + + # Load dummy data from JSON + json_path = r"E:\TheftBlock\TheftBlock\Backend\dummy_data\warehouse_to_customer.json" + await load_json_data(db, json_path) + + # Run anomaly detection with a 48-hour window + result_df = await detect_anomalies(db, interval_seconds=172800) # 48 hours + + # Filter anomalies and select only desired columns + if not result_df.empty: + anomalies = result_df[result_df["is_anomaly"]][["product_id", "product_name", "shelf_location"]] + print(anomalies if not anomalies.empty else "No anomalies detected.") + else: + print("No data returned from anomaly detection.") + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file From 3d0e1be136460101899e03dfcf90fa61ccbe588a Mon Sep 17 00:00:00 2001 From: Pranav Rajesh Waklekar Date: Thu, 6 Mar 2025 00:50:44 +0530 Subject: [PATCH 2/3] updated files in ml folder --- Backend/src/ml/anomaly_detection.py | 8 +--- Backend/src/ml/data_fetcher.py | 49 +++++++++++------------ Backend/src/ml/data_simulator.py | 31 --------------- Backend/src/ml/db_init.py | 2 +- Backend/src/ml/main.py | 27 ------------- Backend/src/ml/test_main.py | 60 +++-------------------------- 6 files changed, 31 insertions(+), 146 deletions(-) delete mode 100644 Backend/src/ml/data_simulator.py delete mode 100644 Backend/src/ml/main.py diff --git a/Backend/src/ml/anomaly_detection.py b/Backend/src/ml/anomaly_detection.py index a85194f..cd4d718 100644 --- a/Backend/src/ml/anomaly_detection.py +++ b/Backend/src/ml/anomaly_detection.py @@ -1,19 +1,16 @@ # Backend/src/ml/anomaly_detection.py import pandas as pd -import numpy as np from sklearn.ensemble import IsolationForest from .data_fetcher import fetch_recent_data -from ..Db.database_management import DatabaseManagement +from Backend.src.Db.database_management import DatabaseManagement async def detect_anomalies(db: DatabaseManagement, interval_seconds: int = 60, contamination: float = 0.1): - """Detect anomalies in the recent inventory and sales data using Isolation Forest.""" + """Detect anomalies in recent inventory and sales data using Isolation Forest.""" time_window_minutes = interval_seconds / 60 df = await fetch_recent_data(db, time_window_minutes) - # print("Data fetched for anomaly detection:", df) if df.empty: - # print("No data available for anomaly detection.") return df features = ["inventory_count", "inventory_change", "sales_count"] @@ -30,5 +27,4 @@ async def detect_anomalies(db: DatabaseManagement, interval_seconds: int = 60, c df["anomaly_score"] = iso_forest.decision_function(X) df["is_anomaly"] = df["anomaly_label"].apply(lambda x: True if x == -1 else False) - # print("Data with anomaly labels and scores:", df) return df \ No newline at end of file diff --git a/Backend/src/ml/data_fetcher.py b/Backend/src/ml/data_fetcher.py index 58ecd5c..ce07aa9 100644 --- a/Backend/src/ml/data_fetcher.py +++ b/Backend/src/ml/data_fetcher.py @@ -7,71 +7,68 @@ from Backend.src.Db.models import Product, ShelfInventory, Sale, Shelf async def fetch_recent_data(db: DatabaseManagement, time_window_minutes: int = 60) -> pd.DataFrame: - """Fetch recent shelf inventory and sales data from the database.""" + """Fetch recent shelf inventory and sales data from the database for anomaly detection.""" cutoff_time = datetime.now() - timedelta(minutes=time_window_minutes) - # print(f"Cutoff Time: {cutoff_time}") # Fetch Shelf Inventory shelf_inv_query = text(""" SELECT shelf_inventory_id, product_id, shelf_id, added_timestamp, removed_timestamp - FROM ShelfInventory - WHERE added_timestamp >= :cutoff OR removed_timestamp >= :cutoff + FROM shelf_inventory + WHERE added_timestamp >= :cutoff OR (removed_timestamp IS NOT NULL AND removed_timestamp >= :cutoff) ORDER BY added_timestamp, removed_timestamp """).bindparams(cutoff=cutoff_time) shelf_inv_results = await db.session.exec(shelf_inv_query) shelf_inv_df = pd.DataFrame(shelf_inv_results.mappings().all()) if shelf_inv_results else pd.DataFrame() - # print("Raw Shelf Inventory DataFrame:", shelf_inv_df) # Fetch Sales sales_query = text(""" SELECT product_id, sale_timestamp, COUNT(*) as sales_count - FROM Sale + FROM sale WHERE sale_timestamp >= :cutoff GROUP BY product_id, sale_timestamp """).bindparams(cutoff=cutoff_time) sales_results = await db.session.exec(sales_query) sales_df = pd.DataFrame(sales_results.mappings().all()) if sales_results else pd.DataFrame() - # print("Raw Sales DataFrame:", sales_df) # Fetch Products products = await db.search(Product, all_results=True) - products_df = pd.DataFrame([p.dict() for p in products]) - # print("Products DataFrame:", products_df) + products_df = pd.DataFrame([p.dict() for p in products]) if products else pd.DataFrame() # Fetch Shelves shelves = await db.search(Shelf, all_results=True) - shelves_df = pd.DataFrame([s.dict() for s in shelves]) - # print("Shelves DataFrame:", shelves_df) + shelves_df = pd.DataFrame([s.dict() for s in shelves]) if shelves else pd.DataFrame() # Preprocess Shelf Inventory Data if not shelf_inv_df.empty: - shelf_inv_df["stock_change"] = np.where(shelf_inv_df["added_timestamp"].notnull() & shelf_inv_df["removed_timestamp"].isnull(), 1, - np.where(shelf_inv_df["removed_timestamp"].notnull(), -1, 0)) + # Calculate stock change: +1 for added, -1 for removed + shelf_inv_df["stock_change"] = np.where( + shelf_inv_df["added_timestamp"].notnull() & shelf_inv_df["removed_timestamp"].isnull(), 1, + np.where(shelf_inv_df["removed_timestamp"].notnull(), -1, 0) + ) shelf_inv_df["timestamp"] = shelf_inv_df.apply( lambda row: row["removed_timestamp"] if pd.notnull(row["removed_timestamp"]) else row["added_timestamp"], axis=1 ) + + # Aggregate stock changes by product and timestamp shelf_inv_df = shelf_inv_df.groupby(["product_id", "shelf_id", "timestamp"])["stock_change"].sum().reset_index() shelf_inv_df["inventory_count"] = shelf_inv_df.groupby("product_id")["stock_change"].cumsum().fillna(0) - shelf_inv_df["inventory_change"] = shelf_inv_df.groupby("product_id")["inventory_count"].diff().fillna(0) - # print("After Preprocessing Shelf Inventory:", shelf_inv_df) + shelf_inv_df["inventory_change"] = shelf_inv_df.groupby("product_id")["inventory_count"].diff().fillna(shelf_inv_df["stock_change"]) + # Merge with product and shelf info shelf_inv_df = shelf_inv_df.merge(products_df[["product_id", "product_name"]], on="product_id", how="left") - # print("After Merging Products:", shelf_inv_df) - shelf_inv_df = shelf_inv_df.merge(shelves_df[["shelf_id", "shelf_location"]], on="shelf_id", how="left") - # print("After Merging Shelves:", shelf_inv_df) + # Merge with sales data if not sales_df.empty: - shelf_inv_df = shelf_inv_df.merge(sales_df[["sale_timestamp", "product_id", "sales_count"]], - left_on=["timestamp", "product_id"], - right_on=["sale_timestamp", "product_id"], - how="left") - # print("After Merging Sales:", shelf_inv_df) - + shelf_inv_df = shelf_inv_df.merge( + sales_df[["sale_timestamp", "product_id", "sales_count"]], + left_on=["timestamp", "product_id"], + right_on=["sale_timestamp", "product_id"], + how="left" + ) shelf_inv_df["sales_count"] = shelf_inv_df["sales_count"].fillna(0) - # print("Final Shelf Inventory DataFrame Before Return:", shelf_inv_df) - # Ensure the returned DataFrame has the expected columns, even if empty + # Define expected columns expected_columns = ["timestamp", "product_id", "product_name", "shelf_location", "inventory_count", "inventory_change", "sales_count"] if shelf_inv_df.empty: return pd.DataFrame(columns=expected_columns) diff --git a/Backend/src/ml/data_simulator.py b/Backend/src/ml/data_simulator.py deleted file mode 100644 index 683f5d5..0000000 --- a/Backend/src/ml/data_simulator.py +++ /dev/null @@ -1,31 +0,0 @@ - -import asyncio -from datetime import datetime, timedelta -from Backend.src.Db.database_management import DatabaseManagement -from Backend.src.Db.models import ShelfInventory, Sale, Product, Supplier, Shelf - -async def simulate_data_insertion(db: DatabaseManagement): - """Simulate real-time data insertion for hackathon demo.""" - base_time = datetime(2025, 3, 5, 10, 0) - for i in range(10): - time = base_time + timedelta(minutes=i * 5) - if i % 3 == 0: - shelf_inv = ShelfInventory( - shelf_inventory_id=f"SI{i+10}", - shelf_id="S1", - product_id="P1", - added_timestamp=base_time, - removed_timestamp=time - ) - else: - shelf_inv = ShelfInventory( - shelf_inventory_id=f"SI{i+10}", - shelf_id="S1", - product_id="P1", - added_timestamp=time, - removed_timestamp=None - ) - sale = Sale(sale_id=f"S{i+10}", product_id="P1", sale_timestamp=time) - await db.insert(sale) - await db.insert(shelf_inv) - await asyncio.sleep(2) \ No newline at end of file diff --git a/Backend/src/ml/db_init.py b/Backend/src/ml/db_init.py index dfb4553..8e41c72 100644 --- a/Backend/src/ml/db_init.py +++ b/Backend/src/ml/db_init.py @@ -1,4 +1,4 @@ -# Backend/src/db_init.py +# Backend/src/ml/db_init.py from sqlalchemy.ext.asyncio import AsyncEngine from sqlmodel import SQLModel diff --git a/Backend/src/ml/main.py b/Backend/src/ml/main.py deleted file mode 100644 index f4ab47d..0000000 --- a/Backend/src/ml/main.py +++ /dev/null @@ -1,27 +0,0 @@ - -import asyncio -from Backend.src.Db.database_management import DatabaseManagement -from Backend.src.Db.db import get_session -from Backend.src.ml.anomaly_detection import detect_anomalies -from Backend.src.ml.data_simulator import simulate_data_insertion -from Backend.src.Db.models import Product, Supplier, Shelf - -async def main(): - - async for session in get_session(): - db = DatabaseManagement(session) - - await db.insert(Product(product_id="P1", rfid_tag="RFID1", product_name="Rice 5kg", status="available", supplier_id="SUP1")) - await db.insert(Product(product_id="P2", rfid_tag="RFID2", product_name="Toothpaste", status="available", supplier_id="SUP1")) - await db.insert(Supplier(supplier_id="SUP1", supplier_name="Supplier A")) - await db.insert(Shelf(shelf_id="S1", shelf_location="Aisle 1, Shelf 2")) - - - sim_task = asyncio.create_task(simulate_data_insertion(db)) - detect_task = asyncio.create_task(detect_anomalies(db, interval_seconds=10)) - - await sim_task - await detect_task - -if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file diff --git a/Backend/src/ml/test_main.py b/Backend/src/ml/test_main.py index 3adbe0a..589bb00 100644 --- a/Backend/src/ml/test_main.py +++ b/Backend/src/ml/test_main.py @@ -1,72 +1,22 @@ # Backend/src/ml/test_main.py import asyncio -import json import pandas as pd from sqlalchemy.ext.asyncio import create_async_engine from sqlmodel.ext.asyncio.session import AsyncSession from sqlalchemy.orm import sessionmaker -from .db_init import initialize_database -from ..Db.database_management import DatabaseManagement +from Backend.src.Db.db import async_engine, get_session # Use actual database engine +from Backend.src.Db.database_management import DatabaseManagement from .anomaly_detection import detect_anomalies -from .data_simulator import simulate_data_insertion -from ..Db.models import Product, Supplier, Shelf, ShelfInventory, Sale - -# In-memory SQLite engine for testing -test_engine = create_async_engine("sqlite+aiosqlite:///:memory:") - -async def get_test_session() -> AsyncSession: - """Provide a session for the in-memory SQLite database.""" - async_session = sessionmaker(bind=test_engine, class_=AsyncSession, expire_on_commit=False) - async with async_session() as session: - yield session - -async def load_json_data(db: DatabaseManagement, json_file: str): - """Load dummy data from JSON into the in-memory database.""" - with open(json_file, 'r') as f: - data = json.load(f) - - # Insert Suppliers - for supplier in data.get("Supplier", []): - await db.insert(Supplier(**supplier)) - - # Insert Products - for product in data.get("Product", []): - await db.insert(Product(**product)) - - # Insert Shelves - for shelf in data.get("Shelf", []): - await db.insert(Shelf(**shelf)) - - # Insert Shelf Inventory - for shelf_inv in data.get("ShelfInventory", []): - shelf_inv["added_timestamp"] = pd.to_datetime(shelf_inv["added_timestamp"]) - if shelf_inv["removed_timestamp"]: - shelf_inv["removed_timestamp"] = pd.to_datetime(shelf_inv["removed_timestamp"]) - await db.insert(ShelfInventory(**shelf_inv)) - - # Insert Sales - for sale in data.get("Sale", []): - sale["sale_timestamp"] = pd.to_datetime(sale["sale_timestamp"]) - await db.insert(Sale(**sale)) - - # Commit the session - await db.session.commit() async def main(): - # Initialize in-memory database - await initialize_database(test_engine) - - async for session in get_test_session(): + # Use the actual database engine (no need to initialize tables here, handled in src/main.py) + async for session in get_session(): db = DatabaseManagement(session) - # Load dummy data from JSON - json_path = r"E:\TheftBlock\TheftBlock\Backend\dummy_data\warehouse_to_customer.json" - await load_json_data(db, json_path) - # Run anomaly detection with a 48-hour window result_df = await detect_anomalies(db, interval_seconds=172800) # 48 hours - # Filter anomalies and select only desired columns + # Filter anomalies and select desired columns if not result_df.empty: anomalies = result_df[result_df["is_anomaly"]][["product_id", "product_name", "shelf_location"]] print(anomalies if not anomalies.empty else "No anomalies detected.") From 95756841ba53929bd66209b330b92ea6b5e5b021 Mon Sep 17 00:00:00 2001 From: aditya-d-d Date: Thu, 6 Mar 2025 06:26:34 +0530 Subject: [PATCH 3/3] test Frontend --- Frontend/TheftBlock/package-lock.json | 1218 ++++++++++++++++- Frontend/TheftBlock/package.json | 12 +- Frontend/TheftBlock/src/App.jsx | 53 +- .../src/components/AddProductForm.jsx | 131 ++ .../src/components/AddSupplierForm.jsx | 79 ++ .../src/components/DiscrepancyTable.jsx | 34 + .../TheftBlock/src/components/Filters.jsx | 62 + Frontend/TheftBlock/src/components/Graphs.jsx | 92 ++ .../src/components/InventoryCard.jsx | 30 + .../src/components/InventoryChart.js | 39 + .../TheftBlock/src/components/ItemTable.jsx | 166 +++ .../TheftBlock/src/components/KeyMetrics.jsx | 54 + Frontend/TheftBlock/src/components/Layout.jsx | 14 + Frontend/TheftBlock/src/components/Navbar.js | 15 + .../src/components/NavigationBar.jsx | 80 ++ .../src/components/ProductTable.jsx | 50 + .../TheftBlock/src/components/RFIDScanLog.jsx | 48 + .../src/components/RemainingProductsTable.jsx | 46 + Frontend/TheftBlock/src/components/Summary.js | 32 + .../src/components/SupplierTable.jsx | 46 + .../src/components/TheftDetailsTable.jsx | 48 + .../src/components/TheftDetectionSection.jsx | 40 + .../TheftBlock/src/components/TheftStatus.jsx | 24 + .../src/context/SelectedInventoryProvider.jsx | 16 + Frontend/TheftBlock/src/main.jsx | 17 +- Frontend/TheftBlock/src/pages/Dashboard.jsx | 69 + .../TheftBlock/src/pages/InventoryDetail.jsx | 84 ++ .../TheftBlock/src/pages/InventoryList.jsx | 30 + Frontend/TheftBlock/src/pages/Products.jsx | 64 + Frontend/TheftBlock/src/pages/Suppliers.jsx | 53 + .../TheftBlock/src/pages/TheftDetection.jsx | 74 + Frontend/TheftBlock/src/services/api.jsx | 159 +++ 32 files changed, 2909 insertions(+), 70 deletions(-) create mode 100644 Frontend/TheftBlock/src/components/AddProductForm.jsx create mode 100644 Frontend/TheftBlock/src/components/AddSupplierForm.jsx create mode 100644 Frontend/TheftBlock/src/components/DiscrepancyTable.jsx create mode 100644 Frontend/TheftBlock/src/components/Filters.jsx create mode 100644 Frontend/TheftBlock/src/components/Graphs.jsx create mode 100644 Frontend/TheftBlock/src/components/InventoryCard.jsx create mode 100644 Frontend/TheftBlock/src/components/InventoryChart.js create mode 100644 Frontend/TheftBlock/src/components/ItemTable.jsx create mode 100644 Frontend/TheftBlock/src/components/KeyMetrics.jsx create mode 100644 Frontend/TheftBlock/src/components/Layout.jsx create mode 100644 Frontend/TheftBlock/src/components/Navbar.js create mode 100644 Frontend/TheftBlock/src/components/NavigationBar.jsx create mode 100644 Frontend/TheftBlock/src/components/ProductTable.jsx create mode 100644 Frontend/TheftBlock/src/components/RFIDScanLog.jsx create mode 100644 Frontend/TheftBlock/src/components/RemainingProductsTable.jsx create mode 100644 Frontend/TheftBlock/src/components/Summary.js create mode 100644 Frontend/TheftBlock/src/components/SupplierTable.jsx create mode 100644 Frontend/TheftBlock/src/components/TheftDetailsTable.jsx create mode 100644 Frontend/TheftBlock/src/components/TheftDetectionSection.jsx create mode 100644 Frontend/TheftBlock/src/components/TheftStatus.jsx create mode 100644 Frontend/TheftBlock/src/context/SelectedInventoryProvider.jsx create mode 100644 Frontend/TheftBlock/src/pages/Dashboard.jsx create mode 100644 Frontend/TheftBlock/src/pages/InventoryDetail.jsx create mode 100644 Frontend/TheftBlock/src/pages/InventoryList.jsx create mode 100644 Frontend/TheftBlock/src/pages/Products.jsx create mode 100644 Frontend/TheftBlock/src/pages/Suppliers.jsx create mode 100644 Frontend/TheftBlock/src/pages/TheftDetection.jsx create mode 100644 Frontend/TheftBlock/src/services/api.jsx diff --git a/Frontend/TheftBlock/package-lock.json b/Frontend/TheftBlock/package-lock.json index dfa0d30..cb323ac 100644 --- a/Frontend/TheftBlock/package-lock.json +++ b/Frontend/TheftBlock/package-lock.json @@ -8,8 +8,18 @@ "name": "theftblock", "version": "0.0.0", "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@mui/icons-material": "^6.4.7", + "@mui/material": "^6.4.7", + "@mui/x-date-pickers": "^7.27.1", + "axios": "^1.8.1", + "chart.js": "^4.4.8", + "dayjs": "^1.11.13", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-chartjs-2": "^5.3.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.2.0" }, "devDependencies": { "@eslint/js": "^9.21.0", @@ -41,7 +51,6 @@ "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", @@ -97,7 +106,6 @@ "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", "integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.26.9", @@ -131,7 +139,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.25.9", @@ -173,7 +180,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -183,7 +189,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -217,7 +222,6 @@ "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.26.9" @@ -261,11 +265,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz", + "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", @@ -280,7 +295,6 @@ "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz", "integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", @@ -299,7 +313,6 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -309,7 +322,6 @@ "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", @@ -319,6 +331,158 @@ "node": ">=6.9.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", + "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", + "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", @@ -955,7 +1119,6 @@ "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", @@ -970,7 +1133,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -980,7 +1142,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -990,20 +1151,350 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.7.tgz", + "integrity": "sha512-XjJrKFNt9zAKvcnoIIBquXyFyhfrHYuttqMsoDS7lM7VwufYG4fAPw4kINjBFg++fqXM2BNAuWR9J7XVIuKIKg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.7.tgz", + "integrity": "sha512-Rk8cs9ufQoLBw582Rdqq7fnSXXZTqhYRbpe1Y5SAz9lJKZP3CIdrj0PfG8HJLGw1hrsHFN/rkkm70IDzhJsG1g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^6.4.7", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.7.tgz", + "integrity": "sha512-K65StXUeGAtFJ4ikvHKtmDCO5Ab7g0FZUu2J5VpoKD+O6Y3CjLYzRi+TMlI3kaL4CL158+FccMoOd/eaddmeRQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/core-downloads-tracker": "^6.4.7", + "@mui/system": "^6.4.7", + "@mui/types": "^7.2.21", + "@mui/utils": "^6.4.6", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^19.0.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^6.4.7", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.6.tgz", + "integrity": "sha512-T5FxdPzCELuOrhpA2g4Pi6241HAxRwZudzAuL9vBvniuB5YU82HCmrARw32AuCiyTfWzbrYGGpZ4zyeqqp9RvQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/utils": "^6.4.6", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.6.tgz", + "integrity": "sha512-vSWYc9ZLX46be5gP+FCzWVn5rvDr4cXC5JBZwSIkYk9xbC7GeV+0kCvB8Q6XLFQJy+a62bbqtmdwS4Ghi9NBlQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.7.tgz", + "integrity": "sha512-7wwc4++Ak6tGIooEVA9AY7FhH2p9fvBMORT4vNLMAysH3Yus/9B9RYMbrn3ANgsOyvT3Z7nE+SP8/+3FimQmcg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/private-theming": "^6.4.6", + "@mui/styled-engine": "^6.4.6", + "@mui/types": "^7.2.21", + "@mui/utils": "^6.4.6", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.21", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.21.tgz", + "integrity": "sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.6.tgz", + "integrity": "sha512-43nZeE1pJF2anGafNydUcYFPtHwAqiBiauRtaMvurdrZI3YrUjHkAu43RBsxef7OFtJMXGiHFvq43kb7lig0sA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/types": "^7.2.21", + "@types/prop-types": "^15.7.14", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-date-pickers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.27.1.tgz", + "integrity": "sha512-2YPhTM9TM39dmIkEQdSB6P6NASePB9LuhXXKQqq0PX4FXGymYEPz/acQXkk617zwfxJJaDhJZ6g8SAv5pklTJQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0", + "@mui/x-internals": "7.26.0", + "@types/react-transition-group": "^4.4.11", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0", + "@mui/system": "^5.15.14 || ^6.0.0", + "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0", + "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2 || ^3.0.0", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, + "node_modules/@mui/x-internals": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.26.0.tgz", + "integrity": "sha512-VxTCYQcZ02d3190pdvys2TDg9pgbvewAVakEopiOgReKAUhLdRlgGJHcOA/eAuGLyK1YIo26A6Ow6ZKlSRLwMg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.34.9", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.9.tgz", @@ -1315,6 +1806,12 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -1329,11 +1826,22 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.0.10", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1349,9 +1857,18 @@ "@types/react": "^19.0.0" } }, - "node_modules/@vitejs/plugin-react": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", "dev": true, "license": "MIT", @@ -1432,6 +1949,38 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz", + "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1483,11 +2032,23 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -1531,6 +2092,27 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.8.tgz", + "integrity": "sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1551,6 +2133,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1565,6 +2159,40 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1584,14 +2212,18 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", "license": "MIT" }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1612,6 +2244,39 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.112", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.112.tgz", @@ -1619,6 +2284,60 @@ "dev": true, "license": "ISC" }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", @@ -1674,7 +2393,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -1894,6 +2612,12 @@ "node": ">=16.0.0" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -1932,6 +2656,41 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1947,6 +2706,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -1957,6 +2725,43 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -1983,6 +2788,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1993,6 +2810,60 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2007,7 +2878,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -2030,6 +2900,27 @@ "node": ">=0.8.19" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2064,7 +2955,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -2084,7 +2974,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -2100,6 +2989,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2151,6 +3046,12 @@ "node": ">= 0.8.0" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2174,6 +3075,18 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2184,6 +3097,36 @@ "yallist": "^3.0.2" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2201,7 +3144,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -2237,6 +3179,15 @@ "dev": true, "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2291,7 +3242,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -2300,6 +3250,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2320,11 +3288,25 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/postcss": { @@ -2366,6 +3348,29 @@ "node": ">= 0.8.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2385,6 +3390,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", @@ -2397,6 +3412,12 @@ "react": "^19.0.0" } }, + "node_modules/react-is": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz", + "integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -2407,11 +3428,92 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.2.0.tgz", + "integrity": "sha512-fXyqzPgCPZbqhrk7k3hPcCpYIlQ2ugIXDboHUzhJISFVy2DEPsmHgN588MyGmkIOv3jDgNfUE3kJi83L28s/LQ==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.2.0.tgz", + "integrity": "sha512-cU7lTxETGtQRQbafJubvZKHEn5izNABxZhBY0Jlzdv0gqQhCPQt2J8aN5ZPjS6mQOXn5NnirWNh+FpE8TTYN0Q==", + "license": "MIT", + "dependencies": { + "react-router": "7.2.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -2472,6 +3574,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2495,6 +3603,15 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2518,6 +3635,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2531,6 +3654,24 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "license": "ISC" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -2690,6 +3831,21 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/Frontend/TheftBlock/package.json b/Frontend/TheftBlock/package.json index 044310d..b1baefd 100644 --- a/Frontend/TheftBlock/package.json +++ b/Frontend/TheftBlock/package.json @@ -10,8 +10,18 @@ "preview": "vite preview" }, "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@mui/icons-material": "^6.4.7", + "@mui/material": "^6.4.7", + "@mui/x-date-pickers": "^7.27.1", + "axios": "^1.8.1", + "chart.js": "^4.4.8", + "dayjs": "^1.11.13", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-chartjs-2": "^5.3.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.2.0" }, "devDependencies": { "@eslint/js": "^9.21.0", diff --git a/Frontend/TheftBlock/src/App.jsx b/Frontend/TheftBlock/src/App.jsx index f67355a..81e0b4f 100644 --- a/Frontend/TheftBlock/src/App.jsx +++ b/Frontend/TheftBlock/src/App.jsx @@ -1,35 +1,28 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' +// src/App.jsx +import React from 'react'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import Layout from './components/Layout'; +import Dashboard from './pages/Dashboard'; +import Suppliers from './pages/Suppliers'; +import Products from './pages/Products'; +import TheftDetection from './pages/TheftDetection'; +import { SelectedInventoryProvider } from './context/SelectedInventoryProvider'; function App() { - const [count, setCount] = useState(0) - return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.jsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- - ) + + + + }> + } /> + } /> + } /> + } /> + + + + + ); } -export default App +export default App; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/components/AddProductForm.jsx b/Frontend/TheftBlock/src/components/AddProductForm.jsx new file mode 100644 index 0000000..d98bd3d --- /dev/null +++ b/Frontend/TheftBlock/src/components/AddProductForm.jsx @@ -0,0 +1,131 @@ +// src/components/AddProductForm.jsx +import React, { useState } from 'react'; +import { Grid, TextField, Button } from '@mui/material'; +import { addProduct } from '../services/api'; + +function AddProductForm({ suppliers, onAdd }) { + const [formData, setFormData] = useState({ + name: '', + total: '', + actual_remaining: '', + price: '', + rfid: '', + supplierId: '', + sold: 0, + }); + + const handleChange = (e) => { + setFormData({ ...formData, [e.target.name]: e.target.value }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + try { + const newProduct = await addProduct({ + ...formData, + id: `prod${Date.now()}`, + total: parseInt(formData.total), + actual_remaining: parseInt(formData.actual_remaining), + price: parseFloat(formData.price), + sold: parseInt(formData.sold), + }); + onAdd(newProduct); + setFormData({ name: '', total: '', actual_remaining: '', price: '', rfid: '', supplierId: '', sold: 0 }); + } catch (error) { + console.error('Error adding product:', error); + } + }; + + return ( +
+
+ + + + + + + + + + + + + + + + + + + + {suppliers.map((supplier) => ( + + ))} + + + + + + +
+
+ ); +} + +export default AddProductForm; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/components/AddSupplierForm.jsx b/Frontend/TheftBlock/src/components/AddSupplierForm.jsx new file mode 100644 index 0000000..6d285d5 --- /dev/null +++ b/Frontend/TheftBlock/src/components/AddSupplierForm.jsx @@ -0,0 +1,79 @@ +// src/components/AddSupplierForm.jsx +import React, { useState } from 'react'; +import { Grid, TextField, Button } from '@mui/material'; +import { addSupplier } from '../services/api'; + +function AddSupplierForm({ onAdd }) { + const [formData, setFormData] = useState({ + name: '', + contact: '', + address: '', + }); + + const handleChange = (e) => { + setFormData({ ...formData, [e.target.name]: e.target.value }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + try { + const newSupplier = await addSupplier({ + ...formData, + id: `sup${Date.now()}`, + }); + onAdd(newSupplier); + setFormData({ name: '', contact: '', address: '' }); + } catch (error) { + console.error('Error adding supplier:', error); + } + }; + + return ( +
+
+ + + + + + + + + + + + + + +
+
+ ); +} + +export default AddSupplierForm; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/components/DiscrepancyTable.jsx b/Frontend/TheftBlock/src/components/DiscrepancyTable.jsx new file mode 100644 index 0000000..9b7c676 --- /dev/null +++ b/Frontend/TheftBlock/src/components/DiscrepancyTable.jsx @@ -0,0 +1,34 @@ +// src/components/DiscrepancyTable.jsx +import React from 'react'; +import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper } from '@mui/material'; + +function DiscrepancyTable({ discrepancies }) { + return ( + + + + + Date + Item ID + Item Name + Discrepancy + Financial Loss (₹) + + + + {discrepancies.map((d, index) => ( + + {d.date} + {d.itemId} + {d.itemName} + {d.discrepancy} + {d.loss.toFixed(2)} + + ))} + +
+
+ ); +} + +export default DiscrepancyTable; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/components/Filters.jsx b/Frontend/TheftBlock/src/components/Filters.jsx new file mode 100644 index 0000000..00a9b39 --- /dev/null +++ b/Frontend/TheftBlock/src/components/Filters.jsx @@ -0,0 +1,62 @@ +// src/components/Filters.js +import React, { useState } from 'react'; +import { Grid, TextField, Button } from '@mui/material'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import dayjs from 'dayjs'; + +function Filters({ onFilterChange, products }) { + const [dateStart, setDateStart] = useState(null); + const [dateEnd, setDateEnd] = useState(null); + const [product, setProduct] = useState(''); + + const handleApply = () => { + onFilterChange({ dateStart, dateEnd, product }); + }; + + return ( + + + + setDateStart(newValue)} + renderInput={(params) => } + /> + + + setDateEnd(newValue)} + renderInput={(params) => } + /> + + + setProduct(e.target.value)} + SelectProps={{ native: true }} + fullWidth + > + + {products.map((p) => ( + + ))} + + + + + + + + ); +} + +export default Filters; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/components/Graphs.jsx b/Frontend/TheftBlock/src/components/Graphs.jsx new file mode 100644 index 0000000..3f1d795 --- /dev/null +++ b/Frontend/TheftBlock/src/components/Graphs.jsx @@ -0,0 +1,92 @@ +// src/components/Graphs.jsx +import React from 'react'; +import { Bar, Line } from 'react-chartjs-2'; +import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, LineElement, PointElement, Title, Tooltip, Legend } from 'chart.js'; + +ChartJS.register(CategoryScale, LinearScale, BarElement, LineElement, PointElement, Title, Tooltip, Legend); + +function Graphs({ products }) { + // Debug log to check props + console.log('Graphs products:', products); + + // Ensure products is an array + const productsList = Array.isArray(products) ? products : []; + + // Bar Chart: Discrepancies per Item + const discrepancyData = { + labels: productsList.map(p => p.name), + datasets: [ + { + label: 'Discrepancy (Units Missing)', + data: productsList.map(p => (p.total - p.sold) - p.actual_remaining), + backgroundColor: productsList.map(p => ((p.total - p.sold) - p.actual_remaining) > 0 ? 'rgba(255, 99, 132, 0.5)' : 'rgba(75, 192, 192, 0.5)'), + borderColor: productsList.map(p => ((p.total - p.sold) - p.actual_remaining) > 0 ? 'rgba(255, 99, 132, 1)' : 'rgba(75, 192, 192, 1)'), + borderWidth: 1, + }, + ], + }; + + const discrepancyOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { position: 'top' }, + title: { display: true, text: 'Discrepancies per Item' }, + zoom: { pan: { enabled: true }, zoom: { enabled: true } }, + }, + scales: { + y: { beginAtZero: true }, + }, + }; + + // Line Chart: Inventory Levels Over Time (Mock Data) + const inventoryTrendData = { + labels: ['Week 1', 'Week 2', 'Week 3', 'Week 4'], + datasets: [ + { + label: 'Total Remaining Items', + data: [productsList.reduce((sum, p) => sum + p.actual_remaining, 0), 250, 240, 230], // Mock trend data + borderColor: 'rgba(75, 192, 192, 1)', + backgroundColor: 'rgba(75, 192, 192, 0.5)', + fill: false, + }, + ], + }; + + const trendOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { position: 'top' }, + title: { display: true, text: 'Inventory Levels Over Time' }, + zoom: { pan: { enabled: true }, zoom: { enabled: true } }, + }, + scales: { + y: { beginAtZero: true }, + }, + }; + + return ( +
+ + Visual Analytics + + {productsList.length > 0 ? ( + <> +
+ +
+
+ +
+ + ) : ( + + No products available for visualization. + + )} +
+ ); +} + +export default Graphs; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/components/InventoryCard.jsx b/Frontend/TheftBlock/src/components/InventoryCard.jsx new file mode 100644 index 0000000..4384f27 --- /dev/null +++ b/Frontend/TheftBlock/src/components/InventoryCard.jsx @@ -0,0 +1,30 @@ +// src/components/InventoryCard.jsx +import React from 'react'; +import { Card, CardContent, Typography, Button } from '@mui/material'; +import { useNavigate } from 'react-router-dom'; + +function InventoryCard({ inventory }) { + const navigate = useNavigate(); + const totalDiscrepancies = inventory.discrepancies.reduce((sum, d) => sum + Math.abs(d.discrepancy), 0); + const totalLoss = inventory.discrepancies.reduce((sum, d) => sum + d.loss, 0); + + return ( + + + {inventory.name} + Total Items: {inventory.items.reduce((sum, i) => sum + i.total, 0)} + Discrepancies: {totalDiscrepancies} + Loss: ₹{totalLoss.toFixed(2)} + + + + ); +} + +export default InventoryCard; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/components/InventoryChart.js b/Frontend/TheftBlock/src/components/InventoryChart.js new file mode 100644 index 0000000..eccf430 --- /dev/null +++ b/Frontend/TheftBlock/src/components/InventoryChart.js @@ -0,0 +1,39 @@ +// src/components/InventoryChart.js +import React from 'react'; +import { Bar } from 'react-chartjs-2'; +import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js'; + +ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); + +function InventoryChart({ chartData }) { + const data = { + labels: chartData.dates, + datasets: [ + { + label: 'Daily Financial Loss (₹)', + data: chartData.losses, + backgroundColor: 'rgba(255, 99, 132, 0.5)', + borderColor: 'rgba(255, 99, 132, 1)', + borderWidth: 1, + }, + ], + }; + + const options = { + responsive: true, + plugins: { + legend: { position: 'top' }, + title: { + display: true, + text: 'Daily Financial Loss Due to Discrepancies', + }, + }, + scales: { + y: { beginAtZero: true }, + }, + }; + + return ; +} + +export default InventoryChart; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/components/ItemTable.jsx b/Frontend/TheftBlock/src/components/ItemTable.jsx new file mode 100644 index 0000000..5c56b16 --- /dev/null +++ b/Frontend/TheftBlock/src/components/ItemTable.jsx @@ -0,0 +1,166 @@ +// src/components/ItemTable.jsx +import React, { useState, useMemo } from 'react'; +import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, TextField, TableSortLabel } from '@mui/material'; + +function ItemTable({ products }) { + // Debug log to check props + console.log('ItemTable products:', products); + + const [search, setSearch] = useState(''); + const [order, setOrder] = useState('asc'); + const [orderBy, setOrderBy] = useState('id'); + + // Ensure products is an array + const productsList = Array.isArray(products) ? products : []; + + const handleRequestSort = (property) => { + const isAsc = orderBy === property && order === 'asc'; + setOrder(isAsc ? 'desc' : 'asc'); + setOrderBy(property); + }; + + const sortedProducts = useMemo(() => { + const comparator = (a, b) => { + const aValue = orderBy === 'discrepancy' ? ((a.total - a.sold) - a.actual_remaining) : a[orderBy]; + const bValue = orderBy === 'discrepancy' ? ((b.total - b.sold) - b.actual_remaining) : b[orderBy]; + if (aValue < bValue) return order === 'asc' ? -1 : 1; + if (aValue > bValue) return order === 'asc' ? 1 : -1; + return 0; + }; + return [...productsList].sort(comparator); + }, [productsList, order, orderBy]); + + const filteredProducts = sortedProducts.filter((product) => + product.name.toLowerCase().includes(search.toLowerCase()) || + product.rfid.toLowerCase().includes(search.toLowerCase()) + ); + + return ( + <> + setSearch(e.target.value)} + fullWidth + sx={{ mt: 2, mb: 2 }} + /> + + + + + + handleRequestSort('id')} + > + Item ID + + + + handleRequestSort('name')} + > + Item Name + + + + handleRequestSort('rfid')} + > + RFID + + + + handleRequestSort('total')} + > + Total Quantity + + + + handleRequestSort('sold')} + > + Sold Quantity + + + + Expected Remaining + + + handleRequestSort('actual_remaining')} + > + Actual Remaining + + + + handleRequestSort('discrepancy')} + > + Discrepancy + + + + handleRequestSort('price')} + > + Price (₹) + + + + Loss (₹) + + + + + {filteredProducts.length > 0 ? ( + filteredProducts.map((product) => { + const expectedRemaining = product.total - product.sold; + const discrepancy = expectedRemaining - product.actual_remaining; + const loss = discrepancy > 0 ? discrepancy * product.price : 0; + return ( + + {product.id} + {product.name} + {product.rfid} + {product.total} + {product.sold} + {expectedRemaining} + {product.actual_remaining} + 0 ? '#d32f2f' : '#333' }}>{discrepancy} + {product.price.toFixed(2)} + 0 ? '#d32f2f' : '#333' }}>{loss.toFixed(2)} + + ); + }) + ) : ( + + + No items available. + + + )} + +
+
+ + ); +} + +export default ItemTable; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/components/KeyMetrics.jsx b/Frontend/TheftBlock/src/components/KeyMetrics.jsx new file mode 100644 index 0000000..d2008c3 --- /dev/null +++ b/Frontend/TheftBlock/src/components/KeyMetrics.jsx @@ -0,0 +1,54 @@ +// src/components/KeyMetrics.jsx +import React from 'react'; +import { Grid, Paper, Typography } from '@mui/material'; + +function KeyMetrics({ products, meta }) { + const totalItems = products.length; + const remainingItems = products.reduce((sum, p) => sum + p.actual_remaining, 0); + const soldItems = products.reduce((sum, p) => sum + p.sold, 0); + const discrepancies = products.reduce((count, p) => ((p.total - p.sold) - p.actual_remaining) > 0 ? count + 1 : count, 0); + const financialLoss = products.reduce( + (sum, p) => ((p.total - p.sold) - p.actual_remaining) > 0 ? sum + (((p.total - p.sold) - p.actual_remaining) * p.price) : sum, + 0 + ); + + return ( + + + + Total Items + {totalItems} + + Total in Data: {meta.totalProducts || 'N/A'} + + + + + + Remaining Items + {remainingItems} + + + + + Sold Items + {soldItems} + + + + + Discrepancies + 0 ? '#d32f2f' : '#388e3c' }}>{discrepancies} + + + + + Financial Loss (₹) + 0 ? '#d32f2f' : '#388e3c' }}>{financialLoss.toFixed(2)} + + + + ); +} + +export default KeyMetrics; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/components/Layout.jsx b/Frontend/TheftBlock/src/components/Layout.jsx new file mode 100644 index 0000000..971e798 --- /dev/null +++ b/Frontend/TheftBlock/src/components/Layout.jsx @@ -0,0 +1,14 @@ +// src/components/Layout.jsx +import React from 'react'; +import NavigationBar from './NavigationBar'; + +const Layout = ({ children }) => { + return ( +
+ + {children} +
+ ); +}; + +export default Layout; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/components/Navbar.js b/Frontend/TheftBlock/src/components/Navbar.js new file mode 100644 index 0000000..4176335 --- /dev/null +++ b/Frontend/TheftBlock/src/components/Navbar.js @@ -0,0 +1,15 @@ +// src/components/Navbar.js +import React from 'react'; +import { AppBar, Toolbar, Typography } from '@mui/material'; + +function Navbar() { + return ( + + + Inventory Theft Detection Dashboard + + + ); +} + +export default Navbar; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/components/NavigationBar.jsx b/Frontend/TheftBlock/src/components/NavigationBar.jsx new file mode 100644 index 0000000..4363c50 --- /dev/null +++ b/Frontend/TheftBlock/src/components/NavigationBar.jsx @@ -0,0 +1,80 @@ +// src/components/NavigationBar.jsx +import React, { useState, useEffect } from 'react'; +import { AppBar, Toolbar, Typography, IconButton, Drawer, List, ListItem, ListItemText } from '@mui/material'; +import MenuIcon from '@mui/icons-material/Menu'; +import { getInventories } from '../services/api'; +import { useSelectedInventory } from '../context/SelectedInventoryProvider'; + +function NavigationBar() { + const [open, setOpen] = useState(false); + const [inventories, setInventories] = useState([]); + const { selectedInventoryId, setSelectedInventoryId } = useSelectedInventory(); + + useEffect(() => { + const fetchInventories = async () => { + try { + const data = await getInventories(); + console.log('Fetched Inventories in NavigationBar:', data); // Debug log + // Ensure data is an array + const inventoryList = Array.isArray(data) ? data : []; + setInventories(inventoryList); + // Set default inventory if none is selected + if (inventoryList.length > 0 && !selectedInventoryId) { + setSelectedInventoryId(inventoryList[0].id); + console.log('Set default inventory ID:', inventoryList[0].id); // Debug log + } + } catch (err) { + console.error('Error fetching inventories:', err); + setInventories([]); + } + }; + fetchInventories(); + }, [setSelectedInventoryId, selectedInventoryId]); + + const toggleDrawer = () => { + setOpen(!open); + }; + + const handleInventorySelect = (id) => { + setSelectedInventoryId(id); + console.log('Selected inventory ID:', id); // Debug log + setOpen(false); + }; + + return ( + <> + + + + + + + TheftBlock Dashboard + + + + + + {inventories.length > 0 ? ( + inventories.map((inventory) => ( + handleInventorySelect(inventory.id)} + sx={{ '&:hover': { backgroundColor: '#e0e0e0' } }} + > + + + )) + ) : ( + + + + )} + + + + ); +} + +export default NavigationBar; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/components/ProductTable.jsx b/Frontend/TheftBlock/src/components/ProductTable.jsx new file mode 100644 index 0000000..52ea1cd --- /dev/null +++ b/Frontend/TheftBlock/src/components/ProductTable.jsx @@ -0,0 +1,50 @@ +// src/components/ProductTable.jsx +import React from 'react'; +import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper } from '@mui/material'; + +function ProductTable({ products }) { + // Debug log to check props + console.log('ProductTable products:', products); + + // Ensure products is an array + const productsList = Array.isArray(products) ? products : []; + + return ( + + + + + ID + Name + Total Quantity + Remaining Quantity + Price (₹) + Supplier ID + + + + {productsList.length > 0 ? ( + productsList.map((product) => ( + + {product.id} + {product.name} + {product.total} + {product.actual_remaining} + {product.price.toFixed(2)} + {product.supplierId} + + )) + ) : ( + + + No products available. + + + )} + +
+
+ ); +} + +export default ProductTable; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/components/RFIDScanLog.jsx b/Frontend/TheftBlock/src/components/RFIDScanLog.jsx new file mode 100644 index 0000000..db9bb11 --- /dev/null +++ b/Frontend/TheftBlock/src/components/RFIDScanLog.jsx @@ -0,0 +1,48 @@ +// src/components/RFIDScanLog.jsx +import React from 'react'; +import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper } from '@mui/material'; + +function RFIDScanLog({ scanLogs }) { + // Debug log to check props + console.log('RFIDScanLog scanLogs:', scanLogs); + + // Ensure scanLogs is an array + const scanLogsList = Array.isArray(scanLogs) ? scanLogs : []; + + return ( + + + + + Product ID + Product Name + Scanned At + Status + + + + {scanLogsList.length > 0 ? ( + scanLogsList.map((log, index) => ( + + {log.productId} + {log.productName} + {new Date(log.scannedAt).toLocaleString()} + + {log.status} + + + )) + ) : ( + + + No scan logs available. + + + )} + +
+
+ ); +} + +export default RFIDScanLog; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/components/RemainingProductsTable.jsx b/Frontend/TheftBlock/src/components/RemainingProductsTable.jsx new file mode 100644 index 0000000..211f942 --- /dev/null +++ b/Frontend/TheftBlock/src/components/RemainingProductsTable.jsx @@ -0,0 +1,46 @@ +// src/components/RemainingProductsTable.jsx +import React from 'react'; +import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper } from '@mui/material'; + +function RemainingProductsTable({ products }) { + // Debug log to check props + console.log('RemainingProductsTable products:', products); + + // Ensure products is an array + const productsList = Array.isArray(products) ? products : []; + + return ( + + + + + Product ID + Name + Remaining Quantity + Supplier ID + + + + {productsList.length > 0 ? ( + productsList.map((product) => ( + + {product.id} + {product.name} + {product.actual_remaining} + {product.supplierId} + + )) + ) : ( + + + No remaining products available. + + + )} + +
+
+ ); +} + +export default RemainingProductsTable; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/components/Summary.js b/Frontend/TheftBlock/src/components/Summary.js new file mode 100644 index 0000000..5e9c936 --- /dev/null +++ b/Frontend/TheftBlock/src/components/Summary.js @@ -0,0 +1,32 @@ +// src/components/Summary.js +import React from 'react'; +import { Grid, Card, CardContent, Typography } from '@mui/material'; + +function Summary({ totalDiscrepancies, totalLoss }) { + return ( + + + + + Total Discrepancies + + {totalDiscrepancies} + + + + + + + + Estimated Financial Loss + + ₹{totalLoss.toFixed(2)} + + + + + + ); +} + +export default Summary; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/components/SupplierTable.jsx b/Frontend/TheftBlock/src/components/SupplierTable.jsx new file mode 100644 index 0000000..f23bafe --- /dev/null +++ b/Frontend/TheftBlock/src/components/SupplierTable.jsx @@ -0,0 +1,46 @@ +// src/components/SupplierTable.jsx +import React from 'react'; +import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper } from '@mui/material'; + +function SupplierTable({ suppliers }) { + // Debug log to check props + console.log('SupplierTable suppliers:', suppliers); + + // Ensure suppliers is an array + const suppliersList = Array.isArray(suppliers) ? suppliers : []; + + return ( + + + + + ID + Name + Contact + Address + + + + {suppliersList.length > 0 ? ( + suppliersList.map((supplier) => ( + + {supplier.id} + {supplier.name} + {supplier.contact} + {supplier.address} + + )) + ) : ( + + + No suppliers available. + + + )} + +
+
+ ); +} + +export default SupplierTable; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/components/TheftDetailsTable.jsx b/Frontend/TheftBlock/src/components/TheftDetailsTable.jsx new file mode 100644 index 0000000..2f40e74 --- /dev/null +++ b/Frontend/TheftBlock/src/components/TheftDetailsTable.jsx @@ -0,0 +1,48 @@ +// src/components/TheftDetailsTable.jsx +import React from 'react'; +import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper } from '@mui/material'; + +function TheftDetailsTable({ theftOccurrences }) { + // Debug log to check props + console.log('TheftDetailsTable theftOccurrences:', theftOccurrences); + + // Ensure theftOccurrences is an array + const occurrencesList = Array.isArray(theftOccurrences) ? theftOccurrences : []; + + return ( + + + + + Product ID + Product Name + Quantity Missing + Financial Loss (₹) + Detected At + + + + {occurrencesList.length > 0 ? ( + occurrencesList.map((theft, index) => ( + + {theft.productId} + {theft.productName} + {theft.quantityMissing} + {theft.loss.toFixed(2)} + {new Date(theft.detectedAt).toLocaleString()} + + )) + ) : ( + + + No theft occurrences found. + + + )} + +
+
+ ); +} + +export default TheftDetailsTable; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/components/TheftDetectionSection.jsx b/Frontend/TheftBlock/src/components/TheftDetectionSection.jsx new file mode 100644 index 0000000..5a20e2b --- /dev/null +++ b/Frontend/TheftBlock/src/components/TheftDetectionSection.jsx @@ -0,0 +1,40 @@ +// src/components/TheftDetectionSection.jsx +import React from 'react'; +import { Typography } from '@mui/material'; +import TheftStatus from './TheftStatus'; +import TheftDetailsTable from './TheftDetailsTable'; +import RFIDScanLog from './RFIDScanLog'; + +function TheftDetectionSection({ products, scanLogs }) { + const theftOccurrences = products + .filter(p => ((p.total - p.sold) - p.actual_remaining) > 0) + .map(p => ({ + productId: p.id, + productName: p.name, + quantityMissing: (p.total - p.sold) - p.actual_remaining, + loss: ((p.total - p.sold) - p.actual_remaining) * p.price, + detectedAt: new Date().toISOString(), + })); + + const hasTheft = theftOccurrences.length > 0; + + return ( +
+ + {hasTheft && ( + <> + + Theft Details + + + + )} + + RFID Scan Logs + + +
+ ); +} + +export default TheftDetectionSection; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/components/TheftStatus.jsx b/Frontend/TheftBlock/src/components/TheftStatus.jsx new file mode 100644 index 0000000..92a66eb --- /dev/null +++ b/Frontend/TheftBlock/src/components/TheftStatus.jsx @@ -0,0 +1,24 @@ +// src/components/TheftStatus.jsx +import React from 'react'; +import { Typography } from '@mui/material'; + +function TheftStatus({ hasTheft, affectedProducts }) { + return ( +
+ + Theft Status + + {hasTheft ? ( + + Theft Detected for {affectedProducts.length} items + + ) : ( + + No Theft Detected + + )} +
+ ); +} + +export default TheftStatus; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/context/SelectedInventoryProvider.jsx b/Frontend/TheftBlock/src/context/SelectedInventoryProvider.jsx new file mode 100644 index 0000000..bce8ccc --- /dev/null +++ b/Frontend/TheftBlock/src/context/SelectedInventoryProvider.jsx @@ -0,0 +1,16 @@ +// src/context/SelectedInventoryProvider.jsx +import { createContext, useContext, useState } from 'react'; + +const SelectedInventoryContext = createContext(); + +export const useSelectedInventory = () => useContext(SelectedInventoryContext); + +export const SelectedInventoryProvider = ({ children }) => { + const [selectedInventoryId, setSelectedInventoryId] = useState(null); + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/main.jsx b/Frontend/TheftBlock/src/main.jsx index b9a1a6d..b32ad0b 100644 --- a/Frontend/TheftBlock/src/main.jsx +++ b/Frontend/TheftBlock/src/main.jsx @@ -1,10 +1,11 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.jsx' +// src/main.jsx +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; -createRoot(document.getElementById('root')).render( - +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + - , -) + +); \ No newline at end of file diff --git a/Frontend/TheftBlock/src/pages/Dashboard.jsx b/Frontend/TheftBlock/src/pages/Dashboard.jsx new file mode 100644 index 0000000..81e6c70 --- /dev/null +++ b/Frontend/TheftBlock/src/pages/Dashboard.jsx @@ -0,0 +1,69 @@ +// src/pages/Dashboard.jsx +import React, { useState, useEffect } from 'react'; +import { Container, Typography } from '@mui/material'; +import { useSelectedInventory } from '../context/SelectedInventoryProvider'; +import KeyMetrics from '../components/KeyMetrics'; +import ItemTable from '../components/ItemTable'; +import Graphs from '../components/Graphs'; +import TheftDetectionSection from '../components/TheftDetectionSection'; +import { getProductsByInventory, getRFIDScans } from '../services/api'; + +function Dashboard() { + const { selectedInventoryId } = useSelectedInventory(); + const [productsData, setProductsData] = useState({ meta: {}, data: [] }); + const [scanLogsData, setScanLogsData] = useState({ meta: {}, data: [] }); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + if (!selectedInventoryId) { + setLoading(false); // Exit loading state if no inventory is selected + return; + } + setLoading(true); + setError(null); + try { + const productsResponse = await getProductsByInventory(selectedInventoryId); + const scansResponse = await getRFIDScans(selectedInventoryId); + console.log('Dashboard - Fetched Products:', productsResponse); // Debug log + console.log('Dashboard - Fetched Scans:', scansResponse); // Debug log + setProductsData(productsResponse); + setScanLogsData(scansResponse); + } catch (err) { + console.error('Error fetching data in Dashboard:', err); + setError('Failed to fetch data'); + } finally { + setLoading(false); + } + }; + fetchData(); + }, [selectedInventoryId]); + + console.log('Dashboard - selectedInventoryId:', selectedInventoryId); // Debug log + console.log('Dashboard - loading:', loading, 'error:', error); // Debug log + + if (loading) return Loading...; + if (error) return {error}; + if (!selectedInventoryId) return Please select an inventory from the menu.; + + return ( + + + Inventory Dashboard + + + Last Updated: {productsData.meta.lastUpdated ? new Date(productsData.meta.lastUpdated).toLocaleString() : 'N/A'} + + + + Items Overview + + + + + + ); +} + +export default Dashboard; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/pages/InventoryDetail.jsx b/Frontend/TheftBlock/src/pages/InventoryDetail.jsx new file mode 100644 index 0000000..7e309a2 --- /dev/null +++ b/Frontend/TheftBlock/src/pages/InventoryDetail.jsx @@ -0,0 +1,84 @@ +// src/pages/InventoryDetail.jsx +import React, { useState, useEffect } from 'react'; +import { Container, Typography, Button } from '@mui/material'; +import { useParams, useNavigate } from 'react-router-dom'; +import Summary from '../components/Summary'; +import ItemTable from '../components/ItemTable'; +import Filters from '../components/Filters'; +import InventoryChart from '../components/InventoryChart'; +import DiscrepancyTable from '../components/DiscrepancyTable'; +import { getInventoryById, getChartData } from '../services/api'; +import dayjs from 'dayjs'; + +function InventoryDetail() { + const { id } = useParams(); + const navigate = useNavigate(); + const [inventory, setInventory] = useState(null); + const [filteredDiscrepancies, setFilteredDiscrepancies] = useState([]); + const [chartData, setChartData] = useState({ dates: [], losses: [] }); + const [filters, setFilters] = useState({ dateStart: null, dateEnd: null, product: '' }); + + useEffect(() => { + const inv = getInventoryById(id); + if (inv) { + setInventory(inv); + setFilteredDiscrepancies(inv.discrepancies); + setChartData(getChartData(inv.discrepancies)); + } + }, [id]); + + const handleFilterChange = (newFilters) => { + setFilters(newFilters); + let filtered = [...inventory.discrepancies]; + if (newFilters.dateStart) { + filtered = filtered.filter((d) => dayjs(d.date).isAfter(dayjs(newFilters.dateStart).subtract(1, 'day'))); + } + if (newFilters.dateEnd) { + filtered = filtered.filter((d) => dayjs(d.date).isBefore(dayjs(newFilters.dateEnd).add(1, 'day'))); + } + if (newFilters.product) { + filtered = filtered.filter((d) => d.itemId === newFilters.product); + } + setFilteredDiscrepancies(filtered); + setChartData(getChartData(filtered)); + }; + + if (!inventory) return Loading...; + + const totalDiscrepancies = filteredDiscrepancies.reduce((sum, d) => sum + Math.abs(d.discrepancy), 0); + const totalLoss = filteredDiscrepancies.reduce((sum, d) => sum + d.loss, 0); + + return ( + + + + {inventory.name} - Overview + + + + + Inventory Items + + + + + Filters + + + + + Discrepancy Trends + + + + + Discrepancy Details + + + + ); +} + +export default InventoryDetail; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/pages/InventoryList.jsx b/Frontend/TheftBlock/src/pages/InventoryList.jsx new file mode 100644 index 0000000..91a5c63 --- /dev/null +++ b/Frontend/TheftBlock/src/pages/InventoryList.jsx @@ -0,0 +1,30 @@ +// src/pages/InventoryList.jsx +import React, { useState, useEffect } from 'react'; +import { Container, Typography, Grid } from '@mui/material'; +import InventoryCard from '../components/InventoryCard'; +import { getInventories } from '../services/api'; + +function InventoryList() { + const [inventories, setInventories] = useState([]); + + useEffect(() => { + setInventories(getInventories()); + }, []); + + return ( + + + Inventory List + + + {inventories.map((inventory) => ( + + + + ))} + + + ); +} + +export default InventoryList; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/pages/Products.jsx b/Frontend/TheftBlock/src/pages/Products.jsx new file mode 100644 index 0000000..2309035 --- /dev/null +++ b/Frontend/TheftBlock/src/pages/Products.jsx @@ -0,0 +1,64 @@ +// src/pages/Products.jsx +import React, { useState, useEffect } from 'react'; +import { Container, Typography } from '@mui/material'; +import ProductTable from '../components/ProductTable'; +import AddProductForm from '../components/AddProductForm'; +import RemainingProductsTable from '../components/RemainingProductsTable'; +import { getSuppliers, getProducts } from '../services/api'; + +function Products() { + const [suppliersData, setSuppliersData] = useState({ meta: {}, data: [] }); + const [productsData, setProductsData] = useState({ meta: {}, data: [] }); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + setLoading(true); + setError(null); + try { + const suppliers = await getSuppliers(); + const products = await getProducts(); + setSuppliersData(suppliers); + setProductsData(products); + } catch (err) { + setError('Failed to fetch data'); + } finally { + setLoading(false); + } + }; + fetchData(); + }, []); + + const handleAddProduct = (newProduct) => { + setProductsData((prev) => ({ + meta: { ...prev.meta, lastUpdated: new Date().toISOString(), totalProducts: prev.data.length + 1 }, + data: [...prev.data, newProduct], + })); + }; + + if (loading) return Loading...; + if (error) return {error}; + + return ( + + + Products + + + Last Updated: {productsData.meta.lastUpdated ? new Date(productsData.meta.lastUpdated).toLocaleString() : 'N/A'} + + + + All Products + + + + Remaining Products + + + + ); +} + +export default Products; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/pages/Suppliers.jsx b/Frontend/TheftBlock/src/pages/Suppliers.jsx new file mode 100644 index 0000000..6abc447 --- /dev/null +++ b/Frontend/TheftBlock/src/pages/Suppliers.jsx @@ -0,0 +1,53 @@ +// src/pages/Suppliers.jsx +import React, { useState, useEffect } from 'react'; +import { Container, Typography } from '@mui/material'; +import SupplierTable from '../components/SupplierTable'; +import AddSupplierForm from '../components/AddSupplierForm'; +import { getSuppliers } from '../services/api'; + +function Suppliers() { + const [suppliersData, setSuppliersData] = useState({ meta: {}, data: [] }); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchSuppliers = async () => { + setLoading(true); + setError(null); + try { + const data = await getSuppliers(); + setSuppliersData(data); + } catch (err) { + setError('Failed to fetch suppliers'); + } finally { + setLoading(false); + } + }; + fetchSuppliers(); + }, []); + + const handleAddSupplier = (newSupplier) => { + setSuppliersData((prev) => ({ + meta: { ...prev.meta, lastUpdated: new Date().toISOString(), totalSuppliers: prev.data.length + 1 }, + data: [...prev.data, newSupplier], + })); + }; + + if (loading) return Loading...; + if (error) return {error}; + + return ( + + + Suppliers + + + Last Updated: {suppliersData.meta.lastUpdated ? new Date(suppliersData.meta.lastUpdated).toLocaleString() : 'N/A'} + + + + + ); +} + +export default Suppliers; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/pages/TheftDetection.jsx b/Frontend/TheftBlock/src/pages/TheftDetection.jsx new file mode 100644 index 0000000..3c2b89b --- /dev/null +++ b/Frontend/TheftBlock/src/pages/TheftDetection.jsx @@ -0,0 +1,74 @@ +// src/pages/TheftDetection.jsx +import React, { useState, useEffect } from 'react'; +import { Container, Typography } from '@mui/material'; +import { useSelectedInventory } from '../context/SelectedInventoryProvider'; +import TheftStatus from '../components/TheftStatus'; +import TheftDetailsTable from '../components/TheftDetailsTable'; +import RFIDScanLog from '../components/RFIDScanLog'; +import { getProductsByInventory, getRFIDScans, detectTheft } from '../services/api'; + +function TheftDetection() { + const { selectedInventoryId } = useSelectedInventory(); + const [productsData, setProductsData] = useState({ meta: {}, data: [] }); + const [scanLogsData, setScanLogsData] = useState({ meta: {}, data: [] }); + const [theftOccurrences, setTheftOccurrences] = useState([]); + const [hasTheft, setHasTheft] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + if (!selectedInventoryId) return; + setLoading(true); + setError(null); + try { + const productsResponse = await getProductsByInventory(selectedInventoryId); + const scansResponse = await getRFIDScans(selectedInventoryId); + const theftData = await detectTheft(productsResponse.data, scansResponse.data); + console.log('Fetched Products in TheftDetection:', productsResponse); // Debug log + console.log('Fetched Scans in TheftDetection:', scansResponse); // Debug log + console.log('Theft Data:', theftData); // Debug log + setProductsData(productsResponse); + setScanLogsData(scansResponse); + setTheftOccurrences(theftData.occurrences); + setHasTheft(theftData.hasTheft); + } catch (err) { + console.error('Error fetching data:', err); + setError('Failed to fetch data'); + } finally { + setLoading(false); + } + }; + fetchData(); + }, [selectedInventoryId]); + + if (loading) return Loading...; + if (error) return {error}; + if (!selectedInventoryId) return Please select an inventory from the menu.; + + return ( + + + Theft Detection + + + Products Last Updated: {productsData.meta.lastUpdated ? new Date(productsData.meta.lastUpdated).toLocaleString() : 'N/A'} | Scans Last Updated: {scanLogsData.meta.lastUpdated ? new Date(scanLogsData.meta.lastUpdated).toLocaleString() : 'N/A'} + + + {hasTheft && ( + <> + + Theft Details + + + + )} + + RFID Scan Logs + + + + ); +} + +export default TheftDetection; \ No newline at end of file diff --git a/Frontend/TheftBlock/src/services/api.jsx b/Frontend/TheftBlock/src/services/api.jsx new file mode 100644 index 0000000..51e48f9 --- /dev/null +++ b/Frontend/TheftBlock/src/services/api.jsx @@ -0,0 +1,159 @@ +// src/services/api.jsx +// Sample data with metadata +let inventories = { + meta: { + lastUpdated: new Date().toISOString(), + source: "Sample Data", + totalInventories: 2, + }, + data: [ + { id: "inv1", name: "Inventory 1" }, + { id: "inv2", name: "Inventory 2" }, + ], +}; + +let suppliers = { + meta: { + lastUpdated: new Date().toISOString(), + source: "Sample Data", + totalSuppliers: 2, + }, + data: [ + { id: "sup1", name: "Supplier A", contact: "123-456-7890", address: "123 Main St" }, + { id: "sup2", name: "Supplier B", contact: "098-765-4321", address: "456 Elm St" }, + ], +}; + +let products = { + meta: { + lastUpdated: new Date().toISOString(), + source: "Sample Data", + totalProducts: 4, + }, + data: [ + { + id: "prod1", + name: "Pen", + total: 100, + actual_remaining: 80, + price: 10, + rfid: "RFID001", + supplierId: "sup1", + inventoryId: "inv1", + sold: 15, + }, + { + id: "prod2", + name: "Pencil", + total: 200, + actual_remaining: 180, + price: 5, + rfid: "RFID002", + supplierId: "sup1", + inventoryId: "inv1", + sold: 20, + }, + { + id: "prod3", + name: "Notebook", + total: 150, + actual_remaining: 140, + price: 50, + rfid: "RFID003", + supplierId: "sup2", + inventoryId: "inv2", + sold: 10, + }, + { + id: "prod4", + name: "Eraser", + total: 300, + actual_remaining: 250, + price: 2, + rfid: "RFID004", + supplierId: "sup2", + inventoryId: "inv2", + sold: 50, + }, + ], +}; + +let rfidScans = { + meta: { + lastUpdated: new Date().toISOString(), + source: "Sample Data", + totalScans: 4, + }, + data: [ + { productId: "prod1", productName: "Pen", scannedAt: "2025-03-05T12:00:00Z", status: "Unsold", inventoryId: "inv1" }, + { productId: "prod2", productName: "Pencil", scannedAt: "2025-03-05T12:05:00Z", status: "Sold", inventoryId: "inv1" }, + { productId: "prod3", productName: "Notebook", scannedAt: "2025-03-05T12:10:00Z", status: "Sold", inventoryId: "inv2" }, + { productId: "prod4", productName: "Eraser", scannedAt: "2025-03-05T12:15:00Z", status: "Unsold", inventoryId: "inv2" }, + ], +}; + +// Functions returning sample data +export const getInventories = async () => { + return inventories.data; +}; + +export const getSuppliers = async () => { + return suppliers; // Return the full object with meta and data +}; + +export const addSupplier = async (supplier) => { + suppliers.data.push(supplier); + suppliers.meta.lastUpdated = new Date().toISOString(); + suppliers.meta.totalSuppliers = suppliers.data.length; + return supplier; +}; + +export const getProducts = async () => { + return products; // Return the full object with meta and data +}; + +export const addProduct = async (product) => { + products.data.push(product); + products.meta.lastUpdated = new Date().toISOString(); + products.meta.totalProducts = products.data.length; + return product; +}; + +export const getProductsByInventory = async (inventoryId) => { + const filteredProducts = products.data.filter(p => p.inventoryId === inventoryId); + return { + meta: products.meta, + data: filteredProducts, + }; +}; + +export const getRFIDScans = async (inventoryId) => { + const filteredScans = rfidScans.data.filter(s => s.inventoryId === inventoryId); + return { + meta: rfidScans.meta, + data: filteredScans, + }; +}; + +export const detectTheft = async (productsData, scanLogsData) => { + const theftOccurrences = []; + scanLogsData.forEach((log) => { + if (log.status === 'Unsold') { + const product = productsData.find((p) => p.id === log.productId); + if (product) { + const discrepancy = (product.total - product.sold) - product.actual_remaining; + if (discrepancy > 0) { + theftOccurrences.push({ + productId: log.productId, + productName: product.name, + quantityMissing: discrepancy, + loss: discrepancy * product.price, + detectedAt: new Date().toISOString(), + }); + } + } + } + }); + const hasTheft = theftOccurrences.length > 0; + return { hasTheft, occurrences: theftOccurrences }; +}; \ No newline at end of file