WIP Work on new Quotes

This commit is contained in:
Nisse Lommerde 2025-02-01 09:57:57 -08:00
parent 9ae273fda0
commit 81afa8207f
20 changed files with 440 additions and 60 deletions

View File

@ -124,7 +124,7 @@ public static function table(Table $table): Table
public static function canAccess(): bool public static function canAccess(): bool
{ {
return auth()->user()->is_admin; return auth()->user()->is_admin ?? false;
} }
public static function getRelations(): array public static function getRelations(): array

View File

@ -6,7 +6,6 @@
use App\Enums\OrderAttributes; use App\Enums\OrderAttributes;
use App\Enums\OrderStatus; use App\Enums\OrderStatus;
use App\Enums\OrderType; use App\Enums\OrderType;
use App\Models\Contact;
use App\Models\Customer; use App\Models\Customer;
use App\Models\Order; use App\Models\Order;
use App\Models\OrderProduct; use App\Models\OrderProduct;
@ -62,18 +61,8 @@ public static function form(Form $form): Form
->required() ->required()
->label('Customer') ->label('Customer')
->options(Customer::all()->pluck('company_name', 'id')) ->options(Customer::all()->pluck('company_name', 'id'))
// ->reactive()
->searchable(), ->searchable(),
// Select::make('contact_id')
// ->label('Contact')
// ->options(fn ($get): array => Contact::where('customer_id', $get('customer_id') ?? null)
// ->get()
// ->pluck('full_name', 'id')
// ->toArray())
// ->searchable(),
// ]),
TextInput::make('customer_po') TextInput::make('customer_po')
->required() ->required()
->label('Customer PO'), ->label('Customer PO'),

View File

@ -11,7 +11,6 @@
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Actions\ViewAction; use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
@ -72,16 +71,8 @@ public static function table(Table $table): Table
->label('Balance') ->label('Balance')
->money(), ->money(),
]) ])
->filters([
//
])
->actions([ ->actions([
ViewAction::make(), ViewAction::make(),
])
->bulkActions([
// Tables\Actions\BulkActionGroup::make([
// Tables\Actions\DeleteBulkAction::make(),
// ]),
]); ]);
} }

View File

@ -4,16 +4,17 @@
use App\Enums\IconEnum; use App\Enums\IconEnum;
use App\Models\Customer; use App\Models\Customer;
use App\Models\Order;
use App\Models\Quote; use App\Models\Quote;
use Filament\Forms\Components\RichEditor; use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Section; use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\Split; use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Tables;
use Filament\Tables\Table; use Filament\Tables\Table;
use Icetalker\FilamentTableRepeater\Forms\Components\TableRepeater;
class QuoteResource extends Resource class QuoteResource extends Resource
{ {
@ -30,28 +31,87 @@ public static function form(Form $form): Form
return $form return $form
->schema([ ->schema([
Section::make([ Section::make([
Split::make([
Select::make('customer_id') Select::make('customer_id')
->required() ->required()
->label('Customer') ->label('Customer')
->options(Customer::all()->pluck('company_name', 'id')) ->options(Customer::all()->pluck('company_name', 'id'))
->reactive() ->reactive()
->searchable(), ->searchable()
->columnSpan(1),
Select::make('order_id') DatePicker::make('date')
->label('Order') ->default(today())
->options(fn ($get): array => Order::where('customer_id', $get('customer_id') ?? null) ->required(),
->get()
->pluck('customer_po', 'id')
->toArray())
->searchable(),
])->columnSpan(2), TextArea::make('notes')
RichEditor::make('body')
->columnSpan(2), ->columnSpan(2),
// ->rows(8), ])
]), ->columns(2),
])->columns(3);
TableRepeater::make('embroideryEntries')
->relationship('embroideryEntries')
->schema([
TextInput::make('logo')
->label('Logo name'),
TextInput::make('placement'),
TextInput::make('quantity')
->prefix('#'),
TextInput::make('width')
->suffix('"'),
TextInput::make('height')
->suffix('"'),
TextInput::make('stitch_count'),
TextInput::make('digitizing_cost')
->prefix('$'),
TextInput::make('run_charge')
->prefix('$'),
])
->addActionLabel('Add Embroidery Entry')
->defaultItems(0),
TableRepeater::make('screenPrintEntries')
->relationship('screenPrintEntries')
->schema([
TextInput::make('logo')
->label('Logo name'),
TextInput::make('quantity')
->prefix('#'),
TextInput::make('width')
->suffix('"'),
TextInput::make('height')
->suffix('"'),
TextInput::make('color_amount'),
TextInput::make('color_match')
->prefix('$'),
TextInput::make('flash')
->prefix('$'),
TextInput::make('fleece')
->prefix('$'),
TextInput::make('poly_ink')
->prefix('$'),
TextInput::make('other_charges')
->prefix('$'),
])
->addActionLabel('Add Screen Print Entry')
->defaultItems(0),
TableRepeater::make('heatTransferEntries')
->relationship('heatTransferEntries')
->schema([
TextInput::make('logo')
->label('Logo name'),
TextInput::make('quantity')
->prefix('#'),
TextInput::make('width')
->suffix('"'),
TextInput::make('height')
->suffix('"'),
TextInput::make('price')
->prefix('$'),
])
->addActionLabel('Add Heat Transfer Entry')
->defaultItems(0),
])->columns(1);
} }
public static function table(Table $table): Table public static function table(Table $table): Table

View File

@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class EmbroideryEntry extends Model
{
protected $fillable = [
'quote_id',
'quantity',
'logo',
'width',
'height',
'placement',
'stitch_count',
'digitizing_cost',
'run_charge',
];
public function quote(): BelongsTo
{
return $this->belongsTo(Quote::class);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class HeatTransferEntry extends Model
{
protected $fillable = [
'quote_id',
'quantity',
'logo',
'width',
'height',
'price',
];
public function quote(): BelongsTo
{
return $this->belongsTo(Quote::class);
}
}

View File

@ -5,18 +5,35 @@
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Quote extends Model class Quote extends Model
{ {
use HasFactory; use HasFactory;
protected $fillable = [ protected $fillable = [
'body', 'customer_id',
'order_id', 'date',
'notes',
]; ];
public function order(): BelongsTo public function order(): BelongsTo
{ {
return $this->belongsTo(Order::class); return $this->belongsTo(Order::class);
} }
public function embroideryEntries(): HasMany
{
return $this->hasMany(EmbroideryEntry::class);
}
public function screenPrintEntries(): HasMany
{
return $this->hasMany(ScreenPrintEntry::class);
}
public function heatTransferEntries(): HasMany
{
return $this->hasMany(HeatTransferEntry::class);
}
} }

View File

@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ScreenPrintEntry extends Model
{
protected $fillable = [
'quote_id',
'quantity',
'logo',
'width',
'height',
'color_amount',
'setup_amount',
'run_charge',
'color_change',
'color_match',
'flash',
'fleece',
'poly_ink',
'other_charges',
'notes',
];
public function quote(): BelongsTo
{
return $this->belongsTo(Quote::class);
}
}

View File

@ -8,11 +8,10 @@ class PaymentService
{ {
public function distributePayments() public function distributePayments()
{ {
$payments = Payment::where('unapplied_amount', '>', 0)->get(); $payments = Payment::where('unapplied_amount', '>', 0)->get();
foreach ($payments as $payment) { foreach ($payments as $payment) {
$payment->applyToInvoices(); // Apply remaining amounts to the new invoice $payment->applyToInvoices();
} }
} }
} }

View File

@ -14,9 +14,9 @@ public function up(): void
Schema::create('quotes', function (Blueprint $table) { Schema::create('quotes', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('order_id')->nullable()->constrained(); $table->foreignId('customer_id')->constrained();
$table->date('date');
$table->longText('body')->nullable(); $table->longText('notes')->nullable();
$table->timestamps(); $table->timestamps();
}); });

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('embroidery_entries', function (Blueprint $table) {
$table->id();
$table->foreignId('quote_id')->constrained();
$table->integer('quantity')->nullable();
$table->string('logo')->nullable();
$table->decimal('width', 6, 2)->nullable();
$table->decimal('height', 6, 2)->nullable();
$table->string('placement')->nullable();
$table->string('stitch_count')->nullable();
$table->string('digitizing_cost')->nullable();
$table->string('run_charge')->nullable();
$table->text('notes')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('embroidery_entries');
}
};

View File

@ -0,0 +1,45 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('screen_print_entries', function (Blueprint $table) {
$table->id();
$table->foreignId('quote_id')->constrained();
$table->integer('quantity')->nullable();
$table->string('logo')->nullable();
$table->decimal('width', 6, 2)->nullable();
$table->decimal('height', 6, 2)->nullable();
$table->integer('color_amount')->nullable();
$table->integer('setup_amount')->nullable();
$table->decimal('run_charge', 8, 2)->nullable();
$table->decimal('color_change', 8, 2)->default(false);
$table->decimal('color_match', 8, 2)->default(false);
$table->decimal('flash', 8, 2)->default(false);
$table->decimal('fleece', 8, 2)->default(false);
$table->decimal('poly_ink', 8, 2)->default(false);
$table->decimal('other_charges', 8, 2)->default(false);
$table->text('notes')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('screen_print_entries');
}
};

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('heat_transfer_entries', function (Blueprint $table) {
$table->id();
$table->foreignId('quote_id')->constrained();
$table->integer('quantity')->nullable();
$table->string('logo')->nullable();
$table->decimal('width', 6, 2)->nullable();
$table->decimal('height', 6, 2)->nullable();
$table->decimal('price', 8, 2)->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('heat_transfer_entries');
}
};

View File

@ -26,7 +26,7 @@ public function run(): void
ServiceTypeSeeder::class, ServiceTypeSeeder::class,
ProductServiceSeeder::class, ProductServiceSeeder::class,
ServiceFileSeeder::class, ServiceFileSeeder::class,
QuoteSeeder::class, // QuoteSeeder::class,
InvoiceSeeder::class, InvoiceSeeder::class,
InvoiceReportSeeder::class, InvoiceReportSeeder::class,
]); ]);

Binary file not shown.

View File

@ -37,7 +37,7 @@
{{ {{
$attributes $attributes
->merge($getExtraAttributes(), escape: false) ->merge($getExtraAttributes(), escape: false)
->class(['bg-white border border-gray-150 rounded-xl relative dark:bg-gray-900 dark:border-gray-700']) ->class(['bg-white border border-gray-150 rounded-xl relative dark:bg-gray-900 dark:border-gray-800'])
}} }}
> >
@ -74,14 +74,14 @@
@endif @endif
</div> </div>
<div class="px-4{{ $isAddable? '' : ' py-2' }}"> <div class="px-4{{ $isAddable? '' : ' py-2' }} pb-4">
<table class="it-table-repeater w-full text-left rtl:text-right table-auto mx-4" x-show="! isCollapsed"> <table class="it-table-repeater w-full text-left rtl:text-right table-auto mx-4" x-show="! isCollapsed">
<thead> <thead>
<tr> <tr>
@foreach($columnLabels as $columnLabel) @foreach($columnLabels as $columnLabel)
@if($columnLabel['display']) @if($columnLabel['display'])
<th class="it-table-repeater-cell-label p-2" <th class="it-table-repeater-cell-label p-2" style="font-weight: 500; font-size: 0.875rem;"
@if($colStyles && isset($colStyles[$columnLabel['component']])) @if($colStyles && isset($colStyles[$columnLabel['component']]))
style="{{ $colStyles[$columnLabel['component']] }}" style="{{ $colStyles[$columnLabel['component']] }}"
@endif @endif

View File

@ -1,6 +1,6 @@
<?php <?php
use App\Filament\Admin\Resources\QuoteResource\Pages\ListQuotes; use App\Filament\Admin\Resources\CustomerReportResource\Pages\ListCustomerReports;
use App\Models\User; use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
@ -8,12 +8,13 @@
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
it('can render the list page', function () { it('can render Customer Report pages', function () {
$this->actingAs(User::factory(['is_admin' => true])->create()); $this->actingAs(User::factory(['is_admin' => true])->create());
livewire(ListQuotes::class)->assertSuccessful();
livewire(ListCustomerReports::class)->assertSuccessful();
}); });
it('cannot render the list page if user isn\'t an admin', function () { it('cannot render the list page if user isn\'t an admin', function () {
$this->actingAs(User::factory()->create()); $this->actingAs(User::factory()->create());
livewire(ListQuotes::class)->assertForbidden(); livewire(ListCustomerReports::class)->assertForbidden();
}); });

View File

@ -1,5 +1,6 @@
<?php <?php
use App\Filament\Admin\Resources\CustomerResource\Pages\EditCustomer;
use App\Filament\Admin\Resources\CustomerResource\Pages\ListCustomers; use App\Filament\Admin\Resources\CustomerResource\Pages\ListCustomers;
use App\Models\User; use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
@ -8,7 +9,11 @@
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
it('can render the list page', function () { it('can render the Customer pages', function () {
$this->actingAs(User::factory()->create()); $this->actingAs(User::factory()->create());
livewire(ListCustomers::class)->assertSuccessful(); livewire(ListCustomers::class)->assertSuccessful();
// livewire(ListCustomers::class)
// ->call('create')
// ->assertSuccessful();
// livewire(EditCustomer::class)->assertSuccessful();
}); });

View File

@ -1,6 +1,8 @@
<?php <?php
use App\Filament\Admin\Resources\QuoteResource\Pages\CreateQuote;
use App\Filament\Admin\Resources\QuoteResource\Pages\ListQuotes; use App\Filament\Admin\Resources\QuoteResource\Pages\ListQuotes;
use App\Models\Customer;
use App\Models\User; use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
@ -17,3 +19,117 @@
$this->actingAs(User::factory()->create()); $this->actingAs(User::factory()->create());
livewire(ListQuotes::class)->assertForbidden(); livewire(ListQuotes::class)->assertForbidden();
}); });
it('can create a quote using the form', function () {
$this->actingAs(User::factory(['is_admin' => true])->create());
$customer = Customer::factory()->create();
$formData = [
'customer_id' => $customer->id,
'date' => today(),
'notes' => 'Some note',
];
$this->livewire(CreateQuote::class)
->fillForm($formData)
->call('create')
->assertHasNoErrors();
$this->assertDatabaseHas('quotes', $formData);
});
it('can add an embroidery entry to the quote using the form', function () {
$this->actingAs(User::factory(['is_admin' => true])->create());
$customer = Customer::factory()->create();
$formData = [
'customer_id' => $customer->id,
'date' => today(),
'notes' => 'Some note',
'embroideryEntries' => [
[
'logo' => 'logo name',
'placement' => 'Right sleeve',
'quantity' => 5,
'width' => 1.5,
'height' => 2.5,
'stitch_count' => '3k - 4k',
'digitizing_cost' => 10.5,
'run_charge' => 12,
],
],
];
$this->livewire(CreateQuote::class)
->fillForm($formData)
->call('create')
->assertHasNoErrors();
$this->assertDatabaseHas('embroidery_entries', $formData['embroideryEntries'][0]);
});
it('can add a screen printing entry to the quote using the form', function () {
$this->actingAs(User::factory(['is_admin' => true])->create());
$customer = Customer::factory()->create();
$formData = [
'customer_id' => $customer->id,
'date' => today(),
'notes' => 'Some note',
'screenPrintEntries' => [
[
'logo' => 'logo name',
'quantity' => 5,
'width' => 1.5,
'height' => 2.5,
'color_amount' => 2,
'color_match' => 5.10,
'flash' => 5.20,
'fleece' => 5.30,
'poly_ink' => 5.40,
'other_charges' => 5.50,
],
],
];
$this->livewire(CreateQuote::class)
->fillForm($formData)
->call('create')
->assertHasNoErrors();
$this->assertDatabaseHas('screen_print_entries', $formData['screenPrintEntries'][0]);
});
it('can add a heat transfer entry to the quote using the form', function () {
$this->actingAs(User::factory(['is_admin' => true])->create());
$customer = Customer::factory()->create();
$formData = [
'customer_id' => $customer->id,
'date' => today(),
'notes' => 'Some note',
'heatTransferEntries' => [
[
'logo' => 'logo name',
'quantity' => 5,
'width' => 1.5,
'height' => 2.5,
'price' => 2,
],
],
];
$this->livewire(CreateQuote::class)
->fillForm($formData)
->call('create')
->assertHasNoErrors();
$this->assertDatabaseHas('heat_transfer_entries', $formData['heatTransferEntries'][0]);
});