Faza 3.4: Finanțe — Plăți + Cheltuieli + Cashflow
Schema: - payments: client_id, work_order_id, user_id (operator), paid_at, amount, method (cash/card/transfer/mobile), reference, notes - expenses: supplier_id, purchase_id, paid_at, category (salary/purchase/rent/ utilities/advance/tax/fuel/tools/marketing/other), name, amount, method, ref Logică auto: - Payment::saved/deleted recalculează automat work_order.pay_status (unpaid → partial → paid) based on suma totală vs work_order.total - WO model are noi metode: payments(), paidAmount(), balanceDue() Filament resources (group Finanțe): - PaymentResource: form cu legare opțională la WO + client; tabel cu Sum summary, filtre azi/luna_curentă/method - ExpenseResource: 10 categorii preset, badge categ, total summary, filtru luna curentă - PaymentsRelationManager pe WO: "Plăți" tab cu auto-fill client_id + user_id la creare Widget FinanceOverview: - Încasări (luna), Cheltuieli (luna), Profit (luna), Datorii clienți - color coded: profit verde sau roșu, datorii galben/verde Settings page fix (Filament v5): - mount() folosește acum $this->form->fill([...]) în loc de $this->data direct - Filament v5 cere fill explicit pentru a inițializa state-ul schemei Seed: - 1 plată parțială pe fișa BMW (200 din 750) - 6 cheltuieli demo: 3 salarii, chirie, electricitate, achiziție piese Total Filament tenant routes: 69.
This commit is contained in:
@@ -6,6 +6,7 @@ use App\Tenancy\TenantManager;
|
||||
use Filament\Forms;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Schemas;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class Settings extends Page
|
||||
@@ -27,9 +28,13 @@ class Settings extends Page
|
||||
public function mount(): void
|
||||
{
|
||||
$company = app(TenantManager::class)->current();
|
||||
if (! $company) {
|
||||
return;
|
||||
}
|
||||
$settings = (array) ($company->settings ?? []);
|
||||
|
||||
$this->data = [
|
||||
// Filament v5: fill via $this->form->fill() (initializes the schema state).
|
||||
$this->form->fill([
|
||||
'display_name' => $company->display_name ?? $company->name,
|
||||
'city' => $company->city,
|
||||
'phone' => $company->phone,
|
||||
@@ -40,7 +45,7 @@ class Settings extends Page
|
||||
'labor_rate' => $settings['labor_rate'] ?? 400,
|
||||
'services' => isset($settings['services']) ? implode(', ', (array) $settings['services']) : '',
|
||||
'cars' => isset($settings['cars']) ? implode(', ', (array) $settings['cars']) : '',
|
||||
];
|
||||
]);
|
||||
}
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
@@ -88,6 +93,10 @@ class Settings extends Page
|
||||
{
|
||||
$data = $this->form->getState();
|
||||
$company = app(TenantManager::class)->current();
|
||||
if (! $company) {
|
||||
Notification::make()->title('Tenant not resolved')->danger()->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$company->update([
|
||||
'display_name' => $data['display_name'] ?? null,
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Tenant\Resources;
|
||||
|
||||
use App\Filament\Tenant\Resources\ExpenseResource\Pages;
|
||||
use App\Models\Tenant\Expense;
|
||||
use App\Models\Tenant\Supplier;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ExpenseResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Expense::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-arrow-trending-down';
|
||||
|
||||
protected static ?string $navigationLabel = 'Cheltuieli';
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Finanțe';
|
||||
|
||||
protected static ?string $modelLabel = 'cheltuială';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'cheltuieli';
|
||||
|
||||
protected static ?int $navigationSort = 51;
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema->components([
|
||||
Schemas\Components\Section::make('Cheltuială')
|
||||
->columns(2)
|
||||
->schema([
|
||||
Forms\Components\DatePicker::make('paid_at')->label('Data')->default(today())->required(),
|
||||
Forms\Components\Select::make('category')
|
||||
->options(Expense::CATEGORIES)
|
||||
->default('other')
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('name')->label('Denumire')->required()->maxLength(160)->columnSpanFull(),
|
||||
Forms\Components\TextInput::make('amount')->label('Sumă')->numeric()->required(),
|
||||
Forms\Components\Select::make('method')
|
||||
->options(Expense::METHODS)
|
||||
->default('cash')
|
||||
->required(),
|
||||
Forms\Components\Select::make('supplier_id')
|
||||
->label('Furnizor (opțional)')
|
||||
->options(fn () => Supplier::pluck('name', 'id'))
|
||||
->searchable(),
|
||||
Forms\Components\TextInput::make('reference')->label('Ref.')->maxLength(64),
|
||||
]),
|
||||
Forms\Components\Textarea::make('notes')->label('Notițe')->columnSpanFull()->rows(2),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('paid_at')->label('Data')->date('d.m.Y')->sortable(),
|
||||
Tables\Columns\TextColumn::make('category')
|
||||
->formatStateUsing(fn ($s) => Expense::CATEGORIES[$s] ?? $s)
|
||||
->badge(),
|
||||
Tables\Columns\TextColumn::make('name')->searchable()->wrap(),
|
||||
Tables\Columns\TextColumn::make('supplier.name')->label('Furnizor')->placeholder('—')->toggleable(),
|
||||
Tables\Columns\TextColumn::make('method')
|
||||
->formatStateUsing(fn ($s) => Expense::METHODS[$s] ?? $s),
|
||||
Tables\Columns\TextColumn::make('amount')->money('MDL')->alignRight()->sortable()
|
||||
->color('danger')
|
||||
->summarize(Tables\Columns\Summarizers\Sum::make()->money('MDL')->label('Total')),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('category')->options(Expense::CATEGORIES),
|
||||
Tables\Filters\Filter::make('this_month')
|
||||
->label('Luna curentă')
|
||||
->query(fn ($q) => $q->whereMonth('paid_at', now()->month)->whereYear('paid_at', now()->year)),
|
||||
])
|
||||
->actions([
|
||||
Actions\EditAction::make(),
|
||||
Actions\DeleteAction::make(),
|
||||
])
|
||||
->defaultSort('paid_at', 'desc');
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListExpenses::route('/'),
|
||||
'create' => Pages\CreateExpense::route('/create'),
|
||||
'edit' => Pages\EditExpense::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Tenant\Resources\ExpenseResource\Pages;
|
||||
|
||||
use App\Filament\Tenant\Resources\ExpenseResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateExpense extends CreateRecord
|
||||
{
|
||||
protected static string $resource = ExpenseResource::class;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Tenant\Resources\ExpenseResource\Pages;
|
||||
|
||||
use App\Filament\Tenant\Resources\ExpenseResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditExpense extends EditRecord
|
||||
{
|
||||
protected static string $resource = ExpenseResource::class;
|
||||
|
||||
protected function getHeaderActions(): array { return [Actions\DeleteAction::make()]; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Tenant\Resources\ExpenseResource\Pages;
|
||||
|
||||
use App\Filament\Tenant\Resources\ExpenseResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListExpenses extends ListRecords
|
||||
{
|
||||
protected static string $resource = ExpenseResource::class;
|
||||
|
||||
protected function getHeaderActions(): array { return [Actions\CreateAction::make()]; }
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Tenant\Resources;
|
||||
|
||||
use App\Filament\Tenant\Resources\PaymentResource\Pages;
|
||||
use App\Models\Tenant\Client;
|
||||
use App\Models\Tenant\Payment;
|
||||
use App\Models\Tenant\WorkOrder;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class PaymentResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Payment::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-banknotes';
|
||||
|
||||
protected static ?string $navigationLabel = 'Plăți';
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Finanțe';
|
||||
|
||||
protected static ?string $modelLabel = 'plată';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'plăți';
|
||||
|
||||
protected static ?int $navigationSort = 50;
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema->components([
|
||||
Schemas\Components\Section::make('Plată')
|
||||
->columns(2)
|
||||
->schema([
|
||||
Forms\Components\DatePicker::make('paid_at')->label('Data')->default(today())->required(),
|
||||
Forms\Components\Select::make('method')
|
||||
->options(Payment::METHODS)
|
||||
->default('cash')
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('amount')->label('Sumă')->numeric()->required(),
|
||||
Forms\Components\TextInput::make('reference')->label('Referință (chitanță / tranzacție)')->maxLength(64),
|
||||
Forms\Components\Select::make('client_id')
|
||||
->label('Client')
|
||||
->options(fn () => Client::pluck('name', 'id'))
|
||||
->searchable(),
|
||||
Forms\Components\Select::make('work_order_id')
|
||||
->label('Fișă lucru')
|
||||
->options(fn () => WorkOrder::orderBy('id', 'desc')->limit(50)
|
||||
->get()->mapWithKeys(fn ($w) => [$w->id => "{$w->number} — " . ($w->client?->name ?? '?')])
|
||||
->toArray())
|
||||
->searchable()
|
||||
->helperText('Plata stabilește automat starea fișei (unpaid/partial/paid)'),
|
||||
Forms\Components\Textarea::make('notes')->label('Notițe')->columnSpanFull()->rows(2),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('paid_at')->label('Data')->date('d.m.Y')->sortable(),
|
||||
Tables\Columns\TextColumn::make('client.name')->label('Client')->searchable(),
|
||||
Tables\Columns\TextColumn::make('workOrder.number')->label('Fișă')->placeholder('—'),
|
||||
Tables\Columns\TextColumn::make('method')
|
||||
->formatStateUsing(fn ($s) => Payment::METHODS[$s] ?? $s)
|
||||
->badge(),
|
||||
Tables\Columns\TextColumn::make('amount')->money('MDL')->alignRight()->sortable()->summarize(Tables\Columns\Summarizers\Sum::make()->money('MDL')->label('Total')),
|
||||
Tables\Columns\TextColumn::make('reference')->label('Ref.')->placeholder('—')->toggleable(),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('method')->options(Payment::METHODS),
|
||||
Tables\Filters\Filter::make('today')
|
||||
->label('Astăzi')
|
||||
->query(fn ($q) => $q->whereDate('paid_at', today())),
|
||||
Tables\Filters\Filter::make('this_month')
|
||||
->label('Luna curentă')
|
||||
->query(fn ($q) => $q->whereMonth('paid_at', now()->month)->whereYear('paid_at', now()->year)),
|
||||
])
|
||||
->actions([
|
||||
Actions\EditAction::make(),
|
||||
Actions\DeleteAction::make(),
|
||||
])
|
||||
->defaultSort('paid_at', 'desc');
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListPayments::route('/'),
|
||||
'create' => Pages\CreatePayment::route('/create'),
|
||||
'edit' => Pages\EditPayment::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Tenant\Resources\PaymentResource\Pages;
|
||||
|
||||
use App\Filament\Tenant\Resources\PaymentResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreatePayment extends CreateRecord
|
||||
{
|
||||
protected static string $resource = PaymentResource::class;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Tenant\Resources\PaymentResource\Pages;
|
||||
|
||||
use App\Filament\Tenant\Resources\PaymentResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditPayment extends EditRecord
|
||||
{
|
||||
protected static string $resource = PaymentResource::class;
|
||||
|
||||
protected function getHeaderActions(): array { return [Actions\DeleteAction::make()]; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Tenant\Resources\PaymentResource\Pages;
|
||||
|
||||
use App\Filament\Tenant\Resources\PaymentResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListPayments extends ListRecords
|
||||
{
|
||||
protected static string $resource = PaymentResource::class;
|
||||
|
||||
protected function getHeaderActions(): array { return [Actions\CreateAction::make()]; }
|
||||
}
|
||||
@@ -145,6 +145,7 @@ class WorkOrderResource extends Resource
|
||||
return [
|
||||
RelationManagers\WorksRelationManager::class,
|
||||
RelationManagers\PartsRelationManager::class,
|
||||
RelationManagers\PaymentsRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Tenant\Resources\WorkOrderResource\RelationManagers;
|
||||
|
||||
use App\Models\Tenant\Payment;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class PaymentsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'payments';
|
||||
|
||||
protected static ?string $title = 'Plăți';
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema->components([
|
||||
Forms\Components\DatePicker::make('paid_at')->label('Data')->default(today())->required(),
|
||||
Forms\Components\TextInput::make('amount')->label('Sumă')->numeric()->required(),
|
||||
Forms\Components\Select::make('method')
|
||||
->options(Payment::METHODS)
|
||||
->default('cash')
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('reference')->label('Referință')->maxLength(64),
|
||||
Forms\Components\Textarea::make('notes')->label('Notițe')->columnSpanFull()->rows(2),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('amount')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('paid_at')->label('Data')->date('d.m.Y'),
|
||||
Tables\Columns\TextColumn::make('amount')->money('MDL')->alignRight()
|
||||
->summarize(Tables\Columns\Summarizers\Sum::make()->money('MDL')->label('Plătit')),
|
||||
Tables\Columns\TextColumn::make('method')
|
||||
->formatStateUsing(fn ($s) => Payment::METHODS[$s] ?? $s)
|
||||
->badge(),
|
||||
Tables\Columns\TextColumn::make('reference')->placeholder('—'),
|
||||
])
|
||||
->headerActions([
|
||||
Actions\CreateAction::make()->mutateFormDataUsing(function (array $data, RelationManager $livewire): array {
|
||||
$wo = $livewire->getOwnerRecord();
|
||||
$data['client_id'] = $wo->client_id;
|
||||
$data['user_id'] = auth()->id();
|
||||
return $data;
|
||||
}),
|
||||
])
|
||||
->actions([
|
||||
Actions\EditAction::make(),
|
||||
Actions\DeleteAction::make(),
|
||||
])
|
||||
->defaultSort('paid_at', 'desc');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Tenant\Widgets;
|
||||
|
||||
use App\Models\Tenant\Expense;
|
||||
use App\Models\Tenant\Payment;
|
||||
use App\Models\Tenant\WorkOrder;
|
||||
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
|
||||
use Filament\Widgets\StatsOverviewWidget\Stat;
|
||||
|
||||
class FinanceOverview extends BaseWidget
|
||||
{
|
||||
protected static ?int $sort = 2;
|
||||
|
||||
protected function getStats(): array
|
||||
{
|
||||
$start = now()->startOfMonth();
|
||||
$end = now()->endOfMonth();
|
||||
|
||||
$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'])
|
||||
->sum('total');
|
||||
$paidOnDebt = (float) Payment::whereIn('work_order_id',
|
||||
WorkOrder::where('pay_status', '!=', 'paid')->pluck('id')
|
||||
)->sum('amount');
|
||||
$debt = max(0, $debtTotal - $paidOnDebt);
|
||||
|
||||
return [
|
||||
Stat::make('Încasări (luna)', number_format($income, 2, '.', ' ') . ' MDL')
|
||||
->icon('heroicon-o-arrow-trending-up')
|
||||
->color('success')
|
||||
->description(now()->translatedFormat('F Y')),
|
||||
|
||||
Stat::make('Cheltuieli (luna)', number_format($expenses, 2, '.', ' ') . ' MDL')
|
||||
->icon('heroicon-o-arrow-trending-down')
|
||||
->color('danger'),
|
||||
|
||||
Stat::make('Profit (luna)', number_format($profit, 2, '.', ' ') . ' MDL')
|
||||
->icon('heroicon-o-banknotes')
|
||||
->color($profit >= 0 ? 'success' : 'danger'),
|
||||
|
||||
Stat::make('Datorii clienți', number_format($debt, 2, '.', ' ') . ' MDL')
|
||||
->icon('heroicon-o-exclamation-circle')
|
||||
->color($debt > 0 ? 'warning' : 'success'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenant;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Expense extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
public const CATEGORIES = [
|
||||
'salary' => 'Salariu',
|
||||
'purchase' => 'Achiziție piese',
|
||||
'rent' => 'Chirie',
|
||||
'utilities' => 'Utilități',
|
||||
'advance' => 'Avans',
|
||||
'tax' => 'Taxe',
|
||||
'fuel' => 'Combustibil',
|
||||
'tools' => 'Scule / consumabile',
|
||||
'marketing' => 'Marketing',
|
||||
'other' => 'Altele',
|
||||
];
|
||||
|
||||
public const METHODS = [
|
||||
'cash' => 'Numerar',
|
||||
'card' => 'Card',
|
||||
'transfer' => 'Virament',
|
||||
'mobile' => 'Mobile pay',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'company_id', 'supplier_id', 'purchase_id', 'user_id',
|
||||
'paid_at', 'category', 'name', 'amount', 'method', 'reference', 'notes',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'paid_at' => 'date',
|
||||
'amount' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function supplier(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Supplier::class);
|
||||
}
|
||||
|
||||
public function purchase(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Purchase::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenant;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Payment extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
public const METHODS = [
|
||||
'cash' => 'Numerar',
|
||||
'card' => 'Card',
|
||||
'transfer' => 'Virament',
|
||||
'mobile' => 'Mobile pay',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'company_id', 'client_id', 'work_order_id', 'user_id',
|
||||
'paid_at', 'amount', 'method', 'reference', 'notes',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'paid_at' => 'date',
|
||||
'amount' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function client(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Client::class);
|
||||
}
|
||||
|
||||
public function workOrder(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(WorkOrder::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* After save/delete, recompute the work order's pay_status.
|
||||
*/
|
||||
protected static function booted(): void
|
||||
{
|
||||
$sync = function (self $payment) {
|
||||
if (! $payment->work_order_id) {
|
||||
return;
|
||||
}
|
||||
$wo = WorkOrder::withoutGlobalScopes()->find($payment->work_order_id);
|
||||
if (! $wo) {
|
||||
return;
|
||||
}
|
||||
$paid = (float) static::withoutGlobalScopes()
|
||||
->where('work_order_id', $wo->id)
|
||||
->whereNull('deleted_at')
|
||||
->sum('amount');
|
||||
$total = (float) $wo->total;
|
||||
|
||||
if ($paid <= 0) {
|
||||
$wo->pay_status = 'unpaid';
|
||||
} elseif ($paid + 0.01 < $total) {
|
||||
$wo->pay_status = 'partial';
|
||||
} else {
|
||||
$wo->pay_status = 'paid';
|
||||
}
|
||||
$wo->save();
|
||||
};
|
||||
|
||||
static::saved($sync);
|
||||
static::deleted($sync);
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,21 @@ class WorkOrder extends Model
|
||||
return $this->hasMany(WorkOrderPart::class);
|
||||
}
|
||||
|
||||
public function payments(): HasMany
|
||||
{
|
||||
return $this->hasMany(Payment::class);
|
||||
}
|
||||
|
||||
public function paidAmount(): float
|
||||
{
|
||||
return (float) $this->payments()->sum('amount');
|
||||
}
|
||||
|
||||
public function balanceDue(): float
|
||||
{
|
||||
return max(0.0, (float) $this->total - $this->paidAmount());
|
||||
}
|
||||
|
||||
public function recalcTotal(): void
|
||||
{
|
||||
$worksTotal = $this->works()->sum('total');
|
||||
|
||||
@@ -44,6 +44,7 @@ class TenantPanelProvider extends PanelProvider
|
||||
->discoverWidgets(in: app_path('Filament/Tenant/Widgets'), for: 'App\\Filament\\Tenant\\Widgets')
|
||||
->widgets([
|
||||
\App\Filament\Tenant\Widgets\StatsOverview::class,
|
||||
\App\Filament\Tenant\Widgets\FinanceOverview::class,
|
||||
\App\Filament\Tenant\Widgets\LowStockTable::class,
|
||||
])
|
||||
->middleware([
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('payments', function (Blueprint $t) {
|
||||
$t->id();
|
||||
$t->foreignId('company_id')->constrained()->cascadeOnDelete();
|
||||
$t->foreignId('client_id')->nullable()->constrained()->nullOnDelete();
|
||||
$t->foreignId('work_order_id')->nullable()->constrained()->nullOnDelete();
|
||||
$t->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); // operator înregistrare
|
||||
|
||||
$t->date('paid_at')->default(now());
|
||||
$t->decimal('amount', 12, 2);
|
||||
$t->string('method')->default('cash'); // cash / card / transfer / mobile
|
||||
$t->string('reference')->nullable(); // nr. chitanță / tranzacție
|
||||
$t->text('notes')->nullable();
|
||||
|
||||
$t->timestamps();
|
||||
$t->softDeletes();
|
||||
|
||||
$t->index(['company_id', 'paid_at']);
|
||||
$t->index(['company_id', 'work_order_id']);
|
||||
$t->index(['company_id', 'method']);
|
||||
});
|
||||
|
||||
Schema::create('expenses', function (Blueprint $t) {
|
||||
$t->id();
|
||||
$t->foreignId('company_id')->constrained()->cascadeOnDelete();
|
||||
$t->foreignId('supplier_id')->nullable()->constrained()->nullOnDelete();
|
||||
$t->foreignId('purchase_id')->nullable()->constrained()->nullOnDelete();
|
||||
$t->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
|
||||
|
||||
$t->date('paid_at')->default(now());
|
||||
$t->string('category'); // salary / purchase / rent / utilities / advance / tax / other
|
||||
$t->string('name'); // descriere
|
||||
$t->decimal('amount', 12, 2);
|
||||
$t->string('method')->default('cash');
|
||||
$t->string('reference')->nullable();
|
||||
$t->text('notes')->nullable();
|
||||
|
||||
$t->timestamps();
|
||||
$t->softDeletes();
|
||||
|
||||
$t->index(['company_id', 'paid_at']);
|
||||
$t->index(['company_id', 'category']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('expenses');
|
||||
Schema::dropIfExists('payments');
|
||||
}
|
||||
};
|
||||
@@ -13,7 +13,9 @@ use App\Models\Tenant\Lead;
|
||||
use App\Models\Tenant\Post;
|
||||
use App\Models\Tenant\User;
|
||||
use App\Models\Tenant\Vehicle;
|
||||
use App\Models\Tenant\Expense;
|
||||
use App\Models\Tenant\Part;
|
||||
use App\Models\Tenant\Payment;
|
||||
use App\Models\Tenant\Purchase;
|
||||
use App\Models\Tenant\PurchaseItem;
|
||||
use App\Models\Tenant\Supplier;
|
||||
@@ -415,6 +417,29 @@ class DatabaseSeeder extends Seeder
|
||||
}
|
||||
$purchase->refresh()->recalcTotal();
|
||||
|
||||
// ─── Plăți demo ──────────────────────────────────────────
|
||||
// Plată parțială pe fișa demo (BMW)
|
||||
Payment::firstOrCreate(
|
||||
['company_id' => $psauto->id, 'work_order_id' => $wo->id, 'paid_at' => today()->subDays(1), 'amount' => 200],
|
||||
['client_id' => $c1->id, 'method' => 'cash', 'reference' => 'CHIT-001', 'user_id' => $admin->id]
|
||||
);
|
||||
|
||||
// ─── Cheltuieli demo ────────────────────────────────────
|
||||
$expensesData = [
|
||||
['salary', 'Salariu Vasile Ivanov', 8000, today()->startOfMonth(), 'cash'],
|
||||
['salary', 'Salariu Andrei Popov', 7500, today()->startOfMonth(), 'cash'],
|
||||
['salary', 'Salariu Nicolae Lupu', 7000, today()->startOfMonth(), 'cash'],
|
||||
['rent', 'Chirie spațiu service (luna curentă)', 4500, today()->startOfMonth(), 'transfer'],
|
||||
['utilities', 'Electricitate', 850, today()->subDays(15), 'card'],
|
||||
['purchase', 'Achiziție stoc (P-26-0001)', 1036, today()->subDays(7), 'transfer'],
|
||||
];
|
||||
foreach ($expensesData as [$cat, $name, $amount, $date, $method]) {
|
||||
Expense::firstOrCreate(
|
||||
['company_id' => $psauto->id, 'name' => $name, 'paid_at' => $date],
|
||||
['category' => $cat, 'amount' => $amount, 'method' => $method, 'user_id' => $admin->id]
|
||||
);
|
||||
}
|
||||
|
||||
app(TenantManager::class)->clear();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user