Work on Payments

This commit is contained in:
Nisse Lommerde 2025-01-24 21:37:05 -05:00
parent f937302159
commit b2c8ce7405
49 changed files with 2589 additions and 1412 deletions

View File

@ -4,21 +4,20 @@
enum IconEnum: string
{
case DEFAULT = 'heroicon-o-rectangle-stack';
case INVOICE = 'lucide-receipt-text';
case ORDER = 'lucide-shopping-cart';
case QUOTE = 'lucide-quote';
case CUSTOMER = 'lucide-building';
case PACKING_SLIP = 'lucide-package';
case SHIPPING_ENTRY = 'lucide-truck';
case USER = 'lucide-users';
case TAX_RATE = 'lucide-circle-dollar-sign';
// case PRODUCT_SERVICE = 'heroicon-o-rectangle-stack';
// case CUSTOMER_SALES = 'heroicon-o-rectangle-stack';
// case INVOICE_REPORT = 'heroicon-o-rectangle-stack';
case TAB_ALL = 'lucide-layout-grid';
case TAB_OVERDUE = 'lucide-calendar-clock';
case TAB_UNPRINTED = 'lucide-printer';
case DEFAULT = 'heroicon-o-rectangle-stack';
case INVOICE = 'lucide-file-text';
case ORDER = 'lucide-shopping-cart';
case QUOTE = 'lucide-quote';
case CUSTOMER = 'lucide-building';
case PACKING_SLIP = 'lucide-package';
case SHIPPING_ENTRY = 'lucide-truck';
case USER = 'lucide-users';
case TAX_RATE = 'lucide-circle-dollar-sign';
case DISTRIBUTE_PAYMENTS = 'lucide-rotate-cw';
case PRODUCT_SERVICE = 'heroicon-o-rectangle';
case CUSTOMER_SALES = 'lucide-book-user';
case INVOICE_REPORT = 'lucide-files';
case TAB_ALL = 'lucide-layout-grid';
case TAB_OVERDUE = 'lucide-calendar-clock';
case TAB_UNPRINTED = 'lucide-printer';
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class InvoiceCreated
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*/
public function __construct()
{
//
}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('channel-name'),
];
}
}

View File

@ -18,13 +18,13 @@ class CustomerReportResource extends Resource
{
protected static ?string $model = Customer::class;
protected static ?string $navigationIcon = IconEnum::CUSTOMER_SALES->value;
protected static ?string $navigationGroup = 'Reports';
protected static ?string $navigationIcon = IconEnum::DEFAULT->value;
protected static ?string $navigationLabel = 'Customer Reports';
protected static ?string $navigationLabel = 'Customer Sales';
protected static ?int $navigationSort = 2;
protected static ?int $navigationSort = 4;
public static function form(Form $form): Form
{

View File

@ -4,6 +4,8 @@
use App\Enums\IconEnum;
use App\Filament\Admin\Resources\CustomerResource\RelationManagers\ContactsRelationManager;
use App\Filament\Admin\Resources\CustomerResource\RelationManagers\InvoicesRelationManager;
use App\Filament\Admin\Resources\CustomerResource\RelationManagers\PaymentsRelationManager;
use App\Filament\Admin\Resources\CustomerResource\RelationManagers\ShippingEntriesRelationManager;
use App\Models\Customer;
use Filament\Forms\Components\Section;
@ -29,7 +31,8 @@ public static function form(Form $form): Form
return $form
->schema([
Section::make([
TextInput::make('company_name'),
TextInput::make('company_name')
->required(),
TextInput::make('phone'),
TextInput::make('shipping_address_line_1'),
TextInput::make('shipping_address_line_2'),
@ -44,10 +47,14 @@ public static function table(Table $table): Table
return $table
->columns([
TextColumn::make('company_name')
->extraHeaderAttributes(['class' => 'w-full'])
->searchable()
->sortable(),
// TextColumn::make('shipping_address'),
TextColumn::make('phone'),
TextColumn::make('balance')
->getStateUsing(fn (Customer $customer) => $customer->calculateBalance())
->money()
->hidden(! auth()->user()->is_admin),
])
->filters([
//
@ -65,19 +72,19 @@ public static function table(Table $table): Table
public static function getRelations(): array
{
return [
// RelationGroup::make('Relations', [
InvoicesRelationManager::class,
PaymentsRelationManager::class,
ContactsRelationManager::class,
ShippingEntriesRelationManager::class,
// ]),
];
}
public static function getPages(): array
{
return [
'index' => \App\Filament\Admin\Resources\CustomerResource\Pages\ListCustomers::route('/'),
'create' => \App\Filament\Admin\Resources\CustomerResource\Pages\CreateCustomer::route('/create'),
'edit' => \App\Filament\Admin\Resources\CustomerResource\Pages\EditCustomer::route('/{record}/edit'),
'index' => \App\Filament\Admin\Resources\CustomerResource\Pages\ListCustomers::route('/'),
// 'create' => \App\Filament\Admin\Resources\CustomerResource\Pages\CreateCustomer::route('/create'),
'edit' => \App\Filament\Admin\Resources\CustomerResource\Pages\EditCustomer::route('/{record}/edit'),
];
}
}

View File

@ -12,7 +12,7 @@ class EditCustomer extends EditRecord
protected function getHeaderActions(): array
{
//todo: make report
// todo: make report
return [
Actions\DeleteAction::make(),

View File

@ -0,0 +1,17 @@
<?php
namespace App\Filament\Admin\Resources\CustomerResource\RelationManagers;
use App\Filament\Admin\Resources\InvoiceResource;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables\Table;
class InvoicesRelationManager extends RelationManager
{
protected static string $relationship = 'invoices';
public function table(Table $table): Table
{
return InvoiceResource::table($table);
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Filament\Admin\Resources\CustomerResource\RelationManagers;
use App\Filament\Admin\Resources\PaymentResource;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
class PaymentsRelationManager extends RelationManager
{
protected static string $relationship = 'payments';
public function form(Form $form): Form
{
return $form
->schema([
// PaymentResource
// Forms\Components\TextInput::make('amount')
// ->required()
// ->maxLength(255),
]);
}
public function table(Table $table): Table
{
return PaymentResource::table($table);
/* return $table
->recordTitleAttribute('amount')
->columns([
Tables\Columns\TextColumn::make('amount'),
])
->headerActions([
Tables\Actions\CreateAction::make(),
]);*/
}
}

View File

@ -17,7 +17,7 @@
class InvoiceReportResource extends Resource
{
protected static ?string $navigationIcon = IconEnum::DEFAULT->value;
protected static ?string $navigationIcon = IconEnum::INVOICE_REPORT->value;
protected static ?string $navigationGroup = 'Reports';

View File

@ -3,6 +3,7 @@
namespace App\Filament\Admin\Resources\InvoiceReportResource\RelationManagers;
use App\Filament\Admin\Resources\InvoiceResource;
use App\Models\Invoice;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
@ -50,9 +51,9 @@ public function table(Table $table): Table
->formatStateUsing(function ($state) {
return $state == 0.00 ? '-' : '$'.$state;
}),
TextColumn::make('total')
->label('Balance Due')
TextColumn::make('balance')
->alignRight()
->getStateUsing(fn (Invoice $record) => $record->remainingBalance())
->money()
->weight(FontWeight::Bold),
TextColumn::make('status'),

View File

@ -4,6 +4,7 @@
use App\Enums\IconEnum;
use App\Enums\InvoiceStatus;
use App\Filament\Admin\Resources\CustomerResource\RelationManagers\InvoicesRelationManager;
use App\Filament\Admin\Resources\InvoiceResource\RelationManagers\OrdersRelationManager;
use App\Filament\Admin\Resources\InvoiceResource\RelationManagers\ProductServicesRelationManager;
use App\Models\Customer;
@ -19,6 +20,7 @@
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Support\Enums\FontWeight;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@ -31,9 +33,9 @@ class InvoiceResource extends Resource
protected static ?string $navigationIcon = IconEnum::INVOICE->value;
protected static ?string $navigationGroup = 'Production';
protected static ?string $navigationGroup = 'Financial';
protected static ?int $navigationSort = 2;
protected static ?int $navigationSort = 1;
public static function form(Form $form): Form
{
@ -42,40 +44,30 @@ public static function form(Form $form): Form
Group::make()
->schema([
Section::make([
Grid::make(2)
->schema([
Select::make('customer_id')
->required()
->label('Customer')
->options(Customer::all()->pluck('company_name', 'id'))
->reactive()
->searchable()
->disabledOn('edit')
->columnSpan(2),
Select::make('customer_id')
->required()
->label('Customer')
->options(Customer::all()->pluck('company_name', 'id'))
->reactive()
->searchable()
->disabledOn('edit')
->columnSpan(2),
Split::make([
DatePicker::make('date')
->required()
->default(today()),
DatePicker::make('due_date'),
])
->columnSpan(2),
Split::make([
DatePicker::make('date')
->required()
->default(today()),
DatePicker::make('due_date'),
])
->columnSpan(2),
ToggleButtons::make('status')
->options(InvoiceStatus::class)
->required()
->inline()
->default(InvoiceStatus::UNPAID)
->columnSpan(2),
])->columnSpan(2),
Grid::make(1)
Grid::make(3)
->schema([
ToggleButtons::make('has_gst')
->label('GST')
->boolean('On', 'Off')
->default(true)
->inline()
// ->inline()
->colors([
'true' => 'info',
'false' => 'info',
@ -85,7 +77,7 @@ public static function form(Form $form): Form
->label('PST')
->boolean('On', 'Off')
->default(false)
->inline()
// ->inline()
->colors([
'true' => 'info',
'false' => 'info',
@ -95,14 +87,22 @@ public static function form(Form $form): Form
->label('HST')
->boolean('On', 'Off')
->default(false)
->inline()
// ->inline()
->colors([
'true' => 'info',
'false' => 'info',
]),
])->columnSpan(1),
ToggleButtons::make('status')
->options(InvoiceStatus::class)
->required()
->inline()
->default(InvoiceStatus::UNPAID)
->columnSpan(1),
])
->columns(3)
->columns(2)
->columnSpan(2),
Section::make()
@ -111,16 +111,15 @@ public static function form(Form $form): Form
->label('ID')
->content(fn (Invoice $record): ?string => $record->internal_id),
Placeholder::make('Tax Rates '.'(if applicable)')
Placeholder::make('Amounts')
->content(fn (Invoice $record): ?string => 'Total: $'.$record->total.', balance: $'.$record->remainingBalance()),
Placeholder::make('Tax Rates when created')
->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->format('Y-m-d')),
Placeholder::make('updated_at')
->label('Last modified')
->content(fn (Invoice $record): ?string => $record->updated_at->format('Y-m-d')),
->label('Timestamps')
->content(fn (Invoice $record): ?string => 'Created at '.$record->created_at->format('Y-m-d').', updated at '.$record->updated_at->format('Y-m-d')),
])
->columnSpan(1)
@ -138,6 +137,7 @@ public static function table(Table $table): Table
return $table
->columns([
TextColumn::make('internal_id')
->extraHeaderAttributes(fn ($livewire) => $livewire::class === InvoicesRelationManager::class ? ['class' => 'w-full'] : false)
->label('ID')
->fontFamily('mono')
->color('primary')
@ -151,6 +151,7 @@ public static function table(Table $table): Table
}),
TextColumn::make('customer.company_name')
->hidden(fn ($livewire) => $livewire::class === InvoicesRelationManager::class)
->sortable()
->extraHeaderAttributes(['class' => 'w-full'])
->searchable(),
@ -186,22 +187,11 @@ public static function table(Table $table): Table
return '-';
})
->alignRight(),
TextColumn::make('has_hst')
->label('HST')
TextColumn::make('balance')
->getStateUsing(fn (Invoice $record) => $record->remainingBalance())
->money()
->formatStateUsing(function (Invoice $record) {
if ($record->has_hst) {
return '$'.number_format($record->hst_amount, 2);
}
return '-';
})
->alignRight(),
TextColumn::make('total')
->label('Total')
->money()
->weight('bold')
->alignRight(),
->alignRight()
->weight(FontWeight::Bold),
TextColumn::make('status')
->badge(InvoiceStatus::class)
->sortable(),
@ -239,7 +229,8 @@ public static function table(Table $table): Table
->defaultSort('id', 'desc')
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\EditAction::make()
->hidden(fn ($livewire) => $livewire::class === InvoicesRelationManager::class),
])
->bulkActions([

View File

@ -24,7 +24,7 @@ class PackingSlipResource extends Resource
protected static ?string $navigationIcon = IconEnum::PACKING_SLIP->value;
protected static ?string $navigationGroup = 'Management';
protected static ?string $navigationGroup = 'Production';
protected static ?int $navigationSort = 2;
@ -112,9 +112,9 @@ public static function getRelations(): array
public static function getPages(): array
{
return [
'index' => \App\Filament\Admin\Resources\PackingSlipResource\Pages\ListPackingSlips::route('/'),
'create' => \App\Filament\Admin\Resources\PackingSlipResource\Pages\CreatePackingSlip::route('/create'),
'edit' => \App\Filament\Admin\Resources\PackingSlipResource\Pages\EditPackingSlip::route('/{record}/edit'),
'index' => \App\Filament\Admin\Resources\PackingSlipResource\Pages\ListPackingSlips::route('/'),
// 'create' => \App\Filament\Admin\Resources\PackingSlipResource\Pages\CreatePackingSlip::route('/create'),
// 'edit' => \App\Filament\Admin\Resources\PackingSlipResource\Pages\EditPackingSlip::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,109 @@
<?php
namespace App\Filament\Admin\Resources;
use App\Filament\Admin\Resources\CustomerResource\RelationManagers\PaymentsRelationManager;
use App\Filament\Admin\Resources\PaymentResource\Pages;
use App\Models\Payment;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
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;
class PaymentResource extends Resource
{
protected static ?string $model = Payment::class;
protected static ?string $navigationIcon = 'lucide-hand-coins';
protected static ?string $navigationGroup = 'Financial';
protected static ?int $navigationSort = 2;
public static function form(Form $form): Form
{
return $form
->schema([
Section::make([
Select::make('customer_id')
->relationship('customer', 'company_name')
->required()
->searchable()
->preload(),
TextInput::make('amount')
->required()
->minValue(0)
->maxValue(99999999)
->numeric(),
Textarea::make('notes'),
]),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('created_at')
->label('Date')
->date('Y-m-d')
->searchable(),
TextColumn::make('customer.company_name')
->hidden(fn ($livewire) => $livewire::class === PaymentsRelationManager::class)
->searchable(),
TextColumn::make('notes')
->limit(100)
->extraHeaderAttributes(['class' => 'w-full']),
TextColumn::make('amount')
->searchable()
->numeric()
->money(),
TextColumn::make('unapplied_amount')
->label('Balance')
->money(),
])
->filters([
//
])
->actions([
ViewAction::make(),
])
->bulkActions([
// Tables\Actions\BulkActionGroup::make([
// Tables\Actions\DeleteBulkAction::make(),
// ]),
]);
}
public static function canAccess(): bool
{
return auth()->user()->is_admin;
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListPayments::route('/'),
// 'view' => Pages\ViewPayment::route('/{record}'),
// 'create' => Pages\CreatePayment::route('/create'),
// 'edit' => Pages\EditPayment::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Admin\Resources\PaymentResource\Pages;
use App\Filament\Admin\Resources\PaymentResource;
use Filament\Resources\Pages\CreateRecord;
class CreatePayment extends CreateRecord
{
protected static string $resource = PaymentResource::class;
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\PaymentResource\Pages;
use App\Filament\Admin\Resources\PaymentResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditPayment extends EditRecord
{
protected static string $resource = PaymentResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Filament\Admin\Resources\PaymentResource\Pages;
use App\Enums\IconEnum;
use App\Filament\Admin\Resources\PaymentResource;
use App\Services\PaymentService;
use Filament\Actions;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords;
class ListPayments extends ListRecords
{
protected static string $resource = PaymentResource::class;
protected function getHeaderActions(): array
{
return [
Actions\Action::make('distributePayments')
->icon(IconEnum::DISTRIBUTE_PAYMENTS->value)
->action(function (PaymentService $paymentService) {
$paymentService->distributePayments();
Notification::make()
->title('Success!')
->body('Payments have been distributed')
->success()
->send();
}),
Actions\CreateAction::make(),
];
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Admin\Resources\PaymentResource\Pages;
use App\Filament\Admin\Resources\PaymentResource;
use Filament\Resources\Pages\ViewRecord;
class ViewPayment extends ViewRecord
{
protected static string $resource = PaymentResource::class;
}

View File

@ -22,6 +22,8 @@ class ServiceTypeResource extends Resource
protected static ?string $label = 'Product Services';
protected static ?int $navigationSort = 2;
public static function getWidgets(): array
{
return [

View File

@ -4,10 +4,11 @@
use App\Enums\IconEnum;
use App\Models\User;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
@ -28,32 +29,49 @@ public static function form(Form $form): Form
{
return $form
->schema([
Section::make()
Section::make('Login details')
->description(fn (string $operation) => $operation == 'edit' ? 'To leave the password unchanged, leave both fields empty,' : false)
->schema([
TextInput::make('username')
->autocomplete(false)
->autocomplete('new-username')
->unique()
->required(),
->required()
->columnSpan(1),
TextInput::make('password')
->password()
->autocomplete(false)
->dehydrated(fn ($state) => ! empty($state))
->required(fn (string $operation): bool => $operation === 'create'),
Grid::make(2)
->schema([
TextInput::make('password')
->password()
->autocomplete('new-password')
->revealable()
->dehydrated(fn ($state) => ! empty($state))
->required(fn (string $operation): bool => $operation === 'create'),
TextInput::make('password_verify')
->label('Verify password')
->password()
->same('password')
->dehydrated(false)
->autocomplete(false)
->required(fn (string $operation) => $operation === 'create'),
TextInput::make('password_verify')
->label('Verify password')
->password()
->revealable()
->same('password')
->dehydrated(false)
->required(fn (string $operation) => $operation === 'create'),
])
->columnSpan(2),
]),
Checkbox::make('is_admin')
->label('Admin')
Section::make('Permissions')
->description('Administrators can access financial information and change settings.')
->schema([
Toggle::make('is_admin')
->label('User is an administrator')
->reactive()
->afterStateUpdated(fn ($state, callable $set) => $set('customer_id', null))
->disabled(fn (?User $record, $operation) => $operation !== 'create' && auth()->user()->id === $record->id),
])
->columns(2),
Section::make('Customer Login')
->description('If this account is for a customer, select them here.')
->schema([
Select::make('customer_id')
->relationship('customer', 'company_name')
@ -73,7 +91,8 @@ public static function table(Table $table): Table
TextColumn::make('username')
->extraHeaderAttributes(['class' => 'w-full']),
TextColumn::make('customer.company_name')
->sortable(),
->sortable()
->placeholder('Internal'),
Tables\Columns\IconColumn::make('is_admin')
->label('Admin')
->boolean()
@ -83,7 +102,7 @@ public static function table(Table $table): Table
//
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\EditAction::make()->modal(),
])
->defaultSort('customer_id', 'asc');
}
@ -103,9 +122,9 @@ public static function getRelations(): array
public static function getPages(): array
{
return [
'index' => \App\Filament\Admin\Resources\UserResource\Pages\ListUsers::route('/'),
'create' => \App\Filament\Admin\Resources\UserResource\Pages\CreateUser::route('/create'),
'edit' => \App\Filament\Admin\Resources\UserResource\Pages\EditUser::route('/{record}/edit'),
'index' => \App\Filament\Admin\Resources\UserResource\Pages\ListUsers::route('/'),
];
}
private static function Grid() {}
}

View File

@ -3,6 +3,7 @@
namespace App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Admin\Resources\UserResource;
use App\Models\User;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
@ -13,7 +14,8 @@ class EditUser extends EditRecord
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
Actions\DeleteAction::make()
->disabled(fn (User $record) => $record->id == auth()->user()->id),
];
}
}

View File

@ -13,7 +13,8 @@ class ListUsers extends ListRecords
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
Actions\CreateAction::make()
->modal(),
];
}
}

View File

@ -25,7 +25,7 @@ public function store(PackingSlipRequest $request): RedirectResponse
]);
}
return redirect()->back(); //todo: change to packing slips page
return redirect()->back(); // todo: change to packing slips page
}
public function show($id): void {}

View File

@ -31,6 +31,14 @@ class Customer extends Model
'total',
];
public function calculateBalance(): float
{
$invoiceTotal = $this->invoices->sum('total');
$paymentTotal = $this->payments->sum('amount');
return $paymentTotal - $invoiceTotal;
}
public function getSubtotalAttribute($created_at = null, $created_until = null): float
{
return $this->invoices()
@ -106,4 +114,9 @@ public function user(): HasOne
{
return $this->hasOne(User::class);
}
public function payments(): HasMany
{
return $this->hasMany(Payment::class);
}
}

View File

@ -54,6 +54,13 @@ class Invoice extends Model
'total' => 'float',
];
public function remainingBalance(): float
{
$applied = $this->payments()->sum('applied_amount');
return max(0, $this->total - $applied);
}
public function calculateTotals(): void
{
$this->refresh();
@ -120,4 +127,11 @@ public function invoiceReports(): BelongsToMany
{
return $this->belongsToMany(InvoiceReport::class);
}
public function payments(): BelongsToMany
{
return $this->belongsToMany(Payment::class)
->withPivot('applied_amount')
->withTimestamps();
}
}

69
app/Models/Payment.php Normal file
View File

@ -0,0 +1,69 @@
<?php
namespace App\Models;
use App\Enums\InvoiceStatus;
use App\Observers\PaymentObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
#[ObservedBy(PaymentObserver::class)]
class Payment extends Model
{
use SoftDeletes;
protected $fillable = [
'customer_id',
'amount',
'unapplied_amount',
'notes',
];
public function applyToInvoices(): void
{
$remaining = $this->unapplied_amount ?? $this->amount;
$invoices = Invoice::where('customer_id', $this->customer_id)
->where('status', InvoiceStatus::UNPAID)
->orderBy('date')
->get();
foreach ($invoices as $invoice) {
$balance = $invoice->remainingBalance();
if ($remaining <= 0) {
break;
}
$applied = min($remaining, $balance);
$invoice->payments()->attach($this->id, ['applied_amount' => $applied]);
$remaining -= $applied;
if ($invoice->remainingBalance() == 0) {
$invoice->setStatus(InvoiceStatus::PAID);
} elseif ($applied > 0) {
$invoice->setStatus(InvoiceStatus::UNPAID);
}
}
$this->unapplied_amount = $remaining;
$this->saveQuietly();
}
public function customer(): BelongsTo
{
return $this->belongsTo(Customer::class);
}
public function invoices(): BelongsToMany
{
return $this->belongsToMany(Invoice::class)
->withPivot('applied_amount')
->withTimestamps();
}
}

View File

@ -22,12 +22,16 @@ public function creating(Invoice $invoice): void
*/
public function created(Invoice $invoice): void
{
$invoice->internal_id = 'INV4'.str_pad($invoice->id, 5, '0', STR_PAD_LEFT);
$invoice->saveQuietly();
$invoice->calculateTotals();
}
public function saved(Invoice $invoice) {}
/**
* Handle the Invoice "updated" event.
*/

View File

@ -0,0 +1,56 @@
<?php
namespace App\Observers;
use App\Models\Payment;
class PaymentObserver
{
/**
* Handle the Payment "saved" event.
*/
public function saved(Payment $payment): void
{
$payment->applyToInvoices();
}
/**
* Handle the Payment "created" event.
*/
public function created(Payment $payment): void
{
//
}
/**
* Handle the Payment "updated" event.
*/
public function updated(Payment $payment): void
{
//
}
/**
* Handle the Payment "deleted" event.
*/
public function deleted(Payment $payment): void
{
//
}
/**
* Handle the Payment "restored" event.
*/
public function restored(Payment $payment): void
{
//
}
/**
* Handle the Payment "force deleted" event.
*/
public function forceDeleted(Payment $payment): void
{
//
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Policies;
use App\Models\User;
class InvoicePolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return auth()->user()->is_admin;
}
}

View File

@ -6,6 +6,7 @@
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
use Filament\Navigation\NavigationGroup;
use Filament\Pages;
use Filament\Panel;
use Filament\PanelProvider;
@ -53,6 +54,13 @@ public function panel(Panel $panel): Panel
->authMiddleware([
Authenticate::class,
])
->sidebarWidth('13rem');
->sidebarWidth('13rem')
->navigationGroups([
NavigationGroup::make('Production'),
NavigationGroup::make('Management'),
NavigationGroup::make('Financial'),
NavigationGroup::make('Reports'),
NavigationGroup::make('Settings'),
]);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Services;
use App\Models\Payment;
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
}
}
}

1505
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -26,7 +26,7 @@ public function definition(): array
'has_pst' => $this->faker->boolean(40),
'has_hst' => false,
'date' => Carbon::now()->subDays(rand(1, 60)),
'status' => $this->faker->randomElement(InvoiceStatus::cases())->value,
'status' => InvoiceStatus::UNPAID->value,
'customer_id' => $customer->id,
'updated_at' => Carbon::now(),
];

View File

@ -12,12 +12,12 @@ public function up()
$table->id();
$table->string('company_name');
$table->string('internal_name');
$table->string('shipping_address_line_1');
$table->string('shipping_address_line_2');
$table->string('billing_address_line_1');
$table->string('billing_address_line_2');
$table->string('phone');
$table->string('internal_name')->nullable();
$table->string('shipping_address_line_1')->nullable();
$table->string('shipping_address_line_2')->nullable();
$table->string('billing_address_line_1')->nullable();
$table->string('billing_address_line_2')->nullable();
$table->string('phone')->nullable();
$table->softDeletes();
$table->timestamps();

View File

@ -11,7 +11,7 @@ public function up(): void
Schema::create('packing_slips', function (Blueprint $table) {
$table->id();
$table->foreignId('order_id')->nullable()->constrained(); //todo: replace this once orders are actually in da system
$table->foreignId('order_id')->nullable()->constrained(); // todo: replace this once orders are actually in da system
$table->foreignId('customer_id')->nullable()->constrained();
$table->date('date_received');
$table->string('amount')->nullable();

View File

@ -0,0 +1,34 @@
<?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('payments', function (Blueprint $table) {
$table->id();
$table->foreignId('customer_id')->constrained();
$table->decimal('amount', 8, 2);
$table->decimal('unapplied_amount', 8, 2)->nullable();
$table->text('notes')->nullable();
$table->softDeletes();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('payments');
}
};

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('invoice_payment', function (Blueprint $table) {
$table->id();
$table->foreignId('invoice_id')->constrained()->cascadeOnDelete();
$table->foreignId('payment_id')->constrained()->cascadeOnDelete();
$table->decimal('applied_amount', 8, 2);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('invoice_payment');
}
};

1473
package-lock.json generated

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -12,22 +12,22 @@
use App\Http\Controllers\ShippingEntryController;
use Illuminate\Support\Facades\Route;
//Route::get('/', function () {
// Route::get('/', function () {
// return redirect()->route('dashboard');
//});
// });
//
//Auth::routes();
// Auth::routes();
Route::get('/pdf/invoicereport/{id}', [PdfController::class, 'invoiceReport'])->name('pdf.invoice-report');
Route::get('orders/{order}/pdf', [OrderController::class, 'pdf'])->name('orders.pdf');
Route::get('invoices/{invoice}/pdf', [InvoiceController::class, 'pdf'])->name('invoice.pdf');
Route::get('customers/{customer}/pdf', [CustomerController::class, 'pdf'])->name('customer.pdf');
//Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
//Route::get('/management/{tab?}', [ManagementController::class, 'index'])->name('management.index');
//Route::resource('order-products', OrderProductController::class);
//Route::resource('contacts', ContactController::class);
//Route::post('/contacts/request-destroy', [ContactController::class, 'requestDestroy'])->name('contacts.requestDestroy');
//Route::resource('packing-slips', PackingSlipController::class);
//Route::resource('shipping-entries', ShippingEntryController::class);
//Route::resource('orders', OrderController::class);
// Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
// Route::get('/management/{tab?}', [ManagementController::class, 'index'])->name('management.index');
// Route::resource('order-products', OrderProductController::class);
// Route::resource('contacts', ContactController::class);
// Route::post('/contacts/request-destroy', [ContactController::class, 'requestDestroy'])->name('contacts.requestDestroy');
// Route::resource('packing-slips', PackingSlipController::class);
// Route::resource('shipping-entries', ShippingEntryController::class);
// Route::resource('orders', OrderController::class);

View File

@ -35,9 +35,9 @@
|
*/
//expect()->extend('toBeOne', function () {
// expect()->extend('toBeOne', function () {
// return $this->toBe(1);
//});
// });
/*
|--------------------------------------------------------------------------

View File

@ -0,0 +1,26 @@
<?php
use App\Filament\Admin\Resources\PaymentResource\Pages\CreatePayment;
use App\Models\Customer;
use App\Models\User;
it('can create a payment', function () {
$user = User::factory(['is_admin' => true])->create();
$this->actingAs($user);
$customer = Customer::factory()->create();
$formData = [
'customer_id' => $customer->id,
'amount' => 500,
];
$this->livewire(CreatePayment::class)
->fillForm($formData)
->call('create')
->assertHasNoErrors();
$this->assertDatabaseHas('payments', [
'customer_id' => $formData['customer_id'],
'amount' => $formData['amount'],
]);
});

2
todos
View File

@ -14,7 +14,7 @@ Customer report
- Save to PDF
Others
-------
- Change price calculations to round to two
- customer name to invoice title / filename