f4ccc306dc
Adds three usability improvements to the marja internă feature: 1. Settings UI section "Marjă internă (nu TVA)" — configurable at tenant level 2. Per-line toggle "Aplică marjă" on each manoperă 3. Global visibility flag to hide margin details in-session == 1. Settings page == New section on /app/settings (gated by FINANCE_VIEW_INTERNAL_MARGIN): - "% marjă implicit" numeric input with % suffix, 0–90 range - "Afișează detalii marjă la procesele calculate" toggle (default on) - Explicit label "Marjă internă (nu TVA)" plus helper text explaining it's not the Moldovan tax — feeds into MarginResolver as the tenant default, applied only when the mechanic has no per-user margin. Persists as company.settings.default_internal_margin_pct and company.settings.show_internal_margin_details. == 2. Per-line "Aplică marjă" toggle == New wo_works.apply_margin boolean, default true. When false: applied_margin_pct = 0 salary_base = total (mechanic gets salaried on the full amount) Use case: oil change, tire mount, and similar "at-cost" services where the shop doesn't want to hold back part of the labor rate. The owner can flag those specific lines while keeping margin on diagnostic and premium labor. WorksRelationManager form gains a Toggle field (gated by FINANCE_VIEW_INTERNAL_MARGIN); table gains a ToggleColumn for quick inline flipping without opening the row. Booted hook now recomputes salary_base when apply_margin is dirtied, so toggling live in the table takes effect immediately. == 3. Show internal margin details flag == Global tenant flag (default on): when off, the gray subtitle line "Bază salariu: 200 · marjă 20%" under the Total column disappears for everyone, even users with FINANCE_VIEW_INTERNAL_MARGIN. Practical use: when reviewing a Fișă face-to-face with the client on the manager's screen, flip the flag off from Settings for the day → no risk of the client accidentally seeing internal numbers. Flip back when done. The flag lives in company.settings.show_internal_margin_details. == Description text on the Total column == Now shows either: - "Bază salariu: 200.00 MDL · marjă 20%" when apply_margin=true - "Fără marjă · bază salariu = Total" when apply_margin=false - nothing when show_internal_margin_details=false or role lacks permission == Tests == InternalMarginToggleTest (5): - apply_margin=false → salary_base equals total, applied_margin_pct=0 - apply_margin=true (default) still applies 20% margin - Toggling apply_margin recomputes salary_base bidirectionally - Company default margin resolves when mechanic has no per-user setting - show_internal_margin_details flag persists correctly in Company.settings Suite: 303 passed (845 assertions). Was 298. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
125 lines
6.1 KiB
PHP
125 lines
6.1 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\Tenant\Resources\WorkOrderResource\RelationManagers;
|
|
|
|
use App\Models\Tenant\Labor;
|
|
use App\Models\Tenant\User;
|
|
use App\Models\Tenant\WorkOrderWork;
|
|
use Filament\Actions;
|
|
use Filament\Forms;
|
|
use Filament\Resources\RelationManagers\RelationManager;
|
|
use Filament\Schemas\Components\Utilities\Set;
|
|
use Filament\Schemas\Schema;
|
|
use Filament\Tables;
|
|
use Filament\Tables\Table;
|
|
|
|
class WorksRelationManager extends RelationManager
|
|
{
|
|
protected static string $relationship = 'works';
|
|
|
|
protected static ?string $title = 'Manopere';
|
|
|
|
public function form(Schema $schema): Schema
|
|
{
|
|
return $schema->components([
|
|
Forms\Components\Select::make('labor_id')
|
|
->label('Catalog manoperă')
|
|
->options(fn () => Labor::where('is_active', true)
|
|
->get()
|
|
->mapWithKeys(fn ($l) => [$l->id => "[{$l->category}] {$l->name_ro} ({$l->hours}h)"])
|
|
->toArray())
|
|
->searchable()
|
|
->live()
|
|
->afterStateUpdated(function ($state, Set $set) {
|
|
if ($state && $labor = Labor::find($state)) {
|
|
$set('name', $labor->name_ro);
|
|
$set('hours', $labor->hours);
|
|
$set('price_per_hour', $labor->hours > 0 ? round($labor->price / max((float) $labor->hours, 1), 2) : 0);
|
|
}
|
|
})
|
|
->columnSpanFull(),
|
|
Forms\Components\TextInput::make('name')->label('Nume')->required()->columnSpanFull(),
|
|
Forms\Components\TextInput::make('hours')->label('Ore')->numeric()->default(1)->required(),
|
|
Forms\Components\TextInput::make('price_per_hour')->label('Preț/h')->numeric()->required(),
|
|
Forms\Components\Select::make('master_id')
|
|
->label('Maistru')
|
|
->options(fn () => User::pluck('name', 'id'))
|
|
->searchable(),
|
|
Forms\Components\Select::make('status')
|
|
->options(WorkOrderWork::STATUSES)
|
|
->default('todo')
|
|
->required(),
|
|
Forms\Components\Toggle::make('apply_margin')
|
|
->label('Aplică marjă internă')
|
|
->default(true)
|
|
->helperText('On = din prețul manoperei se scade marja pentru salariu. Off = manoperă la cost (salariu se calculează pe Total integral).')
|
|
->visible(fn () => auth()->user()?->canDo(\App\Auth\Permissions::FINANCE_VIEW_INTERNAL_MARGIN) ?? false),
|
|
Forms\Components\Textarea::make('notes')->label('Notițe')->columnSpanFull()->rows(2),
|
|
]);
|
|
}
|
|
|
|
/** Global visibility flag from tenant Settings — user can hide margin details in-session. */
|
|
private static function marginDetailsVisible(): bool
|
|
{
|
|
if (! auth()->user()?->canDo(\App\Auth\Permissions::FINANCE_VIEW_INTERNAL_MARGIN)) {
|
|
return false;
|
|
}
|
|
$tenant = app(\App\Tenancy\TenantManager::class)->current();
|
|
return (bool) data_get($tenant?->settings, 'show_internal_margin_details', true);
|
|
}
|
|
|
|
public function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->recordTitleAttribute('name')
|
|
->columns([
|
|
Tables\Columns\TextColumn::make('name')->label('Manoperă')->wrap(),
|
|
Tables\Columns\TextColumn::make('hours')->label('Ore')->numeric(decimalPlaces: 2)->alignRight(),
|
|
Tables\Columns\TextColumn::make('price_per_hour')->label('Preț/h')->money('MDL')->alignRight(),
|
|
Tables\Columns\TextColumn::make('total')
|
|
->label('Total')
|
|
->money('MDL')
|
|
->alignRight()
|
|
->description(function ($record) {
|
|
if (! self::marginDetailsVisible() || $record->salary_base === null) return null;
|
|
if (! $record->apply_margin) {
|
|
return 'Fără marjă · bază salariu = Total';
|
|
}
|
|
return 'Bază salariu: ' . number_format((float) $record->salary_base, 2) . ' MDL · marjă ' . rtrim(rtrim(number_format((float) $record->applied_margin_pct, 2), '0'), '.') . '%';
|
|
}),
|
|
Tables\Columns\ToggleColumn::make('apply_margin')
|
|
->label('Marjă')
|
|
->visible(fn () => auth()->user()?->canDo(\App\Auth\Permissions::FINANCE_VIEW_INTERNAL_MARGIN) ?? false)
|
|
->tooltip('On = se aplică marja internă. Off = manoperă la cost.'),
|
|
Tables\Columns\TextColumn::make('master.name')->label('Maistru')->placeholder('—'),
|
|
Tables\Columns\TextColumn::make('status')
|
|
->formatStateUsing(fn ($s) => WorkOrderWork::STATUSES[$s] ?? $s)
|
|
->badge()
|
|
->colors(['gray' => ['todo'], 'warning' => ['in_progress'], 'success' => ['done']]),
|
|
])
|
|
->headerActions([
|
|
Actions\CreateAction::make()
|
|
->after(function (WorkOrderWork $record) {
|
|
// Auto-add the labor's default parts to the parent WO.
|
|
if (! $record->labor_id) return;
|
|
$labor = Labor::with('laborParts.part')->find($record->labor_id);
|
|
$wo = $record->workOrder;
|
|
if (! $labor || ! $wo || $labor->laborParts->isEmpty()) return;
|
|
$composer = app(\App\Services\ServiceComposer::class);
|
|
foreach ($labor->laborParts as $lp) {
|
|
if ($lp->part) {
|
|
$composer->addPart($wo, $lp->part, (float) $lp->qty, $lp->unit);
|
|
}
|
|
}
|
|
\Filament\Notifications\Notification::make()
|
|
->title('Piese implicite adăugate (' . $labor->laborParts->count() . ')')
|
|
->success()->send();
|
|
}),
|
|
])
|
|
->actions([
|
|
Actions\EditAction::make(),
|
|
Actions\DeleteAction::make(),
|
|
]);
|
|
}
|
|
}
|