Compare commits

..

24 Commits

Author SHA1 Message Date
Matt Young f85d4f20bb Create invoice working 2026-01-28 09:10:32 -06:00
Matt Young 0dc3160678 Enhancements to invoice list. 2026-01-28 08:21:19 -06:00
Matt Young 7ec1a3529e Invoice factory and listing 2026-01-28 07:37:02 -06:00
Matt Young 125f82e382 Invoice factory and listing 2026-01-28 07:18:26 -06:00
Matt Young b3e44efd13 Invoice Listing 2026-01-28 06:57:06 -06:00
Matt Young 11d9ba502d Connect contacts to clients 2026-01-28 06:34:39 -06:00
Matt Young d5de439bb6 Update client listing to show contacts. menu buttons to add and remove contacts (not yet functional) 2026-01-28 05:49:59 -06:00
Matt Young 7f3c7aeca1 add formatMoney helper 2026-01-28 05:16:08 -06:00
Matt Young 26d2d87104 add formatMoney helper 2026-01-28 05:14:04 -06:00
Matt Young 17de29ec91 Product management is working. 2026-01-28 05:01:16 -06:00
Matt Young a1c7ee43f7 Contact management is working. 2026-01-28 04:51:24 -06:00
Matt Young 304238ec98 Edit client links working. 2026-01-28 04:46:42 -06:00
Matt Young 04729c071e Edit client links working. 2026-01-28 04:38:35 -06:00
Matt Young 53b6c1d326 Add client form working. 2026-01-28 04:33:27 -06:00
Matt Young 2d4ef3d6e1 Menu for each client dropdown 2026-01-28 04:13:08 -06:00
Matt Young 63e32ded8a Colored badges for status 2026-01-28 03:47:49 -06:00
Matt Young f066abd0d7 Functioning client list 2026-01-28 03:42:54 -06:00
Matt Young 428e5ba3a0 Functioning client list 2026-01-28 03:42:14 -06:00
Matt Young bfb943af62 Comopser upgrades 2026-01-28 03:24:17 -06:00
Matt Young 9b80414ff1 Working client list. 2026-01-28 02:54:26 -06:00
Matt Young a44f3e77df Add the ability to use ->local on dates to use local timezone 2026-01-28 02:50:12 -06:00
Matt Young b1b46190fc Update routes 2026-01-28 01:44:18 -06:00
Matt Young 4fd2d11137 Phone number cast 2026-01-28 01:19:47 -06:00
Matt Young 6bcd8dcb5f Update factory 2026-01-28 01:06:54 -06:00
37 changed files with 4588 additions and 451 deletions

View File

@ -0,0 +1,33 @@
<?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,4 +6,12 @@ enum ClientStatus: string
{
case ACTIVE = 'active';
case INACTIVE = 'inactive';
public function color(): string
{
return match ($this) {
self::ACTIVE => 'green',
self::INACTIVE => 'zinc',
};
}
}

View File

@ -8,4 +8,24 @@ enum InvoiceStatus: string
case POSTED = 'posted';
case VOID = 'void';
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

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

View File

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

View File

@ -2,7 +2,9 @@
namespace App\Models;
use App\Casts\PhoneNumberCast;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@ -13,6 +15,10 @@ class Contact extends Model
public $fillable = ['first_name', 'last_name', 'email', 'phone'];
public $casts = [
'phone' => PhoneNumberCast::class,
];
public function clients(): BelongsToMany
{
return $this->belongsToMany(Client::class);
@ -22,4 +28,11 @@ class Contact extends Model
{
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,12 +4,32 @@ namespace App\Models;
use App\Casts\MoneyCast;
use App\Enums\InvoiceStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
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 = [
'invoice_number',
'client_id',

View File

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

8
app/helpers.php Normal file
View File

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

View File

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

3359
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -0,0 +1,29 @@
<?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->string('name');
$table->string('abbreviation')->nullable();
$table->date('audition_date');
$table->date('audition_date')->nullable();
$table->string('status')->default('active');
$table->timestamps();
});

View File

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

View File

@ -1,3 +1,12 @@
<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>

View File

@ -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>

View File

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

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

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

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

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

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

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

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

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

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

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

@ -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>

View File

@ -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>

View File

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

@ -0,0 +1,8 @@
<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,5 +1,6 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<!--suppress HtmlRequiredTitleElement -->
<head>
@include('partials.head')
</head>
@ -16,10 +17,22 @@
{{ __('Dashboard') }}
</flux:sidebar.item>
<flux:sidebar.item icon="user" :href="route('clients.index')" :current="request()->routeIs('clients.*')" wire:navigate>
<flux:sidebar.item icon="musical-note" :href="route('clients')" :current="request()->routeIs('clients.*')" wire:navigate>
{{ __('Clients') }}
</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.nav>

View File

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