fix: rework labor tree/list toggle — use Livewire wire:click not Alpine
Previous approach wrapped {{ $this->table }} in x-show which caused
Livewire/Alpine conflicts and broken toggling. New approach:
- viewMode is a Livewire #[Url] property (persisted in URL as ?view=)
- wire:click="setViewMode('list/tree')" triggers a clean Livewire render
- Only one view is rendered at a time (@if, not x-show)
- Tree: Alpine manages per-category open/close state client-side via
indexed array; Open/Close all buttons use setAll() helper
- Visual overhaul: tighter cards, chevron arrows, proper hover states
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ use App\Models\Tenant\Labor;
|
||||
use App\Models\Tenant\LaborCategory;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Livewire\Attributes\Url;
|
||||
|
||||
class ListLabors extends ListRecords
|
||||
{
|
||||
@@ -14,6 +15,14 @@ class ListLabors extends ListRecords
|
||||
|
||||
protected string $view = 'filament.tenant.resources.labor.list-labors';
|
||||
|
||||
#[Url(as: 'view')]
|
||||
public string $viewMode = 'list';
|
||||
|
||||
public function setViewMode(string $mode): void
|
||||
{
|
||||
$this->viewMode = in_array($mode, ['list', 'tree']) ? $mode : 'list';
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [Actions\CreateAction::make()];
|
||||
@@ -25,10 +34,9 @@ class ListLabors extends ListRecords
|
||||
$labors = Labor::orderBy('name_ro')->get()->groupBy('category');
|
||||
|
||||
return $cats->map(fn ($cat) => (object) [
|
||||
'id' => $cat->id,
|
||||
'label' => $cat->label(),
|
||||
'name' => $cat->name,
|
||||
'items' => $labors->get($cat->name, collect()),
|
||||
])->filter(fn ($c) => $c->items->isNotEmpty());
|
||||
])->filter(fn ($c) => $c->items->isNotEmpty())->values();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,135 +1,133 @@
|
||||
<x-filament-panels::page>
|
||||
|
||||
<div x-data="{
|
||||
mode: localStorage.getItem('labor_view_mode') || 'list',
|
||||
open: {},
|
||||
toggleAll(val) {
|
||||
@js($this->getLaborTree()->keys()->values())
|
||||
.forEach(k => this.open[k] = val);
|
||||
}
|
||||
}"
|
||||
x-init="$watch('mode', v => localStorage.setItem('labor_view_mode', v));
|
||||
$nextTick(() => { if (mode === 'tree') toggleAll(true); })">
|
||||
|
||||
{{-- ── View toggle bar ── --}}
|
||||
<div style="display:flex;justify-content:flex-end;gap:8px;margin-bottom:14px;align-items:center;">
|
||||
|
||||
{{-- Expand / Collapse all (only in tree mode) --}}
|
||||
<span x-show="mode === 'tree'" style="display:flex;gap:6px;">
|
||||
<button type="button" @click="toggleAll(true)"
|
||||
style="font-size:.8rem;color:#6b7280;background:none;border:none;cursor:pointer;padding:4px 8px;">
|
||||
↕ {{ __('Deschide tot') }}
|
||||
</button>
|
||||
<button type="button" @click="toggleAll(false)"
|
||||
style="font-size:.8rem;color:#6b7280;background:none;border:none;cursor:pointer;padding:4px 8px;">
|
||||
↕ {{ __('Închide tot') }}
|
||||
</button>
|
||||
</span>
|
||||
|
||||
{{-- Toggle buttons --}}
|
||||
<div style="display:inline-flex;border:1px solid #e5e7eb;border-radius:8px;overflow:hidden;box-shadow:0 1px 2px rgba(0,0,0,.05);">
|
||||
<button type="button" @click="mode='list'"
|
||||
:style="mode==='list'
|
||||
? 'background:#2563eb;color:#fff;'
|
||||
: 'background:#fff;color:#374151;'"
|
||||
style="padding:6px 16px;border:none;cursor:pointer;font-size:.875rem;font-weight:500;display:flex;align-items:center;gap:6px;transition:background .15s;">
|
||||
<svg style="width:15px;height:15px;flex-shrink:0;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
{{-- ── Toggle bar ── --}}
|
||||
<div style="display:flex;justify-content:flex-end;margin-bottom:16px;">
|
||||
<div style="display:inline-flex;border:1px solid #e5e7eb;border-radius:8px;overflow:hidden;box-shadow:0 1px 2px rgba(0,0,0,.06);">
|
||||
<button type="button"
|
||||
wire:click="setViewMode('list')"
|
||||
style="padding:7px 18px;border:none;cursor:pointer;font-size:.875rem;font-weight:500;
|
||||
display:flex;align-items:center;gap:7px;transition:background .12s;
|
||||
{{ $this->viewMode === 'list' ? 'background:#2563eb;color:#fff;' : 'background:#fff;color:#4b5563;' }}">
|
||||
<svg style="width:15px;height:15px;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.2">
|
||||
<path stroke-linecap="round" d="M4 6h16M4 12h16M4 18h16"/>
|
||||
</svg>
|
||||
{{ __('Listă') }}
|
||||
</button>
|
||||
<button type="button" @click="mode='tree'; $nextTick(() => toggleAll(true))"
|
||||
:style="mode==='tree'
|
||||
? 'background:#2563eb;color:#fff;'
|
||||
: 'background:#fff;color:#374151;'"
|
||||
style="padding:6px 16px;border:none;cursor:pointer;font-size:.875rem;font-weight:500;display:flex;align-items:center;gap:6px;transition:background .15s;border-left:1px solid #e5e7eb;">
|
||||
<svg style="width:15px;height:15px;flex-shrink:0;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" d="M3 7h4M3 12h8M3 17h12M9 7h12M13 12h8M17 17h4"/>
|
||||
<button type="button"
|
||||
wire:click="setViewMode('tree')"
|
||||
style="padding:7px 18px;border:none;cursor:pointer;font-size:.875rem;font-weight:500;
|
||||
display:flex;align-items:center;gap:7px;transition:background .12s;
|
||||
border-left:1px solid #e5e7eb;
|
||||
{{ $this->viewMode === 'tree' ? 'background:#2563eb;color:#fff;' : 'background:#fff;color:#4b5563;' }}">
|
||||
<svg style="width:15px;height:15px;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.2">
|
||||
<path stroke-linecap="round" d="M3 5h4v4H3zM9 7h12M3 12h4v4H3zM9 14h8M3 19h4v4H3zM9 21h5"/>
|
||||
</svg>
|
||||
{{ __('Arbore') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── LIST mode: standard Filament table ── --}}
|
||||
<div x-show="mode === 'list'">
|
||||
{{-- ── LIST mode ── --}}
|
||||
@if ($this->viewMode === 'list')
|
||||
{{ $this->table }}
|
||||
</div>
|
||||
|
||||
{{-- ── TREE mode: category accordion ── --}}
|
||||
<div x-show="mode === 'tree'" x-cloak style="display:flex;flex-direction:column;gap:6px;">
|
||||
@endif
|
||||
|
||||
{{-- ── TREE mode ── --}}
|
||||
@if ($this->viewMode === 'tree')
|
||||
@php $tree = $this->getLaborTree(); @endphp
|
||||
|
||||
@forelse ($tree as $cat)
|
||||
<div x-data="{ key: '{{ $cat->name }}' }"
|
||||
style="border:1px solid #e5e7eb;border-radius:10px;overflow:hidden;background:#fff;">
|
||||
<div x-data="{
|
||||
all: {},
|
||||
setAll(v) { Object.keys(all).forEach(k => all[k] = v); }
|
||||
}"
|
||||
x-init="{{ $tree->map(fn($c, $i) => "all[{$i}] = true")->implode('; ') }}">
|
||||
|
||||
{{-- Category header --}}
|
||||
{{-- Open/close all --}}
|
||||
<div style="display:flex;gap:8px;margin-bottom:10px;">
|
||||
<button type="button" @click="setAll(true)"
|
||||
style="font-size:.8rem;color:#6b7280;background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;padding:4px 12px;cursor:pointer;">
|
||||
⬇ {{ __('Deschide tot') }}
|
||||
</button>
|
||||
<button type="button" @click="setAll(false)"
|
||||
style="font-size:.8rem;color:#6b7280;background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;padding:4px 12px;cursor:pointer;">
|
||||
⬆ {{ __('Închide tot') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Category panels --}}
|
||||
<div style="display:flex;flex-direction:column;gap:4px;">
|
||||
@foreach ($tree as $i => $cat)
|
||||
<div style="border:1px solid #e5e7eb;border-radius:8px;overflow:hidden;">
|
||||
|
||||
{{-- Header --}}
|
||||
<button type="button"
|
||||
@click="open[key] = !open[key]"
|
||||
@click="all[{{ $i }}] = !all[{{ $i }}]"
|
||||
style="width:100%;display:flex;align-items:center;justify-content:space-between;
|
||||
padding:11px 16px;background:none;border:none;cursor:pointer;text-align:left;">
|
||||
padding:10px 16px;border:none;cursor:pointer;text-align:left;
|
||||
background:#f9fafb;">
|
||||
<span style="display:flex;align-items:center;gap:10px;">
|
||||
<span style="font-weight:600;font-size:.95rem;color:#111827;">{{ $cat->label }}</span>
|
||||
<span style="font-size:.75rem;color:#9ca3af;background:#f3f4f6;border-radius:20px;padding:1px 8px;">
|
||||
<svg :style="all[{{ $i }}] ? 'transform:rotate(90deg)' : ''"
|
||||
style="width:14px;height:14px;color:#9ca3af;transition:transform .18s;flex-shrink:0;"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
|
||||
<path stroke-linecap="round" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<span style="font-weight:600;font-size:.9rem;color:#111827;">{{ $cat->label }}</span>
|
||||
<span style="font-size:.72rem;color:#9ca3af;background:#e5e7eb;border-radius:20px;padding:1px 8px;font-weight:500;">
|
||||
{{ $cat->items->count() }}
|
||||
</span>
|
||||
</span>
|
||||
<svg :style="open[key] ? 'transform:rotate(180deg)' : ''"
|
||||
style="width:16px;height:16px;color:#9ca3af;transition:transform .2s;flex-shrink:0;"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
|
||||
<path stroke-linecap="round" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{{-- Labor items --}}
|
||||
<div x-show="open[key]" x-cloak
|
||||
style="border-top:1px solid #f3f4f6;">
|
||||
<table style="width:100%;border-collapse:collapse;font-size:.875rem;">
|
||||
{{-- Items table --}}
|
||||
<div x-show="all[{{ $i }}]" x-cloak
|
||||
style="border-top:1px solid #f3f4f6;background:#fff;">
|
||||
<table style="width:100%;border-collapse:collapse;font-size:.85rem;">
|
||||
<thead>
|
||||
<tr style="background:#f9fafb;color:#6b7280;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em;">
|
||||
<th style="padding:7px 16px;text-align:left;font-weight:500;">{{ __('Manoperă') }}</th>
|
||||
<th style="padding:7px 8px;text-align:center;font-weight:500;">{{ __('Mod') }}</th>
|
||||
<th style="padding:7px 8px;text-align:right;font-weight:500;">{{ __('Ore') }}</th>
|
||||
<th style="padding:7px 8px;text-align:right;font-weight:500;">{{ __('Preț') }}</th>
|
||||
<th style="padding:7px 8px;text-align:right;font-weight:500;">{{ __('Cost') }}</th>
|
||||
<th style="padding:7px 16px;text-align:right;font-weight:500;"></th>
|
||||
<tr style="color:#9ca3af;font-size:.72rem;text-transform:uppercase;letter-spacing:.04em;border-bottom:1px solid #f3f4f6;">
|
||||
<th style="padding:6px 16px;text-align:left;font-weight:500;">{{ __('Denumire') }}</th>
|
||||
<th style="padding:6px 8px;text-align:center;font-weight:500;white-space:nowrap;">{{ __('Mod') }}</th>
|
||||
<th style="padding:6px 8px;text-align:right;font-weight:500;">{{ __('Ore') }}</th>
|
||||
<th style="padding:6px 8px;text-align:right;font-weight:500;">{{ __('Preț') }}</th>
|
||||
<th style="padding:6px 8px;text-align:right;font-weight:500;">{{ __('Cost') }}</th>
|
||||
<th style="padding:6px 16px;text-align:right;font-weight:500;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($cat->items as $labor)
|
||||
<tr style="border-top:1px solid #f3f4f6;"
|
||||
x-on:mouseenter="$el.style.background='#f9fafb'"
|
||||
x-on:mouseleave="$el.style.background=''">
|
||||
<td style="padding:8px 16px;">
|
||||
<tr style="border-top:1px solid #f9fafb;"
|
||||
onmouseenter="this.style.background='#f9fafb'"
|
||||
onmouseleave="this.style.background=''">
|
||||
<td style="padding:8px 16px;max-width:320px;">
|
||||
<span style="font-weight:500;color:#111827;">{{ $labor->label() }}</span>
|
||||
@if ($labor->code)
|
||||
<span style="font-size:.75rem;color:#9ca3af;margin-left:6px;">{{ $labor->code }}</span>
|
||||
<span style="font-size:.72rem;color:#9ca3af;margin-left:6px;">{{ $labor->code }}</span>
|
||||
@endif
|
||||
@if (! $labor->is_active)
|
||||
<span style="font-size:.7rem;background:#fee2e2;color:#dc2626;border-radius:4px;padding:1px 5px;margin-left:6px;">{{ __('Inactiv') }}</span>
|
||||
<span style="font-size:.7rem;background:#fee2e2;color:#dc2626;border-radius:4px;padding:1px 6px;margin-left:6px;">{{ __('Inactiv') }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="padding:8px;text-align:center;">
|
||||
<span style="font-size:.75rem;background:{{ $labor->pricing_mode === 'fixed' ? '#dbeafe' : '#f3f4f6' }};
|
||||
color:{{ $labor->pricing_mode === 'fixed' ? '#1d4ed8' : '#6b7280' }};
|
||||
border-radius:4px;padding:2px 7px;">
|
||||
<td style="padding:8px;text-align:center;white-space:nowrap;">
|
||||
<span style="font-size:.72rem;border-radius:4px;padding:2px 8px;
|
||||
{{ $labor->pricing_mode === 'fixed'
|
||||
? 'background:#dbeafe;color:#1d4ed8;'
|
||||
: 'background:#f3f4f6;color:#6b7280;' }}">
|
||||
{{ $labor->pricing_mode === 'fixed' ? __('Fix') : __('Pe oră') }}
|
||||
</span>
|
||||
</td>
|
||||
<td style="padding:8px;text-align:right;color:#374151;">
|
||||
{{ $labor->pricing_mode === 'hourly' ? rtrim(rtrim(number_format((float)$labor->hours, 2), '0'), '.') : '—' }}
|
||||
<td style="padding:8px;text-align:right;color:#374151;white-space:nowrap;">
|
||||
{{ $labor->pricing_mode === 'hourly'
|
||||
? rtrim(rtrim(number_format((float)$labor->hours, 2), '0'), '.')
|
||||
: '—' }}
|
||||
</td>
|
||||
<td style="padding:8px;text-align:right;font-weight:500;color:#111827;">
|
||||
<td style="padding:8px;text-align:right;font-weight:600;color:#111827;white-space:nowrap;">
|
||||
{{ number_format((float)($labor->pricing_mode === 'fixed' ? $labor->fixed_price : $labor->price), 2, '.', ' ') }} MDL
|
||||
</td>
|
||||
<td style="padding:8px;text-align:right;color:#6b7280;font-size:.82rem;">
|
||||
<td style="padding:8px;text-align:right;color:#9ca3af;white-space:nowrap;font-size:.8rem;">
|
||||
{{ $labor->cost_price > 0 ? number_format((float)$labor->cost_price, 2, '.', ' ') . ' MDL' : '—' }}
|
||||
</td>
|
||||
<td style="padding:8px 16px;text-align:right;white-space:nowrap;">
|
||||
<a href="{{ \App\Filament\Tenant\Resources\LaborResource::getUrl('edit', ['record' => $labor->id]) }}"
|
||||
style="font-size:.8rem;color:#2563eb;text-decoration:none;margin-right:10px;">
|
||||
style="font-size:.8rem;color:#2563eb;text-decoration:none;">
|
||||
{{ __('Editează') }}
|
||||
</a>
|
||||
</td>
|
||||
@@ -139,11 +137,9 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div style="text-align:center;color:#9ca3af;padding:32px;">{{ __('Nicio manoperă găsită.') }}</div>
|
||||
@endforelse
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</x-filament-panels::page>
|
||||
|
||||
Reference in New Issue
Block a user