feat: partial payment field + photo upload from dashboard
- WorkOrderResource: pay_status is now live(); added _partial_amount TextInput (visible only when pay_status=partial) that shows current paid amount and accepts a new total — dehydrated so it does not save directly; _achitat hidden when partial is selected to avoid confusion - EditWorkOrder.afterSave: creates a Payment record for the difference when pay_status=partial and entered amount > existing paidAmount - WorkOrderDashboard: WithFileUploads + uploadPhotos / deletePhoto methods using Spatie MediaLibrary - Dashboard blade: Foto tab replaced with upload form + gallery with per-photo delete buttons Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ namespace App\Filament\Tenant\Pages;
|
|||||||
|
|
||||||
use App\Models\Tenant\WorkOrder;
|
use App\Models\Tenant\WorkOrder;
|
||||||
use Filament\Pages\Page;
|
use Filament\Pages\Page;
|
||||||
|
use Livewire\WithFileUploads;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mitchell1-style 3-column dashboard view for a single WorkOrder.
|
* Mitchell1-style 3-column dashboard view for a single WorkOrder.
|
||||||
@@ -12,6 +13,8 @@ use Filament\Pages\Page;
|
|||||||
*/
|
*/
|
||||||
class WorkOrderDashboard extends Page
|
class WorkOrderDashboard extends Page
|
||||||
{
|
{
|
||||||
|
use WithFileUploads;
|
||||||
|
|
||||||
protected static string|\BackedEnum|null $navigationIcon = null;
|
protected static string|\BackedEnum|null $navigationIcon = null;
|
||||||
|
|
||||||
protected static bool $shouldRegisterNavigation = false;
|
protected static bool $shouldRegisterNavigation = false;
|
||||||
@@ -191,6 +194,32 @@ class WorkOrderDashboard extends Page
|
|||||||
->get();
|
->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public array $newPhotos = [];
|
||||||
|
|
||||||
|
public function uploadPhotos(): void
|
||||||
|
{
|
||||||
|
$this->validate(['newPhotos.*' => 'image|max:15360']);
|
||||||
|
foreach ($this->newPhotos as $photo) {
|
||||||
|
$this->record->addMedia($photo->getRealPath())
|
||||||
|
->usingFileName($photo->getClientOriginalName())
|
||||||
|
->toMediaCollection('photos');
|
||||||
|
}
|
||||||
|
$this->newPhotos = [];
|
||||||
|
$this->record->refresh();
|
||||||
|
\Filament\Notifications\Notification::make()
|
||||||
|
->title(__('Fotografii adăugate'))
|
||||||
|
->success()->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deletePhoto(int $mediaId): void
|
||||||
|
{
|
||||||
|
$media = $this->record->getMedia('photos')->firstWhere('id', $mediaId);
|
||||||
|
if ($media && (int) $media->model_id === $this->record->id) {
|
||||||
|
$media->delete();
|
||||||
|
$this->record->refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public string $newMessage = '';
|
public string $newMessage = '';
|
||||||
|
|
||||||
public function sendMessage(): void
|
public function sendMessage(): void
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ class WorkOrderResource extends Resource
|
|||||||
Schemas\Components\Section::make(__('Plată & total'))
|
Schemas\Components\Section::make(__('Plată & total'))
|
||||||
->columns(3)->collapsible()->collapsed()->compact()
|
->columns(3)->collapsible()->collapsed()->compact()
|
||||||
->schema([
|
->schema([
|
||||||
Forms\Components\Select::make('pay_status')->label(__('Statut plată'))->options(\App\Support\I18n::opts(WorkOrder::PAY_STATUSES))->default('unpaid')->required(),
|
Forms\Components\Select::make('pay_status')->label(__('Statut plată'))->options(\App\Support\I18n::opts(WorkOrder::PAY_STATUSES))->default('unpaid')->required()->live(),
|
||||||
Forms\Components\TextInput::make('discount_pct')
|
Forms\Components\TextInput::make('discount_pct')
|
||||||
->label(__('Reducere %'))
|
->label(__('Reducere %'))
|
||||||
->numeric()
|
->numeric()
|
||||||
@@ -177,10 +177,18 @@ class WorkOrderResource extends Resource
|
|||||||
->label(__('Total (MDL)'))
|
->label(__('Total (MDL)'))
|
||||||
->numeric()->readOnly()->dehydrated(false)
|
->numeric()->readOnly()->dehydrated(false)
|
||||||
->extraAttributes(['style' => 'font-weight:600']),
|
->extraAttributes(['style' => 'font-weight:600']),
|
||||||
|
Forms\Components\TextInput::make('_partial_amount')
|
||||||
|
->label(__('Sumă achitată (MDL)'))
|
||||||
|
->numeric()->minValue(0)->step(0.01)
|
||||||
|
->dehydrated(false)
|
||||||
|
->default(fn ($record) => $record ? $record->paidAmount() : 0)
|
||||||
|
->visible(fn (Get $get) => $get('pay_status') === 'partial')
|
||||||
|
->helperText(__('Introduceți suma achitată — se înregistrează automat ca plată la salvare')),
|
||||||
Forms\Components\TextInput::make('_achitat')
|
Forms\Components\TextInput::make('_achitat')
|
||||||
->label(__('Achitat (MDL)'))
|
->label(__('Achitat (MDL)'))
|
||||||
->readOnly()->dehydrated(false)
|
->readOnly()->dehydrated(false)
|
||||||
->default(fn ($record) => $record ? number_format($record->paidAmount(), 2, '.', '') : '0.00')
|
->default(fn ($record) => $record ? number_format($record->paidAmount(), 2, '.', '') : '0.00')
|
||||||
|
->visible(fn (Get $get) => $get('pay_status') !== 'partial')
|
||||||
->extraAttributes(['style' => 'color:#16a34a;font-weight:600']),
|
->extraAttributes(['style' => 'color:#16a34a;font-weight:600']),
|
||||||
Forms\Components\TextInput::make('_rest')
|
Forms\Components\TextInput::make('_rest')
|
||||||
->label(__('Rest de achitat (MDL)'))
|
->label(__('Rest de achitat (MDL)'))
|
||||||
|
|||||||
@@ -31,6 +31,26 @@ class EditWorkOrder extends EditRecord
|
|||||||
protected function afterSave(): void
|
protected function afterSave(): void
|
||||||
{
|
{
|
||||||
$this->record->recalcTotal();
|
$this->record->recalcTotal();
|
||||||
|
|
||||||
|
// Create payment for the difference when pay_status = partial
|
||||||
|
if ($this->record->pay_status === 'partial') {
|
||||||
|
$entered = (float) ($this->form->getState()['_partial_amount'] ?? 0);
|
||||||
|
$alreadyPaid = $this->record->paidAmount();
|
||||||
|
$diff = round($entered - $alreadyPaid, 2);
|
||||||
|
if ($diff > 0.005) {
|
||||||
|
\App\Models\Tenant\Payment::create([
|
||||||
|
'company_id' => $this->record->company_id,
|
||||||
|
'work_order_id' => $this->record->id,
|
||||||
|
'client_id' => $this->record->client_id,
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
'amount' => $diff,
|
||||||
|
'method' => 'cash',
|
||||||
|
'paid_at' => today(),
|
||||||
|
]);
|
||||||
|
$this->record->refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$this->fillForm();
|
$this->fillForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -696,12 +696,34 @@
|
|||||||
{{-- ── PHOTOS TAB ── --}}
|
{{-- ── PHOTOS TAB ── --}}
|
||||||
<div x-show="tab === 'photos'" x-cloak>
|
<div x-show="tab === 'photos'" x-cloak>
|
||||||
@php $photos = $wo->getMedia('photos'); @endphp
|
@php $photos = $wo->getMedia('photos'); @endphp
|
||||||
|
|
||||||
|
{{-- Upload --}}
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;margin-bottom:14px;flex-wrap:wrap;">
|
||||||
|
<label style="display:inline-flex;align-items:center;gap:6px;padding:7px 14px;background:#f3f4f6;border:1px solid #d1d5db;border-radius:6px;cursor:pointer;font-size:.875rem;font-weight:500;">
|
||||||
|
📎 {{ __('Alege foto') }}
|
||||||
|
<input type="file" wire:model="newPhotos" multiple accept="image/*" style="display:none;">
|
||||||
|
</label>
|
||||||
|
@if (!empty($newPhotos))
|
||||||
|
<button type="button" wire:click="uploadPhotos" class="wd-btn wd-btn-primary">
|
||||||
|
⬆ {{ __('Încarcă') }} ({{ count($newPhotos) }})
|
||||||
|
</button>
|
||||||
|
<span wire:loading wire:target="newPhotos" style="font-size:.8rem;color:#6b7280;">{{ __('Se procesează...') }}</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Gallery --}}
|
||||||
@if ($photos->isNotEmpty())
|
@if ($photos->isNotEmpty())
|
||||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px;">
|
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px;">
|
||||||
@foreach ($photos as $ph)
|
@foreach ($photos as $ph)
|
||||||
<a href="{{ $ph->getFullUrl() }}" target="_blank">
|
<div style="position:relative;">
|
||||||
<img src="{{ $ph->getFullUrl() }}" style="width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:6px;border:1px solid var(--wd-border);">
|
<a href="{{ $ph->getFullUrl() }}" target="_blank">
|
||||||
</a>
|
<img src="{{ $ph->getFullUrl() }}" style="width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:6px;border:1px solid var(--wd-border);">
|
||||||
|
</a>
|
||||||
|
<button type="button"
|
||||||
|
wire:click="deletePhoto({{ $ph->id }})"
|
||||||
|
wire:confirm="{{ __('Șterge fotografia?') }}"
|
||||||
|
style="position:absolute;top:4px;right:4px;background:rgba(0,0,0,.55);color:#fff;border:none;border-radius:50%;width:22px;height:22px;font-size:12px;cursor:pointer;line-height:1;">✕</button>
|
||||||
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
|
|||||||
Reference in New Issue
Block a user