diff --git a/app/Filament/Admin/Pages/UsernameLogin.php b/app/Filament/Admin/Pages/UsernameLogin.php index 975ffbd..d8b7723 100644 --- a/app/Filament/Admin/Pages/UsernameLogin.php +++ b/app/Filament/Admin/Pages/UsernameLogin.php @@ -2,8 +2,12 @@ namespace App\Filament\Admin\Pages; +use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException; +use Filament\Facades\Filament; use Filament\Forms\Components\Component; use Filament\Forms\Components\TextInput; +use Filament\Http\Responses\Auth\Contracts\LoginResponse; +use Filament\Models\Contracts\FilamentUser; use Filament\Pages\Auth\Login; use Illuminate\Contracts\Support\Htmlable; use Illuminate\Validation\ValidationException; @@ -28,6 +32,41 @@ protected function getCredentialsFromFormData(array $data): array ]; } + public function authenticate(): ?LoginResponse + { + try { + $this->rateLimit(5); + } catch (TooManyRequestsException $exception) { + $this->getRateLimitedNotification($exception)?->send(); + + return null; + } + + $data = $this->form->getState(); + + if (! Filament::auth()->attempt($this->getCredentialsFromFormData($data), $data['remember'] ?? false)) { + $this->throwFailureValidationException(); + } + + $user = Filament::auth()->user(); + + if (($user instanceof FilamentUser) && (! $user->canAccessPanel(Filament::getCurrentPanel()))) { + Filament::auth()->logout(); + + $this->throwFailureValidationException(); + } elseif ($user->customer_id !== null) { + Filament::auth()->logout(); + + throw ValidationException::withMessages([ + 'data.username' => 'Incorrect username or password.', + ]); + } + + session()->regenerate(); + + return app(LoginResponse::class); + } + protected function throwFailureValidationException(): never { throw ValidationException::withMessages([ diff --git a/app/Filament/Admin/Resources/InvoiceResource.php b/app/Filament/Admin/Resources/InvoiceResource.php index a66b951..766b146 100644 --- a/app/Filament/Admin/Resources/InvoiceResource.php +++ b/app/Filament/Admin/Resources/InvoiceResource.php @@ -90,6 +90,16 @@ public static function form(Form $form): Form 'true' => 'info', 'false' => 'info', ]), + + ToggleButtons::make('has_hst') + ->label('HST') + ->boolean('On', 'Off') + ->default(false) + ->inline() + ->colors([ + 'true' => 'info', + 'false' => 'info', + ]), ])->columnSpan(1), ]) ->columns(3) @@ -101,16 +111,16 @@ public static function form(Form $form): Form ->label('ID') ->content(fn (Invoice $record): ?string => $record->internal_id), - Placeholder::make('Tax Rates') - ->content(fn (Invoice $record): ?string => $record->gst_rate.'% GST, '.$record->pst_rate.'% PST'), + Placeholder::make('Tax Rates '.'(if applicable)') + ->content(fn (Invoice $record): ?string => $record->gst_rate.'% GST, '.$record->pst_rate.'% PST, '.$record->hst_rate.'% HST'), Placeholder::make('created_at') ->label('Created') - ->content(fn (Invoice $record): ?string => $record->created_at?->diffForHumans()), + ->content(fn (Invoice $record): ?string => $record->created_at->format('Y-m-d')), Placeholder::make('updated_at') ->label('Last modified') - ->content(fn (Invoice $record): ?string => $record->updated_at?->diffForHumans()), + ->content(fn (Invoice $record): ?string => $record->updated_at->format('Y-m-d')), ]) ->columnSpan(1) @@ -156,9 +166,10 @@ public static function table(Table $table): Table TextColumn::make('has_gst') ->label('GST') + ->money() ->formatStateUsing(function (Invoice $record) { if ($record->has_gst) { - return '$'.$record->gst_amount; + return '$'.number_format($record->gst_amount, 2); } return '-'; @@ -169,7 +180,18 @@ public static function table(Table $table): Table ->label('PST') ->formatStateUsing(function (Invoice $record) { if ($record->has_pst) { - return '$'.$record->pst_amount; + return '$'.number_format($record->pst_amount, 2); + } + + return '-'; + }) + ->alignRight(), + TextColumn::make('has_hst') + ->label('HST') + ->money() + ->formatStateUsing(function (Invoice $record) { + if ($record->has_hst) { + return '$'.number_format($record->hst_amount, 2); } return '-'; diff --git a/app/Filament/Admin/Resources/UserResource.php b/app/Filament/Admin/Resources/UserResource.php index 0b5f193..ecf4ba9 100644 --- a/app/Filament/Admin/Resources/UserResource.php +++ b/app/Filament/Admin/Resources/UserResource.php @@ -6,6 +6,7 @@ use App\Models\User; use Filament\Forms\Components\Checkbox; use Filament\Forms\Components\Section; +use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Forms\Form; use Filament\Resources\Resource; @@ -48,7 +49,18 @@ public static function form(Form $form): Form ->required(fn (string $operation) => $operation === 'create'), Checkbox::make('is_admin') - ->disabled(fn (User $record) => auth()->user()->id === $record->id), + ->label('Admin') + ->reactive() + ->afterStateUpdated(fn ($state, callable $set) => $set('customer_id', null)) + ->disabled(fn (?User $record, $operation) => $operation !== 'create' && auth()->user()->id === $record->id), + + Select::make('customer_id') + ->relationship('customer', 'company_name') + ->disabled(fn ($get) => $get('is_admin')) + ->afterStateUpdated(fn ($state, callable $set) => $set('is_admin', false)) + ->nullable() + ->searchable() + ->preload(), ]), ]); } diff --git a/app/Filament/Customer/Pages/CustomerLogin.php b/app/Filament/Customer/Pages/CustomerLogin.php new file mode 100644 index 0000000..d565a97 --- /dev/null +++ b/app/Filament/Customer/Pages/CustomerLogin.php @@ -0,0 +1,87 @@ +label('Username') + ->required() + ->autofocus() + ->extraInputAttributes(['tabindex' => 1]) + ->autocomplete(); + } + + protected function getCredentialsFromFormData(array $data): array + { + return [ + 'username' => $data['username'], + 'password' => $data['password'], + ]; + } + + protected function throwFailureValidationException(): never + { + throw ValidationException::withMessages([ + 'data.username' => __('filament-panels::pages/auth/login.messages.failed'), + ]); + } + + public function authenticate(): ?LoginResponse + { + try { + $this->rateLimit(5); + } catch (TooManyRequestsException $exception) { + $this->getRateLimitedNotification($exception)?->send(); + + return null; + } + + $data = $this->form->getState(); + + if (! Filament::auth()->attempt($this->getCredentialsFromFormData($data), $data['remember'] ?? false)) { + $this->throwFailureValidationException(); + } + + $user = Filament::auth()->user(); + + if (($user instanceof FilamentUser) && (! $user->canAccessPanel(Filament::getCurrentPanel()))) { + Filament::auth()->logout(); + + $this->throwFailureValidationException(); + } elseif ($user->customer_id === null) { + Filament::auth()->logout(); + + throw ValidationException::withMessages([ + 'data.username' => 'Incorrect username or password.', + ]); + } + + session()->regenerate(); + + return app(LoginResponse::class); + } + + public function getTitle(): Htmlable|string + { + return __('Login'); + + } + + public function getHeading(): Htmlable|string + { + return __('Login'); + } +} diff --git a/app/Filament/Customer/Resources/OrderResource.php b/app/Filament/Customer/Resources/OrderResource.php index 201b2f3..c2f6c98 100644 --- a/app/Filament/Customer/Resources/OrderResource.php +++ b/app/Filament/Customer/Resources/OrderResource.php @@ -7,7 +7,6 @@ use App\Models\Order; use Filament\Forms\Form; use Filament\Resources\Resource; -use Filament\Tables; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; @@ -29,25 +28,10 @@ public static function table(Table $table): Table { return \App\Filament\Admin\Resources\OrderResource::table($table) ->modifyQueryUsing(function (Builder $query) { - return $query->where('customer_id', 1); + return $query->where('customer_id', auth()->user()->customer_id); }) ->actions([]) ->bulKActions([]); - // $table - // ->columns([ - // // - // ]) - // ->filters([ - // // - // ]) - // ->actions([ - // Tables\Actions\EditAction::make(), - // ]) - // ->bulkActions([ - // Tables\Actions\BulkActionGroup::make([ - // Tables\Actions\DeleteBulkAction::make(), - // ]), - // ]); } public static function getRelations(): array @@ -61,8 +45,6 @@ public static function getPages(): array { return [ 'index' => Pages\ListOrders::route('/'), - // 'create' => Pages\CreateOrder::route('/create'), - // 'edit' => Pages\EditOrder::route('/{record}/edit'), ]; } } diff --git a/app/Models/Customer.php b/app/Models/Customer.php index 9cea0a7..c48123f 100644 --- a/app/Models/Customer.php +++ b/app/Models/Customer.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\SoftDeletes; class Customer extends Model @@ -100,4 +101,9 @@ public function invoices(): HasMany { return $this->hasMany(Invoice::class); } + + public function user(): HasOne + { + return $this->hasOne(User::class); + } } diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index df485d2..1e77b7d 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -12,7 +12,6 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Database\Eloquent\SoftDeletes; -use Illuminate\Support\Facades\Log; #[ObservedBy(InvoiceObserver::class)] @@ -31,10 +30,13 @@ class Invoice extends Model 'total', 'pst_rate', 'gst_rate', + 'hst_rate', 'pst_amount', 'gst_amount', + 'hst_amount', 'has_pst', 'has_gst', + 'has_hst', 'date', 'due_date', ]; @@ -42,11 +44,13 @@ class Invoice extends Model protected $casts = [ 'has_gst' => 'boolean', 'has_pst' => 'boolean', + 'has_hst' => 'boolean', 'date' => 'datetime', 'status' => InvoiceStatus::class, 'subtotal' => 'float', 'pst_amount' => 'float', 'gst_amount' => 'float', + 'hst_amount' => 'float', 'total' => 'float', ]; @@ -60,23 +64,26 @@ public function calculateTotals(): void $this->subtotal = 0; $this->gst_amount = 0; $this->pst_amount = 0; + $this->hst_amount = 0; $this->total = 0; return; } $subtotal = $this->orders->sum(fn (Order $order) => $order->total_service_price); - Log::debug('subtotal: '.$subtotal); $this->subtotal = $subtotal; $this->saveQuietly(); $gstAmount = $this->calculateTaxAmount($subtotal, $this->gst_rate, $this->has_gst); $pstAmount = $this->calculateTaxAmount($subtotal, $this->pst_rate, $this->has_pst); + $hstAmount = $this->calculateTaxAmount($subtotal, $this->hst_rate, $this->has_hst); $this->gst_amount = $gstAmount; $this->pst_amount = $pstAmount; - $this->total = $subtotal + $gstAmount + $pstAmount; + $this->hst_amount = $hstAmount; + + $this->total = $subtotal + $gstAmount + $pstAmount + $hstAmount; $this->saveQuietly(); } diff --git a/app/Models/User.php b/app/Models/User.php index d45a35b..923c878 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -5,6 +5,7 @@ use Database\Factories\UserFactory; use Filament\Models\Contracts\HasName; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; @@ -22,6 +23,7 @@ class User extends Authenticatable implements HasName 'username', 'is_admin', 'password', + 'customer_id', ]; /** @@ -46,8 +48,34 @@ protected function casts(): array ]; } + public function setIsAdminAttribute(bool $value): void + { + $this->attributes['is_admin'] = $value; + $this->ensureCustomerIsNotAdmin(); + } + + public function setCustomerIdAttribute(?int $value): void + { + $this->attributes['customer_id'] = $value; + $this->ensureCustomerIsNotAdmin(); + } + + public function ensureCustomerIsNotAdmin(): void + { + if ($this->is_admin) { + $this->customer_id = null; + } elseif ($this->customer_id) { + $this->is_admin = false; + } + } + public function getFilamentName(): string { return $this->username; } + + public function customer(): BelongsTo + { + return $this->belongsTo(Customer::class); + } } diff --git a/app/Observers/InvoiceObserver.php b/app/Observers/InvoiceObserver.php index e4078cb..339a909 100644 --- a/app/Observers/InvoiceObserver.php +++ b/app/Observers/InvoiceObserver.php @@ -14,6 +14,7 @@ public function creating(Invoice $invoice): void { $invoice->pst_rate = TaxRate::where('name', 'PST')->value('value') ?? 0; $invoice->gst_rate = TaxRate::where('name', 'GST')->value('value') ?? 0; + $invoice->hst_rate = TaxRate::where('name', 'HST')->value('value') ?? 0; } /** diff --git a/app/Providers/Filament/CustomerPanelProvider.php b/app/Providers/Filament/CustomerPanelProvider.php index 6d54cbb..f8779b9 100644 --- a/app/Providers/Filament/CustomerPanelProvider.php +++ b/app/Providers/Filament/CustomerPanelProvider.php @@ -2,6 +2,7 @@ namespace App\Providers\Filament; +use App\Filament\Customer\Pages\CustomerLogin; use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\DisableBladeIconComponents; use Filament\Http\Middleware\DispatchServingFilamentEvent; @@ -9,7 +10,6 @@ use Filament\Panel; use Filament\PanelProvider; use Filament\Support\Colors\Color; -use Filament\Widgets; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; use Illuminate\Cookie\Middleware\EncryptCookies; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; @@ -35,11 +35,9 @@ public function panel(Panel $panel): Panel // ->pages([ // Pages\Dashboard::class, // ]) + ->login(CustomerLogin::class) ->discoverWidgets(in: app_path('Filament/Customer/Widgets'), for: 'App\\Filament\\Customer\\Widgets') - ->widgets([ - Widgets\AccountWidget::class, - Widgets\FilamentInfoWidget::class, - ]) + ->widgets([]) ->middleware([ EncryptCookies::class, AddQueuedCookiesToResponse::class, diff --git a/database/factories/InvoiceFactory.php b/database/factories/InvoiceFactory.php index 341cbfa..f02dadd 100644 --- a/database/factories/InvoiceFactory.php +++ b/database/factories/InvoiceFactory.php @@ -19,10 +19,12 @@ public function definition(): array return [ 'created_at' => Carbon::now()->subDays(rand(1, 30)), - 'pst_rate' => TaxRate::where('name', 'PST')->value('value'), - 'gst_rate' => TaxRate::where('name', 'GST')->value('value'), + 'pst_rate' => TaxRate::where('name', 'PST')->value('value') ?? 0, + 'gst_rate' => TaxRate::where('name', 'GST')->value('value') ?? 0, + 'hst_rate' => TaxRate::where('name', 'HST')->value('value') ?? 0, 'has_gst' => true, 'has_pst' => $this->faker->boolean(40), + 'has_hst' => false, 'date' => Carbon::now()->subDays(rand(1, 60)), 'status' => $this->faker->randomElement(InvoiceStatus::cases())->value, 'customer_id' => $customer->id, diff --git a/database/migrations/004_create_customers_table.php b/database/migrations/000_create_customers_table.php similarity index 100% rename from database/migrations/004_create_customers_table.php rename to database/migrations/000_create_customers_table.php diff --git a/database/migrations/000_create_users_table.php b/database/migrations/001_create_users_table.php similarity index 95% rename from database/migrations/000_create_users_table.php rename to database/migrations/001_create_users_table.php index 7043251..8ab5211 100644 --- a/database/migrations/000_create_users_table.php +++ b/database/migrations/001_create_users_table.php @@ -13,6 +13,7 @@ public function up(): void { Schema::create('users', function (Blueprint $table) { $table->id(); + $table->foreignId('customer_id')->nullable()->constrained(); $table->string('username')->unique(); $table->string('password'); $table->boolean('is_admin')->default(0); diff --git a/database/migrations/001_create_cache_table.php b/database/migrations/002_create_cache_table.php similarity index 100% rename from database/migrations/001_create_cache_table.php rename to database/migrations/002_create_cache_table.php diff --git a/database/migrations/002_create_jobs_table.php b/database/migrations/003_create_jobs_table.php similarity index 100% rename from database/migrations/002_create_jobs_table.php rename to database/migrations/003_create_jobs_table.php diff --git a/database/migrations/003_create_tax_rates_table.php b/database/migrations/004_create_tax_rates_table.php similarity index 100% rename from database/migrations/003_create_tax_rates_table.php rename to database/migrations/004_create_tax_rates_table.php diff --git a/database/migrations/007_create_invoices_table.php b/database/migrations/007_create_invoices_table.php index 3fce718..1c3bc04 100644 --- a/database/migrations/007_create_invoices_table.php +++ b/database/migrations/007_create_invoices_table.php @@ -21,12 +21,15 @@ public function up(): void $table->decimal('pst_rate', 8, 2); $table->decimal('gst_rate', 8, 2); + $table->decimal('hst_rate', 8, 2); $table->decimal('pst_amount', 8, 2)->nullable(); $table->decimal('gst_amount', 8, 2)->nullable(); + $table->decimal('hst_amount', 8, 2)->nullable(); $table->boolean('has_pst')->default(false); $table->boolean('has_gst')->default(true); + $table->boolean('has_hst')->default(false); $table->date('date')->default(today()); $table->date('due_date')->nullable(); diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 5756bbe..8bd5966 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -17,6 +17,7 @@ public function run(): void $this->call([ TaxRateSeeder::class, CustomerSeeder::class, + UserSeeder::class, ContactSeeder::class, ShippingEntrySeeder::class, OrderSeeder::class, diff --git a/database/seeders/TaxRateSeeder.php b/database/seeders/TaxRateSeeder.php index 639cd03..0b9180a 100644 --- a/database/seeders/TaxRateSeeder.php +++ b/database/seeders/TaxRateSeeder.php @@ -14,5 +14,6 @@ public function run(): void { TaxRate::create(['name' => 'GST', 'value' => 5.00]); TaxRate::create(['name' => 'PST', 'value' => 7.00]); + TaxRate::create(['name' => 'HST', 'value' => 13.00]); } } diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php new file mode 100644 index 0000000..b9e84dc --- /dev/null +++ b/database/seeders/UserSeeder.php @@ -0,0 +1,24 @@ + str_replace(',', '', strtolower(explode(' ', $customer->company_name)[0])), + 'password' => 'password', + 'customer_id' => $customer->id, + ])->create(); + } + } +} diff --git a/tests/Unit/InvoiceTest.php b/tests/Unit/InvoiceTest.php index 9e3f806..0bdd36c 100644 --- a/tests/Unit/InvoiceTest.php +++ b/tests/Unit/InvoiceTest.php @@ -19,6 +19,7 @@ $customer = Customer::factory()->create(); // Generates a customer $pst_rate = TaxRate::where('name', 'PST')->value('value') ?? 0; $gst_rate = TaxRate::where('name', 'GST')->value('value') ?? 0; + $hst_rate = TaxRate::where('name', 'HST')->value('value') ?? 0; $formData = [ 'customer_id' => $customer->id, @@ -27,6 +28,7 @@ 'status' => InvoiceStatus::UNPAID->value, 'has_gst' => true, 'has_pst' => true, + 'has_hst' => false, ]; $this->livewire(CreateInvoice::class) @@ -40,8 +42,10 @@ 'status' => $formData['status'], 'has_gst' => $formData['has_gst'], 'has_pst' => $formData['has_pst'], + 'has_hst' => $formData['has_hst'], 'gst_rate' => $gst_rate, 'pst_rate' => $pst_rate, + 'hst_rate' => $hst_rate, ]); $invoice = Invoice::where('internal_id', 'INV400001')->firstOrFail();