113610ea8f
Two changes in one commit:
== 1. Moved apply_margin toggle from line-level to Fișă-level ==
The per-manoperă apply_margin toggle is gone from the Manopere tab.
In its place: a single "Aplică marjă internă" toggle in Fișa's
"Plată & total" section (next to override_margin_pct). One decision
per Fișă instead of per line — cleaner mental model, matches how the
shop actually thinks about at-cost vs. billable work.
Migration: work_orders.apply_margin boolean default true (idempotent).
WorkOrderWork::saving now reads WO.apply_margin from DB directly (not
via belongsTo cache) to determine salary_base:
- WO.apply_margin=false → every line gets salary_base=total, applied_margin_pct=0
- WO.apply_margin=true → resolver chain (WO.override → user margin → tenant default)
Old wo_works.apply_margin column stays untouched (backward-compat with
existing rows), but no longer exposed in UI. Tests updated to new
semantic. All existing tests green.
== 2. Full i18n audit on client-facing portal — RO/RU separated ==
Problem: user selecting Russian saw Romanian mixed into headings,
buttons, labels. Every client-facing Blade file was 100% hardcoded
Romanian — zero __() calls.
Fix: created lang/ro/portal.php + lang/ru/portal.php with 131 keys
across 3 namespaces:
- portal.common (email, phone, save, total, powered_by, ...)
- portal.invitation (welcome_name, activate_account, expired_body, ...)
- portal.tracking (title_fisa, approve, approval_needed_title,
ready_estimated, hours, unit_pcs, ...)
- portal.shop (catalog, cart, checkout_title, order_number, vin_title,
signin_title, add_to_cart, in_stock, ...)
Converted 15 Blade files to __() calls:
- resources/views/invitations/{accept,expired,invalid}.blade.php
- resources/views/tracking/show.blade.php
- resources/views/shop/{layout,catalog,cart,checkout,order,account,part,vin}.blade.php
- resources/views/shop/auth/{login,register,forgot,reset}.blade.php
Each view's <html lang="{{ app()->getLocale() }}"> now reflects the
resolved locale (was hardcoded lang="ro").
SetLocale middleware resolves locale in this order:
1. session locale (user picked via language switcher)
2. authenticated user.locale
3. tenant.settings.language
4. app.locale default (now 'ro')
Config change: config/app.php default locale + fallback both = 'ro'
(was 'en'). English falls back to Romanian for portal.* keys since
we don't ship English portal translations — a Romanian shop that
switches to English shows Romanian text, which is safer than showing
"portal.invitation.activate_account" literals.
phpunit.xml sets APP_LOCALE=ro so test assertSee() calls that look
for Romanian text pass.
Verified via portal.* grep: 131 __() calls across 15 files. Zero
hardcoded Romanian nouns/verbs left in any client-facing view.
Suite: 303 passed (840 assertions). Unchanged count — refactor,
not new tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
116 lines
5.4 KiB
PHP
116 lines
5.4 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\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 ((float) $record->applied_margin_pct === 0.0) {
|
|
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\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(),
|
|
]);
|
|
}
|
|
}
|