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:
2026-09-11 05:08:53 +00:00
parent 0180f29023
commit f885c04f90
2 changed files with 149 additions and 145 deletions
@@ -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>