diff --git a/app/Filament/Admin/Resources/CustomerReportResource.php b/app/Filament/Admin/Resources/CustomerReportResource.php index a86a331..c155484 100644 --- a/app/Filament/Admin/Resources/CustomerReportResource.php +++ b/app/Filament/Admin/Resources/CustomerReportResource.php @@ -124,7 +124,7 @@ public static function table(Table $table): Table public static function canAccess(): bool { - return auth()->user()->is_admin; + return auth()->user()->is_admin ?? false; } public static function getRelations(): array diff --git a/app/Filament/Admin/Resources/OrderResource.php b/app/Filament/Admin/Resources/OrderResource.php index aa9055b..854342c 100644 --- a/app/Filament/Admin/Resources/OrderResource.php +++ b/app/Filament/Admin/Resources/OrderResource.php @@ -6,7 +6,6 @@ use App\Enums\OrderAttributes; use App\Enums\OrderStatus; use App\Enums\OrderType; -use App\Models\Contact; use App\Models\Customer; use App\Models\Order; use App\Models\OrderProduct; @@ -62,18 +61,8 @@ public static function form(Form $form): Form ->required() ->label('Customer') ->options(Customer::all()->pluck('company_name', 'id')) -// ->reactive() ->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') ->required() ->label('Customer PO'), diff --git a/app/Filament/Admin/Resources/PaymentResource.php b/app/Filament/Admin/Resources/PaymentResource.php index 777cda3..9730538 100644 --- a/app/Filament/Admin/Resources/PaymentResource.php +++ b/app/Filament/Admin/Resources/PaymentResource.php @@ -11,7 +11,6 @@ use Filament\Forms\Components\TextInput; use Filament\Forms\Form; use Filament\Resources\Resource; -use Filament\Tables; use Filament\Tables\Actions\ViewAction; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; @@ -72,16 +71,8 @@ public static function table(Table $table): Table ->label('Balance') ->money(), ]) - ->filters([ - // - ]) ->actions([ ViewAction::make(), - ]) - ->bulkActions([ - // Tables\Actions\BulkActionGroup::make([ - // Tables\Actions\DeleteBulkAction::make(), - // ]), ]); } diff --git a/app/Filament/Admin/Resources/QuoteResource.php b/app/Filament/Admin/Resources/QuoteResource.php index 9764112..8081705 100644 --- a/app/Filament/Admin/Resources/QuoteResource.php +++ b/app/Filament/Admin/Resources/QuoteResource.php @@ -4,16 +4,17 @@ use App\Enums\IconEnum; use App\Models\Customer; -use App\Models\Order; use App\Models\Quote; -use Filament\Forms\Components\RichEditor; +use Filament\Forms\Components\DatePicker; use Filament\Forms\Components\Section; 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\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; +use Icetalker\FilamentTableRepeater\Forms\Components\TableRepeater; class QuoteResource extends Resource { @@ -30,28 +31,87 @@ public static function form(Form $form): Form return $form ->schema([ Section::make([ - Split::make([ - Select::make('customer_id') - ->required() - ->label('Customer') - ->options(Customer::all()->pluck('company_name', 'id')) - ->reactive() - ->searchable(), + Select::make('customer_id') + ->required() + ->label('Customer') + ->options(Customer::all()->pluck('company_name', 'id')) + ->reactive() + ->searchable() + ->columnSpan(1), - Select::make('order_id') - ->label('Order') - ->options(fn ($get): array => Order::where('customer_id', $get('customer_id') ?? null) - ->get() - ->pluck('customer_po', 'id') - ->toArray()) - ->searchable(), + DatePicker::make('date') + ->default(today()) + ->required(), - ])->columnSpan(2), - RichEditor::make('body') + TextArea::make('notes') ->columnSpan(2), - // ->rows(8), - ]), - ])->columns(3); + ]) + ->columns(2), + + 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 diff --git a/app/Models/EmbroideryEntry.php b/app/Models/EmbroideryEntry.php new file mode 100644 index 0000000..52ea268 --- /dev/null +++ b/app/Models/EmbroideryEntry.php @@ -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', + 'stitches', + 'digitizing_cost', + 'run_charge', + ]; + + public function quote(): BelongsTo + { + return $this->belongsTo(Quote::class); + } +} diff --git a/app/Models/HeatTransferEntry.php b/app/Models/HeatTransferEntry.php new file mode 100644 index 0000000..1087cdd --- /dev/null +++ b/app/Models/HeatTransferEntry.php @@ -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); + } +} diff --git a/app/Models/Quote.php b/app/Models/Quote.php index e86a67f..0c4074a 100644 --- a/app/Models/Quote.php +++ b/app/Models/Quote.php @@ -5,18 +5,35 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; class Quote extends Model { use HasFactory; protected $fillable = [ - 'body', - 'order_id', + 'customer_id', + 'date', + 'notes', ]; public function order(): BelongsTo { 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); + } } diff --git a/app/Models/ScreenPrintEntry.php b/app/Models/ScreenPrintEntry.php new file mode 100644 index 0000000..c732a56 --- /dev/null +++ b/app/Models/ScreenPrintEntry.php @@ -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); + } +} diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 195ee36..0392e51 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -8,11 +8,10 @@ class PaymentService { public function distributePayments() { - $payments = Payment::where('unapplied_amount', '>', 0)->get(); foreach ($payments as $payment) { - $payment->applyToInvoices(); // Apply remaining amounts to the new invoice + $payment->applyToInvoices(); } } } diff --git a/database/migrations/015_create_quotes_table.php b/database/migrations/015_create_quotes_table.php index ea4193a..d4d59fa 100644 --- a/database/migrations/015_create_quotes_table.php +++ b/database/migrations/015_create_quotes_table.php @@ -14,9 +14,9 @@ public function up(): void Schema::create('quotes', function (Blueprint $table) { $table->id(); - $table->foreignId('order_id')->nullable()->constrained(); - - $table->longText('body')->nullable(); + $table->foreignId('customer_id')->constrained(); + $table->date('date'); + $table->longText('notes')->nullable(); $table->timestamps(); }); diff --git a/database/migrations/020_create_embroidery_entries_table.php b/database/migrations/020_create_embroidery_entries_table.php new file mode 100644 index 0000000..1e013b3 --- /dev/null +++ b/database/migrations/020_create_embroidery_entries_table.php @@ -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('stitches')->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'); + } +}; diff --git a/database/migrations/021_create_screen_print_entries_table.php b/database/migrations/021_create_screen_print_entries_table.php new file mode 100644 index 0000000..4de50a0 --- /dev/null +++ b/database/migrations/021_create_screen_print_entries_table.php @@ -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'); + } +}; diff --git a/database/migrations/022_create_heat_transfer_entries_table.php b/database/migrations/022_create_heat_transfer_entries_table.php new file mode 100644 index 0000000..d8dd7d6 --- /dev/null +++ b/database/migrations/022_create_heat_transfer_entries_table.php @@ -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'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 8bd5966..3c3f748 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -26,7 +26,7 @@ public function run(): void ServiceTypeSeeder::class, ProductServiceSeeder::class, ServiceFileSeeder::class, - QuoteSeeder::class, + // QuoteSeeder::class, InvoiceSeeder::class, InvoiceReportSeeder::class, ]); diff --git a/public/invoice-inv400038.pdf b/public/invoice-inv400038.pdf new file mode 100644 index 0000000..e3db113 Binary files /dev/null and b/public/invoice-inv400038.pdf differ diff --git a/resources/css/app.css b/resources/css/app.css index 4443db6..4318477 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1,3 +1,3 @@ /*@tailwind base;*/ /*@tailwind components;*/ -/*@tailwind utilities;*/ +/*@tailwind utilities;*/ \ No newline at end of file diff --git a/resources/views/vendor/filament-table-repeater/table-repeater.blade.php b/resources/views/vendor/filament-table-repeater/table-repeater.blade.php index 5e1f198..2f90438 100644 --- a/resources/views/vendor/filament-table-repeater/table-repeater.blade.php +++ b/resources/views/vendor/filament-table-repeater/table-repeater.blade.php @@ -74,14 +74,14 @@ @endif </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"> <thead> <tr> @foreach($columnLabels as $columnLabel) @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']])) style="{{ $colStyles[$columnLabel['component']] }}" @endif diff --git a/tests/Unit/CustomerReportTest.php b/tests/Unit/CustomerReportTest.php index 570766b..2590667 100644 --- a/tests/Unit/CustomerReportTest.php +++ b/tests/Unit/CustomerReportTest.php @@ -1,6 +1,6 @@ <?php -use App\Filament\Admin\Resources\QuoteResource\Pages\ListQuotes; +use App\Filament\Admin\Resources\CustomerReportResource\Pages\ListCustomerReports; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -8,12 +8,13 @@ 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()); - livewire(ListQuotes::class)->assertSuccessful(); + + livewire(ListCustomerReports::class)->assertSuccessful(); }); it('cannot render the list page if user isn\'t an admin', function () { $this->actingAs(User::factory()->create()); - livewire(ListQuotes::class)->assertForbidden(); + livewire(ListCustomerReports::class)->assertForbidden(); }); diff --git a/tests/Unit/CustomerTest.php b/tests/Unit/CustomerTest.php index c5d4375..914987b 100644 --- a/tests/Unit/CustomerTest.php +++ b/tests/Unit/CustomerTest.php @@ -1,5 +1,6 @@ <?php +use App\Filament\Admin\Resources\CustomerResource\Pages\EditCustomer; use App\Filament\Admin\Resources\CustomerResource\Pages\ListCustomers; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -8,7 +9,11 @@ uses(RefreshDatabase::class); -it('can render the list page', function () { +it('can render the Customer pages', function () { $this->actingAs(User::factory()->create()); livewire(ListCustomers::class)->assertSuccessful(); + // livewire(ListCustomers::class) + // ->call('create') + // ->assertSuccessful(); + // livewire(EditCustomer::class)->assertSuccessful(); }); diff --git a/tests/Unit/QuoteTest.php b/tests/Unit/QuoteTest.php index 570766b..27b0b28 100644 --- a/tests/Unit/QuoteTest.php +++ b/tests/Unit/QuoteTest.php @@ -1,6 +1,8 @@ <?php +use App\Filament\Admin\Resources\QuoteResource\Pages\CreateQuote; use App\Filament\Admin\Resources\QuoteResource\Pages\ListQuotes; +use App\Models\Customer; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -17,3 +19,28 @@ $this->actingAs(User::factory()->create()); 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 () {}); + +it('can add a screen printing entry to the quote using the form', function () {}); + +it('can add a heat transfer entry to the quote using the form', function () {});