roles guard + docs + logo

This commit is contained in:
Thanakorn S
2026-05-06 16:37:20 +07:00
parent 797dee5d17
commit 2eb6a1a315
60 changed files with 496 additions and 105 deletions
+2 -2
View File
@@ -145,5 +145,5 @@ vite.config.ts.timestamp-*
# custom files
AGENTS.md
CLAUDE.md
ROLES.md
SESSION.php
SESSION.php
docs/
+87
View File
@@ -0,0 +1,87 @@
# Changelog
All notable changes to TRx WMS are documented here.
Format: `## [version] — YYYY-MM-DD` with sections Added / Changed / Fixed / Removed.
---
## [Unreleased]
### Planned
- Role-based access control enforcement across all API endpoints and UI pages (see `ROLES.md`)
---
## [0.6.0] — 2026-05-06
### Added
- Barcode system — `md_barcode` table, SKU label page (`ics/sku_barcode_label.php`), Location label page (`ics/location_barcode_label.php`)
- `scanner.js` — unified scanner module supporting USB scanner, handheld, phone camera (Html5Qrcode), and clipboard paste
- Scan-driven stock flows — scan SKU label → scan location label → F2 to save (no mouse required)
- `ics/api/engine/barcode_lookup.php` — validates barcode string, returns typed result (`sku` / `loc` / `raw`)
- `ics/api/engine/validate_scan_location.php` — pre-save location validation for stock in/out/transfer
- `ics/api/engine/sku_label_lots.php`, `sku_label_products.php` — lot and product data feeds for SKU label page
- Cost, price, and margin fields on `md_product`
- Contact module linked to setting sidebar
### Fixed
- `location_barcode_label.php` — `ON DUPLICATE KEY UPDATE` now includes `status = 1` so previewing a previously-disabled location barcode re-enables it
---
## [0.5.0] — 2026-04
### Added
- Purchase Orders (`po/`) — create, confirm, receive, cancel, payment tracking
- Order module (`order/`) — sales orders, returns, invoices (invoice / credit note / debit note), order confirmation, payment status
- `td_order`, `td_invoice`, `td_return`, `td_purchase_order` tables
- Invoice print view (`order/print_invoice.php`)
---
## [0.4.0] — 2026-03
### Added
- Warehouse location modes — simple (warehouse + rack) and advanced (warehouse + zone + aisle + rack)
- `company_setting` keys: `advanced_location`, `location_label_rack/zone/aisle`, `default_stock_status`, `auto_complete_on_ship`, `auto_invoice_and_credit_note`
- Per-warehouse stock tables (`td_stock_{warehouse_id}`) — balance = `SUM(in) - SUM(out)` where `status = 1`
- Stock approval flow — transactions created as `status = 0` (draft) and approved separately
- Rack occupancy report (`reports/occupy_rack.php`)
- Expired / near-expiry report (`reports/expired_stock.php`)
### Changed
- Warehouse layers made switchable via `advanced_location` setting
---
## [0.3.0] — 2026-02
### Added
- Contact module (`contact/`) — supplier/customer contacts, contact types
- Stock movement report with date and SKU filters
- Product lot report with expiry date tracking
- Low-stock dashboard widget with restock shortcut
---
## [0.2.0] — 2026-01
### Added
- Multi-tenant branch support — `company_list`, `company_map_user`, branch switcher in topbar
- User management (`setting/users.php`) — invite by email, role assignment, remove member
- SMTP configuration (`setting/smtp.php`)
- OTP validation on every API request (HMAC-SHA1 based on user password + session time)
- CSRF token enforcement on all POST requests
---
## [0.1.0] — 2025-12
### Added
- Initial project scaffold — PHP + MySQL + Bootstrap 5 + jQuery
- Login / registration / onboarding flow
- Product master (`inventory/product.php`) — SKU, name, UOM, category, barcode
- Warehouse master (`inventory/warehouse.php`) — warehouses and rack definitions
- Stock In / Out / Transfer pages with manual form entry
- `td_stock` base table structure
- Dashboard with basic stock summary
+136 -2
View File
@@ -1,2 +1,136 @@
# inapp-pro
inapp-pro Inventory Dashboard Admin Template
# TRx WMS
A multi-tenant Warehouse Management System (WMS) built with PHP, MySQL, and vanilla JavaScript. Designed for small-to-medium operations that need barcode-driven stock control, lot/serial traceability, and multi-warehouse support — accessible from any browser without installing a native app.
---
## Features
### Dashboard
- Live stock overview with key metrics (total SKUs, low-stock count, near-expiry alerts)
- Low-stock product list with one-click restock shortcut to Stock In
- Dashboard widgets per warehouse
### Inventory Management
- **Product master** — SKU, name, unit of measure, barcode, category, cost/price/margin tracking
- **Categories** — group products for filtering and reporting
- **Warehouse master** — multi-warehouse support; simple or advanced location mode (warehouse → rack, or warehouse → zone → aisle → rack)
- **Storage / rack master** — define physical rack locations per warehouse
- **Product lots** — lot number, expiry date, per-lot traceability across all warehouses
### Stock Control (ICS)
- **Stock In** — receive stock into a specific location; supports lot, expiry, serial, unit price, and contact (supplier)
- **Stock Out** — remove stock from a location; cascaded lot/serial dropdowns filtered to available stock
- **Stock Transfer** — move stock between any two locations (same or different warehouse)
- **Stock Overview** — real-time balance per SKU across all warehouses and locations
- Two-step approval flow: transactions created as `pending` and approved separately
### Barcode System
- **SKU barcode labels** — generate `SKU|{sku}|{lot}|{serial}` labels; print, disable, re-enable per serial
- **Location barcode labels** — generate `LOC|{warehouse}|{rack}` (simple) or `LOC|{warehouse}|{zone}|{aisle}|{rack}` (advanced) labels
- **Scanner support** — USB scanner, handheld scanner, phone camera (Html5Qrcode), and clipboard paste all handled by a unified `scanner.js` module
- Scan-driven stock flows: scan SKU label → scan location label → press F2 to save (no mouse required)
- Disabled barcodes are rejected at scan time with a clear error message
### Orders
- **Sales orders** — create, confirm, and track customer orders
- **Returns** — manage product returns linked to original orders
- **Invoices** — generate and print invoices per order
- **Purchase orders (PO)** — create and track supplier purchase orders
### Contacts
- Supplier and customer contact management
- Contact types (supplier, customer, other)
- Linked to stock-in, stock-out, orders, and POs
### Reports
- **Stock Movement** — full transaction history with in/out/net summary; filterable by date, SKU, warehouse
- **Product Lots** — lot-level stock balance with expiry dates across all warehouses
- **Expired / Near Expiry** — products approaching or past their expiry date
- **Rack Occupancy** — visual overview of which racks are occupied, empty, or locked
### Settings
- **Company profile** — name, logo, branch details
- **System config** — location mode (simple/advanced), custom zone/aisle/rack labels, stock uniqueness rules
- **User management** — invite users by email, assign roles (admin / staff / viewer), remove members
- **SMTP** — configure outbound email for notifications
- **Profile** — per-user name, username, password, profile picture
### Multi-Tenant / Branch Support
- Each company is isolated; users can belong to multiple companies
- Branch switcher in the topbar for users with access to more than one company
- All data (products, stock, orders, contacts) is scoped to the active company
### Security
- Session-based authentication with TOTP-style OTP validation on every API request
- CSRF token enforcement on all POST requests
- Role-based access control: `owner`, `admin`, `staff`, `viewer` (see `ROLES.md`)
- Passwords hashed; profile picture uploads sandboxed to `uploads/profile/`
---
## Tech Stack
| Layer | Technology |
|-------|-----------|
| Backend | PHP 8.x, Apache |
| Databases | MySQL — `wms` (system/auth), `wms2` (operational data) |
| Frontend | Bootstrap 5, jQuery, Flatpickr, ApexCharts, JsBarcode, Html5Qrcode |
| Build | Vite (`npm run dev` / `npm run build`) |
| Auth | Session + HMAC-SHA1 OTP + CSRF tokens |
---
## Module Layout
```
app/
├── dashboard/ # Dashboard and low-stock widgets
├── ics/ # Stock In / Out / Transfer, barcode labels
├── inventory/ # Product, warehouse, rack, category masters
├── order/ # Sales orders, returns, invoices
├── po/ # Purchase orders
├── reports/ # Stock movement, lots, expiry, rack occupancy
├── contact/ # Supplier / customer contacts
├── setting/ # Company, users, SMTP, system config, profile
├── assets/
│ ├── js/ # main.js, scanner.js, custom.js
│ ├── css/ # main.css, custom.css
│ └── utils/ # db_auth.php, db_helpers.php, shared classes
└── login/ # Login, OTP, onboarding
```
---
## Local Development
```bash
# Front-end assets (Vite)
npm run dev
npm run build
# Syntax-check a PHP file
php -l app/ics/manage_stock_in.php
# Apply a schema migration to the client database
mysql -uroot -p2618 wms2 < migration.sql
```
App is served by Apache at `http://localhost/wms/app/`.
---
## Documentation
| File | Contents |
|------|----------|
| `CHANGELOG.md` | Version history and notable changes |
| `docs/ROLES.md` | Role-based access control spec (admin / staff / viewer) |
| `docs/DATABASE.md` | Full schema for both databases — tables, columns, relationships |
| `docs/API.md` | All API endpoints — request fields, response format, error codes |
| `docs/DEPLOYMENT.md` | Installation, Apache/PHP/MySQL setup, environment checklist |
| `docs/TESTING.md` | Manual test checklists — barcode flow, stock ops, regression |
| `docs/SECURITY.md` | Auth model, OTP flow, CSRF, session management, XSS/SQL rules |
| `docs/STOCK.md` | Stock ledger, approval flow, rack lifecycle, StockManager/WarehouseManager |
| `docs/SCANNER.md` | scanner.js internals, device support, integrating scanning into new pages |
| `docs/CONTRIBUTING.md` | Patterns for adding new APIs, pages, modules, settings, and schema changes |
+12 -1
View File
@@ -88,4 +88,15 @@
justify-content: center;
z-index: 2;
border-radius: inherit;
}
}
.logo-area > a > img:first-child {
width: 40px;
height: 40px;
transition: width 0.2s ease, height 0.2s ease;
}
.sidebar.collapsed .logo-area > a > img:first-child {
width: 24px !important;
height: 24px !important;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

+23 -1
View File
@@ -785,4 +785,26 @@ function set_btn_state(selector, enabled, hint) {
.prop('disabled', true)
.attr('title', hint || '');
}
}
}
var ROLE_RANK = { viewer: 0, staff: 1, admin: 2, owner: 3 };
function apply_role_ui() {
var role = document.getElementById('session-context')?.dataset.role || 'viewer';
var rank = ROLE_RANK[role] ?? 0;
$('[data-min-role]').each(function() {
var required = ROLE_RANK[$(this).data('min-role')] ?? 99;
if (rank < required) $(this).hide();
});
}
$(function() {
apply_role_ui();
if (window.MutationObserver) {
var roleObserver = new MutationObserver(function() {
apply_role_ui();
});
roleObserver.observe(document.body, { childList: true, subtree: true });
}
});
+14 -1
View File
@@ -9,6 +9,15 @@ require_once __DIR__."/db_helpers.php";
// xhr.responseJSON from being populated automatically in ajax_request().
header('Content-Type: application/json; charset=utf-8');
if (!function_exists('require_role')) {
function require_role(string $user_role, array $allowed): void {
if (!in_array($user_role, $allowed, true)) {
http_response_code(403);
exit(json_encode(['success' => 0, 'message' => 'Access denied.']));
}
}
}
if(!empty($_SESSION["login_company_id"])){
// CSRF Validation — add right at the top of the logged-in block
@@ -63,6 +72,9 @@ if(!empty($_SESSION["login_company_id"])){
exit(json_encode($answer));
}
$user_role = $map[0]['role'] ?? 'viewer';
$_SESSION['login_role'] = $user_role;
}
// set up ANSWER
@@ -86,6 +98,7 @@ if(!empty($_SESSION["login_company_id"])){
$company_id = (int)$_SESSION["login_company_id"];
$user_id = (int)$_SESSION["login_user_id"];
$user_role = $_SESSION['login_role'] ?? ($user_role ?? 'viewer');
$uuid = bin2hex(random_bytes(16));
// create json for table logging
@@ -98,4 +111,4 @@ if(!empty($_SESSION["login_company_id"])){
}
?>
?>
@@ -33,6 +33,10 @@
$aisle = trim($data['aisle'] ?? '');
$rack = trim($data['rack'] ?? '');
if ($label_action !== 'list') {
require_role($user_role, ['owner', 'admin', 'staff']);
}
if ($label_action === 'list') {
$sth = $pdo2->prepare(
"SELECT barcode, warehouse_id, zone, aisle, rack, status, print_count, last_printed_dt
+2
View File
@@ -5,6 +5,8 @@
require '../../../assets/utils/classes/WarehouseManager.php';
require '../../../assets/utils/classes/CompanySettingManager.php';
require_role($user_role, ['owner', 'admin', 'staff']);
$csm = new CompanySettingManager($pdo1, $company_id);
$auto_approve = (int)$csm->get('default_stock_status') === 1;
+2
View File
@@ -5,6 +5,8 @@
require '../../../assets/utils/classes/WarehouseManager.php';
require '../../../assets/utils/classes/CompanySettingManager.php';
require_role($user_role, ['owner', 'admin', 'staff']);
$csm = new CompanySettingManager($pdo1, $company_id);
$auto_approve = (int)$csm->get('default_stock_status') === 1;
@@ -5,6 +5,8 @@
require '../../../assets/utils/classes/WarehouseManager.php';
require '../../../assets/utils/classes/CompanySettingManager.php';
require_role($user_role, ['owner', 'admin', 'staff']);
$csm = new CompanySettingManager($pdo1, $company_id);
$auto_approve = (int)$csm->get('default_stock_status') === 1;
+4
View File
@@ -127,6 +127,10 @@
$product_sku = trim($data['product_sku'] ?? '');
$lot_number = trim($data['lot_number'] ?? '');
if (in_array($label_action, ['create', 'print', 'disable', 'enable'], true)) {
require_role($user_role, ['owner', 'admin', 'staff']);
}
if ($label_action === 'print') {
$barcode = trim($data['barcode'] ?? '');
if ($barcode === '') {
@@ -3,6 +3,8 @@
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/WarehouseManager.php';
require_role($user_role, ['owner', 'admin', 'staff']);
$whMgmt = new WarehouseManager($pdo2, $company_id);
$mode = $data['mode'] ?? '';
+7 -3
View File
@@ -1,6 +1,10 @@
<?php
session_start();
require '../config.php';
if (!in_array($_SESSION['login_role'] ?? 'viewer', ['owner', 'admin', 'staff'], true)) {
http_response_code(403);
exit('Access denied.');
}
require '../include_header.php';
?>
@@ -17,13 +21,13 @@
<p class="mb-0 text-muted">Print or reprint labels from the rack master list.</p>
</div>
<div class="d-flex gap-2">
<button id="btn_print" class="btn btn-outline-success" onclick="print_selected_label()" disabled>
<button id="btn_print" class="btn btn-outline-success" data-min-role="staff" onclick="print_selected_label()" disabled>
<i class="ti ti-printer me-1"></i>Print
</button>
<button id="btn_disable" class="btn btn-outline-danger" onclick="disable_selected_label()" disabled>
<button id="btn_disable" class="btn btn-outline-danger" data-min-role="staff" onclick="disable_selected_label()" disabled>
<i class="ti ti-ban me-1"></i>Disable
</button>
<button id="btn_reenable" class="btn btn-outline-primary d-none" onclick="reenable_selected_label()">
<button id="btn_reenable" class="btn btn-outline-primary d-none" data-min-role="staff" onclick="reenable_selected_label()">
<i class="ti ti-refresh me-1"></i>Re-enable
</button>
</div>
+5 -1
View File
@@ -1,6 +1,10 @@
<?php
session_start();
require '../config.php';
if (!in_array($_SESSION['login_role'] ?? 'viewer', ['owner', 'admin', 'staff'], true)) {
http_response_code(403);
exit('Access denied.');
}
require '../include_header.php';
?>
@@ -114,7 +118,7 @@
<!-- Submit Button -->
<div class="mb-3 col-lg-12">
<button type="submit" class="btn btn-primary" onclick="manage_stock_in();">Add Stock</button>
<button type="submit" class="btn btn-primary" data-min-role="staff" onclick="manage_stock_in();">Add Stock</button>
<button type="button" class="btn btn-success ms-2 d-none" id="approve_stock_btn" onclick="approve_current_stock();">Approve</button>
</div>
</form>
+5 -1
View File
@@ -1,6 +1,10 @@
<?php
session_start();
require '../config.php';
if (!in_array($_SESSION['login_role'] ?? 'viewer', ['owner', 'admin', 'staff'], true)) {
http_response_code(403);
exit('Access denied.');
}
require '../include_header.php';
?>
@@ -103,7 +107,7 @@
<!-- Submit Button -->
<div class="mb-3 col-lg-12">
<button type="submit" class="btn btn-primary" onclick="manage_stock_out();">Remove Stock</button>
<button type="submit" class="btn btn-primary" data-min-role="staff" onclick="manage_stock_out();">Remove Stock</button>
<button type="button" class="btn btn-success ms-2 d-none" id="approve_stock_btn" onclick="approve_current_stock();">Approve</button>
</div>
</form>
+5 -1
View File
@@ -1,6 +1,10 @@
<?php
session_start();
require '../config.php';
if (!in_array($_SESSION['login_role'] ?? 'viewer', ['owner', 'admin', 'staff'], true)) {
http_response_code(403);
exit('Access denied.');
}
require '../include_header.php';
?>
@@ -135,7 +139,7 @@
<!-- Submit -->
<div class="mb-3 col-lg-12">
<button type="submit" class="btn btn-primary" onclick="manage_stock_transfer();">Transfer Stock</button>
<button type="submit" class="btn btn-primary" data-min-role="staff" onclick="manage_stock_transfer();">Transfer Stock</button>
<button type="button" class="btn btn-success ms-2 d-none" id="approve_stock_btn" onclick="approve_current_stock();">Approve</button>
</div>
</form>
+7 -3
View File
@@ -1,6 +1,10 @@
<?php
session_start();
require '../config.php';
if (!in_array($_SESSION['login_role'] ?? 'viewer', ['owner', 'admin', 'staff'], true)) {
http_response_code(403);
exit('Access denied.');
}
require '../include_header.php';
?>
@@ -17,13 +21,13 @@
<p class="mb-0 text-muted">Print or reprint product identity labels from the lot master list.</p>
</div>
<div class="d-flex gap-2">
<button class="btn btn-success" onclick="open_create_label_modal()">
<button class="btn btn-success" data-min-role="staff" onclick="open_create_label_modal()">
<i class="ti ti-plus me-1"></i>New Barcode
</button>
<button id="btn_print" class="btn btn-outline-success" onclick="print_selected_label()" disabled>
<button id="btn_print" class="btn btn-outline-success" data-min-role="staff" onclick="print_selected_label()" disabled>
<i class="ti ti-printer me-1"></i>Print
</button>
<button id="btn_disable" class="btn btn-outline-danger" onclick="disable_selected_label()" disabled>
<button id="btn_disable" class="btn btn-outline-danger" data-min-role="staff" onclick="disable_selected_label()" disabled>
<i class="ti ti-ban me-1"></i>Disable
</button>
</div>
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<title>ThreeWMS</title>
<title>TRx WMS</title>
<meta name="csrf-token" content="<?= htmlspecialchars($_SESSION['csrf_token'] ?? '') ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="apple-touch-icon" sizes="180x180" href="<?php echo $server_url?>assets/images/favicon.png">
+12 -22
View File
@@ -1,5 +1,7 @@
<?php
$current_page = basename($_SERVER['PHP_SELF']);
$master_role = $_SESSION['login_role'] ?? 'viewer';
$master_can_admin = in_array($master_role, ['owner', 'admin'], true);
?>
<!-- SIDEBAR -->
@@ -28,27 +30,15 @@
</a>
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>ics/sku_barcode_label.php">
<i class="ti ti-barcode"></i>
<span class="nav-text">SKU Labels</span>
</a>
</li>
<li>
<a class="nav-link <?php echo in_array($current_page, ['warehouse.php', 'manage_warehouse.php', 'manage_storage.php']) ? 'active' : ''; ?>"
href="<?php echo $server_url?>inventory/warehouse.php">
<i class="ti ti-map-pin"></i>
<span class="nav-text">Warehouse Location</span>
</a>
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>ics/location_barcode_label.php">
<i class="ti ti-map-pin-code"></i>
<span class="nav-text">Location Labels</span>
</a>
</li>
<?php if ($master_can_admin): ?>
<li>
<a class="nav-link <?php echo in_array($current_page, ['warehouse.php', 'manage_warehouse.php', 'manage_storage.php']) ? 'active' : ''; ?>"
href="<?php echo $server_url?>inventory/warehouse.php">
<i class="ti ti-map-pin"></i>
<span class="nav-text">Warehouse Location</span>
</a>
</li>
<?php endif; ?>
<li>
<a class="nav-link <?php echo in_array($current_page, ['contact.php', 'manage_contact.php', 'manage_contact_type.php']) ? 'active' : ''; ?>"
@@ -72,4 +62,4 @@
</ul>
</aside>
</aside>
+21 -15
View File
@@ -1,5 +1,7 @@
<?php
$current_page = basename($_SERVER['PHP_SELF']);
$setting_role = $_SESSION['login_role'] ?? 'viewer';
$setting_can_admin = in_array($setting_role, ['owner', 'admin'], true);
?>
<!-- SIDEBAR -->
@@ -36,13 +38,15 @@
</a>
</li>
<li>
<a class="nav-link <?php echo $current_page === 'users.php' ? 'active' : ''; ?>"
href="<?php echo $server_url?>setting/users.php">
<i class="ti ti-users"></i>
<span class="nav-text">Users Access</span>
</a>
</li>
<?php if ($setting_can_admin): ?>
<li>
<a class="nav-link <?php echo $current_page === 'users.php' ? 'active' : ''; ?>"
href="<?php echo $server_url?>setting/users.php">
<i class="ti ti-users"></i>
<span class="nav-text">Users Access</span>
</a>
</li>
<?php endif; ?>
<li>
<a class="nav-link <?php echo $current_page === 'smtp.php' ? 'active' : ''; ?>"
@@ -52,13 +56,15 @@
</a>
</li>
<li>
<a class="nav-link <?php echo $current_page === 'system_config.php' ? 'active' : ''; ?>"
href="<?php echo $server_url?>setting/system_config.php">
<i class="ti ti-adjustments-cog"></i>
<span class="nav-text">System Configuration</span>
</a>
</li>
<?php if ($setting_can_admin): ?>
<li>
<a class="nav-link <?php echo $current_page === 'system_config.php' ? 'active' : ''; ?>"
href="<?php echo $server_url?>setting/system_config.php">
<i class="ti ti-adjustments-cog"></i>
<span class="nav-text">System Configuration</span>
</a>
</li>
<?php endif; ?>
<!-- Back -->
<li class="nav-text-space mt-2">
@@ -74,4 +80,4 @@
</ul>
</aside>
</aside>
+39 -19
View File
@@ -1,10 +1,14 @@
<?php
$sidebar_role = $_SESSION['login_role'] ?? 'viewer';
$sidebar_can_staff = in_array($sidebar_role, ['owner', 'admin', 'staff'], true);
?>
<!-- SIDEBAR -->
<aside id="sidebar" class="sidebar overflow-y-auto">
<div class="logo-area">
<a href="index.html" class="d-inline-flex">
<img src="<?php echo $server_url?>assets/images/favicon.png" alt="" width="40"/>
<span class="logo-text ms-2">
<img src="<?php echo $server_url?>assets/images/logo.svg" alt="" />
<img src="<?php echo $server_url?>assets/images/logo.png" alt="" height="40"/>
</span>
</a>
</div>
@@ -42,26 +46,42 @@
</a>
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>ics/stock_in.php">
<i class="ti ti-package-import"></i>
<span class="nav-text">Stock In</span>
</a>
</li>
<?php if ($sidebar_can_staff): ?>
<li>
<a class="nav-link" href="<?php echo $server_url?>ics/stock_in.php">
<i class="ti ti-package-import"></i>
<span class="nav-text">Stock In</span>
</a>
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>ics/stock_out.php">
<i class="ti ti-package-export"></i>
<span class="nav-text">Stock Out</span>
</a>
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>ics/stock_out.php">
<i class="ti ti-package-export"></i>
<span class="nav-text">Stock Out</span>
</a>
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>ics/stock_transfer.php">
<i class="ti ti-package"></i>
<span class="nav-text">Stock Transfer</span>
</a>
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>ics/stock_transfer.php">
<i class="ti ti-package"></i>
<span class="nav-text">Stock Transfer</span>
</a>
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>ics/sku_barcode_label.php">
<i class="ti ti-barcode"></i>
<span class="nav-text">SKU Labels</span>
</a>
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>ics/location_barcode_label.php">
<i class="ti ti-map-pin-code"></i>
<span class="nav-text">Location Labels</span>
</a>
</li>
<?php endif; ?>
<!-- Orders & Sales -->
<li class="nav-text-space">
+4 -2
View File
@@ -21,7 +21,8 @@ $topbar_avatar_src = $topbar_picture
<!-- Global Variables -->
<div id="session-context"
data-company-id="<?php echo htmlspecialchars($_SESSION['login_company_id'] ?? 0, ENT_QUOTES, 'UTF-8'); ?>"
data-otp="<?php echo htmlspecialchars($_SESSION['otp'] ?? '', ENT_QUOTES, 'UTF-8'); ?>">
data-otp="<?php echo htmlspecialchars($_SESSION['otp'] ?? '', ENT_QUOTES, 'UTF-8'); ?>"
data-role="<?php echo htmlspecialchars($_SESSION['login_role'] ?? 'viewer', ENT_QUOTES, 'UTF-8'); ?>">
</div>
<!-- Switch Branch Modal -->
@@ -121,6 +122,7 @@ $topbar_avatar_src = $topbar_picture
// Global Variables
var server_url = '<?php echo $server_url; ?>';
var prop_limit = '<?php echo $prop['limit']; ?>';
var user_role = document.getElementById('session-context')?.dataset.role || 'viewer';
// ── Log out ───────────────────────────────────────────────────────────────────
function log_out() {
@@ -189,4 +191,4 @@ function do_switch_branch(company_id) {
}
});
}
</script>
</script>
+3 -1
View File
@@ -3,6 +3,8 @@
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/ProductManager.php';
require_role($user_role, ['owner', 'admin']);
try {
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging) {
$product = new ProductManager($pdo, $company_id);
@@ -23,4 +25,4 @@
}
exit(json_encode($answer));
?>
?>
+3 -1
View File
@@ -4,6 +4,8 @@
require '../../../assets/utils/classes/FileUploader.php';
require '../../../assets/utils/classes/ProductManager.php';
require_role($user_role, ['owner', 'admin']);
$id = (int)$data['id'];
$min_stock = (float)($data['min_stock'] ?? 0);
$reorder_point = (float)($data['reorder_point'] ?? 0);
@@ -68,4 +70,4 @@
}
exit(json_encode($answer));
?>
?>
@@ -4,6 +4,8 @@
require '../../../assets/utils/classes/WarehouseManager.php';
require '../../../assets/utils/classes/CompanySettingManager.php';
require_role($user_role, ['owner', 'admin']);
try {
$setting = new CompanySettingManager($pdo1, $company_id, $pdo2);
$advanced_location = (int)$setting->get('advanced_location') === 1;
@@ -3,6 +3,8 @@
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/WarehouseManager.php';
require_role($user_role, ['owner', 'admin']);
try {
$newWarehouseId = null;
+3 -1
View File
@@ -3,6 +3,8 @@
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/ProductManager.php';
require_role($user_role, ['owner', 'admin']);
try {
dbTransaction($pdo2, function($pdo) use ($data, $company_id) {
@@ -26,4 +28,4 @@
}
exit(json_encode($answer));
?>
?>
+3 -1
View File
@@ -3,6 +3,8 @@
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/ProductManager.php';
require_role($user_role, ['owner', 'admin']);
try {
dbTransaction($pdo2, function($pdo) use ($data, $company_id) {
@@ -26,4 +28,4 @@
}
exit(json_encode($answer));
?>
?>
+3 -1
View File
@@ -3,6 +3,8 @@
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/WarehouseManager.php';
require_role($user_role, ['owner', 'admin']);
try {
dbTransaction($pdo2, function($pdo) use ($data, $company_id) {
@@ -26,4 +28,4 @@
}
exit(json_encode($answer));
?>
?>
@@ -3,6 +3,8 @@
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/WarehouseManager.php';
require_role($user_role, ['owner', 'admin']);
try {
dbTransaction($pdo2, function($pdo) use ($data, $company_id) {
@@ -26,4 +28,4 @@
}
exit(json_encode($answer));
?>
?>
+5 -1
View File
@@ -1,6 +1,10 @@
<?php
session_start();
require '../config.php';
if (!in_array($_SESSION['login_role'] ?? 'viewer', ['owner', 'admin'], true)) {
http_response_code(403);
exit('Access denied.');
}
require '../include_header.php';
?>
@@ -124,7 +128,7 @@
placeholder="Enter product description"></textarea>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary" onclick="manage_product();">Create</button>
<button type="submit" class="btn btn-primary" data-min-role="admin" onclick="manage_product();">Create</button>
</div>
</div>
</div>
+5 -1
View File
@@ -1,6 +1,10 @@
<?php
session_start();
require '../config.php';
if (!in_array($_SESSION['login_role'] ?? 'viewer', ['owner', 'admin'], true)) {
http_response_code(403);
exit('Access denied.');
}
require '../include_header.php';
?>
@@ -99,7 +103,7 @@
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary" onclick="manage_storage();">Add Storage</button>
<button type="submit" class="btn btn-primary" data-min-role="admin" onclick="manage_storage();">Add Storage</button>
<button type="reset" class="btn btn-secondary" onclick="reset_input('#storageForm')">Clear</button>
</div>
+6 -2
View File
@@ -1,6 +1,10 @@
<?php
session_start();
require '../config.php';
if (!in_array($_SESSION['login_role'] ?? 'viewer', ['owner', 'admin'], true)) {
http_response_code(403);
exit('Access denied.');
}
require '../include_header.php';
?>
@@ -60,7 +64,7 @@
placeholder="Enter warehouse description"></textarea>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary" onclick="manage_warehouse();">Add Warehouse</button>
<button type="submit" class="btn btn-primary" data-min-role="admin" onclick="manage_warehouse();">Add Warehouse</button>
<button type="reset" class="btn btn-secondary" onclick="reset_input('#warehouseForm')">Clear</button>
</div>
</div>
@@ -155,4 +159,4 @@
</body>
</html>
</html>
+3 -3
View File
@@ -171,7 +171,7 @@
<p class="mb-0">List of all products</p>
</div>
<div>
<a href="<?php echo $server_url?>inventory/manage_product.php" class="btn btn-secondary">
<a href="<?php echo $server_url?>inventory/manage_product.php" class="btn btn-secondary" data-min-role="admin">
<span class="me-1"><i class="ti ti-plus"></i></span>
<span>Add Product</span>
</a>
@@ -388,8 +388,8 @@
${(item['status']==1)?'<span class="badge bg-success">Active</span>':'<span class="badge bg-secondary">Inactive</span>'}
</td>
<td class="py-3">
<a href="<?php echo $server_url?>inventory/manage_product.php?id=${item['id']}" class=""><i class="ti ti-eye fs-5"></i></a>
<a href="javascript:;" class="link-danger" onclick="remove_product(${item['id']},$(this))"><i class="ti ti-trash ms-2 fs-5"></i></a>
<a href="<?php echo $server_url?>inventory/manage_product.php?id=${item['id']}" class="" data-min-role="admin"><i class="ti ti-eye fs-5"></i></a>
<a href="javascript:;" class="link-danger" data-min-role="admin" onclick="remove_product(${item['id']},$(this))"><i class="ti ti-trash ms-2 fs-5"></i></a>
</td>
</tr>`;
});
+11 -7
View File
@@ -1,6 +1,10 @@
<?php
session_start();
require '../config.php';
if (!in_array($_SESSION['login_role'] ?? 'viewer', ['owner', 'admin'], true)) {
http_response_code(403);
exit('Access denied.');
}
require '../include_header.php';
?>
@@ -152,7 +156,7 @@
<p>All warehouse facilities and their current status</p>
</div>
<div>
<a href="<?php echo $server_url?>inventory/manage_warehouse.php" class="btn btn-primary">
<a href="<?php echo $server_url?>inventory/manage_warehouse.php" class="btn btn-primary" data-min-role="admin">
Add Warehouse
</a>
</div>
@@ -205,7 +209,7 @@
<p id="storage_subtitle">Zones, aisles, and rack assignments</p>
</div>
<div>
<a href="<?php echo $server_url?>inventory/manage_storage.php" class="btn btn-secondary">
<a href="<?php echo $server_url?>inventory/manage_storage.php" class="btn btn-secondary" data-min-role="admin">
Add Storage
</a>
</div>
@@ -390,8 +394,8 @@
${(item['status']==1)?'<span class="badge bg-success">Active</span>':'<span class="badge bg-secondary">Inactive</span>'}
</td>
<td class="py-3">
<a href="<?php echo $server_url?>inventory/manage_warehouse.php?id=${item['id']}" class=""><i class="ti ti-eye fs-5"></i></a>
<a href="javascript:;" class="link-danger" onclick="remove_warehouse(${item['id']},$(this))"><i class="ti ti-trash ms-2 fs-5"></i></a>
<a href="<?php echo $server_url?>inventory/manage_warehouse.php?id=${item['id']}" class="" data-min-role="admin"><i class="ti ti-eye fs-5"></i></a>
<a href="javascript:;" class="link-danger" data-min-role="admin" onclick="remove_warehouse(${item['id']},$(this))"><i class="ti ti-trash ms-2 fs-5"></i></a>
</td>
</tr>`;
});
@@ -456,8 +460,8 @@
${location_cells}
<td class="py-3"></td>
<td class="py-3">
<a href="<?php echo $server_url?>inventory/manage_storage.php?id=${item['id']}" class=""><i class="ti ti-eye fs-5"></i></a>
<a href="javascript:;" class="link-danger" onclick="remove_storage(${item['id']},$(this))"><i class="ti ti-trash ms-2 fs-5"></i></a>
<a href="<?php echo $server_url?>inventory/manage_storage.php?id=${item['id']}" class="" data-min-role="admin"><i class="ti ti-eye fs-5"></i></a>
<a href="javascript:;" class="link-danger" data-min-role="admin" onclick="remove_storage(${item['id']},$(this))"><i class="ti ti-trash ms-2 fs-5"></i></a>
</td>
</tr>`;
});
@@ -554,4 +558,4 @@
</body>
</html>
</html>
+12 -1
View File
@@ -124,7 +124,18 @@ $_SESSION["login_surname"] = $temp["surname"];
$_SESSION["login_company_id"] = $temp["default_company"];
$_SESSION["login_profile_picture"] = $temp["profile_picture"] ?? '';
$role_sth = $pdo1->prepare(
"SELECT role FROM company_map_user
WHERE company_id = :company_id AND user_id = :user_id
LIMIT 1"
);
$role_sth->execute([
':company_id' => $_SESSION["login_company_id"],
':user_id' => $_SESSION["login_user_id"],
]);
$_SESSION["login_role"] = $role_sth->fetchColumn() ?: 'viewer';
// ── Step 6: Respond ───────────────────────────────────────────────────────────
$answer["success"] = 1;
$answer["message"] = "Login Complete!";
exit(json_encode($answer));
exit(json_encode($answer));
+1
View File
@@ -1,6 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin']);
require '../../../assets/utils/classes/OrderManager.php';
require '../../../assets/utils/classes/WarehouseManager.php';
+1
View File
@@ -1,6 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin']);
require '../../../assets/utils/classes/ReturnManager.php';
require '../../../assets/utils/classes/WarehouseManager.php';
+1
View File
@@ -1,6 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin', 'staff']);
require '../../../assets/utils/classes/OrderManager.php';
require '../../../assets/utils/classes/StockManager.php';
require '../../../assets/utils/classes/WarehouseManager.php';
+1
View File
@@ -1,6 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin', 'staff']);
require '../../../assets/utils/classes/ReturnManager.php';
require '../../../assets/utils/classes/InvoiceManager.php';
require '../../../assets/utils/classes/WarehouseManager.php';
+1
View File
@@ -1,6 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin', 'staff']);
require '../../../assets/utils/classes/InvoiceManager.php';
$id = (int)($data['id'] ?? 0);
+1
View File
@@ -1,6 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin', 'staff']);
require '../../../assets/utils/classes/InvoiceManager.php';
try {
+1
View File
@@ -1,6 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin', 'staff']);
require '../../../assets/utils/classes/OrderManager.php';
// Decode items JSON string back to array
+1
View File
@@ -1,6 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin', 'staff']);
require '../../../assets/utils/classes/ReturnManager.php';
$data['items'] = json_decode($data['items'] ?? '[]', true) ?: [];
@@ -1,6 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin', 'staff']);
require '../../../assets/utils/classes/InvoiceManager.php';
$order_id = (int)($data['order_id'] ?? 0);
@@ -1,6 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin', 'staff']);
require '../../../assets/utils/classes/InvoiceManager.php';
$id = (int)($data['invoice_id'] ?? 0);
@@ -1,6 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin', 'staff']);
require '../../../assets/utils/classes/OrderManager.php';
require '../../../assets/utils/classes/CompanySettingManager.php';
@@ -1,6 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin', 'staff']);
$id = (int)($data['id'] ?? 0);
$payment_status = (int)($data['payment_status'] ?? 0);
+1
View File
@@ -1,6 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin']);
require '../../../assets/utils/classes/InvoiceManager.php';
$id = (int)($data['id'] ?? 0);
+1
View File
@@ -1,6 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin']);
require '../../../assets/utils/classes/PurchaseOrderManager.php';
$id = (int)($data['id'] ?? 0);
+1
View File
@@ -1,6 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin', 'staff']);
require '../../../assets/utils/classes/PurchaseOrderManager.php';
$id = (int)($data['id'] ?? 0);
+1
View File
@@ -1,6 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin', 'staff']);
require '../../../assets/utils/classes/PurchaseOrderManager.php';
$data['items'] = json_decode($data['items'] ?? '[]', true) ?: [];
+1
View File
@@ -1,6 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin', 'staff']);
require '../../../assets/utils/classes/PurchaseOrderManager.php';
require '../../../assets/utils/classes/StockManager.php';
require '../../../assets/utils/classes/WarehouseManager.php';
+1
View File
@@ -1,6 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin', 'staff']);
require '../../../assets/utils/classes/PurchaseOrderManager.php';
$id = (int)($data['id'] ?? 0);
@@ -3,5 +3,9 @@ session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/CompanySettingManager.php';
if (($data['action'] ?? '') === 'update') {
require_role($user_role, ['owner', 'admin']);
}
$csm = new CompanySettingManager($pdo1, $company_id, $pdo2);
$csm->handle($data);
+5 -3
View File
@@ -45,19 +45,21 @@ if ($action === 'update') {
// Verify user actually belongs to the requested company
$sth = $pdo1->prepare(
"SELECT company_id FROM company_map_user
"SELECT company_id, role FROM company_map_user
WHERE company_id = :company_id AND user_id = :user_id
LIMIT 1"
);
$sth->execute([':company_id' => $target_company_id, ':user_id' => $user_id]);
$target_map = $sth->fetch(PDO::FETCH_ASSOC);
if (!$sth->fetch()) {
if (!$target_map) {
http_response_code(403);
$answer['message'] = 'You do not have access to this company.';
exit(json_encode($answer));
}
$_SESSION['login_company_id'] = $target_company_id;
$_SESSION['login_role'] = $target_map['role'] ?? 'viewer';
$answer['success'] = 1;
$answer['message'] = 'Switched successfully.';
@@ -66,4 +68,4 @@ if ($action === 'update') {
http_response_code(400);
$answer['message'] = 'Invalid action.';
exit(json_encode($answer));
exit(json_encode($answer));
+4
View File
@@ -1,6 +1,10 @@
<?php
session_start();
require '../config.php';
if (!in_array($_SESSION['login_role'] ?? 'viewer', ['owner', 'admin'], true)) {
http_response_code(403);
exit('Access denied.');
}
require '../include_header.php';
?>
+5 -1
View File
@@ -1,6 +1,10 @@
<?php
session_start();
require '../config.php';
if (!in_array($_SESSION['login_role'] ?? 'viewer', ['owner', 'admin'], true)) {
http_response_code(403);
exit('Access denied.');
}
require '../include_header.php';
?>
@@ -421,4 +425,4 @@
</script>
</body>
</html>
</html>