Compare commits
No commits in common. "f85d4f20bb6af7a92fe932821a95c513fa5b56bf" and "a2be833d4619291a59d6763f0afca7113f3c266c" have entirely different histories.
f85d4f20bb
...
a2be833d46
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,12 +6,4 @@ enum ClientStatus: string
|
|||
{
|
||||
case ACTIVE = 'active';
|
||||
case INACTIVE = 'inactive';
|
||||
|
||||
public function color(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::ACTIVE => 'green',
|
||||
self::INACTIVE => 'zinc',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,24 +8,4 @@ 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',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
class ClientController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return view('clients.index');
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@
|
|||
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;
|
||||
|
|
@ -31,9 +30,11 @@ class Client extends Model
|
|||
->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
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@
|
|||
|
||||
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;
|
||||
|
|
@ -15,10 +13,6 @@ 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);
|
||||
|
|
@ -28,11 +22,4 @@ 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'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,32 +4,12 @@ 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',
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
namespace App\Providers;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Date;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
|
@ -25,10 +24,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
<?php
|
||||
|
||||
if (! function_exists('formatMoney')) {
|
||||
function formatMoney(int|float $dollars): string
|
||||
{
|
||||
return '$'.number_format($dollars, 2);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,28 +9,24 @@
|
|||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"require-dev": {
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"app/helpers.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
|
|
@ -71,7 +67,8 @@
|
|||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"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": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
|
|
@ -101,4 +98,4 @@
|
|||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -66,8 +66,6 @@ return [
|
|||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
'display_timezone' => env('APP_DISPLAY_TIMEZONE', 'UTC'),
|
||||
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class ClientFactory extends Factory
|
|||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->company(),
|
||||
'name' => $this->faker->name(),
|
||||
'abbreviation' => $this->faker->word(),
|
||||
'audition_date' => $this->faker->dateTimeBetween('+5 days', '+1 year'),
|
||||
'status' => ClientStatus::ACTIVE,
|
||||
|
|
@ -27,10 +27,7 @@ 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(),
|
||||
['is_primary' => true]
|
||||
);
|
||||
$client->contacts()->attach($contact ?? Contact::factory()->create());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,10 +23,7 @@ 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(),
|
||||
['is_primary' => true]
|
||||
);
|
||||
$contact->clients()->attach($client ?? Client::factory()->create());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ return new class extends Migration
|
|||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('abbreviation')->nullable();
|
||||
$table->date('audition_date')->nullable();
|
||||
$table->date('audition_date');
|
||||
$table->string('status')->default('active');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ return new class extends Migration
|
|||
{
|
||||
Schema::create('client_contact', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignIdFor(Client::class)->constrained()->cascadeOnDelete();
|
||||
$table->foreignIdFor(Contact::class)->constrained()->cascadeOnDelete();
|
||||
$table->foreignIdFor(Client::class)->constrained();
|
||||
$table->foreignIdFor(Contact::class)->constrained();
|
||||
$table->boolean('is_primary')->default(false);
|
||||
$table->timestamps();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,3 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
<!--suppress HtmlRequiredTitleElement -->
|
||||
<head>
|
||||
@include('partials.head')
|
||||
</head>
|
||||
|
|
@ -17,22 +16,10 @@
|
|||
{{ __('Dashboard') }}
|
||||
</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') }}
|
||||
</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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -1,26 +1,17 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\ClientController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', function () {
|
||||
return view('welcome');
|
||||
})->name('home');
|
||||
|
||||
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('dashboard', 'dashboard')
|
||||
->middleware(['auth', 'verified'])
|
||||
->name('dashboard');
|
||||
|
||||
// Route::view('dashboard', 'dashboard')
|
||||
// ->middleware(['auth', 'verified'])
|
||||
// ->name('dashboard');
|
||||
|
||||
// Route::view('clients', 'clients.index')
|
||||
// ->middleware(['auth', 'verified'])
|
||||
// ->name('clients.index');
|
||||
Route::view('clients', 'clients.index')
|
||||
->middleware(['auth', 'verified'])
|
||||
->name('clients.index');
|
||||
|
||||
require __DIR__.'/settings.php';
|
||||
|
|
|
|||
Loading…
Reference in New Issue