3911012c65
Broad sweep across the whole codebase:
- Blade views (39 files, 315 wraps): tag-text and title/placeholder/alt
attributes wrapped with {{ __() }}. Excludes scripts, styles, @php,
@verbatim, {{ }}, {!! !!}, comments to avoid touching interpolations.
- PHP (54 files, 179 wraps): array 'key' => 'RO value' patterns and
list items with diacritics wrapped with __(). Reverted __() inside
const arrays (PHP disallows non-constant expressions).
- Added 269 new keys to lang/{ru,en}.json (identity fallback for
unknowns → 161 human RU + 163 EN translations added for the most
common enums, stages, roles, statuses, payment methods, vehicle
categories, warehouse, portal, form actions.
Missing translations fall back to RO so the UI never breaks. All 306
tests pass; view cache compiles cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
189 lines
6.5 KiB
PHP
189 lines
6.5 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\Tenant\Pages;
|
|
|
|
use App\Models\Tenant\Expense;
|
|
use App\Models\Tenant\Payment;
|
|
use App\Models\Tenant\WorkOrder;
|
|
use App\Models\Tenant\WorkOrderPart;
|
|
use App\Models\Tenant\WorkOrderWork;
|
|
use Carbon\Carbon;
|
|
use Filament\Pages\Page;
|
|
|
|
class Finance extends Page
|
|
{
|
|
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-currency-dollar';
|
|
|
|
public static function getNavigationLabel(): string
|
|
{
|
|
return __('nav.label.Finanțe (consolidat)');
|
|
}
|
|
|
|
public static function getNavigationGroup(): ?string
|
|
{
|
|
return __('nav.group.Finanțe');
|
|
}
|
|
|
|
protected static ?int $navigationSort = 49;
|
|
|
|
protected static ?string $title = null;
|
|
|
|
public function getTitle(): string
|
|
{
|
|
return __('Finanțe');
|
|
}
|
|
|
|
protected string $view = 'filament.tenant.pages.finance';
|
|
|
|
public string $tab = 'overview';
|
|
|
|
public string $period = 'this_month';
|
|
|
|
public function setTab(string $tab): void { $this->tab = $tab; }
|
|
public function setPeriod(string $p): void { $this->period = $p; }
|
|
|
|
public function tabs(): array
|
|
{
|
|
return [
|
|
'overview' => '📊 Overview',
|
|
'cashflow' => '💸 Cashflow',
|
|
'pnl' => '📈 P&L',
|
|
'balance' => __('⚖️ Balanță'),
|
|
];
|
|
}
|
|
|
|
public function periods(): array
|
|
{
|
|
return [
|
|
'this_month' => __('Luna curentă'),
|
|
'last_month' => __('Luna trecută'),
|
|
'this_year' => 'Anul curent',
|
|
'last_30' => 'Ultimele 30 zile',
|
|
];
|
|
}
|
|
|
|
public function dateRange(): array
|
|
{
|
|
return match ($this->period) {
|
|
'last_month' => [Carbon::now()->subMonthNoOverflow()->startOfMonth(), Carbon::now()->subMonthNoOverflow()->endOfMonth()],
|
|
'this_year' => [Carbon::now()->startOfYear(), Carbon::now()->endOfYear()],
|
|
'last_30' => [Carbon::now()->subDays(30)->startOfDay(), Carbon::now()->endOfDay()],
|
|
default => [Carbon::now()->startOfMonth(), Carbon::now()->endOfMonth()],
|
|
};
|
|
}
|
|
|
|
public function data(): array
|
|
{
|
|
[$start, $end] = $this->dateRange();
|
|
|
|
return match ($this->tab) {
|
|
'overview' => $this->overview($start, $end),
|
|
'cashflow' => $this->cashflow($start, $end),
|
|
'pnl' => $this->pnl($start, $end),
|
|
'balance' => $this->balance($start, $end),
|
|
default => [],
|
|
};
|
|
}
|
|
|
|
protected function overview($start, $end): array
|
|
{
|
|
$income = (float) Payment::whereBetween('paid_at', [$start, $end])->sum('amount');
|
|
$expenses = (float) Expense::whereBetween('paid_at', [$start, $end])->sum('amount');
|
|
$profit = $income - $expenses;
|
|
|
|
$debtTotal = (float) WorkOrder::where('pay_status', '!=', 'paid')
|
|
->whereNotIn('status', ['cancelled'])
|
|
->get()
|
|
->sum(fn ($w) => max(0, (float) $w->total - (float) $w->payments()->sum('amount')));
|
|
|
|
return compact('income', 'expenses', 'profit', 'debtTotal');
|
|
}
|
|
|
|
protected function cashflow($start, $end): array
|
|
{
|
|
// Aggregate per day.
|
|
$byDay = [];
|
|
$cur = $start->copy();
|
|
while ($cur <= $end) {
|
|
$key = $cur->format('Y-m-d');
|
|
$byDay[$key] = ['date' => $cur->format('d.m'), 'in' => 0, 'out' => 0];
|
|
$cur->addDay();
|
|
}
|
|
|
|
Payment::whereBetween('paid_at', [$start, $end])
|
|
->selectRaw('DATE(paid_at) d, SUM(amount) total')
|
|
->groupBy('d')->get()
|
|
->each(fn ($r) => $byDay[$r->d]['in'] = (float) $r->total);
|
|
|
|
Expense::whereBetween('paid_at', [$start, $end])
|
|
->selectRaw('DATE(paid_at) d, SUM(amount) total')
|
|
->groupBy('d')->get()
|
|
->each(fn ($r) => $byDay[$r->d]['out'] = (float) $r->total);
|
|
|
|
// Totals + max (for bar chart scale).
|
|
$totalIn = array_sum(array_column($byDay, 'in'));
|
|
$totalOut = array_sum(array_column($byDay, 'out'));
|
|
$maxAbs = max(0.001, max(...array_map(fn ($d) => max($d['in'], $d['out']), $byDay)));
|
|
|
|
return [
|
|
'rows' => array_values($byDay),
|
|
'totalIn' => $totalIn,
|
|
'totalOut' => $totalOut,
|
|
'net' => $totalIn - $totalOut,
|
|
'maxAbs' => $maxAbs,
|
|
];
|
|
}
|
|
|
|
protected function pnl($start, $end): array
|
|
{
|
|
// Revenue split: works vs parts vs other (manually entered payments without WO link).
|
|
$worksRevenue = (float) WorkOrderWork::whereHas('workOrder', fn ($q) => $q->whereBetween('opened_at', [$start, $end]))
|
|
->where('status', 'done')
|
|
->sum('total');
|
|
$partsRevenue = (float) WorkOrderPart::whereHas('workOrder', fn ($q) => $q->whereBetween('opened_at', [$start, $end]))
|
|
->where('status', 'installed')
|
|
->sum('total');
|
|
$partsCost = (float) WorkOrderPart::whereHas('workOrder', fn ($q) => $q->whereBetween('opened_at', [$start, $end]))
|
|
->where('status', 'installed')
|
|
->get()->sum(fn ($p) => (float) $p->buy_price * (float) $p->qty);
|
|
$partsMargin = $partsRevenue - $partsCost;
|
|
|
|
$expensesByCat = Expense::whereBetween('paid_at', [$start, $end])
|
|
->selectRaw('category, SUM(amount) total')
|
|
->groupBy('category')->orderByDesc('total')->get();
|
|
|
|
$totalExpenses = (float) $expensesByCat->sum('total');
|
|
$totalRevenue = $worksRevenue + $partsRevenue;
|
|
$netProfit = $totalRevenue - $partsCost - $totalExpenses;
|
|
$marginPct = $totalRevenue > 0 ? round($netProfit / $totalRevenue * 100, 1) : 0;
|
|
|
|
return compact(
|
|
'worksRevenue', 'partsRevenue', 'partsCost', 'partsMargin',
|
|
'expensesByCat', 'totalExpenses', 'totalRevenue', 'netProfit', 'marginPct'
|
|
);
|
|
}
|
|
|
|
protected function balance($start, $end): array
|
|
{
|
|
$allPayments = (float) Payment::sum('amount');
|
|
$allExpenses = (float) Expense::sum('amount');
|
|
|
|
$debt = (float) WorkOrder::where('pay_status', '!=', 'paid')
|
|
->whereNotIn('status', ['cancelled'])
|
|
->get()
|
|
->sum(fn ($w) => max(0, (float) $w->total - (float) $w->payments()->sum('amount')));
|
|
|
|
$stockValue = (float) \App\Models\Tenant\Part::where('is_active', true)
|
|
->get()
|
|
->sum(fn ($p) => (float) $p->qty * (float) $p->buy_price);
|
|
|
|
return [
|
|
'allTimePayments' => $allPayments,
|
|
'allTimeExpenses' => $allExpenses,
|
|
'debt' => $debt,
|
|
'stockValue' => $stockValue,
|
|
'netCash' => $allPayments - $allExpenses,
|
|
];
|
|
}
|
|
}
|