Compare commits

..

No commits in common. "f85d4f20bb6af7a92fe932821a95c513fa5b56bf" and "a2be833d4619291a59d6763f0afca7113f3c266c" have entirely different histories.

37 changed files with 450 additions and 4587 deletions

View File

@ -1,33 +0,0 @@
<?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class PhoneNumberCast implements CastsAttributes
{
public function get(Model $model, string $key, $value, array $attributes): ?string
{
if ($value === null) {
return null;
}
return match (strlen($value)) {
7 => substr($value, 0, 3).'-'.substr($value, 3),
10 => '('.substr($value, 0, 3).') '.substr($value, 3, 3).'-'.substr($value, 6),
default => strlen($value) > 10
? '+'.substr($value, 0, -10).' ('.substr($value, -10, 3).') '.substr($value, -7, 3).'-'.substr($value,
-4)
: $value,
};
}
public function set(Model $model, string $key, $value, array $attributes)
{
if ($value === null) {
return null;
}
return preg_replace('/\D/', '', $value);
}
}

View File

@ -6,12 +6,4 @@ enum ClientStatus: string
{ {
case ACTIVE = 'active'; case ACTIVE = 'active';
case INACTIVE = 'inactive'; case INACTIVE = 'inactive';
public function color(): string
{
return match ($this) {
self::ACTIVE => 'green',
self::INACTIVE => 'zinc',
};
}
} }

View File

@ -8,24 +8,4 @@ enum InvoiceStatus: string
case POSTED = 'posted'; case POSTED = 'posted';
case VOID = 'void'; case VOID = 'void';
case PAID = 'paid'; case PAID = 'paid';
public function label(): string
{
return match ($this) {
self::DRAFT => 'Draft',
self::POSTED => 'Posted',
self::VOID => 'Voided',
self::PAID => 'Paid',
};
}
public function color(): string
{
return match ($this) {
self::DRAFT => 'gray',
self::POSTED => 'green',
self::VOID => 'zinc',
self::PAID => 'blue',
};
}
} }

View File

@ -1,11 +0,0 @@
<?php
namespace App\Http\Controllers;
class ClientController extends Controller
{
public function index()
{
return view('clients.index');
}
}

View File

@ -3,7 +3,6 @@
namespace App\Models; namespace App\Models;
use App\Enums\ClientStatus; use App\Enums\ClientStatus;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@ -31,9 +30,11 @@ class Client extends Model
->withPivot('is_primary'); ->withPivot('is_primary');
} }
protected function primaryContact(): Attribute public function primaryContact(): BelongsToMany
{ {
return Attribute::get(fn () => $this->contacts()->wherePivot('is_primary', true)->first()); return $this->belongsToMany(Contact::class)
->wherePivot('is_primary', true)
->withPivot('is_primary');
} }
public function invoices(): HasMany public function invoices(): HasMany

View File

@ -2,9 +2,7 @@
namespace App\Models; namespace App\Models;
use App\Casts\PhoneNumberCast;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@ -15,10 +13,6 @@ class Contact extends Model
public $fillable = ['first_name', 'last_name', 'email', 'phone']; public $fillable = ['first_name', 'last_name', 'email', 'phone'];
public $casts = [
'phone' => PhoneNumberCast::class,
];
public function clients(): BelongsToMany public function clients(): BelongsToMany
{ {
return $this->belongsToMany(Client::class); return $this->belongsToMany(Client::class);
@ -28,11 +22,4 @@ class Contact extends Model
{ {
return Invoice::whereIn('client_id', $this->clients()->pluck('clients.id')); return Invoice::whereIn('client_id', $this->clients()->pluck('clients.id'));
} }
protected function fullName(): Attribute
{
return Attribute::make(
get: fn (mixed $value, array $attributes) => $attributes['first_name'].' '.$attributes['last_name'],
);
}
} }

View File

@ -4,32 +4,12 @@ namespace App\Models;
use App\Casts\MoneyCast; use App\Casts\MoneyCast;
use App\Enums\InvoiceStatus; use App\Enums\InvoiceStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
class Invoice extends Model class Invoice extends Model
{ {
use HasFactory;
public static function booted(): void
{
static::creating(function (Invoice $invoice) {
$invoice->invoice_number ??= static::generateInvoiceNumber();
});
}
public static function generateInvoiceNumber(): string
{
$prefix = date('y').'-';
do {
$number = $prefix.str_pad(random_int(0, 99999), 5, '0', STR_PAD_LEFT);
} while (static::where('invoice_number', $number)->exists());
return $number;
}
protected $fillable = [ protected $fillable = [
'invoice_number', 'invoice_number',
'client_id', 'client_id',

View File

@ -3,7 +3,6 @@
namespace App\Providers; namespace App\Providers;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Date; use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
@ -25,10 +24,6 @@ class AppServiceProvider extends ServiceProvider
public function boot(): void public function boot(): void
{ {
$this->configureDefaults(); $this->configureDefaults();
Carbon::macro('local', function () {
return $this->tz(config('app.display_timezone', 'UTC'));
});
} }
protected function configureDefaults(): void protected function configureDefaults(): void

View File

@ -1,8 +0,0 @@
<?php
if (! function_exists('formatMoney')) {
function formatMoney(int|float $dollars): string
{
return '$'.number_format($dollars, 2);
}
}

View File

@ -9,28 +9,24 @@
], ],
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": "^8.3", "php": "^8.2",
"laravel/fortify": "^1.34", "laravel/fortify": "^1.30",
"laravel/framework": "^12.49", "laravel/framework": "^12.0",
"laravel/tinker": "^2.11.0", "laravel/tinker": "^2.10.1",
"livewire/flux": "^2.11.1", "livewire/flux": "^2.9.0",
"livewire/livewire": "^4.1" "livewire/livewire": "^4.0"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.24.1", "fakerphp/faker": "^1.23",
"laravel/pail": "^1.2.4", "laravel/pail": "^1.2.2",
"laravel/pint": "^1.27", "laravel/pint": "^1.24",
"laravel/sail": "^1.52", "laravel/sail": "^1.41",
"mockery/mockery": "^1.6.12", "mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.8.3", "nunomaduro/collision": "^8.6",
"pestphp/pest": "^4.0", "pestphp/pest": "^3.8",
"pestphp/pest-plugin-browser": "^4.0", "pestphp/pest-plugin-laravel": "^3.2"
"pestphp/pest-plugin-laravel": "^4.0"
}, },
"autoload": { "autoload": {
"files": [
"app/helpers.php"
],
"psr-4": { "psr-4": {
"App\\": "app/", "App\\": "app/",
"Database\\Factories\\": "database/factories/", "Database\\Factories\\": "database/factories/",
@ -71,7 +67,8 @@
"@php artisan package:discover --ansi" "@php artisan package:discover --ansi"
], ],
"post-update-cmd": [ "post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force" "@php artisan vendor:publish --tag=laravel-assets --ansi --force",
"@php artisan boost:update --ansi"
], ],
"post-root-package-install": [ "post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
@ -101,4 +98,4 @@
}, },
"minimum-stability": "stable", "minimum-stability": "stable",
"prefer-stable": true "prefer-stable": true
} }

3357
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -66,8 +66,6 @@ return [
*/ */
'timezone' => 'UTC', 'timezone' => 'UTC',
'display_timezone' => env('APP_DISPLAY_TIMEZONE', 'UTC'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -15,7 +15,7 @@ class ClientFactory extends Factory
public function definition(): array public function definition(): array
{ {
return [ return [
'name' => $this->faker->company(), 'name' => $this->faker->name(),
'abbreviation' => $this->faker->word(), 'abbreviation' => $this->faker->word(),
'audition_date' => $this->faker->dateTimeBetween('+5 days', '+1 year'), 'audition_date' => $this->faker->dateTimeBetween('+5 days', '+1 year'),
'status' => ClientStatus::ACTIVE, 'status' => ClientStatus::ACTIVE,
@ -27,10 +27,7 @@ class ClientFactory extends Factory
public function withContact(?Contact $contact = null): static public function withContact(?Contact $contact = null): static
{ {
return $this->afterCreating(function (Client $client) use ($contact) { return $this->afterCreating(function (Client $client) use ($contact) {
$client->contacts()->attach( $client->contacts()->attach($contact ?? Contact::factory()->create());
$contact ?? Contact::factory()->create(),
['is_primary' => true]
);
}); });
} }
} }

View File

@ -23,10 +23,7 @@ class ContactFactory extends Factory
public function withClient(?Client $client = null): static public function withClient(?Client $client = null): static
{ {
return $this->afterCreating(function (Contact $contact) use ($client) { return $this->afterCreating(function (Contact $contact) use ($client) {
$contact->clients()->attach( $contact->clients()->attach($client ?? Client::factory()->create());
$client ?? Client::factory()->create(),
['is_primary' => true]
);
}); });
} }
} }

View File

@ -1,29 +0,0 @@
<?php
namespace Database\Factories;
use App\Enums\InvoiceStatus;
use App\Models\Client;
use App\Models\Invoice;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
class InvoiceFactory extends Factory
{
protected $model = Invoice::class;
public function definition(): array
{
return [
'status' => InvoiceStatus::DRAFT,
'invoice_date' => Carbon::now(),
'due_date' => Carbon::now()->addDays(30),
'notes' => $this->faker->word(),
'internal_notes' => $this->faker->word(),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
'client_id' => Client::factory()->withContact(),
];
}
}

View File

@ -15,7 +15,7 @@ return new class extends Migration
$table->id(); $table->id();
$table->string('name'); $table->string('name');
$table->string('abbreviation')->nullable(); $table->string('abbreviation')->nullable();
$table->date('audition_date')->nullable(); $table->date('audition_date');
$table->string('status')->default('active'); $table->string('status')->default('active');
$table->timestamps(); $table->timestamps();
}); });

View File

@ -15,8 +15,8 @@ return new class extends Migration
{ {
Schema::create('client_contact', function (Blueprint $table) { Schema::create('client_contact', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignIdFor(Client::class)->constrained()->cascadeOnDelete(); $table->foreignIdFor(Client::class)->constrained();
$table->foreignIdFor(Contact::class)->constrained()->cascadeOnDelete(); $table->foreignIdFor(Contact::class)->constrained();
$table->boolean('is_primary')->default(false); $table->boolean('is_primary')->default(false);
$table->timestamps(); $table->timestamps();

View File

@ -1,12 +1,3 @@
<x-layouts::app :title="__('Clients')"> <x-layouts::app :title="__('Clients')">
<div class="max-w-7xl mx-auto space-y-4">
<div class="flex justify-end">
<livewire:create-client />
</div>
<livewire:client-list />
<livewire:edit-client />
<livewire:add-client-contact />
<livewire:remove-client-contact />
<livewire:set-primary-contact />
</div>
</x-layouts::app> </x-layouts::app>

View File

@ -1,188 +0,0 @@
<?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>

View File

@ -1,152 +0,0 @@
<?php
use App\Enums\ClientStatus;
use App\Models\Client;
use Livewire\Component;
use Livewire\Attributes\Computed;
use Livewire\Attributes\On;
use Livewire\WithPagination;
new class extends Component {
use WithPagination;
public string $sortBy = 'abbreviation';
public string $sortDirection = 'desc';
public function sort($column): void
{
if ($this->sortBy === $column) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortBy = $column;
$this->sortDirection = 'asc';
}
}
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
{
}
#[Computed]
public function clients()
{
return Client::orderBy($this->sortBy, $this->sortDirection)->paginate(10);
}
};
?>
<!--suppress RequiredAttributes -->
<div>
<flux:table :paginate="$this->clients">
<flux:table.columns>
<flux:table.column sortable :sorted="$sortBy === 'name'" :direction="$sortDirection"
wire:click="sort('name')">
Name
</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'abbreviation'" :direction="$sortDirection"
wire:click="sort('abbreviation')">
Abbreviation
</flux:table.column>
<flux:table.column>
Contacts
</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'audition_date'" :direction="$sortDirection"
wire:click="sort('audition_date')">
Audition Date
</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'status'" :direction="$sortDirection"
wire:click="sort('status')">
Status
</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'created_at'" :direction="$sortDirection"
wire:click="sort('created_at')">
Created
</flux:table.column>
<flux:table.column></flux:table.column>
</flux:table.columns>
<flux:table.rows>
@foreach($this->clients as $client)
<flux:table.row :key="$client->id">
<flux:table.cell>{{ $client->name }}</flux:table.cell>
<flux:table.cell>{{ $client->abbreviation ?? '' }}</flux:table.cell>
<flux:table.cell>
@if($client->primary_contact)
<div class="flex items-center gap-1">
{{ $client->primary_contact?->full_name }}
<flux:icon.star variant="micro"/>
</div>
@endif
@foreach($client->secondaryContacts as $contact)
<p>{{ $contact->full_name }}</p>
@endforeach
</flux:table.cell>
<flux:table.cell>{{ $client->audition_date?->local()->format('m/d/Y') ?? '' }}</flux:table.cell>
<flux:table.cell>
<flux:badge :color="$client->status->color()">
{{ $client->status->value }}
</flux:badge>
</flux:table.cell>
<flux:table.cell>{{ $client->created_at->local()->format('m/d/Y | g:i A') }}</flux:table.cell>
<flux:table.cell>
<flux:dropdown position="bottom" align="start">
<flux:button variant="ghost" size="sm" icon="ellipsis-horizontal"
inset="top bottom"></flux:button>
<flux:navmenu>
<flux:menu.group heading="{{ $client->abbreviation }}">
<flux:menu.separator></flux:menu.separator>
<flux:menu.item
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>
@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>
</flux:table.cell>
</flux:table.row>
@endforeach
</flux:table.rows>
</flux:table>
</div>

View File

@ -1,89 +0,0 @@
<?php
use App\Models\Contact;
use Livewire\Component;
use Livewire\Attributes\Computed;
use Livewire\Attributes\On;
use Livewire\WithPagination;
new class extends Component {
use WithPagination;
public string $sortBy = 'last_name';
public string $sortDirection = 'asc';
public function sort($column): void
{
if ($this->sortBy === $column) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortBy = $column;
$this->sortDirection = 'asc';
}
}
#[On('contact-created')]
#[On('contact-updated')]
public function refresh(): void {}
#[Computed]
public function contacts()
{
return Contact::orderBy($this->sortBy, $this->sortDirection)->paginate(10);
}
};
?>
<!--suppress RequiredAttributes -->
<div>
<flux:table :paginate="$this->contacts">
<flux:table.columns>
<flux:table.column sortable :sorted="$sortBy === 'first_name'" :direction="$sortDirection"
wire:click="sort('first_name')">
First Name
</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'last_name'" :direction="$sortDirection"
wire:click="sort('last_name')">
Last Name
</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'email'" :direction="$sortDirection"
wire:click="sort('email')">
Email
</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'phone'" :direction="$sortDirection"
wire:click="sort('phone')">
Phone
</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'created_at'" :direction="$sortDirection"
wire:click="sort('created_at')">
Created
</flux:table.column>
<flux:table.column></flux:table.column>
</flux:table.columns>
<flux:table.rows>
@foreach($this->contacts as $contact)
<flux:table.row :key="$contact->id">
<flux:table.cell>{{ $contact->first_name }}</flux:table.cell>
<flux:table.cell>{{ $contact->last_name }}</flux:table.cell>
<flux:table.cell>{{ $contact->email }}</flux:table.cell>
<flux:table.cell>{{ $contact->phone }}</flux:table.cell>
<flux:table.cell>{{ $contact->created_at->local()->format('m/d/Y | g:i A') }}</flux:table.cell>
<flux:table.cell>
<flux:dropdown position="bottom" align="start">
<flux:button variant="ghost" size="sm" icon="ellipsis-horizontal"
inset="top bottom"></flux:button>
<flux:navmenu>
<flux:menu.group heading="{{ $contact->first_name }} {{ $contact->last_name }}">
<flux:menu.separator></flux:menu.separator>
<flux:menu.item wire:click="$dispatch('edit-contact', { contactId: {{ $contact->id }} })" icon="pencil">Edit</flux:menu.item>
</flux:menu.group>
</flux:navmenu>
</flux:dropdown>
</flux:table.cell>
</flux:table.row>
@endforeach
</flux:table.rows>
</flux:table>
</div>

View File

@ -1,58 +0,0 @@
<?php
use App\Models\Client;
use Livewire\Component;
use Livewire\Attributes\Validate;
use Flux\Flux;
new class extends Component {
#[Validate('required|string|max:255|unique:clients,name')]
public string $name = '';
#[Validate('required|string|max:10|unique:clients,abbreviation')]
public string $abbreviation = '';
#[Validate('nullable|date|after_or_equal:today')]
public ?string $audition_date = null;
public function save(): void
{
$this->validate();
Client::create([
'name' => $this->name,
'abbreviation' => $this->abbreviation,
'audition_date' => $this->audition_date ?: null,
]);
$this->reset();
Flux::modal('create-client')->close();
$this->dispatch('client-created');
}
};
?>
<div>
<flux:modal.trigger name="create-client">
<flux:button icon="plus" variant="primary">
New Client
</flux:button>
</flux:modal.trigger>
<flux:modal name="create-client" class="md:w-96">
<form wire:submit="save" class="space-y-6">
<flux:heading size="lg">Create Client</flux:heading>
<flux:input label="Name" wire:model="name" />
<flux:input label="Abbreviation" wire:model="abbreviation" maxlength="10" />
<flux:input label="Audition Date" wire:model="audition_date" type="date" />
<div class="flex gap-2">
<flux:spacer />
{{-- <flux:button variant="ghost" wire:click="$flux.modal('create-client').close()">Cancel</flux:button>--}}
<flux:button type="submit" variant="primary">Create</flux:button>
</div>
</form>
</flux:modal>
</div>

View File

@ -1,61 +0,0 @@
<?php
use App\Models\Contact;
use Livewire\Component;
use Livewire\Attributes\Validate;
use Flux\Flux;
new class extends Component {
#[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 function save(): void
{
$this->validate();
Contact::create([
'first_name' => $this->first_name,
'last_name' => $this->last_name,
'email' => $this->email,
'phone' => $this->phone ?: null,
]);
$this->reset();
Flux::modal('create-contact')->close();
$this->dispatch('contact-created');
}
};
?>
<div>
<flux:modal.trigger name="create-contact">
<flux:button icon="plus" variant="primary">
New Contact
</flux:button>
</flux:modal.trigger>
<flux:modal name="create-contact" class="md:w-96">
<form wire:submit="save" class="space-y-6">
<flux:heading size="lg">Create Contact</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" />
<div class="flex gap-2">
<flux:spacer />
<flux:button type="submit" variant="primary">Create</flux:button>
</div>
</form>
</flux:modal>
</div>

View File

@ -1,76 +0,0 @@
<?php
use App\Enums\InvoiceStatus;
use App\Models\Client;
use App\Models\Invoice;
use Livewire\Component;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Validate;
use Flux\Flux;
new class extends Component {
#[Validate('required|integer|exists:clients,id')]
public int $client_id;
public InvoiceStatus $status = InvoiceStatus::DRAFT;
#[Validate('nullable|string')]
public ?string $notes = null;
#[Validate('nullable|string')]
public ?string $internal_notes = null;
public function save(): void
{
$this->validate();
Invoice::create([
'client_id' => $this->client_id,
'status' => $this->status,
'notes' => $this->notes,
'internal_notes' => $this->notes,
]);
$this->reset();
Flux::modal('create-invoice')->close();
$this->dispatch('invoice-created');
}
#[Computed]
public function clients()
{
return Client::where('status', 'active')->orderBy('abbreviation')->get();
}
};
?>
<div>
<flux:modal.trigger name="create-invoice">
<flux:button icon="plus" variant="primary">
Create Invoice
</flux:button>
</flux:modal.trigger>
<flux:modal name="create-invoice" class="md:w-96">
<form wire:submit="save" class="space-y-6">
<flux:heading size="lg">Create Invoice</flux:heading>
<flux:select wire:model="client_id" label="Client" placeholder="Choose client...">
@foreach($this->clients as $client)
<flux:select.option :value="$client->id">{{ $client->name }}</flux:select.option>
@endforeach
</flux:select>
<flux:textarea wire:model="notes" label="Notes" placeholder="Add notes to this invoice..."></flux:textarea>
<flux:textarea wire:model="internal_notes" label="Internal Notes" placeholder="Add internal notes to this invoice..."></flux:textarea>
<div class="flex gap-2">
<flux:spacer />
<flux:button type="submit" variant="primary">Create</flux:button>
</div>
</form>
</flux:modal>
</div>

View File

@ -1,66 +0,0 @@
<?php
use App\Models\Product;
use Livewire\Component;
use Livewire\Attributes\Validate;
use Flux\Flux;
new class extends Component {
#[Validate('required|string|max:50|unique:products,sku')]
public string $sku = '';
#[Validate('required|string|max:255')]
public string $name = '';
#[Validate('nullable|string|max:1000')]
public string $description = '';
#[Validate('required|numeric|min:0')]
public string $price = '';
#[Validate('boolean')]
public bool $active = true;
public function save(): void
{
$this->validate();
Product::create([
'sku' => $this->sku,
'name' => $this->name,
'description' => $this->description ?: null,
'price' => $this->price,
'active' => $this->active,
]);
$this->reset();
Flux::modal('create-product')->close();
$this->dispatch('product-created');
}
};
?>
<div>
<flux:modal.trigger name="create-product">
<flux:button icon="plus" variant="primary">
New Product
</flux:button>
</flux:modal.trigger>
<flux:modal name="create-product" class="md:w-96">
<form wire:submit="save" class="space-y-6">
<flux:heading size="lg">Create Product</flux:heading>
<flux:input label="SKU" wire:model="sku" />
<flux:input label="Name" wire:model="name" />
<flux:textarea label="Description" wire:model="description" rows="3" />
<flux:input label="Price" wire:model="price" type="number" step="0.01" min="0" />
<flux:switch label="Active" wire:model="active" />
<div class="flex gap-2">
<flux:spacer />
<flux:button type="submit" variant="primary">Create</flux:button>
</div>
</form>
</flux:modal>
</div>

View File

@ -1,72 +0,0 @@
<?php
use App\Models\Client;
use Livewire\Component;
use Livewire\Attributes\Validate;
use Livewire\Attributes\On;
use Flux\Flux;
new class extends Component {
public ?int $clientId = null;
#[Validate('required|string|max:255')]
public string $name = '';
#[Validate('required|string|max:10')]
public string $abbreviation = '';
#[Validate('nullable|date|after_or_equal:today')]
public ?string $audition_date = null;
#[On('edit-client')]
public function edit(int $clientId): void
{
$this->clientId = $clientId;
$client = Client::findOrFail($clientId);
$this->name = $client->name;
$this->abbreviation = $client->abbreviation;
$this->audition_date = $client->audition_date?->format('Y-m-d');
$this->resetValidation();
Flux::modal('edit-client')->show();
}
public function save(): void
{
$this->validate([
'name' => 'required|string|max:255|unique:clients,name,' . $this->clientId,
'abbreviation' => 'required|string|max:10|unique:clients,abbreviation,' . $this->clientId,
'audition_date' => 'nullable|date|after_or_equal:today',
]);
$client = Client::findOrFail($this->clientId);
$client->update([
'name' => $this->name,
'abbreviation' => $this->abbreviation,
'audition_date' => $this->audition_date ?: null,
]);
$this->reset();
Flux::modal('edit-client')->close();
$this->dispatch('client-updated');
}
};
?>
<div>
<flux:modal name="edit-client" class="md:w-96">
<form wire:submit="save" class="space-y-6">
<flux:heading size="lg">Edit Client</flux:heading>
<flux:input label="Name" wire:model="name" />
<flux:input label="Abbreviation" wire:model="abbreviation" maxlength="10" />
<flux:input label="Audition Date" wire:model="audition_date" type="date" />
<div class="flex gap-2">
<flux:spacer />
<flux:button type="submit" variant="primary">Save</flux:button>
</div>
</form>
</flux:modal>
</div>

View File

@ -1,79 +0,0 @@
<?php
use App\Models\Contact;
use Livewire\Component;
use Livewire\Attributes\Validate;
use Livewire\Attributes\On;
use Flux\Flux;
new class extends Component {
public ?int $contactId = null;
#[Validate('required|string|max:255')]
public string $first_name = '';
#[Validate('required|string|max:255')]
public string $last_name = '';
#[Validate('required|email|max:255')]
public string $email = '';
#[Validate('nullable|string|max:20')]
public string $phone = '';
#[On('edit-contact')]
public function edit(int $contactId): void
{
$this->contactId = $contactId;
$contact = Contact::findOrFail($contactId);
$this->first_name = $contact->first_name;
$this->last_name = $contact->last_name;
$this->email = $contact->email;
$this->phone = $contact->phone ?? '';
$this->resetValidation();
Flux::modal('edit-contact')->show();
}
public function save(): void
{
$this->validate([
'first_name' => 'required|string|max:255',
'last_name' => 'required|string|max:255',
'email' => 'required|email|max:255|unique:contacts,email,' . $this->contactId,
'phone' => 'nullable|string|max:20',
]);
$contact = Contact::findOrFail($this->contactId);
$contact->update([
'first_name' => $this->first_name,
'last_name' => $this->last_name,
'email' => $this->email,
'phone' => $this->phone ?: null,
]);
$this->reset();
Flux::modal('edit-contact')->close();
$this->dispatch('contact-updated');
}
};
?>
<div>
<flux:modal name="edit-contact" class="md:w-96">
<form wire:submit="save" class="space-y-6">
<flux:heading size="lg">Edit Contact</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" />
<div class="flex gap-2">
<flux:spacer />
<flux:button type="submit" variant="primary">Save</flux:button>
</div>
</form>
</flux:modal>
</div>

View File

@ -1,86 +0,0 @@
<?php
use App\Models\Product;
use Livewire\Component;
use Livewire\Attributes\Validate;
use Livewire\Attributes\On;
use Flux\Flux;
new class extends Component {
public ?int $productId = null;
#[Validate('required|string|max:50')]
public string $sku = '';
#[Validate('required|string|max:255')]
public string $name = '';
#[Validate('nullable|string|max:1000')]
public string $description = '';
#[Validate('required|numeric|min:0')]
public string $price = '';
#[Validate('boolean')]
public bool $active = true;
#[On('edit-product')]
public function edit(int $productId): void
{
$this->productId = $productId;
$product = Product::findOrFail($productId);
$this->sku = $product->sku;
$this->name = $product->name;
$this->description = $product->description ?? '';
$this->price = (string) $product->getRawOriginal('price') / 100;
$this->active = $product->active;
$this->resetValidation();
Flux::modal('edit-product')->show();
}
public function save(): void
{
$this->validate([
'sku' => 'required|string|max:50|unique:products,sku,' . $this->productId,
'name' => 'required|string|max:255',
'description' => 'nullable|string|max:1000',
'price' => 'required|numeric|min:0',
'active' => 'boolean',
]);
$product = Product::findOrFail($this->productId);
$product->update([
'sku' => $this->sku,
'name' => $this->name,
'description' => $this->description ?: null,
'price' => $this->price,
'active' => $this->active,
]);
$this->reset();
Flux::modal('edit-product')->close();
$this->dispatch('product-updated');
}
};
?>
<div>
<flux:modal name="edit-product" class="md:w-96">
<form wire:submit="save" class="space-y-6">
<flux:heading size="lg">Edit Product</flux:heading>
<flux:input label="SKU" wire:model="sku" />
<flux:input label="Name" wire:model="name" />
<flux:textarea label="Description" wire:model="description" rows="3" />
<flux:input label="Price" wire:model="price" type="number" step="0.01" min="0" />
<flux:switch label="Active" wire:model="active" />
<div class="flex gap-2">
<flux:spacer />
<flux:button type="submit" variant="primary">Save</flux:button>
</div>
</form>
</flux:modal>
</div>

View File

@ -1,111 +0,0 @@
<?php
use App\Enums\InvoiceStatus;
use App\Models\Invoice;
use Livewire\Component;
use Livewire\WithPagination;
use Livewire\Attributes\Computed;
use Livewire\Attributes\On;
new class extends Component {
use WithPagination;
public string $sortBy = 'created_at';
public string $sortDirection = 'desc';
public function sort($column): void
{
if ($this->sortBy === $column) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortBy = $column;
$this->sortDirection = 'asc';
}
}
#[On('invoice-created')]
public function refresh(): void {}
#[Computed]
public function invoices()
{
return Invoice::orderBy($this->sortBy, $this->sortDirection)->paginate(10);
}
};
?>
<!--suppress RequiredAttributes -->
<div>
<flux:table :pagination="$this->invoices">
<flux:table.columns>
<flux:table.column sortable :sorted="$sortBy === 'invoice_number'" :direction="$sortDirection"
wire:click="sort('invoice_number')">
Invoice Number
</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'client_id'" :direction="$sortDirection"
wire:click="sort('client_id')">
Client
</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'status'" :direction="$sortDirection"
wire:click="sort('status')">
Status
</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'invoice_date'" :direction="$sortDirection"
wire:click="sort('invoice_date')">
Invoice Date
</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'sent_at'" :direction="$sortDirection"
wire:click="sort('sent_at')">
Sent
</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'due_date'" :direction="$sortDirection"
wire:click="sort('due_date')">
Due Date
</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'total'" :direction="$sortDirection"
wire:click="sort('total')">
Total
</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@foreach($this->invoices as $invoice)
<flux:table.row :key="$invoice->id">
<flux:table.cell>{{ $invoice->invoice_number }}</flux:table.cell>
<flux:table.cell>{{ $invoice->client->abbreviation }}</flux:table.cell>
<flux:table.cell>
<flux:badge :color="$invoice->status->color()" rounded size="sm">
{{ $invoice->status->label() }}
</flux:badge>
</flux:table.cell>
<flux:table.cell>{{ $invoice->invoice_date?->format('m/d/Y') }}</flux:table.cell>
<flux:table.cell>
@if($invoice->sent_at)
<flux:badge color="green" rounded
size="sm">{{ $invoice->sent_at->format('m/d/Y') }}</flux:badge>
@elseif($invoice->status === InvoiceStatus::POSTED)
<flux:badge color="red" rounded size="sm">Not Sent</flux:badge>
@endif
</flux:table.cell>
<flux:table.cell>
@if($invoice->due_date)
<flux:badge size="sm" rounded
:color="$invoice->due_date?->isPast() && $invoice->status === InvoiceStatus::POSTED ? 'red' : 'blue'">
{{ $invoice->due_date?->format('m/d/Y') }}
</flux:badge>
@endif
</flux:table.cell>
<flux:table.cell>{{ formatMoney($invoice->total) }}</flux:table.cell>
</flux:table.row>
@endforeach
</flux:table.rows>
</flux:table>
</div>

View File

@ -1,93 +0,0 @@
<?php
use App\Models\Product;
use Livewire\Component;
use Livewire\Attributes\Computed;
use Livewire\Attributes\On;
use Livewire\WithPagination;
new class extends Component {
use WithPagination;
public string $sortBy = 'name';
public string $sortDirection = 'asc';
public function sort($column): void
{
if ($this->sortBy === $column) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortBy = $column;
$this->sortDirection = 'asc';
}
}
#[On('product-created')]
#[On('product-updated')]
public function refresh(): void {}
#[Computed]
public function products()
{
return Product::orderBy($this->sortBy, $this->sortDirection)->paginate(10);
}
};
?>
<!--suppress RequiredAttributes -->
<div>
<flux:table :paginate="$this->products">
<flux:table.columns>
<flux:table.column sortable :sorted="$sortBy === 'sku'" :direction="$sortDirection"
wire:click="sort('sku')">
SKU
</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'name'" :direction="$sortDirection"
wire:click="sort('name')">
Name
</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'description'" :direction="$sortDirection"
wire:click="sort('description')">
Description
</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'price'" :direction="$sortDirection"
wire:click="sort('price')">
Price
</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'active'" :direction="$sortDirection"
wire:click="sort('active')">
Status
</flux:table.column>
<flux:table.column></flux:table.column>
</flux:table.columns>
<flux:table.rows>
@foreach($this->products as $product)
<flux:table.row :key="$product->id">
<flux:table.cell>{{ $product->sku }}</flux:table.cell>
<flux:table.cell>{{ $product->name }}</flux:table.cell>
<flux:table.cell class="max-w-xs truncate">{{ $product->description }}</flux:table.cell>
<flux:table.cell>{{ formatMoney($product->price) }}</flux:table.cell>
<flux:table.cell>
<flux:badge :color="$product->active ? 'green' : 'zinc'">
{{ $product->active ? 'Active' : 'Inactive' }}
</flux:badge>
</flux:table.cell>
<flux:table.cell>
<flux:dropdown position="bottom" align="start">
<flux:button variant="ghost" size="sm" icon="ellipsis-horizontal"
inset="top bottom"></flux:button>
<flux:navmenu>
<flux:menu.group heading="{{ $product->sku }}">
<flux:menu.separator></flux:menu.separator>
<flux:menu.item wire:click="$dispatch('edit-product', { productId: {{ $product->id }} })" icon="pencil">Edit</flux:menu.item>
</flux:menu.group>
</flux:navmenu>
</flux:dropdown>
</flux:table.cell>
</flux:table.row>
@endforeach
</flux:table.rows>
</flux:table>
</div>

View File

@ -1,181 +0,0 @@
<?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>

View File

@ -1,84 +0,0 @@
<?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>

View File

@ -1,9 +0,0 @@
<x-layouts::app :title="__('Contacts')">
<div class="max-w-7xl mx-auto space-y-4">
<div class="flex justify-end">
<livewire:create-contact />
</div>
<livewire:contact-list />
<livewire:edit-contact />
</div>
</x-layouts::app>

View File

@ -1,8 +0,0 @@
<x-layouts::app :title="__('Contacts')">
<div class="max-w-7xl mx-auto space-y-4">
<div class="flex justify-end">
<livewire:create-invoice />
</div>
<livewire:invoice-list />
</div>
</x-layouts::app>

View File

@ -1,6 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark"> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<!--suppress HtmlRequiredTitleElement -->
<head> <head>
@include('partials.head') @include('partials.head')
</head> </head>
@ -17,22 +16,10 @@
{{ __('Dashboard') }} {{ __('Dashboard') }}
</flux:sidebar.item> </flux:sidebar.item>
<flux:sidebar.item icon="musical-note" :href="route('clients')" :current="request()->routeIs('clients.*')" wire:navigate> <flux:sidebar.item icon="user" :href="route('clients.index')" :current="request()->routeIs('clients.*')" wire:navigate>
{{ __('Clients') }} {{ __('Clients') }}
</flux:sidebar.item> </flux:sidebar.item>
<flux:sidebar.item icon="user" :href="route('contacts')" :current="request()->routeIs('contacts.*')" wire:navigate>
{{ __('Contacts') }}
</flux:sidebar.item>
<flux:sidebar.item icon="archive-box" :href="route('products')" :current="request()->routeIs('products.*')" wire:navigate>
{{ __('Products') }}
</flux:sidebar.item>
<flux:sidebar.item icon="document-currency-dollar" :href="route('invoices')" :current="request()->routeIs('invoices.*')" wire:navigate>
{{ __('Invoices') }}
</flux:sidebar.item>
</flux:sidebar.group> </flux:sidebar.group>
</flux:sidebar.nav> </flux:sidebar.nav>

View File

@ -1,9 +0,0 @@
<x-layouts::app :title="__('Products')">
<div class="max-w-7xl mx-auto space-y-4">
<div class="flex justify-end">
<livewire:create-product />
</div>
<livewire:product-list />
<livewire:edit-product />
</div>
</x-layouts::app>

View File

@ -1,26 +1,17 @@
<?php <?php
use App\Http\Controllers\ClientController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::get('/', function () { Route::get('/', function () {
return view('welcome'); return view('welcome');
})->name('home'); })->name('home');
Route::middleware(['auth', 'verified'])->group(function () { Route::view('dashboard', 'dashboard')
Route::view('dashboard', 'dashboard')->name('dashboard'); ->middleware(['auth', 'verified'])
Route::view('clients', 'clients.index')->name('clients'); ->name('dashboard');
Route::view('contacts', 'contacts.index')->name('contacts');
Route::view('products', 'products.index')->name('products');
Route::view('invoices', 'invoices.index')->name('invoices');
});
// Route::view('dashboard', 'dashboard') Route::view('clients', 'clients.index')
// ->middleware(['auth', 'verified']) ->middleware(['auth', 'verified'])
// ->name('dashboard'); ->name('clients.index');
// Route::view('clients', 'clients.index')
// ->middleware(['auth', 'verified'])
// ->name('clients.index');
require __DIR__.'/settings.php'; require __DIR__.'/settings.php';