Introduction
The Inventory Management System is a comprehensive web-based solution designed to help businesses efficiently track, manage, and control their inventory. This system provides real-time visibility into stock levels, automates reordering processes, manages supplier relationships, handles multiple warehouses, and generates detailed reports. With both admin and staff interfaces, it streamlines inventory operations, reduces manual errors, and optimizes stock management.
Project Features
Admin Features:
- Dashboard with Key Metrics
- Product Management (Add, Edit, Delete, Import/Export)
- Category Management
- Supplier Management
- Purchase Order Management
- Sales Order Management
- Stock Adjustment Management
- Multiple Warehouse Support
- User Management (Staff Accounts)
- Role-Based Access Control
- Report Generation (Stock, Sales, Purchase, Profit/Loss)
- Low Stock Alerts
- Expiry Date Tracking
- Barcode Generation
- Audit Logs
- System Settings
Staff Features:
- View Products
- Process Sales
- Check Stock Levels
- Create Purchase Requests
- Receive Stock
- Transfer Stock Between Warehouses
- View Reports (Limited Access)
- Profile Management
General Features:
- Real-time Stock Updates
- Search and Filter Products
- Pagination for Large Datasets
- Responsive Design
- Export to Excel/PDF
- Print Invoices
- Email Notifications
- Dark/Light Mode Toggle
Project File Structure
inventory-management-system/ │ ├── assets/ │ ├── css/ │ │ ├── style.css │ │ ├── admin-style.css │ │ └── responsive.css │ ├── js/ │ │ ├── main.js │ │ ├── dashboard.js │ │ ├── products.js │ │ ├── sales.js │ │ └── charts.js │ ├── images/ │ │ ├── products/ │ │ └── avatars/ │ └── vendor/ │ ├── bootstrap/ │ ├── font-awesome/ │ └── datatables/ │ ├── database/ │ └── inventory_db.sql │ ├── includes/ │ ├── config.php │ ├── db_connection.php │ ├── functions.php │ ├── session.php │ ├── auth.php │ └── validation.php │ ├── admin/ │ ├── index.php (Dashboard) │ ├── login.php │ ├── logout.php │ ├── profile.php │ ├── products/ │ │ ├── index.php │ │ ├── add.php │ │ ├── edit.php │ │ ├── view.php │ │ ├── delete.php │ │ ├── import.php │ │ ├── export.php │ │ └── barcode.php │ ├── categories/ │ │ ├── index.php │ │ ├── add.php │ │ ├── edit.php │ │ └── delete.php │ ├── suppliers/ │ │ ├── index.php │ │ ├── add.php │ │ ├── edit.php │ │ ├── view.php │ │ └── delete.php │ ├── purchases/ │ │ ├── index.php │ │ ├── create.php │ │ ├── edit.php │ │ ├── receive.php │ │ └── view.php │ ├── sales/ │ │ ├── index.php │ │ ├── create.php │ │ ├── invoice.php │ │ └── return.php │ ├── warehouses/ │ │ ├── index.php │ │ ├── add.php │ │ ├── edit.php │ │ └── stock.php │ ├── transfers/ │ │ ├── index.php │ │ ├── create.php │ │ └── receive.php │ ├── adjustments/ │ │ ├── index.php │ │ ├── create.php │ │ └── history.php │ ├── reports/ │ │ ├── index.php │ │ ├── stock-report.php │ │ ├── sales-report.php │ │ ├── purchase-report.php │ │ ├── profit-loss.php │ │ └── low-stock.php │ ├── users/ │ │ ├── index.php │ │ ├── add.php │ │ ├── edit.php │ │ └── permissions.php │ └── settings/ │ ├── index.php │ ├── general.php │ └── backup.php │ ├── staff/ │ ├── index.php (Dashboard) │ ├── login.php │ ├── logout.php │ ├── profile.php │ ├── products/ │ │ ├── index.php │ │ └── search.php │ ├── sales/ │ │ ├── create.php │ │ └── history.php │ ├── purchases/ │ │ ├── requests.php │ │ └── receive.php │ └── reports/ │ └── stock.php │ ├── api/ │ ├── get-products.php │ ├── update-stock.php │ ├── process-sale.php │ ├── generate-report.php │ └── search-suppliers.php │ ├── uploads/ │ ├── products/ │ └── imports/ │ ├── exports/ │ └── reports/ │ ├── index.php (Landing/Login Redirect) ├── .htaccess └── README.md
Database Schema (inventory_db.sql)
-- Create Database
CREATE DATABASE IF NOT EXISTS inventory_db;
USE inventory_db;
-- Table: users
CREATE TABLE users (
user_id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
full_name VARCHAR(100) NOT NULL,
phone VARCHAR(20),
avatar VARCHAR(255),
role ENUM('admin', 'manager', 'staff') DEFAULT 'staff',
warehouse_id INT NULL,
is_active BOOLEAN DEFAULT TRUE,
last_login DATETIME,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Insert default admin
INSERT INTO users (username, email, password, full_name, role) VALUES
('admin', '[email protected]', MD5('Admin@123'), 'System Administrator', 'admin'),
('manager', '[email protected]', MD5('Manager@123'), 'Store Manager', 'manager'),
('staff', '[email protected]', MD5('Staff@123'), 'Staff Member', 'staff');
-- Table: warehouses
CREATE TABLE warehouses (
warehouse_id INT PRIMARY KEY AUTO_INCREMENT,
warehouse_name VARCHAR(100) NOT NULL,
location VARCHAR(255),
address TEXT,
phone VARCHAR(20),
email VARCHAR(100),
manager_id INT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (manager_id) REFERENCES users(user_id) ON DELETE SET NULL
);
-- Insert default warehouse
INSERT INTO warehouses (warehouse_name, location) VALUES ('Main Warehouse', 'Downtown');
-- Update users with warehouse
UPDATE users SET warehouse_id = 1 WHERE role IN ('manager', 'staff');
-- Table: categories
CREATE TABLE categories (
category_id INT PRIMARY KEY AUTO_INCREMENT,
category_name VARCHAR(100) NOT NULL,
category_code VARCHAR(50) UNIQUE,
description TEXT,
parent_category_id INT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (parent_category_id) REFERENCES categories(category_id) ON DELETE CASCADE
);
-- Insert sample categories
INSERT INTO categories (category_name, category_code, description) VALUES
('Electronics', 'ELEC', 'Electronic items and gadgets'),
('Clothing', 'CLTH', 'Apparel and fashion items'),
('Food & Beverages', 'FOOD', 'Food products and beverages'),
('Furniture', 'FURN', 'Home and office furniture'),
('Stationery', 'STAT', 'Office and school supplies');
-- Table: suppliers
CREATE TABLE suppliers (
supplier_id INT PRIMARY KEY AUTO_INCREMENT,
supplier_name VARCHAR(100) NOT NULL,
supplier_code VARCHAR(50) UNIQUE,
contact_person VARCHAR(100),
email VARCHAR(100),
phone VARCHAR(20),
mobile VARCHAR(20),
address TEXT,
city VARCHAR(50),
state VARCHAR(50),
postal_code VARCHAR(20),
country VARCHAR(50),
tax_id VARCHAR(50),
payment_terms VARCHAR(50),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Insert sample suppliers
INSERT INTO suppliers (supplier_name, supplier_code, contact_person, email, phone) VALUES
('ABC Electronics', 'SUP001', 'John Smith', '[email protected]', '123-456-7890'),
('XYZ Clothing', 'SUP002', 'Jane Doe', '[email protected]', '098-765-4321'),
('Global Foods', 'SUP003', 'Bob Wilson', '[email protected]', '555-123-4567');
-- Table: products
CREATE TABLE products (
product_id INT PRIMARY KEY AUTO_INCREMENT,
product_code VARCHAR(50) UNIQUE NOT NULL,
product_name VARCHAR(255) NOT NULL,
category_id INT,
supplier_id INT,
description TEXT,
unit VARCHAR(20) DEFAULT 'pcs',
purchase_price DECIMAL(10,2) NOT NULL,
selling_price DECIMAL(10,2) NOT NULL,
wholesale_price DECIMAL(10,2),
tax_rate DECIMAL(5,2) DEFAULT 0.00,
min_stock_level INT DEFAULT 5,
max_stock_level INT DEFAULT 100,
reorder_level INT DEFAULT 10,
barcode VARCHAR(100),
has_expiry BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES categories(category_id) ON DELETE SET NULL,
FOREIGN KEY (supplier_id) REFERENCES suppliers(supplier_id) ON DELETE SET NULL,
INDEX idx_product_code (product_code),
INDEX idx_product_name (product_name)
);
-- Insert sample products
INSERT INTO products (product_code, product_name, category_id, supplier_id, unit, purchase_price, selling_price, min_stock_level, reorder_level) VALUES
('PRD001', 'Smartphone X', 1, 1, 'pcs', 500.00, 699.99, 5, 10),
('PRD002', 'Laptop Pro', 1, 1, 'pcs', 800.00, 1099.99, 3, 5),
('PRD003', 'T-Shirt Cotton', 2, 2, 'pcs', 10.00, 19.99, 20, 30),
('PRD004', 'Jeans Slim Fit', 2, 2, 'pcs', 25.00, 49.99, 15, 20),
('PRD005', 'Rice 5kg', 3, 3, 'bag', 8.00, 12.99, 50, 30),
('PRD006', 'Cooking Oil 1L', 3, 3, 'bottle', 3.00, 5.99, 40, 25);
-- Table: product_images
CREATE TABLE product_images (
image_id INT PRIMARY KEY AUTO_INCREMENT,
product_id INT NOT NULL,
image_path VARCHAR(255) NOT NULL,
is_primary BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (product_id) REFERENCES products(product_id) ON DELETE CASCADE
);
-- Table: stock
CREATE TABLE stock (
stock_id INT PRIMARY KEY AUTO_INCREMENT,
product_id INT NOT NULL,
warehouse_id INT NOT NULL,
quantity INT DEFAULT 0,
reserved_quantity INT DEFAULT 0,
available_quantity INT GENERATED ALWAYS AS (quantity - reserved_quantity) STORED,
location_aisle VARCHAR(50),
location_rack VARCHAR(50),
location_bin VARCHAR(50),
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (product_id) REFERENCES products(product_id) ON DELETE CASCADE,
FOREIGN KEY (warehouse_id) REFERENCES warehouses(warehouse_id) ON DELETE CASCADE,
UNIQUE KEY unique_product_warehouse (product_id, warehouse_id)
);
-- Initialize stock for main warehouse
INSERT INTO stock (product_id, warehouse_id, quantity) VALUES
(1, 1, 50),
(2, 1, 30),
(3, 1, 100),
(4, 1, 80),
(5, 1, 200),
(6, 1, 150);
-- Table: purchase_orders
CREATE TABLE purchase_orders (
po_id INT PRIMARY KEY AUTO_INCREMENT,
po_number VARCHAR(50) UNIQUE NOT NULL,
supplier_id INT NOT NULL,
warehouse_id INT NOT NULL,
order_date DATE NOT NULL,
expected_delivery DATE,
status ENUM('draft', 'pending', 'approved', 'ordered', 'received', 'cancelled') DEFAULT 'draft',
payment_status ENUM('unpaid', 'partial', 'paid') DEFAULT 'unpaid',
payment_method VARCHAR(50),
subtotal DECIMAL(10,2),
tax_amount DECIMAL(10,2),
discount_amount DECIMAL(10,2),
shipping_cost DECIMAL(10,2) DEFAULT 0.00,
total_amount DECIMAL(10,2),
notes TEXT,
created_by INT,
approved_by INT,
received_by INT,
received_date DATETIME,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (supplier_id) REFERENCES suppliers(supplier_id),
FOREIGN KEY (warehouse_id) REFERENCES warehouses(warehouse_id),
FOREIGN KEY (created_by) REFERENCES users(user_id),
FOREIGN KEY (approved_by) REFERENCES users(user_id),
FOREIGN KEY (received_by) REFERENCES users(user_id)
);
-- Table: purchase_order_items
CREATE TABLE purchase_order_items (
po_item_id INT PRIMARY KEY AUTO_INCREMENT,
po_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL,
received_quantity INT DEFAULT 0,
unit_price DECIMAL(10,2) NOT NULL,
total_price DECIMAL(10,2) GENERATED ALWAYS AS (quantity * unit_price) STORED,
FOREIGN KEY (po_id) REFERENCES purchase_orders(po_id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
-- Table: sales_orders
CREATE TABLE sales_orders (
so_id INT PRIMARY KEY AUTO_INCREMENT,
so_number VARCHAR(50) UNIQUE NOT NULL,
customer_name VARCHAR(100),
customer_email VARCHAR(100),
customer_phone VARCHAR(20),
customer_address TEXT,
warehouse_id INT NOT NULL,
user_id INT,
order_date DATETIME DEFAULT CURRENT_TIMESTAMP,
status ENUM('pending', 'processing', 'completed', 'cancelled', 'refunded') DEFAULT 'pending',
payment_status ENUM('unpaid', 'partial', 'paid') DEFAULT 'unpaid',
payment_method VARCHAR(50),
subtotal DECIMAL(10,2),
tax_amount DECIMAL(10,2),
discount_amount DECIMAL(10,2),
shipping_cost DECIMAL(10,2) DEFAULT 0.00,
total_amount DECIMAL(10,2),
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (warehouse_id) REFERENCES warehouses(warehouse_id),
FOREIGN KEY (user_id) REFERENCES users(user_id)
);
-- Table: sales_order_items
CREATE TABLE sales_order_items (
so_item_id INT PRIMARY KEY AUTO_INCREMENT,
so_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL,
unit_price DECIMAL(10,2) NOT NULL,
total_price DECIMAL(10,2) GENERATED ALWAYS AS (quantity * unit_price) STORED,
FOREIGN KEY (so_id) REFERENCES sales_orders(so_id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
-- Table: stock_movements
CREATE TABLE stock_movements (
movement_id INT PRIMARY KEY AUTO_INCREMENT,
product_id INT NOT NULL,
from_warehouse_id INT,
to_warehouse_id INT,
quantity INT NOT NULL,
movement_type ENUM('purchase', 'sale', 'transfer', 'adjustment', 'return') NOT NULL,
reference_type VARCHAR(50),
reference_id INT,
notes TEXT,
created_by INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (product_id) REFERENCES products(product_id),
FOREIGN KEY (from_warehouse_id) REFERENCES warehouses(warehouse_id),
FOREIGN KEY (to_warehouse_id) REFERENCES warehouses(warehouse_id),
FOREIGN KEY (created_by) REFERENCES users(user_id)
);
-- Table: stock_adjustments
CREATE TABLE stock_adjustments (
adjustment_id INT PRIMARY KEY AUTO_INCREMENT,
product_id INT NOT NULL,
warehouse_id INT NOT NULL,
previous_quantity INT NOT NULL,
new_quantity INT NOT NULL,
adjustment_type ENUM('add', 'remove', 'damage', 'expired', 'found', 'lost') NOT NULL,
reason TEXT,
created_by INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (product_id) REFERENCES products(product_id),
FOREIGN KEY (warehouse_id) REFERENCES warehouses(warehouse_id),
FOREIGN KEY (created_by) REFERENCES users(user_id)
);
-- Table: stock_transfers
CREATE TABLE stock_transfers (
transfer_id INT PRIMARY KEY AUTO_INCREMENT,
transfer_number VARCHAR(50) UNIQUE NOT NULL,
from_warehouse_id INT NOT NULL,
to_warehouse_id INT NOT NULL,
status ENUM('pending', 'in_transit', 'completed', 'cancelled') DEFAULT 'pending',
request_date DATETIME DEFAULT CURRENT_TIMESTAMP,
completed_date DATETIME,
notes TEXT,
requested_by INT,
approved_by INT,
received_by INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (from_warehouse_id) REFERENCES warehouses(warehouse_id),
FOREIGN KEY (to_warehouse_id) REFERENCES warehouses(warehouse_id),
FOREIGN KEY (requested_by) REFERENCES users(user_id),
FOREIGN KEY (approved_by) REFERENCES users(user_id),
FOREIGN KEY (received_by) REFERENCES users(user_id)
);
-- Table: transfer_items
CREATE TABLE transfer_items (
transfer_item_id INT PRIMARY KEY AUTO_INCREMENT,
transfer_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL,
received_quantity INT DEFAULT 0,
FOREIGN KEY (transfer_id) REFERENCES stock_transfers(transfer_id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
-- Table: product_batches (for expiry tracking)
CREATE TABLE product_batches (
batch_id INT PRIMARY KEY AUTO_INCREMENT,
product_id INT NOT NULL,
batch_number VARCHAR(100),
supplier_id INT,
purchase_price DECIMAL(10,2),
quantity INT NOT NULL,
remaining_quantity INT NOT NULL,
manufacture_date DATE,
expiry_date DATE,
received_date DATE,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (product_id) REFERENCES products(product_id),
FOREIGN KEY (supplier_id) REFERENCES suppliers(supplier_id)
);
-- Table: activity_logs
CREATE TABLE activity_logs (
log_id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
action VARCHAR(100) NOT NULL,
entity_type VARCHAR(50),
entity_id INT,
details TEXT,
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE SET NULL
);
-- Table: system_settings
CREATE TABLE system_settings (
setting_id INT PRIMARY KEY AUTO_INCREMENT,
setting_key VARCHAR(100) UNIQUE NOT NULL,
setting_value TEXT,
setting_description TEXT,
updated_by INT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Insert default settings
INSERT INTO system_settings (setting_key, setting_value, setting_description) VALUES
('company_name', 'Inventory Management System', 'Company name'),
('company_address', '123 Business Street, City, Country', 'Company address'),
('company_phone', '+1 234 567 890', 'Company phone'),
('company_email', '[email protected]', 'Company email'),
('company_tax_id', 'TAX123456', 'Company tax ID'),
('currency', 'USD', 'Default currency'),
('currency_symbol', '$', 'Currency symbol'),
('tax_rate', '10.00', 'Default tax rate'),
('low_stock_threshold', '10', 'Low stock alert threshold'),
('enable_barcode', '1', 'Enable barcode scanning'),
('date_format', 'Y-m-d', 'Date format'),
('time_format', 'H:i:s', 'Time format'),
('items_per_page', '25', 'Items per page in tables'),
('backup_path', '/backups/', 'Database backup path');
-- Table: customer (optional for CRM)
CREATE TABLE customers (
customer_id INT PRIMARY KEY AUTO_INCREMENT,
customer_code VARCHAR(50) UNIQUE,
customer_name VARCHAR(100) NOT NULL,
customer_type ENUM('regular', 'wholesale', 'vip') DEFAULT 'regular',
email VARCHAR(100),
phone VARCHAR(20),
mobile VARCHAR(20),
address TEXT,
city VARCHAR(50),
state VARCHAR(50),
postal_code VARCHAR(20),
country VARCHAR(50),
tax_id VARCHAR(50),
credit_limit DECIMAL(10,2) DEFAULT 0.00,
payment_terms VARCHAR(50),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Create indexes for performance
CREATE INDEX idx_product_category ON products(category_id);
CREATE INDEX idx_product_supplier ON products(supplier_id);
CREATE INDEX idx_stock_product ON stock(product_id);
CREATE INDEX idx_stock_warehouse ON stock(warehouse_id);
CREATE INDEX idx_movement_product ON stock_movements(product_id);
CREATE INDEX idx_movement_date ON stock_movements(created_at);
CREATE INDEX idx_po_supplier ON purchase_orders(supplier_id);
CREATE INDEX idx_po_status ON purchase_orders(status);
CREATE INDEX idx_so_customer ON sales_orders(customer_name);
CREATE INDEX idx_so_date ON sales_orders(order_date);
CREATE INDEX idx_logs_user ON activity_logs(user_id);
CREATE INDEX idx_logs_date ON activity_logs(created_at);
Core PHP Files
1. includes/config.php
<?php
// Database configuration
define('DB_HOST', 'localhost');
define('DB_USER', 'root');
define('DB_PASS', '');
define('DB_NAME', 'inventory_db');
// Application configuration
define('SITE_NAME', 'Inventory Management System');
define('SITE_URL', 'http://localhost/inventory-management-system/');
define('ADMIN_URL', SITE_URL . 'admin/');
define('STAFF_URL', SITE_URL . 'staff/');
// Paths
define('ROOT_PATH', $_SERVER['DOCUMENT_ROOT'] . '/inventory-management-system/');
define('INCLUDES_PATH', ROOT_PATH . 'includes/');
define('UPLOAD_PATH', ROOT_PATH . 'uploads/');
define('EXPORT_PATH', ROOT_PATH . 'exports/');
define('PRODUCT_IMG_PATH', UPLOAD_PATH . 'products/');
// File upload settings
define('MAX_FILE_SIZE', 5242880); // 5MB
define('ALLOWED_IMAGE_TYPES', 'jpg,jpeg,png,gif');
define('ALLOWED_DOC_TYPES', 'pdf,doc,docx,xls,xlsx');
// Pagination settings
define('ITEMS_PER_PAGE', 25);
define('MAX_PAGE_LINKS', 5);
// Tax settings
define('DEFAULT_TAX_RATE', 10.00);
define('CURRENCY', '$');
define('CURRENCY_CODE', 'USD');
// Low stock alert threshold
define('LOW_STOCK_THRESHOLD', 10);
// Session timeout (30 minutes)
define('SESSION_TIMEOUT', 1800);
// Start session if not started
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Error reporting (disable in production)
error_reporting(E_ALL);
ini_set('display_errors', 1);
// Timezone setting
date_default_timezone_set('UTC');
?>
2. includes/db_connection.php
<?php
require_once 'config.php';
class Database {
private $connection;
private static $instance = null;
private $transaction_count = 0;
private function __construct() {
$this->connect();
}
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new Database();
}
return self::$instance;
}
private function connect() {
$this->connection = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
if ($this->connection->connect_error) {
die("Connection failed: " . $this->connection->connect_error);
}
$this->connection->set_charset("utf8mb4");
$this->connection->query("SET time_zone = '+00:00'");
}
public function getConnection() {
return $this->connection;
}
public function escapeString($string) {
return $this->connection->real_escape_string(trim($string));
}
public function prepare($sql) {
return $this->connection->prepare($sql);
}
public function query($sql) {
return $this->connection->query($sql);
}
public function getLastInsertId() {
return $this->connection->insert_id;
}
public function affectedRows() {
return $this->connection->affected_rows;
}
public function beginTransaction() {
if ($this->transaction_count === 0) {
$this->connection->begin_transaction();
}
$this->transaction_count++;
}
public function commit() {
if ($this->transaction_count === 1) {
$this->connection->commit();
}
$this->transaction_count = max(0, $this->transaction_count - 1);
}
public function rollback() {
if ($this->transaction_count === 1) {
$this->connection->rollback();
}
$this->transaction_count = max(0, $this->transaction_count - 1);
}
public function inTransaction() {
return $this->transaction_count > 0;
}
public function __destruct() {
if ($this->connection) {
$this->connection->close();
}
}
}
// Global database instance
$db = Database::getInstance();
$conn = $db->getConnection();
?>
3. includes/functions.php
<?php
require_once 'db_connection.php';
// Redirect to specified page
function redirect($url) {
header("Location: $url");
exit();
}
// Check if user is logged in
function isLoggedIn() {
return isset($_SESSION['user_id']) && !empty($_SESSION['user_id']);
}
// Check if user is admin
function isAdmin() {
return isset($_SESSION['user_role']) && $_SESSION['user_role'] === 'admin';
}
// Check if user is manager
function isManager() {
return isset($_SESSION['user_role']) && $_SESSION['user_role'] === 'manager';
}
// Check if user is staff
function isStaff() {
return isset($_SESSION['user_role']) && $_SESSION['user_role'] === 'staff';
}
// Get current user ID
function getCurrentUserId() {
return $_SESSION['user_id'] ?? 0;
}
// Get current user role
function getCurrentUserRole() {
return $_SESSION['user_role'] ?? '';
}
// Get current user warehouse
function getCurrentUserWarehouse() {
return $_SESSION['warehouse_id'] ?? null;
}
// Format currency
function formatCurrency($amount) {
return CURRENCY . ' ' . number_format($amount, 2);
}
// Format date
function formatDate($date, $format = null) {
if (!$date) return '';
$format = $format ?? getSetting('date_format', 'Y-m-d');
return date($format, strtotime($date));
}
// Format datetime
function formatDateTime($datetime) {
$date_format = getSetting('date_format', 'Y-m-d');
$time_format = getSetting('time_format', 'H:i:s');
return date($date_format . ' ' . $time_format, strtotime($datetime));
}
// Generate unique code
function generateCode($prefix, $length = 8) {
$unique_id = uniqid();
$random = substr(str_shuffle('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, $length);
return $prefix . '-' . date('Ymd') . '-' . $random;
}
// Generate barcode
function generateBarcode($product_id) {
$prefix = '20'; // Product identifier
$unique = str_pad($product_id, 6, '0', STR_PAD_LEFT);
$check_digit = calculateCheckDigit($prefix . $unique);
return $prefix . $unique . $check_digit;
}
// Calculate check digit for barcode
function calculateCheckDigit($number) {
$sum = 0;
for ($i = 0; $i < strlen($number); $i++) {
$digit = intval($number[$i]);
if ($i % 2 == 0) {
$sum += $digit;
} else {
$sum += $digit * 3;
}
}
$check = (10 - ($sum % 10)) % 10;
return $check;
}
// Get product stock
function getProductStock($product_id, $warehouse_id = null) {
global $conn;
if ($warehouse_id) {
$sql = "SELECT quantity, available_quantity FROM stock
WHERE product_id = ? AND warehouse_id = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ii", $product_id, $warehouse_id);
} else {
$sql = "SELECT SUM(quantity) as total_quantity FROM stock WHERE product_id = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("i", $product_id);
}
$stmt->execute();
$result = $stmt->get_result();
$row = $result->fetch_assoc();
return $row;
}
// Update stock after sale
function updateStockAfterSale($product_id, $warehouse_id, $quantity) {
global $conn;
$sql = "UPDATE stock SET quantity = quantity - ?
WHERE product_id = ? AND warehouse_id = ? AND quantity >= ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("iiii", $quantity, $product_id, $warehouse_id, $quantity);
if ($stmt->execute() && $stmt->affected_rows > 0) {
// Log stock movement
logStockMovement($product_id, $warehouse_id, null, $quantity, 'sale');
return true;
}
return false;
}
// Update stock after purchase
function updateStockAfterPurchase($product_id, $warehouse_id, $quantity) {
global $conn;
// Check if stock record exists
$sql = "SELECT stock_id FROM stock WHERE product_id = ? AND warehouse_id = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ii", $product_id, $warehouse_id);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$sql = "UPDATE stock SET quantity = quantity + ?
WHERE product_id = ? AND warehouse_id = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("iii", $quantity, $product_id, $warehouse_id);
} else {
$sql = "INSERT INTO stock (product_id, warehouse_id, quantity) VALUES (?, ?, ?)";
$stmt = $conn->prepare($sql);
$stmt->bind_param("iii", $product_id, $warehouse_id, $quantity);
}
if ($stmt->execute()) {
logStockMovement($product_id, null, $warehouse_id, $quantity, 'purchase');
return true;
}
return false;
}
// Transfer stock between warehouses
function transferStock($product_id, $from_warehouse, $to_warehouse, $quantity) {
global $conn;
$conn->begin_transaction();
try {
// Reduce from source warehouse
$sql = "UPDATE stock SET quantity = quantity - ?
WHERE product_id = ? AND warehouse_id = ? AND quantity >= ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("iiii", $quantity, $product_id, $from_warehouse, $quantity);
$stmt->execute();
if ($stmt->affected_rows === 0) {
throw new Exception("Insufficient stock in source warehouse");
}
// Add to destination warehouse
$sql = "INSERT INTO stock (product_id, warehouse_id, quantity)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE quantity = quantity + ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("iiii", $product_id, $to_warehouse, $quantity, $quantity);
$stmt->execute();
// Log movement
logStockMovement($product_id, $from_warehouse, $to_warehouse, $quantity, 'transfer');
$conn->commit();
return true;
} catch (Exception $e) {
$conn->rollback();
return false;
}
}
// Log stock movement
function logStockMovement($product_id, $from_warehouse, $to_warehouse, $quantity, $type, $reference_type = null, $reference_id = null) {
global $conn;
$sql = "INSERT INTO stock_movements (product_id, from_warehouse_id, to_warehouse_id, quantity, movement_type, reference_type, reference_id, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
$stmt = $conn->prepare($sql);
$user_id = getCurrentUserId();
$stmt->bind_param("iiiissii", $product_id, $from_warehouse, $to_warehouse, $quantity, $type, $reference_type, $reference_id, $user_id);
return $stmt->execute();
}
// Log user activity
function logActivity($action, $entity_type = null, $entity_id = null, $details = null) {
global $conn;
$user_id = getCurrentUserId();
$ip_address = $_SERVER['REMOTE_ADDR'] ?? '';
$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
$sql = "INSERT INTO activity_logs (user_id, action, entity_type, entity_id, details, ip_address, user_agent)
VALUES (?, ?, ?, ?, ?, ?, ?)";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ississs", $user_id, $action, $entity_type, $entity_id, $details, $ip_address, $user_agent);
return $stmt->execute();
}
// Get low stock products
function getLowStockProducts($warehouse_id = null) {
global $conn;
$sql = "SELECT p.*, s.quantity, s.available_quantity, w.warehouse_name
FROM products p
JOIN stock s ON p.product_id = s.product_id
JOIN warehouses w ON s.warehouse_id = w.warehouse_id
WHERE s.quantity <= p.reorder_level AND p.is_active = 1";
if ($warehouse_id) {
$sql .= " AND s.warehouse_id = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("i", $warehouse_id);
} else {
$stmt = $conn->prepare($sql);
}
$stmt->execute();
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
}
// Get expiring products
function getExpiringProducts($days = 30) {
global $conn;
$expiry_date = date('Y-m-d', strtotime("+$days days"));
$sql = "SELECT pb.*, p.product_name, p.product_code
FROM product_batches pb
JOIN products p ON pb.product_id = p.product_id
WHERE pb.expiry_date <= ? AND pb.expiry_date >= CURDATE() AND pb.remaining_quantity > 0
ORDER BY pb.expiry_date ASC";
$stmt = $conn->prepare($sql);
$stmt->bind_param("s", $expiry_date);
$stmt->execute();
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
}
// Generate sales report
function generateSalesReport($start_date, $end_date, $warehouse_id = null) {
global $conn;
$sql = "SELECT DATE(order_date) as sale_date,
COUNT(*) as order_count,
SUM(total_amount) as total_sales,
SUM(tax_amount) as total_tax,
SUM(shipping_cost) as total_shipping
FROM sales_orders
WHERE status = 'completed'
AND DATE(order_date) BETWEEN ? AND ?";
$params = [$start_date, $end_date];
$types = "ss";
if ($warehouse_id) {
$sql .= " AND warehouse_id = ?";
$params[] = $warehouse_id;
$types .= "i";
}
$sql .= " GROUP BY DATE(order_date) ORDER BY sale_date DESC";
$stmt = $conn->prepare($sql);
$stmt->bind_param($types, ...$params);
$stmt->execute();
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
}
// Generate purchase report
function generatePurchaseReport($start_date, $end_date, $supplier_id = null) {
global $conn;
$sql = "SELECT DATE(order_date) as purchase_date,
COUNT(*) as order_count,
SUM(total_amount) as total_purchases
FROM purchase_orders
WHERE status = 'received'
AND DATE(order_date) BETWEEN ? AND ?";
$params = [$start_date, $end_date];
$types = "ss";
if ($supplier_id) {
$sql .= " AND supplier_id = ?";
$params[] = $supplier_id;
$types .= "i";
}
$sql .= " GROUP BY DATE(order_date) ORDER BY purchase_date DESC";
$stmt = $conn->prepare($sql);
$stmt->bind_param($types, ...$params);
$stmt->execute();
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
}
// Get dashboard statistics
function getDashboardStats($warehouse_id = null) {
global $conn;
$stats = [];
// Total products
$sql = "SELECT COUNT(*) as total FROM products WHERE is_active = 1";
$result = $conn->query($sql);
$stats['total_products'] = $result->fetch_assoc()['total'];
// Low stock products
if ($warehouse_id) {
$sql = "SELECT COUNT(*) as total FROM stock s
JOIN products p ON s.product_id = p.product_id
WHERE s.warehouse_id = ? AND s.quantity <= p.reorder_level";
$stmt = $conn->prepare($sql);
$stmt->bind_param("i", $warehouse_id);
$stmt->execute();
$result = $stmt->get_result();
} else {
$sql = "SELECT COUNT(DISTINCT s.product_id) as total FROM stock s
JOIN products p ON s.product_id = p.product_id
WHERE s.quantity <= p.reorder_level";
$result = $conn->query($sql);
}
$stats['low_stock'] = $result->fetch_assoc()['total'];
// Today's sales
$today = date('Y-m-d');
$sql = "SELECT COUNT(*) as count, COALESCE(SUM(total_amount), 0) as total
FROM sales_orders WHERE DATE(order_date) = ? AND status = 'completed'";
if ($warehouse_id) {
$sql .= " AND warehouse_id = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("si", $today, $warehouse_id);
} else {
$stmt = $conn->prepare($sql);
$stmt->bind_param("s", $today);
}
$stmt->execute();
$result = $stmt->get_result();
$row = $result->fetch_assoc();
$stats['today_sales_count'] = $row['count'];
$stats['today_sales_total'] = $row['total'];
// Pending orders
$sql = "SELECT COUNT(*) as count FROM purchase_orders WHERE status = 'ordered'";
if ($warehouse_id) {
$sql .= " AND warehouse_id = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("i", $warehouse_id);
$stmt->execute();
$result = $stmt->get_result();
} else {
$result = $conn->query($sql);
}
$stats['pending_orders'] = $result->fetch_assoc()['count'];
return $stats;
}
// Get system setting
function getSetting($key, $default = null) {
global $conn;
$sql = "SELECT setting_value FROM system_settings WHERE setting_key = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("s", $key);
$stmt->execute();
$result = $stmt->get_result();
if ($row = $result->fetch_assoc()) {
return $row['setting_value'];
}
return $default;
}
// Update system setting
function updateSetting($key, $value) {
global $conn;
$sql = "UPDATE system_settings SET setting_value = ?, updated_by = ? WHERE setting_key = ?";
$stmt = $conn->prepare($sql);
$user_id = getCurrentUserId();
$stmt->bind_param("sis", $value, $user_id, $key);
return $stmt->execute();
}
// Generate CSRF token
function generateCSRFToken() {
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
// Verify CSRF token
function verifyCSRFToken($token) {
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}
// Upload file
function uploadFile($file, $target_dir, $allowed_types = null) {
$allowed_types = $allowed_types ?? explode(',', ALLOWED_IMAGE_TYPES);
if ($file['error'] !== UPLOAD_ERR_OK) {
return ['success' => false, 'message' => 'Upload error'];
}
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($extension, $allowed_types)) {
return ['success' => false, 'message' => 'File type not allowed'];
}
if ($file['size'] > MAX_FILE_SIZE) {
return ['success' => false, 'message' => 'File too large'];
}
if (!file_exists($target_dir)) {
mkdir($target_dir, 0755, true);
}
$filename = uniqid() . '_' . time() . '.' . $extension;
$target_path = $target_dir . $filename;
if (move_uploaded_file($file['tmp_name'], $target_path)) {
return ['success' => true, 'filename' => $filename];
}
return ['success' => false, 'message' => 'Failed to save file'];
}
// Export to CSV
function exportToCSV($data, $filename, $headers = null) {
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename="' . $filename . '"');
$output = fopen('php://output', 'w');
if ($headers) {
fputcsv($output, $headers);
}
foreach ($data as $row) {
fputcsv($output, $row);
}
fclose($output);
exit();
}
// Export to PDF (requires TCPDF or similar library)
function exportToPDF($html, $filename) {
// This would require a PDF library
// For now, just redirect to a PDF generation script
$_SESSION['pdf_html'] = $html;
redirect(SITE_URL . 'api/generate-pdf.php?file=' . $filename);
}
// Pagination helper
function paginate($current_page, $total_pages, $url_pattern) {
$html = '<nav><ul class="pagination">';
// Previous button
if ($current_page > 1) {
$html .= '<li class="page-item"><a class="page-link" href="' . str_replace('{page}', $current_page - 1, $url_pattern) . '">Previous</a></li>';
} else {
$html .= '<li class="page-item disabled"><span class="page-link">Previous</span></li>';
}
// Page numbers
$start = max(1, $current_page - floor(MAX_PAGE_LINKS / 2));
$end = min($total_pages, $start + MAX_PAGE_LINKS - 1);
for ($i = $start; $i <= $end; $i++) {
if ($i == $current_page) {
$html .= '<li class="page-item active"><span class="page-link">' . $i . '</span></li>';
} else {
$html .= '<li class="page-item"><a class="page-link" href="' . str_replace('{page}', $i, $url_pattern) . '">' . $i . '</a></li>';
}
}
// Next button
if ($current_page < $total_pages) {
$html .= '<li class="page-item"><a class="page-link" href="' . str_replace('{page}', $current_page + 1, $url_pattern) . '">Next</a></li>';
} else {
$html .= '<li class="page-item disabled"><span class="page-link">Next</span></li>';
}
$html .= '</ul></nav>';
return $html;
}
?>
4. admin/index.php (Dashboard)
<?php
require_once '../includes/config.php';
require_once '../includes/functions.php';
require_once '../includes/auth.php';
// Check if user is logged in and is admin/manager
if (!isLoggedIn() || !(isAdmin() || isManager())) {
redirect('login.php');
}
$user_id = $_SESSION['user_id'];
$user_role = $_SESSION['user_role'];
$warehouse_id = isManager() ? $_SESSION['warehouse_id'] : null;
// Get dashboard statistics
$stats = getDashboardStats($warehouse_id);
// Get recent sales
$sql = "SELECT so.*, u.full_name as processed_by
FROM sales_orders so
LEFT JOIN users u ON so.user_id = u.user_id
WHERE so.status = 'completed'";
if ($warehouse_id) {
$sql .= " AND so.warehouse_id = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("i", $warehouse_id);
} else {
$stmt = $conn->prepare($sql);
}
$sql .= " ORDER BY so.order_date DESC LIMIT 10";
$stmt->execute();
$recent_sales = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
// Get low stock alerts
$low_stock = getLowStockProducts($warehouse_id);
// Get expiring products
$expiring = getExpiringProducts(30);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - <?php echo SITE_NAME; ?></title>
<link rel="stylesheet" href="../assets/css/admin-style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<div class="admin-container">
<!-- Sidebar -->
<?php include 'includes/sidebar.php'; ?>
<!-- Main Content -->
<div class="main-content">
<!-- Top Navigation -->
<?php include 'includes/topnav.php'; ?>
<!-- Dashboard Content -->
<div class="content-wrapper">
<div class="page-header">
<h1>Dashboard</h1>
<div class="date-range">
<select id="dateRange" class="form-select">
<option value="today">Today</option>
<option value="yesterday">Yesterday</option>
<option value="week" selected>This Week</option>
<option value="month">This Month</option>
<option value="year">This Year</option>
<option value="custom">Custom Range</option>
</select>
<button class="btn btn-primary" onclick="refreshDashboard()">
<i class="fas fa-sync-alt"></i> Refresh
</button>
</div>
</div>
<!-- Statistics Cards -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon bg-primary">
<i class="fas fa-boxes"></i>
</div>
<div class="stat-details">
<h3><?php echo $stats['total_products']; ?></h3>
<p>Total Products</p>
<small class="text-muted">Active in inventory</small>
</div>
</div>
<div class="stat-card">
<div class="stat-icon bg-warning">
<i class="fas fa-exclamation-triangle"></i>
</div>
<div class="stat-details">
<h3><?php echo $stats['low_stock']; ?></h3>
<p>Low Stock Items</p>
<small class="text-muted">Need reordering</small>
</div>
</div>
<div class="stat-card">
<div class="stat-icon bg-success">
<i class="fas fa-shopping-cart"></i>
</div>
<div class="stat-details">
<h3><?php echo $stats['today_sales_count']; ?></h3>
<p>Today's Sales</p>
<h4><?php echo formatCurrency($stats['today_sales_total']); ?></h4>
</div>
</div>
<div class="stat-card">
<div class="stat-icon bg-info">
<i class="fas fa-truck"></i>
</div>
<div class="stat-details">
<h3><?php echo $stats['pending_orders']; ?></h3>
<p>Pending Orders</p>
<small class="text-muted">Awaiting delivery</small>
</div>
</div>
</div>
<!-- Charts Row -->
<div class="charts-row">
<div class="chart-container">
<h3>Sales Overview</h3>
<canvas id="salesChart"></canvas>
</div>
<div class="chart-container">
<h3>Stock Distribution</h3>
<canvas id="stockChart"></canvas>
</div>
</div>
<!-- Alerts Section -->
<?php if (!empty($low_stock) || !empty($expiring)): ?>
<div class="alerts-section">
<h3>Alerts & Notifications</h3>
<div class="alerts-grid">
<?php if (!empty($low_stock)): ?>
<div class="alert-card alert-warning">
<div class="alert-icon">
<i class="fas fa-exclamation-triangle"></i>
</div>
<div class="alert-content">
<h4>Low Stock Alert (<?php echo count($low_stock); ?> items)</h4>
<ul>
<?php foreach (array_slice($low_stock, 0, 5) as $item): ?>
<li>
<?php echo htmlspecialchars($item['product_name']); ?>
- Only <?php echo $item['quantity']; ?> left
(Min: <?php echo $item['reorder_level']; ?>)
</li>
<?php endforeach; ?>
</ul>
<a href="reports/low-stock.php" class="btn-link">View All <i class="fas fa-arrow-right"></i></a>
</div>
</div>
<?php endif; ?>
<?php if (!empty($expiring)): ?>
<div class="alert-card alert-danger">
<div class="alert-icon">
<i class="fas fa-calendar-times"></i>
</div>
<div class="alert-content">
<h4>Expiring Soon (<?php echo count($expiring); ?> items)</h4>
<ul>
<?php foreach (array_slice($expiring, 0, 5) as $item): ?>
<li>
<?php echo htmlspecialchars($item['product_name']); ?>
- Expires: <?php echo formatDate($item['expiry_date']); ?>
</li>
<?php endforeach; ?>
</ul>
<a href="reports/expiring.php" class="btn-link">View All <i class="fas fa-arrow-right"></i></a>
</div>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<!-- Recent Sales -->
<div class="recent-sales">
<div class="section-header">
<h3>Recent Sales</h3>
<a href="sales/index.php" class="btn-link">View All Sales <i class="fas fa-arrow-right"></i></a>
</div>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Order #</th>
<th>Customer</th>
<th>Amount</th>
<th>Payment Status</th>
<th>Date</th>
<th>Processed By</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($recent_sales as $sale): ?>
<tr>
<td>
<a href="sales/view.php?id=<?php echo $sale['so_id']; ?>">
<?php echo htmlspecialchars($sale['so_number']); ?>
</a>
</td>
<td><?php echo htmlspecialchars($sale['customer_name'] ?: 'Walk-in Customer'); ?></td>
<td><?php echo formatCurrency($sale['total_amount']); ?></td>
<td>
<span class="badge badge-<?php echo $sale['payment_status']; ?>">
<?php echo ucfirst($sale['payment_status']); ?>
</span>
</td>
<td><?php echo formatDateTime($sale['order_date']); ?></td>
<td><?php echo htmlspecialchars($sale['processed_by'] ?: 'N/A'); ?></td>
<td>
<a href="sales/invoice.php?id=<?php echo $sale['so_id']; ?>" class="btn-icon" title="Print Invoice">
<i class="fas fa-print"></i>
</a>
<a href="sales/view.php?id=<?php echo $sale['so_id']; ?>" class="btn-icon" title="View">
<i class="fas fa-eye"></i>
</a>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($recent_sales)): ?>
<tr>
<td colspan="7" class="text-center">No sales yet</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- Quick Actions -->
<div class="quick-actions">
<h3>Quick Actions</h3>
<div class="actions-grid">
<a href="products/add.php" class="action-card">
<i class="fas fa-plus-circle"></i>
<span>Add Product</span>
</a>
<a href="sales/create.php" class="action-card">
<i class="fas fa-shopping-cart"></i>
<span>New Sale</span>
</a>
<a href="purchases/create.php" class="action-card">
<i class="fas fa-truck"></i>
<span>Purchase Order</span>
</a>
<a href="transfers/create.php" class="action-card">
<i class="fas fa-exchange-alt"></i>
<span>Transfer Stock</span>
</a>
<a href="reports/stock-report.php" class="action-card">
<i class="fas fa-file-alt"></i>
<span>Stock Report</span>
</a>
<a href="adjustments/create.php" class="action-card">
<i class="fas fa-balance-scale"></i>
<span>Stock Adjustment</span>
</a>
</div>
</div>
</div>
</div>
</div>
<script>
// Sales Chart
const salesCtx = document.getElementById('salesChart').getContext('2d');
new Chart(salesCtx, {
type: 'line',
data: {
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
datasets: [{
label: 'Sales',
data: [1200, 1900, 1500, 2100, 1800, 2400, 2000],
borderColor: '#4361ee',
backgroundColor: 'rgba(67, 97, 238, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return '$' + value;
}
}
}
}
}
});
// Stock Distribution Chart
const stockCtx = document.getElementById('stockChart').getContext('2d');
new Chart(stockCtx, {
type: 'doughnut',
data: {
labels: ['In Stock', 'Low Stock', 'Out of Stock'],
datasets: [{
data: [70, 20, 10],
backgroundColor: ['#4cc9f0', '#f8961e', '#f72585'],
borderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
function refreshDashboard() {
// Refresh dashboard data via AJAX
location.reload();
}
</script>
</body>
</html>
5. admin/products/index.php
<?php
require_once '../../includes/config.php';
require_once '../../includes/functions.php';
require_once '../../includes/auth.php';
// Check permissions
if (!isLoggedIn() || !(isAdmin() || isManager())) {
redirect('../login.php');
}
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
$category = isset($_GET['category']) ? (int)$_GET['category'] : 0;
$warehouse = isset($_GET['warehouse']) ? (int)$_GET['warehouse'] : 0;
$offset = ($page - 1) * ITEMS_PER_PAGE;
// Build query
$sql = "SELECT p.*, c.category_name,
(SELECT SUM(quantity) FROM stock WHERE product_id = p.product_id) as total_stock
FROM products p
LEFT JOIN categories c ON p.category_id = c.category_id
WHERE 1=1";
$count_sql = "SELECT COUNT(*) as total FROM products p WHERE 1=1";
$params = [];
$types = "";
if (!empty($search)) {
$sql .= " AND (p.product_name LIKE ? OR p.product_code LIKE ? OR p.description LIKE ?)";
$count_sql .= " AND (p.product_name LIKE ? OR p.product_code LIKE ? OR p.description LIKE ?)";
$search_term = "%$search%";
$params = array_merge($params, [$search_term, $search_term, $search_term]);
$types .= "sss";
}
if ($category > 0) {
$sql .= " AND p.category_id = ?";
$count_sql .= " AND p.category_id = ?";
$params[] = $category;
$types .= "i";
}
// Get total count
$stmt = $conn->prepare($count_sql);
if (!empty($params)) {
$stmt->bind_param($types, ...$params);
}
$stmt->execute();
$total_items = $stmt->get_result()->fetch_assoc()['total'];
$total_pages = ceil($total_items / ITEMS_PER_PAGE);
// Get products
$sql .= " ORDER BY p.created_at DESC LIMIT ? OFFSET ?";
$params[] = ITEMS_PER_PAGE;
$params[] = $offset;
$types .= "ii";
$stmt = $conn->prepare($sql);
if (!empty($params)) {
$stmt->bind_param($types, ...$params);
}
$stmt->execute();
$products = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
// Get categories for filter
$categories = $conn->query("SELECT * FROM categories WHERE is_active = 1 ORDER BY category_name")->fetch_all(MYSQLI_ASSOC);
// Get warehouses for filter
$warehouses = $conn->query("SELECT * FROM warehouses WHERE is_active = 1 ORDER BY warehouse_name")->fetch_all(MYSQLI_ASSOC);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Products - <?php echo SITE_NAME; ?></title>
<link rel="stylesheet" href="../../assets/css/admin-style.css">
<link rel="stylesheet" href="https://cdn.datatables.net/1.11.5/css/dataTables.bootstrap5.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body>
<div class="admin-container">
<!-- Sidebar -->
<?php include '../includes/sidebar.php'; ?>
<!-- Main Content -->
<div class="main-content">
<!-- Top Navigation -->
<?php include '../includes/topnav.php'; ?>
<!-- Content -->
<div class="content-wrapper">
<div class="page-header">
<h1>Products</h1>
<div class="header-actions">
<a href="add.php" class="btn btn-primary">
<i class="fas fa-plus"></i> Add Product
</a>
<a href="import.php" class="btn btn-secondary">
<i class="fas fa-upload"></i> Import
</a>
<a href="export.php" class="btn btn-secondary">
<i class="fas fa-download"></i> Export
</a>
</div>
</div>
<!-- Filters -->
<div class="filters-card">
<form method="GET" class="filters-form">
<div class="filter-group">
<input type="text" name="search" placeholder="Search products..."
value="<?php echo htmlspecialchars($search); ?>" class="form-control">
</div>
<div class="filter-group">
<select name="category" class="form-select">
<option value="0">All Categories</option>
<?php foreach ($categories as $cat): ?>
<option value="<?php echo $cat['category_id']; ?>"
<?php echo $category == $cat['category_id'] ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($cat['category_name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="filter-group">
<select name="warehouse" class="form-select">
<option value="0">All Warehouses</option>
<?php foreach ($warehouses as $wh): ?>
<option value="<?php echo $wh['warehouse_id']; ?>"
<?php echo $warehouse == $wh['warehouse_id'] ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($wh['warehouse_name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary">Filter</button>
<a href="index.php" class="btn btn-secondary">Clear</a>
</form>
</div>
<!-- Products Table -->
<div class="table-card">
<div class="table-responsive">
<table class="table" id="productsTable">
<thead>
<tr>
<th>Image</th>
<th>Code</th>
<th>Name</th>
<th>Category</th>
<th>Price</th>
<th>Stock</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($products as $product): ?>
<tr>
<td>
<img src="../../uploads/products/<?php echo $product['product_image'] ?? 'default.jpg'; ?>"
alt="<?php echo htmlspecialchars($product['product_name']); ?>"
class="product-thumbnail">
</td>
<td><?php echo htmlspecialchars($product['product_code']); ?></td>
<td>
<a href="view.php?id=<?php echo $product['product_id']; ?>">
<?php echo htmlspecialchars($product['product_name']); ?>
</a>
</td>
<td><?php echo htmlspecialchars($product['category_name'] ?? 'Uncategorized'); ?></td>
<td>
<div>Purchase: <?php echo formatCurrency($product['purchase_price']); ?></div>
<div>Selling: <?php echo formatCurrency($product['selling_price']); ?></div>
</td>
<td>
<?php
$stock = $product['total_stock'] ?? 0;
$stock_class = $stock <= $product['reorder_level'] ? 'text-danger' : 'text-success';
?>
<span class="<?php echo $stock_class; ?>">
<?php echo $stock; ?> <?php echo $product['unit']; ?>
</span>
<?php if ($stock <= $product['reorder_level']): ?>
<i class="fas fa-exclamation-triangle text-warning" title="Low Stock"></i>
<?php endif; ?>
</td>
<td>
<span class="badge badge-<?php echo $product['is_active'] ? 'success' : 'danger'; ?>">
<?php echo $product['is_active'] ? 'Active' : 'Inactive'; ?>
</span>
</td>
<td>
<div class="action-buttons">
<a href="view.php?id=<?php echo $product['product_id']; ?>" class="btn-icon" title="View">
<i class="fas fa-eye"></i>
</a>
<a href="edit.php?id=<?php echo $product['product_id']; ?>" class="btn-icon" title="Edit">
<i class="fas fa-edit"></i>
</a>
<a href="barcode.php?id=<?php echo $product['product_id']; ?>" class="btn-icon" title="Print Barcode">
<i class="fas fa-barcode"></i>
</a>
<?php if (isAdmin()): ?>
<a href="delete.php?id=<?php echo $product['product_id']; ?>"
class="btn-icon text-danger"
onclick="return confirm('Are you sure you want to delete this product?')"
title="Delete">
<i class="fas fa-trash"></i>
</a>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($products)): ?>
<tr>
<td colspan="8" class="text-center">No products found</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Pagination -->
<?php if ($total_pages > 1): ?>
<div class="pagination-wrapper">
<?php
$url_pattern = "index.php?page={page}&search=" . urlencode($search) . "&category=$category&warehouse=$warehouse";
echo paginate($page, $total_pages, $url_pattern);
?>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.datatables.net/1.11.5/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.11.5/js/dataTables.bootstrap5.min.js"></script>
<script>
$(document).ready(function() {
$('#productsTable').DataTable({
paging: false,
searching: false,
ordering: true,
info: false
});
});
</script>
</body>
</html>
6. staff/sales/create.php (Point of Sale Interface)
<?php
require_once '../../includes/config.php';
require_once '../../includes/functions.php';
require_once '../../includes/auth.php';
// Check if staff is logged in
if (!isLoggedIn() || !isStaff()) {
redirect('login.php');
}
$warehouse_id = $_SESSION['warehouse_id'];
// Get products for this warehouse
$sql = "SELECT p.*, s.quantity, s.available_quantity
FROM products p
JOIN stock s ON p.product_id = s.product_id
WHERE s.warehouse_id = ? AND p.is_active = 1 AND s.quantity > 0
ORDER BY p.product_name";
$stmt = $conn->prepare($sql);
$stmt->bind_param("i", $warehouse_id);
$stmt->execute();
$products = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Point of Sale - <?php echo SITE_NAME; ?></title>
<link rel="stylesheet" href="../../assets/css/admin-style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body>
<div class="pos-container">
<!-- POS Header -->
<div class="pos-header">
<div class="pos-title">
<h1>Point of Sale</h1>
<span class="warehouse-badge"><?php echo $_SESSION['warehouse_name'] ?? 'Main Warehouse'; ?></span>
</div>
<div class="pos-actions">
<button class="btn btn-secondary" onclick="window.location.href='history.php'">
<i class="fas fa-history"></i> Sales History
</button>
<button class="btn btn-secondary" onclick="window.location.href='../dashboard.php'">
<i class="fas fa-arrow-left"></i> Back to Dashboard
</button>
</div>
</div>
<!-- POS Main Content -->
<div class="pos-main">
<!-- Products Grid -->
<div class="products-section">
<div class="search-bar">
<i class="fas fa-search"></i>
<input type="text" id="productSearch" placeholder="Search products by name or scan barcode...">
</div>
<div class="category-tabs">
<button class="category-tab active" data-category="all">All</button>
<?php
$categories = $conn->query("SELECT * FROM categories WHERE is_active = 1 ORDER BY category_name");
while ($cat = $categories->fetch_assoc()):
?>
<button class="category-tab" data-category="<?php echo $cat['category_id']; ?>">
<?php echo htmlspecialchars($cat['category_name']); ?>
</button>
<?php endwhile; ?>
</div>
<div class="products-grid" id="productsGrid">
<?php foreach ($products as $product): ?>
<div class="product-card"
data-id="<?php echo $product['product_id']; ?>"
data-name="<?php echo htmlspecialchars($product['product_name']); ?>"
data-price="<?php echo $product['selling_price']; ?>"
data-stock="<?php echo $product['available_quantity']; ?>"
data-category="<?php echo $product['category_id']; ?>">
<div class="product-image">
<img src="../../uploads/products/<?php echo $product['product_image'] ?? 'default.jpg'; ?>"
alt="<?php echo htmlspecialchars($product['product_name']); ?>">
</div>
<div class="product-info">
<h4><?php echo htmlspecialchars($product['product_name']); ?></h4>
<p class="product-code"><?php echo $product['product_code']; ?></p>
<div class="product-price">
<?php echo formatCurrency($product['selling_price']); ?>
</div>
<div class="product-stock">
Stock: <?php echo $product['available_quantity']; ?> <?php echo $product['unit']; ?>
</div>
</div>
<button class="add-to-cart-btn" onclick="addToCart(<?php echo $product['product_id']; ?>)">
<i class="fas fa-cart-plus"></i>
</button>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- Cart Section -->
<div class="cart-section">
<h3>Shopping Cart</h3>
<div class="cart-items" id="cartItems">
<!-- Cart items will be added here dynamically -->
</div>
<div class="cart-summary">
<div class="summary-row">
<span>Subtotal:</span>
<span id="subtotal">$0.00</span>
</div>
<div class="summary-row">
<span>Tax (<?php echo DEFAULT_TAX_RATE; ?>%):</span>
<span id="tax">$0.00</span>
</div>
<div class="summary-row total">
<span>Total:</span>
<span id="total">$0.00</span>
</div>
</div>
<div class="customer-info">
<input type="text" id="customerName" placeholder="Customer Name (optional)">
<input type="text" id="customerPhone" placeholder="Customer Phone (optional)">
</div>
<div class="payment-section">
<select id="paymentMethod" class="form-select">
<option value="cash">Cash</option>
<option value="card">Credit/Debit Card</option>
<option value="mobile">Mobile Money</option>
<option value="credit">Credit Account</option>
</select>
<div class="payment-actions">
<button class="btn btn-success btn-large" onclick="processSale()">
<i class="fas fa-check"></i> Complete Sale
</button>
<button class="btn btn-secondary" onclick="clearCart()">
<i class="fas fa-trash"></i> Clear Cart
</button>
</div>
</div>
</div>
</div>
</div>
<script>
// Cart array
let cart = [];
// Add to cart function
function addToCart(productId) {
const productCard = document.querySelector(`.product-card[data-id="${productId}"]`);
const productName = productCard.dataset.name;
const productPrice = parseFloat(productCard.dataset.price);
const maxStock = parseInt(productCard.dataset.stock);
// Check if product already in cart
const existingItem = cart.find(item => item.id === productId);
if (existingItem) {
if (existingItem.quantity < maxStock) {
existingItem.quantity++;
} else {
alert('Maximum stock limit reached');
return;
}
} else {
cart.push({
id: productId,
name: productName,
price: productPrice,
quantity: 1,
maxStock: maxStock
});
}
updateCartDisplay();
}
// Update cart display
function updateCartDisplay() {
const cartContainer = document.getElementById('cartItems');
const subtotalSpan = document.getElementById('subtotal');
const taxSpan = document.getElementById('tax');
const totalSpan = document.getElementById('total');
let subtotal = 0;
let html = '';
cart.forEach((item, index) => {
subtotal += item.price * item.quantity;
html += `
<div class="cart-item">
<div class="item-details">
<h5>${item.name}</h5>
<div class="item-price">${formatCurrency(item.price)} x ${item.quantity}</div>
</div>
<div class="item-actions">
<div class="quantity-controls">
<button onclick="updateQuantity(${index}, -1)" ${item.quantity <= 1 ? 'disabled' : ''}>-</button>
<span>${item.quantity}</span>
<button onclick="updateQuantity(${index}, 1)" ${item.quantity >= item.maxStock ? 'disabled' : ''}>+</button>
</div>
<button class="remove-item" onclick="removeFromCart(${index})">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`;
});
if (cart.length === 0) {
html = '<div class="empty-cart">Your cart is empty</div>';
}
cartContainer.innerHTML = html;
const tax = subtotal * (<?php echo DEFAULT_TAX_RATE; ?> / 100);
const total = subtotal + tax;
subtotalSpan.textContent = formatCurrency(subtotal);
taxSpan.textContent = formatCurrency(tax);
totalSpan.textContent = formatCurrency(total);
}
// Update quantity
function updateQuantity(index, change) {
const item = cart[index];
const newQuantity = item.quantity + change;
if (newQuantity >= 1 && newQuantity <= item.maxStock) {
item.quantity = newQuantity;
updateCartDisplay();
}
}
// Remove from cart
function removeFromCart(index) {
cart.splice(index, 1);
updateCartDisplay();
}
// Clear cart
function clearCart() {
if (confirm('Clear all items from cart?')) {
cart = [];
updateCartDisplay();
}
}
// Process sale
function processSale() {
if (cart.length === 0) {
alert('Cart is empty');
return;
}
const customerName = document.getElementById('customerName').value || 'Walk-in Customer';
const customerPhone = document.getElementById('customerPhone').value;
const paymentMethod = document.getElementById('paymentMethod').value;
const saleData = {
customer_name: customerName,
customer_phone: customerPhone,
payment_method: paymentMethod,
items: cart
};
// Send to server
fetch('../../api/process-sale.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(saleData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Sale completed successfully!');
window.location.href = 'invoice.php?id=' + data.sale_id;
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred');
});
}
// Search products
document.getElementById('productSearch').addEventListener('input', function(e) {
const searchTerm = e.target.value.toLowerCase();
const products = document.querySelectorAll('.product-card');
products.forEach(product => {
const name = product.dataset.name.toLowerCase();
if (name.includes(searchTerm)) {
product.style.display = 'block';
} else {
product.style.display = 'none';
}
});
});
// Category filter
document.querySelectorAll('.category-tab').forEach(tab => {
tab.addEventListener('click', function() {
// Update active tab
document.querySelectorAll('.category-tab').forEach(t => t.classList.remove('active'));
this.classList.add('active');
const category = this.dataset.category;
const products = document.querySelectorAll('.product-card');
products.forEach(product => {
if (category === 'all' || product.dataset.category === category) {
product.style.display = 'block';
} else {
product.style.display = 'none';
}
});
});
});
// Format currency
function formatCurrency(amount) {
return '$' + amount.toFixed(2);
}
</script>
<style>
.pos-container {
display: flex;
flex-direction: column;
height: 100vh;
background: #f8f9fa;
}
.pos-header {
background: white;
padding: 1rem 2rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.pos-title {
display: flex;
align-items: center;
gap: 1rem;
}
.pos-title h1 {
margin: 0;
font-size: 1.5rem;
}
.warehouse-badge {
background: var(--primary-color);
color: white;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.875rem;
}
.pos-main {
display: flex;
flex: 1;
overflow: hidden;
}
.products-section {
flex: 2;
padding: 1rem;
overflow-y: auto;
}
.search-bar {
position: relative;
margin-bottom: 1rem;
}
.search-bar i {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: var(--gray-color);
}
.search-bar input {
width: 100%;
padding: 0.75rem 1rem 0.75rem 2.5rem;
border: 1px solid var(--border-color);
border-radius: 8px;
font-size: 1rem;
}
.category-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
overflow-x: auto;
padding-bottom: 0.5rem;
}
.category-tab {
padding: 0.5rem 1rem;
background: white;
border: 1px solid var(--border-color);
border-radius: 20px;
cursor: pointer;
white-space: nowrap;
}
.category-tab.active {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
.product-card {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
position: relative;
cursor: pointer;
transition: var(--transition);
}
.product-card:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0,0,0,0.15);
}
.product-image {
height: 150px;
overflow: hidden;
}
.product-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.product-info {
padding: 1rem;
}
.product-info h4 {
margin: 0 0 0.25rem;
font-size: 1rem;
}
.product-code {
color: var(--gray-color);
font-size: 0.75rem;
margin-bottom: 0.5rem;
}
.product-price {
font-weight: bold;
color: var(--primary-color);
margin-bottom: 0.25rem;
}
.product-stock {
font-size: 0.75rem;
color: var(--gray-color);
}
.add-to-cart-btn {
position: absolute;
top: 0.5rem;
right: 0.5rem;
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--primary-color);
color: white;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: var(--transition);
}
.product-card:hover .add-to-cart-btn {
opacity: 1;
}
.cart-section {
flex: 1;
background: white;
border-left: 1px solid var(--border-color);
display: flex;
flex-direction: column;
padding: 1rem;
}
.cart-section h3 {
margin: 0 0 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--border-color);
}
.cart-items {
flex: 1;
overflow-y: auto;
margin-bottom: 1rem;
}
.cart-item {
background: var(--light-color);
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 0.5rem;
}
.item-details h5 {
margin: 0 0 0.25rem;
font-size: 0.9rem;
}
.item-price {
font-size: 0.8rem;
color: var(--gray-color);
}
.item-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0.5rem;
}
.quantity-controls {
display: flex;
align-items: center;
gap: 0.5rem;
}
.quantity-controls button {
width: 24px;
height: 24px;
border-radius: 4px;
border: 1px solid var(--border-color);
background: white;
cursor: pointer;
}
.quantity-controls button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.remove-item {
color: var(--danger-color);
background: none;
border: none;
cursor: pointer;
}
.cart-summary {
border-top: 2px solid var(--border-color);
padding-top: 1rem;
margin-bottom: 1rem;
}
.summary-row {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.summary-row.total {
font-size: 1.25rem;
font-weight: bold;
color: var(--primary-color);
border-top: 1px solid var(--border-color);
padding-top: 0.5rem;
margin-top: 0.5rem;
}
.customer-info input {
width: 100%;
padding: 0.5rem;
margin-bottom: 0.5rem;
border: 1px solid var(--border-color);
border-radius: 4px;
}
.payment-section select {
width: 100%;
padding: 0.5rem;
margin-bottom: 1rem;
}
.payment-actions {
display: flex;
gap: 0.5rem;
}
.btn-large {
flex: 1;
padding: 1rem;
}
.empty-cart {
text-align: center;
color: var(--gray-color);
padding: 2rem;
}
</style>
</body>
</html>
7. api/process-sale.php
<?php
require_once '../includes/config.php';
require_once '../includes/functions.php';
require_once '../includes/auth.php';
header('Content-Type: application/json');
// Check if user is logged in
if (!isLoggedIn()) {
echo json_encode(['success' => false, 'message' => 'Not authenticated']);
exit();
}
// Get POST data
$data = json_decode(file_get_contents('php://input'), true);
if (!$data || empty($data['items'])) {
echo json_encode(['success' => false, 'message' => 'Invalid request data']);
exit();
}
$user_id = $_SESSION['user_id'];
$warehouse_id = $_SESSION['warehouse_id'] ?? 1;
$customer_name = $conn->escapeString($data['customer_name'] ?? 'Walk-in Customer');
$customer_phone = $conn->escapeString($data['customer_phone'] ?? '');
$payment_method = $conn->escapeString($data['payment_method'] ?? 'cash');
// Begin transaction
$conn->begin_transaction();
try {
// Calculate totals
$subtotal = 0;
$items = $data['items'];
foreach ($items as $item) {
// Verify stock availability
$sql = "SELECT available_quantity, selling_price FROM stock s
JOIN products p ON s.product_id = p.product_id
WHERE s.product_id = ? AND s.warehouse_id = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ii", $item['id'], $warehouse_id);
$stmt->execute();
$result = $stmt->get_result();
$product = $result->fetch_assoc();
if (!$product || $product['available_quantity'] < $item['quantity']) {
throw new Exception("Insufficient stock for product ID: " . $item['id']);
}
$subtotal += $product['selling_price'] * $item['quantity'];
}
$tax_rate = DEFAULT_TAX_RATE;
$tax_amount = $subtotal * ($tax_rate / 100);
$total_amount = $subtotal + $tax_amount;
// Generate order number
$so_number = generateCode('SALE');
// Insert sales order
$sql = "INSERT INTO sales_orders (so_number, customer_name, customer_phone, warehouse_id, user_id,
subtotal, tax_amount, total_amount, payment_method, payment_status, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'paid', 'completed')";
$stmt = $conn->prepare($sql);
$stmt->bind_param("sssidddds", $so_number, $customer_name, $customer_phone, $warehouse_id,
$user_id, $subtotal, $tax_amount, $total_amount, $payment_method);
$stmt->execute();
$so_id = $conn->insert_id;
// Insert order items and update stock
foreach ($items as $item) {
// Get product price
$sql = "SELECT selling_price FROM products WHERE product_id = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("i", $item['id']);
$stmt->execute();
$price = $stmt->get_result()->fetch_assoc()['selling_price'];
// Insert order item
$sql = "INSERT INTO sales_order_items (so_id, product_id, quantity, unit_price)
VALUES (?, ?, ?, ?)";
$stmt = $conn->prepare($sql);
$stmt->bind_param("iiid", $so_id, $item['id'], $item['quantity'], $price);
$stmt->execute();
// Update stock
$sql = "UPDATE stock SET quantity = quantity - ?
WHERE product_id = ? AND warehouse_id = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("iii", $item['quantity'], $item['id'], $warehouse_id);
$stmt->execute();
// Log stock movement
logStockMovement($item['id'], $warehouse_id, null, $item['quantity'], 'sale', 'sales_order', $so_id);
}
// Log activity
logActivity('Created sale', 'sales_order', $so_id, "Sale amount: " . formatCurrency($total_amount));
$conn->commit();
echo json_encode([
'success' => true,
'sale_id' => $so_id,
'sale_number' => $so_number,
'message' => 'Sale completed successfully'
]);
} catch (Exception $e) {
$conn->rollback();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
?>
8. assets/css/admin-style.css
/* Admin Panel Styles */
:root {
--sidebar-width: 250px;
--sidebar-bg: #1e1b4b;
--sidebar-hover: #2a2356;
--primary-color: #4361ee;
--success-color: #4cc9f0;
--warning-color: #f8961e;
--danger-color: #f72585;
--dark-color: #1e1b4b;
--light-color: #f8f9fa;
--gray-color: #6c757d;
--border-color: #dee2e6;
}
/* Admin Container */
.admin-container {
display: flex;
min-height: 100vh;
}
/* Sidebar */
.sidebar {
width: var(--sidebar-width);
background: var(--sidebar-bg);
color: white;
position: fixed;
height: 100vh;
overflow-y: auto;
transition: all 0.3s;
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.sidebar-header h3 {
margin: 0;
color: white;
font-size: 1.25rem;
}
.sidebar-header p {
margin: 0.25rem 0 0;
font-size: 0.75rem;
opacity: 0.7;
}
.sidebar-menu {
padding: 1rem 0;
}
.sidebar-menu ul {
list-style: none;
padding: 0;
margin: 0;
}
.sidebar-menu li {
margin-bottom: 0.25rem;
}
.sidebar-menu a {
display: flex;
align-items: center;
padding: 0.75rem 1.5rem;
color: rgba(255,255,255,0.8);
text-decoration: none;
transition: all 0.3s;
}
.sidebar-menu a:hover,
.sidebar-menu a.active {
background: var(--sidebar-hover);
color: white;
}
.sidebar-menu i {
width: 24px;
margin-right: 0.75rem;
font-size: 1.1rem;
}
.sidebar-menu .menu-arrow {
margin-left: auto;
transition: transform 0.3s;
}
.sidebar-menu .submenu {
display: none;
padding-left: 2.5rem;
background: rgba(0,0,0,0.1);
}
.sidebar-menu .submenu a {
padding: 0.5rem 1.5rem;
font-size: 0.9rem;
}
.sidebar-menu li.open > .submenu {
display: block;
}
.sidebar-menu li.open > a > .menu-arrow {
transform: rotate(90deg);
}
/* Main Content */
.main-content {
flex: 1;
margin-left: var(--sidebar-width);
background: #f5f5f5;
min-height: 100vh;
}
/* Top Navigation */
.top-nav {
background: white;
padding: 1rem 2rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.nav-left {
display: flex;
align-items: center;
}
.menu-toggle {
display: none;
background: none;
border: none;
font-size: 1.5rem;
color: var(--dark-color);
cursor: pointer;
margin-right: 1rem;
}
.page-title {
font-size: 1.25rem;
font-weight: 500;
color: var(--dark-color);
}
.nav-right {
display: flex;
align-items: center;
gap: 1.5rem;
}
.nav-item {
position: relative;
}
.nav-icon {
font-size: 1.25rem;
color: var(--gray-color);
cursor: pointer;
position: relative;
}
.badge {
position: absolute;
top: -5px;
right: -5px;
background: var(--danger-color);
color: white;
font-size: 0.65rem;
padding: 0.15rem 0.35rem;
border-radius: 50%;
}
.user-profile {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.user-profile img {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
}
/* Content Wrapper */
.content-wrapper {
padding: 2rem;
}
/* Page Header */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.page-header h1 {
margin: 0;
font-size: 1.75rem;
color: var(--dark-color);
}
.header-actions {
display: flex;
gap: 0.5rem;
}
/* Statistics Cards */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: white;
border-radius: 10px;
padding: 1.5rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
display: flex;
align-items: center;
gap: 1rem;
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
}
.stat-icon.bg-primary {
background: rgba(67, 97, 238, 0.1);
color: var(--primary-color);
}
.stat-icon.bg-success {
background: rgba(76, 201, 240, 0.1);
color: var(--success-color);
}
.stat-icon.bg-warning {
background: rgba(248, 150, 30, 0.1);
color: var(--warning-color);
}
.stat-icon.bg-danger {
background: rgba(247, 37, 133, 0.1);
color: var(--danger-color);
}
.stat-details h3 {
margin: 0;
font-size: 1.5rem;
color: var(--dark-color);
}
.stat-details p {
margin: 0.25rem 0 0;
color: var(--gray-color);
font-size: 0.875rem;
}
.stat-details h4 {
margin: 0.25rem 0 0;
font-size: 1rem;
color: var(--primary-color);
}
/* Charts Row */
.charts-row {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 1.5rem;
margin-bottom: 2rem;
}
.chart-container {
background: white;
border-radius: 10px;
padding: 1.5rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.chart-container h3 {
margin: 0 0 1rem;
font-size: 1.1rem;
color: var(--dark-color);
}
.chart-container canvas {
width: 100% !important;
height: 300px !important;
}
/* Alerts Section */
.alerts-section {
margin-bottom: 2rem;
}
.alerts-section h3 {
margin-bottom: 1rem;
}
.alerts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 1.5rem;
}
.alert-card {
background: white;
border-radius: 10px;
padding: 1.5rem;
display: flex;
gap: 1rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.alert-card.alert-warning {
border-left: 4px solid var(--warning-color);
}
.alert-card.alert-danger {
border-left: 4px solid var(--danger-color);
}
.alert-icon {
font-size: 2rem;
}
.alert-warning .alert-icon {
color: var(--warning-color);
}
.alert-danger .alert-icon {
color: var(--danger-color);
}
.alert-content {
flex: 1;
}
.alert-content h4 {
margin: 0 0 0.5rem;
font-size: 1rem;
}
.alert-content ul {
margin: 0 0 0.5rem;
padding-left: 1rem;
}
.alert-content li {
margin-bottom: 0.25rem;
color: var(--gray-color);
}
.btn-link {
color: var(--primary-color);
text-decoration: none;
font-size: 0.875rem;
}
/* Tables */
.table-card {
background: white;
border-radius: 10px;
padding: 1.5rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.table-responsive {
overflow-x: auto;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th {
text-align: left;
padding: 1rem;
background: var(--light-color);
color: var(--dark-color);
font-weight: 600;
font-size: 0.875rem;
border-bottom: 2px solid var(--border-color);
}
.table td {
padding: 1rem;
border-bottom: 1px solid var(--border-color);
vertical-align: middle;
}
.table tbody tr:hover {
background: var(--light-color);
}
/* Product Thumbnail */
.product-thumbnail {
width: 50px;
height: 50px;
border-radius: 5px;
object-fit: cover;
}
/* Badges */
.badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
}
.badge-success {
background: rgba(76, 201, 240, 0.1);
color: var(--success-color);
}
.badge-warning {
background: rgba(248, 150, 30, 0.1);
color: var(--warning-color);
}
.badge-danger {
background: rgba(247, 37, 133, 0.1);
color: var(--danger-color);
}
.badge-info {
background: rgba(67, 97, 238, 0.1);
color: var(--primary-color);
}
.badge-paid {
background: #d4edda;
color: #155724;
}
.badge-unpaid {
background: #f8d7da;
color: #721c24;
}
/* Action Buttons */
.action-buttons {
display: flex;
gap: 0.5rem;
}
.btn-icon {
width: 32px;
height: 32px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
background: var(--light-color);
color: var(--gray-color);
transition: var(--transition);
border: none;
cursor: pointer;
}
.btn-icon:hover {
background: var(--primary-color);
color: white;
}
.btn-icon.text-danger:hover {
background: var(--danger-color);
color: white;
}
/* Forms */
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--dark-color);
}
.form-control {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 1rem;
transition: var(--transition);
}
.form-control:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.1);
}
.form-select {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-color);
border-radius: 4px;
background: white;
font-size: 1rem;
}
textarea.form-control {
min-height: 100px;
resize: vertical;
}
/* Filters Card */
.filters-card {
background: white;
border-radius: 10px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.filters-form {
display: flex;
gap: 1rem;
align-items: flex-end;
flex-wrap: wrap;
}
.filter-group {
flex: 1;
min-width: 200px;
}
/* Pagination */
.pagination-wrapper {
margin-top: 2rem;
display: flex;
justify-content: center;
}
.pagination {
display: flex;
gap: 0.25rem;
list-style: none;
padding: 0;
}
.page-item {
margin: 0;
}
.page-link {
display: block;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-color);
border-radius: 4px;
color: var(--primary-color);
text-decoration: none;
transition: var(--transition);
}
.page-item.active .page-link {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.page-item.disabled .page-link {
color: var(--gray-color);
pointer-events: none;
}
/* Responsive */
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
z-index: 1000;
}
.sidebar.open {
transform: translateX(0);
}
.main-content {
margin-left: 0;
}
.menu-toggle {
display: block;
}
.stats-grid {
grid-template-columns: 1fr;
}
.charts-row {
grid-template-columns: 1fr;
}
.alerts-grid {
grid-template-columns: 1fr;
}
.filters-form {
flex-direction: column;
}
.filter-group {
width: 100%;
}
}
How to Use This Project - Step by Step Guide
Step 1: System Requirements
- XAMPP/WAMP/MAMP (PHP 7.4+)
- MySQL 5.7+
- Web browser (Chrome, Firefox)
- 2GB RAM minimum
- 1GB free disk space
Step 2: Installation
- Download and Extract
# Navigate to htdocs (XAMPP) or www (WAMP) cd C:\xampp\htdocs\ # Create project folder mkdir inventory-management-system # Extract all files into this folder
- Database Setup
- Open phpMyAdmin (http://localhost/phpmyadmin)
- Create new database:
inventory_db - Import
database/inventory_db.sql - Verify all tables are created (users, products, categories, etc.)
- Configure Project
- Open
includes/config.php - Update database credentials if needed:
define('DB_HOST', 'localhost');
define('DB_USER', 'root');
define('DB_PASS', ''); // Add password if set
define('DB_NAME', 'inventory_db');
- Create Required Directories
# Create upload directories mkdir uploads mkdir uploads/products mkdir uploads/imports mkdir exports mkdir exports/reports # Set permissions (Linux/Mac) chmod -R 755 uploads/ chmod -R 755 exports/ # Windows: Ensure write permissions for these folders
Step 3: Initial Login
Default Admin Account:
- URL: http://localhost/inventory-management-system/admin/login.php
- Username: admin
- Password: Admin@123
Default Manager Account:
- Username: manager
- Password: Manager@123
Default Staff Account:
- Username: staff
- Password: Staff@123
Step 4: Initial Setup
- Configure Company Settings
- Login as admin
- Go to Settings > General
- Update:
- Company name and address
- Currency and tax rate
- Date format
- Low stock threshold
- Add Warehouses
- Go to Warehouses > Add Warehouse
- Add your physical warehouse locations
- Assign managers to each warehouse
- Add Categories
- Go to Categories > Add Category
- Create product categories (Electronics, Clothing, etc.)
- Add Suppliers
- Go to Suppliers > Add Supplier
- Add your supplier information
- Add Products
- Go to Products > Add Product
- Add initial stock products
- Upload product images
- Set prices and stock levels
Step 5: Using the System
Admin Functions:
- Dashboard Overview
- View key metrics
- Monitor low stock alerts
- Check recent sales
- View charts and statistics
- Product Management
- Add new products
- Edit product details
- Import products via CSV
- Export product list
- Generate barcodes
- Category Management
- Create categories
- Set parent categories
- Organize product hierarchy
- Supplier Management
- Add supplier details
- Track supplier performance
- Manage contact information
- Purchase Orders
- Create purchase orders
- Track order status
- Receive stock
- Print purchase orders
- Sales Management
- View all sales
- Process returns
- Print invoices
- Track payment status
- Warehouse Management
- Manage multiple warehouses
- Transfer stock between warehouses
- Monitor stock levels per warehouse
- Stock Adjustments
- Add/remove stock
- Record damaged items
- Track expired products
- Inventory counts
- Reports
- Stock reports
- Sales reports
- Purchase reports
- Profit/Loss statements
- Low stock alerts
- Expiry reports
- User Management
- Create staff accounts
- Set permissions
- Manage roles
- View activity logs
Manager Functions:
- Dashboard Access
- View warehouse-specific stats
- Monitor team performance
- Check stock alerts
- Product Management
- View products
- Update stock
- Request purchases
- Sales Processing
- Create sales
- Print invoices
- Process returns
- Stock Transfers
- Request transfers
- Receive transfers
- Track transfer status
- Reports
- Generate warehouse reports
- View team performance
Staff Functions:
- Point of Sale (POS)
- Process customer sales
- Scan barcodes
- Calculate totals
- Print receipts
- Stock Check
- View available stock
- Check product locations
- Report low stock
- Purchase Requests
- Create purchase requests
- Receive incoming stock
- Simple Reports
- View current stock
- Daily sales summary
Step 6: Daily Operations
Processing a Sale:
- Go to Staff > Point of Sale
- Scan products or search manually
- Add items to cart
- Enter customer details
- Select payment method
- Complete sale
- Print receipt/invoice
Receiving Stock:
- Go to Purchases > Purchase Orders
- Select pending order
- Click "Receive Stock"
- Verify quantities
- Confirm receipt
- Stock is automatically updated
Transferring Stock:
- Go to Transfers > Create Transfer
- Select source and destination warehouses
- Add products and quantities
- Submit transfer request
- Approve and process transfer
- Receive at destination
Stock Adjustment:
- Go to Adjustments > Create Adjustment
- Select warehouse and product
- Enter new quantity
- Select adjustment type (add/remove/damage)
- Add reason
- Submit adjustment
Step 7: Reports and Analysis
- Daily Sales Report
- Go to Reports > Sales Report
- Select date range
- View sales summary
- Export to PDF/Excel
- Stock Status Report
- Go to Reports > Stock Report
- View all products with stock levels
- Filter by category/warehouse
- Export for inventory count
- Low Stock Alert
- Automated alerts on dashboard
- Email notifications (if configured)
- Reorder suggestions
- Profit/Loss Report
- Compare sales vs purchases
- Calculate profit margins
- View by period
Step 8: Advanced Features
- Barcode Scanning
- Generate barcodes for products
- Print barcode labels
- Scan at POS for quick entry
- Mobile scanner support
- Batch Tracking
- Track by batch numbers
- Monitor expiry dates
- FIFO/FEFO support
- Multiple Warehouses
- Separate stock locations
- Inter-warehouse transfers
- Consolidated reporting
- Import/Export
- Bulk product import via CSV
- Export reports to Excel
- Data backup
Step 9: Security Settings
- User Roles
- Admin: Full access
- Manager: Limited admin access
- Staff: POS and basic functions
- Password Policies
- Minimum 8 characters
- Mix of letters and numbers
- Password expiry (optional)
- Session Management
- Auto logout after inactivity
- Concurrent login prevention
- IP tracking
- Data Backup
- Automated database backups
- File system backups
- Backup restoration
Step 10: Troubleshooting
Common Issues and Solutions:
- Login Failed
- Check username/password
- Verify user is active
- Clear browser cache
- Check database connection
- Stock Not Updating
- Verify transaction completed
- Check warehouse selection
- Review stock movements
- Check for pending transfers
- Reports Not Generating
- Check date range selection
- Verify data exists
- Check file permissions
- Memory limit issues
- Upload Errors
- Check file size limits
- Verify file types
- Check folder permissions
- Disk space availability
Step 11: Performance Optimization
- Database Optimization
- Regular indexing
- Query optimization
- Archive old data
- Regular cleanup
- Caching
- Enable query caching
- Session caching
- Page caching for reports
- File Management
- Compress images
- Clean up temp files
- Archive old exports
Step 12: Deployment to Production
- Server Requirements
- PHP 7.4 or higher
- MySQL 5.7 or higher
- Apache/Nginx
- SSL certificate
- 20GB+ storage
- Security Hardening
- Change default passwords
- Enable HTTPS
- Set proper file permissions
- Disable error display
- Enable firewall
- Regular security updates
- Backup Strategy
- Daily database backups
- Weekly full backups
- Offsite backup storage
- Test restoration process
- Monitoring
- Server monitoring
- Error logging
- Performance monitoring
- User activity logs
Key Features Summary
Inventory Management
- Real-time stock tracking
- Multiple warehouse support
- Batch and expiry tracking
- Low stock alerts
- Stock adjustments
- Stock transfers
Product Management
- Product catalog
- Categories and subcategories
- Product images
- Barcode generation
- Price management
- Supplier linking
Purchase Management
- Purchase orders
- Supplier management
- Order tracking
- Receiving management
- Purchase history
Sales Management
- Point of Sale (POS)
- Sales orders
- Invoice generation
- Payment tracking
- Customer management
- Sales returns
Reporting & Analytics
- Sales reports
- Stock reports
- Purchase reports
- Profit/Loss statements
- Expiry reports
- Low stock alerts
User Management
- Role-based access
- User permissions
- Activity logging
- Login history
System Features
- Responsive design
- Dark/Light mode
- Export to Excel/PDF
- Import from CSV
- Barcode scanning
- Multi-language ready
This comprehensive Inventory Management System provides everything needed to run a business's inventory operations efficiently. The modular design allows for easy customization and scaling based on specific business requirements.