schema([ Hidden::make('status') ->default('DRAFT'), Actions::make([ Action::make('copy') ->label('Copy') ->icon('heroicon-o-document-duplicate') ->color('info') ->action(function ($record, Set $set) { $set('status', 'DRAFT'); $record->save(); }) ->size(ActionSize::ExtraLarge), Action::make('send_email') ->label('Send e-mail') ->icon('heroicon-o-envelope') ->color('info') ->action(function ($record, Set $set) { $set('status', 'SEND_EMAIL'); $record->save(); }) ->disabled(fn ($record) => $record?->status && in_array($record->status, ['ACCEPTED', 'REJECTED'])) ->size(ActionSize::ExtraLarge), Action::make('accept') ->label('Accept') ->icon('heroicon-o-check-circle') ->color('success') ->action(function ($record, Set $set) { $set('status', 'ACCEPTED'); // Invoice creation notification Notification::make() ->title('This will create an invoice') ->body('Accepting this offer will automatically create an invoice.') ->warning() ->send(); $record->save(); }) ->size(ActionSize::ExtraLarge), Action::make('reject') ->label('Reject') ->icon('heroicon-o-x-circle') ->color('danger') ->action(function ($record, Set $set) { $set('status', 'REJECTED'); $record->save(); }) ->size(ActionSize::ExtraLarge), Action::make('close') ->label('Close') ->icon('heroicon-o-arrow-down-tray') ->color('gray') ->action(function ($record) { $record->save(); return redirect()->route('filament.admin.resources.offers.index'); }) ->size(ActionSize::ExtraLarge), ]) ->columnSpanFull(), // Makes the actions span the full width Section::make() ->schema([ Grid::make(2)->schema([ TextInput::make('offer_number') ->required() ->label('Offer Number') ->default(fn () => Offer::getNextOfferNumber()) ->disabled() ->dehydrated(), DatePicker::make('valid_until') ->required() ->label('Valid Until') ->default(now()->addDays(30)) ->minDate(now()), ]) ->inlineLabel(), Grid::make(2)->schema([ Select::make('debtor_id') ->relationship('debtor', 'name', function ($query) { // Only show debtors belonging to the current customer if user is a customer if (auth()->user()->isCustomer()) { return $query->where('customer_id', auth()->user()->customer_id); } return $query; }) ->preload() ->required() ->label('Debtor') ->createOptionForm([ Forms\Components\TextInput::make('name') ->required(), Forms\Components\TextInput::make('email') ->email() ->required(), Forms\Components\TextInput::make('phonenumber') ->tel() ->maxLength(20), Forms\Components\Textarea::make('cocnumber') ->columnSpanFull(), ]) ->searchable() ->afterStateUpdated(function ($state, Set $set) { if ($state) { // Get the debtor's customer_id and set it on the offer $debtor = \App\Models\Debtor::find($state); if ($debtor) { $set('customer_id', $debtor->customer_id); } } }), Hidden::make('customer_id') ->dehydrated(), TextInput::make('description') ->label('Description'), ]) ->inlineLabel(), TableRepeater::make('lineItems') ->columnSpanFull() ->stackAt(MaxWidth::Medium) ->headers([ Header::make('Article')->width('125px'), Header::make('Description')->width('150px'), Header::make('Price'), Header::make('Quantity'), Header::make('Unit'), Header::make('Vat'), Header::make('Total'), ]) ->relationship('lineItems') ->label(false) ->schema([ static::getArticleSelectionField(), Textarea::make('description') ->required() ->autosize() ->rows(1) ->placeholder('Enter description'), TextInput::make('price') ->numeric() ->required() ->prefix('€') ->minValue(0) ->live() ->afterStateUpdated(function (Get $get, Set $set, $state, $context) { if ($context === 'creation') return; static::calculateLineItemTotal($get, $set); static::calculateTotals($get, $set); }), TextInput::make('quantity') ->numeric() ->required() ->minValue(1) ->default(1) ->live() ->afterStateUpdated(function (Get $get, Set $set) { static::calculateLineItemTotal($get, $set); static::calculateTotals($get, $set); }), static::getUnitSelectField(), Select::make('vat_percentage') ->options([ 0 => '0%', 9 => '9%', 21 => '21%', ]) ->required() ->default(21) ->live() ->afterStateUpdated(function (Get $get, Set $set, $state) { static::calculateLineItemTotal($get, $set); static::calculateTotals($get, $set); }), TextInput::make('line_total') ->numeric() ->required() ->prefix('€') ->disabled() ->extraInputAttributes(['class' => 'text-right']) ->dehydrated(), ]) ->defaultItems(1) ->reorderable(false) ->streamlined() ->live() ->afterStateUpdated(function (Get $get, Set $set) { static::calculateTotals($get, $set); }) ->deleteAction( fn (Forms\Components\Actions\Action $action) => $action ->icon('heroicon-m-trash') ->iconButton() ->color('danger') ->size('sm') ->after(function (Get $get, Set $set) { static::calculateTotals($get, $set); }) ) ->addActionLabel('Add detail'), Grid::make(3) ->schema([ // Your existing 5 columns fields here // Then use columnSpan() to position the financial fields TextInput::make('subtotal') ->columnStart(3) // Aligns with the 5th column ->inlineLabel() ->numeric() ->required() ->prefix('€') ->extraInputAttributes(['class' => 'text-right']) ->disabled() ->dehydrated(), TextInput::make('vat_total') ->columnStart(3) ->inlineLabel() ->numeric() ->required() ->prefix('€') ->disabled() ->extraInputAttributes(['class' => 'text-right']) ->dehydrated(), TextInput::make('total') ->columnStart(3) ->inlineLabel() ->numeric() ->required() ->prefix('€') ->disabled() ->extraInputAttributes(['class' => 'text-right']) ->dehydrated(), ]), ]) ->compact() ]); } public static function table(Table $table): Table { return $table ->columns([ TextColumn::make('offer_number') ->sortable() ->searchable(), TextColumn::make('debtor.name') ->label('Debtor') ->sortable() ->searchable(), TextColumn::make('contact.name') ->label('Contact') ->toggleable(isToggledHiddenByDefault: false), TextColumn::make('valid_until') ->date() ->sortable(), TextColumn::make('total') ->money('EUR') ->sortable(), TextColumn::make('status') ->badge() ->color(fn (string $state): string => match ($state) { 'DRAFT' => 'info', 'EMAIL_SENT' => 'warning', 'ACCEPTED' => 'success', 'REJECTED' => 'danger', }) ->icon(fn (string $state): string => match ($state) { 'DRAFT' => 'heroicon-o-pencil', 'EMAIL_SENT' => 'heroicon-o-paper-airplane', 'ACCEPTED' => 'heroicon-o-check-circle', 'REJECTED' => 'heroicon-o-x-circle', }), TextColumn::make('created_at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('updated_at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), ]) ->defaultSort('created_at', 'desc') ->filters([ Tables\Filters\SelectFilter::make('status') ->options([ 'DRAFT' => 'Draft', 'EMAIL_SENT' => 'Email sent', 'ACCEPTED' => 'Accepted', 'REJECTED' => 'Rejected', ]), Tables\Filters\Filter::make('expired') ->label('Expired Offers') ->query(fn (Builder $query): Builder => $query->where('valid_until', '<', now()) ->whereNotIn('status', ['ACCEPTED', 'REJECTED'])), Tables\Filters\Filter::make('valid') ->label('Valid Offers') ->query(fn (Builder $query): Builder => $query->where('valid_until', '>=', now())), ]) ->actions([ Tables\Actions\EditAction::make(), Tables\Actions\ViewAction::make(), Tables\Actions\Action::make('duplicate') ->label('Duplicate') ->icon('heroicon-o-document-duplicate') ->color('gray') ->action(function (Offer $record) { $newOffer = $record->replicate(); $newOffer->offer_number = Offer::getNextOfferNumber(); $newOffer->status = 'DRAFT'; $newOffer->valid_until = now()->addDays(30); $newOffer->created_at = now(); $newOffer->updated_at = now(); $newOffer->save(); // Duplicate line items foreach ($record->lineItems as $lineItem) { $newLineItem = $lineItem->replicate(); $newLineItem->offer_id = $newOffer->id; $newLineItem->save(); } return redirect()->route('filament.admin.resources.offers.edit', $newOffer); }) ->requiresConfirmation(), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ Tables\Actions\DeleteBulkAction::make(), ]), ]); } public static function getRelations(): array { return [ // ]; } public static function getPages(): array { return [ 'index' => Pages\ListOffers::route('/'), 'create' => Pages\CreateOffer::route('/create'), 'edit' => Pages\EditOffer::route('/{record}/edit'), ]; } public static function getEloquentQuery(): Builder { $query = parent::getEloquentQuery(); if (auth()->user()->isAccountant()) { $query->whereHas('customer', function ($query) { $query->where('accountant_id', auth()->user()->accountant_id); }); } elseif (auth()->user()->isCustomer()) { $query->where('customer_id', auth()->user()->customer_id); } return $query; } }