Compare commits

...

43 Commits
0.99.1 ... main

Author SHA1 Message Date
43eebd9528 Merge branch 'development'
Some checks failed
Deploy / deploy (push) Failing after 8s
2025-03-11 12:52:46 -04:00
ea4f46f96d #122 Non-admins can see payments in customer edit page 2025-03-11 12:52:34 -04:00
3defbd8253 Merge pull request 'Version 20250311' (#121) from development into main
Some checks failed
Deploy / deploy (push) Failing after 6s
Reviewed-on: #121
2025-03-11 17:33:40 +01:00
77b9c558af #107: Fix Dashboard
Some checks failed
Deploy / deploy (pull_request) Failing after 9s
2025-03-11 12:31:12 -04:00
6814821100 #118: Improve customer form
Also re-added creating a new customer from the order form
2025-03-11 12:18:04 -04:00
47669851ff #117: Draft orders should not show up in order tabs 2025-03-11 12:18:04 -04:00
0385f614d6 #116: 'Ready for invoice'-badge shows 0 instead of hiding 2025-03-11 12:18:04 -04:00
ef5acd3d06 Add default user seeder
Some checks failed
Deploy / deploy (push) Failing after 8s
2025-03-09 15:17:28 -04:00
8a34981742 Update changelog
Some checks failed
Deploy / deploy (push) Failing after 10s
2025-03-09 15:01:57 -04:00
nisse
f3e7819908 Fix .gitignore
Some checks failed
Deploy / deploy (push) Failing after 13s
2025-03-09 19:21:34 +01:00
44ec2068c3 Merge pull request 'development' (#115) from development into main
Some checks failed
Deploy / deploy (push) Failing after 15s
Reviewed-on: #115
2025-03-08 19:13:36 +01:00
195722c570 Fix create modal redirects 2025-03-08 13:03:11 -05:00
e6fc32c369 Add app version in config 2025-03-08 12:09:54 -05:00
aa340852e7 #110 Invoice Report form feedback 2025-03-08 12:09:16 -05:00
nisse
1192ca681b Work on deployment 2025-03-05 20:52:54 +01:00
d025570d8d Testing deploy runner
Some checks failed
Deploy / deploy (push) Failing after 11s
2025-03-04 15:11:01 -05:00
a75296f997 Add deploy runner
Some checks failed
Deploy / deploy (push) Failing after 21s
2025-03-04 12:17:23 -05:00
2a831578a9 Add demo runner
Some checks failed
Deploy to sewtopnotch.com / deploy (push) Has been cancelled
2025-03-04 10:46:53 -05:00
6433bb8485 Fix: add canAccessPanel function to User model 2025-03-04 10:33:47 -05:00
435224ff7c Cleanup 2025-03-03 11:13:18 -05:00
a4e4c30118 #108 Fix Invoice and Quote Internal IDs 2025-03-01 13:51:06 -05:00
8f8b3a9943 #112 Fix balance unsearchable in invoices
also fixes the invoice status label thing in pdfs
2025-02-28 15:50:11 -05:00
076dcccbb8 Merge branch 'development' of git.niisse.net:nisse/topnotch_website into development 2025-02-28 11:17:28 -05:00
899377d594 #93 WIP Payments 2025-02-19 23:02:44 -05:00
629b3276b5 Reduced padding for repeater view 2025-02-17 17:32:05 -05:00
ec6ae88888 #106 Slugify enums that interact with database 2025-02-17 16:13:22 -05:00
dddbbb8f9b #102 Create 'Ready for invoicing'-status, and associated ways to create invoices from bulk orders 2025-02-16 22:41:32 -05:00
779d46d708 #79 Add sums of columns for Customer Reports 2025-02-13 16:17:45 -05:00
6fc753fc19 #92 Implement qoutes feedback 2025-02-13 14:46:11 -05:00
4955780f67 WIP #92 Implement qoutes feedback 2025-02-12 13:32:39 -05:00
cab2b2c478 #98 Product Services Sum / Totals 2025-02-12 13:32:24 -05:00
e45041566a Add icon to all Create buttons in list views 2025-02-11 21:40:22 -05:00
16e17de6fb #104 Customer Report form side panel 2025-02-11 21:40:03 -05:00
4fbb62353a #93 Add 'Partially paid' status to invoices 2025-02-11 19:54:55 -05:00
6b5a758dbe #99 Add big fat PAID to invoice, when applicable 2025-02-11 18:21:56 -05:00
686cda21bf Moved hard-coded icon names to IconEnum 2025-02-11 14:50:09 -05:00
aa60cf18ee #78 Invoice button on Order edit page
Also cleaned up some icon stuff
2025-02-11 14:32:10 -05:00
2b377f72aa #97 Search and sort invoices by prices 2025-02-11 14:01:56 -05:00
5bec1fc3d8 #96 Add total column to Invoices and Invoice Reports 2025-02-10 15:48:59 -05:00
20dd032b40 #94 Change Invoice GST columns to GST/HST 2025-02-10 14:41:53 -05:00
0d1501362c #95 Change invoice internal ID to TN4xxxx 2025-02-10 14:12:08 -05:00
09cc6b249e Merge branch 'development' of git.niisse.net:nisse/topnotch_website into development 2025-02-10 14:10:01 -05:00
f0f10a4907 Added numeric validation rules, some styling fixes 2025-02-07 23:53:15 -05:00
147 changed files with 2908 additions and 1976 deletions

25
.dockerignore Normal file
View File

@ -0,0 +1,25 @@
# .dockerignore
/deploy/docker-compose.yml
/deploy/Dockerfile
/.phpunit.cache
/node_modules
/public/build
/public/hot
/public/storage
/public/bucket
/storage/*.key
/vendor
.env
.env.example
.env.backup
.env.production
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
/.fleet
/.idea
/.vscode
.git

View File

@ -0,0 +1,27 @@
name: Deploy
# Trigger the workflow on push and
# pull request events on the production branch
on:
push:
branches:
- main
pull_request:
branches:
- main
# Authenticate to the the server via ssh
# and run our deployment script
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Deploy to server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
port: ${{ secrets.PORT }}
key: ${{ secrets.SSHKEY }}
script: "cd /var/www/sewtopnotch.com && ./.scripts/deploy.sh"

1
.gitignore vendored
View File

@ -108,3 +108,4 @@ fabric.properties
.directory
public
_ide_helper.php
public/build

32
.scripts/deploy.sh Normal file
View File

@ -0,0 +1,32 @@
#!/bin/bash
set -e
echo "Deployment started ..."
# Enter maintenance mode or return true
# if already is in maintenance mode
(php artisan down) || true
# Pull the latest version of the app from main branch
git pull origin main
# Install composer dependencies
composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader
# Clear the old cache
php artisan clear-compiled
# Recreate cache
php artisan optimize
# Npm stuff (ci will fail when lockfile modified)
npm ci
npm run build
# Run database migrations
php artisan migrate --force
# Exit maintenance mode
php artisan up
echo "Deployment finished!"

View File

@ -1,70 +1,8 @@
https://github.com/spatie/laravel-pdf/discussions/90
# Changelog
for spatie/pdf stuff
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
## Laravel Sponsors
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
### Premium Partners
- **[Vehikl](https://vehikl.com/)**
- **[Tighten Co.](https://tighten.co)**
- **[WebReinvent](https://webreinvent.com/)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
- **[Cyber-Duck](https://cyber-duck.co.uk)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Jump24](https://jump24.co.uk)**
- **[Redberry](https://redberry.international/laravel/)**
- **[Active Logic](https://activelogic.com)**
- **[byte5](https://byte5.de)**
- **[OP.GG](https://op.gg)**
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
**2025-03-11**
- Fixed #122 - Non-admins can see payments
- Fixed #107 - Fix dashboard
- Fixed #118 - Improved customer form and re-add 'create customer' to order form
- Fixed #117 - Draft orders should not show up in order tabs
- Fixed #116 - 'Ready for invoice'-badge shows 0 instead of hiding

View File

@ -4,20 +4,59 @@
enum IconEnum: string
{
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';
// Sidebar Icons
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 PAYMENTS = 'lucide-hand-coins';
case USER = 'lucide-users';
case TAX_RATE = 'lucide-circle-dollar-sign';
case PRODUCT_SERVICE = 'heroicon-o-rectangle';
case CUSTOMER_SALES = 'lucide-book-user';
case INVOICE_REPORT = 'lucide-file-spreadsheet';
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';
// Tabs
case TAB_ALL = 'lucide-layout-grid';
case TAB_OVERDUE = 'lucide-calendar-clock';
// Action Icons
case PRINT = 'lucide-printer';
case TRASH = 'lucide-trash-2';
case SAVE = 'lucide-save';
case COPY = 'lucide-copy';
case NEW = 'lucide-plus';
// Invoice Status
case UNPAID = 'lucide-circle-x';
case PARTIALLY_PAID = 'lucide-circle-minus';
case PAID = 'lucide-circle-check';
case VOID = 'lucide-circle-slash';
// Order Attributes
case NEW_ART = 'lucide-brush';
case REPEAT = 'lucide-files';
case RUSH = 'lucide-bell-ring';
case EVENT = 'lucide-calendar-range';
case DIGITIZING = 'lucide-computer';
case GARMENTS = 'lucide-shirt';
case SUPPLIED_FILE = 'lucide-file-check';
// Order Status
case DRAFT = 'lucide-pencil';
case APPROVED = 'lucide-check-check';
case PRODUCTION = 'lucide-refresh-cw';
case SHIPPED = 'lucide-send';
case INVOICING = 'lucide-calendar';
case INVOICED = 'lucide-credit-card';
// Shipping Types (THEY_SHIP => SHIPPING_ENTRY)
case WE_SHIP = 'lucide-house';
case PICKUP = 'lucide-handshake';
case SHIPPING_OTHER = 'lucide-ellipsis';
}

View File

@ -8,30 +8,38 @@
enum InvoiceStatus: string implements HasColor, HasIcon, HasLabel
{
case UNPAID = 'Not paid';
case PAID = 'Paid';
case VOID = 'Void';
case UNPAID = 'not_paid';
case PARTIALLY_PAID = 'partially_paid';
case PAID = 'paid';
case VOID = 'void';
public function getLabel(): ?string
public function getLabel(): string
{
return $this->value;
return match ($this) {
self::UNPAID => 'Not paid',
self::PARTIALLY_PAID => 'Partially paid',
self::PAID => 'Paid',
self::VOID => 'Void',
};
}
public function getColor(): string|array|null
{
return match ($this) {
self::UNPAID => 'warning',
self::PAID => 'success',
self::VOID => 'gray'
self::UNPAID => 'danger',
self::PARTIALLY_PAID => 'warning',
self::PAID => 'success',
self::VOID => 'gray'
};
}
public function getIcon(): ?string
{
return match ($this) {
self::UNPAID => 'lucide-circle-x',
self::PAID => 'lucide-circle-check',
self::VOID => 'lucide-circle-slash',
self::UNPAID => IconEnum::UNPAID->value,
self::PARTIALLY_PAID => IconEnum::PARTIALLY_PAID->value,
self::PAID => IconEnum::PAID->value,
self::VOID => IconEnum::VOID->value,
};
}
}

View File

@ -23,13 +23,13 @@ public function getLabel(): ?string
public function getIcon(): ?string
{
return match ($this) {
self::new_art => 'lucide-brush',
self::repeat => 'lucide-files',
self::rush => 'lucide-bell-ring',
self::event => 'lucide-calendar-range',
self::digitizing => 'lucide-computer',
self::garments => 'lucide-shirt',
self::supplied_file => 'lucide-file-check',
self::new_art => IconEnum::NEW_ART->value,
self::repeat => IconEnum::REPEAT->value,
self::rush => IconEnum::RUSH->value,
self::event => IconEnum::EVENT->value,
self::digitizing => IconEnum::DIGITIZING->value,
self::garments => IconEnum::GARMENTS->value,
self::supplied_file => IconEnum::SUPPLIED_FILE->value,
};
}
}

View File

@ -8,36 +8,46 @@
enum OrderStatus: string implements HasColor, HasIcon, HasLabel
{
case DRAFT = 'Draft';
case APPROVED = 'Approved';
case PRODUCTION = 'Production';
case SHIPPED = 'Shipped';
case INVOICED = 'Invoiced';
case DRAFT = 'draft';
case APPROVED = 'approved';
case PRODUCTION = 'production';
case SHIPPED = 'shipped';
case READY_FOR_INVOICE = 'ready_for_invoice';
case INVOICED = 'invoiced';
public function getLabel(): ?string
public function getLabel(): string
{
return $this->value;
return match ($this) {
self::DRAFT => 'Draft',
self::APPROVED => 'Approved',
self::PRODUCTION => 'Production',
self::SHIPPED => 'Shipped',
self::READY_FOR_INVOICE => 'Ready for Invoice',
self::INVOICED => 'Invoiced',
};
}
public function getColor(): string|array|null
{
return match ($this) {
self::DRAFT => 'gray',
self::APPROVED => 'success',
self::PRODUCTION => 'primary',
self::SHIPPED => 'warning',
self::INVOICED => 'invoiced',
self::DRAFT => 'gray',
self::APPROVED => 'success',
self::PRODUCTION => 'primary',
self::SHIPPED => 'warning',
self::READY_FOR_INVOICE => 'invoicing',
self::INVOICED => 'invoiced',
};
}
public function getIcon(): ?string
{
return match ($this) {
self::DRAFT => 'lucide-pencil',
self::APPROVED => 'lucide-check-check',
self::PRODUCTION => 'lucide-refresh-cw',
self::SHIPPED => 'lucide-send',
self::INVOICED => 'lucide-credit-card',
self::DRAFT => IconEnum::DRAFT->value,
self::APPROVED => IconEnum::APPROVED->value,
self::PRODUCTION => IconEnum::PRODUCTION->value,
self::SHIPPED => IconEnum::SHIPPED->value,
self::READY_FOR_INVOICE => IconEnum::INVOICING->value,
self::INVOICED => IconEnum::INVOICED->value,
};
}
}

View File

@ -6,14 +6,20 @@
enum OrderType: string implements HasLabel
{
case EMB = 'Embroidery';
case SCP = 'Screen printing';
case DTG = 'Direct-to-garment';
case VINYL = 'Vinyl';
case MISC = 'Misc';
case EMB = 'embroidery';
case SCP = 'screen_printing';
case DTG = 'direct_to_garment';
case VINYL = 'vinyl';
case MISC = 'misc';
public function getLabel(): ?string
public function getLabel(): string
{
return $this->value;
return match ($this) {
self::EMB => 'Embroidery',
self::SCP => 'Screen printing',
self::DTG => 'Direct-to-garment',
self::VINYL => 'Vinyl',
self::MISC => 'Misc',
};
}
}

View File

@ -20,10 +20,10 @@ public function getLabel(): ?string
public function getIcon(): ?string
{
return match ($this) {
self::THEY_SHIP => 'lucide-truck',
self::WE_SHIP => 'lucide-house',
self::PICKUP => 'lucide-handshake',
self::OTHER => 'lucide-ellipsis'
self::THEY_SHIP => IconEnum::SHIPPING_ENTRY->value,
self::WE_SHIP => IconEnum::WE_SHIP->value,
self::PICKUP => IconEnum::PICKUP->value,
self::OTHER => IconEnum::SHIPPING_OTHER->value,
};
}
}

View File

@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\ContactResource\Pages;
use App\Enums\IconEnum;
use App\Filament\Resources\ContactResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
@ -13,7 +14,8 @@ class ListContacts extends ListRecords
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
Actions\CreateAction::make()
->icon(IconEnum::NEW->value),
];
}
}

View File

@ -4,15 +4,15 @@
use App\Enums\IconEnum;
use App\Models\Customer;
use App\Models\Invoice;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Support\Enums\FontWeight;
use Filament\Tables;
use Filament\Tables\Columns\Summarizers\Summarizer;
use Filament\Tables\Columns\Summarizers\Sum;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\Builder;
class CustomerReportResource extends Resource
{
@ -41,68 +41,66 @@ public static function table(Table $table): Table
Tables\Columns\TextColumn::make('company_name')
->label('Customer')
->sortable()
->searchable()
// ->searchable()
->extraHeaderAttributes(['class' => 'w-full']),
Tables\Columns\TextColumn::make('subtotal')
Tables\Columns\TextColumn::make('invoices.subtotal')
->label('Subtotal')
->money()
// ->summarize(Summarizer::make()->using(function ($query, Table $table) {
// $createdAt = $table->getfilter('created_at')->getstate()['created_at'] ?? '1900-01-01';
// $createdUntil = $table->getfilter('created_until')->getstate()['created_until'] ?? '2100-01-01';
//
// $invoiceSum = invoice::wherebetween('date', [$createdAt, $createdUntil])->sum('subtotal');
//
// return '$'.number_format(round($invoiceSum, 2), 2, '.', ',');
// }))
->alignRight()
->getStateUsing(function (Table $table, Model $record) {
return $record->getSubtotalAttribute(
$table->getFilter('created_at')->getState()['created_at'],
$table->getFilter('created_until')->getState()['created_until']
);
}),
->getStateUsing(fn (Table $table, Model $record) => $record->invoices()->tap(fn ($q) => self::applyDateFilters($q, $table))->sum('subtotal'))
->summarize(Sum::make('subtotal')
->label('')
->money()
->using(fn (Table $table, $query) => $query->tap(fn ($q) => self::applyDateFilters($q, $table))->sum('subtotal'))
),
Tables\Columns\TextColumn::make('gst')
Tables\Columns\TextColumn::make('invoices.hst_amount')
->label('HST')
->money()
->alignRight()
->getStateUsing(fn (Table $table, Model $record) => $record->invoices()->tap(fn ($q) => self::applyDateFilters($q, $table))->sum('hst_amount'))
->summarize(Sum::make('hst_amount')
->label('')
->money()
->using(fn (Table $table, Builder $query) => $query->tap(fn ($q) => self::applyDateFilters($q, $table))->sum('hst_amount'))
),
Tables\Columns\TextColumn::make('invoices.gst_amount')
->label('GST')
->money()
->alignRight()
->getStateUsing(function (Table $table, Model $record) {
return $record->getGstAttribute(
$table->getFilter('created_at')->getState()['created_at'],
$table->getFilter('created_until')->getState()['created_until']
);
}),
->getStateUsing(fn (Table $table, Model $record) => $record->invoices()->tap(fn ($q) => self::applyDateFilters($q, $table))->sum('gst_amount'))
->summarize(Sum::make('gst_amount')
->label('')
->money()
->using(fn (Table $table, $query) => $query->tap(fn ($q) => self::applyDateFilters($q, $table))->sum('gst_amount'))
),
Tables\Columns\TextColumn::make('pst')
Tables\Columns\TextColumn::make('invoices.pst_amount')
->label('PST')
->money()
->alignRight()
->getStateUsing(function (Table $table, Customer $record) {
return $record->getPstAttribute(
$table->getFilter('created_at')->getState()['created_at'],
$table->getFilter('created_until')->getState()['created_until']
);
}),
->getStateUsing(fn (Table $table, Model $record) => $record->invoices()->tap(fn ($q) => self::applyDateFilters($q, $table))->sum('pst_amount'))
->summarize(Sum::make('pst_amount')
->label('')
->money()
->using(fn (Table $table, $query) => $query->tap(fn ($q) => self::applyDateFilters($q, $table))->sum('pst_amount'))
),
Tables\Columns\TextColumn::make('total')
Tables\Columns\TextColumn::make('invoices.total')
->label('Total')
->money()
// ->summarize(summarizer::make()->using(function ($query, table $table) {
// $createdAt = $table->getfilter('created_at')->getstate()['created_at'] ?? '1900-01-01';
// $createdUntil = $table->getfilter('created_until')->getstate()['created_until'] ?? '2100-01-01';
//
// $invoiceSum = invoice::wherebetween('date', [$createdAt, $createdUntil])->sum('total');
//
// return '$'.number_format(round($invoiceSum, 2), 2, '.', ',');
// }))
->weight(FontWeight::Bold)
->alignRight()
->getStateUsing(function (Table $table, Model $record) {
return $record->getTotalAttribute(
$table->getFilter('created_at')->getState()['created_at'],
$table->getFilter('created_until')->getState()['created_until']
);
}),
->getStateUsing(fn (Table $table, Model $record) => $record->invoices()->tap(fn ($q) => self::applyDateFilters($q, $table))->sum('total'))
->summarize(Sum::make('total')
->label('')
->money()
->using(fn (Table $table, $query) => $query->tap(fn ($q) => self::applyDateFilters($q, $table))->sum('total'))
),
])
->filters([
Tables\Filters\Filter::make('created_at')
->form([
@ -115,11 +113,16 @@ public static function table(Table $table): Table
DatePicker::make('created_until')
->label('Until date'),
]),
])
->actions([
])
->bulkActions([
]);
], layout: Tables\Enums\FiltersLayout::AboveContent);
}
protected static function applyDateFilters($query, Table $table): void
{
$createdAt = $table->getFilter('created_at')?->getState()['created_at'] ?? null;
$createdUntil = $table->getFilter('created_until')?->getState()['created_until'] ?? null;
$query->when($createdAt, fn ($q, $date) => $q->whereDate('created_at', '>=', $date));
$query->when($createdUntil, fn ($q, $date) => $q->whereDate('created_at', '<=', $date));
}
public static function canAccess(): bool

View File

@ -8,6 +8,7 @@
use App\Filament\Admin\Resources\CustomerResource\RelationManagers\PaymentsRelationManager;
use App\Filament\Admin\Resources\CustomerResource\RelationManagers\ShippingEntriesRelationManager;
use App\Models\Customer;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
@ -29,16 +30,44 @@ class CustomerResource extends Resource
public static function form(Form $form): Form
{
return $form
->columns(1)
->schema([
Section::make([
TextInput::make('company_name')
->required(),
TextInput::make('phone'),
TextInput::make('shipping_address_line_1'),
TextInput::make('shipping_address_line_2'),
TextInput::make('billing_address_line_1'),
TextInput::make('billing_address_line_2'),
])->columns(2),
Fieldset::make('Primary Information')
->columns(1)
->columnSpan(fn (?Customer $record) => $record ? 1 : 3)
->schema([
TextInput::make('company_name')
->required(),
TextInput::make('phone'),
]),
Fieldset::make('Shipping Address')
->columns(1)
->columnSpan(fn (?Customer $record) => $record ? 1 : 3)
->schema([
TextInput::make('shipping_address_line_1')
->label('Line 1')
->placeholder('618 East Kent Ave S #108'),
TextInput::make('shipping_address_line_2')
->label('Line 2')
->placeholder('Vancouver, BC V5X 0B2, Canada'),
]),
Fieldset::make('Billing Address')
->columns(1)
->columnSpan(fn (?Customer $record) => $record ? 1 : 3)
->schema([
TextInput::make('billing_address_line_1')
->label('Line 1')
->placeholder('618 East Kent Ave S #108'),
TextInput::make('billing_address_line_2')
->label('Line 2')
->placeholder('Vancouver, BC V5X 0B2, Canada'),
]),
])
->columns(3)
->columnSpan(fn (?Customer $record) => $record ? 1 : 3),
]);
}
@ -54,7 +83,7 @@ public static function table(Table $table): Table
TextColumn::make('balance')
->getStateUsing(fn (Customer $customer) => $customer->calculateBalance())
->money()
->hidden(! auth()->user()->is_admin),
->hidden(! auth()->user()->is_admin ?? false),
])
->filters([
//
@ -83,8 +112,7 @@ 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'),
'edit' => \App\Filament\Admin\Resources\CustomerResource\Pages\EditCustomer::route('/{record}/edit'),
];
}
}

View File

@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\CustomerResource\Pages;
use App\Enums\IconEnum;
use App\Filament\Admin\Resources\CustomerResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
@ -13,7 +14,11 @@ class ListCustomers extends ListRecords
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
Actions\CreateAction::make()
->modal()
->modalWidth('lg')
->icon(IconEnum::NEW->value)
->successRedirectUrl(fn ($record) => CustomerResource::getUrl('edit', ['record' => $record->id])),
];
}
}

View File

@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\CustomerResource\RelationManagers;
use App\Enums\IconEnum;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
@ -41,7 +42,8 @@ public function table(Table $table): Table
//
])
->headerActions([
Tables\Actions\CreateAction::make(),
Tables\Actions\CreateAction::make()
->icon(IconEnum::NEW->value),
])
->actions([
Tables\Actions\EditAction::make(),

View File

@ -6,8 +6,8 @@
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
class PaymentsRelationManager extends RelationManager
{
@ -27,14 +27,10 @@ public function form(Form $form): Form
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(),
]);*/
public static function canViewForRecord(Model $ownerRecord, string $pageClass): bool
{
return auth()->user()->is_admin ?? false;
}
}

View File

@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\CustomerResource\RelationManagers;
use App\Enums\IconEnum;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
@ -40,7 +41,8 @@ public function table(Table $table): Table
//
])
->headerActions([
Tables\Actions\CreateAction::make(),
Tables\Actions\CreateAction::make()
->icon(IconEnum::NEW->value),
])
->actions([
// Tables\Actions\EditAction::make(),

View File

@ -3,8 +3,12 @@
namespace App\Filament\Admin\Resources;
use App\Enums\IconEnum;
use App\Enums\InvoiceStatus;
use App\Filament\Admin\Resources\InvoiceReportResource\RelationManagers\InvoicesRelationManager;
use App\Models\InvoiceReport;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Group;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\ToggleButtons;
@ -30,28 +34,47 @@ public static function form(Form $form): Form
return $form
->schema([
Section::make([
Select::make('customer_id')
->relationship('customer', 'company_name')
->preload()
->required()
->searchable(),
ToggleButtons::make('filter_paid')
->boolean()
->required()
->default(false)
->colors([
'true' => 'info',
'false' => 'info',
])
->inline(),
DatePicker::make('date_start')
->required(),
DatePicker::make('date_end')
->required()
->default(today()),
Group::make([
Select::make('customer_id')
->relationship('customer', 'company_name')
->preload()
->required()
->columnSpanFull()
->searchable(),
ToggleButtons::make('payment_types')
->required()
->options(InvoiceStatus::class)
->multiple()
->columnSpanFull()
->inline(),
DatePicker::make('date_start')
->required()
->columnSpan(1),
DatePicker::make('date_end')
->required()
->default(today())
->columnSpan(1),
])->columnSpan(fn (?InvoiceReport $record) => $record === null ? 5 : 3)
->columns(2),
])
->columns(2)
->columnSpan(3),
->columns(5)
->columnSpan(fn ($record) => $record === null ? 3 : 2),
Section::make([
Placeholder::make('created_at')
->label('Created')
->content(fn (InvoiceReport $record): ?string => $record->created_at?->diffForHumans()),
Placeholder::make('updated_at')
->label('Last modified')
->content(fn (InvoiceReport $record): ?string => $record->updated_at?->diffForHumans()),
])
->columnSpan(1)
->hidden(fn (?InvoiceReport $record) => $record === null)
->extraAttributes(['class' => 'h-full']),
])->columns(3);
}
@ -80,7 +103,6 @@ public static function table(Table $table): Table
TextColumn::make('balance')
->weight(FontWeight::Bold)
->money(),
// ->getStateUsing(fn (Invoice))
])
->defaultSort('id', 'desc');
}
@ -100,9 +122,9 @@ public static function getRelations(): array
public static function getPages(): array
{
return [
'index' => \App\Filament\Admin\Resources\InvoiceReportResource\Pages\ListInvoiceReports::route('/'),
'create' => \App\Filament\Admin\Resources\InvoiceReportResource\Pages\CreateInvoiceReport::route('/create'),
'view' => \App\Filament\Admin\Resources\InvoiceReportResource\Pages\ViewInvoiceReport::route('/{record}'),
'index' => \App\Filament\Admin\Resources\InvoiceReportResource\Pages\ListInvoiceReports::route('/'),
// 'create' => \App\Filament\Admin\Resources\InvoiceReportResource\Pages\CreateInvoiceReport::route('/create'),
'view' => \App\Filament\Admin\Resources\InvoiceReportResource\Pages\ViewInvoiceReport::route('/{record}'),
];
}
}

View File

@ -2,10 +2,6 @@
namespace App\Filament\Admin\Resources\InvoiceReportResource\Pages;
use App\Filament\Admin\Resources\InvoiceReportResource;
use Filament\Resources\Pages\CreateRecord;
class CreateInvoiceReport extends CreateRecord
{
protected static string $resource = InvoiceReportResource::class;
}
class CreateInvoiceReport extends CreateRecord {}

View File

@ -2,6 +2,8 @@
namespace App\Filament\Admin\Resources\InvoiceReportResource\Pages;
use App\Enums\IconEnum;
use App\Enums\InvoiceStatus;
use App\Filament\Admin\Resources\InvoiceReportResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
@ -15,7 +17,32 @@ class ListInvoiceReports extends ListRecords
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
Actions\CreateAction::make()
->modalWidth('xl')
->icon(IconEnum::NEW->value)
->mutateFormDataUsing(function ($data) {
/* Initialize all payment statues to false,
map selected payment types to corresponding status,
assign filtered statuses to specific keys */
$paymentTypes = array_fill_keys(array_map(fn ($status) => $status->name, InvoiceStatus::cases()), false);
if (! empty($data['payment_types'])) {
foreach ($data['payment_types'] as $type) {
$statusName = InvoiceStatus::from($type)->name;
$paymentTypes[$statusName] = true;
}
}
foreach ($paymentTypes as $status => $value) {
$data['with_'.strtolower($status)] = $value;
}
unset($data['payment_types']);
return $data;
})
->successRedirectUrl(fn ($record) => InvoiceReportResource::getUrl('view', ['record' => $record->id])),
];
}
}

View File

@ -2,10 +2,12 @@
namespace App\Filament\Admin\Resources\InvoiceReportResource\Pages;
use App\Enums\InvoiceStatus;
use App\Filament\Admin\Resources\InvoiceReportResource;
use App\Models\InvoiceReport;
use Filament\Actions\Action;
use Filament\Resources\Pages\ViewRecord;
use Illuminate\Contracts\Support\Htmlable;
class ViewInvoiceReport extends ViewRecord
{
@ -13,6 +15,24 @@ class ViewInvoiceReport extends ViewRecord
protected static ?string $title = 'View Invoice Report';
public function getTitle(): string|Htmlable
{
return parent::getTitle().' '.$this->record->internal_id;
}
public function mutateFormDataBeforeFill(array $data): array
{
foreach (InvoiceStatus::cases() as $case) {
$name = 'with_'.strtolower($case->name);
if ($data[$name]) {
$data['payment_types'][] = $case->value ?? null;
}
}
return $data;
}
protected function getHeaderActions(): array
{
return [

View File

@ -35,22 +35,37 @@ public function table(Table $table): Table
->label('ID')
->extraHeaderAttributes(['class' => 'w-full'])
->color('primary'),
TextColumn::make('date')
->label('Created')
->date(),
->date('Y-m-d'),
TextColumn::make('subtotal')
->alignRight()
->money(),
TextColumn::make('gst_amount')
->label('GST')
->label('GST/HST')
->getStateUsing(function (Invoice $record) {
return $record->has_gst
? '$'.number_format($record->gst_amount, 2)
: ($record->has_hst ? '$'.number_format($record->hst_amount, 2) : '-');
})
->alignRight()
->money(),
TextColumn::make('pst_amount')
->label('PST')
->alignRight()
->formatStateUsing(function ($state) {
return $state == 0.00 ? '-' : '$'.$state;
}),
TextColumn::make('total')
->money()
->alignRight()
->weight(FontWeight::Medium),
TextColumn::make('balance')
->alignRight()
->getStateUsing(fn (Invoice $record) => $record->remainingBalance())

View File

@ -6,9 +6,11 @@
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\PaymentsRelationManager;
use App\Filament\Admin\Resources\InvoiceResource\RelationManagers\ProductServicesRelationManager;
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\Payment;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Group;
@ -16,12 +18,14 @@
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Split;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Support\Enums\FontWeight;
use Filament\Tables;
use Filament\Tables\Actions\BulkAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
@ -44,66 +48,53 @@ public static function form(Form $form): Form
Group::make()
->schema([
Section::make([
Select::make('customer_id')
->required()
->label('Customer')
->options(Customer::all()->pluck('company_name', 'id'))
->reactive()
->searchable()
->disabledOn('edit')
->columnSpan(2),
Group::make([
Split::make([
DatePicker::make('date')
Select::make('customer_id')
->required()
->default(today()),
DatePicker::make('due_date'),
])
->columnSpan(2),
->label('Customer')
->options(Customer::all()->pluck('company_name', 'id'))
->reactive()
->searchable()
->disabledOn('edit')
->columnSpan(2),
Grid::make(3)
->schema([
ToggleButtons::make('has_gst')
->label('GST')
->boolean('On', 'Off')
->default(true)
// ->inline()
->colors([
'true' => 'info',
'false' => 'info',
]),
Split::make([
DatePicker::make('date')
->required()
->default(today()),
DatePicker::make('due_date'),
])->columnSpan(2),
ToggleButtons::make('has_pst')
->label('PST')
->boolean('On', 'Off')
->default(false)
// ->inline()
->colors([
'true' => 'info',
'false' => 'info',
]),
ToggleButtons::make('status')
->options(InvoiceStatus::class)
->required()
->inline()
->default(InvoiceStatus::UNPAID)
->columnSpan(2),
ToggleButtons::make('has_hst')
->label('HST')
->boolean('On', 'Off')
->default(false)
// ->inline()
->colors([
'true' => 'info',
'false' => 'info',
]),
])->columnSpan(1),
Grid::make(3)
->schema([
Toggle::make('has_gst')
->label('GST')
->inline(false)
->default(true),
ToggleButtons::make('status')
->options(InvoiceStatus::class)
->required()
->inline()
->default(InvoiceStatus::UNPAID)
->columnSpan(1),
Toggle::make('has_pst')
->label('PST')
->inline(false)
->default(false),
Toggle::make('has_hst')
->label('HST')
->inline(false)
->default(false),
]),
])->columnSpan(fn (?Invoice $record) => $record === null ? 2 : 1),
])
->columns(2)
->columnSpan(2),
->columnSpan(fn (?Invoice $record) => $record === null ? 3 : 2),
Section::make()
->schema([
@ -159,40 +150,60 @@ public static function table(Table $table): Table
TextColumn::make('created_at')
->label('Created')
->date()
->date('Y-m-d')
->searchable()
->sortable(),
TextColumn::make('subtotal')
->money()
->alignRight(),
->alignRight()
->sortable()
->searchable(),
TextColumn::make('has_gst')
->label('GST')
// FIXME: sortable doesn't sort correctly
TextColumn::make('gst_amount')
->label('GST/HST')
->money()
->formatStateUsing(function (Invoice $record) {
if ($record->has_gst) {
return '$'.number_format($record->gst_amount, 2);
}
return '-';
->getStateUsing(function (Invoice $record) {
return $record->has_gst
? '$'.number_format($record->gst_amount, 2)
: ($record->has_hst ? '$'.number_format($record->hst_amount, 2) : '-');
})
->alignRight(),
->alignRight()
->searchable(query: function (Builder $query, string $search) {
$query->where(function ($query) use ($search) {
$query->where('hst_amount', 'like', "%{$search}%")
->orWhere('gst_amount', 'like', "%{$search}%");
});
}),
// ->sortable(query: function (Builder $query, string $direction) {
// $query->orderByRaw("COALESCE(hst_amount, gst_amount, 0) $direction");
// }),
TextColumn::make('has_pst')
TextColumn::make('pst_amount')
->label('PST')
->formatStateUsing(function (Invoice $record) {
if ($record->has_pst) {
return '$'.number_format($record->pst_amount, 2);
}
return '-';
->getStateUsing(function (Invoice $record) {
return $record->has_pst ? '$'.number_format($record->pst_amount, 2) : '-';
})
->alignRight(),
->alignRight()
->sortable()
->searchable(),
TextColumn::make('total')
->money()
->alignRight()
->weight(FontWeight::Medium)
->sortable()
->searchable(),
TextColumn::make('balance')
->getStateUsing(fn (Invoice $record) => $record->remainingBalance())
->searchable(query: fn (Builder $query, string $search) => $query->searchByBalance($search))
->label('Balance')
->money()
->alignRight()
->weight(FontWeight::Bold),
TextColumn::make('status')
->badge(InvoiceStatus::class)
->sortable(),
@ -235,18 +246,48 @@ public static function table(Table $table): Table
])
->bulkActions([
Tables\Actions\BulkAction::make('Mark as paid')
->action(function (Collection $records) {
$records->each->setStatus(InvoiceStatus::PAID);
BulkAction::make('Create Payment')
->icon(IconEnum::PAYMENTS->value)
->form(fn ($form) => PaymentResource::form($form))
->action(function (Collection $records, array $data) {
if ($records->pluck('customer_id')->unique()->count() !== 1) {
Notification::make()
->title('Invalid order combination')
->body('Make sure all orders are from the same customer')
->danger()
->send();
return;
}
$payment = Payment::create([
'customer_id' => $records->pluck('customer_id')->first(),
'amount' => $data['amount'],
'date' => $data['date'],
'check_number' => $data['check_number'],
'notes' => $data['notes'],
]);
$payment->applyToInvoices($records);
Notification::make()
->title(count($records).' item(s) saved successfully')
->title('Payment created successfully')
->success()
->send();
})
->icon('lucide-circle-check')
->deselectRecordsAfterCompletion(),
}),
Tables\Actions\BulkActionGroup::make([
Tables\Actions\BulkAction::make('Mark as paid')
->action(function (Collection $records) {
$records->each->setStatus(InvoiceStatus::PAID);
Notification::make()
->title(count($records).' item(s) saved successfully')
->success()
->send();
})
->icon('lucide-circle-check')
->deselectRecordsAfterCompletion(),
Tables\Actions\BulkAction::make('Mark as unpaid')
->action(function (Collection $records) {
$records->each->setStatus(InvoiceStatus::UNPAID);
@ -274,15 +315,16 @@ public static function getRelations(): array
return [
OrdersRelationManager::class,
ProductServicesRelationManager::class,
PaymentsRelationManager::class,
];
}
public static function getPages(): array
{
return [
'index' => \App\Filament\Admin\Resources\InvoiceResource\Pages\ListInvoices::route('/'),
'create' => \App\Filament\Admin\Resources\InvoiceResource\Pages\CreateInvoice::route('/create'),
'edit' => \App\Filament\Admin\Resources\InvoiceResource\Pages\EditInvoice::route('/{record}/edit'),
'index' => \App\Filament\Admin\Resources\InvoiceResource\Pages\ListInvoices::route('/'),
// 'create' => \App\Filament\Admin\Resources\InvoiceResource\Pages\CreateInvoice::route('/create'),
'edit' => \App\Filament\Admin\Resources\InvoiceResource\Pages\EditInvoice::route('/{record}/edit'),
];
}
}

View File

@ -7,11 +7,17 @@
use Filament\Actions;
use Filament\Actions\Action;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Contracts\Support\Htmlable;
class EditInvoice extends EditRecord
{
protected static string $resource = InvoiceResource::class;
public function getTitle(): string|Htmlable
{
return parent::getTitle().' '.$this->record->internal_id;
}
protected function getHeaderActions(): array
{
return [

View File

@ -23,6 +23,10 @@ public function getTabs(): array
->query(fn ($query) => $query->where('status', InvoiceStatus::UNPAID))
->icon(InvoiceStatus::UNPAID->getIcon()),
'partially_paid' => Tab::make('Partially Paid')
->query(fn ($query) => $query->where('status', InvoiceStatus::PARTIALLY_PAID))
->icon(InvoiceStatus::PARTIALLY_PAID->getIcon()),
'paid' => Tab::make('Paid')
->query(fn ($query) => $query->where('status', InvoiceStatus::PAID))
->icon(InvoiceStatus::PAID->getIcon()),
@ -37,7 +41,11 @@ public function getTabs(): array
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
Actions\CreateAction::make()
->modal()
->modalWidth('lg')
->icon(IconEnum::NEW->value)
->successRedirectUrl(fn ($record) => InvoiceResource::getUrl('edit', ['record' => $record->id])),
];
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Filament\Admin\Resources\InvoiceResource\RelationManagers;
use App\Filament\Admin\Resources\PaymentResource;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables\Table;
class PaymentsRelationManager extends RelationManager
{
protected static string $relationship = 'payments';
public function form(Form $form): Form
{
return PaymentResource::form($form);
}
public function table(Table $table): Table
{
return PaymentResource::paymentRelationManagerTable($table)
->recordTitleAttribute('date');
}
}

View File

@ -3,10 +3,11 @@
namespace App\Filament\Admin\Resources;
use App\Enums\IconEnum;
use App\Enums\InvoiceStatus;
use App\Enums\OrderAttributes;
use App\Enums\OrderStatus;
use App\Enums\OrderType;
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\Order;
use App\Models\OrderProduct;
use App\Models\ProductService;
@ -28,6 +29,8 @@
use Filament\Resources\Resource;
use Filament\Support\Enums\MaxWidth;
use Filament\Tables;
use Filament\Tables\Actions\BulkAction;
use Filament\Tables\Actions\BulkActionGroup;
use Filament\Tables\Columns\IconColumn\IconColumnSize;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@ -57,11 +60,13 @@ public static function form(Form $form): Form
->options(OrderType::class)
->searchable(),
// Split::make([
Select::make('customer_id')
->required()
->label('Customer')
->options(Customer::all()->pluck('company_name', 'id'))
->relationship(name: 'customer', titleAttribute: 'company_name')
->preload()
->createOptionForm(fn ($form) => CustomerResource::form($form))
->createOptionAction(fn ($action) => $action->modalWidth('lg'))
->searchable(),
TextInput::make('customer_po')
@ -181,6 +186,7 @@ public static function form(Form $form): Form
->defaultItems(1),
Repeater::make('services')
->view('filament.forms.compact-repeater')
->label('Product Services')
->schema([
Grid::make(19)
@ -202,13 +208,13 @@ public static function form(Form $form): Form
->createOptionUsing(function (array $data): int {
return ServiceType::create($data)->getKey();
}),
TextInput::make('placement')
->datalist(ProductService::all()->unique('placement')->pluck('placement')->toArray())
->columnSpan(3),
TextInput::make('serviceFileName')
->datalist(ServiceFile::all()->unique('name')->pluck('name')->toArray())
->columnSpan(3)
->label('Logo Name'),
TextInput::make('placement')
->datalist(ProductService::all()->unique('placement')->pluck('placement')->toArray())
->columnSpan(3),
TextInput::make('serviceFileSetupNumber')
->label('Setup')
->columnSpan(1)
@ -343,7 +349,6 @@ public static function table(Table $table): Table
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkAction::make('updateStatus')
->form([
Select::make('status')
@ -366,16 +371,93 @@ public static function table(Table $table): Table
->color('info')
->deselectRecordsAfterCompletion(),
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
BulkActionGroup::make([
BulkAction::make('Create individual invoices')
->icon(IconEnum::INVOICE->value)
->action(function (Collection $records): void {
[$invoiced, $toInvoice] = $records->partition(fn ($record) => $record->invoice);
public static function getRelations(): array
{
return [
];
$toInvoice->each(function ($record) {
$invoice = Invoice::create([
'customer_id' => $record->customer->id,
'date' => today(),
'status' => InvoiceStatus::UNPAID->value,
]);
$invoice->orders()->save($record);
$invoice->calculateTotals();
$record->update(['status' => OrderStatus::INVOICED->value]);
});
if ($invoiced->isNotEmpty()) {
Notification::make()
->title("{$invoiced->count()} orders are already invoiced")
->warning()
->send();
}
if ($toInvoice->isNotEmpty()) {
Notification::make()
->title("Successfully created {$toInvoice->count()} invoice(s)")
->success()
->send();
}
}),
BulkAction::make('Add all to new invoice')
->icon(IconEnum::REPEAT->value)
->action(function (Collection $records): void {
if ($records->pluck('customer_id')->unique()->count() !== 1) {
Notification::make()
->title('Invalid order combination')
->body('Make sure all orders are from the same customer')
->danger()
->send();
return;
}
[$invoiced, $validOrders] = $records->partition(fn ($record) => $record->invoice);
if ($validOrders->isNotEmpty()) {
$invoice = Invoice::create([
'customer_id' => $records->first()->customer_id,
'date' => today(),
'status' => InvoiceStatus::UNPAID->value,
]);
$invoice->orders()->saveMany($validOrders);
$invoice->calculateTotals(); // FIXME: Investigate why this is needed.
Order::whereIn('id', $validOrders->pluck('id'))->update([
'status' => OrderStatus::INVOICED->value,
]);
}
if ($invoiced->isNotEmpty()) {
Notification::make()
->title('Some orders are already invoiced')
->body("{$invoiced->count()} orders are already invoiced and will not be added")
->warning()
->send();
}
if ($validOrders->isNotEmpty()) {
Notification::make()
->title('Invoice created')
->body("{$validOrders->count()} orders have been added to this invoice")
->success()
->send();
}
}),
])
->label('Invoicing')
->hidden(fn () => ! auth()->user()->is_admin),
BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
])->label('Other actions'),
]);
}
public static function getPages(): array

View File

@ -2,9 +2,13 @@
namespace App\Filament\Admin\Resources\OrderResource\Pages;
use App\Enums\IconEnum;
use App\Enums\InvoiceStatus;
use App\Enums\OrderAttributes;
use App\Enums\OrderStatus;
use App\Filament\Admin\Resources\InvoiceResource;
use App\Filament\Admin\Resources\OrderResource;
use App\Models\Invoice;
use App\Models\Order;
use App\Models\OrderProduct;
use App\Models\ProductService;
@ -13,13 +17,20 @@
use App\Models\ServiceType;
use Filament\Actions;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Model;
class EditOrder extends EditRecord
{
protected static string $resource = OrderResource::class;
public function getTitle(): string|Htmlable
{
return parent::getTitle().' '.$this->record->internal_po;
}
protected function mutateFormDataBeforeFill(array $data): array
{
$order = Order::findOrFail($data['id']);
@ -148,12 +159,11 @@ protected function getHeaderActions(): array
Action::make('save')
->label('Save changes')
->action('save')
->icon('lucide-save'),
->icon(IconEnum::SAVE->value),
Actions\ReplicateAction::make()
->label('Duplicate')
->icon('lucide-copy')
->color('info')
->icon(IconEnum::COPY->value)
->mutateRecordDataUsing(function (array $data): array {
$po = 'Duplicate of '.$data['customer_po'];
$data['customer_po'] = $po;
@ -195,23 +205,37 @@ protected function getHeaderActions(): array
})
->successRedirectUrl(fn (Model $replica): string => OrderResource::getUrl('edit', [$replica])),
// Action::make('invoice')
// ->visible(fn () => auth()->user()->is_admin)
// ->label('To Invoice')
// ->icon('lucide-receipt-text'),
//
Action::make('invoice')
->visible(fn () => auth()->user()->is_admin)
->label(fn (Order $record) => $record->invoice()->exists() ? 'To Invoice' : 'Make Invoice')
->icon(IconEnum::INVOICE->value)
->action(function (Order $record) {
if ($record->invoice()->exists()) {
return redirect()->to(InvoiceResource::getUrl('edit', ['record' => $record->invoice->id]));
}
$invoice = Invoice::create([
'customer_id' => $record->customer_id,
'date' => today(),
'status' => InvoiceStatus::UNPAID->value,
]);
$invoice->orders()->save($record);
return Notification::make()
->title('Invoice '.$invoice->internal_id.' created successfully')
->body('Click the button again to go to the invoice')
->success()
->send();
}),
Action::make('print')
->icon('lucide-printer')
->icon(IconEnum::PRINT->value)
->url(fn (Order $record) => route('orders.pdf', $record))
->openUrlInNewTab(),
Actions\DeleteAction::make()
->icon('lucide-trash-2'),
->icon(IconEnum::TRASH->value),
];
}
// protected function getRedirectUrl(): string
// {
// return $this->previousUrl ?? $this->getResource()::getUrl('index');
// }
}

View File

@ -15,77 +15,69 @@ class ListOrders extends ListRecords
{
protected static string $resource = OrderResource::class;
private function excludeStatuses($query): mixed
{
return $query
->whereNot('status', OrderStatus::DRAFT)
->whereNot('status', OrderStatus::READY_FOR_INVOICE)
->whereNot('status', OrderStatus::INVOICED)
->whereNot('status', OrderStatus::SHIPPED);
}
private function getBadgeCount(callable $queryCallback): ?int
{
$count = Order::query()->when(true, $queryCallback)
->whereNot('status', OrderStatus::DRAFT)
->whereNot('status', OrderStatus::READY_FOR_INVOICE)
->whereNot('status', OrderStatus::INVOICED)
->whereNot('status', OrderStatus::SHIPPED)
->count();
return $count > 0 ? $count : null;
}
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
Actions\CreateAction::make()
->icon(IconEnum::NEW->value),
];
}
public function getTabs(): array
{
return [
'all' => Tab::make('All')
->icon(IconEnum::TAB_ALL->value),
'active' => Tab::make()
->query(function ($query) {
return $query
->whereNot('status', OrderStatus::INVOICED)
->whereNot('status', ORderStatus::SHIPPED);
})
->query(fn ($query) => $this->excludeStatuses($query))
->icon(OrderStatus::PRODUCTION->getIcon())
->badge(function () {
return Order::whereNot('status', OrderStatus::SHIPPED)
->whereNot('status', OrderStatus::INVOICED)
->count();
}),
->badge(fn () => $this->getBadgeCount(fn ($query) => $this->excludeStatuses($query))),
'unprinted' => Tab::make()
->query(function ($query) {
return $query->where('printed', false);
})
->icon(IconEnum::TAB_UNPRINTED->value)
->badge(function () {
$count = Order::where('printed', false)->count();
return $count > 0 ? $count : null;
})
->query(fn ($query) => $this->excludeStatuses($query)->where('printed', false))
->icon(IconEnum::PRINT->value)
->badge(fn () => $this->getBadgeCount(fn ($query) => $query->where('printed', false)))
->badgeColor('success'),
'overdue' => Tab::make()
->query(function ($query) {
return $query->whereDate('due_date', '<=', today())
->whereNot('status', OrderStatus::INVOICED)
->whereNot('status', ORderStatus::SHIPPED);
})
->query(fn ($query) => $this->excludeStatuses($query)->whereDate('due_date', '<=', today()))
->icon(IconEnum::TAB_OVERDUE->value)
->badge(function () {
$count = Order::whereDate('due_date', '<=', today())
->whereNot('status', OrderStatus::INVOICED)
->whereNot('status', ORderStatus::SHIPPED)
->count();
return $count > 0 ? $count : null;
})
->badge(fn () => $this->getBadgeCount(fn ($query) => $query->whereDate('due_date', '<=', today())))
->badgeColor('danger'),
'rush' => Tab::make()
->query(function ($query) {
return $query->where('rush', true)
->whereNot('status', OrderStatus::INVOICED)
->whereNot('status', OrderStatus::SHIPPED);
})
->query(fn ($query) => $this->excludeStatuses($query)->where('rush', true))
->icon(OrderAttributes::rush->getIcon())
->badge(function () {
$count = Order::where('rush', true)
->whereNot('status', OrderStatus::INVOICED)
->whereNot('status', OrderStatus::SHIPPED)
->count();
return $count > 0 ? $count : null;
})
->badge(fn () => $this->getBadgeCount(fn ($query) => $query->where('rush', true)))
->badgeColor('warning'),
'all' => Tab::make('All')
->icon(IconEnum::TAB_ALL->value),
'ready_for_invoice' => Tab::make()
->query(fn ($query) => $query->where('status', OrderStatus::READY_FOR_INVOICE))
->icon(OrderStatus::READY_FOR_INVOICE->getIcon())
->badge(fn () => ($count = Order::query()->where('status', OrderStatus::READY_FOR_INVOICE)->count()) > 0 ? $count : null)
->badgeColor(OrderStatus::READY_FOR_INVOICE->getColor()),
];
}
}

View File

@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\OrderResource\RelationManagers;
use App\Enums\IconEnum;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
@ -35,15 +36,18 @@ public function table(Table $table): Table
//
])
->headerActions([
Tables\Actions\CreateAction::make(),
Tables\Actions\CreateAction::make()
->icon(IconEnum::NEW->value),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
Tables\Actions\DeleteAction::make()
->icon(IconEnum::TRASH->value),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
Tables\Actions\DeleteBulkAction::make()
->icon(IconEnum::TRASH->value),
]),
]);
}

View File

@ -46,7 +46,6 @@ public static function form(Form $form): Form
->searchable(),
Select::make('order_id')
->label('Order')
->required()
->options(fn ($get): array => Order::where('customer_id', $get('customer_id') ?? null)
->get()
->pluck('customer_po', 'id')

View File

@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\PackingSlipResource\Pages;
use App\Enums\IconEnum;
use App\Filament\Admin\Resources\PackingSlipResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
@ -13,7 +14,8 @@ class EditPackingSlip extends EditRecord
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
Actions\DeleteAction::make()
->icon(IconEnum::TRASH->value),
];
}
}

View File

@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\PackingSlipResource\Pages;
use App\Enums\IconEnum;
use App\Filament\Admin\Resources\PackingSlipResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
@ -13,7 +14,8 @@ class ListPackingSlips extends ListRecords
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
Actions\CreateAction::make()
->icon(IconEnum::NEW->value),
];
}
}

View File

@ -2,16 +2,20 @@
namespace App\Filament\Admin\Resources;
use App\Filament\Admin\Resources\CustomerResource\RelationManagers\PaymentsRelationManager;
use App\Enums\IconEnum;
use App\Filament\Admin\Resources\InvoiceResource\Pages\ListInvoices;
use App\Filament\Admin\Resources\PaymentResource\Pages;
use App\Filament\Admin\Resources\PaymentResource\RelationManagers\InvoicesRelationManager;
use App\Models\Payment;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Group;
use Filament\Forms\Components\Placeholder;
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\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@ -19,7 +23,7 @@ class PaymentResource extends Resource
{
protected static ?string $model = Payment::class;
protected static ?string $navigationIcon = 'lucide-hand-coins';
protected static ?string $navigationIcon = IconEnum::PAYMENTS->value;
protected static ?string $navigationGroup = 'Financial';
@ -30,61 +34,108 @@ 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'),
]),
Group::make([
Select::make('customer_id')
->relationship('customer', 'company_name')
->required()
->searchable()
->hidden(fn ($livewire) => $livewire::class === ListInvoices::class)
->preload()
->columnSpanFull(),
TextInput::make('amount')
->required()
->prefix('$')
->rules('numeric')
->minValue(0)
->maxValue(99999999)
->columnSpan(3),
TextInput::make('check_number')
->columnSpan(6),
DatePicker::make('date')
->default(today())
->columnSpan(4),
Placeholder::make('break_2')->columnSpan(3)->hiddenLabel(),
Textarea::make('notes')
->columnSpanFull(),
])->columnSpan(fn (?Payment $record) => $record === null ? 9 : 3)
->columns(9),
])->columns(9),
]);
}
public static function table(Table $table): Table
public static function table(Table $table, ?bool $showSearchable = true): Table
{
return $table
->columns([
TextColumn::make('created_at')
->label('Date')
->date('Y-m-d')
->searchable(),
->searchable($showSearchable),
TextColumn::make('customer.company_name')
->hidden(fn ($livewire) => $livewire::class === PaymentsRelationManager::class)
->searchable(),
->hidden(fn ($livewire) => $livewire::class !== Pages\ListPayments::class)
->searchable($showSearchable),
TextColumn::make('notes')
->limit(100)
TextColumn::make('check_number')
->searchable($showSearchable)
->extraHeaderAttributes(['class' => 'w-full']),
TextColumn::make('amount')
->searchable()
->numeric()
->searchable($showSearchable)
->alignRight()
->money(),
TextColumn::make('unapplied_amount')
->searchable($showSearchable)
->label('Balance')
->alignRight()
->money(),
])
->actions([
ViewAction::make(),
\Filament\Tables\Actions\EditAction::make(),
]);
}
public static function paymentRelationManagerTable(Table $table): Table
{
return $table
->columns([
TextColumn::make('id')
->color('primary'),
TextColumn::make('created_at')
->label('Date')
->date('Y-m-d'),
TextColumn::make('check_number')
->extraHeaderAttributes(['class' => 'w-full']),
TextColumn::make('amount')
->label('Total amount')
->alignRight()
->money(),
TextColumn::make('applied_amount')
->alignRight()
->money(),
]);
}
public static function canAccess(): bool
{
return auth()->user()->is_admin;
return auth()->user()->is_admin ?? false;
}
public static function getRelations(): array
{
return [
//
InvoicesRelationManager::class,
];
}
@ -92,9 +143,7 @@ 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'),
'edit' => Pages\EditPayment::route('/{record}/edit'),
];
}
}

View File

@ -16,7 +16,7 @@ class ListPayments extends ListRecords
protected function getHeaderActions(): array
{
return [
Actions\Action::make('distributePayments')
/* Actions\Action::make('distributePayments')
->icon(IconEnum::DISTRIBUTE_PAYMENTS->value)
->action(function (PaymentService $paymentService) {
$paymentService->distributePayments();
@ -26,9 +26,12 @@ protected function getHeaderActions(): array
->body('Payments have been distributed')
->success()
->send();
}),
}),*/
Actions\CreateAction::make(),
Actions\CreateAction::make()
->modalWidth('lg')
->icon(IconEnum::NEW->value)
->successRedirectUrl(fn ($record) => PaymentResource::getUrl('edit', ['record' => $record->id])),
];
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Filament\Admin\Resources\PaymentResource\RelationManagers;
use App\Filament\Admin\Resources\InvoiceResource;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class InvoicesRelationManager extends RelationManager
{
protected static string $relationship = 'invoices';
public function form(Form $form): Form
{
return $form
->schema([
TextInput::make('internal_id')
->required()
->maxLength(255),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('internal_id')
->columns([
TextColumn::make('internal_id')
->color('primary')
->url(fn ($record) => InvoiceResource::getUrl('edit', ['record' => $record->id])),
]);
}
}

View File

@ -7,6 +7,7 @@
use App\Models\Quote;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Group;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
@ -36,24 +37,28 @@ public static function form(Form $form): Form
Grid::make(3)
->schema([
Section::make([
Select::make('customer_id')
->required()
->label('Customer')
->options(Customer::all()->pluck('company_name', 'id'))
->reactive()
->searchable()
->columnSpan(1),
Group::make([
DatePicker::make('date')
->default(today())
->required(),
Select::make('customer_id')
->required()
->label('Customer')
->options(Customer::all()->pluck('company_name', 'id'))
->reactive()
->searchable()
->columnSpan(1),
TextArea::make('notes')
->rows(3)
->columnSpan(2),
DatePicker::make('date')
->default(today())
->required(),
TextArea::make('notes')
->rows(3)
->columnSpan(2),
]),
])
->columns(2)
->columnSpan(fn (?Quote $record) => $record === null ? 3 : 2),
->columnSpan(fn (?Quote $record) => $record === null ? 3 : 2)
->extraAttributes(['class' => 'h-full']),
Section::make()
->schema([
@ -69,7 +74,8 @@ public static function form(Form $form): Form
])
->columnSpan(1)
->hidden(fn (?Quote $record) => $record === null),
->hidden(fn (?Quote $record) => $record === null)
->extraAttributes(['class' => 'h-full']),
]),
TableRepeater::make('embroideryEntries')
@ -115,30 +121,24 @@ public static function form(Form $form): Form
TextInput::make('logo')
->label('Logo name')
->columnSpan(2),
TextInput::make('placement'),
TextInput::make('quantity')
->rules(['numeric']),
->rules(['numeric'])
->label('Qty'),
TextInput::make('width')
->rules('numeric'),
TextInput::make('height')
->rules('numeric'),
TextInput::make('setup_amount')
->label('Setup qty')
->rules('numeric'),
TextInput::make('color_amount')
->label('Color qty')
->rules('numeric'),
TextInput::make('color_match')
->rules('numeric'),
TextInput::make('color_change')
->rules('numeric'),
Select::make('color_match')
->required()
->options([
true => 'Yes',
false => 'No',
])
->default(false),
Select::make('color_change')
->required()
->options([
true => 'Yes',
false => 'No',
])
->default(false),
TextInput::make('flash')
->rules(['numeric']),
TextInput::make('fleece')
@ -147,19 +147,33 @@ public static function form(Form $form): Form
->rules('numeric'),
TextInput::make('run_charge')
->rules('numeric'),
TextInput::make('other_charges')
TextInput::make('artwork_fee')
->label('Artwork fee')
->rules('numeric'),
TextInput::make('repacking_fee')
->label('Repack. fee')
->rules('numeric'),
])
->addActionLabel('Add Screen Print Entry')
->defaultItems(0)
->reorderable()
->colStyles([
'logo' => 'width: 15%',
'quantity' => 'width: 5%',
'width' => 'width: 6%',
'height' => 'width: 6%',
'setup_amount' => 'width: 5%',
'color_amount' => 'width: 5%',
'logo' => 'width: 11%',
'placement' => 'width: 11%',
'quantity' => 'width: 5%',
'width' => 'width: 6%',
'height' => 'width: 6%',
'setup_amount' => 'width: 5%',
'color_amount' => 'width: 5%',
'color_match' => 'width: 6%',
'color_change' => 'width: 5%',
'flash' => 'width: 6%',
'fleece' => 'width: 6%',
'poly_ink' => 'width: 6%',
'run_charge' => 'width: 6%',
'artwork_fee' => 'width: 6%',
'repacking_fee' => 'width: 6%',
]),
TableRepeater::make('heatTransferEntries')
@ -167,6 +181,7 @@ public static function form(Form $form): Form
->schema([
TextInput::make('logo')
->label('Logo name'),
TextInput::make('placement'),
TextInput::make('quantity')
->prefix('#')
->rules('numeric'),
@ -184,11 +199,12 @@ public static function form(Form $form): Form
->defaultItems(0)
->reorderable()
->colStyles([
'logo' => 'width: 20%',
'quantity' => 'width: 10%',
'width' => 'width: 11%',
'height' => 'width: 11%',
'price' => 'width: 15%',
'logo' => 'width: 25%',
'placement' => 'width: 25%',
'quantity' => 'width: 10%',
'width' => 'width: 11%',
'height' => 'width: 11%',
'price' => 'width: 15%',
]),
])->columns(1);
@ -202,6 +218,8 @@ public static function table(Table $table): Table
->color('primary')
->searchable(),
TextColumn::make('internal_id'),
TextColumn::make('date')
->date('Y-m-d')
->sortable()

View File

@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\QuoteResource\Pages;
use App\Enums\IconEnum;
use App\Filament\Admin\Resources\QuoteResource;
use App\Models\Quote;
use Filament\Actions;
@ -12,20 +13,26 @@ class EditQuote extends EditRecord
{
protected static string $resource = QuoteResource::class;
public function getTitle(): string|\Illuminate\Contracts\Support\Htmlable
{
return parent::getTitle().' '.$this->record->getKey();
}
protected function getHeaderActions(): array
{
return [
Action::make('save')
->label('Save changes')
->action('save')
->icon('lucide-save'),
->icon(IconEnum::SAVE->value),
Action::make('print')
->icon('lucide-printer')
->icon(IconEnum::PRINT->value)
->url(fn (Quote $record) => route('pdf.quote', $record))
->openUrlInNewTab(),
Actions\DeleteAction::make(),
Actions\DeleteAction::make()
->icon(IconEnum::TRASH->value),
];
}
}

View File

@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\QuoteResource\Pages;
use App\Enums\IconEnum;
use App\Filament\Admin\Resources\QuoteResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
@ -13,7 +14,8 @@ class ListQuotes extends ListRecords
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
Actions\CreateAction::make()
->icon(IconEnum::NEW->value),
];
}
}

View File

@ -9,6 +9,8 @@
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Columns\Summarizers\Sum;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
@ -42,44 +44,67 @@ public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
TextColumn::make('name')
->label('Code'),
Tables\Columns\TextColumn::make('value')
TextColumn::make('value')
->label('Long Name')
->extraHeaderAttributes(['class' => 'w-full']),
Tables\Columns\TextColumn::make('quantity')
TextColumn::make('productServices.amount')
->label('Quantity')
->alignRight()
->getStateUsing(function (Table $table, Model $record) {
return $record->getQuantityAttribute(
$table->getFilter('created_at')->getState()['created_at'],
$table->getFilter('created_until')->getState()['created_until']
);
}),
})
->summarize(Sum::make('amount')
->label('')
->using(fn (Table $table, $query) => $query
->when($createdAt = $table->getFilter('created_at')->getState()['created_at'] ?? null,
fn ($q) => $q->whereDate('created_at', '>=', $createdAt))
->when($createdUntil = $table->getFilter('created_until')->getState()['created_until'] ?? null,
fn ($q) => $q->whereDate('created_at', '<=', $createdUntil))
->sum('amount')
)
),
Tables\Columns\TextColumn::make('amount')
TextColumn::make('productServices.amount_price')
->label('Amount')
->alignRight()
->money()
->getStateUsing(function (Table $table, Model $record) {
return $record->getAmountAttribute(
$table->getFilter('created_at')->getState()['created_at'],
$table->getFilter('created_until')->getState()['created_until']
);
})
->money('usd'),
->summarize(Sum::make('amount_price')
->label('')
->money()
->using(fn (Table $table, $query) => $query
->when($createdAt = $table->getFilter('created_at')->getState()['created_at'] ?? null,
fn ($q) => $q->whereDate('created_at', '>=', $createdAt))
->when($createdUntil = $table->getFilter('created_until')->getState()['created_until'] ?? null,
fn ($q) => $q->whereDate('created_at', '<=', $createdUntil))
->sum('amount_price')
)
),
Tables\Columns\TextColumn::make('salesPercentage')
TextColumn::make('salesPercentage')
->alignRight()
->suffix('%')
->label('% sales')
->getStateUsing(function (Table $table, Model $record) {
return $record->getSalesPercentageAttribute(
$table->getFilter('created_at')->getState()['created_at'],
$table->getFilter('created_until')->getState()['created_until']
);
})
->suffix('%')
->label('% sales'),
}),
Tables\Columns\TextColumn::make('averagePrice')
TextColumn::make('averagePrice')
->getStateUsing(function (Table $table, Model $record) {
return $record->getAveragePriceAttribute(
$table->getFilter('created_at')->getState()['created_at'],

View File

@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\ShippingEntryResource\Pages;
use App\Enums\IconEnum;
use App\Filament\Admin\Resources\ShippingEntryResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
@ -13,7 +14,8 @@ class ListShippingEntries extends ListRecords
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
Actions\CreateAction::make()
->icon(IconEnum::NEW->value),
];
}
}

View File

@ -5,7 +5,6 @@
use App\Enums\IconEnum;
use App\Filament\Admin\Resources\TaxRateResource\Pages;
use App\Models\TaxRate;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
@ -36,24 +35,8 @@ public static function form(Form $form): Form
->label('Value in percentage')
->numeric()
->prefix('%'),
])
->columns(1)
->columnSpan(2),
Section::make()
->schema([
Placeholder::make('created_at')
->label('Created')
->content(fn (TaxRate $record): ?string => $record->created_at?->diffForHumans()),
Placeholder::make('updated_at')
->label('Last modified')
->content(fn (TaxRate $record): ?string => $record->updated_at?->diffForHumans()),
])
->columnSpan(1)
->hidden(fn (?TaxRate $record) => $record === null)
->extraAttributes(['class' => 'h-full']),
])->columns(3);
]),
]);
}
public static function table(Table $table): Table
@ -70,7 +53,8 @@ public static function table(Table $table): Table
//
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\EditAction::make()
->modalWidth('xs'),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([

View File

@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\TaxRateResource\Pages;
use App\Enums\IconEnum;
use App\Filament\Admin\Resources\TaxRateResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
@ -13,7 +14,10 @@ class ListTaxRates extends ListRecords
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
Actions\CreateAction::make()
->modalWidth('xs')
->icon(IconEnum::NEW->value)
->createAnother(false),
];
}
}

View File

@ -4,7 +4,6 @@
use App\Enums\IconEnum;
use App\Models\User;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
@ -36,32 +35,29 @@ public static function form(Form $form): Form
->autocomplete('new-username')
->unique()
->required()
->columnSpan(1),
->columnSpanFull(),
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')
->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()
->revealable()
->same('password')
->dehydrated(false)
->required(fn (string $operation) => $operation === 'create'),
])
->columnSpan(2),
TextInput::make('password_verify')
->label('Verify password')
->password()
->revealable()
->same('password')
->dehydrated(false)
->required(fn (string $operation) => $operation === 'create'),
]),
Section::make('Permissions')
->description('Administrators can access financial information and change settings.')
->description('Administrators can access invoices and settings')
->schema([
Toggle::make('is_admin')
->columnSpanFull()
->label('User is an administrator')
->reactive()
->afterStateUpdated(fn ($state, callable $set) => $set('customer_id', null))
@ -70,7 +66,7 @@ public static function form(Form $form): Form
->columns(2),
Section::make('Customer Login')
->description('If this account is for a customer, select them here.')
->description('If this account is for a customer, select them here')
->schema([
Select::make('customer_id')
@ -102,7 +98,9 @@ public static function table(Table $table): Table
//
])
->actions([
Tables\Actions\EditAction::make()->modal(),
Tables\Actions\EditAction::make()
->modalWidth('md')
->modal(),
])
->defaultSort('customer_id', 'asc');
}

View File

@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\UserResource\Pages;
use App\Enums\IconEnum;
use App\Filament\Admin\Resources\UserResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
@ -14,6 +15,8 @@ protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make()
->modalWidth('md')
->icon(IconEnum::NEW->value)
->modal(),
];
}

View File

@ -1,34 +0,0 @@
<?php
namespace App\Filament\Admin\Widgets;
use App\Enums\OrderStatus;
use App\Models\Order;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as BaseWidget;
class ActiveOrdersTable extends BaseWidget
{
protected static ?int $sort = 2;
public function table(Table $table): Table
{
return $table
->query(
Order::query()
->where('status', '!=', OrderStatus::SHIPPED)
->where('status', '!=', OrderStatus::INVOICED)
)
->columns([
TextColumn::make('customer.company_name'),
TextColumn::make('customer_po')
->color('code')
->weight('bold'),
TextColumn::make('status')
// ->color(OrderStatus::class)
->badge(),
])
->defaultPaginationPageOption(5);
}
}

View File

@ -35,7 +35,9 @@ protected function getStats(): array
private function getActiveOrders(): string
{
return Order::all()
->where('order_status', '!=', OrderStatus::DRAFT)
->where('order_status', '!=', OrderStatus::SHIPPED)
->where('order_status', '!=', OrderStatus::READY_FOR_INVOICE)
->where('order_status', '!=', OrderStatus::INVOICED)
->count();
}
@ -43,6 +45,8 @@ private function getActiveOrders(): string
private function getOrdersPast30Days(): string
{
return Order::all()
->where('order_status', '!=', OrderStatus::DRAFT)
->where('order_status', '!=', OrderStatus::READY_FOR_INVOICE)
->where('order_status', '!=', OrderStatus::SHIPPED)
->where('order_status', '!=', OrderStatus::INVOICED)
->whereBetween('created_at', [now()->startOfMonth(), now()->endOfMonth()])
@ -65,6 +69,8 @@ private function getOrdersInPast30DaysChart(): array
private function getDueOrders(): string
{
return Order::all()
->where('order_status', '!=', OrderStatus::DRAFT)
->where('order_status', '!=', OrderStatus::READY_FOR_INVOICE)
->where('order_status', '!=', OrderStatus::SHIPPED)
->where('order_status', '!=', OrderStatus::INVOICED)
->where('due_date', '<=', now())

View File

@ -1,33 +0,0 @@
<?php
namespace App\Filament\Admin\Widgets;
use App\Enums\OrderStatus;
use App\Models\Order;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as BaseWidget;
class RushOrdersTable extends BaseWidget
{
public function table(Table $table): Table
{
return $table
->query(
Order::query()
->where('status', '!=', OrderStatus::SHIPPED)
->where('status', '!=', OrderStatus::INVOICED)
->where('rush', true)
->orderByDesc('due_date')
)
->columns([
TextColumn::make('customer.company_name'),
TextColumn::make('customer_po')
->color('code')
->weight('bold'),
TextColumn::make('status')
->badge(),
])
->defaultPaginationPageOption(5);
}
}

View File

@ -1,43 +0,0 @@
<?php
namespace App\Livewire;
use App\Enums\OrderStatus;
use App\Enums\OrderType;
use App\Models\Customer;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Carbon;
use Livewire\Component;
class CreateOrder extends Component
{
public Collection $customers;
public string $selectedCustomer;
public $contacts;
public function mount(Collection $customers): void
{
$this->customers = $customers;
$this->contacts = $customers->first()->contacts;
}
public function getContacts(): void
{
$this->contacts = Customer::find($this->selectedCustomer)->contacts;
}
public function render(): View
{
return view('livewire.create-order', [
'contacts' => $this->contacts,
'order_types' => OrderType::cases(),
'order_status' => OrderStatus::cases(),
'customers' => $this->customers,
'today' => Carbon::today()->format('Y-m-d'),
'due_default' => Carbon::today()->addDay(10)->format('Y-m-d'),
]);
}
}

View File

@ -1,41 +0,0 @@
<?php
namespace App\Livewire;
use App\Models\Customer;
use Illuminate\Support\Collection;
use Livewire\Component;
class CustomerAndContactSelect extends Component
{
public Collection $customers;
public Collection $contacts;
public string $selectedCustomer;
public function mount(Collection $customers)
{
$this->customers = $customers;
if (isset($this->selectedCustomer)) {
$this->contacts = Customer::find($this->selectedCustomer)->contacts;
} else {
$this->contacts = $customers->first()->contacts;
}
}
public function updateContactList()
{
$this->contacts = Customer::find($this->selectedCustomer)->contacts;
}
public function render()
{
return view('livewire.customer-and-contact-select', [
'customers' => $this->customers,
'contacts' => $this->contacts,
]);
}
}

View File

@ -1,172 +0,0 @@
<?php
namespace App\Livewire;
use Exception;
use Illuminate\Contracts\View\Factory;
use Illuminate\Foundation\Application;
use Illuminate\Support\Collection;
use Illuminate\View\View;
use Livewire\Component;
class OrderProductsCreate extends Component
{
/** @var Collection<string, int> */
public Collection $productInputs;
/** @var Collection<string, int> */
public Collection $serviceInputs;
/** @var array<int> */
public array $sizes = [];
/** @var array<int> */
public array $totals = [];
/** @var array<int> */
public array $units = [];
/**
* @var array<int>
*/
public array $prices = [];
/**
* @var array<int>
*/
public array $priceTotals = [];
public int $totalQuantity = 0;
public string $totalPrice = '$0.00';
public function updated(): void
{
try {
foreach ($this->sizes as $index => $size) {
$this->totals[$index] = array_sum($size);
}
} catch (Exception $e) {
}
try {
foreach ($this->units as $index => $unit) {
$this->priceTotals[$index] = $unit * $this->prices[$index];
}
} catch (Exception $e) {
}
$this->totalQuantity = array_sum($this->totals);
$this->totalPrice = '$'.number_format(round(array_sum($this->priceTotals), 2), 2);
}
public function addProductInput(): void
{
$index = $this->productInputs->count();
$this->productInputs->push([
$index => [
'sku' => '',
'product_name' => '',
'product_color' => '',
'size_xs' => '',
'size_s' => '',
'size_m' => '',
'size_l' => '',
'size_xl' => '',
'size_2xl' => '',
'size_3xl' => '',
'size_osfa' => '',
'product_total' => '',
],
]);
}
public function determineAddProductRow(int $index): void
{
if ($index == $this->productInputs->count() - 1) {
$this->addProductInput();
}
}
public function determineAddServiceProductRow(int $index): void
{
if ($index == $this->serviceInputs->count() - 1) {
$this->addServiceInput();
}
}
public function removeProductInput(int $key): void
{
if ($this->productInputs->count() > 1) {
$this->productInputs->pull($key);
}
}
public function addServiceInput(): void
{
$this->serviceInputs->push([
$this->serviceInputs->count() => [
'service_name' => '',
'product_name' => '',
'product_color' => '',
'logo_name' => '',
'setup_number' => '',
'service_width' => '',
'service_height' => '',
'service_setup_unit' => '',
'service_setup_price' => '',
'service_total' => '',
],
]);
}
public function removeServiceInput(int $key): void
{
if ($this->serviceInputs->count() > 1) {
$this->serviceInputs->pull($key);
}
}
public function mount(): void
{
$this->fill([
'productInputs' => collect([
[
'sku' => '',
'product_name' => '',
'product_color' => '',
'size_xs' => '',
'size_s' => '',
'size_m' => '',
'size_l' => '',
'size_xl' => '',
'size_2xl' => '',
'size_3xl' => '',
'size_osfa' => '',
'product_total' => '0',
],
]),
'serviceInputs' => collect([
[
'sku' => '',
'product_name' => '',
'product_color' => '',
'logo_name' => '',
'setup_number' => '',
'service_width' => '',
'service_height' => '',
'service_setup_unit' => '',
'service_setup_price' => '',
'service_total' => '',
],
]),
]);
}
public function render(): \Illuminate\Contracts\View\View|Factory|Application|View
{
return view('livewire.order-products-create');
}
}

View File

@ -1,64 +0,0 @@
<?php
namespace App\Livewire;
use App\Models\Order;
use Illuminate\Contracts\View\Factory;
use Illuminate\Foundation\Application;
use Illuminate\Support\Carbon;
use Illuminate\View\View;
use Livewire\Component;
use Livewire\WithPagination;
class OrdersTable extends Component
{
use WithPagination;
protected string $paginationTheme = 'bootstrap';
public bool $showCustomerColumn;
public string $orderType = 'active';
public string $search = '';
public string $title = '';
public string $customer_id = '';
public Carbon $today;
public function mount(bool $showCustomerColumn, string $orderType, string $title, ?string $customer_id = null): void
{
$this->today = Carbon::today();
$this->showCustomerColumn = $showCustomerColumn;
$this->orderType = $orderType;
$this->title = $title;
$this->customer_id = $customer_id ?? '';
}
public function render(): \Illuminate\Contracts\View\View|Factory|Application|View
{
return view('livewire.orders-table', [
'orders' => Order::with('customer')
->when($this->customer_id != null, fn ($q) => $q->where('customer_id', $this->customer_id))
->when($this->orderType === 'active', fn ($q) => $q->active())
->when($this->orderType === 'invoiced', fn ($q) => $q->invoiced())
->when($this->orderType === 'finished', fn ($q) => $q->finished())
->when($this->search !== '', function ($query) {
$query->whereHas('customer', function ($query) {
$query->where('company_name', 'like', '%'.$this->search.'%');
})->orWhere('customer_po', 'like', '%'.$this->search.'%')
->orWhere('internal_po', 'like', '%'.$this->search.'%')
->orWhere('order_date', 'like', '%'.$this->search.'%')
->orWhere('due_date', 'like', '%'.$this->search.'%')
->orWhere('status', 'like', '%'.$this->search.'%');
})
->orderByDesc('rush')
->orderBy('due_date')
->paginate(15)
->withQueryString(),
'today' => $this->today,
]);
}
}

View File

@ -12,6 +12,7 @@ class HeatTransferEntry extends Model
protected $fillable = [
'quote_id',
'placement',
'quantity',
'logo',
'width',

View File

@ -5,6 +5,7 @@
use App\Enums\InvoiceStatus;
use App\Observers\InvoiceObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -55,6 +56,19 @@ class Invoice extends Model
'total' => 'float',
];
public function scopeSearchByBalance(Builder $query, $amount): Builder
{
if (! is_numeric($amount)) {
return $query;
}
return $query->whereRaw('total - (SELECT IFNULL(SUM(applied_amount), 0)
FROM payments
INNER JOIN invoice_payment
ON payments.id = invoice_payment.payment_id
WHERE invoice_payment.invoice_id = invoices.id) = ?', [$amount]);
}
public function remainingBalance(): float
{
$applied = $this->payments()->sum('applied_amount');

View File

@ -17,8 +17,11 @@ class InvoiceReport extends Model
'customer_id',
'date_start',
'date_end',
'filter_paid',
'subtotal',
'with_unpaid',
'with_partially_paid',
'with_paid',
'with_void',
'pst',
'gst',
];
@ -38,25 +41,26 @@ public static function boot(): void
parent::boot();
static::created(function (InvoiceReport $model) {
// Set ID after creation
$model->attributes['internal_id'] = 'TNR'.str_pad($model->id, 4, '0', STR_PAD_LEFT);
// Associate all relevant invoices
$invoices = Invoice::whereBetween('date', [$model->date_start, $model->date_end])
->where('customer_id', $model->customer_id)
->when($model->filter_paid, function ($query) {
$query->where('status', InvoiceStatus::UNPAID);
});
->when(! $model->with_unpaid, fn ($query) => $query->whereNot('status', InvoiceStatus::UNPAID))
->when(! $model->with_partially_paid, fn ($query) => $query->whereNot('status', InvoiceStatus::PARTIALLY_PAID))
->when(! $model->with_paid, fn ($query) => $query->whereNot('status', InvoiceStatus::PAID))
->when(! $model->with_void, fn ($query) => $query->whereNot('status', InvoiceStatus::VOID));
$model->invoices()->sync($invoices->pluck('id')->toArray());
// $model->total = $model->invoices()->where('status', 'UNPAID')->sum('total');
// Finally, save
$model->save();
});
}
public function combinedGstHstSum(): float
{
return $this->invoices()->sum('gst_amount') + $this->invoices()->sum('hst_amount');
}
public function getTotalAttribute()
{
return $this->invoices()->sum('total');

View File

@ -9,6 +9,7 @@
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
#[ObservedBy(PaymentObserver::class)]
@ -18,21 +19,23 @@ class Payment extends Model
protected $fillable = [
'customer_id',
'check_number',
'date',
'amount',
'unapplied_amount',
'notes',
];
public function applyToInvoices(): void
public function applyToInvoices(Collection $invoices): void
{
$remaining = $this->unapplied_amount ?? $this->amount;
$invoices = Invoice::where('customer_id', $this->customer_id)
->where('status', InvoiceStatus::UNPAID)
->orderBy('date')
->get();
$filteredInvoices = $invoices->whereIn('status', [
InvoiceStatus::UNPAID,
InvoiceStatus::PARTIALLY_PAID,
]);
foreach ($invoices as $invoice) {
foreach ($filteredInvoices as $invoice) {
$balance = $invoice->remainingBalance();
if ($remaining <= 0) {
@ -47,7 +50,7 @@ public function applyToInvoices(): void
if ($invoice->remainingBalance() == 0) {
$invoice->setStatus(InvoiceStatus::PAID);
} elseif ($applied > 0) {
$invoice->setStatus(InvoiceStatus::UNPAID);
$invoice->setStatus(InvoiceStatus::PARTIALLY_PAID);
}
}

View File

@ -2,16 +2,21 @@
namespace App\Models;
use App\Observers\QuoteObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[ObservedBy(QuoteObserver::class)]
class Quote extends Model
{
use HasFactory;
protected $fillable = [
'internal_id',
'customer_id',
'date',
'notes',
@ -30,15 +35,11 @@ public function getTotalAttribute(): float
$embDigitizingTotal = $this->embroideryEntries()->sum('digitizing_cost');
$embRunChargeTotal = $this->embroideryEntries()->sum('run_charge');
$scpRunChargeTotal = $this->screenPrintEntries()->sum('run_charge');
$scpOtherChargeTotal = $this->screenPrintEntries()->sum('other_charges');
$scpFleeceTotal = $this->screenPrintEntries()->sum('fleece');
$scpFlashTotal = $this->screenPrintEntries()->sum('flash');
$scpPolyInkTotal = $this->screenPrintEntries()->sum('poly_ink');
$scpTotal = $this->screenPrintEntries->sum(fn (ScreenPrintEntry $record) => $record->total_price);
$heatTransferTotal = $this->heatTransferEntries()->sum('price');
return $embDigitizingTotal + $embRunChargeTotal + $scpRunChargeTotal + $scpOtherChargeTotal + $scpFleeceTotal + $scpFlashTotal + $scpPolyInkTotal + $heatTransferTotal;
return $embDigitizingTotal + $embRunChargeTotal + $scpTotal + $heatTransferTotal;
}
public function customer(): BelongsTo

View File

@ -14,6 +14,7 @@ class ScreenPrintEntry extends Model
'quote_id',
'quantity',
'logo',
'placement',
'width',
'height',
'color_amount',
@ -24,7 +25,8 @@ class ScreenPrintEntry extends Model
'flash',
'fleece',
'poly_ink',
'other_charges',
'artwork_fee',
'repacking_fee',
'notes',
];
@ -34,7 +36,9 @@ class ScreenPrintEntry extends Model
protected function getTotalPriceAttribute(): float
{
return $this->flash + $this->fleece + $this->poly_ink + $this->run_charge + $this->other_charges;
$perUnitTotals = ($this->flash + $this->fleece + $this->poly_ink + $this->run_charge + $this->repacking_fee) * $this->quantity ?? 0;
return $perUnitTotals + $this->artwork_fee + $this->color_change + $this->color_match;
}
public function quote(): BelongsTo

View File

@ -42,7 +42,7 @@ public function getSalesPercentageAttribute($created_at = null, $created_until =
{
$query = ProductService::query()
->when($created_at, fn ($query) => $query->whereDate('created_at', '>=', $created_at))
->when($created_until, fn ($query) => $query->whereDate('created_until', '<=', $created_until));
->when($created_until, fn ($query) => $query->whereDate('created_at', '<=', $created_until));
$total = $query->count();

View File

@ -3,13 +3,15 @@
namespace App\Models;
use Database\Factories\UserFactory;
use Filament\Models\Contracts\FilamentUser;
use Filament\Models\Contracts\HasName;
use Filament\Panel;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable implements HasName
class User extends Authenticatable implements FilamentUser, HasName
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable;
@ -48,6 +50,11 @@ protected function casts(): array
];
}
public function canAccessPanel(Panel $panel): bool
{
return true;
}
public function setIsAdminAttribute(bool $value): void
{
$this->attributes['is_admin'] = $value;

View File

@ -23,7 +23,8 @@ public function creating(Invoice $invoice): void
public function created(Invoice $invoice): void
{
$invoice->internal_id = 'INV4'.str_pad($invoice->id, 4, '0', STR_PAD_LEFT);
// $invoice->internal_id = 'TN4'.str_pad($invoice->id, 4, '0', STR_PAD_LEFT);
$invoice->internal_id = 'TN'.$invoice->id + 4000;
$invoice->saveQuietly();
$invoice->calculateTotals();

View File

@ -12,7 +12,7 @@ class OrderObserver
*/
public function created(Order $order): void
{
if ($order->invoice()->exists()) {
if ($order->invoice != null) {
$order->invoice->calculateTotals();
}
}
@ -22,7 +22,7 @@ public function created(Order $order): void
*/
public function updated(Order $order): void
{
if ($order->invoice()->exists()) {
if ($order->invoice != null) {
$order->invoice->calculateTotals();
}
}
@ -33,7 +33,9 @@ public function updated(Order $order): void
public function saved(Order $order): void
{
if ($order->isDirty(['invoice_id']) && Invoice::where('id', $order->invoice_id)->exists()) {
$order->invoice->calculateTotals();
if ($order->invoice != null) {
$order->invoice->calculateTotals();
}
}
}
@ -42,7 +44,7 @@ public function saved(Order $order): void
*/
public function deleted(Order $order): void
{
if ($order->invoice()->exists()) {
if ($order->invoice != null) {
$order->invoice->calculateTotals();
}
}

View File

@ -11,7 +11,7 @@ class PaymentObserver
*/
public function saved(Payment $payment): void
{
$payment->applyToInvoices();
// $payment->applyToInvoices();
}
/**

View File

@ -0,0 +1,17 @@
<?php
namespace App\Observers;
use App\Models\Quote;
class QuoteObserver
{
public function created(Quote $quote): void
{
$company_string = strtoupper(substr($quote->customer->company_name, 0, 3));
$padded_id = str_pad($quote->id, 4, '0', STR_PAD_LEFT);
$quote->internal_id = 'Q'.$padded_id.'-'.$company_string;
$quote->save();
}
}

View File

@ -2,7 +2,6 @@
namespace App\Providers;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
@ -18,8 +17,5 @@ public function register(): void
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Paginator::useBootstrapFive();
}
public function boot(): void {}
}

View File

@ -29,9 +29,10 @@ public function panel(Panel $panel): Panel
->path('admin')
->login(UsernameLogin::class)
->colors([
'primary' => Color::Blue,
'code' => Color::hex('#d63384'),
'invoiced' => Color::hex('#900090'),
'primary' => Color::Blue,
'code' => Color::hex('#d63384'),
'invoicing' => Color::hex('#DD00DD'),
'invoiced' => Color::hex('#900090'),
])
->discoverResources(in: app_path('Filament/Admin/Resources/'), for: 'App\\Filament\\Admin\\Resources')
->discoverPages(in: app_path('Filament/Admin/Pages'), for: 'App\\Filament\\Admin\\Pages')

View File

@ -14,11 +14,11 @@
"laravel/tinker": "^2.9",
"livewire/livewire": "^3.5",
"mallardduck/blade-lucide-icons": "^1.23",
"spatie/laravel-pdf": "^1.5"
"spatie/laravel-pdf": "^1.5",
"fakerphp/faker": "^1.23"
},
"require-dev": {
"barryvdh/laravel-ide-helper": "^3.5",
"fakerphp/faker": "^1.23",
"larastan/larastan": "^2.0",
"laravel/pint": "^1.17",
"laravel/sail": "^1.26",

1121
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,18 @@
'name' => env('APP_NAME', 'Laravel'),
/*
|--------------------------------------------------------------------------
| Application Version
|--------------------------------------------------------------------------
|
| This value is the version of your application. This value is used when
| the framework needs to place the application's version in a notification
| or any other location as required by the application or its packages.
*/
'version' => '20250311',
/*
|--------------------------------------------------------------------------
| Application Environment

View File

@ -17,11 +17,12 @@ class HeatTransferEntryFactory extends Factory
public function definition(): array
{
return [
'quantity' => $this->faker->numberBetween(1, 10),
'logo' => $this->faker->words(2, true),
'width' => $this->faker->randomFloat(2, 1, 5),
'height' => $this->faker->randomFloat(2, 1, 5),
'price' => $this->faker->randomFloat(2, 1, 10),
'quantity' => $this->faker->numberBetween(1, 10),
'placement' => $this->faker->words(2, true),
'logo' => $this->faker->words(2, true),
'width' => $this->faker->randomFloat(2, 1, 5),
'height' => $this->faker->randomFloat(2, 1, 5),
'price' => $this->faker->randomFloat(2, 1, 10),
];
}
}

View File

@ -14,12 +14,15 @@ class InvoiceReportFactory extends Factory
public function definition(): array
{
return [
'customer_id' => Customer::all()->shuffle()->first()->id,
'date_start' => Carbon::now()->subYear(),
'date_end' => Carbon::now(),
'filter_paid' => $this->faker->boolean(40),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
'customer_id' => Customer::all()->shuffle()->first()->id,
'date_start' => Carbon::now()->subYear(),
'date_end' => Carbon::now(),
'with_unpaid' => $this->faker->boolean(40),
'with_partially_paid' => $this->faker->boolean(40),
'with_paid' => $this->faker->boolean(40),
'with_void' => $this->faker->boolean(40),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
];
}
}

View File

@ -19,17 +19,19 @@ public function definition(): array
return [
'quantity' => random_int(1, 10),
'logo' => $this->faker->words(2, true),
'placement' => $this->faker->words(2, true),
'width' => $this->faker->randomFloat(2, 1, 5),
'height' => $this->faker->randomFloat(2, 1, 5),
'color_amount' => random_int(1, 5),
'setup_amount' => random_int(1, 5),
'run_charge' => $this->faker->randomFloat(2, 1, 10),
'color_change' => $this->faker->boolean(),
'color_match' => $this->faker->boolean(),
'flash' => $this->faker->randomFloat(2, 1, 10),
'fleece' => $this->faker->randomFloat(2, 1, 10),
'poly_ink' => $this->faker->randomFloat(2, 1, 10),
'other_charges' => $this->faker->randomFloat(2, 1, 10),
'artwork_fee' => $this->faker->randomFloat(2, 1, 10),
'color_change' => $this->faker->randomFLoat(2, 1, 10),
'color_match' => $this->faker->randomFLoat(2, 1, 10),
'flash' => $this->faker->randomFloat(2, 0, 2),
'fleece' => $this->faker->randomFloat(2, 0, 2),
'poly_ink' => $this->faker->randomFloat(2, 0, 2),
'run_charge' => $this->faker->randomFloat(2, 0, 2),
'repacking_fee' => $this->faker->randomFloat(2, 0, 2),
];
}
}

View File

@ -17,7 +17,8 @@ class TaxRateFactory extends Factory
public function definition(): array
{
return [
//
'name' => strtoupper($this->faker->randomLetter()).'ST',
'value' => random_int(1, 15),
];
}
}

View File

@ -15,6 +15,7 @@ public function up(): void
$table->id();
$table->foreignId('customer_id')->constrained();
$table->string('internal_id')->nullable();
$table->date('date');
$table->longText('notes')->nullable();

View File

@ -11,16 +11,16 @@ public function up(): void
Schema::create('invoice_reports', function (Blueprint $table) {
$table->id();
$table->string('internal_id')->nullable();
$table->foreignId('customer_id')->constrained();
$table->date('date_start');
$table->date('date_end');
$table->boolean('filter_paid');
$table->boolean('with_unpaid')->default(false);
$table->boolean('with_partially_paid')->default(false);
$table->boolean('with_paid')->default(false);
$table->boolean('with_void')->default(false);
$table->float('subtotal', 2)->default(0);
$table->float('pst', 2)->default(0);
$table->float('gst', 2)->default(0);
// $table->float('total', 2)->default(0);
$table->timestamps();
});

View File

@ -15,6 +15,8 @@ public function up(): void
$table->id();
$table->foreignId('customer_id')->constrained();
$table->text('check_number')->nullable();
$table->date('date')->default(today());
$table->decimal('amount', 8, 2);
$table->decimal('unapplied_amount', 8, 2)->nullable();
$table->text('notes')->nullable();

View File

@ -18,17 +18,19 @@ public function up(): void
$table->integer('quantity')->nullable();
$table->string('logo')->nullable();
$table->string('placement')->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->boolean('color_change')->default(false);
$table->boolean('color_match')->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->decimal('color_change', 8, 2)->nullable();
$table->decimal('color_match', 8, 2)->nullable();
$table->decimal('flash', 8, 2)->nullable();
$table->decimal('fleece', 8, 2)->nullable();
$table->decimal('poly_ink', 8, 2)->nullable();
$table->decimal('artwork_fee', 8, 2)->nullable();
$table->decimal('repacking_fee', 8, 2)->nullable();
$table->text('notes')->nullable();
$table->timestamps();

View File

@ -17,6 +17,7 @@ public function up(): void
$table->foreignId('quote_id')->constrained()->cascadeOnDelete();
$table->integer('quantity')->nullable();
$table->string('placement')->nullable();
$table->string('logo')->nullable();
$table->decimal('width', 6, 2)->nullable();
$table->decimal('height', 6, 2)->nullable();

View File

@ -2,7 +2,6 @@
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
@ -34,9 +33,5 @@ public function run(): void
InvoiceReportSeeder::class,
]);
User::factory()->create([
'username' => 'admin',
'is_admin' => true,
]);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
class DefaultUserSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
User::factory()->create([
'username' => 'admin',
'password' => \Hash::make('TopNotch13579!'),
'is_admin' => true,
]);
}
}

View File

@ -13,6 +13,11 @@ class UserSeeder extends Seeder
*/
public function run(): void
{
User::factory()->create([
'username' => 'admin',
'is_admin' => true,
]);
foreach (Customer::all() as $customer) {
User::factory([
'username' => str_replace(',', '', strtolower(explode(' ', $customer->company_name)[0])),

91
deploy/Dockerfile Normal file
View File

@ -0,0 +1,91 @@
# deploy/Dockerfile
# stage 1: build stage
FROM php:8.3-fpm-alpine as build
# installing system dependencies and php extensions
RUN apk add --no-cache \
zip \
libzip-dev \
freetype \
libjpeg-turbo \
libpng \
freetype-dev \
libjpeg-turbo-dev \
libpng-dev \
nodejs \
npm \
icu-dev \
&& docker-php-ext-configure intl \
&& docker-php-ext-install intl \
&& docker-php-ext-configure zip \
&& docker-php-ext-install zip pdo pdo_mysql \
&& docker-php-ext-configure gd --with-freetype=/usr/include/ --with-jpeg=/usr/include/ \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-enable gd
# install composer
COPY --from=composer:2.7.6 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
# copy necessary files and change permissions
COPY . .
RUN chown -R www-data:www-data /var/www/html \
&& chmod -R 775 /var/www/html/storage \
&& chmod -R 775 /var/www/html/bootstrap/cache
# install php and node.js dependencies
RUN composer install --no-dev --prefer-dist \
&& npm install \
&& npm run build
RUN chown -R www-data:www-data /var/www/html/vendor \
&& chmod -R 775 /var/www/html/vendor
# stage 2: production stage
FROM php:8.3-fpm-alpine
# install nginx
RUN apk add --no-cache \
zip \
libzip-dev \
freetype \
libjpeg-turbo \
libpng \
freetype-dev \
libjpeg-turbo-dev \
libpng-dev \
oniguruma-dev \
gettext-dev \
freetype-dev \
nginx \
icu-dev \
&& docker-php-ext-configure zip intl \
&& docker-php-ext-install zip pdo pdo_mysql intl \
&& docker-php-ext-enable intl \
&& docker-php-ext-configure gd --with-freetype=/usr/include/ --with-jpeg=/usr/include/ \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-enable gd \
&& docker-php-ext-install bcmath \
&& docker-php-ext-enable bcmath \
&& docker-php-ext-install exif \
&& docker-php-ext-enable exif \
&& docker-php-ext-install gettext \
&& docker-php-ext-enable gettext \
&& docker-php-ext-install opcache \
&& docker-php-ext-enable opcache \
&& rm -rf /var/cache/apk/*
# copy files from the build stage
COPY --from=build /var/www/html /var/www/html
COPY ./deploy/nginx.conf /etc/nginx/http.d/default.conf
COPY ./deploy/php.ini "$PHP_INI_DIR/conf.d/app.ini"
WORKDIR /var/www/html
# add all folders where files are being stored that require persistence. if needed, otherwise remove this line.
VOLUME ["/var/www/html/storage/app"]
CMD ["sh", "-c", "nginx && php-fpm"]

63
deploy/docker-compose.yml Normal file
View File

@ -0,0 +1,63 @@
# deploy/docker-compose.yml
version: '3.8'
services:
laravel:
restart: unless-stopped
container_name: sewtopnotch.com
build:
context: ../
dockerfile: ./deploy/Dockerfile
# allocate as many volumes as necessary, if needed.
volumes:
- ../storage/app:/var/www/html/storage/app
environment:
APP_NAME: ${APP_NAME}
APP_ENV: ${APP_ENV}
APP_DEBUG: ${APP_DEBUG}
APP_KEY: ${APP_KEY}
APP_VERSION: ${APP_VERSION}
APP_URL: ${APP_URL}
DB_CONNECTION: mysql
DB_HOST: database
DB_PORT: 3306
DB_DATABASE: ${DB_DATABASE}
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
MAIL_MAILER: ${MAIL_MAILER}
MAIL_HOST: ${MAIL_HOST}
MAIL_PORT: ${MAIL_PORT}
MAIL_USERNAME: ${MAIL_USERNAME}
MAIL_PASSWORD: ${MAIL_PASSWORD}
MAIL_ENCRYPTION: ${MAIL_ENCRYPTION}
MAIL_FROM_ADDRESS: ${MAIL_FROM_ADDRESS}
MAIL_FROM_NAME: ${MAIL_FROM_NAME}
ports:
- "8080:80"
networks:
- n-laravel
depends_on:
- database
database:
restart: unless-stopped
image: mariadb:lts-jammy
volumes:
- v-database:/var/lib/mysql
environment:
MARIADB_DATABASE: ${DB_DATABASE}
MARIADB_USER: ${DB_USERNAME}
MARIADB_PASSWORD: ${DB_PASSWORD}
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
networks:
- n-laravel
volumes:
v-database:
networks:
n-laravel:
driver: bridge

35
deploy/nginx.conf Normal file
View File

@ -0,0 +1,35 @@
# deploy/nginx.conf
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/html/public;
client_max_body_size 10M;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico {
access_log off; log_not_found off;
}
location = /robots.txt {
access_log off; log_not_found off;
}
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
}

0
deploy/php.ini Normal file
View File

View File

@ -1,131 +0,0 @@
// Use DBML to define your database structure
// Docs: https://dbml.dbdiagram.io/docs
// Table follows {
// following_user_id integer
// followed_user_id integer
// created_at timestamp
// }
Table users {
id uuid [primary key]
username varchar
role enum [note: 'admin, employee, customer']
created_at timestamp
}
Table orders {
id uuid [primary key]
fk_customer_id uuid
product_order varchar
order_date datetime
due_date datetime
status enum [note: 'waiting, approved, processing, waiting to ship, shipped']
rush bool
event bool
new_art bool
repeat bool
digitizing bool
purchased_garments bool
supplied_file bool
// code varchar
notes varchar
created_at timestamp
updated_at timestamp
deleted_at timestamp
}
Ref: orders.fk_customer_id > customers.id
Table order_types {
id uuid [primary key]
name varchar
value varchar [note: 'DTG, Embroidery, Screen Printing, Vinyl']
}
Table orders_order_types {
id uuid [primary key]
fk_order_id uuid
fk_order_type_id uuid
}
Ref: orders_order_types.fk_order_id > orders.id
Ref: orders_order_types.fk_order_type_id > order_types.id
Table order_products {
id uuid [pk]
fk_order_id uuid
sku varchar
product_name varchar
color varchar
}
Ref: order_products.fk_order_id > orders.id
Table product_sizes {
id uuid [pk]
fk_order_product_id uuid
size varchar
amount integer
}
Ref: product_sizes.fk_order_product_id > order_products.id
Table product_services {
id uuid [primary key]
index int
fk_order_product_id uuid
service varchar
file varchar
placement varchar
logo_name varchar
logo_width decimal
logo_height decimal
setup_amount integer
amount integer
amount_price decimal
}
Ref: product_services.fk_order_product_id > order_products.id
Table logos {
id uuid [primary key]
name varchar
width decimal
height decimal
}
Table customers {
id uuid [primary key]
fk_user_id uuid
company_name varchar
internal_name varchar [note: 'image group quadreal for example']
shipping_address varchar
billing_address varchar
created_at timestamp
updated_at timestamp
deleted_at timestamp
}
Ref: customers.fk_user_id > users.id
Table contacts {
id uuid [primary key]
fk_customer_id uuid
first_name varchar
last_name varchar
email varchar
phone varchar
notes varchar
}
Ref: contacts.fk_customer_id > customers.id
Table invoices {
id uuid [primary key]
fk_order_id uuid
}
Ref: invoices.fk_order_id > orders.id

@ -1 +0,0 @@
Subproject commit 65fe7885813f3d4dc87f0fc52495247592fe276c

View File

@ -1 +0,0 @@
ERROR [mprocs::error] Error: channel closed

View File

@ -1,5 +0,0 @@
procs:
sail:
shell: "/home/nisse/Code/topnotch_website/vendor/bin/sail up"
npm:
shell: "npm run dev"

597
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,10 +13,11 @@
"laravel-vite-plugin": "^1.0",
"postcss": "^8.4.47",
"sass": "^1.56.1",
"tailwindcss": "^3.4.13",
"tailwindcss": "^3.4.17",
"vite": "^5.0"
},
"dependencies": {
"@tailwindcss/vite": "^4.0.6",
"bootstrap-icons": "^1.11.3",
"puppeteer": "^23.8.0"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,21 +0,0 @@
{
"node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff": {
"file": "assets/bootstrap-icons-BOrJxbIo.woff",
"src": "node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff"
},
"node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff2": {
"file": "assets/bootstrap-icons-BtvjY1KL.woff2",
"src": "node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff2"
},
"resources/js/app.js": {
"file": "assets/app-L09tcoah.js",
"name": "app",
"src": "resources/js/app.js",
"isEntry": true
},
"resources/sass/app.scss": {
"file": "assets/app-Dgcz2HWl.css",
"src": "resources/sass/app.scss",
"isEntry": true
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More