Port expense report grouped itemized view + i18n + return types from master

Ports the net behaviour from three master commits into v3.0 as a single change, because v3.0 has already diverged structurally (controller moved from V1/Admin/Report to Company/Report, blade has its own CSS rework using the bundled fonts partial, and v3.0's App\Facades\Pdf replaces Barryvdh\DomPDF\Facade\Pdf). The three source commits are: 834b53ea (grouped itemized expenses), e22050bc (DomPDF facade + Pint — adapted to v3.0's App\Facades\Pdf), 0e9f18d4 (expenses.uncategorized + pdf_expense_group_total_label i18n keys + View|Response return type).

Controller: replaces the expenseCategories aggregate fetch with an itemized Expense query ordered by date, groups by category name with expenses.uncategorized fallback, and shares an expenseGroups collection of {name, expenses, total} plus the overall totalExpense. Adds expense_category_id to applyFilters. Updates the docblock return type from JsonResponse to View|Response. Keeps v3.0's App\Facades\Pdf.

Blade: replaces the single expenseCategories aggregate table with a per-group itemized table (date / note / amount columns + per-group total line using the new pdf_expense_group_total_label i18n key). Adds the item-table-* CSS classes and removes the old expense-total-table bottom block.

lang/en.json: adds expenses.uncategorized = "Uncategorized" and pdf_expense_group_total_label = "Group total:".
This commit is contained in:
Darko Gjorgjijoski
2026-04-07 17:28:34 +02:00
parent f5e876122d
commit 119a1712b0
3 changed files with 98 additions and 33 deletions

View File

@@ -9,8 +9,9 @@ use App\Models\CompanySetting;
use App\Models\Currency; use App\Models\Currency;
use App\Models\Expense; use App\Models\Expense;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Http\JsonResponse; use Illuminate\Contracts\View\View;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
class ExpensesReportController extends Controller class ExpensesReportController extends Controller
@@ -19,7 +20,7 @@ class ExpensesReportController extends Controller
* Handle the incoming request. * Handle the incoming request.
* *
* @param string $hash * @param string $hash
* @return JsonResponse * @return View|Response
*/ */
public function __invoke(Request $request, $hash) public function __invoke(Request $request, $hash)
{ {
@@ -31,14 +32,26 @@ class ExpensesReportController extends Controller
App::setLocale($locale); App::setLocale($locale);
$expenseCategories = Expense::with('category') // Fetch individual expenses (filtered and ordered by date), then group by category
$expenses = Expense::with('category')
->whereCompanyId($company->id) ->whereCompanyId($company->id)
->applyFilters($request->only(['from_date', 'to_date'])) ->applyFilters($request->only(['from_date', 'to_date', 'expense_category_id']))
->expensesAttributes() ->orderBy('expense_date', 'asc')
->get(); ->get();
$totalAmount = 0;
foreach ($expenseCategories as $category) { $totalAmount = $expenses->sum('base_amount');
$totalAmount += $category->total_amount;
$grouped = $expenses->groupBy(function ($item) {
return $item->category ? $item->category->name : trans('expenses.uncategorized');
});
$expenseGroups = collect();
foreach ($grouped as $categoryName => $group) {
$expenseGroups->push([
'name' => $categoryName,
'expenses' => $group,
'total' => $group->sum('base_amount'),
]);
} }
$dateFormat = CompanySetting::getSetting('carbon_date_format', $company->id); $dateFormat = CompanySetting::getSetting('carbon_date_format', $company->id);
@@ -62,7 +75,7 @@ class ExpensesReportController extends Controller
->get(); ->get();
view()->share([ view()->share([
'expenseCategories' => $expenseCategories, 'expenseGroups' => $expenseGroups,
'colorSettings' => $colorSettings, 'colorSettings' => $colorSettings,
'totalExpense' => $totalAmount, 'totalExpense' => $totalAmount,
'company' => $company, 'company' => $company,

View File

@@ -675,6 +675,7 @@
"currency": "Currency", "currency": "Currency",
"contact": "Contact", "contact": "Contact",
"category": "Category", "category": "Category",
"uncategorized": "Uncategorized",
"from_date": "From Date", "from_date": "From Date",
"to_date": "To Date", "to_date": "To Date",
"expense_date": "Date", "expense_date": "Date",
@@ -1722,6 +1723,7 @@
"pdf_total_tax_label": "TOTAL TAX", "pdf_total_tax_label": "TOTAL TAX",
"pdf_tax_types_label": "Tax Types", "pdf_tax_types_label": "Tax Types",
"pdf_expenses_label": "Expenses", "pdf_expenses_label": "Expenses",
"pdf_expense_group_total_label": "Group total:",
"pdf_bill_to": "Bill to,", "pdf_bill_to": "Bill to,",
"pdf_ship_to": "Ship to,", "pdf_ship_to": "Ship to,",
"pdf_received_from": "Received from:", "pdf_received_from": "Received from:",

View File

@@ -134,6 +134,62 @@
line-height: 21px; line-height: 21px;
color: #5851D8; color: #5851D8;
} }
/* -- Items Table (grouped itemized expenses) -- */
.item-table-heading-left {
font-size: 13.5px;
text-align: left;
color: rgba(0, 0, 0, 0.85);
padding: 5px;
padding-bottom: 10px;
}
.item-table-heading-right {
font-size: 13.5px;
text-align: right;
color: rgba(0, 0, 0, 0.85);
padding: 5px;
padding-bottom: 10px;
}
tr.item-table-heading-row th {
border-bottom: 0.620315px solid #E8E8E8;
font-size: 12px;
line-height: 18px;
}
tr.item-row td {
font-size: 12px;
line-height: 18px;
}
.item-cell-left {
font-size: 13px;
color: #040405;
text-align: left;
padding: 5px;
padding-top: 10px;
border-color: #d9d9d9;
}
.item-cell-right {
font-size: 13px;
color: #040405;
text-align: right;
padding: 5px;
padding-top: 10px;
border-color: #d9d9d9;
}
.item-table-group-total {
font-size: 14px;
font-weight: bold;
text-align: right;
color: rgba(0, 0, 0, 0.85);
padding: 5px;
padding-bottom: 10px;
}
</style> </style>
</head> </head>
@@ -156,33 +212,27 @@
</tr> </tr>
</table> </table>
<p class="expenses-title">@lang('pdf_expenses_label')</p> <p class="expenses-title">@lang('pdf_expenses_label')</p>
<div class="expenses-table-container"> @foreach ($expenseGroups as $group)
<table class="expenses-table"> <p class="expense-title">{{ $group['name'] }}</p>
@foreach ($expenseCategories as $expenseCategory) <table width="100%" style="margin-bottom:18px;">
<tr> <tr class="item-table-heading-row">
<td> <th style="width: 15%;" class="text-left item-table-heading-left">@lang('expenses.date')</th>
<p class="expense-title"> <th style="width: 70%;" class="text-left item-table-heading-left">@lang('expenses.note')</th>
{{ $expenseCategory->category->name }} <th style="width: 15%;" class="text-right item-table-heading-right">@lang('expenses.amount')</th>
</p> </tr>
</td> @foreach ($group['expenses'] as $expense)
<td> <tr class="item-row">
<p class="expense-amount"> <td style="width: 15%;" class="text-left item-cell-left">{{ $expense->formatted_expense_date }}</td>
{!! format_money_pdf($expenseCategory->total_amount, $currency) !!} <td style="width: 70%;" class="text-left item-cell-left">{{ $expense->notes ? $expense->notes : '-' }}</td>
</p> <td style="width: 15%;" class="text-right item-cell-right">{!! format_money_pdf($expense->base_amount, $currency) !!}</td>
</td>
</tr> </tr>
@endforeach @endforeach
</table> </table>
</div> <div class="item-table-group-total">
<p>@lang('pdf_expense_group_total_label')&nbsp;&nbsp;&nbsp;<span style="color: #5851D8;">{!! format_money_pdf($group['total'], $currency) !!}</span></p>
</div>
@endforeach
</div> </div>
<table class="expense-total-table">
<tr>
<td class="expense-total-cell">
<p class="expense-total">{!! format_money_pdf($totalExpense, $currency) !!}</p>
</td>
</tr>
</table>
<table class="report-footer"> <table class="report-footer">
<tr> <tr>
<td> <td>