Connect contacts to clients
This commit is contained in:
parent
d5de439bb6
commit
11d9ba502d
|
|
@ -5,5 +5,8 @@
|
|||
</div>
|
||||
<livewire:client-list />
|
||||
<livewire:edit-client />
|
||||
<livewire:add-client-contact />
|
||||
<livewire:remove-client-contact />
|
||||
<livewire:set-primary-contact />
|
||||
</div>
|
||||
</x-layouts::app>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,188 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Models\Contact;
|
||||
use Livewire\Component;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\On;
|
||||
use Flux\Flux;
|
||||
|
||||
new class extends Component {
|
||||
public ?int $clientId = null;
|
||||
public ?Client $client = null;
|
||||
|
||||
public ?int $contactId = null;
|
||||
public bool $isPrimary = false;
|
||||
|
||||
// For creating new contact
|
||||
#[Validate('required|string|max:255')]
|
||||
public string $first_name = '';
|
||||
|
||||
#[Validate('required|string|max:255')]
|
||||
public string $last_name = '';
|
||||
|
||||
#[Validate('required|email|max:255|unique:contacts,email')]
|
||||
public string $email = '';
|
||||
|
||||
#[Validate('nullable|string|max:20')]
|
||||
public string $phone = '';
|
||||
|
||||
public bool $newContactIsPrimary = false;
|
||||
|
||||
#[On('add-client-contact')]
|
||||
public function open(int $clientId): void
|
||||
{
|
||||
$this->clientId = $clientId;
|
||||
$this->client = Client::findOrFail($clientId);
|
||||
$this->contactId = null;
|
||||
$this->isPrimary = !$this->client->contacts()->exists();
|
||||
|
||||
$this->resetValidation();
|
||||
Flux::modal('add-contact')->show();
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function availableContacts()
|
||||
{
|
||||
if (!$this->client) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$existingContactIds = $this->client->contacts()->pluck('contacts.id');
|
||||
|
||||
return Contact::whereNotIn('id', $existingContactIds)
|
||||
->orderBy('last_name')
|
||||
->orderBy('first_name')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function openCreateModal(): void
|
||||
{
|
||||
$this->first_name = '';
|
||||
$this->last_name = '';
|
||||
$this->email = '';
|
||||
$this->phone = '';
|
||||
$this->newContactIsPrimary = !$this->client->contacts()->exists();
|
||||
|
||||
$this->resetValidation();
|
||||
Flux::modal('add-contact')->close();
|
||||
Flux::modal('create-client-contact')->show();
|
||||
}
|
||||
|
||||
public function backToSelect(): void
|
||||
{
|
||||
Flux::modal('create-client-contact')->close();
|
||||
Flux::modal('add-contact')->show();
|
||||
}
|
||||
|
||||
public function attachContact(): void
|
||||
{
|
||||
if (!$this->contactId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->isPrimary) {
|
||||
$this->client->contacts()->updateExistingPivot(
|
||||
$this->client->contacts()->wherePivot('is_primary', true)->pluck('contacts.id'),
|
||||
['is_primary' => false]
|
||||
);
|
||||
}
|
||||
|
||||
$this->client->contacts()->attach($this->contactId, ['is_primary' => $this->isPrimary]);
|
||||
|
||||
$this->reset(['clientId', 'client', 'contactId', 'isPrimary']);
|
||||
Flux::modal('add-contact')->close();
|
||||
$this->dispatch('client-updated');
|
||||
}
|
||||
|
||||
public function createAndAttach(): void
|
||||
{
|
||||
$this->validate([
|
||||
'first_name' => 'required|string|max:255',
|
||||
'last_name' => 'required|string|max:255',
|
||||
'email' => 'required|email|max:255|unique:contacts,email',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
]);
|
||||
|
||||
$contact = Contact::create([
|
||||
'first_name' => $this->first_name,
|
||||
'last_name' => $this->last_name,
|
||||
'email' => $this->email,
|
||||
'phone' => $this->phone ?: null,
|
||||
]);
|
||||
|
||||
if ($this->newContactIsPrimary) {
|
||||
$this->client->contacts()->updateExistingPivot(
|
||||
$this->client->contacts()->wherePivot('is_primary', true)->pluck('contacts.id'),
|
||||
['is_primary' => false]
|
||||
);
|
||||
}
|
||||
|
||||
$this->client->contacts()->attach($contact->id, ['is_primary' => $this->newContactIsPrimary]);
|
||||
|
||||
$this->reset(['clientId', 'client', 'contactId', 'isPrimary', 'first_name', 'last_name', 'email', 'phone', 'newContactIsPrimary']);
|
||||
Flux::modal('create-client-contact')->close();
|
||||
$this->dispatch('client-updated');
|
||||
$this->dispatch('contact-created');
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function clientHasContacts(): bool
|
||||
{
|
||||
return $this->client?->contacts()->exists() ?? false;
|
||||
}
|
||||
};
|
||||
?>
|
||||
|
||||
<div>
|
||||
<flux:modal name="add-contact" class="md:w-96">
|
||||
<div class="space-y-6">
|
||||
<flux:heading size="lg">Add Contact to {{ $client?->name }}</flux:heading>
|
||||
|
||||
<flux:select label="Select Contact" wire:model.live="contactId" placeholder="Choose a contact...">
|
||||
@foreach($this->availableContacts as $contact)
|
||||
<flux:select.option value="{{ $contact->id }}">
|
||||
{{ $contact->full_name }} ({{ $contact->email }})
|
||||
</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
|
||||
<flux:button variant="subtle" wire:click="openCreateModal" icon="plus" class="w-full">
|
||||
Create New Contact
|
||||
</flux:button>
|
||||
|
||||
@if($this->clientHasContacts)
|
||||
<flux:checkbox wire:model="isPrimary" label="Set as primary contact" />
|
||||
@endif
|
||||
|
||||
<div class="flex gap-2">
|
||||
<flux:spacer />
|
||||
<flux:button type="button" variant="primary" wire:click="attachContact" :disabled="!$contactId">
|
||||
Add Contact
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
|
||||
<flux:modal name="create-client-contact" class="md:w-96">
|
||||
<form wire:submit="createAndAttach" class="space-y-6">
|
||||
<flux:heading size="lg">Create Contact for {{ $client?->name }}</flux:heading>
|
||||
|
||||
<flux:input label="First Name" wire:model="first_name" />
|
||||
<flux:input label="Last Name" wire:model="last_name" />
|
||||
<flux:input label="Email" wire:model="email" type="email" />
|
||||
<flux:input label="Phone" wire:model="phone" type="tel" />
|
||||
|
||||
@if($this->clientHasContacts)
|
||||
<flux:checkbox wire:model="newContactIsPrimary" label="Set as primary contact" />
|
||||
@endif
|
||||
|
||||
<div class="flex gap-2">
|
||||
<flux:button type="button" variant="ghost" wire:click="backToSelect">Back</flux:button>
|
||||
<flux:spacer />
|
||||
<flux:button type="submit" variant="primary">Create & Add</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
</flux:modal>
|
||||
</div>
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\ClientStatus;
|
||||
use App\Models\Client;
|
||||
use Livewire\Component;
|
||||
use Livewire\Attributes\Computed;
|
||||
|
|
@ -23,6 +24,14 @@ new class extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
public function changeStatus(Client $client): void
|
||||
{
|
||||
$client->status = $client->status === ClientStatus::ACTIVE
|
||||
? ClientStatus::INACTIVE
|
||||
: ClientStatus::ACTIVE;
|
||||
$client->save();
|
||||
}
|
||||
|
||||
#[On('client-created')]
|
||||
#[On('client-updated')]
|
||||
public function refresh(): void
|
||||
|
|
@ -75,8 +84,8 @@ new class extends Component {
|
|||
<flux:table.cell>
|
||||
@if($client->primary_contact)
|
||||
<div class="flex items-center gap-1">
|
||||
<flux:icon.star variant="micro"/>
|
||||
{{ $client->primary_contact?->full_name }}
|
||||
<flux:icon.star variant="micro"/>
|
||||
</div>
|
||||
@endif
|
||||
@foreach($client->secondaryContacts as $contact)
|
||||
|
|
@ -102,14 +111,35 @@ new class extends Component {
|
|||
wire:click="$dispatch('edit-client', { clientId: {{ $client->id }} })"
|
||||
icon="pencil">Edit Client
|
||||
</flux:menu.item>
|
||||
@if($client->status === ClientStatus::ACTIVE)
|
||||
<flux:menu.item
|
||||
wire:click="changeStatus({{ $client }})"
|
||||
icon="minus-circle">Make Inactive
|
||||
</flux:menu.item>
|
||||
@else
|
||||
<flux:menu.item
|
||||
wire:click="changeStatus({{ $client }})"
|
||||
icon="plus-circle">Make Active
|
||||
</flux:menu.item>
|
||||
@endif
|
||||
</flux:menu.group>
|
||||
<flux:menu.group heading="Contacts">
|
||||
<flux:menu.item
|
||||
wire:click="$dispatch('add-client-contact', { clientId: {{ $client->id }} })"
|
||||
icon="user-plus">Add Contact
|
||||
</flux:menu.item>
|
||||
<flux:menu.item
|
||||
icon="user-minus">Remove Contact
|
||||
</flux:menu.item>
|
||||
@if($client->contacts()->count() > 0)
|
||||
<flux:menu.item
|
||||
wire:click="$dispatch('remove-client-contact', { clientId: {{ $client->id }} })"
|
||||
icon="user-minus">Remove Contact
|
||||
</flux:menu.item>
|
||||
@endif
|
||||
@if($client->contacts()->count() > 1)
|
||||
<flux:menu.item
|
||||
wire:click="$dispatch('set-primary-contact', { clientId: {{ $client->id }} })"
|
||||
icon="user">Set Primary Contact
|
||||
</flux:menu.item>
|
||||
@endif
|
||||
</flux:menu.group>
|
||||
</flux:navmenu>
|
||||
</flux:dropdown>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,181 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Models\Contact;
|
||||
use Livewire\Component;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\On;
|
||||
use Flux\Flux;
|
||||
|
||||
new class extends Component {
|
||||
public ?int $clientId = null;
|
||||
public ?Client $client = null;
|
||||
|
||||
public ?int $contactId = null;
|
||||
public ?int $newPrimaryId = null;
|
||||
|
||||
#[On('remove-client-contact')]
|
||||
public function open(int $clientId): void
|
||||
{
|
||||
$this->clientId = $clientId;
|
||||
$this->client = Client::findOrFail($clientId);
|
||||
$this->contactId = null;
|
||||
$this->newPrimaryId = null;
|
||||
|
||||
$this->resetValidation();
|
||||
Flux::modal('remove-contact')->show();
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function clientContacts()
|
||||
{
|
||||
if (!$this->client) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return $this->client->contacts()
|
||||
->orderBy('last_name')
|
||||
->orderBy('first_name')
|
||||
->get();
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function selectedContact(): ?Contact
|
||||
{
|
||||
if (!$this->contactId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Contact::find($this->contactId);
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function isRemovingPrimary(): bool
|
||||
{
|
||||
if (!$this->contactId || !$this->client) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->client->contacts()
|
||||
->wherePivot('is_primary', true)
|
||||
->where('contacts.id', $this->contactId)
|
||||
->exists();
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function otherContacts()
|
||||
{
|
||||
if (!$this->client || !$this->contactId) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return $this->client->contacts()
|
||||
->where('contacts.id', '!=', $this->contactId)
|
||||
->orderBy('last_name')
|
||||
->orderBy('first_name')
|
||||
->get();
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function needsNewPrimarySelection(): bool
|
||||
{
|
||||
return $this->isRemovingPrimary && $this->otherContacts->count() > 1;
|
||||
}
|
||||
|
||||
public function removeContact(): void
|
||||
{
|
||||
if (!$this->contactId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$otherContacts = $this->otherContacts;
|
||||
|
||||
// Detach the selected contact
|
||||
$this->client->contacts()->detach($this->contactId);
|
||||
|
||||
// Handle primary contact assignment
|
||||
if ($otherContacts->count() === 1) {
|
||||
// Only one remaining - make them primary
|
||||
$this->client->contacts()->updateExistingPivot(
|
||||
$otherContacts->first()->id,
|
||||
['is_primary' => true]
|
||||
);
|
||||
} elseif ($otherContacts->count() > 1 && $this->isRemovingPrimary) {
|
||||
// Multiple remaining and removing primary - use selected new primary
|
||||
if ($this->newPrimaryId) {
|
||||
// Clear any existing primary
|
||||
$this->client->contacts()->wherePivot('is_primary', true)
|
||||
->each(fn ($contact) => $this->client->contacts()->updateExistingPivot(
|
||||
$contact->id,
|
||||
['is_primary' => false]
|
||||
));
|
||||
|
||||
// Set new primary
|
||||
$this->client->contacts()->updateExistingPivot(
|
||||
$this->newPrimaryId,
|
||||
['is_primary' => true]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->reset(['clientId', 'client', 'contactId', 'newPrimaryId']);
|
||||
Flux::modal('remove-contact')->close();
|
||||
$this->dispatch('client-updated');
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function canSubmit(): bool
|
||||
{
|
||||
if (!$this->contactId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->needsNewPrimarySelection && !$this->newPrimaryId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
?>
|
||||
|
||||
<div>
|
||||
<flux:modal name="remove-contact" class="md:w-96">
|
||||
<div class="space-y-6">
|
||||
<flux:heading size="lg">Remove Contact from {{ $client?->name }}</flux:heading>
|
||||
|
||||
@if($this->clientContacts->isEmpty())
|
||||
<p class="text-zinc-500">This client has no contacts.</p>
|
||||
@else
|
||||
<flux:select label="Select Contact to Remove" wire:model.live="contactId" placeholder="Choose a contact...">
|
||||
@foreach($this->clientContacts as $contact)
|
||||
<flux:select.option value="{{ $contact->id }}">
|
||||
{{ $contact->full_name }}
|
||||
@if($contact->pivot->is_primary) (Primary) @endif
|
||||
</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
|
||||
@if($this->needsNewPrimarySelection)
|
||||
<flux:radio.group wire:model.live="newPrimaryId" label="Select New Primary Contact">
|
||||
@foreach($this->otherContacts as $contact)
|
||||
<flux:radio value="{{ $contact->id }}" label="{{ $contact->full_name }}" />
|
||||
@endforeach
|
||||
</flux:radio.group>
|
||||
@endif
|
||||
|
||||
<div class="flex gap-2">
|
||||
<flux:spacer />
|
||||
<flux:button
|
||||
type="button"
|
||||
variant="danger"
|
||||
wire:click="removeContact"
|
||||
:disabled="!$this->canSubmit"
|
||||
>
|
||||
Remove Contact
|
||||
</flux:button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</flux:modal>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Client;
|
||||
use Livewire\Component;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\On;
|
||||
use Flux\Flux;
|
||||
|
||||
new class extends Component {
|
||||
public ?int $clientId = null;
|
||||
public ?Client $client = null;
|
||||
public ?int $primaryId = null;
|
||||
|
||||
#[On('set-primary-contact')]
|
||||
public function open(int $clientId): void
|
||||
{
|
||||
$this->clientId = $clientId;
|
||||
$this->client = Client::findOrFail($clientId);
|
||||
$this->primaryId = $this->client->contacts()
|
||||
->wherePivot('is_primary', true)
|
||||
->first()?->id;
|
||||
|
||||
Flux::modal('set-primary-contact')->show();
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function clientContacts()
|
||||
{
|
||||
if (!$this->client) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return $this->client->contacts()
|
||||
->orderBy('last_name')
|
||||
->orderBy('first_name')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
if (!$this->primaryId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing primary
|
||||
$this->client->contacts()->wherePivot('is_primary', true)
|
||||
->each(fn ($contact) => $this->client->contacts()->updateExistingPivot(
|
||||
$contact->id,
|
||||
['is_primary' => false]
|
||||
));
|
||||
|
||||
// Set new primary
|
||||
$this->client->contacts()->updateExistingPivot(
|
||||
$this->primaryId,
|
||||
['is_primary' => true]
|
||||
);
|
||||
|
||||
$this->reset(['clientId', 'client', 'primaryId']);
|
||||
Flux::modal('set-primary-contact')->close();
|
||||
$this->dispatch('client-updated');
|
||||
}
|
||||
};
|
||||
?>
|
||||
|
||||
<div>
|
||||
<flux:modal name="set-primary-contact" class="md:w-96">
|
||||
<div class="space-y-6">
|
||||
<flux:heading size="lg">Set Primary Contact for {{ $client?->name }}</flux:heading>
|
||||
|
||||
<flux:radio.group wire:model="primaryId" label="Select Primary Contact">
|
||||
@foreach($this->clientContacts as $contact)
|
||||
<flux:radio value="{{ $contact->id }}" label="{{ $contact->full_name }}" />
|
||||
@endforeach
|
||||
</flux:radio.group>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<flux:spacer />
|
||||
<flux:button type="button" variant="primary" wire:click="save">
|
||||
Save
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
</div>
|
||||
Loading…
Reference in New Issue