Compare commits

...

103 Commits
0.9 ... 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
eac70f0f53 Added numeric validation rules, some styling fixes 2025-02-09 12:42:03 -05:00
f0f10a4907 Added numeric validation rules, some styling fixes 2025-02-07 23:53:15 -05:00
66168d0919 Work on new Quotes 2025-02-07 16:06:38 -05:00
0b8fd00f67 Merge branch 'development' of git.niisse.net:nisse/topnotch_website into development 2025-02-06 14:18:26 -05:00
d3a9f183ca WIP Work on new Quotes 2025-02-05 23:51:50 -05:00
f08bff9684 WIP Work on new Quotes 2025-02-05 14:14:03 -05:00
9ae273fda0 Added tests to ensure every list page loads correctly 2025-01-30 11:58:21 -08:00
b2c8ce7405 Work on Payments 2025-01-29 12:30:54 -08:00
f937302159 Work on customer login 2025-01-23 19:19:52 -05:00
b320915fc1 Work on tests 2025-01-21 17:06:38 -05:00
efe78bb49f More Tax Rates 2025-01-17 16:23:00 -05:00
9efde6fa34 Change icons to enum 2025-01-16 10:39:50 -05:00
0d68062055 WIP Tax Rates 2025-01-15 22:16:01 -05:00
306afd630b Work on post-oplevering issues 2025-01-09 21:09:40 -05:00
056462f511 Finished user table 2025-01-04 16:41:24 -05:00
f122f2925a Work work 2025-01-04 15:59:16 -05:00
90ef3c9c29 Merge branch 'orders' of git.niisse.net:nisse/topnotch_website into orders 2024-12-06 15:46:58 -08:00
e4899de42a Work on invoice reports 2024-12-04 01:30:56 -05:00
0b359b7255 Work on invoice reports 2024-12-02 12:21:01 -05:00
0528506dfa Merge branch 'orders' into branch-2 2024-12-01 17:55:25 -05:00
9e8eb05e88 Work on invoice reports 2024-12-01 17:22:31 -05:00
548e56335f More work on shippingEntries
Table groupified + search working despite lack of column; create/edit
form
2024-11-29 12:42:00 -05:00
74a5a43c85 More work on shippingEntries
Table groupified + search working despite lack of column; create/edit
form
2024-11-25 16:22:53 -05:00
487ea48c14 Work on ordertest, splitting address lines, and shipping entry stuff 2024-11-25 13:16:56 -05:00
2c0fbfde5b Merge branch 'orders' of git.niisse.net:nisse/topnotch_website into orders 2024-11-22 15:58:26 -05:00
fcb1cef6fd Work on invoice report table (added date groups) 2024-11-22 14:29:56 -05:00
f945ad2f71 Work on invoice report table (added date groups) 2024-11-20 21:09:15 -05:00
b47dd597e1 Improving Order table front-end 2024-11-20 17:23:32 -05:00
cf4a56ee84 Implement invoice report table 2024-11-19 16:14:56 -05:00
2aaf7ab8a2 WIP work on invoice reports 2024-11-18 22:18:41 -05:00
73df66d0eb merge 2024-11-18 19:45:10 -05:00
eceaf3e676 Work 2024-11-18 18:53:14 -05:00
bdbc65cedb Work 2024-11-17 19:39:40 -05:00
f051f20ad9 Setup Pest for testing and hooks for pint and pest on commit 2024-11-17 15:47:45 -05:00
1ffd38fd53 Work on invoices and table spacing 2024-11-17 15:41:32 -05:00
1f1f783aa9 Optimized product services reports code 2024-11-15 19:22:07 -05:00
7740160b4f Product Services filtering seems to work 2024-11-15 15:22:57 -05:00
fcb1eda56f work on reports 2024-11-15 00:37:01 -05:00
fdbc2653d4 pre service-type modelisation commitation 2024-11-14 12:11:18 -05:00
162c8839e2 Work on invoice status and reports 2024-11-13 23:34:53 -05:00
2de11bcaba Work on invoice PDF
to do
- split address lines in two
- look into footer
2024-11-12 16:54:48 -05:00
1b891f8350 created invoice seeder 2024-11-11 19:50:11 -05:00
7e2a22e016 Work 2024-11-09 15:13:04 -05:00
562e499d12 More work on invoices 2024-11-07 12:22:20 -05:00
256cc1f7ed Invoice Create and Edit Page mostly done 2024-11-05 18:07:19 -05:00
a12e0b29d3 Work on invoices 2024-11-05 11:35:23 -05:00
d950955371 Added pre-pro and printed toggles to order 2024-11-01 11:34:47 -04:00
3c98de929e Various little things 2024-11-01 11:06:09 -04:00
913a4477a7 Replace icons with lucide and work on quote 2024-10-30 11:55:21 -04:00
b3868e8b0a Order CRUD complete 2024-10-29 10:43:14 -04:00
e79d1839fe Re-worked seeders 2024-10-29 10:12:54 -04:00
2361ec0b88 Orders are once again writeable 2024-10-26 18:32:12 -04:00
b9346c4466 Work work work 2024-10-23 15:45:10 -04:00
3e2c5d5fac Convert Filament JSON to model bools 2024-10-22 12:48:05 -04:00
c0053a8969 Add TableRepeater package 2024-10-21 16:05:57 -04:00
a5c40ea161 Add Filament Cluster 2024-10-21 12:00:00 -04:00
542e1346f4 Added Filament 2024-10-21 10:51:31 -04:00
08f0a99551 PDF Setup and Order PDF view 2024-09-23 16:01:54 -07:00
8e80ba9480 Order Show page 2024-09-19 15:22:56 -07:00
7074596cb7 Work on PHPStan error fixing 2024-09-18 16:00:06 -07:00
61831b1940 WIP orders 2024-09-18 15:25:17 -07:00
330 changed files with 19263 additions and 4598 deletions

5
.directory Normal file
View File

@ -0,0 +1,5 @@
[Dolphin]
Timestamp=2024,11,17,16,40,18.95
Version=4
ViewMode=1
VisibleRoles=Icons_text,Icons_size

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

69
.env.testing Normal file
View File

@ -0,0 +1,69 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:vRgghlbIdXQxXIEvgUArbI9FURhgdyqx3LDXDwHYSmA=
APP_DEBUG=true
APP_TIMEZONE=UTC
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=testing
DB_USERNAME=sail
DB_PASSWORD=password
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://meilisearch:7700
MEILISEARCH_NO_ANALYTICS=false

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"

6
.gitignore vendored
View File

@ -103,3 +103,9 @@ fabric.properties
.idea/caches/build_file_checksums.ser
.idea
.directory
.directory
.directory
public
_ide_helper.php
public/build

9
.puppeteer.cjs Normal file
View File

@ -0,0 +1,9 @@
const {join} = require('path');
/**
* @type {import("puppeteer").Configuration}
*/
module.exports = {
// Changes the cache location for Puppeteer.
cacheDirectory: join(__dirname, '.cache', 'puppeteer'),
};

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,66 +1,8 @@
<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>
# Changelog
<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

62
app/Enums/IconEnum.php Normal file
View File

@ -0,0 +1,62 @@
<?php
namespace App\Enums;
enum IconEnum: string
{
// 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';
// 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

@ -0,0 +1,45 @@
<?php
namespace App\Enums;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum InvoiceStatus: string implements HasColor, HasIcon, HasLabel
{
case UNPAID = 'not_paid';
case PARTIALLY_PAID = 'partially_paid';
case PAID = 'paid';
case VOID = 'void';
public function getLabel(): string
{
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 => 'danger',
self::PARTIALLY_PAID => 'warning',
self::PAID => 'success',
self::VOID => 'gray'
};
}
public function getIcon(): ?string
{
return match ($this) {
self::UNPAID => IconEnum::UNPAID->value,
self::PARTIALLY_PAID => IconEnum::PARTIALLY_PAID->value,
self::PAID => IconEnum::PAID->value,
self::VOID => IconEnum::VOID->value,
};
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Enums;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum OrderAttributes: string implements HasIcon, HasLabel
{
case new_art = 'New Art';
case repeat = 'Repeat';
case rush = 'Rush';
case event = 'Event';
case digitizing = 'Digitizing';
case garments = 'Garments';
case supplied_file = 'Customer Supplied File';
public function getLabel(): ?string
{
return $this->value;
}
public function getIcon(): ?string
{
return match ($this) {
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

@ -2,10 +2,52 @@
namespace App\Enums;
enum OrderStatus: string
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum OrderStatus: string implements HasColor, HasIcon, HasLabel
{
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
{
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::READY_FOR_INVOICE => 'invoicing',
self::INVOICED => 'invoiced',
};
}
public function getIcon(): ?string
{
return match ($this) {
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

@ -2,11 +2,24 @@
namespace App\Enums;
enum OrderType: string
use Filament\Support\Contracts\HasLabel;
enum OrderType: string implements HasLabel
{
case EMBROIDERY = 'Embroidery';
case SCREEN = '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
{
return match ($this) {
self::EMB => 'Embroidery',
self::SCP => 'Screen printing',
self::DTG => 'Direct-to-garment',
self::VINYL => 'Vinyl',
self::MISC => 'Misc',
};
}
}

View File

@ -2,9 +2,28 @@
namespace App\Enums;
enum ShippingType: string
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum ShippingType: string implements HasIcon, HasLabel
{
case THEY_SHIP = 'They ship';
case WE_SHIP = 'We ship';
case PICKUP = 'Pickup';
case OTHER = 'Other';
public function getLabel(): ?string
{
return $this->value;
}
public function getIcon(): ?string
{
return match ($this) {
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

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

View File

@ -0,0 +1,87 @@
<?php
namespace App\Filament\Admin\Pages;
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
use Filament\Facades\Filament;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\TextInput;
use Filament\Http\Responses\Auth\Contracts\LoginResponse;
use Filament\Models\Contracts\FilamentUser;
use Filament\Pages\Auth\Login;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Validation\ValidationException;
class UsernameLogin extends Login
{
protected function getEmailFormComponent(): Component
{
return TextInput::make('username')
->label('Username')
->required()
->autofocus()
->extraInputAttributes(['tabindex' => 1])
->autocomplete();
}
protected function getCredentialsFromFormData(array $data): array
{
return [
'username' => $data['username'],
'password' => $data['password'],
];
}
public function authenticate(): ?LoginResponse
{
try {
$this->rateLimit(5);
} catch (TooManyRequestsException $exception) {
$this->getRateLimitedNotification($exception)?->send();
return null;
}
$data = $this->form->getState();
if (! Filament::auth()->attempt($this->getCredentialsFromFormData($data), $data['remember'] ?? false)) {
$this->throwFailureValidationException();
}
$user = Filament::auth()->user();
if (($user instanceof FilamentUser) && (! $user->canAccessPanel(Filament::getCurrentPanel()))) {
Filament::auth()->logout();
$this->throwFailureValidationException();
} elseif ($user->customer_id !== null) {
Filament::auth()->logout();
throw ValidationException::withMessages([
'data.username' => 'Incorrect username or password.',
]);
}
session()->regenerate();
return app(LoginResponse::class);
}
protected function throwFailureValidationException(): never
{
throw ValidationException::withMessages([
'data.username' => __('filament-panels::pages/auth/login.messages.failed'),
]);
}
public function getTitle(): Htmlable|string
{
return __('Login');
}
public function getHeading(): Htmlable|string
{
return __('Login');
}
}

View File

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

View File

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

View File

@ -0,0 +1,21 @@
<?php
namespace App\Filament\Admin\Resources\ContactResource\Pages;
use App\Enums\IconEnum;
use App\Filament\Resources\ContactResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListContacts extends ListRecords
{
protected static string $resource = ContactResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make()
->icon(IconEnum::NEW->value),
];
}
}

View File

@ -0,0 +1,146 @@
<?php
namespace App\Filament\Admin\Resources;
use App\Enums\IconEnum;
use App\Models\Customer;
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\Sum;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\Builder;
class CustomerReportResource extends Resource
{
protected static ?string $model = Customer::class;
protected static ?string $navigationIcon = IconEnum::CUSTOMER_SALES->value;
protected static ?string $navigationGroup = 'Reports';
protected static ?string $navigationLabel = 'Customer Reports';
protected static ?int $navigationSort = 4;
public static function form(Form $form): Form
{
return $form
->schema([
//
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('company_name')
->label('Customer')
->sortable()
// ->searchable()
->extraHeaderAttributes(['class' => 'w-full']),
Tables\Columns\TextColumn::make('invoices.subtotal')
->label('Subtotal')
->money()
->alignRight()
->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('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(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('invoices.pst_amount')
->label('PST')
->money()
->alignRight()
->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('invoices.total')
->label('Total')
->money()
->weight(FontWeight::Bold)
->alignRight()
->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([
DatePicker::make('created_at')
->label('From date'),
]),
Tables\Filters\Filter::make('created_until')
->form([
DatePicker::make('created_until')
->label('Until date'),
]),
], 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
{
return auth()->user()->is_admin ?? false;
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => \App\Filament\Admin\Resources\CustomerReportResource\Pages\ListCustomerReports::route('/'),
];
}
}

View File

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

View File

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

View File

@ -0,0 +1,20 @@
<?php
namespace App\Filament\Admin\Resources\CustomerReportResource\Pages;
use App\Filament\Admin\Resources\CustomerReportResource;
use Filament\Resources\Pages\ListRecords;
class ListCustomerReports extends ListRecords
{
protected static string $resource = CustomerReportResource::class;
protected static ?string $title = 'Customer Reports';
protected function getHeaderActions(): array
{
return [
// Actions\CreateAction::make(),
];
}
}

View File

@ -0,0 +1,118 @@
<?php
namespace App\Filament\Admin\Resources;
use App\Enums\IconEnum;
use App\Filament\Admin\Resources\CustomerResource\RelationManagers\ContactsRelationManager;
use App\Filament\Admin\Resources\CustomerResource\RelationManagers\InvoicesRelationManager;
use App\Filament\Admin\Resources\CustomerResource\RelationManagers\PaymentsRelationManager;
use App\Filament\Admin\Resources\CustomerResource\RelationManagers\ShippingEntriesRelationManager;
use App\Models\Customer;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class CustomerResource extends Resource
{
protected static ?string $model = Customer::class;
protected static ?string $navigationIcon = IconEnum::CUSTOMER->value;
protected static ?string $navigationGroup = 'Management';
protected static ?int $navigationSort = 1;
public static function form(Form $form): Form
{
return $form
->columns(1)
->schema([
Section::make([
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),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('company_name')
->extraHeaderAttributes(['class' => 'w-full'])
->searchable()
->sortable(),
TextColumn::make('phone'),
TextColumn::make('balance')
->getStateUsing(fn (Customer $customer) => $customer->calculateBalance())
->money()
->hidden(! auth()->user()->is_admin ?? false),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
InvoicesRelationManager::class,
PaymentsRelationManager::class,
ContactsRelationManager::class,
ShippingEntriesRelationManager::class,
];
}
public static function getPages(): array
{
return [
'index' => \App\Filament\Admin\Resources\CustomerResource\Pages\ListCustomers::route('/'),
'edit' => \App\Filament\Admin\Resources\CustomerResource\Pages\EditCustomer::route('/{record}/edit'),
];
}
}

View File

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

View File

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

View File

@ -0,0 +1,24 @@
<?php
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;
class ListCustomers extends ListRecords
{
protected static string $resource = CustomerResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make()
->modal()
->modalWidth('lg')
->icon(IconEnum::NEW->value)
->successRedirectUrl(fn ($record) => CustomerResource::getUrl('edit', ['record' => $record->id])),
];
}
}

View File

@ -0,0 +1,58 @@
<?php
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;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class ContactsRelationManager extends RelationManager
{
protected static string $relationship = 'contacts';
public function form(Form $form): Form
{
return $form
->schema([
TextInput::make('first_name'),
TextInput::make('last_name'),
TextInput::make('email')
->email(),
TextInput::make('phone'),
TextInput::make('notes'),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('id')
->columns([
TextColumn::make('full_name'),
TextColumn::make('email'),
TextColumn::make('phone'),
TextColumn::make('notes')
->extraHeaderAttributes(['class' => 'w-full']),
])
->filters([
//
])
->headerActions([
Tables\Actions\CreateAction::make()
->icon(IconEnum::NEW->value),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
// Tables\Actions\BulkActionGroup::make([
// Tables\Actions\DeleteBulkAction::make(),
// ]),
]);
}
}

View File

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

View File

@ -0,0 +1,36 @@
<?php
namespace App\Filament\Admin\Resources\CustomerResource\RelationManagers;
use App\Filament\Admin\Resources\PaymentResource;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
class PaymentsRelationManager extends RelationManager
{
protected static string $relationship = 'payments';
public function form(Form $form): Form
{
return $form
->schema([
// PaymentResource
// Forms\Components\TextInput::make('amount')
// ->required()
// ->maxLength(255),
]);
}
public function table(Table $table): Table
{
return PaymentResource::table($table);
}
public static function canViewForRecord(Model $ownerRecord, string $pageClass): bool
{
return auth()->user()->is_admin ?? false;
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Filament\Admin\Resources\CustomerResource\RelationManagers;
use App\Enums\IconEnum;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
class ShippingEntriesRelationManager extends RelationManager
{
protected static string $relationship = 'ShippingEntries';
public function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('id')
->required()
->maxLength(255),
]);
}
public function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('courier'),
Tables\Columns\TextColumn::make('account_title'),
Tables\Columns\TextColumn::make('account_username')
->label('Username'),
Tables\Columns\TextColumn::make('account_password')
->label('Password'),
Tables\Columns\TextColumn::make('info_needed'),
// ->extraHeaderAttributes(['class' => 'w-full']),
Tables\Columns\TextColumn::make('notes'),
])
->filters([
//
])
->headerActions([
Tables\Actions\CreateAction::make()
->icon(IconEnum::NEW->value),
])
->actions([
// Tables\Actions\EditAction::make(),
// Tables\Actions\DeleteAction::make(),
]);
}
}

View File

@ -0,0 +1,130 @@
<?php
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;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Support\Enums\FontFamily;
use Filament\Support\Enums\FontWeight;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class InvoiceReportResource extends Resource
{
protected static ?string $navigationIcon = IconEnum::INVOICE_REPORT->value;
protected static ?string $navigationGroup = 'Reports';
protected static ?string $navigationLabel = 'Invoice Reports';
protected static ?int $navigationSort = 2;
public static function form(Form $form): Form
{
return $form
->schema([
Section::make([
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(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);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('internal_id')
->label('ID')
->fontFamily(FontFamily::Mono)
->color('primary')
->searchable()
->sortable(),
TextColumn::make('customer.company_name')
->extraHeaderAttributes(['class' => 'w-full'])
->searchable(),
TextColumn::make('date_start')
->label('Start Date')
->date('Y-m-d'),
TextColumn::make('date_end')
->label('End Date')
->date('Y-m-d'),
TextColumn::make('total')
->weight(FontWeight::Bold)
->money(),
TextColumn::make('balance')
->weight(FontWeight::Bold)
->money(),
])
->defaultSort('id', 'desc');
}
public static function canAccess(): bool
{
return auth()->user()->is_admin ?? false;
}
public static function getRelations(): array
{
return [
InvoicesRelationManager::class,
];
}
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}'),
];
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace App\Filament\Admin\Resources\InvoiceReportResource\Pages;
use Filament\Resources\Pages\CreateRecord;
class CreateInvoiceReport extends CreateRecord {}

View File

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

View File

@ -0,0 +1,48 @@
<?php
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;
class ListInvoiceReports extends ListRecords
{
protected static string $resource = InvoiceReportResource::class;
protected static ?string $title = 'Invoice Reports';
protected function getHeaderActions(): array
{
return [
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

@ -0,0 +1,45 @@
<?php
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
{
protected static string $resource = InvoiceReportResource::class;
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 [
Action::make('print')
->icon('lucide-printer')
->url(fn (InvoiceReport $record) => route('pdf.invoice-report', $record))
->openUrlInNewTab(),
];
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace App\Filament\Admin\Resources\InvoiceReportResource\RelationManagers;
use App\Filament\Admin\Resources\InvoiceResource;
use App\Models\Invoice;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Support\Enums\FontWeight;
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([
Forms\Components\TextInput::make('id')
->required()
->maxLength(255),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('internal_id')
->recordUrl(fn ($record) => InvoiceResource::getUrl('edit', ['record' => $record->id]))
->columns([
TextColumn::make('internal_id')
->label('ID')
->extraHeaderAttributes(['class' => 'w-full'])
->color('primary'),
TextColumn::make('date')
->label('Created')
->date('Y-m-d'),
TextColumn::make('subtotal')
->alignRight()
->money(),
TextColumn::make('gst_amount')
->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())
->money()
->weight(FontWeight::Bold),
TextColumn::make('status'),
])
->filters([
//
])
->headerActions([
])
->defaultSort('invoices.id')
->actions([
])
->bulkActions([
]);
}
}

View File

@ -0,0 +1,330 @@
<?php
namespace App\Filament\Admin\Resources;
use App\Enums\IconEnum;
use App\Enums\InvoiceStatus;
use App\Filament\Admin\Resources\CustomerResource\RelationManagers\InvoicesRelationManager;
use App\Filament\Admin\Resources\InvoiceResource\RelationManagers\OrdersRelationManager;
use App\Filament\Admin\Resources\InvoiceResource\RelationManagers\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;
use Filament\Forms\Components\Placeholder;
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;
use Illuminate\Support\Collection;
class InvoiceResource extends Resource
{
protected static ?string $model = Invoice::class;
protected static ?string $navigationIcon = IconEnum::INVOICE->value;
protected static ?string $navigationGroup = 'Financial';
protected static ?int $navigationSort = 1;
public static function form(Form $form): Form
{
return $form
->schema([
Group::make()
->schema([
Section::make([
Group::make([
Select::make('customer_id')
->required()
->label('Customer')
->options(Customer::all()->pluck('company_name', 'id'))
->reactive()
->searchable()
->disabledOn('edit')
->columnSpan(2),
Split::make([
DatePicker::make('date')
->required()
->default(today()),
DatePicker::make('due_date'),
])->columnSpan(2),
ToggleButtons::make('status')
->options(InvoiceStatus::class)
->required()
->inline()
->default(InvoiceStatus::UNPAID)
->columnSpan(2),
Grid::make(3)
->schema([
Toggle::make('has_gst')
->label('GST')
->inline(false)
->default(true),
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(fn (?Invoice $record) => $record === null ? 3 : 2),
Section::make()
->schema([
Placeholder::make('Id')
->label('ID')
->content(fn (Invoice $record): ?string => $record->internal_id),
Placeholder::make('Amounts')
->content(fn (Invoice $record): ?string => 'Total: $'.number_format($record->total, 2).', balance: $'.number_format($record->remainingBalance(), 2)),
Placeholder::make('Tax Rates when created')
->content(fn (Invoice $record): ?string => $record->gst_rate.'% GST, '.$record->pst_rate.'% PST, '.$record->hst_rate.'% HST'),
Placeholder::make('created_at')
->label('Timestamps')
->content(fn (Invoice $record): ?string => 'Created at '.$record->created_at->format('Y-m-d').', updated at '.$record->updated_at->format('Y-m-d')),
])
->columnSpan(1)
->hidden(fn (?Invoice $record) => $record === null)
->extraAttributes(['class' => 'h-full']),
])
->columns(3)
->columnSpan(2),
])->columns(2);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('internal_id')
->extraHeaderAttributes(fn ($livewire) => $livewire::class === InvoicesRelationManager::class ? ['class' => 'w-full'] : [false])
->label('ID')
->fontFamily('mono')
->color('primary')
->sortable()
->searchable(query: function (Builder $query, $search) {
return $query->where('internal_id', 'like', "%{$search}%")
->orWhereHas('orders', function (Builder $query) use ($search) {
return $query->where('customer_po', 'like', "%{$search}%")
->orWhere('internal_po', 'like', "%{$search}%");
});
}),
TextColumn::make('customer.company_name')
->hidden(fn ($livewire) => $livewire::class === InvoicesRelationManager::class)
->sortable()
->extraHeaderAttributes(['class' => 'w-full'])
->searchable(),
TextColumn::make('created_at')
->label('Created')
->date('Y-m-d')
->searchable()
->sortable(),
TextColumn::make('subtotal')
->money()
->alignRight()
->sortable()
->searchable(),
// FIXME: sortable doesn't sort correctly
TextColumn::make('gst_amount')
->label('GST/HST')
->money()
->getStateUsing(function (Invoice $record) {
return $record->has_gst
? '$'.number_format($record->gst_amount, 2)
: ($record->has_hst ? '$'.number_format($record->hst_amount, 2) : '-');
})
->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('pst_amount')
->label('PST')
->getStateUsing(function (Invoice $record) {
return $record->has_pst ? '$'.number_format($record->pst_amount, 2) : '-';
})
->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(),
])
->filters([
Tables\Filters\Filter::make('created_at')
->form([
DatePicker::make('created_from')
->label('From date'),
DatePicker::make('created_until')
->label('Until date'),
])
->query(function (Builder $query, array $data): Builder {
return $query
->when(
$data['created_from'],
fn (Builder $query, $date): Builder => $query->whereDate('date', '>=', $date),
)
->when(
$data['created_until'],
fn (Builder $query, $date): Builder => $query->whereDate('date', '<=', $date),
);
}),
Tables\Filters\SelectFilter::make('status')
->options(InvoiceStatus::class),
], )
->groups([
'status',
])
->defaultSort('id', 'desc')
->actions([
Tables\Actions\EditAction::make()
->hidden(fn ($livewire) => $livewire::class === InvoicesRelationManager::class),
])
->bulkActions([
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('Payment created successfully')
->success()
->send();
}),
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);
Notification::make()
->title(count($records).' item(s) saved successfully')
->success()
->send();
})
->icon('lucide-circle-x')
->deselectRecordsAfterCompletion(),
Tables\Actions\DeleteBulkAction::make(),
])
->label('Other actions'),
])
->selectCurrentPageOnly();
}
public static function canAccess(): bool
{
return auth()->user()->is_admin ?? false;
}
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'),
];
}
}

View File

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

View File

@ -0,0 +1,32 @@
<?php
namespace App\Filament\Admin\Resources\InvoiceResource\Pages;
use App\Filament\Admin\Resources\InvoiceResource;
use App\Models\Invoice;
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 [
Action::make('print')
->icon('lucide-printer')
->url(fn (Invoice $record) => route('invoice.pdf', $record))
->openUrlInNewTab(),
Actions\DeleteAction::make()
->icon('lucide-trash-2'),
];
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Filament\Admin\Resources\InvoiceResource\Pages;
use App\Enums\IconEnum;
use App\Enums\InvoiceStatus;
use App\Filament\Admin\Resources\InvoiceResource;
use Filament\Actions;
use Filament\Resources\Components\Tab;
use Filament\Resources\Pages\ListRecords;
class ListInvoices extends ListRecords
{
protected static string $resource = InvoiceResource::class;
public function getTabs(): array
{
return [
'all' => Tab::make('All')
->icon(IconEnum::TAB_ALL->value),
'unpaid' => Tab::make('Unpaid')
->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()),
'void' => Tab::make('Void')
->query(fn ($query) => $query->where('status', InvoiceStatus::VOID))
->icon(InvoiceStatus::VOID->getIcon()),
];
}
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make()
->modal()
->modalWidth('lg')
->icon(IconEnum::NEW->value)
->successRedirectUrl(fn ($record) => InvoiceResource::getUrl('edit', ['record' => $record->id])),
];
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace App\Filament\Admin\Resources\InvoiceResource\RelationManagers;
use App\Filament\Admin\Resources\OrderResource;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Support\Enums\FontFamily;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
class OrdersRelationManager extends RelationManager
{
protected static string $relationship = 'orders';
public function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('customer_po')
->required()
->maxLength(100),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('customer_po')
->recordUrl(fn ($record) => OrderResource::getUrl('edit', ['record' => $record->id]))
->columns([
Tables\Columns\TextColumn::make('internal_po')
->label('Internal PO')
->color('primary')
->fontFamily(FontFamily::Mono),
Tables\Columns\TextColumn::make('customer_po')
->label('Customer PO')
->color('code')
->weight('bold')
->extraHeaderAttributes(['class' => 'w-full']),
Tables\Columns\TextColumn::make('total_product_quantity')
->label('Total QTY')
->alignRight(),
Tables\Columns\TextColumn::make('total_service_price')
->alignRight()
->label('Total price')
->money(),
])
->filters([
//
])
->headerActions([
Tables\Actions\AssociateAction::make()
->multiple()
->preloadRecordSelect()
->recordSelectOptionsQuery(fn (Builder $query) => $query->where('customer_id', $this->ownerRecord->customer->id))
->after(function () {
$this->ownerRecord->calculateTotals();
}),
])
->actions([
Tables\Actions\DissociateAction::make()
->after(function () {
$this->ownerRecord->calculateTotals();
}),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DissociateBulkAction::make(),
]),
])
->inverseRelationship('invoice');
}
}

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

@ -0,0 +1,64 @@
<?php
namespace App\Filament\Admin\Resources\InvoiceResource\RelationManagers;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
class ProductServicesRelationManager extends RelationManager
{
protected static string $relationship = 'ProductServices';
public function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('id')
->required()
->maxLength(255),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('id')
->columns([
Tables\Columns\TextColumn::make('order.internal_po')
->label('WO')
->color('primary')
->fontFamily('mono')
->sortable(),
Tables\Columns\TextColumn::make('order.customer_po')
->label('PO')
->color('code')
->weight('bold')
->sortable(),
Tables\Columns\TextColumn::make('serviceType.name')
->label('Type')
->weight('bold')
->sortable(),
Tables\Columns\TextColumn::make('service_details'),
Tables\Columns\TextColumn::make('amount')
->label('QTY'),
Tables\Columns\TextColumn::make('amount_price')
->label('Rate')
->prefix('$'),
Tables\Columns\TextColumn::make('price')
->label('Amount')
->prefix('$'),
])
->filters([
])
->headerActions([
])
->actions([
])
->bulkActions([
])
->defaultPaginationPageOption('all');
}
}

View File

@ -0,0 +1,471 @@
<?php
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\Invoice;
use App\Models\Order;
use App\Models\OrderProduct;
use App\Models\ProductService;
use App\Models\ServiceFile;
use App\Models\ServiceType;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Group;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Split;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
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;
use Guava\FilamentClusters\Forms\Cluster;
use Icetalker\FilamentTableRepeater\Forms\Components\TableRepeater;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
class OrderResource extends Resource
{
protected static ?string $model = Order::class;
protected static ?string $navigationIcon = IconEnum::ORDER->value;
protected static ?string $navigationGroup = 'Production';
public static function form(Form $form): Form
{
return $form->schema([
Group::make()
->schema([
Section::make([
Grid::make(1)
->schema([
Select::make('order_type')
->required()
->options(OrderType::class)
->searchable(),
Select::make('customer_id')
->required()
->label('Customer')
->relationship(name: 'customer', titleAttribute: 'company_name')
->preload()
->createOptionForm(fn ($form) => CustomerResource::form($form))
->createOptionAction(fn ($action) => $action->modalWidth('lg'))
->searchable(),
TextInput::make('customer_po')
->required()
->label('Customer PO'),
Split::make([
DatePicker::make('order_date')
->required()
->default(today()),
DatePicker::make('due_date')
->required()
->default(today()->add('10 days')),
]),
Textarea::make('notes')
->rows(3),
])->columnSpan(1),
Grid::make(1)
->schema([
ToggleButtons::make('status')
->required()
->default(OrderStatus::DRAFT->value)
->options(OrderStatus::class)
->inline(),
ToggleButtons::make('order_attributes')
->options(OrderAttributes::class)
->multiple()
->inline(),
ToggleButtons::make('printed')
->boolean()
->default(false)
->inline(),
ToggleButtons::make('pre_production')
->label('Pre-production')
->default(false)
->boolean()
->inline()
->colors([
'true' => 'info',
'false' => 'info',
]),
])->columnSpan(1),
])->columns(2)
->columnSpan(fn (?Order $record) => $record === null ? 6 : 5),
Section::make()
->schema([
Placeholder::make('ID')
->label('Order ID')
->content(fn (Order $record): ?string => $record->internal_po),
Placeholder::make('total_service_price')
->label('Total service price')
->content(fn (Order $record): ?string => '$'.number_format($record->total_service_price, 2)),
Placeholder::make('created_at')
->label('Created')
->content(fn (Order $record): ?string => $record->created_at?->diffForHumans()),
Placeholder::make('updated_at')
->label('Last modified')
->content(fn (Order $record): ?string => $record->updated_at?->diffForHumans()),
])
->columnSpan(1)
->hidden(fn (?Order $record) => $record === null)
->extraAttributes(['class' => 'h-full']),
])
->columns(6)
->columnSpan(2),
TableRepeater::make('order_products')
->label('Garments')
->schema([
TextInput::make('sku')
->datalist(OrderProduct::all()->unique('sku')->pluck('sku')->toArray()),
TextInput::make('product_name')
->datalist(OrderProduct::all()->unique('product_name')->pluck('product_name')->toArray())
->required(),
TextInput::make('color')
->datalist(OrderProduct::all()->unique('color')->pluck('color')->toArray()),
Cluster::make([
TextInput::make('xs')
->placeholder('xs')
->rules('numeric'),
TextInput::make('s')
->placeholder('s')
->rules('numeric'),
TextInput::make('m')
->placeholder('m')
->rules('numeric'),
TextInput::make('l')
->placeholder('l')
->rules('numeric'),
TextInput::make('xl')
->placeholder('xl')
->rules('numeric'),
TextInput::make('2xl')
->placeholder('2xl')
->rules('numeric'),
TextInput::make('3xl')
->placeholder('3xl')
->rules('numeric'),
TextInput::make('osfa')
->placeholder('osfa')
->rules('numeric'),
])
->label('Sizes'),
])
->reorderable()
->cloneable()
->defaultItems(1),
Repeater::make('services')
->view('filament.forms.compact-repeater')
->label('Product Services')
->schema([
Grid::make(19)
->schema([
Select::make('serviceType')
->options(ServiceType::all()->pluck('value', 'id'))
->columnSpan(4)
->placeholder('Select...')
->searchable()
->createOptionForm([
TextInput::make('name')
->label('Code')
->placeholder('Abbreviation here (example: \'Emb\'')
->required(),
TextInput::make('value')
->placeholder('Full name here (example: \'Embroidery\'')
->required(),
])
->createOptionUsing(function (array $data): int {
return ServiceType::create($data)->getKey();
}),
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)
->rules('numeric'),
Cluster::make([
TextInput::make('serviceFileWidth')
->prefix('w')
->rules('numeric'),
TextInput::make('serviceFileHeight')
->prefix('h')
->rules('numeric'),
])
->label('Dimensions (inches)')
->columnSpan(4),
TextInput::make('amount')
->label('Quantity')
->live()
->prefix('#')
->columnSpan(2)
->rules('numeric'),
TextInput::make('amount_price')
->label('Amount')
->prefix('$')
->columnSpan(2)
->rules('numeric'),
]),
Grid::make(9)
->schema([
TextInput::make('serviceFileCode')
->label('Code')
->datalist(ServiceFile::all()->unique('code')->pluck('code')->toArray())
->columnSpan(1)
->placeholder('A1234'),
Textarea::make('notes')
->placeholder('Thread colors...')
->columnSpan(8),
]),
])
->reorderable()
->cloneable()
->columns(4)
->columnSpan(2)
->defaultItems(1),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\IconColumn::make('alert')
->getStateUsing(fn ($record) => $record->is_alert_danger || $record->is_alert_warning)
->label('')
->color(fn ($record) => $record->is_alert_danger ? 'danger' : 'warning')
->icon(function ($record) {
return $record->is_alert_danger
? 'lucide-calendar-clock' : ($record->rush
? OrderAttributes::rush->getIcon() : null);
})
->size(IconColumnSize::Medium),
TextColumn::make('internal_po')
->label('Internal PO')
->fontFamily('mono')
->color('info')
->searchable(query: function (Builder $query, $search) {
return $query
->where('internal_po', 'like', "%{$search}%")
->orWhereHas('productServices', function (Builder $query) use ($search) {
return $query->where('placement', 'like', "%{$search}%")
->orWhereHas('serviceFile', function (Builder $query) use ($search) {
return $query->where('code', 'like', "%{$search}%")
->orWhere('name', 'like', "%{$search}%");
});
});
})
->sortable(),
TextColumn::make('customer.company_name')
->searchable()
->sortable(),
TextColumn::make('customer_po')
->label('PO')
->wrap()
->weight('bold')
->color('code')
->searchable()
->sortable()
->extraHeaderAttributes([
'class' => 'w-full',
]),
TextColumn::make('order_date')
->searchable()
->sortable(),
TextColumn::make('due_date')
->searchable()
->sortable(),
TextColumn::make('status')
->badge()
->searchable()
->sortable(),
])
->defaultSort('order_date', 'desc')
->filters([
Tables\Filters\Filter::make('order_date')
->form([
DatePicker::make('created_from'),
DatePicker::make('created_until'),
])
->query(function (Builder $query, array $data): Builder {
return $query
->when(
$data['created_from'],
fn (Builder $query, $date): Builder => $query->whereDate('order_date', '>=', $date),
)
->when(
$data['created_until'],
fn (Builder $query, $date): Builder => $query->whereDate('order_date', '<=', $date),
);
}),
], )
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkAction::make('updateStatus')
->form([
Select::make('status')
->options(OrderStatus::class),
])
->modalHeading('Change selected orders status')
->modalWidth(MaxWidth::Medium)
->action(function (array $data, Collection $records): void {
foreach ($records as $record) {
$record->status = $data['status'];
$record->save();
}
Notification::make()
->title(count($records).' item(s) updated successfully')
->success()
->send();
})
->icon('lucide-pen')
->color('info')
->deselectRecordsAfterCompletion(),
BulkActionGroup::make([
BulkAction::make('Create individual invoices')
->icon(IconEnum::INVOICE->value)
->action(function (Collection $records): void {
[$invoiced, $toInvoice] = $records->partition(fn ($record) => $record->invoice);
$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
{
return [
'index' => \App\Filament\Admin\Resources\OrderResource\Pages\ListOrders::route('/'),
'create' => \App\Filament\Admin\Resources\OrderResource\Pages\CreateOrder::route('/create'),
'edit' => \App\Filament\Admin\Resources\OrderResource\Pages\EditOrder::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace App\Filament\Admin\Resources\OrderResource\Pages;
use App\Enums\OrderAttributes;
use App\Filament\Admin\Resources\OrderResource;
use App\Models\Order;
use App\Models\OrderProduct;
use App\Models\ProductService;
use App\Models\ProductSize;
use App\Models\ServiceFile;
use App\Models\ServiceType;
use Filament\Resources\Pages\CreateRecord;
class CreateOrder extends CreateRecord
{
protected static string $resource = OrderResource::class;
protected function handleRecordCreation(array $data): Order
{
// Attributes
foreach (OrderAttributes::cases() as $case) {
$data[$case->name] = false;
}
$data['order_attributes'] = array_filter($data['order_attributes']);
foreach ($data['order_attributes'] as $attribute) {
$data[OrderAttributes::from($attribute)->name] = true;
}
unset($data['order_attributes']);
$order = Order::create($data);
// Create Order Products
foreach ($data['order_products'] as $product) {
$orderProduct = OrderProduct::create([
'sku' => $product['sku'],
'product_name' => $product['product_name'],
'color' => $product['color'],
'order_id' => $order->id,
]);
$sizes = ['xs', 's', 'm', 'l', 'xl', '2xl', '3xl', 'osfa'];
foreach ($sizes as $size) {
if ($product[$size] > 0) {
ProductSize::create([
'amount' => $product[$size],
'size' => $size,
'order_product_id' => $orderProduct->id,
]);
}
}
}
// ProductServices and ServiceFiles
foreach ($data['services'] as $service) {
$serviceFile = ServiceFile::create([
'name' => strtoupper($service['serviceFileName']) ?? '',
'code' => strtoupper($service['serviceFileCode']) ?? '',
'width' => $service['serviceFileWidth'] ?? null,
'height' => $service['serviceFileHeight'] ?? null,
'setup_number' => $service['serviceFileSetupNumber'] ?? null,
]);
ProductService::create([
'service_type_id' => ServiceType::findOrFail($service['serviceType'])->id ?? null,
'placement' => strtoupper($service['placement']) ?? null,
'notes' => strtoupper($service['notes']) ?? null,
'amount' => $service['amount'] ?? null,
'amount_price' => $service['amount_price'] ?? null,
'total_price' => $service['total_price'] ?? null,
'service_file_id' => $serviceFile->id,
'order_id' => $order->id,
]);
}
return $order;
}
}

View File

@ -0,0 +1,241 @@
<?php
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;
use App\Models\ProductSize;
use App\Models\ServiceFile;
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']);
// Order Products
foreach ($order->orderProducts as $key => $product) {
$data['order_products'][$key] = [
'sku' => $product->sku,
'product_name' => $product->product_name,
'color' => $product->color,
'xs' => $product->productSizes->where('size', 'xs')->first()->amount ?? null,
's' => $product->productSizes->where('size', 's')->first()->amount ?? null,
'm' => $product->productSizes->where('size', 'm')->first()->amount ?? null,
'l' => $product->productSizes->where('size', 'l')->first()->amount ?? null,
'xl' => $product->productSizes->where('size', 'xl')->first()->amount ?? null,
'2xl' => $product->productSizes->where('size', '2xl')->first()->amount ?? null,
'3xl' => $product->productSizes->where('size', '3xl')->first()->amount ?? null,
'osfa' => $product->productSizes->where('size', 'osfa')->first()->amount ?? null,
];
}
// Product Services
foreach ($order->productServices as $key => $service) {
$data['services'][$key] = [
'placement' => $service->placement ?? '',
'amount' => $service->amount ?? '',
'amount_price' => $service->amount_price ?? '',
'notes' => $service->notes ?? '',
'serviceType' => $service->serviceType->id ?? '',
'serviceFileName' => $service->serviceFile->name ?? '',
'serviceFileWidth' => $service->serviceFile->width ?? '',
'serviceFileHeight' => $service->serviceFile->height ?? '',
'serviceFileCode' => $service->serviceFile->code ?? '',
'serviceFileSetupNumber' => $service->serviceFile->setup_number ?? '',
];
}
foreach (OrderAttributes::cases() as $case) {
if ($data[$case->name]) {
$data['order_attributes'][] = $case->value ?? null;
}
}
return $data;
}
public function handleRecordUpdate(Model $record, array $data): Model
{
// Correctly set attribute booleans
foreach (OrderAttributes::cases() as $case) {
$data[$case->name] = false;
}
$data['order_attributes'] = array_filter($data['order_attributes']);
foreach ($data['order_attributes'] as $attribute) {
$data[OrderAttributes::from($attribute)->name] = true;
}
unset($data['order_attributes']);
$record->update($data);
// Delete old and create new Order Products
foreach ($record->orderProducts as $product) {
foreach ($product->productSizes as $size) {
$size->delete();
}
$product->delete();
}
foreach ($data['order_products'] as $product) {
$orderProduct = OrderProduct::create([
'sku' => $product['sku'],
'product_name' => $product['product_name'],
'color' => $product['color'],
'order_id' => $record->id,
]);
$sizes = ['xs', 's', 'm', 'l', 'xl', '2xl', '3xl', 'osfa'];
foreach ($sizes as $size) {
if ($product[$size] > 0) {
ProductSize::create([
'amount' => $product[$size],
'size' => $size,
'order_product_id' => $orderProduct->id,
]);
}
}
}
// Delete old and create new services
foreach ($record->productServices as $service) {
$service->delete();
}
foreach ($data['services'] as $service) {
$serviceFile = ServiceFile::create([
'name' => strtoupper($service['serviceFileName']) ?? '',
'code' => strtoupper($service['serviceFileCode']) ?? '',
'width' => $service['serviceFileWidth'] ?? null,
'height' => $service['serviceFileHeight'] ?? null,
'setup_number' => $service['serviceFileSetupNumber'] ?? null,
]);
ProductService::create([
'service_type_id' => ServiceType::findOrFail($service['serviceType'])->id ?? null,
'placement' => strtoupper($service['placement']) ?? null,
'notes' => strtoupper($service['notes']) ?? null,
'amount' => $service['amount'] ?? null,
'amount_price' => $service['amount_price'] ?? null,
'total_price' => $service['total_price'] ?? null,
'service_file_id' => $serviceFile->id,
'order_id' => $record->id,
]);
}
return $record;
}
protected function getHeaderActions(): array
{
return [
Action::make('save')
->label('Save changes')
->action('save')
->icon(IconEnum::SAVE->value),
Actions\ReplicateAction::make()
->label('Duplicate')
->icon(IconEnum::COPY->value)
->mutateRecordDataUsing(function (array $data): array {
$po = 'Duplicate of '.$data['customer_po'];
$data['customer_po'] = $po;
return $data;
})
->beforeReplicaSaved(function (Order $replica): void {
$replica->customer_po = 'Repeat of '.$replica->customer_po;
$replica->status = OrderStatus::DRAFT;
$replica->printed = false;
$replica->pre_production = false;
$replica->order_date = today();
$replica->due_date = today()->addDays(10);
$replica->save();
foreach ($this->record->orderProducts as $product) {
$newProduct = $product->replicate();
$newProduct->order_id = $replica->id;
$newProduct->save();
foreach ($product->productSizes as $size) {
$newSize = $size->replicate();
$newSize->order_product_id = $newProduct->id;
$newSize->save();
}
}
/** @var ProductService $service */
foreach ($this->record->productServices as $service) {
/** @var ServiceFile $newServiceFile */
$newServiceFile = $service->serviceFile->replicate();
$newService = $service->replicate();
$newService->order_id = $replica->id;
$newService->service_file_id = $newServiceFile->id;
$newService->save();
}
})
->successRedirectUrl(fn (Model $replica): string => OrderResource::getUrl('edit', [$replica])),
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(IconEnum::PRINT->value)
->url(fn (Order $record) => route('orders.pdf', $record))
->openUrlInNewTab(),
Actions\DeleteAction::make()
->icon(IconEnum::TRASH->value),
];
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace App\Filament\Admin\Resources\OrderResource\Pages;
use App\Enums\IconEnum;
use App\Enums\OrderAttributes;
use App\Enums\OrderStatus;
use App\Filament\Admin\Resources\OrderResource;
use App\Models\Order;
use Filament\Actions;
use Filament\Resources\Components\Tab;
use Filament\Resources\Pages\ListRecords;
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()
->icon(IconEnum::NEW->value),
];
}
public function getTabs(): array
{
return [
'all' => Tab::make('All')
->icon(IconEnum::TAB_ALL->value),
'active' => Tab::make()
->query(fn ($query) => $this->excludeStatuses($query))
->icon(OrderStatus::PRODUCTION->getIcon())
->badge(fn () => $this->getBadgeCount(fn ($query) => $this->excludeStatuses($query))),
'unprinted' => Tab::make()
->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(fn ($query) => $this->excludeStatuses($query)->whereDate('due_date', '<=', today()))
->icon(IconEnum::TAB_OVERDUE->value)
->badge(fn () => $this->getBadgeCount(fn ($query) => $query->whereDate('due_date', '<=', today())))
->badgeColor('danger'),
'rush' => Tab::make()
->query(fn ($query) => $this->excludeStatuses($query)->where('rush', true))
->icon(OrderAttributes::rush->getIcon())
->badge(fn () => $this->getBadgeCount(fn ($query) => $query->where('rush', true)))
->badgeColor('warning'),
'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

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

View File

@ -0,0 +1,54 @@
<?php
namespace App\Filament\Admin\Resources\OrderResource\RelationManagers;
use App\Enums\IconEnum;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
class OrderProductsRelationManager extends RelationManager
{
protected static string $relationship = 'orderProducts';
public function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('id')
->required()
->maxLength(255),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('id')
->columns([
Tables\Columns\TextColumn::make('sku'),
Tables\Columns\TextColumn::make('product_name'),
Tables\Columns\TextColumn::make('color'),
])
->filters([
//
])
->headerActions([
Tables\Actions\CreateAction::make()
->icon(IconEnum::NEW->value),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make()
->icon(IconEnum::TRASH->value),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make()
->icon(IconEnum::TRASH->value),
]),
]);
}
}

View File

@ -0,0 +1,119 @@
<?php
namespace App\Filament\Admin\Resources;
use App\Enums\IconEnum;
use App\Models\Customer;
use App\Models\Order;
use App\Models\PackingSlip;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Split;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class PackingSlipResource extends Resource
{
protected static ?string $model = PackingSlip::class;
protected static ?string $navigationIcon = IconEnum::PACKING_SLIP->value;
protected static ?string $navigationGroup = 'Production';
protected static ?int $navigationSort = 2;
public static function form(Form $form): Form
{
return $form
->schema([
Section::make([
Split::make([
Grid::make(1)
->schema([
DatePicker::make('date_received')
->default(today())
->required(),
Select::make('customer_id')
->label('Customer')
->options(Customer::all()->pluck('company_name', 'id'))
->reactive()
->searchable(),
Select::make('order_id')
->label('Order')
->options(fn ($get): array => Order::where('customer_id', $get('customer_id') ?? null)
->get()
->pluck('customer_po', 'id')
->toArray())
->searchable(),
])
->columnSpan(1),
Grid::make(1)
->schema([
TextArea::make('contents')
->rows(9),
])
->columnSpan(1),
]),
]),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('date_received')
->sortable()
->searchable(),
TextColumn::make('order.customer_po')
// ->url(fn ($record) => $record->to)
->url(fn ($record) => OrderResource::getUrl('edit', ['record' => $record->id]))
->weight('bold')
->color('code')
->sortable()
->searchable(),
TextColumn::make('contents')
->extraHeaderAttributes(['class' => 'w-full']),
// TextColumn::make('amount')
// ->label('Quantity'),
TextColumn::make('order.customer.company_name')
->sortable()
->searchable(),
])
->defaultSort('date_received', 'desc')
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => \App\Filament\Admin\Resources\PackingSlipResource\Pages\ListPackingSlips::route('/'),
// 'create' => \App\Filament\Admin\Resources\PackingSlipResource\Pages\CreatePackingSlip::route('/create'),
// 'edit' => \App\Filament\Admin\Resources\PackingSlipResource\Pages\EditPackingSlip::route('/{record}/edit'),
];
}
}

View File

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

View File

@ -0,0 +1,21 @@
<?php
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;
class EditPackingSlip extends EditRecord
{
protected static string $resource = PackingSlipResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make()
->icon(IconEnum::TRASH->value),
];
}
}

View File

@ -0,0 +1,21 @@
<?php
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;
class ListPackingSlips extends ListRecords
{
protected static string $resource = PackingSlipResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make()
->icon(IconEnum::NEW->value),
];
}
}

View File

@ -0,0 +1,149 @@
<?php
namespace App\Filament\Admin\Resources;
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\Columns\TextColumn;
use Filament\Tables\Table;
class PaymentResource extends Resource
{
protected static ?string $model = Payment::class;
protected static ?string $navigationIcon = IconEnum::PAYMENTS->value;
protected static ?string $navigationGroup = 'Financial';
protected static ?int $navigationSort = 2;
public static function form(Form $form): Form
{
return $form
->schema([
Section::make([
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, ?bool $showSearchable = true): Table
{
return $table
->columns([
TextColumn::make('created_at')
->label('Date')
->date('Y-m-d')
->searchable($showSearchable),
TextColumn::make('customer.company_name')
->hidden(fn ($livewire) => $livewire::class !== Pages\ListPayments::class)
->searchable($showSearchable),
TextColumn::make('check_number')
->searchable($showSearchable)
->extraHeaderAttributes(['class' => 'w-full']),
TextColumn::make('amount')
->searchable($showSearchable)
->alignRight()
->money(),
TextColumn::make('unapplied_amount')
->searchable($showSearchable)
->label('Balance')
->alignRight()
->money(),
])
->actions([
\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 ?? false;
}
public static function getRelations(): array
{
return [
InvoicesRelationManager::class,
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListPayments::route('/'),
'edit' => Pages\EditPayment::route('/{record}/edit'),
];
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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

@ -0,0 +1,272 @@
<?php
namespace App\Filament\Admin\Resources;
use App\Enums\IconEnum;
use App\Models\Customer;
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;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Icetalker\FilamentTableRepeater\Forms\Components\TableRepeater;
class QuoteResource extends Resource
{
protected static ?string $model = Quote::class;
protected static ?string $navigationIcon = IconEnum::QUOTE->value;
protected static ?string $navigationGroup = 'Production';
protected static ?int $navigationSort = 1;
public static function form(Form $form): Form
{
return $form
->schema([
Grid::make(3)
->schema([
Section::make([
Group::make([
Select::make('customer_id')
->required()
->label('Customer')
->options(Customer::all()->pluck('company_name', 'id'))
->reactive()
->searchable()
->columnSpan(1),
DatePicker::make('date')
->default(today())
->required(),
TextArea::make('notes')
->rows(3)
->columnSpan(2),
]),
])
->columns(2)
->columnSpan(fn (?Quote $record) => $record === null ? 3 : 2)
->extraAttributes(['class' => 'h-full']),
Section::make()
->schema([
Placeholder::make('Id')
->label('ID')
->content(fn (Quote $record): ?string => $record->id),
Placeholder::make('created_at')
->content(fn (Quote $record): ?string => $record->created_at?->diffForHumans().' at '.$record->created_at->format('Y-m-d')),
Placeholder::make('updated_at')
->content(fn (Quote $record): ?string => $record->updated_at?->diffForHumans().' at '.$record->updated_at->format('Y-m-d')),
])
->columnSpan(1)
->hidden(fn (?Quote $record) => $record === null)
->extraAttributes(['class' => 'h-full']),
]),
TableRepeater::make('embroideryEntries')
->relationship('embroideryEntries')
->schema([
TextInput::make('logo')
->label('Logo name'),
TextInput::make('placement'),
TextInput::make('quantity')
->prefix('#')
->rules('numeric'),
TextInput::make('width')
->suffix('inch')
->rules('numeric'),
TextInput::make('height')
->suffix('inch')
->rules('numeric'),
TextInput::make('stitch_count'),
TextInput::make('digitizing_cost')
->prefix('$')
->rules('numeric'),
TextInput::make('run_charge')
->prefix('$')
->rules('numeric'),
])
->addActionLabel('Add Embroidery Entry')
->reorderable()
->defaultItems(0)
->colStyles([
'logo' => 'width: 15%',
'placement' => 'width: 15%',
'quantity' => 'width: 10%',
'width' => 'width: 11%',
'height' => 'width: 11%',
'stitch_count' => 'width: 16%',
'digitizing_cost' => 'width: 11%',
'run_charge' => 'width: 11%',
]),
TableRepeater::make('screenPrintEntries')
->relationship('screenPrintEntries')
->schema([
TextInput::make('logo')
->label('Logo name')
->columnSpan(2),
TextInput::make('placement'),
TextInput::make('quantity')
->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'),
TextInput::make('flash')
->rules(['numeric']),
TextInput::make('fleece')
->rules('numeric'),
TextInput::make('poly_ink')
->rules('numeric'),
TextInput::make('run_charge')
->rules('numeric'),
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: 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')
->relationship('heatTransferEntries')
->schema([
TextInput::make('logo')
->label('Logo name'),
TextInput::make('placement'),
TextInput::make('quantity')
->prefix('#')
->rules('numeric'),
TextInput::make('width')
->suffix('inch')
->rules('numeric'),
TextInput::make('height')
->rules('numeric')
->suffix('inch'),
TextInput::make('price')
->rules('numeric')
->prefix('$'),
])
->addActionLabel('Add Heat Transfer Entry')
->defaultItems(0)
->reorderable()
->colStyles([
'logo' => 'width: 25%',
'placement' => 'width: 25%',
'quantity' => 'width: 10%',
'width' => 'width: 11%',
'height' => 'width: 11%',
'price' => 'width: 15%',
]),
])->columns(1);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('id')
->color('primary')
->searchable(),
TextColumn::make('internal_id'),
TextColumn::make('date')
->date('Y-m-d')
->sortable()
->searchable(),
TextColumn::make('customer.company_name')
->sortable()
->searchable(),
TextColumn::make('notes')
->searchable()
->extraHeaderAttributes(['class' => 'w-full']),
TextColumn::make('total')
->money(),
])
->defaultSort('created_at', 'desc')
->groups([
'customer.company_name',
])
->filters([
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
]);
}
public static function canAccess(): bool
{
return auth()->user()->is_admin ?? false;
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => \App\Filament\Admin\Resources\QuoteResource\Pages\ListQuotes::route('/'),
'create' => \App\Filament\Admin\Resources\QuoteResource\Pages\CreateQuote::route('/create'),
'edit' => \App\Filament\Admin\Resources\QuoteResource\Pages\EditQuote::route('/{record}/edit'),
];
}
}

View File

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

View File

@ -0,0 +1,38 @@
<?php
namespace App\Filament\Admin\Resources\QuoteResource\Pages;
use App\Enums\IconEnum;
use App\Filament\Admin\Resources\QuoteResource;
use App\Models\Quote;
use Filament\Actions;
use Filament\Actions\Action;
use Filament\Resources\Pages\EditRecord;
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(IconEnum::SAVE->value),
Action::make('print')
->icon(IconEnum::PRINT->value)
->url(fn (Quote $record) => route('pdf.quote', $record))
->openUrlInNewTab(),
Actions\DeleteAction::make()
->icon(IconEnum::TRASH->value),
];
}
}

View File

@ -0,0 +1,21 @@
<?php
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;
class ListQuotes extends ListRecords
{
protected static string $resource = QuoteResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make()
->icon(IconEnum::NEW->value),
];
}
}

View File

@ -0,0 +1,158 @@
<?php
namespace App\Filament\Admin\Resources;
use App\Enums\IconEnum;
use App\Filament\Admin\Resources\ServiceTypeResource\Widgets\ServiceTypeOverview;
use App\Models\ServiceType;
use Filament\Forms\Components\DatePicker;
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;
class ServiceTypeResource extends Resource
{
protected static ?string $model = ServiceType::class;
protected static ?string $navigationIcon = IconEnum::DEFAULT->value;
protected static ?string $navigationGroup = 'Reports';
protected static ?string $label = 'Product Services';
protected static ?int $navigationSort = 2;
public static function getWidgets(): array
{
return [
ServiceTypeOverview::class,
];
}
public static function form(Form $form): Form
{
return $form
->schema([
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label('Code'),
TextColumn::make('value')
->label('Long Name')
->extraHeaderAttributes(['class' => 'w-full']),
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')
)
),
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']
);
})
->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')
)
),
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']
);
}),
TextColumn::make('averagePrice')
->getStateUsing(function (Table $table, Model $record) {
return $record->getAveragePriceAttribute(
$table->getFilter('created_at')->getState()['created_at'],
$table->getFilter('created_until')->getState()['created_until']
);
})
->alignRight()
->label('Average')
->prefix('$'),
])
->filters([
Tables\Filters\Filter::make('created_at')
->form([
DatePicker::make('created_at')
->label('From date'),
]),
Tables\Filters\Filter::make('created_until')
->form([
DatePicker::make('created_until')
->label('Until date'),
]),
], layout: Tables\Enums\FiltersLayout::AboveContent)
// ])
->actions([
])
->bulkActions([
]);
}
public static function canAccess(): bool
{
return auth()->user()->is_admin ?? false;
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => \App\Filament\Admin\Resources\ServiceTypeResource\Pages\ListServiceTypes::route('/'),
'create' => \App\Filament\Admin\Resources\ServiceTypeResource\Pages\CreateServiceType::route('/create'),
];
}
}

View File

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

View File

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

View File

@ -0,0 +1,25 @@
<?php
namespace App\Filament\Admin\Resources\ServiceTypeResource\Pages;
use App\Filament\Admin\Resources\ServiceTypeResource;
use Filament\Resources\Pages\ListRecords;
class ListServiceTypes extends ListRecords
{
protected static string $resource = ServiceTypeResource::class;
protected function getHeaderWidgets(): array
{
return [
// ServiceTypeResource\Widgets\ServiceTypeOverview::class,
];
}
protected function getHeaderActions(): array
{
return [
// Actions\CreateAction::make(),
];
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Filament\Admin\Resources\ServiceTypeResource\Widgets;
use Filament\Widgets\ChartWidget;
class ServiceTypeOverview extends ChartWidget
{
protected static ?string $heading = 'Services';
protected static ?string $maxHeight = '200px';
protected function getData(): array
{
return [
'datasets' => [
[
'label' => 'Test Label',
'data' => [30, 15, 25, 30],
],
],
'labels' => [
'Test 1',
'Test 2',
'Test 3',
'Test 4',
],
];
}
protected function getType(): string
{
return 'pie';
}
}

View File

@ -0,0 +1,155 @@
<?php
namespace App\Filament\Admin\Resources;
use App\Enums\IconEnum;
use App\Enums\ShippingType;
use App\Models\ShippingEntry;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Split;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Support\Enums\IconPosition;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Grouping\Group;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
class ShippingEntryResource extends Resource
{
protected static ?string $model = ShippingEntry::class;
protected static ?string $navigationIcon = IconEnum::SHIPPING_ENTRY->value;
protected static ?string $navigationGroup = 'Management';
protected static ?int $navigationSort = 3;
public static function form(Form $form): Form
{
return $form
->schema([
Section::make([
Fieldset::make('Primary information')
->schema([
Select::make('customer')
->relationship('customer', 'company_name')
->searchable()
->required(),
ToggleButtons::make('shipping_type')
->options(ShippingType::class)
->inline()
->required(),
TextInput::make('courier')
->placeholder('UPS, Purolator...'),
]),
Split::make([
Fieldset::make('Account Details')
->schema([
TextInput::make('account_title')
->label('Title')
->prefixIcon('lucide-folder-pen')
->placeholder('What is this account used for?')
->columnSpan(2),
TextInput::make('account_url')
->label('URL')
->prefixIcon('lucide-globe')
->placeholder('Shipping website')
->url()
->columnSpan(2),
TextInput::make('account_username')
->label('Username')
->prefixIcon('lucide-circle-user')
->placeholder('...'),
TextInput::make('account_password')
->label('Password')
->prefixIcon('lucide-key-round')
->placeholder('...'),
])->columnSpan(1),
Fieldset::make('Shipping Instructions')
->schema([
TextInput::make('info_needed')
->label('Instructions')
->prefixIcon('lucide-pencil')
->placeholder('Example: put PO on box')
->columnSpan(2),
TextInput::make('notify')
->placeholder('Who to email and CC?')
->prefixIcon('lucide-users-round')
->columnSpan(2),
TextArea::make('notes')
->placeholder('Any additional information...')
->rows(2)
->columnSpan(2),
]),
])->columnSpan(2),
])->columns(2),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('shipping_type')
->label('Type')
->sortable(),
TextColumn::make('courier')
->url(fn ($record) => $record->account_url ?? null, shouldOpenInNewTab: true)
->icon(fn ($record) => $record->account_url ? 'lucide-external-link' : null)
->iconPosition(IconPosition::After)
->searchable(query: function (Builder $query, $search) {
return $query
->where('courier', 'like', "%{$search}%")
->orWhereHas('customer', function (Builder $query) use ($search) {
return $query->where('company_name', 'like', "%{$search}%");
});
}),
TextColumn::make('account_title'),
TextColumn::make('info_needed'),
TextColumn::make('notify'),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
// Tables\Actions\BulkActionGroup::make([
// Tables\Actions\DeleteBulkAction::make(),
// ]),
])
->defaultGroup(
Group::make('customer.company_name')
->titlePrefixedWithLabel(false)
);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => \App\Filament\Admin\Resources\ShippingEntryResource\Pages\ListShippingEntries::route('/'),
'create' => \App\Filament\Admin\Resources\ShippingEntryResource\Pages\CreateShippingEntry::route('/create'),
'edit' => \App\Filament\Admin\Resources\ShippingEntryResource\Pages\EditShippingEntry::route('/{record}/edit'),
];
}
}

View File

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

View File

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

View File

@ -0,0 +1,21 @@
<?php
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;
class ListShippingEntries extends ListRecords
{
protected static string $resource = ShippingEntryResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make()
->icon(IconEnum::NEW->value),
];
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace App\Filament\Admin\Resources;
use App\Enums\IconEnum;
use App\Filament\Admin\Resources\TaxRateResource\Pages;
use App\Models\TaxRate;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Support\Enums\FontWeight;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class TaxRateResource extends Resource
{
protected static ?string $model = TaxRate::class;
protected static ?string $navigationIcon = IconEnum::TAX_RATE->value;
protected static ?string $navigationGroup = 'Settings';
protected static ?int $navigationSort = 11;
public static function form(Form $form): Form
{
return $form
->schema([
Section::make([
TextInput::make('name')
->disabledOn('edit'),
TextInput::make('value')
->label('Value in percentage')
->numeric()
->prefix('%'),
]),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->weight(FontWeight::Bold),
TextColumn::make('value')
->extraHeaderAttributes(['class' => 'w-full'])
->suffix(' %'),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make()
->modalWidth('xs'),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function canAccess(): bool
{
return auth()->user()->is_admin ?? false;
}
public static function getPages(): array
{
return [
'index' => Pages\ListTaxRates::route('/'),
// 'create' => Pages\CreateTaxRate::route('/create'),
// 'edit' => Pages\EditTaxRate::route('/{record}/edit'),
];
}
}

View File

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

View File

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

View File

@ -0,0 +1,23 @@
<?php
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;
class ListTaxRates extends ListRecords
{
protected static string $resource = TaxRateResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make()
->modalWidth('xs')
->icon(IconEnum::NEW->value)
->createAnother(false),
];
}
}

View File

@ -0,0 +1,128 @@
<?php
namespace App\Filament\Admin\Resources;
use App\Enums\IconEnum;
use App\Models\User;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class UserResource extends Resource
{
protected static ?int $navigationSort = 10;
protected static ?string $model = User::class;
protected static ?string $navigationIcon = IconEnum::USER->value;
protected static ?string $navigationGroup = 'Settings';
public static function form(Form $form): Form
{
return $form
->schema([
Section::make('Login details')
->description(fn (string $operation) => $operation == 'edit' ? 'To leave the password unchanged, leave both fields empty,' : false)
->schema([
TextInput::make('username')
->autocomplete('new-username')
->unique()
->required()
->columnSpanFull(),
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'),
]),
Section::make('Permissions')
->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))
->disabled(fn (?User $record, $operation) => $operation !== 'create' && auth()->user()->id === $record->id),
])
->columns(2),
Section::make('Customer Login')
->description('If this account is for a customer, select them here')
->schema([
Select::make('customer_id')
->relationship('customer', 'company_name')
->disabled(fn ($get) => $get('is_admin'))
->afterStateUpdated(fn ($state, callable $set) => $set('is_admin', false))
->nullable()
->searchable()
->preload(),
]),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('username')
->extraHeaderAttributes(['class' => 'w-full']),
TextColumn::make('customer.company_name')
->sortable()
->placeholder('Internal'),
Tables\Columns\IconColumn::make('is_admin')
->label('Admin')
->boolean()
->alignRight(),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make()
->modalWidth('md')
->modal(),
])
->defaultSort('customer_id', 'asc');
}
public static function canAccess(): bool
{
return auth()->user()->is_admin ?? false;
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => \App\Filament\Admin\Resources\UserResource\Pages\ListUsers::route('/'),
];
}
private static function Grid() {}
}

View File

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

View File

@ -0,0 +1,21 @@
<?php
namespace App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Admin\Resources\UserResource;
use App\Models\User;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditUser extends EditRecord
{
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make()
->disabled(fn (User $record) => $record->id == auth()->user()->id),
];
}
}

View File

@ -0,0 +1,23 @@
<?php
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;
class ListUsers extends ListRecords
{
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make()
->modalWidth('md')
->icon(IconEnum::NEW->value)
->modal(),
];
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace App\Filament\Admin\Widgets;
use App\Enums\OrderStatus;
use App\Models\Order;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
class OrderStats extends BaseWidget
{
// protected int|string|array $columnSpan = '2';
protected function getStats(): array
{
return [
Stat::make('This Month', $this->getOrdersPast30Days())
->icon('heroicon-s-calendar')
->chartColor('success')
->chart($this->getOrdersInPast30DaysChart())
->description('New orders in the past 30 days'),
Stat::make('Active Orders', $this->getActiveOrders())
->icon('heroicon-o-arrow-path')
->description('Orders that have yet to be completed'),
Stat::make('Due Today', $this->getDueOrders())
->icon('heroicon-o-clock')
->chartColor('info')
->chart($this->getDueOrdersChart())
->description('Orders that are scheduled to be due today'),
];
}
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();
}
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()])
->count();
}
private function getOrdersInPast30DaysChart(): array
{
$chart = [];
$points = 30;
$startDate = today()->subDays(31);
for ($i = 0; $i < $points; $i++) {
$chart[$i] = Order::where('order_date', $startDate->addDay())->count();
}
return $chart;
}
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())
->count();
}
private function getDueOrdersChart(): array
{
$chart = [];
$points = 30;
$startDate = today()->subDays(31);
for ($i = 0; $i < $points; $i++) {
$chart[$i] = Order::where('due_date', $startDate->addDay())->count();
}
return $chart;
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace App\Filament\Customer\Pages;
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
use Filament\Facades\Filament;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\TextInput;
use Filament\Http\Responses\Auth\Contracts\LoginResponse;
use Filament\Models\Contracts\FilamentUser;
use Filament\Pages\Auth\Login;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Validation\ValidationException;
class CustomerLogin extends Login
{
protected function getEmailFormComponent(): Component
{
return TextInput::make('username')
->label('Username')
->required()
->autofocus()
->extraInputAttributes(['tabindex' => 1])
->autocomplete();
}
protected function getCredentialsFromFormData(array $data): array
{
return [
'username' => $data['username'],
'password' => $data['password'],
];
}
protected function throwFailureValidationException(): never
{
throw ValidationException::withMessages([
'data.username' => __('filament-panels::pages/auth/login.messages.failed'),
]);
}
public function authenticate(): ?LoginResponse
{
try {
$this->rateLimit(5);
} catch (TooManyRequestsException $exception) {
$this->getRateLimitedNotification($exception)?->send();
return null;
}
$data = $this->form->getState();
if (! Filament::auth()->attempt($this->getCredentialsFromFormData($data), $data['remember'] ?? false)) {
$this->throwFailureValidationException();
}
$user = Filament::auth()->user();
if (($user instanceof FilamentUser) && (! $user->canAccessPanel(Filament::getCurrentPanel()))) {
Filament::auth()->logout();
$this->throwFailureValidationException();
} elseif ($user->customer_id === null) {
Filament::auth()->logout();
throw ValidationException::withMessages([
'data.username' => 'Incorrect username or password.',
]);
}
session()->regenerate();
return app(LoginResponse::class);
}
public function getTitle(): Htmlable|string
{
return __('Login');
}
public function getHeading(): Htmlable|string
{
return __('Login');
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Filament\Customer\Resources;
use App\Enums\IconEnum;
use App\Filament\Customer\Resources\OrderResource\Pages;
use App\Models\Order;
use Filament\Infolists\Components\TextEntry;
use Filament\Infolists\Infolist;
use Filament\Resources\Resource;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
class OrderResource extends Resource
{
protected static ?string $model = Order::class;
protected static ?string $navigationIcon = IconEnum::ORDER->value;
public static function infolist(Infolist $infolist): Infolist
{
return $infolist
->schema([
TextEntry::make('internal_po'),
TextEntry::make('customer_po'),
TextEntry::make('order_date'),
TextEntry::make('due_date'),
TextEntry::make('status'),
]);
}
public static function table(Table $table): Table
{
return \App\Filament\Admin\Resources\OrderResource::table($table)
->modifyQueryUsing(function (Builder $query) {
return $query->where('customer_id', auth()->user()->customer_id);
})
->actions([
ViewAction::make(),
])
->bulKActions([]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListOrders::route('/'),
];
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Filament\Customer\Resources\OrderResource\Pages;
use App\Filament\Customer\Resources\OrderResource;
use Filament\Resources\Pages\ListRecords;
class ListOrders extends ListRecords
{
protected static string $resource = OrderResource::class;
protected function getHeaderActions(): array
{
return [
];
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Filament\Guest\Pages;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Section;
use Filament\Pages\Page;
use Illuminate\Support\HtmlString;
class ContactUs extends Page
{
protected static ?string $navigationIcon = 'lucide-contact';
protected static ?int $navigationSort = 2;
protected static string $view = 'filament.guest.pages.contact-us';
protected function getFormSchema(): array
{
return [
Section::make()
->schema([
Placeholder::make('digitizingContent')
->hiddenLabel()
->content(new HtmlString('<p class="w-3/4">
<b>Address</b>
<br>
<ul class="list-none list-inside">
<li>108 - 618 East Kent Ave. South,</li>
<li> Vancouver BC, V5X 0B1</li>
</ul>
<br>
<b>Contact Numbers</b>
<br>
<ul class="list-none list-inside">
<li>Tel: (604)871-9991</li>
<li>Fax: (604)871-9980</li>
<li>E-mail: info@sewtopnotch.com</li>
</ul>
</p>')),
])
->columns(2)
->columnSpan(1),
];
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Filament\Guest\Pages;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Section;
use Filament\Pages\Page;
use Illuminate\Support\HtmlString;
class Digitizing extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-document-text';
protected static ?int $navigationSort = -1;
protected static string $view = 'filament.guest.pages.digitizing';
protected function getFormSchema(): array
{
return [
Section::make()
->schema([
Placeholder::make('digitizingContent')
->hiddenLabel()
->content(new HtmlString('<p class="w-3/4">
<b>Overview</b>
<br>
<ul class="list-disc">
<li>Digitizing is the process of converting an artwork/picture into instructions an embroidery machine can understand.</li>
<br>
<li>The quality of digitizing plays a very important role in the final embroidery quality.</li>
<br>
<li>We have our own digitizing expert working in the shop, so digitizing files can be adjusted according to fabric, placement, and colors.</li>
<br>
<li>We always digitize by ourselves.</li>
</ul>
</p>')),
])
->columns(2)
->columnSpan(1),
];
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Filament\Guest\Pages;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Section;
use Filament\Pages\Page;
use Illuminate\Support\HtmlString;
class Embroidery extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-document-text';
protected static ?int $navigationSort = 0;
protected static string $view = 'filament.guest.pages.embroidery';
protected function getFormSchema(): array
{
return [
Section::make()
->schema([
Placeholder::make('embContent')
->hiddenLabel()
->content(new HtmlString('<p class="w-3/4">
<b>Overview</b>
<br>
<ul class="list-disc list-inside">
<li>We can do regular embroidery, appliqué, patches, puff (3D). They can apply on apparels, hats, bags, towels, blankets, etc. </li>
<br>
<li> Comparing with screen printing, embroidery means high end. To make sure it is really high end, we strictly apply 3 steps: </li>
</ul>
<br>
<ul class="list-inside list-decimal">
<li>1. Good digitizing</li>
<li>2. Best quality materials (such as threads and backings)</li>
<li>3. Detailed QC (Quality Control) procedures</li>
</ul>
<br>
<ul class="list-disc list-inside">
<li>As for the material, there are every kind of threads, backings, and other materials that embroidery needs to use. We always use the best quality.</li>
<br>
<li>QC is the last step before our embroidery products leave the shop. We consider QC as important as digitizing and embroidery. The time we spent on QC is twice as others.</li>
</ul>
</p>')),
])
->columns(2)
->columnSpan(1),
];
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Filament\Guest\Pages;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Section;
use Filament\Pages\Page;
use Illuminate\Support\HtmlString;
class Home extends Page
{
protected static ?string $navigationIcon = 'lucide-house';
protected static ?int $navigationSort = -2;
protected static string $view = 'filament.guest.pages.home';
protected function getFormSchema(): array
{
return [
Section::make()
->schema([
Placeholder::make('homeContent')
->hiddenLabel()
->content(new HtmlString('<p class="w-3/4">
<b>Welcome to Top-Notch Embroidery and Digitizing Ltd.</b>
<br>
<ul class="list-disc list-inside">
<li>We specialize in digitizing, embroidery, vinyl and screen printing.</li>
<br>
<li>Our digitizing is done by our experienced digitizer right in our shop.</li>
<br>
<li>We can embellish on jackets, shirts, pants, sweaters, hoodies, sports jerseys,
team wears, bags, towels, hats, blankets, wedding gawns, gloves, head bands, wrist bands, etc.</li>
</ul>
</p>')),
])
->columns(2)
->columnSpan(1),
];
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Filament\Guest\Pages;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Section;
use Filament\Pages\Page;
use Illuminate\Support\HtmlString;
class Vinyl extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-document-text';
protected static ?int $navigationSort = 1;
protected static string $view = 'filament.guest.pages.vinyl';
protected function getFormSchema(): array
{
return [
Section::make()
->schema([
Placeholder::make('digitizingContent')
->hiddenLabel()
->content(new HtmlString('<p class="w-3/4">
<b>Overview</b>
<br>
<ul class="list-disc">
<li>Vinyl is widely used in jerseys and other apparels. </li>
<li>We mostly use 3M material.</li>
</ul>
</p>')),
])
->columns(2)
->columnSpan(1),
];
}
}

View File

@ -5,13 +5,15 @@
use App\Http\Requests\ContactRequest;
use App\Models\Contact;
use App\Models\Customer;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class ContactController extends Controller
{
public function index() {}
public function index(): void {}
public function create(Request $request)
public function create(Request $request): View
{
return view('contacts.create', [
'customers' => Customer::all(),
@ -19,20 +21,20 @@ public function create(Request $request)
]);
}
public function store(ContactRequest $request)
public function store(ContactRequest $request): RedirectResponse
{
$contact = Contact::create($request->validated());
return redirect()->route('customers.show', [$contact->customer, 'contacts'])->with('status', 'Contact created successfully');
}
public function show($id) {}
public function show(int $id): void {}
public function edit($id) {}
public function edit(int $id): void {}
public function update(Request $request, $id) {}
public function update(Request $request, int $id): void {}
public function requestDestroy(Request $request)
public function requestDestroy(Request $request): RedirectResponse
{
$contact = Contact::find($request->get('contact'));
$contact->delete();
@ -40,5 +42,5 @@ public function requestDestroy(Request $request)
return redirect()->route('customers.show', [$contact->customer->id, 'contacts'])->with('status', 'Contact deleted successfully');
}
public function destroy($id) {}
public function destroy(int $id): void {}
}

View File

@ -5,12 +5,16 @@
use App\Http\Requests\CustomerRequest;
use App\Models\Customer;
use App\Models\PackingSlip;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Foundation\Application;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
class CustomerController extends Controller
{
public function index() {}
public function index(): void {}
public function store(CustomerRequest $request)
{
@ -19,12 +23,12 @@ public function store(CustomerRequest $request)
return redirect()->route('management.index')->with('status', 'Customer created successfully.');
}
public function create()
public function create(): Factory|View|Application|\Illuminate\View\View
{
return view('customers.create');
}
public function show(Customer $customer, ?string $tab = null)
public function show(Customer $customer, ?string $tab = null): RedirectResponse|View
{
if (! $tab) {
return redirect()->route('customers.show', [$customer, 'tab' => 'details']);
@ -40,14 +44,14 @@ public function show(Customer $customer, ?string $tab = null)
]);
}
public function update(CustomerRequest $request, Customer $customer)
public function update(CustomerRequest $request, Customer $customer): RedirectResponse
{
$customer->update($request->validated());
return redirect()->route('customers.show', $customer)->with('status', 'Customer updated successfully.');
}
public function requestDestroy(Request $request)
public function requestDestroy(Request $request): RedirectResponse
{
$customer = Customer::find($request->id);
$customer->delete();
@ -55,10 +59,15 @@ public function requestDestroy(Request $request)
return redirect()->route('management.index')->with('status', 'Customer deleted successfully.');
}
public function destroy(Customer $customer)
public function destroy(Customer $customer): RedirectResponse
{
$customer->delete();
return redirect()->route('management.index')->with('status', 'Customer deleted successfully.');
}
public function pdf(Customer $customer, ?bool $paid = false, ?string $created_from = null, ?string $created_until = null): RedirectResponse
{
dd($customer, $paid, $created_from, $created_until);
}
}

View File

@ -26,9 +26,9 @@ public function index(Request $request)
return redirect()->route('dashboard', ['tab' => 'active_orders']);
}
return view('dashboard', [
'today' => Carbon::today(),
'tab' => $request->get('tab'),
]);
// return view('dashboard', [
// 'today' => Carbon::today(),
// 'tab' => $request->get('tab'),
// ]);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Controllers;
use App\Models\Invoice;
use Spatie\Browsershot\Browsershot;
use Spatie\LaravelPdf\Facades\Pdf;
class InvoiceController extends Controller
{
public function pdf(int $id)
{
$invoice = Invoice::find($id);
$url = strtolower('invoice-'.$invoice->internal_id.'.pdf');
Pdf::view('pdf.invoice', ['invoice' => $invoice])
->withBrowsershot(function (Browsershot $browsershot) {
$browsershot->noSandbox();
})
->margins(8, 8, 15, 8)
->footerView('pdf.invoice-footer', ['invoice' => $invoice])
->save($url);
return redirect($url);
}
}

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Models\Customer;
use App\Models\ServiceFile;
class ManagementController extends Controller
{
@ -15,8 +16,9 @@ public function index(?string $tab = null)
}
return view('management.index', [
'customers' => Customer::all(),
'tab' => $tab,
'customers' => Customer::all(),
'serviceFiles' => ServiceFile::paginate(15),
'tab' => $tab,
]);
}
}

View File

@ -11,8 +11,13 @@
use App\Models\ProductService;
use App\Models\ProductSize;
use App\Models\ServiceFile;
use Illuminate\Contracts\View\Factory;
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\View\View;
use Spatie\Browsershot\Browsershot;
use Spatie\LaravelPdf\Facades\Pdf;
class OrderController extends Controller
{
@ -80,30 +85,29 @@ public function store(OrderRequest $request)
// Create productServices
for ($i = 0; $i < count($request->get('serviceInputCount')) - 1; $i++) {
$productService = ProductService::create([
'order_id' => $order->id,
'service_type' => $request->get('service_type')[$i],
'placement' => $request->get('placement')[$i],
'setup_amount' => $request->get('setup_amount')[$i],
'amount' => $request->get('amount')[$i],
'amount_price' => $request->get('amount_price')[$i],
$serviceFile = ServiceFile::create([
'code' => $request->get('service_file_name')[$i],
'name' => $request->get('logo_name')[$i],
'width' => $request->get('service_width')[$i],
'height' => $request->get('service_height')[$i],
'setup_number' => $request->get('setup_amount')[$i],
]);
ProductService::create([
'order_id' => $order->id,
'service_file_id' => $serviceFile->id,
'service_type' => $request->get('service_type')[$i],
'placement' => $request->get('placement')[$i],
'amount' => $request->get('amount')[$i],
'amount_price' => $request->get('amount_price')[$i],
'notes' => $request->get('service_notes')[$i],
]);
ServiceFile::create([
'product_service_id' => $productService,
'code' => $request->get('service_file_name')[$i],
'name' => $request->get('logo_name')[$i],
'width' => $request->get('service_width')[$i],
'height' => $request->get('service_height')[$i],
'unit' => $request->get('service_setup_unit')[$i],
'setup_number' => $request->get('setup_number')[$i],
]);
}
return redirect()->route('order-products.create', ['order' => $order->id]);
return redirect()->route('orders.show', $order);
}
public function show($id)
public function show(int $id): Factory|\Illuminate\Contracts\View\View|Application|View
{
return view('orders.show', [
'order' => Order::find($id),
@ -111,9 +115,25 @@ public function show($id)
]);
}
public function edit($id) {}
public function edit(int $id) {}
public function update(Request $request, $id) {}
public function destroy($id) {}
public function destroy(int $id): void {}
public function pdf(int $id)
{
$order = Order::find($id);
$url = strtolower('order-'.$order->internal_po.'.pdf');
Pdf::view('pdf.order', ['order' => $order])
->withBrowsershot(function (Browsershot $browsershot) {
$browsershot->noSandbox();
})
->margins(8, 8, 15, 8)
->footerView('pdf.order-footer', ['order' => $order])
->save($url);
return redirect($url);
}
}

View File

@ -2,24 +2,27 @@
namespace App\Http\Controllers;
use Illuminate\Contracts\View\Factory;
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
use Illuminate\View\View;
class OrderProductController extends Controller
{
public function index() {}
public function index(): void {}
public function create()
public function create(): Factory|\Illuminate\Contracts\View\View|Application|View
{
return view('order-products.create');
}
public function store(Request $request) {}
public function store(Request $request): void {}
public function show($id) {}
public function show($id): void {}
public function edit($id) {}
public function edit($id): void {}
public function update(Request $request, $id) {}
public function update(Request $request, $id): void {}
public function destroy($id) {}
public function destroy($id): void {}
}

View File

@ -5,15 +5,16 @@
use App\Http\Requests\PackingSlipRequest;
use App\Models\Customer;
use App\Models\PackingSlip;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PackingSlipController extends Controller
{
public function index() {}
public function index(): void {}
public function create() {}
public function create(): void {}
public function store(PackingSlipRequest $request)
public function store(PackingSlipRequest $request): RedirectResponse
{
PackingSlip::create($request->validated());
@ -24,14 +25,14 @@ public function store(PackingSlipRequest $request)
]);
}
return redirect()->back(); //todo: change to packing slips page
return redirect()->back(); // todo: change to packing slips page
}
public function show($id) {}
public function show($id): void {}
public function edit($id) {}
public function edit($id): void {}
public function update(Request $request, $id) {}
public function update(Request $request, $id): void {}
public function destroy($id) {}
public function destroy($id): void {}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers;
use App\Models\InvoiceReport;
use App\Models\Quote;
use Spatie\Browsershot\Browsershot;
use Spatie\LaravelPdf\Facades\Pdf;
class PdfController extends Controller
{
public function invoiceReport(int $id)
{
$invoiceReport = InvoiceReport::find($id);
$url = strtolower('invoicereport-'.$invoiceReport->internal_id.'.pdf');
Pdf::view('pdf.invoice-report', ['invoiceReport' => $invoiceReport])
->withBrowsershot(function (Browsershot $browsershot) {
$browsershot->noSandbox();
})
->margins(8, 8, 15, 8)
->footerView('pdf.invoice-report-footer', ['invoiceReport' => $invoiceReport])
->save($url);
return redirect($url);
}
public function quote(int $id)
{
$quote = Quote::find($id);
$company_name = $quote->customer->company_name ?? '';
$url = strtolower('TN-quote-'.$quote->id.'.pdf');
Pdf::view('pdf.quote', ['quote' => $quote])
->withBrowsershot(function (Browsershot $browsershot) {
$browsershot->noSandbox();
})
->margins(8, 8, 15, 8)
->footerView('pdf.quote-footer', ['quote' => $quote])
->save($url);
return redirect($url);
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
class QuoteController extends Controller
{
//
}

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