Skip to content

Commit ad59139

Browse files
committed
Phases 211-215: Executive Dashboard + Bulk Import — 19 tests passing
ExecutiveDashboardController: cross-module KPIs (financial, operations, people, pipeline), 6-month revenue trend, recent activity feed. Executive.tsx with pure CSS bar chart and activity feed. ImportController: CSV import for Products (match-on-SKU), Employees (match-on-email), Contacts (supplier→vendor mapping), with skip-bad-rows error handling and flash counts. Import/Index.tsx with 3 import cards and data URI sample downloads. Sidebar: Executive Dashboard link at top, Import Data under Admin. 19/19 tests, 1825 total passing. https://claude.ai/code/session_01RdUGwo74JXChRCF88Yu27d
1 parent 77fc2d5 commit ad59139

8 files changed

Lines changed: 1276 additions & 2 deletions

File tree

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use App\Modules\CRM\Models\CrmLead;
6+
use App\Modules\Finance\Models\Bill;
7+
use App\Modules\Finance\Models\ExpenseClaim;
8+
use App\Modules\Finance\Models\Invoice;
9+
use App\Modules\Helpdesk\Models\HelpdeskTicket;
10+
use App\Modules\HR\Models\Employee;
11+
use App\Modules\HR\Models\LeaveRequest;
12+
use App\Modules\Inventory\Models\Product;
13+
use App\Modules\Manufacturing\Models\ManufacturingOrder;
14+
use Carbon\Carbon;
15+
use Illuminate\Http\Request;
16+
use Inertia\Inertia;
17+
18+
class ExecutiveDashboardController extends Controller
19+
{
20+
public function index(Request $request)
21+
{
22+
$tenantId = $request->user()->tenant_id;
23+
$now = Carbon::now();
24+
25+
// ── Financial KPIs ────────────────────────────────────────────────────
26+
27+
// Monthly revenue: sum of paid invoice line-item totals this month
28+
$monthlyRevenue = Invoice::where('tenant_id', $tenantId)
29+
->whereIn('status', ['paid', 'partial'])
30+
->whereYear('issue_date', $now->year)
31+
->whereMonth('issue_date', $now->month)
32+
->with('items')
33+
->get()
34+
->sum(fn ($inv) => $inv->total);
35+
36+
// Monthly expenses: approved/paid expense claims this month
37+
$monthlyExpenses = ExpenseClaim::where('tenant_id', $tenantId)
38+
->whereIn('status', ['approved', 'paid'])
39+
->whereYear('claim_date', $now->year)
40+
->whereMonth('claim_date', $now->month)
41+
->sum('total_amount');
42+
43+
// Outstanding invoices: status in (sent, partial, overdue)
44+
$outstandingInvoices = Invoice::where('tenant_id', $tenantId)
45+
->whereIn('status', ['sent', 'partial'])
46+
->with(['items', 'payments'])
47+
->get();
48+
49+
$outstandingInvoicesCount = $outstandingInvoices->count();
50+
$outstandingInvoicesTotal = $outstandingInvoices->sum(fn ($inv) => $inv->amount_due);
51+
52+
// Overdue invoices: status=overdue OR (due_date < today AND not paid/cancelled/draft)
53+
$overdueInvoicesCount = Invoice::where('tenant_id', $tenantId)
54+
->where(function ($q) use ($now) {
55+
$q->where(function ($sub) use ($now) {
56+
$sub->whereNotIn('status', ['paid', 'cancelled', 'draft'])
57+
->where('due_date', '<', $now->startOfDay()->toDateString());
58+
});
59+
})
60+
->count();
61+
62+
// ── Operations KPIs ───────────────────────────────────────────────────
63+
64+
// Low stock: products where stock_quantity <= reorder_point
65+
$lowStockCount = Product::where('tenant_id', $tenantId)
66+
->whereColumn('stock_quantity', '<=', 'reorder_point')
67+
->where('reorder_point', '>', 0)
68+
->count();
69+
70+
// Open purchase orders: bills with status draft or received (closest to "sent")
71+
$openPurchaseOrders = Bill::where('tenant_id', $tenantId)
72+
->whereIn('status', ['draft', 'received'])
73+
->count();
74+
75+
// Active manufacturing orders
76+
$activeManufacturingOrders = ManufacturingOrder::where('tenant_id', $tenantId)
77+
->whereIn('status', ['confirmed', 'in_progress'])
78+
->count();
79+
80+
// ── People KPIs ───────────────────────────────────────────────────────
81+
82+
$totalEmployees = Employee::where('tenant_id', $tenantId)
83+
->where('status', 'active')
84+
->count();
85+
86+
$pendingLeaveRequests = LeaveRequest::where('tenant_id', $tenantId)
87+
->where('status', 'pending')
88+
->count();
89+
90+
$openHelpdeskTickets = HelpdeskTicket::where('tenant_id', $tenantId)
91+
->whereIn('status', ['open', 'in_progress', 'pending'])
92+
->count();
93+
94+
// ── CRM KPIs ──────────────────────────────────────────────────────────
95+
96+
$openLeads = CrmLead::where('tenant_id', $tenantId)
97+
->where('type', 'lead')
98+
->where('status', 'open')
99+
->count();
100+
101+
$openOpportunities = CrmLead::where('tenant_id', $tenantId)
102+
->where('type', 'opportunity')
103+
->where('status', 'open')
104+
->count();
105+
106+
$pipelineValue = CrmLead::where('tenant_id', $tenantId)
107+
->where('type', 'opportunity')
108+
->where('status', 'open')
109+
->sum('expected_revenue');
110+
111+
// ── Revenue Trend (last 6 months) ─────────────────────────────────────
112+
113+
$revenueTrend = collect();
114+
for ($i = 5; $i >= 0; $i--) {
115+
$date = $now->copy()->subMonths($i);
116+
$rev = Invoice::where('tenant_id', $tenantId)
117+
->whereIn('status', ['paid', 'partial', 'sent'])
118+
->whereYear('issue_date', $date->year)
119+
->whereMonth('issue_date', $date->month)
120+
->with('items')
121+
->get()
122+
->sum(fn ($inv) => $inv->total);
123+
124+
$revenueTrend->push([
125+
'month' => $date->format('M Y'),
126+
'revenue' => round($rev, 2),
127+
]);
128+
}
129+
130+
// ── Recent Activity ───────────────────────────────────────────────────
131+
132+
$recentInvoices = Invoice::where('tenant_id', $tenantId)
133+
->with('contact')
134+
->latest()
135+
->take(3)
136+
->get()
137+
->map(fn ($inv) => [
138+
'type' => 'Invoice',
139+
'title' => ($inv->number ?? 'INV') . ($inv->contact ? '' . $inv->contact->name : ''),
140+
'status' => $inv->status,
141+
'created_at' => $inv->created_at,
142+
'url' => '/finance/invoices/' . $inv->id,
143+
]);
144+
145+
$recentLeads = CrmLead::where('tenant_id', $tenantId)
146+
->latest()
147+
->take(3)
148+
->get()
149+
->map(fn ($lead) => [
150+
'type' => 'Lead',
151+
'title' => $lead->title ?? ($lead->contact_name ?? 'Lead'),
152+
'status' => $lead->status,
153+
'created_at' => $lead->created_at,
154+
'url' => '/crm/leads/' . $lead->id,
155+
]);
156+
157+
$recentTickets = HelpdeskTicket::where('tenant_id', $tenantId)
158+
->latest()
159+
->take(3)
160+
->get()
161+
->map(fn ($t) => [
162+
'type' => 'Ticket',
163+
'title' => $t->subject ?? 'Ticket #' . $t->id,
164+
'status' => $t->status,
165+
'created_at' => $t->created_at,
166+
'url' => '/helpdesk/tickets/' . $t->id,
167+
]);
168+
169+
$recentOrders = ManufacturingOrder::where('tenant_id', $tenantId)
170+
->latest()
171+
->take(1)
172+
->get()
173+
->map(fn ($mo) => [
174+
'type' => 'Order',
175+
'title' => $mo->mo_number ?? 'MO #' . $mo->id,
176+
'status' => $mo->status,
177+
'created_at' => $mo->created_at,
178+
'url' => '/manufacturing/manufacturing-orders/' . $mo->id,
179+
]);
180+
181+
$recentActivity = $recentInvoices
182+
->concat($recentLeads)
183+
->concat($recentTickets)
184+
->concat($recentOrders)
185+
->sortByDesc('created_at')
186+
->take(10)
187+
->values()
188+
->map(fn ($item) => array_merge($item, [
189+
'created_at' => $item['created_at'] ? $item['created_at']->toIso8601String() : null,
190+
]));
191+
192+
return Inertia::render('Dashboard/Executive', [
193+
// Financial
194+
'monthly_revenue' => round($monthlyRevenue, 2),
195+
'monthly_expenses' => round($monthlyExpenses, 2),
196+
'outstanding_invoices_count' => $outstandingInvoicesCount,
197+
'outstanding_invoices_total' => round($outstandingInvoicesTotal, 2),
198+
'overdue_invoices_count' => $overdueInvoicesCount,
199+
// Operations
200+
'low_stock_count' => $lowStockCount,
201+
'open_purchase_orders' => $openPurchaseOrders,
202+
'active_manufacturing_orders' => $activeManufacturingOrders,
203+
// People
204+
'total_employees' => $totalEmployees,
205+
'pending_leave_requests' => $pendingLeaveRequests,
206+
'open_helpdesk_tickets' => $openHelpdeskTickets,
207+
// CRM
208+
'open_leads' => $openLeads,
209+
'open_opportunities' => $openOpportunities,
210+
'pipeline_value' => round($pipelineValue, 2),
211+
// Charts
212+
'revenue_trend' => $revenueTrend->values(),
213+
'recent_activity' => $recentActivity,
214+
]);
215+
}
216+
}

0 commit comments

Comments
 (0)