Skip to content

Commit 8826eb8

Browse files
committed
feat: UI quality pass — categories tree, module dashboards, sidebar nav, AppLayout upgrades
- Rewrite Inventory/Categories/Index.tsx with tree view (parent + children rows), inline create form for top-level categories, inline edit per row, and "+ Sub" button to add sub-categories with parent_id pre-filled; removes broken links to non-existent /create and /edit routes - Add InventoryDashboardController, FinanceDashboardController, HRDashboardController with KPI queries and recent-record lists; register GET dashboard routes in each module's route file - Create Inventory/Dashboard.tsx, Finance/Dashboard.tsx, HR/Dashboard.tsx with KPI cards, data tables, and AppLayout - Update Sidebar.tsx: add Dashboard as first child of Inventory, Finance, and HR sections; add Shipments and RMA Requests to Inventory; add Profit Centers and Expense Budgets to Finance - Upgrade 8 stub pages (Shipments Index/Show/Create, RmaRequests Index/Show, ProfitCenters Index, ExpenseBudgets Index, EmergencyContacts Index) from bare HTML to AppLayout + Button + proper Tailwind slate/indigo design system https://claude.ai/code/session_01RdUGwo74JXChRCF88Yu27d
1 parent 2ea54bb commit 8826eb8

19 files changed

Lines changed: 1704 additions & 451 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace App\Modules\Finance\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\Finance\Models\Invoice;
7+
use App\Modules\Finance\Models\Bill;
8+
use App\Modules\Finance\Models\Contact;
9+
use Inertia\Inertia;
10+
use Inertia\Response;
11+
12+
class FinanceDashboardController extends Controller
13+
{
14+
public function index(): Response
15+
{
16+
$tenantId = auth()->user()->tenant_id;
17+
18+
$openInvoices = Invoice::where('tenant_id', $tenantId)->whereNotIn('status', ['paid', 'cancelled'])->count();
19+
$openInvoicesTotal = Invoice::where('tenant_id', $tenantId)->whereNotIn('status', ['paid', 'cancelled'])
20+
->with(['items', 'payments'])->get()->sum(fn($i) => $i->amount_due);
21+
$overdueCount = Invoice::where('tenant_id', $tenantId)->whereNotIn('status', ['paid', 'cancelled'])
22+
->whereNotNull('due_date')->where('due_date', '<', now())->count();
23+
$unpaidBills = Bill::where('tenant_id', $tenantId)->whereNotIn('status', ['paid', 'cancelled'])
24+
->with(['items', 'payments'])->get()->sum(fn($b) => $b->amount_due);
25+
$totalContacts = Contact::where('tenant_id', $tenantId)->count();
26+
$revenueThisMonth = Invoice::where('tenant_id', $tenantId)->whereNotIn('status', ['cancelled'])
27+
->whereYear('issue_date', now()->year)->whereMonth('issue_date', now()->month)
28+
->with('items')->get()->sum(fn($i) => $i->total);
29+
30+
$recentInvoices = Invoice::where('tenant_id', $tenantId)
31+
->with(['contact', 'items', 'payments'])
32+
->latest()
33+
->take(6)
34+
->get()
35+
->map(fn($i) => [
36+
'id' => $i->id,
37+
'number' => $i->number,
38+
'contact' => $i->contact?->name,
39+
'total' => round($i->total, 2),
40+
'amount_due' => round($i->amount_due, 2),
41+
'status' => $i->status,
42+
'issue_date' => $i->issue_date,
43+
]);
44+
45+
$recentBills = Bill::where('tenant_id', $tenantId)
46+
->with(['items', 'payments'])
47+
->latest()
48+
->take(5)
49+
->get()
50+
->map(fn($b) => [
51+
'id' => $b->id,
52+
'status' => $b->status,
53+
'total' => round($b->total ?? 0, 2),
54+
'amount_due' => round($b->amount_due ?? 0, 2),
55+
'issue_date' => $b->issue_date,
56+
]);
57+
58+
return Inertia::render('Finance/Dashboard', compact(
59+
'openInvoices', 'openInvoicesTotal', 'overdueCount', 'unpaidBills',
60+
'totalContacts', 'revenueThisMonth', 'recentInvoices', 'recentBills'
61+
));
62+
}
63+
}

erp/app/Modules/Finance/routes/finance.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
use App\Modules\Finance\Http\Controllers\FinanceDashboardController;
4+
35
use App\Modules\Finance\Http\Controllers\AccountController;
46
use App\Modules\Finance\Http\Controllers\BudgetController;
57
use App\Modules\Finance\Http\Controllers\BudgetLineController;
@@ -45,6 +47,11 @@
4547
use App\Modules\Finance\Http\Controllers\PaymentTermController;
4648
use App\Modules\Finance\Http\Controllers\CustomerGroupController;
4749

50+
51+
Route::middleware(['web','auth','verified'])->prefix('finance')->name('finance.')->group(function() {
52+
Route::get('dashboard', [FinanceDashboardController::class, 'index'])->name('dashboard');
53+
});
54+
4855
Route::middleware(['web', 'auth', 'verified'])->prefix('finance')->name('finance.')->group(function () {
4956

5057
// Chart of Accounts
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
namespace App\Modules\HR\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\HR\Models\Employee;
7+
use App\Modules\HR\Models\LeaveRequest;
8+
use App\Modules\HR\Models\Department;
9+
use App\Modules\HR\Models\JobPosition;
10+
use Inertia\Inertia;
11+
use Inertia\Response;
12+
13+
class HRDashboardController extends Controller
14+
{
15+
public function index(): Response
16+
{
17+
$tenantId = auth()->user()->tenant_id;
18+
19+
$totalEmployees = Employee::where('tenant_id', $tenantId)->where('status', 'active')->count();
20+
$pendingLeaves = LeaveRequest::where('tenant_id', $tenantId)->where('status', 'pending')->count();
21+
$onLeaveToday = LeaveRequest::where('tenant_id', $tenantId)->where('status', 'approved')
22+
->whereDate('start_date', '<=', now())->whereDate('end_date', '>=', now())->count();
23+
$totalDepartments = Department::where('tenant_id', $tenantId)->count();
24+
$openPositions = JobPosition::where('tenant_id', $tenantId)->where('status', 'open')->count();
25+
$newHiresThisMonth = Employee::where('tenant_id', $tenantId)
26+
->whereYear('created_at', now()->year)->whereMonth('created_at', now()->month)->count();
27+
28+
$recentLeaveRequests = LeaveRequest::where('tenant_id', $tenantId)
29+
->where('status', 'pending')
30+
->with(['employee', 'leaveType'])
31+
->latest()
32+
->take(5)
33+
->get()
34+
->map(fn($lr) => [
35+
'id' => $lr->id,
36+
'employee' => $lr->employee?->first_name . ' ' . $lr->employee?->last_name,
37+
'type' => $lr->leaveType?->name,
38+
'start_date' => $lr->start_date,
39+
'end_date' => $lr->end_date,
40+
'status' => $lr->status,
41+
]);
42+
43+
$departmentHeadcount = Department::where('tenant_id', $tenantId)
44+
->withCount(['employees as active_count' => fn($q) => $q->where('status', 'active')])
45+
->orderByDesc('active_count')
46+
->take(8)
47+
->get()
48+
->map(fn($d) => ['name' => $d->name, 'count' => $d->active_count]);
49+
50+
return Inertia::render('HR/Dashboard', compact(
51+
'totalEmployees', 'pendingLeaves', 'onLeaveToday', 'totalDepartments',
52+
'openPositions', 'newHiresThisMonth', 'recentLeaveRequests', 'departmentHeadcount'
53+
));
54+
}
55+
}

erp/app/Modules/HR/routes/hr.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
use App\Modules\HR\Http\Controllers\HRDashboardController;
4+
35
use App\Modules\HR\Http\Controllers\AttendanceController;
46
use App\Modules\HR\Http\Controllers\DisciplinaryCaseController;
57
use App\Modules\HR\Http\Controllers\GrievanceController;
@@ -29,6 +31,11 @@
2931
use App\Modules\HR\Models\ExpenseClaimItem;
3032
use Illuminate\Support\Facades\Route;
3133

34+
35+
Route::middleware(['web','auth','verified'])->prefix('hr')->name('hr.')->group(function() {
36+
Route::get('dashboard', [HRDashboardController::class, 'index'])->name('dashboard');
37+
});
38+
3239
Route::middleware(['web', 'auth', 'verified'])->prefix('hr')->name('hr.')->group(function () {
3340

3441
// Departments — full CRUD
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace App\Modules\Inventory\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\Inventory\Models\Product;
7+
use App\Modules\Inventory\Models\PurchaseOrder;
8+
use App\Modules\Inventory\Models\StockMovement;
9+
use App\Modules\Inventory\Models\Warehouse;
10+
use App\Modules\Inventory\Models\Supplier;
11+
use Inertia\Inertia;
12+
use Inertia\Response;
13+
14+
class InventoryDashboardController extends Controller
15+
{
16+
public function index(): Response
17+
{
18+
$tenantId = auth()->user()->tenant_id;
19+
20+
$totalProducts = Product::where('tenant_id', $tenantId)->count();
21+
$lowStockCount = Product::where('tenant_id', $tenantId)->with('stockLevels')
22+
->get()->filter(fn($p) => $p->stockLevels->sum('quantity') < $p->reorder_point)->count();
23+
$outOfStockCount = Product::where('tenant_id', $tenantId)->with('stockLevels')
24+
->get()->filter(fn($p) => $p->stockLevels->sum('quantity') <= 0)->count();
25+
$pendingPoCount = PurchaseOrder::where('tenant_id', $tenantId)
26+
->whereIn('status', ['draft', 'submitted', 'approved'])->count();
27+
$totalWarehouses = Warehouse::where('tenant_id', $tenantId)->count();
28+
$activeSuppliers = Supplier::where('tenant_id', $tenantId)->where('is_active', true)->count();
29+
30+
$lowStockItems = Product::where('tenant_id', $tenantId)
31+
->with('stockLevels')
32+
->get()
33+
->map(fn($p) => [
34+
'id' => $p->id,
35+
'name' => $p->name,
36+
'sku' => $p->sku,
37+
'quantity' => round($p->stockLevels->sum('quantity'), 2),
38+
'reorder_point' => $p->reorder_point,
39+
])
40+
->filter(fn($p) => $p['quantity'] < $p['reorder_point'])
41+
->sortBy('quantity')
42+
->take(10)
43+
->values();
44+
45+
$recentPos = PurchaseOrder::where('tenant_id', $tenantId)
46+
->with('supplier')
47+
->latest()
48+
->take(5)
49+
->get()
50+
->map(fn($po) => [
51+
'id' => $po->id,
52+
'status' => $po->status,
53+
'supplier' => $po->supplier?->name,
54+
'created_at' => $po->created_at?->format('M d, Y'),
55+
]);
56+
57+
$movements7d = StockMovement::where('tenant_id', $tenantId)
58+
->where('created_at', '>=', now()->subDays(7))
59+
->selectRaw('DATE(created_at) as date, type, SUM(quantity) as total')
60+
->groupBy('date', 'type')
61+
->orderBy('date')
62+
->get();
63+
64+
return Inertia::render('Inventory/Dashboard', compact(
65+
'totalProducts', 'lowStockCount', 'outOfStockCount', 'pendingPoCount',
66+
'totalWarehouses', 'activeSuppliers', 'lowStockItems', 'recentPos', 'movements7d'
67+
));
68+
}
69+
}

erp/app/Modules/Inventory/routes/inventory.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
use App\Modules\Inventory\Http\Controllers\InventoryDashboardController;
4+
35
use App\Modules\Inventory\Http\Controllers\AssetController;
46
use App\Modules\Inventory\Http\Controllers\CostingController;
57
use App\Modules\Inventory\Http\Controllers\AssetMaintenanceController;
@@ -26,6 +28,11 @@
2628
use App\Modules\Inventory\Http\Controllers\StockTransferController;
2729
use Illuminate\Support\Facades\Route;
2830

31+
32+
Route::middleware(['web','auth','verified'])->prefix('inventory')->name('inventory.')->group(function() {
33+
Route::get('dashboard', [InventoryDashboardController::class, 'index'])->name('dashboard');
34+
});
35+
2936
Route::middleware(['web', 'auth', 'verified'])->prefix('inventory')->name('inventory.')->group(function () {
3037

3138
// Products

erp/resources/js/Components/Layout/Sidebar.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const navItems: NavItem[] = [
5454
icon: inventoryIcon,
5555
permission: 'inventory.view',
5656
children: [
57+
{ label: 'Dashboard', href: '/inventory/dashboard', icon: <span /> },
5758
{ label: 'Products', href: '/inventory/products', icon: inventoryIcon },
5859
{ label: 'Categories', href: '/inventory/categories', icon: inventoryIcon },
5960
{ label: 'Product Categories', href: '/inventory/product-categories', icon: inventoryIcon },
@@ -89,6 +90,8 @@ const navItems: NavItem[] = [
8990
{ label: 'Units of Measure', href: '/inventory/units-of-measure', icon: <span /> },
9091
{ label: 'Cycle Counts', href: '/inventory/cycle-counts', icon: <span /> },
9192
{ label: 'Product Tags', href: '/inventory/product-tags', icon: <span /> },
93+
{ label: 'Shipments', href: '/inventory/shipments', icon: <span /> },
94+
{ label: 'RMA Requests', href: '/inventory/rma-requests', icon: <span /> },
9295
],
9396
},
9497
{
@@ -101,6 +104,7 @@ const navItems: NavItem[] = [
101104
),
102105
permission: 'finance.view',
103106
children: [
107+
{ label: 'Dashboard', href: '/finance/dashboard', icon: <span /> },
104108
{ label: 'Invoices', href: '/finance/invoices', icon: <span /> },
105109
{ label: 'Recurring Invoices', href: '/finance/recurring-invoices', icon: <span /> },
106110
{ label: 'Quotes', href: '/finance/quotes', icon: <span /> },
@@ -152,6 +156,8 @@ const navItems: NavItem[] = [
152156
{ label: 'Petty Cash', href: '/finance/petty-cash', icon: <span /> },
153157
{ label: 'Bank Transfers', href: '/finance/bank-transfers', icon: <span /> },
154158
{ label: 'Advance Payments', href: '/finance/advance-payments', icon: <span /> },
159+
{ label: 'Profit Centers', href: '/finance/profit-centers', icon: <span /> },
160+
{ label: 'Expense Budgets', href: '/finance/expense-budgets', icon: <span /> },
155161
],
156162
},
157163
{
@@ -164,6 +170,7 @@ const navItems: NavItem[] = [
164170
),
165171
permission: 'hr.view',
166172
children: [
173+
{ label: 'Dashboard', href: '/hr/dashboard', icon: <span /> },
167174
{ label: 'Employees', href: '/hr/employees', icon: <span /> },
168175
{ label: 'Departments', href: '/hr/departments', icon: <span /> },
169176
{ label: 'Leave Types', href: '/hr/leave-types', icon: <span /> },

0 commit comments

Comments
 (0)