cmplete story 4.1 with qa tests
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\TimelineStatus;
|
||||
use App\Models\AdminLog;
|
||||
use App\Models\Timeline;
|
||||
use App\Models\User;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
public string $search = '';
|
||||
public ?int $selectedUserId = null;
|
||||
public ?User $selectedUser = null;
|
||||
public string $caseName = '';
|
||||
public string $caseReference = '';
|
||||
public string $initialNotes = '';
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'selectedUserId' => ['required', 'exists:users,id'],
|
||||
'caseName' => ['required', 'string', 'max:255'],
|
||||
'caseReference' => ['nullable', 'string', 'max:50', 'unique:timelines,case_reference'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'selectedUserId.required' => __('timelines.client_required'),
|
||||
'caseReference.unique' => __('timelines.case_reference_exists'),
|
||||
];
|
||||
}
|
||||
|
||||
public function updatedSearch(): void
|
||||
{
|
||||
if ($this->selectedUser && ! str_contains(strtolower($this->selectedUser->full_name), strtolower($this->search))) {
|
||||
$this->selectedUserId = null;
|
||||
$this->selectedUser = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function getClientsProperty()
|
||||
{
|
||||
if (strlen($this->search) < 2) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return User::query()
|
||||
->clients()
|
||||
->active()
|
||||
->where(function ($query) {
|
||||
$query->where('full_name', 'like', "%{$this->search}%")
|
||||
->orWhere('email', 'like', "%{$this->search}%");
|
||||
})
|
||||
->limit(10)
|
||||
->get();
|
||||
}
|
||||
|
||||
public function selectUser(int $userId): void
|
||||
{
|
||||
$this->selectedUserId = $userId;
|
||||
$this->selectedUser = User::find($userId);
|
||||
$this->search = $this->selectedUser->full_name;
|
||||
}
|
||||
|
||||
public function clearSelection(): void
|
||||
{
|
||||
$this->selectedUserId = null;
|
||||
$this->selectedUser = null;
|
||||
$this->search = '';
|
||||
}
|
||||
|
||||
public function create(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$timeline = Timeline::create([
|
||||
'user_id' => $this->selectedUserId,
|
||||
'case_name' => $this->caseName,
|
||||
'case_reference' => $this->caseReference ?: null,
|
||||
'status' => TimelineStatus::Active,
|
||||
]);
|
||||
|
||||
if ($this->initialNotes) {
|
||||
$timeline->updates()->create([
|
||||
'admin_id' => auth()->id(),
|
||||
'update_text' => $this->initialNotes,
|
||||
]);
|
||||
}
|
||||
|
||||
AdminLog::create([
|
||||
'admin_id' => auth()->id(),
|
||||
'action' => 'create',
|
||||
'target_type' => 'timeline',
|
||||
'target_id' => $timeline->id,
|
||||
'new_values' => $timeline->toArray(),
|
||||
'ip_address' => request()->ip(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
session()->flash('success', __('messages.timeline_created'));
|
||||
|
||||
$this->redirect(route('admin.dashboard'), navigate: true);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div>
|
||||
<div class="mb-6">
|
||||
<flux:button variant="ghost" :href="route('admin.dashboard')" wire:navigate icon="arrow-left">
|
||||
{{ __('timelines.back_to_timelines') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<flux:heading size="xl">{{ __('timelines.create_timeline') }}</flux:heading>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<form wire:submit="create" class="space-y-6">
|
||||
{{-- Client Selection --}}
|
||||
<flux:field>
|
||||
<flux:label>{{ __('timelines.select_client') }} *</flux:label>
|
||||
|
||||
@if($selectedUser)
|
||||
<div class="flex items-center gap-3 rounded-lg border border-zinc-200 bg-zinc-50 p-3 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<flux:avatar size="sm" name="{{ $selectedUser->full_name }}" />
|
||||
<div class="flex-1">
|
||||
<div class="font-medium text-zinc-900 dark:text-zinc-100">{{ $selectedUser->full_name }}</div>
|
||||
<div class="text-sm text-zinc-500 dark:text-zinc-400">{{ $selectedUser->email }}</div>
|
||||
</div>
|
||||
<flux:button variant="ghost" size="sm" wire:click="clearSelection" icon="x-mark" />
|
||||
</div>
|
||||
@else
|
||||
<div class="relative">
|
||||
<flux:input
|
||||
wire:model.live.debounce.300ms="search"
|
||||
type="text"
|
||||
placeholder="{{ __('timelines.search_client') }}"
|
||||
icon="magnifying-glass"
|
||||
autocomplete="off"
|
||||
/>
|
||||
|
||||
@if(strlen($search) >= 2)
|
||||
<div class="absolute z-10 mt-1 w-full rounded-lg border border-zinc-200 bg-white shadow-lg dark:border-zinc-700 dark:bg-zinc-800">
|
||||
@forelse($this->clients as $client)
|
||||
<button
|
||||
type="button"
|
||||
wire:key="client-{{ $client->id }}"
|
||||
wire:click="selectUser({{ $client->id }})"
|
||||
class="flex w-full items-center gap-3 px-4 py-3 text-left hover:bg-zinc-50 dark:hover:bg-zinc-700 first:rounded-t-lg last:rounded-b-lg"
|
||||
>
|
||||
<flux:avatar size="sm" name="{{ $client->full_name }}" />
|
||||
<div>
|
||||
<div class="font-medium text-zinc-900 dark:text-zinc-100">{{ $client->full_name }}</div>
|
||||
<div class="text-sm text-zinc-500 dark:text-zinc-400">{{ $client->email }}</div>
|
||||
</div>
|
||||
</button>
|
||||
@empty
|
||||
<div class="px-4 py-3 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{{ __('timelines.no_clients_found') }}
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
@elseif(strlen($search) > 0 && strlen($search) < 2)
|
||||
<div class="absolute z-10 mt-1 w-full rounded-lg border border-zinc-200 bg-white p-3 shadow-lg dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<div class="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{{ __('timelines.type_to_search') }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<flux:error name="selectedUserId" />
|
||||
</flux:field>
|
||||
|
||||
<div class="grid gap-6 sm:grid-cols-2">
|
||||
{{-- Case Name --}}
|
||||
<flux:field>
|
||||
<flux:label>{{ __('timelines.case_name') }} *</flux:label>
|
||||
<flux:input
|
||||
wire:model="caseName"
|
||||
type="text"
|
||||
required
|
||||
placeholder="{{ __('timelines.case_name_placeholder') }}"
|
||||
/>
|
||||
<flux:error name="caseName" />
|
||||
</flux:field>
|
||||
|
||||
{{-- Case Reference --}}
|
||||
<flux:field>
|
||||
<flux:label>{{ __('timelines.case_reference') }} <span class="text-zinc-400">({{ __('common.optional') }})</span></flux:label>
|
||||
<flux:input
|
||||
wire:model="caseReference"
|
||||
type="text"
|
||||
placeholder="{{ __('timelines.case_reference_placeholder') }}"
|
||||
/>
|
||||
<flux:error name="caseReference" />
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
{{-- Initial Notes --}}
|
||||
<flux:field>
|
||||
<flux:label>{{ __('timelines.initial_notes') }} <span class="text-zinc-400">({{ __('common.optional') }})</span></flux:label>
|
||||
<flux:textarea
|
||||
wire:model="initialNotes"
|
||||
rows="4"
|
||||
placeholder="{{ __('timelines.initial_notes_placeholder') }}"
|
||||
/>
|
||||
<flux:error name="initialNotes" />
|
||||
</flux:field>
|
||||
|
||||
<div class="flex items-center justify-end gap-4 border-t border-zinc-200 pt-6 dark:border-zinc-700">
|
||||
<flux:button variant="ghost" :href="route('admin.dashboard')" wire:navigate>
|
||||
{{ __('timelines.cancel') }}
|
||||
</flux:button>
|
||||
<flux:button variant="primary" type="submit">
|
||||
{{ __('timelines.create') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user