Compare commits
451 Commits
refactor/o
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6db9512a4b | ||
|
|
ac9ab7e257 | ||
|
|
06e35cb296 | ||
|
|
4b347112c6 | ||
|
|
632ddce08a | ||
|
|
35c603944f | ||
|
|
ea3ed4de0a | ||
|
|
179c9a7818 | ||
|
|
6835a19b39 | ||
|
|
3b9ddd8865 | ||
|
|
d9d8190835 | ||
|
|
8af01a6772 | ||
|
|
e11a934766 | ||
|
|
86c2e0cf1c | ||
|
|
f899e5f8cb | ||
|
|
f2b1ceebe9 | ||
|
|
b0e343f2b5 | ||
|
|
609d55d5c9 | ||
|
|
d649c8239f | ||
|
|
86b7d8db4e | ||
|
|
701534dd6b | ||
|
|
f341fc6673 | ||
|
|
103b7a6077 | ||
|
|
5a57fd1e27 | ||
|
|
6f56d21936 | ||
|
|
44cf1423e4 | ||
|
|
ceea43823b | ||
|
|
618d5aeea9 | ||
|
|
9c3e3b1c7b | ||
|
|
b3a5eebd56 | ||
|
|
dc804e8e25 | ||
|
|
20709d201f | ||
|
|
b3ae727c5a | ||
|
|
c004ee3b1e | ||
|
|
41f8bee6a6 | ||
|
|
f53124cd2e | ||
|
|
1d1ac2d520 | ||
|
|
bca2cd5c77 | ||
|
|
ff25196d51 | ||
|
|
58006d7b19 | ||
|
|
4237cf45ab | ||
|
|
5f591bee19 | ||
|
|
c9fa8d7578 | ||
|
|
7c3f5a27a3 | ||
|
|
13d2fa3ac7 | ||
|
|
fab181128a | ||
|
|
fbb1619c38 | ||
|
|
9a9bfeae35 | ||
|
|
c7f3af5f39 | ||
|
|
0db14bda0e | ||
|
|
0ff3b64f80 | ||
|
|
2ca4338e7e | ||
|
|
d905805980 | ||
|
|
2f5cb5c0e7 | ||
|
|
86fef4d021 | ||
|
|
5b95c8b365 | ||
|
|
5c1863218f | ||
|
|
ee30c65c34 | ||
|
|
d10357758d | ||
|
|
59cd1c5a6b | ||
|
|
2b865f2633 | ||
|
|
d7f79c6a5b | ||
|
|
7eb658ef6c | ||
|
|
150ecb9124 | ||
|
|
2fe3e7abd9 | ||
|
|
dc975a4206 | ||
|
|
04668d1b29 | ||
|
|
d13184819f | ||
|
|
f05211c924 | ||
|
|
5cd86ed463 | ||
|
|
7dd4cd314f | ||
|
|
29c95be27b | ||
|
|
b37cb2b5c9 | ||
|
|
a95d875564 | ||
|
|
41e65bf3b0 | ||
|
|
fbac9498fd | ||
|
|
a155999bbb | ||
|
|
9978e1efcc | ||
|
|
6fbcc1a451 | ||
|
|
3fd89291e7 | ||
|
|
e4588ec8b6 | ||
|
|
82bd313d21 | ||
|
|
ada6ede429 | ||
|
|
549bdf0e93 | ||
|
|
b8ed494c41 | ||
|
|
0f843fa0f2 | ||
|
|
01859205f5 | ||
|
|
5e4ce9f21b | ||
|
|
a9f30cdfaa | ||
|
|
09c0d1bbe8 | ||
|
|
13908d0d3a | ||
|
|
43f852b618 | ||
|
|
0df1694dad | ||
|
|
4d0c9698d6 | ||
|
|
0ed49f947c | ||
|
|
aa788e9fe2 | ||
|
|
0e1f145c45 | ||
|
|
b926a627f2 | ||
|
|
e017ddf762 | ||
|
|
6eee8d8c07 | ||
|
|
0b62d8371f | ||
|
|
60a375960f | ||
|
|
8e1162a1c9 | ||
|
|
0d4d57c51f | ||
|
|
ceb0526f0f | ||
|
|
cc2bedff41 | ||
|
|
1cfc8983a9 | ||
|
|
90dd3f415d | ||
|
|
281fc7f5a1 | ||
|
|
d20162c5b2 | ||
|
|
3318880afd | ||
|
|
0a06a02bf6 | ||
|
|
ffe059a4d5 | ||
|
|
59ed05dd53 | ||
|
|
19eee0d36f | ||
|
|
9967e39dc8 | ||
|
|
6a4bd75b33 | ||
|
|
61a680b7e3 | ||
|
|
0f248ca178 | ||
|
|
00782038d3 | ||
|
|
3c1a7da11a | ||
|
|
9833cc592d | ||
|
|
54e8ff474f | ||
|
|
efc61680c9 | ||
|
|
8a72453cc2 | ||
|
|
07c5a1e336 | ||
|
|
d16c1a3746 | ||
|
|
81745fbf70 | ||
|
|
6c3be5221b | ||
|
|
1e6cb75422 | ||
|
|
b4bc8c129f | ||
|
|
86e656a89b | ||
|
|
c7c15fa484 | ||
|
|
1e60212644 | ||
|
|
33607ff982 | ||
|
|
bb34d24e1b | ||
|
|
94e67c5955 | ||
|
|
7606484317 | ||
|
|
e57212437d | ||
|
|
c9b68ba61e | ||
|
|
bd9abe29b9 | ||
|
|
6223dcc024 | ||
|
|
3de733a528 | ||
|
|
eccaedf219 | ||
|
|
a4e465c428 | ||
|
|
b96f5d6d59 | ||
|
|
28d1701904 | ||
|
|
4cb6b87134 | ||
|
|
e3f7181558 | ||
|
|
456b44681c | ||
|
|
e60accf724 | ||
|
|
66db854ebc | ||
|
|
2d02493b24 | ||
|
|
e3c7d14001 | ||
|
|
966d381740 | ||
|
|
1eff01496b | ||
|
|
bf83c4bc63 | ||
|
|
aec4a12af8 | ||
|
|
49ef373cbe | ||
|
|
9a40e1945e | ||
|
|
99e34832a0 | ||
|
|
e1ebf245b2 | ||
|
|
10688606ca | ||
|
|
f36aad8d6d | ||
|
|
f543fe930a | ||
|
|
62be464ebe | ||
|
|
3b245b421f | ||
|
|
8f45d86315 | ||
|
|
629831cdd8 | ||
|
|
3ac21c22ec | ||
|
|
60362f5792 | ||
|
|
078e4f380c | ||
|
|
2457d81061 | ||
|
|
dec35f9eea | ||
|
|
6840f0a583 | ||
|
|
759bbe90b0 | ||
|
|
3a7e49f176 | ||
|
|
ca661b8649 | ||
|
|
430f7efe5c | ||
|
|
d06c66f703 | ||
|
|
0b2a22c5c9 | ||
|
|
33deab99b2 | ||
|
|
5696db0023 | ||
|
|
394e0ba201 | ||
|
|
d8b7230512 | ||
|
|
20b9fa8dc7 | ||
|
|
c5878de5d2 | ||
|
|
85936a643b | ||
|
|
4d50ab2fab | ||
|
|
163168d561 | ||
|
|
afab8bc2c9 | ||
|
|
492890b2d8 | ||
|
|
e907e3d610 | ||
|
|
2db314509f | ||
|
|
46314b16c0 | ||
|
|
ef49a5566d | ||
|
|
fbdd770d69 | ||
|
|
d183cf6ec1 | ||
|
|
d257f5b8a3 | ||
|
|
b73439ae90 | ||
|
|
9c1313171c | ||
|
|
8b379a3653 | ||
|
|
53fe654340 | ||
|
|
1c3f0e1efb | ||
|
|
37cc8994ad | ||
|
|
2dc6119e98 | ||
|
|
56464e0f5b | ||
|
|
a7a0ee9ce8 | ||
|
|
c8538e155c | ||
|
|
37db77cbb2 | ||
|
|
e2f4667818 | ||
|
|
2ca5cb048b | ||
|
|
6426016c2e | ||
|
|
d08d080937 | ||
|
|
8c7beccdc8 | ||
|
|
0584111357 | ||
|
|
87174f80c5 | ||
|
|
bd01908b52 | ||
|
|
af8666bd42 | ||
|
|
4f5faa5d39 | ||
|
|
2831def53a | ||
|
|
a0baf3ad39 | ||
|
|
16e002ccb9 | ||
|
|
bf0dea6ee3 | ||
|
|
602c060a0a | ||
|
|
2c0d1d5658 | ||
|
|
f8d1f9dc91 | ||
|
|
7887a695f7 | ||
|
|
654a76c5db | ||
|
|
a339d8fc75 | ||
|
|
482789ca41 | ||
|
|
28a66fba92 | ||
|
|
8903759335 | ||
|
|
ecade68740 | ||
|
|
64b77477fb | ||
|
|
1e763882c6 | ||
|
|
ddf6d2470b | ||
|
|
e538b45d5b | ||
|
|
b922ab2556 | ||
|
|
9207453164 | ||
|
|
5d17cbccfb | ||
|
|
4d46f29404 | ||
|
|
dd598ccd50 | ||
|
|
6049658ad9 | ||
|
|
96791a7611 | ||
|
|
7bffe6dbf7 | ||
|
|
7eff3f74be | ||
|
|
cc44f47a3f | ||
|
|
c19617244e | ||
|
|
18381bb2fe | ||
|
|
1dcf78621b | ||
|
|
a38906d91e | ||
|
|
603a50931b | ||
|
|
d5ddccc318 | ||
|
|
615d221c0c | ||
|
|
5227def0d8 | ||
|
|
745a41b811 | ||
|
|
4f8bafc6dd | ||
|
|
d56bc5d21a | ||
|
|
3a26392bd0 | ||
|
|
8a23f5438b | ||
|
|
1d837c0bf0 | ||
|
|
d8739a71a5 | ||
|
|
9821984630 | ||
|
|
63f1fb6bf9 | ||
|
|
7a26ae7ac9 | ||
|
|
b4a057b5f7 | ||
|
|
7e2438c44f | ||
|
|
48a80e8e76 | ||
|
|
490ef0ae0a | ||
|
|
5f99fba396 | ||
|
|
84f364de74 | ||
|
|
39c955cdc4 | ||
|
|
e02ca54187 | ||
|
|
ac46ee004b | ||
|
|
17a6eb260d | ||
|
|
5ea80366be | ||
|
|
99aa0cb980 | ||
|
|
3de53a76d0 | ||
|
|
7fa9b6aff8 | ||
|
|
79e156bd24 | ||
|
|
12a6a8eb69 | ||
|
|
eb71477ec1 | ||
|
|
2ed54eced2 | ||
|
|
32fd2b0ab8 | ||
|
|
ded374de3c | ||
|
|
1cd11cbf67 | ||
|
|
26bf7ac377 | ||
|
|
ac1084d6fe | ||
|
|
1e2a579c4f | ||
|
|
37394786be | ||
|
|
0ce850decf | ||
|
|
02facc77c2 | ||
|
|
a5640375c3 | ||
|
|
fb28283f39 | ||
|
|
00903d7cb7 | ||
|
|
18de0fc97a | ||
|
|
4830d53f63 | ||
|
|
43625660bc | ||
|
|
985aec9c8a | ||
|
|
544c955cf4 | ||
|
|
fec27f1aeb | ||
|
|
c898c02b8b | ||
|
|
4ea1dbd1c2 | ||
|
|
19b86d9f0e | ||
|
|
b847f3745e | ||
|
|
00fcfe8a21 | ||
|
|
95bad62728 | ||
|
|
a7074d55e1 | ||
|
|
62d3dafe56 | ||
|
|
6ff88440ff | ||
|
|
9f07155517 | ||
|
|
2bdb752c21 | ||
|
|
bf81929587 | ||
|
|
73d4ecc7f5 | ||
|
|
5a1570468e | ||
|
|
b4c5b24294 | ||
|
|
af3a2dc61b | ||
|
|
52a8fe00e1 | ||
|
|
e23f3aff2f | ||
|
|
bd001e9547 | ||
|
|
1933983071 | ||
|
|
ae9de21a16 | ||
|
|
be2b7e56c5 | ||
|
|
d3516cdd60 | ||
|
|
8e0ce323a1 | ||
|
|
a284cb3eb6 | ||
|
|
1df59f527a | ||
|
|
d3f7a374ec | ||
|
|
2380d94d02 | ||
|
|
7c1fe3070f | ||
|
|
079e211a7c | ||
|
|
75c01d54e9 | ||
|
|
6158df0443 | ||
|
|
8e50a61811 | ||
|
|
b2353bfe23 | ||
|
|
7b1292448a | ||
|
|
62c617a8db | ||
|
|
8aa5e51d1c | ||
|
|
7616c5e7f4 | ||
|
|
0406d13b92 | ||
|
|
d0ad85c943 | ||
|
|
8f41e08bc6 | ||
|
|
2c82099bdd | ||
|
|
dd967ff223 | ||
|
|
569e84562e | ||
|
|
a51398a336 | ||
|
|
6e97798f5b | ||
|
|
25181ec31b | ||
|
|
e8a1a62898 | ||
|
|
23e46f8085 | ||
|
|
4d3401eb84 | ||
|
|
0c9822b20f | ||
|
|
860bd093c0 | ||
|
|
41b970adc7 | ||
|
|
f5931b490c | ||
|
|
ae4ef621b9 | ||
|
|
7b0ee691e9 | ||
|
|
b82ab7d28b | ||
|
|
d602661e98 | ||
|
|
790fefd5cc | ||
|
|
011bb7433c | ||
|
|
3b57116c79 | ||
|
|
ce6667a54d | ||
|
|
2c3fa7680d | ||
|
|
79b4212efd | ||
|
|
1360468a48 | ||
|
|
58eb8bf636 | ||
|
|
9bddad6725 | ||
|
|
a5f4d99046 | ||
|
|
1925055d2a | ||
|
|
35db3a5f34 | ||
|
|
19668e7550 | ||
|
|
1f477d388e | ||
|
|
8b249a8b4a | ||
|
|
2982eb085b | ||
|
|
e341627aa1 | ||
|
|
0855fef884 | ||
|
|
9207b3ebbe | ||
|
|
1fae0c6426 | ||
|
|
51f96bdc04 | ||
|
|
72dd0bcebd | ||
|
|
eb9f737bbe | ||
|
|
d3ea27dff2 | ||
|
|
5e8c987edb | ||
|
|
257e819e75 | ||
|
|
aa95ad4e47 | ||
|
|
06e9402719 | ||
|
|
43883f5b36 | ||
|
|
06a004bc96 | ||
|
|
48060b2c18 | ||
|
|
0178fe6a75 | ||
|
|
7d4babc23a | ||
|
|
39708e31d8 | ||
|
|
439d5e4d90 | ||
|
|
12ad2254c9 | ||
|
|
8801ec59b0 | ||
|
|
8e232284a8 | ||
|
|
5c8b006ce6 | ||
|
|
0453d90aa7 | ||
|
|
751c452d6a | ||
|
|
863f0ba162 | ||
|
|
77fb5f9b30 | ||
|
|
9baf1e2a88 | ||
|
|
ee7fee23fd | ||
|
|
2ce56f59c5 | ||
|
|
dec859b486 | ||
|
|
f80d018de7 | ||
|
|
4c548fec6f | ||
|
|
b74e7f9e49 | ||
|
|
dbdd002906 | ||
|
|
b2a6b2a709 | ||
|
|
afb5e5293a | ||
|
|
34197a9551 | ||
|
|
b54edcbfcf | ||
|
|
adfaa0f088 | ||
|
|
b129e8a1f7 | ||
|
|
6bed3500e6 | ||
|
|
75fcd4842f | ||
|
|
a516a1cd3c | ||
|
|
8aa87e4464 | ||
|
|
9634985a3a | ||
|
|
2bf5acc257 | ||
|
|
34dfdb437b | ||
|
|
26d2718b23 | ||
|
|
1dd828bc12 | ||
|
|
4dbee9ed64 | ||
|
|
51979b23fa | ||
|
|
dc29cf58bd | ||
|
|
c478666fb9 | ||
|
|
b6045062cb | ||
|
|
2dff75e209 | ||
|
|
e1d4468667 | ||
|
|
a4b88abdaa | ||
|
|
58a1b6099f | ||
|
|
d3f17ce805 | ||
|
|
60bf5ac8c5 | ||
|
|
23e6cf1cff | ||
|
|
865ec44b33 | ||
|
|
01e9a4df0a | ||
|
|
9515901506 | ||
|
|
bedae4fc85 | ||
|
|
704ced2366 | ||
|
|
44468532b6 | ||
|
|
7ef12da2fc | ||
|
|
289d611ac3 | ||
|
|
1568f1da2e | ||
|
|
83411b429f | ||
|
|
0d1262e224 | ||
|
|
0d696435e7 | ||
|
|
1cef063a6c |
9
.blade-formatter.json
Normal file
9
.blade-formatter.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"indentSize": 4,
|
||||
"wrapAttributes": "auto",
|
||||
"wrapLineLength": 120,
|
||||
"endWithNewLine": true,
|
||||
"useTabs": false,
|
||||
"sortTailwindcssClasses": true,
|
||||
"sortHtmlAttributes": "none"
|
||||
}
|
||||
72
.env.example
72
.env.example
@@ -8,6 +8,10 @@ APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
# Stock Notification Settings
|
||||
# Number of days before stock notification requests expire (default: 30)
|
||||
STOCK_NOTIFICATION_EXPIRATION_DAYS=30
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
# APP_MAINTENANCE_STORE=database
|
||||
|
||||
@@ -33,10 +37,30 @@ SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
BROADCAST_CONNECTION=reverb
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=redis
|
||||
|
||||
# Laravel Reverb (WebSocket Server for Real-Time Broadcasting)
|
||||
# Generate credentials: php artisan reverb:install
|
||||
REVERB_APP_ID=182567
|
||||
REVERB_APP_KEY=ieplst7x2k8avnqcmmo6
|
||||
REVERB_APP_SECRET=ckhvaobktbozwpljzlrv
|
||||
# IMPORTANT: Use Docker service name when running in Sail
|
||||
# - Docker/Sail: REVERB_HOST=reverb
|
||||
# - Local dev: REVERB_HOST=localhost
|
||||
REVERB_HOST=reverb
|
||||
REVERB_PORT=8080
|
||||
REVERB_SCHEME=http
|
||||
|
||||
# Vite Environment Variables (Frontend WebSocket Connection)
|
||||
# IMPORTANT: Vite doesn't support ${} expansion - values must be LITERAL
|
||||
# These values are for the browser, so always use "localhost" (not "reverb")
|
||||
VITE_REVERB_APP_KEY=ieplst7x2k8avnqcmmo6
|
||||
VITE_REVERB_HOST=localhost
|
||||
VITE_REVERB_PORT=8080
|
||||
VITE_REVERB_SCHEME=http
|
||||
|
||||
CACHE_STORE=redis
|
||||
CACHE_PREFIX=
|
||||
|
||||
@@ -57,10 +81,48 @@ MAIL_ENCRYPTION=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
# MinIO/S3 Storage Configuration
|
||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
# Versioning is enabled in all environments for asset recovery
|
||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
# ┌─────────────────────────────────────────────────────────────────────┐
|
||||
# │ LOCAL DEVELOPMENT (Docker MinIO) │
|
||||
# └─────────────────────────────────────────────────────────────────────┘
|
||||
# Use local MinIO container for development (versioning enabled)
|
||||
# Access MinIO Console: http://localhost:9001 (minioadmin/minioadmin)
|
||||
FILESYSTEM_DISK=minio
|
||||
AWS_ACCESS_KEY_ID=minioadmin
|
||||
AWS_SECRET_ACCESS_KEY=minioadmin
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
AWS_BUCKET=media
|
||||
AWS_ENDPOINT=http://minio:9000
|
||||
AWS_URL=http://localhost:9000/media
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=true
|
||||
|
||||
# ┌─────────────────────────────────────────────────────────────────────┐
|
||||
# │ STAGING/DEVELOP (media-dev bucket) │
|
||||
# └─────────────────────────────────────────────────────────────────────┘
|
||||
# FILESYSTEM_DISK=minio
|
||||
# AWS_ACCESS_KEY_ID=<staging-access-key>
|
||||
# AWS_SECRET_ACCESS_KEY=<staging-secret-key>
|
||||
# AWS_DEFAULT_REGION=us-east-1
|
||||
# AWS_BUCKET=media-dev
|
||||
# AWS_ENDPOINT=https://cdn.cannabrands.app
|
||||
# AWS_URL=https://cdn.cannabrands.app/media-dev
|
||||
# AWS_USE_PATH_STYLE_ENDPOINT=true
|
||||
|
||||
# ┌─────────────────────────────────────────────────────────────────────┐
|
||||
# │ PRODUCTION (media bucket) │
|
||||
# └─────────────────────────────────────────────────────────────────────┘
|
||||
# FILESYSTEM_DISK=minio
|
||||
# AWS_ACCESS_KEY_ID=TrLoFnMOVQC2CqLm9711
|
||||
# AWS_SECRET_ACCESS_KEY=4tfik06LitWz70L4VLIA45yXla4gi3zQI2IA3oSZ
|
||||
# AWS_DEFAULT_REGION=us-east-1
|
||||
# AWS_BUCKET=media
|
||||
# AWS_ENDPOINT=https://cdn.cannabrands.app
|
||||
# AWS_URL=https://cdn.cannabrands.app/media
|
||||
# AWS_USE_PATH_STYLE_ENDPOINT=true
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
@@ -23,10 +23,11 @@ chmod +x .githooks/*
|
||||
|
||||
### `pre-commit` - Laravel Pint Auto-formatter ✅ ENFORCED
|
||||
**What it does:**
|
||||
- Runs Laravel Pint on staged files only (`--dirty`)
|
||||
- Runs Laravel Pint on staged PHP files only (not unstaged files)
|
||||
- Auto-formats code to match team standards
|
||||
- Automatically stages formatted files
|
||||
- Automatically re-stages the formatted files
|
||||
- Fast feedback (runs in seconds)
|
||||
- Safe: Won't format or stage files you haven't explicitly added
|
||||
|
||||
**When it runs:**
|
||||
- Every time you run `git commit`
|
||||
|
||||
@@ -1,22 +1,37 @@
|
||||
#!/bin/sh
|
||||
# Laravel Pint Pre-commit Hook
|
||||
# Automatically format code before committing
|
||||
# Automatically format staged PHP files before committing
|
||||
|
||||
echo "🎨 Running Laravel Pint..."
|
||||
|
||||
# Run Pint on staged files only
|
||||
./vendor/bin/pint --dirty
|
||||
# Get only staged PHP files
|
||||
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.php$')
|
||||
|
||||
# Check if Pint made changes
|
||||
if ! git diff --quiet; then
|
||||
echo "✅ Code formatted! Files have been updated."
|
||||
echo " Changes have been staged automatically."
|
||||
|
||||
# Stage the formatted files
|
||||
git add -u
|
||||
|
||||
exit 0
|
||||
else
|
||||
echo "✅ Code style looks good!"
|
||||
# Exit early if no PHP files are staged
|
||||
if [ -z "$STAGED_FILES" ]; then
|
||||
echo "✅ No PHP files staged"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Run Pint only on staged files
|
||||
echo "$STAGED_FILES" | xargs ./vendor/bin/pint
|
||||
|
||||
# Check if Pint made changes to any of the staged files
|
||||
CHANGED=false
|
||||
for file in $STAGED_FILES; do
|
||||
if ! git diff --quiet "$file" 2>/dev/null; then
|
||||
CHANGED=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Re-stage the formatted files (only the ones that were already staged)
|
||||
if [ "$CHANGED" = true ]; then
|
||||
echo "✅ Code formatted! Files have been updated."
|
||||
echo " Changes have been staged automatically."
|
||||
echo "$STAGED_FILES" | xargs git add
|
||||
else
|
||||
echo "✅ Code style looks good!"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -1,27 +1,70 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Pre-push hook - Runs tests before pushing
|
||||
# Pre-push hook - Optionally run tests before pushing
|
||||
# Can be skipped with: git push --no-verify
|
||||
#
|
||||
# This is OPTIONAL - CI/CD will run comprehensive tests automatically.
|
||||
# Running tests locally can catch issues faster, but it's not required.
|
||||
#
|
||||
|
||||
echo "🧪 Running tests before push..."
|
||||
echo " (Use 'git push --no-verify' to skip)"
|
||||
echo "🚀 Preparing to push..."
|
||||
echo ""
|
||||
|
||||
# Run tests
|
||||
./vendor/bin/sail artisan test --parallel
|
||||
# Detect which environment is running
|
||||
SAIL_RUNNING=false
|
||||
K8S_RUNNING=false
|
||||
|
||||
# Check exit code
|
||||
if [ $? -ne 0 ]; then
|
||||
echo ""
|
||||
echo "❌ Tests failed!"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " 1. Fix the failing tests (recommended)"
|
||||
echo " 2. Push anyway with: git push --no-verify"
|
||||
echo ""
|
||||
exit 1
|
||||
# Check if Sail is running (use vendor/bin/sail ps which works for all project names)
|
||||
if [ -f ./vendor/bin/sail ] && ./vendor/bin/sail ps 2>/dev/null | grep -q "Up"; then
|
||||
SAIL_RUNNING=true
|
||||
fi
|
||||
|
||||
# Check if k8s namespace exists for this worktree
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
K8S_NS=$(echo "$BRANCH" | sed 's/feature\//feat-/' | sed 's/bugfix\//fix-/' | sed 's/\//-/g')
|
||||
|
||||
if kubectl get namespace "$K8S_NS" >/dev/null 2>&1; then
|
||||
K8S_RUNNING=true
|
||||
fi
|
||||
|
||||
# Offer to run tests if environment is available
|
||||
if [ "$SAIL_RUNNING" = true ] || [ "$K8S_RUNNING" = true ]; then
|
||||
echo "💡 Tests will run automatically in CI/CD"
|
||||
echo ""
|
||||
read -p "Run tests locally before push? (y/N) " -n 1 -r
|
||||
echo ""
|
||||
echo ""
|
||||
|
||||
if [ "$REPLY" = "y" ] || [ "$REPLY" = "Y" ]; then
|
||||
echo "🧪 Running tests..."
|
||||
echo ""
|
||||
|
||||
if [ "$SAIL_RUNNING" = true ]; then
|
||||
./vendor/bin/sail artisan test --parallel
|
||||
TEST_EXIT_CODE=$?
|
||||
elif [ "$K8S_RUNNING" = true ]; then
|
||||
kubectl -n "$K8S_NS" exec deploy/web -- php artisan test
|
||||
TEST_EXIT_CODE=$?
|
||||
fi
|
||||
|
||||
if [ $TEST_EXIT_CODE -ne 0 ]; then
|
||||
echo ""
|
||||
echo "❌ Tests failed!"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " 1. Fix the failing tests (recommended)"
|
||||
echo " 2. Push anyway - CI will catch failures: git push --no-verify"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ All tests passed!"
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "⚡ Pushing to remote (CI will run full test suite)..."
|
||||
echo ""
|
||||
echo "✅ All tests passed! Pushing..."
|
||||
|
||||
exit 0
|
||||
|
||||
30
.gitignore
vendored
30
.gitignore
vendored
@@ -8,6 +8,7 @@
|
||||
/storage/clockwork
|
||||
/vendor
|
||||
.DS_Store
|
||||
docker-compose.override.yml
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
@@ -29,6 +30,9 @@ yarn-error.log
|
||||
# Node symlink (for ARM-based machines)
|
||||
/node
|
||||
|
||||
# Git worktrees directory
|
||||
/.worktrees/
|
||||
|
||||
# Database backups
|
||||
*.gz
|
||||
*.sql.gz
|
||||
@@ -37,3 +41,29 @@ yarn-error.log
|
||||
# Version files (generated at build time or locally)
|
||||
version.txt
|
||||
version.env
|
||||
# Local secrets backup (DO NOT COMMIT)
|
||||
*SECRETS_BACKUP*
|
||||
.cannabrands-secrets/
|
||||
reverb-keys*
|
||||
|
||||
# Core dumps and debug files
|
||||
core
|
||||
core.*
|
||||
*.core
|
||||
|
||||
# Random image files (screenshots, etc.)
|
||||
*.png
|
||||
*.jpg
|
||||
*.jpeg
|
||||
!public/**/*.png
|
||||
!public/**/*.jpg
|
||||
!public/**/*.jpeg
|
||||
!resources/**/*.png
|
||||
!resources/**/*.jpg
|
||||
!resources/**/*.jpeg
|
||||
# Claude Code settings (personal AI preferences)
|
||||
.claude/
|
||||
|
||||
storage/tmp/*
|
||||
!storage/tmp/.gitignore
|
||||
SESSION_ACTIVE
|
||||
|
||||
20
.stylelintrc.json
Normal file
20
.stylelintrc.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "stylelint-config-standard",
|
||||
"plugins": [
|
||||
"stylelint-no-unsupported-browser-features"
|
||||
],
|
||||
"rules": {
|
||||
"no-descending-specificity": null,
|
||||
"selector-class-pattern": null,
|
||||
"custom-property-pattern": null,
|
||||
"declaration-block-no-duplicate-properties": true,
|
||||
"no-duplicate-selectors": true
|
||||
},
|
||||
"ignoreFiles": [
|
||||
"**/*.js",
|
||||
"**/*.php",
|
||||
"node_modules/**",
|
||||
"vendor/**",
|
||||
"public/**"
|
||||
]
|
||||
}
|
||||
@@ -1,27 +1,18 @@
|
||||
# Woodpecker CI/CD Pipeline for Cannabrands CRM
|
||||
# Woodpecker CI/CD Pipeline for Cannabrands Hub
|
||||
# Documentation: https://woodpecker-ci.org/docs/intro
|
||||
#
|
||||
# 3-Environment Workflow:
|
||||
# - develop branch → dev.cannabrands.com (unstable, daily integration)
|
||||
# - master branch → staging.cannabrands.com (stable, pre-production)
|
||||
# - tags (2025.X) → hub.cannabrands.com (production releases)
|
||||
# - develop branch → dev.cannabrands.app (unstable, daily integration)
|
||||
# - master branch → staging.cannabrands.app (stable, pre-production)
|
||||
# - tags (2025.X) → cannabrands.app (production releases)
|
||||
|
||||
when:
|
||||
- branch: [develop, master]
|
||||
event: push
|
||||
- event: [pull_request, tag]
|
||||
|
||||
# PHP Syntax Check
|
||||
# Install dependencies first (needed for php-lint to resolve traits/classes)
|
||||
steps:
|
||||
php-lint:
|
||||
image: php:8.3-cli
|
||||
commands:
|
||||
- echo "Checking PHP syntax..."
|
||||
- find app -name "*.php" -exec php -l {} \;
|
||||
- find routes -name "*.php" -exec php -l {} \;
|
||||
- find database -name "*.php" -exec php -l {} \;
|
||||
- echo "PHP syntax check complete!"
|
||||
|
||||
# Restore Composer cache
|
||||
restore-composer-cache:
|
||||
image: meltwater/drone-cache:dev
|
||||
@@ -44,7 +35,7 @@ steps:
|
||||
- apt-get install -y -qq git zip unzip libicu-dev libzip-dev libpng-dev libjpeg-dev libfreetype6-dev libpq-dev
|
||||
- echo "Installing PHP extensions..."
|
||||
- docker-php-ext-configure gd --with-freetype --with-jpeg
|
||||
- docker-php-ext-install -j$(nproc) intl pdo pdo_pgsql zip gd
|
||||
- docker-php-ext-install -j$(nproc) intl pdo pdo_pgsql zip gd pcntl
|
||||
- echo "Installing Composer..."
|
||||
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet
|
||||
- echo "Creating minimal .env for package discovery..."
|
||||
@@ -89,6 +80,16 @@ steps:
|
||||
volumes:
|
||||
- /tmp/woodpecker-cache:/tmp/cache
|
||||
|
||||
# PHP Syntax Check (runs after composer install so traits/classes are available)
|
||||
php-lint:
|
||||
image: php:8.3-cli
|
||||
commands:
|
||||
- echo "Checking PHP syntax..."
|
||||
- find app -name "*.php" -exec php -l {} \;
|
||||
- find routes -name "*.php" -exec php -l {} \;
|
||||
- find database -name "*.php" -exec php -l {} \;
|
||||
- echo "PHP syntax check complete!"
|
||||
|
||||
# Run Laravel Pint (Code Style)
|
||||
code-style:
|
||||
image: php:8.3-cli
|
||||
@@ -99,11 +100,12 @@ steps:
|
||||
|
||||
# Run PHPUnit Tests
|
||||
# Note: Uses array cache/session for speed and isolation (Laravel convention)
|
||||
# Only use real Redis/Queue services if testing cache/queue-specific behavior
|
||||
# Redis + Reverb services used for real-time broadcasting tests
|
||||
tests:
|
||||
image: kirschbaumdevelopment/laravel-test-runner:8.3
|
||||
environment:
|
||||
APP_ENV: testing
|
||||
BROADCAST_CONNECTION: reverb
|
||||
CACHE_STORE: array
|
||||
SESSION_DRIVER: array
|
||||
QUEUE_CONNECTION: sync
|
||||
@@ -113,56 +115,101 @@ steps:
|
||||
DB_DATABASE: testing
|
||||
DB_USERNAME: testing
|
||||
DB_PASSWORD: testing
|
||||
REDIS_HOST: redis
|
||||
REVERB_APP_ID: test-app-id
|
||||
REVERB_APP_KEY: test-key
|
||||
REVERB_APP_SECRET: test-secret
|
||||
REVERB_HOST: localhost
|
||||
REVERB_PORT: 8080
|
||||
REVERB_SCHEME: http
|
||||
commands:
|
||||
- echo "Setting up Laravel environment..."
|
||||
- cp .env.example .env
|
||||
- php artisan key:generate
|
||||
- echo "Starting Reverb server in background..."
|
||||
- php artisan reverb:start --host=0.0.0.0 --port=8080 > /dev/null 2>&1 &
|
||||
- sleep 2
|
||||
- echo "Running tests..."
|
||||
- php artisan test --parallel
|
||||
- echo "Tests complete!"
|
||||
|
||||
# Validate seeders that run in dev/staging environments
|
||||
# This prevents deployment failures caused by seeder errors (e.g., fake() crashes)
|
||||
# Uses APP_ENV=development to match K8s init container behavior
|
||||
validate-seeders:
|
||||
image: kirschbaumdevelopment/laravel-test-runner:8.3
|
||||
environment:
|
||||
APP_ENV: development
|
||||
DB_CONNECTION: pgsql
|
||||
DB_HOST: postgres
|
||||
DB_PORT: 5432
|
||||
DB_DATABASE: testing
|
||||
DB_USERNAME: testing
|
||||
DB_PASSWORD: testing
|
||||
CACHE_STORE: array
|
||||
SESSION_DRIVER: array
|
||||
QUEUE_CONNECTION: sync
|
||||
commands:
|
||||
- echo "Validating seeders (matches K8s init container)..."
|
||||
- cp .env.example .env
|
||||
- php artisan key:generate
|
||||
- echo "Running migrate:fresh --seed with APP_ENV=development..."
|
||||
- php artisan migrate:fresh --seed --force
|
||||
- echo "✅ Seeder validation complete!"
|
||||
when:
|
||||
branch: [develop, master]
|
||||
event: push
|
||||
status: success
|
||||
|
||||
# Build and push Docker image for DEV environment (develop branch)
|
||||
build-image-dev:
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
settings:
|
||||
registry: code.cannabrands.com
|
||||
repo: code.cannabrands.com/cannabrands/hub
|
||||
registry: code.cannabrands.app
|
||||
repo: code.cannabrands.app/cannabrands/hub
|
||||
username:
|
||||
from_secret: gitea_username
|
||||
password:
|
||||
from_secret: gitea_token
|
||||
tags:
|
||||
- dev # Latest dev build → dev.cannabrands.com
|
||||
- dev # Latest dev build → dev.cannabrands.app
|
||||
- dev-${CI_COMMIT_SHA:0:7} # Unique dev tag with SHA
|
||||
- sha-${CI_COMMIT_SHA:0:7} # Commit SHA (industry standard)
|
||||
- ${CI_COMMIT_BRANCH} # Branch name (develop)
|
||||
build_args:
|
||||
- GIT_COMMIT_SHA=${CI_COMMIT_SHA:0:7}
|
||||
- APP_VERSION=dev-${CI_COMMIT_SHA:0:7}
|
||||
GIT_COMMIT_SHA: "${CI_COMMIT_SHA:0:7}"
|
||||
APP_VERSION: "dev"
|
||||
VITE_REVERB_APP_KEY: "6VDQTxU0fknXHCgKOI906Py03abktP8GatzNw3DvJkU="
|
||||
VITE_REVERB_HOST: "dev.cannabrands.app"
|
||||
VITE_REVERB_PORT: "443"
|
||||
VITE_REVERB_SCHEME: "https"
|
||||
cache_images:
|
||||
- code.cannabrands.app/cannabrands/hub:buildcache-dev
|
||||
platforms: linux/amd64
|
||||
when:
|
||||
branch: develop
|
||||
event: push
|
||||
status: success
|
||||
|
||||
# Auto-deploy to dev.cannabrands.com (develop branch only)
|
||||
# Auto-deploy to dev.cannabrands.app (develop branch only)
|
||||
deploy-dev:
|
||||
image: bitnami/kubectl:latest
|
||||
environment:
|
||||
KUBECONFIG_CONTENT:
|
||||
from_secret: kubeconfig_dev
|
||||
commands:
|
||||
- echo "🚀 Auto-deploying to dev.cannabrands.com..."
|
||||
- echo "🚀 Auto-deploying to dev.cannabrands.app..."
|
||||
- echo "Commit SHA${CI_COMMIT_SHA:0:7}"
|
||||
- echo ""
|
||||
# Setup kubeconfig
|
||||
- mkdir -p ~/.kube
|
||||
- echo "$KUBECONFIG_CONTENT" | base64 -d > ~/.kube/config
|
||||
- echo "$KUBECONFIG_CONTENT" | tr -d '[:space:]' | base64 -d > ~/.kube/config
|
||||
- chmod 600 ~/.kube/config
|
||||
# Update deployment to use new SHA-tagged image
|
||||
# Update deployment to use new SHA-tagged image (both app and init containers)
|
||||
- |
|
||||
kubectl set image deployment/cannabrands-hub \
|
||||
app=code.cannabrands.com/cannabrands/hub:dev-${CI_COMMIT_SHA:0:7} \
|
||||
app=code.cannabrands.app/cannabrands/hub:dev-${CI_COMMIT_SHA:0:7} \
|
||||
migrate=code.cannabrands.app/cannabrands/hub:dev-${CI_COMMIT_SHA:0:7} \
|
||||
-n cannabrands-dev
|
||||
# Wait for rollout to complete (timeout 5 minutes)
|
||||
- kubectl rollout status deployment/cannabrands-hub -n cannabrands-dev --timeout=300s
|
||||
@@ -185,19 +232,21 @@ steps:
|
||||
build-image-staging:
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
settings:
|
||||
registry: code.cannabrands.com
|
||||
repo: code.cannabrands.com/cannabrands/hub
|
||||
registry: code.cannabrands.app
|
||||
repo: code.cannabrands.app/cannabrands/hub
|
||||
username:
|
||||
from_secret: gitea_username
|
||||
password:
|
||||
from_secret: gitea_token
|
||||
tags:
|
||||
- staging # Latest staging build → staging.cannabrands.com
|
||||
- staging # Latest staging build → staging.cannabrands.app
|
||||
- sha-${CI_COMMIT_SHA:0:7} # Commit SHA (industry standard)
|
||||
- ${CI_COMMIT_BRANCH} # Branch name (master)
|
||||
build_args:
|
||||
- GIT_COMMIT_SHA=${CI_COMMIT_SHA:0:7}
|
||||
- APP_VERSION=staging
|
||||
GIT_COMMIT_SHA: "${CI_COMMIT_SHA:0:7}"
|
||||
APP_VERSION: "staging"
|
||||
cache_images:
|
||||
- code.cannabrands.app/cannabrands/hub:buildcache-staging
|
||||
platforms: linux/amd64
|
||||
when:
|
||||
branch: master
|
||||
@@ -208,8 +257,8 @@ steps:
|
||||
build-image-release:
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
settings:
|
||||
registry: code.cannabrands.com
|
||||
repo: code.cannabrands.com/cannabrands/hub
|
||||
registry: code.cannabrands.app
|
||||
repo: code.cannabrands.app/cannabrands/hub
|
||||
username:
|
||||
from_secret: gitea_username
|
||||
password:
|
||||
@@ -218,8 +267,10 @@ steps:
|
||||
- ${CI_COMMIT_TAG} # CalVer tag (e.g., 2025.10.1)
|
||||
- latest # Latest stable release
|
||||
build_args:
|
||||
- GIT_COMMIT_SHA=${CI_COMMIT_SHA:0:7}
|
||||
- APP_VERSION=${CI_COMMIT_TAG}
|
||||
GIT_COMMIT_SHA: "${CI_COMMIT_SHA:0:7}"
|
||||
APP_VERSION: "${CI_COMMIT_TAG}"
|
||||
cache_images:
|
||||
- code.cannabrands.app/cannabrands/hub:buildcache-prod
|
||||
platforms: linux/amd64
|
||||
when:
|
||||
event: tag
|
||||
@@ -240,14 +291,14 @@ steps:
|
||||
echo "🎉 PRODUCTION RELEASE BUILD COMPLETE"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Version: ${CI_COMMIT_TAG}"
|
||||
echo "Registry: code.cannabrands.com/cannabrands/hub"
|
||||
echo "Registry: code.cannabrands.app/cannabrands/hub"
|
||||
echo ""
|
||||
echo "Available as:"
|
||||
echo " - code.cannabrands.com/cannabrands/hub:${CI_COMMIT_TAG}"
|
||||
echo " - code.cannabrands.com/cannabrands/hub:latest"
|
||||
echo " - code.cannabrands.app/cannabrands/hub:${CI_COMMIT_TAG}"
|
||||
echo " - code.cannabrands.app/cannabrands/hub:latest"
|
||||
echo ""
|
||||
echo "🚀 Deploy to PRODUCTION (hub.cannabrands.com):"
|
||||
echo " docker pull code.cannabrands.com/cannabrands/hub:${CI_COMMIT_TAG}"
|
||||
echo "🚀 Deploy to PRODUCTION (cannabrands.app):"
|
||||
echo " docker pull code.cannabrands.app/cannabrands/hub:${CI_COMMIT_TAG}"
|
||||
echo " docker-compose -f docker-compose.production.yml up -d"
|
||||
echo ""
|
||||
echo "⚠️ This is a CUSTOMER-FACING release!"
|
||||
@@ -258,18 +309,18 @@ steps:
|
||||
echo "🧪 STAGING BUILD COMPLETE"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Branch: master"
|
||||
echo "Registry: code.cannabrands.com/cannabrands/hub"
|
||||
echo "Registry: code.cannabrands.app/cannabrands/hub"
|
||||
echo "Tags:"
|
||||
echo " - staging"
|
||||
echo " - sha-${CI_COMMIT_SHA:0:7}"
|
||||
echo " - ${CI_COMMIT_BRANCH}"
|
||||
echo ""
|
||||
echo "📦 Deploy to STAGING (staging.cannabrands.com):"
|
||||
echo " docker pull code.cannabrands.com/cannabrands/hub:staging"
|
||||
echo "📦 Deploy to STAGING (staging.cannabrands.app):"
|
||||
echo " docker pull code.cannabrands.app/cannabrands/hub:staging"
|
||||
echo " docker-compose -f docker-compose.staging.yml up -d"
|
||||
echo ""
|
||||
echo "👥 Next steps:"
|
||||
echo " 1. Super-admin tests on staging.cannabrands.com"
|
||||
echo " 1. Super-admin tests on staging.cannabrands.app"
|
||||
echo " 2. Validate all features work"
|
||||
echo " 3. When ready, create production tag:"
|
||||
echo " git tag -a 2025.10.1 -m 'Release 2025.10.1'"
|
||||
@@ -284,28 +335,28 @@ steps:
|
||||
echo "Commit: ${CI_COMMIT_SHA:0:7}"
|
||||
echo ""
|
||||
echo "✅ Built & Tagged:"
|
||||
echo " - code.cannabrands.com/cannabrands/hub:dev"
|
||||
echo " - code.cannabrands.com/cannabrands/hub:dev-${CI_COMMIT_SHA:0:7}"
|
||||
echo " - code.cannabrands.com/cannabrands/hub:sha-${CI_COMMIT_SHA:0:7}"
|
||||
echo " - code.cannabrands.app/cannabrands/hub:dev"
|
||||
echo " - code.cannabrands.app/cannabrands/hub:dev-${CI_COMMIT_SHA:0:7}"
|
||||
echo " - code.cannabrands.app/cannabrands/hub:sha-${CI_COMMIT_SHA:0:7}"
|
||||
echo ""
|
||||
echo "✅ Auto-Deployed to Kubernetes:"
|
||||
echo " - Environment: dev.cannabrands.com"
|
||||
echo " - Environment: dev.cannabrands.app"
|
||||
echo " - Namespace: cannabrands-dev"
|
||||
echo " - Image: dev-${CI_COMMIT_SHA:0:7}"
|
||||
echo ""
|
||||
echo "🧪 Test your changes:"
|
||||
echo " - Visit: https://dev.cannabrands.com"
|
||||
echo " - Visit: https://dev.cannabrands.app"
|
||||
echo " - Login: admin@example.com / password"
|
||||
echo " - Check: https://dev.cannabrands.com/telescope"
|
||||
echo " - Check: https://dev.cannabrands.app/telescope"
|
||||
echo ""
|
||||
echo "👥 Next steps:"
|
||||
echo " 1. Verify feature works on dev.cannabrands.com"
|
||||
echo " 1. Verify feature works on dev.cannabrands.app"
|
||||
echo " 2. When stable, merge to master for staging:"
|
||||
echo " git checkout master && git merge develop && git push"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
fi
|
||||
|
||||
# Database service for tests
|
||||
# Services for tests
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
@@ -313,3 +364,8 @@ services:
|
||||
POSTGRES_USER: testing
|
||||
POSTGRES_PASSWORD: testing
|
||||
POSTGRES_DB: testing
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
commands:
|
||||
- redis-server --bind 0.0.0.0
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
## Overview
|
||||
|
||||
This document explains how to set up automatic deployment to `dev.cannabrands.com` after successful CI builds.
|
||||
This document explains how to set up automatic deployment to `dev.cannabrands.app` after successful CI builds.
|
||||
|
||||
**What happens:**
|
||||
```
|
||||
Push to develop → CI Passes → Docker Build → Auto-Deploy to K8s → Live on dev.cannabrands.com
|
||||
Push to develop → CI Passes → Docker Build → Auto-Deploy to K8s → Live on dev.cannabrands.app
|
||||
```
|
||||
|
||||
---
|
||||
@@ -35,7 +35,7 @@ cat ~/.kube/config | base64 # Linux/Windows (copy output)
|
||||
|
||||
### Step 2: Add Secret to Woodpecker
|
||||
|
||||
1. Go to Woodpecker CI: `https://ci.cannabrands.com`
|
||||
1. Go to Woodpecker CI: `https://ci.cannabrands.app`
|
||||
2. Navigate to: **Repositories → cannabrands/hub → Settings → Secrets**
|
||||
3. Click **"Add Secret"**
|
||||
4. Fill in:
|
||||
@@ -58,7 +58,7 @@ git push origin develop
|
||||
# - Tests pass ✅
|
||||
# - Build completes ✅
|
||||
# - Deploy step runs ✅
|
||||
# - Check dev.cannabrands.com - your change is live!
|
||||
# - Check dev.cannabrands.app - your change is live!
|
||||
```
|
||||
|
||||
---
|
||||
@@ -69,14 +69,14 @@ git push origin develop
|
||||
|
||||
**Before (Mutable Tags - Problematic):**
|
||||
```
|
||||
code.cannabrands.com/cannabrands/hub:dev # Overwritten each build
|
||||
code.cannabrands.app/cannabrands/hub:dev # Overwritten each build
|
||||
```
|
||||
|
||||
**After (Immutable Tags - Best Practice):**
|
||||
```
|
||||
code.cannabrands.com/cannabrands/hub:dev-a28d5b5 # Unique SHA tag
|
||||
code.cannabrands.com/cannabrands/hub:dev # Latest dev (convenience)
|
||||
code.cannabrands.com/cannabrands/hub:sha-a28d5b5 # Generic SHA
|
||||
code.cannabrands.app/cannabrands/hub:dev-a28d5b5 # Unique SHA tag
|
||||
code.cannabrands.app/cannabrands/hub:dev # Latest dev (convenience)
|
||||
code.cannabrands.app/cannabrands/hub:sha-a28d5b5 # Generic SHA
|
||||
```
|
||||
|
||||
### Auto-Deploy Flow
|
||||
@@ -89,7 +89,7 @@ graph LR
|
||||
D --> E[Tag: dev-SHA]
|
||||
E --> F[Deploy Step: kubectl set image]
|
||||
F --> G[Rollout & Health Check]
|
||||
G --> H[Live on dev.cannabrands.com]
|
||||
G --> H[Live on dev.cannabrands.app]
|
||||
```
|
||||
|
||||
**Key Safety Features:**
|
||||
@@ -109,14 +109,14 @@ If a deployment breaks dev, roll back to the previous version:
|
||||
kubectl get deployment cannabrands-hub -n cannabrands-dev \
|
||||
-o jsonpath='{.spec.template.spec.containers[0].image}'
|
||||
|
||||
# Output: code.cannabrands.com/cannabrands/hub:dev-a28d5b5
|
||||
# Output: code.cannabrands.app/cannabrands/hub:dev-a28d5b5
|
||||
|
||||
# 2. Check git log for previous commit
|
||||
git log --oneline develop | head -5
|
||||
|
||||
# 3. Rollback to previous SHA
|
||||
kubectl set image deployment/cannabrands-hub \
|
||||
app=code.cannabrands.com/cannabrands/hub:dev-PREVIOUS_SHA \
|
||||
app=code.cannabrands.app/cannabrands/hub:dev-PREVIOUS_SHA \
|
||||
-n cannabrands-dev
|
||||
|
||||
# 4. Verify rollback
|
||||
@@ -144,7 +144,7 @@ kubectl rollout status deployment/cannabrands-hub -n cannabrands-dev
|
||||
Want auto-deploy for staging too? Add this to `.woodpecker/.ci.yml`:
|
||||
|
||||
```yaml
|
||||
# Auto-deploy to staging.cannabrands.com (master branch)
|
||||
# Auto-deploy to staging.cannabrands.app (master branch)
|
||||
deploy-staging:
|
||||
image: bitnami/kubectl:latest
|
||||
environment:
|
||||
@@ -156,7 +156,7 @@ deploy-staging:
|
||||
- chmod 600 ~/.kube/config
|
||||
- |
|
||||
kubectl set image deployment/cannabrands-hub \
|
||||
app=code.cannabrands.com/cannabrands/hub:staging-${CI_COMMIT_SHA:0:7} \
|
||||
app=code.cannabrands.app/cannabrands/hub:staging-${CI_COMMIT_SHA:0:7} \
|
||||
-n cannabrands-staging
|
||||
- kubectl rollout status deployment/cannabrands-hub -n cannabrands-staging --timeout=300s
|
||||
when:
|
||||
@@ -189,12 +189,12 @@ kubectl logs -n cannabrands-dev deployment/cannabrands-hub --tail=100
|
||||
|
||||
### Woodpecker CI Logs
|
||||
|
||||
1. Go to: `https://ci.cannabrands.com`
|
||||
1. Go to: `https://ci.cannabrands.app`
|
||||
2. Click on latest pipeline
|
||||
3. Expand "deploy-dev" step
|
||||
4. See deployment output:
|
||||
```
|
||||
🚀 Auto-deploying to dev.cannabrands.com...
|
||||
🚀 Auto-deploying to dev.cannabrands.app...
|
||||
Commit SHA: a28d5b5
|
||||
|
||||
deployment.apps/cannabrands-hub image updated
|
||||
@@ -207,7 +207,7 @@ kubectl logs -n cannabrands-dev deployment/cannabrands-hub --tail=100
|
||||
cannabrands-hub-7d85986845-gnkbv 1/1 Running 0 45s
|
||||
|
||||
Image deployed:
|
||||
code.cannabrands.com/cannabrands/hub:dev-a28d5b5
|
||||
code.cannabrands.app/cannabrands/hub:dev-a28d5b5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -47,8 +47,8 @@ steps:
|
||||
build-image:
|
||||
image: plugins/docker
|
||||
settings:
|
||||
registry: code.cannabrands.com
|
||||
repo: code.cannabrands.com/cannabrands/hub
|
||||
registry: code.cannabrands.app
|
||||
repo: code.cannabrands.app/cannabrands/hub
|
||||
tags: [latest, ${CI_COMMIT_SHA:0:8}]
|
||||
when:
|
||||
branch: master
|
||||
@@ -68,7 +68,7 @@ steps:
|
||||
```bash
|
||||
# On production server
|
||||
ssh cannabrands-prod
|
||||
docker pull code.cannabrands.com/cannabrands/hub:bef77df8
|
||||
docker pull code.cannabrands.app/cannabrands/hub:bef77df8
|
||||
docker-compose up -d
|
||||
# Or use deployment tool like Ansible, Deployer, etc.
|
||||
```
|
||||
@@ -108,7 +108,7 @@ steps:
|
||||
from_secret: ssh_private_key
|
||||
script:
|
||||
- cd /var/www/cannabrands
|
||||
- docker pull code.cannabrands.com/cannabrands/hub:${CI_COMMIT_SHA:0:8}
|
||||
- docker pull code.cannabrands.app/cannabrands/hub:${CI_COMMIT_SHA:0:8}
|
||||
- docker-compose up -d
|
||||
- docker exec cannabrands php artisan migrate --force
|
||||
- docker exec cannabrands php artisan config:cache
|
||||
@@ -141,7 +141,7 @@ Developer → Feature branch → CI tests
|
||||
|
||||
**Git branching:**
|
||||
```
|
||||
develop (latest features) → auto-deploy to staging.cannabrands.com
|
||||
develop (latest features) → auto-deploy to staging.cannabrands.app
|
||||
master (production-ready) → auto-deploy to app.cannabrands.com
|
||||
```
|
||||
|
||||
@@ -154,13 +154,13 @@ steps:
|
||||
deploy-staging:
|
||||
image: appleboy/drone-ssh
|
||||
settings:
|
||||
host: staging.cannabrands.com
|
||||
host: staging.cannabrands.app
|
||||
username: deploy
|
||||
key:
|
||||
from_secret: ssh_private_key
|
||||
script:
|
||||
- cd /var/www/cannabrands
|
||||
- docker pull code.cannabrands.com/cannabrands/hub:${CI_COMMIT_SHA:0:8}
|
||||
- docker pull code.cannabrands.app/cannabrands/hub:${CI_COMMIT_SHA:0:8}
|
||||
- docker-compose up -d
|
||||
when:
|
||||
branch: develop
|
||||
@@ -176,7 +176,7 @@ steps:
|
||||
from_secret: ssh_private_key
|
||||
script:
|
||||
- cd /var/www/cannabrands
|
||||
- docker pull code.cannabrands.com/cannabrands/hub:${CI_COMMIT_SHA:0:8}
|
||||
- docker pull code.cannabrands.app/cannabrands/hub:${CI_COMMIT_SHA:0:8}
|
||||
- docker-compose up -d
|
||||
when:
|
||||
branch: master
|
||||
@@ -337,7 +337,7 @@ Local Development:
|
||||
|
||||
Staging:
|
||||
- Mirrors production
|
||||
- staging.cannabrands.com
|
||||
- staging.cannabrands.app
|
||||
- Real-like data (anonymized)
|
||||
- Auto-deployed from develop branch
|
||||
|
||||
@@ -367,7 +367,7 @@ Production:
|
||||
```bash
|
||||
# Quick rollback (under 2 minutes)
|
||||
ssh cannabrands-prod
|
||||
docker pull code.cannabrands.com/cannabrands/hub:PREVIOUS_COMMIT_SHA
|
||||
docker pull code.cannabrands.app/cannabrands/hub:PREVIOUS_COMMIT_SHA
|
||||
docker-compose up -d
|
||||
|
||||
# Database rollback (if migrations ran)
|
||||
@@ -536,8 +536,8 @@ steps:
|
||||
build-image:
|
||||
image: plugins/docker
|
||||
settings:
|
||||
registry: code.cannabrands.com
|
||||
repo: code.cannabrands.com/cannabrands/hub
|
||||
registry: code.cannabrands.app
|
||||
repo: code.cannabrands.app/cannabrands/hub
|
||||
tags:
|
||||
- ${CI_COMMIT_BRANCH}
|
||||
- ${CI_COMMIT_SHA:0:8}
|
||||
@@ -553,13 +553,13 @@ steps:
|
||||
deploy-staging:
|
||||
image: appleboy/drone-ssh
|
||||
settings:
|
||||
host: staging.cannabrands.com
|
||||
host: staging.cannabrands.app
|
||||
username: deploy
|
||||
key:
|
||||
from_secret: staging_ssh_key
|
||||
script:
|
||||
- cd /var/www/cannabrands
|
||||
- docker pull code.cannabrands.com/cannabrands/hub:${CI_COMMIT_SHA:0:8}
|
||||
- docker pull code.cannabrands.app/cannabrands/hub:${CI_COMMIT_SHA:0:8}
|
||||
- docker-compose up -d
|
||||
- docker exec cannabrands php artisan migrate --force
|
||||
- docker exec cannabrands php artisan config:cache
|
||||
@@ -582,7 +582,7 @@ steps:
|
||||
- echo "To deploy to production:"
|
||||
- echo " ssh cannabrands-prod"
|
||||
- echo " cd /var/www/cannabrands"
|
||||
- echo " docker pull code.cannabrands.com/cannabrands/hub:${CI_COMMIT_SHA:0:8}"
|
||||
- echo " docker pull code.cannabrands.app/cannabrands/hub:${CI_COMMIT_SHA:0:8}"
|
||||
- echo " docker-compose up -d"
|
||||
- echo ""
|
||||
- echo "⚠️ Remember: Check deployment checklist first!"
|
||||
|
||||
@@ -102,7 +102,7 @@ Push to master → Woodpecker runs:
|
||||
→ Build Docker image
|
||||
→ Tag: cannabrands-hub:c165bf9 (commit SHA)
|
||||
→ Tag: cannabrands-hub:latest
|
||||
→ Push to code.cannabrands.com/cannabrands/hub
|
||||
→ Push to code.cannabrands.app/cannabrands/hub
|
||||
→ Image ready, no deployment yet
|
||||
```
|
||||
|
||||
@@ -115,7 +115,7 @@ Push to master → Woodpecker runs:
|
||||
### Phase 3: Auto-Deploy to Staging ⏭️
|
||||
```
|
||||
Push to master → Tests pass → Build image
|
||||
→ Auto-deploy to staging.cannabrands.com
|
||||
→ Auto-deploy to staging.cannabrands.app
|
||||
→ Use staging environment variables
|
||||
→ Manual QA testing
|
||||
→ Production still manual
|
||||
@@ -177,7 +177,7 @@ CMD ["php-fpm"]
|
||||
### Staging Deployment:
|
||||
```bash
|
||||
# Pull the same image
|
||||
docker pull code.cannabrands.com/cannabrands/hub:c165bf9
|
||||
docker pull code.cannabrands.app/cannabrands/hub:c165bf9
|
||||
|
||||
# Run with staging environment
|
||||
docker run \
|
||||
@@ -186,13 +186,13 @@ docker run \
|
||||
-e DB_DATABASE=cannabrands_staging \
|
||||
-e APP_DEBUG=true \
|
||||
-e MAIL_MAILER=log \
|
||||
code.cannabrands.com/cannabrands/hub:c165bf9
|
||||
code.cannabrands.app/cannabrands/hub:c165bf9
|
||||
```
|
||||
|
||||
### Production Deployment:
|
||||
```bash
|
||||
# Pull THE EXACT SAME IMAGE
|
||||
docker pull code.cannabrands.com/cannabrands/hub:c165bf9
|
||||
docker pull code.cannabrands.app/cannabrands/hub:c165bf9
|
||||
|
||||
# Run with production environment
|
||||
docker run \
|
||||
@@ -201,7 +201,7 @@ docker run \
|
||||
-e DB_DATABASE=cannabrands_production \
|
||||
-e APP_DEBUG=false \
|
||||
-e MAIL_MAILER=smtp \
|
||||
code.cannabrands.com/cannabrands/hub:c165bf9
|
||||
code.cannabrands.app/cannabrands/hub:c165bf9
|
||||
```
|
||||
|
||||
**Key point**: Notice it's the **exact same image** (`c165bf9`), only environment variables differ.
|
||||
@@ -218,7 +218,7 @@ docker run \
|
||||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
image: code.cannabrands.com/cannabrands/hub:latest
|
||||
image: code.cannabrands.app/cannabrands/hub:latest
|
||||
env_file:
|
||||
- .env.staging # Staging-specific vars
|
||||
ports:
|
||||
@@ -253,7 +253,7 @@ secrets:
|
||||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
image: code.cannabrands.com/cannabrands/hub:c165bf9 # Specific SHA
|
||||
image: code.cannabrands.app/cannabrands/hub:c165bf9 # Specific SHA
|
||||
env_file:
|
||||
- .env.production # Production-specific vars
|
||||
ports:
|
||||
@@ -301,7 +301,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: app
|
||||
image: code.cannabrands.com/cannabrands/hub:c165bf9
|
||||
image: code.cannabrands.app/cannabrands/hub:c165bf9
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: app-config-staging # Different per namespace
|
||||
@@ -350,8 +350,8 @@ steps:
|
||||
build-and-publish:
|
||||
image: plugins/docker
|
||||
settings:
|
||||
registry: code.cannabrands.com
|
||||
repo: code.cannabrands.com/cannabrands/hub
|
||||
registry: code.cannabrands.app
|
||||
repo: code.cannabrands.app/cannabrands/hub
|
||||
tags:
|
||||
- latest # Always overwrite
|
||||
- ${CI_COMMIT_SHA:0:8} # Immutable SHA
|
||||
@@ -384,7 +384,7 @@ Date: 2025-01-15 14:30:00 PST
|
||||
Image: cannabrands-hub:c165bf9
|
||||
Deployed by: jon@cannabrands.com
|
||||
Approved by: compliance@cannabrands.com
|
||||
Git commit: https://code.cannabrands.com/.../c165bf9
|
||||
Git commit: https://code.cannabrands.app/.../c165bf9
|
||||
Changes: Invoice picking workflow update
|
||||
Tests passed: ✅ 28/28
|
||||
Staging tested: ✅ 2 hours
|
||||
@@ -424,7 +424,7 @@ Rollback image: cannabrands-hub:a1b2c3d
|
||||
```bash
|
||||
# On production server
|
||||
ssh cannabrands-prod
|
||||
docker pull code.cannabrands.com/cannabrands/hub:c165bf9
|
||||
docker pull code.cannabrands.app/cannabrands/hub:c165bf9
|
||||
docker-compose -f docker-compose.production.yml up -d
|
||||
```
|
||||
|
||||
@@ -487,14 +487,14 @@ steps:
|
||||
security-scan:
|
||||
image: aquasec/trivy
|
||||
commands:
|
||||
- trivy image code.cannabrands.com/cannabrands/hub:${CI_COMMIT_SHA:0:8}
|
||||
- trivy image code.cannabrands.app/cannabrands/hub:${CI_COMMIT_SHA:0:8}
|
||||
```
|
||||
|
||||
### 4. Sign Images (Advanced)
|
||||
|
||||
Use Cosign to cryptographically sign images:
|
||||
```bash
|
||||
cosign sign code.cannabrands.com/cannabrands/hub:c165bf9
|
||||
cosign sign code.cannabrands.app/cannabrands/hub:c165bf9
|
||||
```
|
||||
|
||||
Compliance benefit: Prove image hasn't been tampered with.
|
||||
@@ -507,10 +507,10 @@ Compliance benefit: Prove image hasn't been tampered with.
|
||||
|
||||
```bash
|
||||
# List recent deployments
|
||||
docker images code.cannabrands.com/cannabrands/hub
|
||||
docker images code.cannabrands.app/cannabrands/hub
|
||||
|
||||
# Rollback to previous version
|
||||
docker pull code.cannabrands.com/cannabrands/hub:a1b2c3d
|
||||
docker pull code.cannabrands.app/cannabrands/hub:a1b2c3d
|
||||
docker-compose -f docker-compose.production.yml up -d
|
||||
```
|
||||
|
||||
@@ -531,7 +531,7 @@ deploy:
|
||||
# Before risky deployment
|
||||
git tag -a v1.5.2-stable -m "Last known good version"
|
||||
docker tag cannabrands-hub:current cannabrands-hub:v1.5.2-stable
|
||||
docker push code.cannabrands.com/cannabrands/hub:v1.5.2-stable
|
||||
docker push code.cannabrands.app/cannabrands/hub:v1.5.2-stable
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -41,7 +41,7 @@ The current implementation uses **filesystem caching** (Option 1 below) with the
|
||||
### Option 1: Enable Repository Trust (Recommended)
|
||||
|
||||
**In Woodpecker UI:**
|
||||
1. Go to: https://ci.cannabrands.com/repos/1/settings
|
||||
1. Go to: https://ci.cannabrands.app/repos/1/settings
|
||||
2. Click **"Settings"** tab
|
||||
3. Enable **"Trusted"** toggle
|
||||
4. Save settings
|
||||
@@ -254,25 +254,25 @@ WORKDIR /woodpecker/src
|
||||
|
||||
**Build and push to Gitea:**
|
||||
```bash
|
||||
docker build -f docker/ci-php.Dockerfile -t code.cannabrands.com/cannabrands/ci-php:8.3 .
|
||||
docker push code.cannabrands.com/cannabrands/ci-php:8.3
|
||||
docker build -f docker/ci-php.Dockerfile -t code.cannabrands.app/cannabrands/ci-php:8.3 .
|
||||
docker push code.cannabrands.app/cannabrands/ci-php:8.3
|
||||
```
|
||||
|
||||
**Update `.woodpecker/.ci.yml`:**
|
||||
```yaml
|
||||
steps:
|
||||
php-lint:
|
||||
image: code.cannabrands.com/cannabrands/ci-php:8.3
|
||||
image: code.cannabrands.app/cannabrands/ci-php:8.3
|
||||
commands:
|
||||
- find app routes database -name "*.php" -exec php -l {} \;
|
||||
|
||||
composer-install:
|
||||
image: code.cannabrands.com/cannabrands/ci-php:8.3
|
||||
image: code.cannabrands.app/cannabrands/ci-php:8.3
|
||||
commands:
|
||||
- composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||
|
||||
code-style:
|
||||
image: code.cannabrands.com/cannabrands/ci-php:8.3
|
||||
image: code.cannabrands.app/cannabrands/ci-php:8.3
|
||||
commands:
|
||||
- ./vendor/bin/pint --test
|
||||
```
|
||||
@@ -323,4 +323,4 @@ steps:
|
||||
**Enable caching when**: Team grows or build frequency increases
|
||||
**Recommended method**: Repository trust + filesystem cache
|
||||
|
||||
To enable caching now, just go to https://ci.cannabrands.com/repos/1/settings and enable "Trusted".
|
||||
To enable caching now, just go to https://ci.cannabrands.app/repos/1/settings and enable "Trusted".
|
||||
|
||||
@@ -1,578 +0,0 @@
|
||||
# Git Branching Strategy for Cannabrands
|
||||
|
||||
## Evolution of Your Workflow
|
||||
|
||||
Your branching strategy should evolve with your team size and customer base. This document outlines the transition path.
|
||||
|
||||
---
|
||||
|
||||
## Phase 0: Pre-Release (Current)
|
||||
|
||||
**Team Size:** 1-2 developers
|
||||
**Customers:** None yet
|
||||
**Goal:** Move fast, iterate quickly
|
||||
|
||||
### Current Workflow: Direct to Master
|
||||
|
||||
```
|
||||
Local changes → Commit to master → Push → CI tests → (Optional: Deploy to dev)
|
||||
```
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
# Make changes
|
||||
git add .
|
||||
git commit -m "feat: add new feature"
|
||||
git push origin master
|
||||
```
|
||||
|
||||
**When this works:**
|
||||
- ✅ Solo developer or tight 2-person team
|
||||
- ✅ No customers depending on stability
|
||||
- ✅ Fast iteration is priority #1
|
||||
- ✅ Quick fixes needed immediately
|
||||
|
||||
**When to stop:**
|
||||
- ❌ First paying customer signs up
|
||||
- ❌ Team grows to 3+ developers
|
||||
- ❌ Too many merge conflicts
|
||||
- ❌ Need code review before deployment
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Feature Branches (Transition)
|
||||
|
||||
**Team Size:** 2-5 developers
|
||||
**Customers:** First few customers OR approaching launch
|
||||
**Goal:** Add safety through code review
|
||||
|
||||
### GitHub Flow: Feature Branches + PRs
|
||||
|
||||
```
|
||||
master (stable, auto-deploys to dev)
|
||||
↑
|
||||
Pull Requests (code review required)
|
||||
↑
|
||||
feature branches (work in progress)
|
||||
```
|
||||
|
||||
**Workflow:**
|
||||
|
||||
```bash
|
||||
# 1. Create feature branch from master
|
||||
git checkout master
|
||||
git pull origin master
|
||||
git checkout -b feature/add-payment-terms
|
||||
|
||||
# 2. Make changes
|
||||
# ... edit files ...
|
||||
git add .
|
||||
git commit -m "feat: add payment term surcharge calculation"
|
||||
|
||||
# 3. Push feature branch
|
||||
git push origin feature/add-payment-terms
|
||||
|
||||
# 4. Create Pull Request in Gitea
|
||||
# - Go to Gitea UI
|
||||
# - Click "New Pull Request"
|
||||
# - Base: master, Compare: feature/add-payment-terms
|
||||
# - Add description and assign reviewer
|
||||
|
||||
# 5. After approval, merge to master
|
||||
# - Click "Merge" button in Gitea
|
||||
# - Delete feature branch
|
||||
|
||||
# 6. Pull updated master
|
||||
git checkout master
|
||||
git pull origin master
|
||||
git branch -d feature/add-payment-terms
|
||||
```
|
||||
|
||||
**Branch Naming Conventions:**
|
||||
```
|
||||
feature/short-description # New features
|
||||
fix/bug-description # Bug fixes
|
||||
refactor/what-changed # Code refactoring
|
||||
docs/what-documented # Documentation only
|
||||
test/what-tested # Test additions
|
||||
|
||||
Examples:
|
||||
feature/buyer-registration
|
||||
fix/invoice-calculation-error
|
||||
refactor/order-service
|
||||
docs/deployment-guide
|
||||
test/checkout-flow
|
||||
```
|
||||
|
||||
**Commit Message Format:**
|
||||
```
|
||||
type(scope): subject
|
||||
|
||||
Examples:
|
||||
feat(checkout): add payment term selection
|
||||
fix(invoice): correct tax calculation for multi-state orders
|
||||
refactor(orders): extract order validation logic
|
||||
docs(readme): update local setup instructions
|
||||
test(auth): add buyer login tests
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- ✅ Code review before merging
|
||||
- ✅ Catch bugs early
|
||||
- ✅ Knowledge sharing across team
|
||||
- ✅ Cleaner git history
|
||||
- ✅ Can work on multiple features in parallel
|
||||
|
||||
**When to stop:**
|
||||
- ❌ Team grows beyond 10 people
|
||||
- ❌ Need to manage multiple versions
|
||||
- ❌ Need longer release cycles
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Environment Branches (Mature)
|
||||
|
||||
**Team Size:** 5+ developers
|
||||
**Customers:** Growing customer base
|
||||
**Goal:** Staged rollout with stability
|
||||
|
||||
### Two-Branch Model: Develop + Master
|
||||
|
||||
```
|
||||
master (production) ← deploys to app.cannabrands.com
|
||||
↑
|
||||
Pull Requests (from develop, tested on staging)
|
||||
↑
|
||||
develop (integration) ← deploys to staging.cannabrands.com
|
||||
↑
|
||||
Pull Requests (from feature branches)
|
||||
↑
|
||||
feature branches (work in progress)
|
||||
```
|
||||
|
||||
**Workflow:**
|
||||
|
||||
```bash
|
||||
# 1. Create feature branch from develop
|
||||
git checkout develop
|
||||
git pull origin develop
|
||||
git checkout -b feature/bulk-order-import
|
||||
|
||||
# 2. Make changes and commit
|
||||
git add .
|
||||
git commit -m "feat(orders): add CSV bulk import"
|
||||
git push origin feature/bulk-order-import
|
||||
|
||||
# 3. Create PR: feature → develop
|
||||
# - Merging to develop auto-deploys to staging.cannabrands.com
|
||||
# - Team tests on staging
|
||||
|
||||
# 4. When staging is stable, create PR: develop → master
|
||||
# - Requires approval from team lead
|
||||
# - Merging to master deploys to production
|
||||
|
||||
# 5. Regular develop → master promotions
|
||||
# - Every 1-2 weeks
|
||||
# - Or after major features are tested
|
||||
```
|
||||
|
||||
**Branch Protection Rules:**
|
||||
|
||||
**Master Branch:**
|
||||
- ✅ Require pull request reviews (1+ approvals)
|
||||
- ✅ Require status checks to pass (CI tests)
|
||||
- ✅ Require branches to be up to date
|
||||
- ✅ Restrict who can push (admin only)
|
||||
|
||||
**Develop Branch:**
|
||||
- ✅ Require pull request reviews (1+ approval)
|
||||
- ✅ Require status checks to pass
|
||||
- ⬜ Allow force pushes (optional)
|
||||
|
||||
**Setup in Gitea:**
|
||||
```
|
||||
Settings → Repository → Branches → Add Branch Protection Rule
|
||||
- Branch name pattern: master
|
||||
- Enable protection
|
||||
- Require pull request reviews before merging
|
||||
- Dismiss stale pull request approvals when new commits are pushed
|
||||
- Require status checks to pass before merging
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- ✅ Staging environment matches production
|
||||
- ✅ Catch integration issues before customers see them
|
||||
- ✅ Multiple features can be tested together
|
||||
- ✅ Rollback is easier (master = last known good)
|
||||
- ✅ QA team has stable environment to test
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Release Branches (Enterprise)
|
||||
|
||||
**Team Size:** 10+ developers
|
||||
**Customers:** Large customer base, SLA commitments
|
||||
**Goal:** Support multiple versions, scheduled releases
|
||||
|
||||
### Git Flow: Full Enterprise Model
|
||||
|
||||
```
|
||||
master (production) ← hotfixes, release branches
|
||||
↑
|
||||
release/v2.1.0 (release preparation)
|
||||
↑
|
||||
develop (integration) ← feature branches
|
||||
↑
|
||||
feature/* (work in progress)
|
||||
```
|
||||
|
||||
**When you need this:**
|
||||
- Multiple versions in production (e.g., self-hosted + SaaS)
|
||||
- Need to support old versions
|
||||
- Scheduled release cycles (monthly/quarterly)
|
||||
- Regulatory requirements for change control
|
||||
|
||||
**This is probably overkill for most cannabis startups.**
|
||||
|
||||
---
|
||||
|
||||
## Hotfix Workflow (All Phases)
|
||||
|
||||
**When:** Critical bug in production needs immediate fix
|
||||
|
||||
### Quick Hotfix Process
|
||||
|
||||
```bash
|
||||
# 1. Create hotfix branch from master
|
||||
git checkout master
|
||||
git pull origin master
|
||||
git checkout -b hotfix/invoice-calculation-fix
|
||||
|
||||
# 2. Make MINIMAL changes to fix the bug
|
||||
git add .
|
||||
git commit -m "fix(invoice): correct tax calculation for CA"
|
||||
|
||||
# 3. Test locally
|
||||
./vendor/bin/sail artisan test
|
||||
|
||||
# 4. Create PR to master (expedited review)
|
||||
# - Mark as "HOTFIX - URGENT"
|
||||
# - Get quick approval from team lead
|
||||
|
||||
# 5. After merging to master:
|
||||
# - Deploy to production immediately
|
||||
# - Backport fix to develop branch
|
||||
|
||||
git checkout develop
|
||||
git merge master
|
||||
git push origin develop
|
||||
```
|
||||
|
||||
**Hotfix Rules:**
|
||||
- ⚠️ Only for critical production bugs
|
||||
- ⚠️ Must be small, focused changes
|
||||
- ⚠️ Expedited review process
|
||||
- ⚠️ Deploy ASAP after merge
|
||||
|
||||
---
|
||||
|
||||
## Recommended Transition Timeline
|
||||
|
||||
### Now → First Customer (Phase 0)
|
||||
|
||||
**Strategy:** Direct to master
|
||||
**Why:** Fast iteration, no customer impact
|
||||
|
||||
```bash
|
||||
# Your current workflow - keep doing this
|
||||
git commit -am "feat: add new feature"
|
||||
git push origin master
|
||||
```
|
||||
|
||||
### First Customer → 5 Customers (Phase 1)
|
||||
|
||||
**Trigger:** First paying customer OR 3+ developers
|
||||
**Strategy:** Feature branches with PRs
|
||||
**Timeline:** Implement within 2 weeks of first customer
|
||||
|
||||
**Action Items:**
|
||||
1. Create `CONTRIBUTING.md` with PR guidelines
|
||||
2. Enable branch protection on master
|
||||
3. Train team on PR workflow
|
||||
4. Set up code review rotation
|
||||
|
||||
### 5+ Customers → Growth Phase (Phase 2)
|
||||
|
||||
**Trigger:**
|
||||
- 5+ developers on team OR
|
||||
- 50+ active customers OR
|
||||
- Need for staging environment
|
||||
|
||||
**Strategy:** Develop + Master branches
|
||||
**Timeline:** Plan 1 month for transition
|
||||
|
||||
**Action Items:**
|
||||
1. Set up staging.cannabrands.com server
|
||||
2. Create develop branch
|
||||
3. Update CI/CD for both branches
|
||||
4. Document new workflow for team
|
||||
|
||||
---
|
||||
|
||||
## Pull Request Best Practices
|
||||
|
||||
### Writing Good PRs
|
||||
|
||||
**Title Format:**
|
||||
```
|
||||
type(scope): brief description
|
||||
|
||||
Examples:
|
||||
feat(orders): add bulk order import
|
||||
fix(invoice): correct tax calculation
|
||||
refactor(auth): simplify login flow
|
||||
```
|
||||
|
||||
**Description Template:**
|
||||
```markdown
|
||||
## What Changed
|
||||
Brief description of what this PR does
|
||||
|
||||
## Why
|
||||
Explain the problem this solves or feature it adds
|
||||
|
||||
## How to Test
|
||||
1. Go to /orders/import
|
||||
2. Upload sample CSV
|
||||
3. Verify orders are created correctly
|
||||
|
||||
## Screenshots (if UI changes)
|
||||
[Attach screenshots]
|
||||
|
||||
## Checklist
|
||||
- [ ] Tests added/updated
|
||||
- [ ] Documentation updated
|
||||
- [ ] Tested locally
|
||||
- [ ] No merge conflicts
|
||||
```
|
||||
|
||||
### Reviewing PRs
|
||||
|
||||
**What to look for:**
|
||||
- ✅ Code solves the stated problem
|
||||
- ✅ Tests cover new functionality
|
||||
- ✅ Follows existing code style
|
||||
- ✅ No obvious bugs or security issues
|
||||
- ✅ Documentation is updated
|
||||
|
||||
**How to provide feedback:**
|
||||
```markdown
|
||||
## Blocking Issues (must fix before merge)
|
||||
- [ ] Line 45: This will cause a division by zero error
|
||||
|
||||
## Suggestions (nice to have)
|
||||
- Line 23: Consider extracting this to a helper method
|
||||
- Could add a comment explaining this logic
|
||||
|
||||
## Praise (always include!)
|
||||
- Great test coverage!
|
||||
- Clean implementation of the CSV parser
|
||||
```
|
||||
|
||||
### PR Etiquette
|
||||
|
||||
**Author:**
|
||||
- Keep PRs small (<400 lines changed)
|
||||
- Respond to feedback within 24 hours
|
||||
- Don't merge your own PRs (unless emergency)
|
||||
- Update PR if master changes
|
||||
|
||||
**Reviewer:**
|
||||
- Review within 24 hours
|
||||
- Be kind and constructive
|
||||
- Ask questions instead of making demands
|
||||
- Approve when ready (don't hold up progress)
|
||||
|
||||
---
|
||||
|
||||
## Merge Strategies
|
||||
|
||||
### Squash and Merge (Recommended for Feature Branches)
|
||||
|
||||
**What it does:** Combines all commits into one when merging
|
||||
|
||||
**Use when:**
|
||||
- Feature branch has messy commit history
|
||||
- Want clean master history
|
||||
- PRs are self-contained features
|
||||
|
||||
**Gitea Setting:** "Squash and merge" button in PR
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Before merge (feature branch):
|
||||
- fix typo
|
||||
- wip: add validation
|
||||
- add tests
|
||||
- fix tests
|
||||
- update docs
|
||||
|
||||
After merge (master):
|
||||
- feat(orders): add bulk import with CSV validation (#42)
|
||||
```
|
||||
|
||||
### Regular Merge (For Long-Running Branches)
|
||||
|
||||
**What it does:** Preserves all commits and creates merge commit
|
||||
|
||||
**Use when:**
|
||||
- Merging develop → master
|
||||
- Want to preserve detailed history
|
||||
- Multiple developers collaborated on branch
|
||||
|
||||
---
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Starting New Feature
|
||||
|
||||
```bash
|
||||
# Update master
|
||||
git checkout master
|
||||
git pull origin master
|
||||
|
||||
# Create feature branch
|
||||
git checkout -b feature/product-variants
|
||||
|
||||
# Make changes
|
||||
# ... work work work ...
|
||||
|
||||
# Commit regularly (small commits)
|
||||
git add .
|
||||
git commit -m "feat(products): add variant model"
|
||||
git push origin feature/product-variants
|
||||
|
||||
# Create PR when ready
|
||||
```
|
||||
|
||||
### Updating Feature Branch with Latest Master
|
||||
|
||||
```bash
|
||||
# Your feature branch is behind master
|
||||
git checkout feature/product-variants
|
||||
|
||||
# Option 1: Merge master into feature (preserves commits)
|
||||
git merge origin/master
|
||||
git push origin feature/product-variants
|
||||
|
||||
# Option 2: Rebase on master (cleaner history)
|
||||
git rebase origin/master
|
||||
git push -f origin feature/product-variants # Force push needed after rebase
|
||||
```
|
||||
|
||||
**When to use each:**
|
||||
- **Merge:** Safer, preserves history, good for collaboration
|
||||
- **Rebase:** Cleaner history, good for solo feature branches
|
||||
|
||||
### Fixing Merge Conflicts
|
||||
|
||||
```bash
|
||||
# Pull latest master
|
||||
git checkout master
|
||||
git pull origin master
|
||||
|
||||
# Try to merge (conflict!)
|
||||
git checkout feature/product-variants
|
||||
git merge master
|
||||
|
||||
# Git shows conflicts
|
||||
# CONFLICT (content): Merge conflict in app/Models/Product.php
|
||||
|
||||
# Open conflicted file
|
||||
nano app/Models/Product.php
|
||||
|
||||
# Look for conflict markers:
|
||||
<<<<<<< HEAD
|
||||
// Your changes
|
||||
=======
|
||||
// Changes from master
|
||||
>>>>>>> master
|
||||
|
||||
# Resolve conflicts, remove markers
|
||||
# Save file
|
||||
|
||||
# Mark as resolved
|
||||
git add app/Models/Product.php
|
||||
git commit -m "fix: resolve merge conflicts with master"
|
||||
git push origin feature/product-variants
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Branch Cleanup
|
||||
|
||||
### Deleting Merged Feature Branches
|
||||
|
||||
```bash
|
||||
# After PR is merged, delete local branch
|
||||
git branch -d feature/product-variants
|
||||
|
||||
# Delete remote branch (Gitea can auto-delete)
|
||||
git push origin --delete feature/product-variants
|
||||
|
||||
# Remove stale remote references
|
||||
git remote prune origin
|
||||
|
||||
# See all branches
|
||||
git branch -a
|
||||
```
|
||||
|
||||
### Finding Old Branches
|
||||
|
||||
```bash
|
||||
# List branches by last commit date
|
||||
git for-each-ref --sort=-committerdate refs/heads/
|
||||
|
||||
# Delete branches older than 30 days
|
||||
git branch --merged master | grep -v "master" | xargs git branch -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary: Your Transition Path
|
||||
|
||||
### Today (Pre-Release)
|
||||
✅ **Direct to master** - current setup
|
||||
- Fast iteration
|
||||
- No PR overhead
|
||||
- Good for solo/pair development
|
||||
|
||||
### First Customer (In 1-3 Months)
|
||||
🔜 **Feature branches + PRs**
|
||||
- Add code review
|
||||
- Protect master branch
|
||||
- Enable CI on PRs
|
||||
|
||||
### Growing Team (In 6-12 Months)
|
||||
🔜 **Develop + Master branches**
|
||||
- Add staging environment
|
||||
- Staged rollouts
|
||||
- Better stability
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Phase | Team Size | Branches | Deploy To | When |
|
||||
|-------|-----------|----------|-----------|------|
|
||||
| 0: Pre-release | 1-2 | master | dev | Now |
|
||||
| 1: Feature branches | 2-5 | master + feature/* | dev | First customer |
|
||||
| 2: Environment branches | 5-10 | master + develop + feature/* | staging + prod | Growing team |
|
||||
| 3: Release branches | 10+ | master + develop + release/* + feature/* | Multiple envs | Enterprise |
|
||||
|
||||
**Recommended for Cannabrands:**
|
||||
- **Now:** Phase 0 (direct to master)
|
||||
- **Next:** Phase 1 (feature branches) - within 2 weeks of first customer
|
||||
- **Future:** Phase 2 (develop branch) - when team grows to 5+
|
||||
|
||||
**Key principle:** *"Choose the simplest workflow that meets your current needs. You can always add complexity later."*
|
||||
@@ -32,7 +32,7 @@ Treat your **pre-release testing environment** as if it were production, even th
|
||||
|
||||
**Environment Setup:**
|
||||
```
|
||||
master branch → CI tests pass → Build image → Deploy to dev.cannabrands.com
|
||||
master branch → CI tests pass → Build image → Deploy to dev.cannabrands.app
|
||||
↓
|
||||
Team tests here (internal only)
|
||||
```
|
||||
@@ -52,19 +52,19 @@ master branch → CI tests pass → Build image → Deploy to dev.cannabrands.co
|
||||
|
||||
---
|
||||
|
||||
## Setting Up dev.cannabrands.com
|
||||
## Setting Up dev.cannabrands.app
|
||||
|
||||
### Step 1: Server Preparation
|
||||
|
||||
**Server Requirements:**
|
||||
- Ubuntu 22.04+ or similar
|
||||
- Docker + Docker Compose installed
|
||||
- Domain pointed to server (dev.cannabrands.com)
|
||||
- Domain pointed to server (dev.cannabrands.app)
|
||||
- SSL certificate (Let's Encrypt)
|
||||
|
||||
**Create deployment user:**
|
||||
```bash
|
||||
# On dev.cannabrands.com server
|
||||
# On dev.cannabrands.app server
|
||||
sudo adduser deployer
|
||||
sudo usermod -aG docker deployer
|
||||
sudo mkdir -p /var/www/cannabrands
|
||||
@@ -77,7 +77,7 @@ sudo chown deployer:deployer /var/www/cannabrands
|
||||
ssh-keygen -t ed25519 -C "woodpecker-deploy" -f ~/.ssh/cannabrands_deploy
|
||||
|
||||
# Add public key to server
|
||||
ssh deployer@dev.cannabrands.com
|
||||
ssh deployer@dev.cannabrands.app
|
||||
mkdir -p ~/.ssh
|
||||
nano ~/.ssh/authorized_keys
|
||||
# Paste public key, save
|
||||
@@ -95,7 +95,7 @@ nano ~/.ssh/authorized_keys
|
||||
|
||||
### Step 2: Create Deployment Docker Compose
|
||||
|
||||
**On dev.cannabrands.com server:**
|
||||
**On dev.cannabrands.app server:**
|
||||
```bash
|
||||
cd /var/www/cannabrands
|
||||
nano docker-compose.yml
|
||||
@@ -107,7 +107,7 @@ version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
image: code.cannabrands.com/cannabrands/hub:latest
|
||||
image: code.cannabrands.app/cannabrands/hub:latest
|
||||
container_name: cannabrands_app
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -115,7 +115,7 @@ services:
|
||||
environment:
|
||||
- APP_ENV=development
|
||||
- APP_DEBUG=true
|
||||
- APP_URL=https://dev.cannabrands.com
|
||||
- APP_URL=https://dev.cannabrands.app
|
||||
- DB_HOST=postgres
|
||||
- DB_PORT=5432
|
||||
- DB_DATABASE=cannabrands
|
||||
@@ -168,7 +168,7 @@ APP_NAME="Cannabrands Dev"
|
||||
APP_ENV=development
|
||||
APP_KEY=base64:YOUR_KEY_HERE
|
||||
APP_DEBUG=true
|
||||
APP_URL=https://dev.cannabrands.com
|
||||
APP_URL=https://dev.cannabrands.app
|
||||
|
||||
DB_CONNECTION=pgsql
|
||||
DB_HOST=postgres
|
||||
@@ -204,8 +204,8 @@ steps:
|
||||
build-image:
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
settings:
|
||||
registry: code.cannabrands.com
|
||||
repo: code.cannabrands.com/cannabrands/hub
|
||||
registry: code.cannabrands.app
|
||||
repo: code.cannabrands.app/cannabrands/hub
|
||||
username:
|
||||
from_secret: gitea_username
|
||||
password:
|
||||
@@ -222,12 +222,12 @@ steps:
|
||||
deploy-dev:
|
||||
image: appleboy/drone-ssh
|
||||
settings:
|
||||
host: dev.cannabrands.com
|
||||
host: dev.cannabrands.app
|
||||
username: deployer
|
||||
key:
|
||||
from_secret: dev_ssh_key
|
||||
script:
|
||||
- echo "🚀 Deploying to dev.cannabrands.com..."
|
||||
- echo "🚀 Deploying to dev.cannabrands.app..."
|
||||
- cd /var/www/cannabrands
|
||||
- docker compose pull app
|
||||
- docker compose up -d app
|
||||
@@ -238,7 +238,7 @@ steps:
|
||||
- docker compose exec -T app php artisan route:cache
|
||||
- docker compose exec -T app php artisan view:cache
|
||||
- echo "✅ Deployment complete!"
|
||||
- echo "🌐 Visit https://dev.cannabrands.com"
|
||||
- echo "🌐 Visit https://dev.cannabrands.app"
|
||||
when:
|
||||
branch: master
|
||||
event: push
|
||||
@@ -252,7 +252,7 @@ steps:
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "✅ DEV ENVIRONMENT UPDATED"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "URL: https://dev.cannabrands.com"
|
||||
echo "URL: https://dev.cannabrands.app"
|
||||
echo "Commit: ${CI_COMMIT_SHA:0:8}"
|
||||
echo "Author: ${CI_COMMIT_AUTHOR}"
|
||||
echo "Message: ${CI_COMMIT_MESSAGE}"
|
||||
@@ -373,7 +373,7 @@ git push origin feature/add-payment-terms
|
||||
|
||||
## Dev Environment
|
||||
|
||||
**URL:** https://dev.cannabrands.com
|
||||
**URL:** https://dev.cannabrands.app
|
||||
|
||||
**Status:** 🟡 Active Development
|
||||
- Updates automatically on every master push
|
||||
@@ -470,8 +470,8 @@ Questions? Ask in #development Slack channel or email dev@cannabrands.com
|
||||
|
||||
**What changes:**
|
||||
1. **Rename environments:**
|
||||
- `dev.cannabrands.com` → stays as dev (unstable)
|
||||
- Add `staging.cannabrands.com` → stable pre-release testing
|
||||
- `dev.cannabrands.app` → stays as dev (unstable)
|
||||
- Add `staging.cannabrands.app` → stable pre-release testing
|
||||
- Add `app.cannabrands.com` → production (customers)
|
||||
|
||||
2. **Update git workflow:**
|
||||
@@ -527,7 +527,7 @@ Questions? Ask in #development Slack channel or email dev@cannabrands.com
|
||||
|
||||
```bash
|
||||
# SSH into dev server
|
||||
ssh deployer@dev.cannabrands.com
|
||||
ssh deployer@dev.cannabrands.app
|
||||
|
||||
# Navigate to app directory
|
||||
cd /var/www/cannabrands
|
||||
@@ -564,7 +564,7 @@ docker images | grep cannabrands
|
||||
|
||||
```bash
|
||||
# Pull previous commit's image
|
||||
docker pull code.cannabrands.com/cannabrands/hub:PREVIOUS_SHA
|
||||
docker pull code.cannabrands.app/cannabrands/hub:PREVIOUS_SHA
|
||||
|
||||
# Update docker-compose.yml to use specific tag
|
||||
docker compose up -d app
|
||||
@@ -586,10 +586,10 @@ docker compose exec app php artisan migrate --seed
|
||||
**Your current setup:**
|
||||
- ✅ CI tests pass on every push
|
||||
- ⬜ Build Docker images (add this)
|
||||
- ⬜ Auto-deploy to dev.cannabrands.com (add this)
|
||||
- ⬜ Auto-deploy to dev.cannabrands.app (add this)
|
||||
|
||||
**What this enables:**
|
||||
- Teammates can test at https://dev.cannabrands.com
|
||||
- Teammates can test at https://dev.cannabrands.app
|
||||
- Always reflects latest master
|
||||
- Fast iteration (deploy in ~2 minutes)
|
||||
- No manual deployment needed
|
||||
|
||||
@@ -11,10 +11,10 @@ Once you implement production deployments, Woodpecker will:
|
||||
|
||||
Your images will be available at:
|
||||
```
|
||||
code.cannabrands.com/cannabrands/hub
|
||||
code.cannabrands.app/cannabrands/hub
|
||||
```
|
||||
|
||||
**View packages**: https://code.cannabrands.com/Cannabrands/hub/-/packages
|
||||
**View packages**: https://code.cannabrands.app/Cannabrands/hub/-/packages
|
||||
|
||||
## Step 1: Enable Gitea Package Registry
|
||||
|
||||
@@ -22,7 +22,7 @@ First, verify the registry is enabled on your Gitea instance:
|
||||
|
||||
1. **Check as admin**: Admin → Site Administration → Configuration
|
||||
2. **Look for**: `[packages]` section with `ENABLED = true`
|
||||
3. **Test**: Visit https://code.cannabrands.com/-/packages
|
||||
3. **Test**: Visit https://code.cannabrands.app/-/packages
|
||||
|
||||
If not enabled, ask your Gitea admin to enable it in `app.ini`:
|
||||
```ini
|
||||
@@ -44,7 +44,7 @@ Create a token for Woodpecker to authenticate with Gitea:
|
||||
|
||||
Configure Woodpecker to authenticate with your Gitea registry:
|
||||
|
||||
1. **Go to**: https://ci.cannabrands.com/repos/1/settings/secrets
|
||||
1. **Go to**: https://ci.cannabrands.app/repos/1/settings/secrets
|
||||
2. **Add secrets**:
|
||||
- Name: `gitea_username`, Value: Your Gitea username
|
||||
- Name: `gitea_token`, Value: Personal access token from Step 2
|
||||
@@ -61,8 +61,8 @@ steps:
|
||||
build-and-publish:
|
||||
image: plugins/docker
|
||||
settings:
|
||||
registry: code.cannabrands.com
|
||||
repo: code.cannabrands.com/cannabrands/hub
|
||||
registry: code.cannabrands.app
|
||||
repo: code.cannabrands.app/cannabrands/hub
|
||||
tags:
|
||||
- latest
|
||||
- ${CI_COMMIT_SHA:0:8}
|
||||
@@ -136,15 +136,15 @@ Once images are published, you can pull them on your production servers:
|
||||
|
||||
```bash
|
||||
# Login to Gitea registry
|
||||
docker login code.cannabrands.com
|
||||
docker login code.cannabrands.app
|
||||
# Username: your-gitea-username
|
||||
# Password: your-personal-access-token
|
||||
|
||||
# Pull latest image
|
||||
docker pull code.cannabrands.com/cannabrands/hub:latest
|
||||
docker pull code.cannabrands.app/cannabrands/hub:latest
|
||||
|
||||
# Or pull specific commit
|
||||
docker pull code.cannabrands.com/cannabrands/hub:bef77df8
|
||||
docker pull code.cannabrands.app/cannabrands/hub:bef77df8
|
||||
```
|
||||
|
||||
## Image Tagging Strategy
|
||||
@@ -218,8 +218,8 @@ steps:
|
||||
build-and-publish:
|
||||
image: plugins/docker
|
||||
settings:
|
||||
registry: code.cannabrands.com
|
||||
repo: code.cannabrands.com/cannabrands/hub
|
||||
registry: code.cannabrands.app
|
||||
repo: code.cannabrands.app/cannabrands/hub
|
||||
tags:
|
||||
- latest
|
||||
- ${CI_COMMIT_SHA:0:8}
|
||||
@@ -236,7 +236,7 @@ steps:
|
||||
notify-deploy:
|
||||
image: alpine:latest
|
||||
commands:
|
||||
- echo "✅ New image published: code.cannabrands.com/cannabrands/hub:${CI_COMMIT_SHA:0:8}"
|
||||
- echo "✅ New image published: code.cannabrands.app/cannabrands/hub:${CI_COMMIT_SHA:0:8}"
|
||||
- echo "Ready for deployment to production!"
|
||||
when:
|
||||
- branch: master
|
||||
@@ -271,8 +271,8 @@ services:
|
||||
- Subsequent builds will work fine
|
||||
|
||||
**Images not appearing in Gitea packages**
|
||||
- Check Gitea packages are enabled: https://code.cannabrands.com/-/packages
|
||||
- Verify registry URL is `code.cannabrands.com` (not `ci.cannabrands.com`)
|
||||
- Check Gitea packages are enabled: https://code.cannabrands.app/-/packages
|
||||
- Verify registry URL is `code.cannabrands.app` (not `ci.cannabrands.app`)
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ git push origin 2025.11.3
|
||||
|
||||
### Step 3: Wait for CI Build (2-4 minutes)
|
||||
|
||||
Watch at: `code.cannabrands.com/cannabrands/hub/pipelines`
|
||||
Watch at: `code.cannabrands.app/cannabrands/hub/pipelines`
|
||||
|
||||
CI will automatically:
|
||||
- Run tests
|
||||
@@ -113,7 +113,7 @@ git push origin master
|
||||
```bash
|
||||
# Deploy specific version
|
||||
kubectl set image deployment/cannabrands \
|
||||
app=code.cannabrands.com/cannabrands/hub:2025.11.3
|
||||
app=code.cannabrands.app/cannabrands/hub:2025.11.3
|
||||
|
||||
# Watch deployment
|
||||
kubectl rollout status deployment/cannabrands
|
||||
@@ -131,7 +131,7 @@ kubectl get pods
|
||||
```bash
|
||||
# Option 1: Rollback to previous version
|
||||
kubectl set image deployment/cannabrands \
|
||||
app=code.cannabrands.com/cannabrands/hub:2025.11.2
|
||||
app=code.cannabrands.app/cannabrands/hub:2025.11.2
|
||||
|
||||
# Option 2: Kubernetes automatic rollback
|
||||
kubectl rollout undo deployment/cannabrands
|
||||
@@ -154,7 +154,7 @@ git push origin 2025.11.4
|
||||
|
||||
# 4. Deploy when confident
|
||||
kubectl set image deployment/cannabrands \
|
||||
app=code.cannabrands.com/cannabrands/hub:2025.11.4
|
||||
app=code.cannabrands.app/cannabrands/hub:2025.11.4
|
||||
```
|
||||
|
||||
---
|
||||
@@ -170,7 +170,7 @@ master → Branch tracking
|
||||
|
||||
**Use in K3s dev/staging:**
|
||||
```yaml
|
||||
image: code.cannabrands.com/cannabrands/hub:latest-dev
|
||||
image: code.cannabrands.app/cannabrands/hub:latest-dev
|
||||
imagePullPolicy: Always
|
||||
```
|
||||
|
||||
@@ -182,7 +182,7 @@ stable → Latest production release
|
||||
|
||||
**Use in K3s production:**
|
||||
```yaml
|
||||
image: code.cannabrands.com/cannabrands/hub:2025.11.3
|
||||
image: code.cannabrands.app/cannabrands/hub:2025.11.3
|
||||
imagePullPolicy: IfNotPresent
|
||||
```
|
||||
|
||||
@@ -214,7 +214,7 @@ docker build -t cannabrands:test .
|
||||
### View CI Status
|
||||
```bash
|
||||
# Visit Woodpecker
|
||||
open https://code.cannabrands.com/cannabrands/hub/pipelines
|
||||
open https://code.cannabrands.app/cannabrands/hub/pipelines
|
||||
|
||||
# Or check latest build
|
||||
# (Visit Gitea → Repository → Pipelines)
|
||||
@@ -227,7 +227,7 @@ open https://code.cannabrands.com/cannabrands/hub/pipelines
|
||||
### CI Build Failing
|
||||
```bash
|
||||
# Check Woodpecker logs
|
||||
# Visit: code.cannabrands.com/cannabrands/hub/pipelines
|
||||
# Visit: code.cannabrands.app/cannabrands/hub/pipelines
|
||||
|
||||
# Run tests locally first
|
||||
./vendor/bin/sail artisan test
|
||||
@@ -291,6 +291,42 @@ npm run changelog
|
||||
|
||||
---
|
||||
|
||||
## CI/CD Pipeline Stages
|
||||
|
||||
The Woodpecker CI pipeline runs the following stages for every push to `develop` or `master`:
|
||||
|
||||
1. **PHP Lint** - Syntax validation
|
||||
2. **Code Style (Pint)** - Formatting check
|
||||
3. **Tests** - PHPUnit/Pest tests with `APP_ENV=testing`
|
||||
4. **Seeder Validation** - Validates seeders with `APP_ENV=development`
|
||||
5. **Docker Build** - Creates container image
|
||||
6. **Auto-Deploy** - Deploys to dev.cannabrands.app (develop branch only)
|
||||
|
||||
### Why Seeder Validation?
|
||||
|
||||
The dev environment (`dev.cannabrands.app`) runs `migrate:fresh --seed` on every K8s deployment via init container. If seeders have bugs (e.g., undefined functions, missing relationships), the deployment fails and pods crash.
|
||||
|
||||
**The Problem:**
|
||||
- Tests run with `APP_ENV=testing` which **skips DevSeeder**
|
||||
- K8s runs with `APP_ENV=development` which **runs DevSeeder**
|
||||
- Seeder bugs passed CI but crashed in K8s
|
||||
|
||||
**The Solution:**
|
||||
- Add dedicated seeder validation step with `APP_ENV=development`
|
||||
- Runs the exact same command as K8s init container
|
||||
- Catches seeder errors before deployment
|
||||
|
||||
**Time Cost:** ~20-30 seconds added to CI pipeline
|
||||
|
||||
**What It Catches:**
|
||||
- Runtime errors (e.g., `fake()` outside factory context)
|
||||
- Database constraint violations
|
||||
- Missing relationships (foreign key errors)
|
||||
- Invalid enum values
|
||||
- Seeder syntax errors
|
||||
|
||||
---
|
||||
|
||||
## Pre-Commit Checklist
|
||||
|
||||
Before committing:
|
||||
@@ -300,6 +336,7 @@ Before committing:
|
||||
|
||||
Before releasing:
|
||||
- [ ] All tests green in CI
|
||||
- [ ] **Seeder validation passed in CI**
|
||||
- [ ] Tested in dev/staging environment
|
||||
- [ ] Release notes written
|
||||
- [ ] CHANGELOG updated (auto-generated)
|
||||
@@ -325,8 +362,8 @@ Before deploying:
|
||||
- Pair with senior dev for first release
|
||||
|
||||
### CI/CD
|
||||
- Woodpecker: `code.cannabrands.com/cannabrands/hub`
|
||||
- Gitea: `code.cannabrands.com/cannabrands/hub`
|
||||
- Woodpecker: `code.cannabrands.app/cannabrands/hub`
|
||||
- Gitea: `code.cannabrands.app/cannabrands/hub`
|
||||
- K3s Dashboard: (ask devops for link)
|
||||
|
||||
---
|
||||
@@ -334,13 +371,13 @@ Before deploying:
|
||||
## Important URLs
|
||||
|
||||
**Code Repository:**
|
||||
https://code.cannabrands.com/cannabrands/hub
|
||||
https://code.cannabrands.app/cannabrands/hub
|
||||
|
||||
**CI/CD Pipeline:**
|
||||
https://code.cannabrands.com/cannabrands/hub/pipelines
|
||||
https://code.cannabrands.app/cannabrands/hub/pipelines
|
||||
|
||||
**Container Registry:**
|
||||
https://code.cannabrands.com/-/packages/container/cannabrands%2Fhub
|
||||
https://code.cannabrands.app/-/packages/container/cannabrands%2Fhub
|
||||
|
||||
**Documentation:**
|
||||
`.woodpecker/` directory in repository
|
||||
@@ -393,7 +430,7 @@ Closes #42"
|
||||
| Deploy | `kubectl set image deployment/cannabrands app=...:2025.11.1` |
|
||||
| Rollback | `kubectl set image deployment/cannabrands app=...:2025.11.0` |
|
||||
| Check version | `kubectl get deployment cannabrands -o jsonpath='{.spec.template.spec.containers[0].image}'` |
|
||||
| View builds | Visit `code.cannabrands.com/cannabrands/hub/pipelines` |
|
||||
| View builds | Visit `code.cannabrands.app/cannabrands/hub/pipelines` |
|
||||
|
||||
---
|
||||
|
||||
@@ -33,7 +33,7 @@ git push origin master
|
||||
2. Tests run (PHP lint, Pint, PHPUnit)
|
||||
3. Docker image builds (if tests pass)
|
||||
4. Tagged as: latest-dev, dev-c658193, master
|
||||
5. Pushed to code.cannabrands.com/cannabrands/hub
|
||||
5. Pushed to code.cannabrands.app/cannabrands/hub
|
||||
6. Available in K3s dev namespace (manual or auto-pull)
|
||||
```
|
||||
|
||||
@@ -47,7 +47,7 @@ git push origin master
|
||||
**Use in K3s:**
|
||||
```yaml
|
||||
# dev/staging namespace
|
||||
image: code.cannabrands.com/cannabrands/hub:latest-dev
|
||||
image: code.cannabrands.app/cannabrands/hub:latest-dev
|
||||
imagePullPolicy: Always # Always pull newest
|
||||
```
|
||||
|
||||
@@ -81,7 +81,7 @@ git push origin 2025.11.1
|
||||
**Use in K3s:**
|
||||
```yaml
|
||||
# production namespace
|
||||
image: code.cannabrands.com/cannabrands/hub:2025.11.1
|
||||
image: code.cannabrands.app/cannabrands/hub:2025.11.1
|
||||
imagePullPolicy: IfNotPresent # Pin to specific version
|
||||
```
|
||||
|
||||
@@ -212,7 +212,7 @@ git push origin master
|
||||
./vendor/bin/sail artisan test
|
||||
|
||||
# Check CI is green
|
||||
# Visit: code.cannabrands.com/cannabrands/hub/pipelines
|
||||
# Visit: code.cannabrands.app/cannabrands/hub/pipelines
|
||||
|
||||
# Test in staging/dev environment
|
||||
# Verify key workflows work
|
||||
@@ -264,12 +264,12 @@ git push origin 2025.11.3
|
||||
|
||||
```bash
|
||||
# Watch Woodpecker build
|
||||
# Visit: code.cannabrands.com/cannabrands/hub/pipelines
|
||||
# Visit: code.cannabrands.app/cannabrands/hub/pipelines
|
||||
|
||||
# Wait for success (2-4 minutes)
|
||||
# CI will build and push:
|
||||
# - code.cannabrands.com/cannabrands/hub:2025.11.3
|
||||
# - code.cannabrands.com/cannabrands/hub:stable
|
||||
# - code.cannabrands.app/cannabrands/hub:2025.11.3
|
||||
# - code.cannabrands.app/cannabrands/hub:stable
|
||||
```
|
||||
|
||||
#### 5. Deploy to Production (When Ready)
|
||||
@@ -277,7 +277,7 @@ git push origin 2025.11.3
|
||||
```bash
|
||||
# Deploy new version
|
||||
kubectl set image deployment/cannabrands \
|
||||
app=code.cannabrands.com/cannabrands/hub:2025.11.3
|
||||
app=code.cannabrands.app/cannabrands/hub:2025.11.3
|
||||
|
||||
# Watch rollout
|
||||
kubectl rollout status deployment/cannabrands
|
||||
@@ -328,11 +328,11 @@ git push origin master
|
||||
```bash
|
||||
# Option 1: Rollback to specific version
|
||||
kubectl set image deployment/cannabrands \
|
||||
app=code.cannabrands.com/cannabrands/hub:2025.11.2
|
||||
app=code.cannabrands.app/cannabrands/hub:2025.11.2
|
||||
|
||||
# Option 2: Use previous stable
|
||||
kubectl set image deployment/cannabrands \
|
||||
app=code.cannabrands.com/cannabrands/hub:stable
|
||||
app=code.cannabrands.app/cannabrands/hub:stable
|
||||
|
||||
# Note: 'stable' is updated on every release
|
||||
# So if you just deployed 2025.11.3, 'stable' points to 2025.11.3
|
||||
@@ -367,7 +367,7 @@ git push origin 2025.11.4
|
||||
|
||||
# Deploy
|
||||
kubectl set image deployment/cannabrands \
|
||||
app=code.cannabrands.com/cannabrands/hub:2025.11.4
|
||||
app=code.cannabrands.app/cannabrands/hub:2025.11.4
|
||||
```
|
||||
|
||||
---
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
**Current tagging strategy:**
|
||||
```
|
||||
code.cannabrands.com/cannabrands/hub:latest # Always changes
|
||||
code.cannabrands.com/cannabrands/hub:c658193 # Commit SHA (meaningless)
|
||||
code.cannabrands.com/cannabrands/hub:master # Branch name (changes)
|
||||
code.cannabrands.app/cannabrands/hub:latest # Always changes
|
||||
code.cannabrands.app/cannabrands/hub:c658193 # Commit SHA (meaningless)
|
||||
code.cannabrands.app/cannabrands/hub:master # Branch name (changes)
|
||||
```
|
||||
|
||||
**Issues:**
|
||||
@@ -143,8 +143,8 @@ The CI pipeline now builds images with version metadata for both development and
|
||||
build-image-dev:
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
settings:
|
||||
registry: code.cannabrands.com
|
||||
repo: code.cannabrands.com/cannabrands/hub
|
||||
registry: code.cannabrands.app
|
||||
repo: code.cannabrands.app/cannabrands/hub
|
||||
tags:
|
||||
- dev # Latest dev build
|
||||
- sha-${CI_COMMIT_SHA:0:7} # Commit SHA
|
||||
@@ -170,13 +170,13 @@ build-image-release:
|
||||
**Result:**
|
||||
```
|
||||
# Development push to master
|
||||
code.cannabrands.com/cannabrands/hub:dev
|
||||
code.cannabrands.com/cannabrands/hub:sha-c658193
|
||||
code.cannabrands.com/cannabrands/hub:master
|
||||
code.cannabrands.app/cannabrands/hub:dev
|
||||
code.cannabrands.app/cannabrands/hub:sha-c658193
|
||||
code.cannabrands.app/cannabrands/hub:master
|
||||
|
||||
# Release (git tag 2025.10.1)
|
||||
code.cannabrands.com/cannabrands/hub:2025.10.1 # Specific version
|
||||
code.cannabrands.com/cannabrands/hub:latest # Latest stable
|
||||
code.cannabrands.app/cannabrands/hub:2025.10.1 # Specific version
|
||||
code.cannabrands.app/cannabrands/hub:latest # Latest stable
|
||||
```
|
||||
|
||||
---
|
||||
@@ -243,11 +243,11 @@ git checkout c658193
|
||||
```bash
|
||||
# Option 1: Rollback to specific version (recommended)
|
||||
kubectl set image deployment/cannabrands \
|
||||
app=code.cannabrands.com/cannabrands/hub:v1.2.2
|
||||
app=code.cannabrands.app/cannabrands/hub:v1.2.2
|
||||
|
||||
# Option 2: Rollback to last stable
|
||||
kubectl set image deployment/cannabrands \
|
||||
app=code.cannabrands.com/cannabrands/hub:stable
|
||||
app=code.cannabrands.app/cannabrands/hub:stable
|
||||
|
||||
# Option 3: Kubernetes rollback (uses previous deployment)
|
||||
kubectl rollout undo deployment/cannabrands
|
||||
@@ -281,7 +281,7 @@ cat CHANGELOG.md
|
||||
|
||||
# 5. Deploy specific version
|
||||
kubectl set image deployment/cannabrands \
|
||||
app=code.cannabrands.com/cannabrands/hub:v1.2.1
|
||||
app=code.cannabrands.app/cannabrands/hub:v1.2.1
|
||||
```
|
||||
|
||||
---
|
||||
@@ -357,7 +357,7 @@ audit-deployment:
|
||||
```
|
||||
Developer → Commit to master → CI tests → Build dev image
|
||||
↓
|
||||
code.cannabrands.com/cannabrands/hub:dev-COMMIT
|
||||
code.cannabrands.app/cannabrands/hub:dev-COMMIT
|
||||
↓
|
||||
Deploy to dev/staging (optional)
|
||||
```
|
||||
@@ -486,7 +486,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: app
|
||||
image: code.cannabrands.com/cannabrands/hub:v1.2.3
|
||||
image: code.cannabrands.app/cannabrands/hub:v1.2.3
|
||||
imagePullPolicy: IfNotPresent # Don't pull if tag exists
|
||||
ports:
|
||||
- containerPort: 80
|
||||
@@ -535,7 +535,7 @@ git push origin master
|
||||
|
||||
# 5. Deploy to production (manual)
|
||||
kubectl set image deployment/cannabrands \
|
||||
app=code.cannabrands.com/cannabrands/hub:v1.3.0
|
||||
app=code.cannabrands.app/cannabrands/hub:v1.3.0
|
||||
```
|
||||
|
||||
### Emergency Rollback
|
||||
@@ -546,7 +546,7 @@ kubectl rollout undo deployment/cannabrands
|
||||
|
||||
# Or specific version
|
||||
kubectl set image deployment/cannabrands \
|
||||
app=code.cannabrands.com/cannabrands/hub:v1.2.3
|
||||
app=code.cannabrands.app/cannabrands/hub:v1.2.3
|
||||
|
||||
# Verify
|
||||
kubectl rollout status deployment/cannabrands
|
||||
410
CHANGELOG.md
410
CHANGELOG.md
@@ -3,223 +3,223 @@
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add background color to Choices.js dropdown items ([fcf3689](https://code.cannabrands.com/Cannabrands/hub/commit/fcf36893d9750b33f087f750623f7a34468a5a15))
|
||||
* add background to nested choices list container ([36a66d6](https://code.cannabrands.com/Cannabrands/hub/commit/36a66d65e2124d830cb40011c5116e28b56b463b))
|
||||
* add high-specificity selectors for dropdown background ([c7de8d5](https://code.cannabrands.com/Cannabrands/hub/commit/c7de8d5bbe002b8c65bcfe51d279c1fc16e6476a))
|
||||
* add libpq-dev for PostgreSQL extension and show build output ([e800eab](https://code.cannabrands.com/Cannabrands/hub/commit/e800eab40ea75d6057db837dbca8c26a031001a3))
|
||||
* add missing Action import in LatestNotifications widget ([3b12568](https://code.cannabrands.com/Cannabrands/hub/commit/3b1256801805e608d7475fe5bbf0efd317663852))
|
||||
* add missing compliance document methods to Business model ([0c3a935](https://code.cannabrands.com/Cannabrands/hub/commit/0c3a9350d9b74ad18e18f3125c787bf3f521ee55))
|
||||
* add null user checks to OrderNotificationService ([a81c9a0](https://code.cannabrands.com/Cannabrands/hub/commit/a81c9a09ff71f0fb126220d18003bcff77e7e2fb))
|
||||
* add overflow-visible to parent containers for dropdown menus ([dd5b7d2](https://code.cannabrands.com/Cannabrands/hub/commit/dd5b7d2b97679ea93f99df9e629e910ba6a09163))
|
||||
* add z-index and opacity to dropdown to prevent see-through ([1991941](https://code.cannabrands.com/Cannabrands/hub/commit/1991941c4d8fc1a065188352cca0de3cdef08f1f))
|
||||
* apply conditional dropdown-top to invoices table last row ([8e41203](https://code.cannabrands.com/Cannabrands/hub/commit/8e412031ea84fa350b221f90290de90ed6108fb4))
|
||||
* broken product images on order show page ([7af3403](https://code.cannabrands.com/Cannabrands/hub/commit/7af34031872e873389a61e9889fc8959ebe0d2df))
|
||||
* center empty state icon above 'No Locations Yet' text ([5041fec](https://code.cannabrands.com/Cannabrands/hub/commit/5041fece20f7523dd30414950dedd1bceeb28c20))
|
||||
* change notification actions to redirect instead of returning JSON ([16621ce](https://code.cannabrands.com/Cannabrands/hub/commit/16621ce01c51436a3cdf2244623b559477bc1fac))
|
||||
* change notification ID type from int to string for UUID support ([a53ce10](https://code.cannabrands.com/Cannabrands/hub/commit/a53ce10864bcf43798a9b10207684f4622c6e5d0))
|
||||
* connect Analytics navigation link to actual route ([592a05d](https://code.cannabrands.com/Cannabrands/hub/commit/592a05d927715bcdc6acb72173a5165857ee73b5))
|
||||
* convert business license type to dropdown in Filament admin ([95764a1](https://code.cannabrands.com/Cannabrands/hub/commit/95764a1484b459aa345488d6ea025eafd39aa7ba))
|
||||
* correct anime.js import path for ES modules ([67e1ed2](https://code.cannabrands.com/Cannabrands/hub/commit/67e1ed270838fb8d67f1890f34e24d020be85ec7))
|
||||
* correct brand colors column name from brand_colors to colors ([ba4bd33](https://code.cannabrands.com/Cannabrands/hub/commit/ba4bd33924d81561e8fdc44428f3e27186d83479))
|
||||
* correct Business model relationships and remove non-existent fields ([911865e](https://code.cannabrands.com/Cannabrands/hub/commit/911865e48fe47df45b23d4917812bacd4dc165ee))
|
||||
* correct business relationship reference in buyer invoice view (Bug [#9](https://code.cannabrands.com/Cannabrands/hub/issues/9)) ([0f3939c](https://code.cannabrands.com/Cannabrands/hub/commit/0f3939c76a04b74195727a2b7ebf34bf1216d1df))
|
||||
* correct Company-Location relationship foreign key ([5b1969c](https://code.cannabrands.com/Cannabrands/hub/commit/5b1969c0e4d00dcbf615940bb92d9e5642d22be8))
|
||||
* correct data format for ApexCharts treemaps and Cal-heatmap ([080e1c5](https://code.cannabrands.com/Cannabrands/hub/commit/080e1c5859bf1b49e01a80a0ff738bb41f3fe3b6))
|
||||
* correct database column names and currency handling in analytics ([b0c7b2d](https://code.cannabrands.com/Cannabrands/hub/commit/b0c7b2d5559199574a559ef0046f3bed9212bfb8))
|
||||
* correct database references from MySQL to PostgreSQL in README ([3a4fe0a](https://code.cannabrands.com/Cannabrands/hub/commit/3a4fe0ae5fe4c6f518a499e75427e43881b75100))
|
||||
* correct invoice-brand relationship path (Bug [#9](https://code.cannabrands.com/Cannabrands/hub/issues/9)) ([b1dcb36](https://code.cannabrands.com/Cannabrands/hub/commit/b1dcb363cda527dca0284e61ae801b969e68405f))
|
||||
* correct picking ticket complete route name (Bug [#8](https://code.cannabrands.com/Cannabrands/hub/issues/8)) ([f0596b2](https://code.cannabrands.com/Cannabrands/hub/commit/f0596b2a839c0ca8be138c0189515d29cf8643d4))
|
||||
* correct picking ticket route name in seller order view ([a41cdf1](https://code.cannabrands.com/Cannabrands/hub/commit/a41cdf17586f1d65977744d7c7286906279c7ac9)), closes [#7](https://code.cannabrands.com/Cannabrands/hub/issues/7)
|
||||
* correct Section import for Filament 4 Schema API ([26583b1](https://code.cannabrands.com/Cannabrands/hub/commit/26583b13ddd67a692970934aad882c9ec56251a6))
|
||||
* correct user_type default and invoice business_id bug ([559d705](https://code.cannabrands.com/Cannabrands/hub/commit/559d7058507480b855f4a38d3b9dcadfdfac6254))
|
||||
* force Choices.js dropdown to stay closed on page load ([9ddc883](https://code.cannabrands.com/Cannabrands/hub/commit/9ddc88343e24b8d4b7fa42df9dd0eacff2c7d4cd))
|
||||
* handle null user in invoice show view for manual invoices ([ad2d7f8](https://code.cannabrands.com/Cannabrands/hub/commit/ad2d7f8fda1d27683158e1dd4edf0c01dbdedb5a))
|
||||
* handle null user in picking ticket view ([92cbc21](https://code.cannabrands.com/Cannabrands/hub/commit/92cbc219eb2dd9c75a70976109ed74f385f8a80c))
|
||||
* handle null user in seller invoices index view ([7e1c6a0](https://code.cannabrands.com/Cannabrands/hub/commit/7e1c6a0743a413b76b09ad9164134f13a0967d26))
|
||||
* handle null user in seller order show view for manual invoices ([a772502](https://code.cannabrands.com/Cannabrands/hub/commit/a7725025f987fb666f927bcfe4afbc56188ef3ad))
|
||||
* improve dark mode text readability with white text ([9264b8b](https://code.cannabrands.com/Cannabrands/hub/commit/9264b8bf1844ab2379067e9b3a6c17b90cc4ddea)), closes [#e3f1eb](https://code.cannabrands.com/Cannabrands/hub/issues/e3f1eb) [#ffffff](https://code.cannabrands.com/Cannabrands/hub/issues/ffffff)
|
||||
* improve UX when no contacts exist for invoice customer ([5c7d9e6](https://code.cannabrands.com/Cannabrands/hub/commit/5c7d9e6c18e9d82f08f3f789200d7ec4ac04cf67))
|
||||
* install PHP intl extension and dependencies for Filament ([0ef61eb](https://code.cannabrands.com/Cannabrands/hub/commit/0ef61eb4d10dc9af2c7228cbb8d7a8c216d0fe72))
|
||||
* make Choices.js vendor dropdown compatible with DaisyUI theme ([a032d98](https://code.cannabrands.com/Cannabrands/hub/commit/a032d98ab5e5d55614a0515deadca00663f6ca95))
|
||||
* make invoice creation page header theme-aware for dark mode ([5940ed1](https://code.cannabrands.com/Cannabrands/hub/commit/5940ed189f4be97b796395817c1d64338a53264f))
|
||||
* make locations page header theme-aware and add icon ([91fcf1e](https://code.cannabrands.com/Cannabrands/hub/commit/91fcf1e401a96d1f1fea6e309ddd38d441df5818))
|
||||
* make vendor dropdown reactive to dark mode toggle ([4b62fab](https://code.cannabrands.com/Cannabrands/hub/commit/4b62fab96f2589fa082a15be8fd4650d4dbb7fc1))
|
||||
* move postgres to services section and add Composer caching ([bef77df](https://code.cannabrands.com/Cannabrands/hub/commit/bef77df1f6a0d35f3ed170b37f41d3c022106b68))
|
||||
* only show 'awaiting buyer approval' after invoice is sent ([ea6d2ef](https://code.cannabrands.com/Cannabrands/hub/commit/ea6d2ef2d90feabead3bae5ad06a52198180b3dd))
|
||||
* prevent Choices.js dropdown auto-opening and clean up single-select display ([36b7e5e](https://code.cannabrands.com/Cannabrands/hub/commit/36b7e5e3d63c80645a732123b4b2a696224e89b8))
|
||||
* prevent dropdown menu clipping in table rows ([b831dd4](https://code.cannabrands.com/Cannabrands/hub/commit/b831dd4eb5a263ae8aa33132485fd4ced50814da))
|
||||
* remove [@apply](https://code.cannabrands.com/apply) directives causing invalid CSS variables ([6409436](https://code.cannabrands.com/Cannabrands/hub/commit/6409436f339c8fd329d73dccf1ed2775bcbad5ad))
|
||||
* remove Alpine x-for from vendor select to fix scope error ([5dc5379](https://code.cannabrands.com/Cannabrands/hub/commit/5dc5379f7023f95211116f723bc256883bc1fcdb))
|
||||
* remove Alpine.js code and restore working toggle implementation ([2a8f072](https://code.cannabrands.com/Cannabrands/hub/commit/2a8f07200f3de3eea61fe84f712ea0c01bdc2760))
|
||||
* remove Alpine.js code causing text rendering issues ([4e1706d](https://code.cannabrands.com/Cannabrands/hub/commit/4e1706dae032dc1acb17845494d3a2a8c098145b))
|
||||
* remove Canna brand - now 12 brands total ([d228dd1](https://code.cannabrands.com/Cannabrands/hub/commit/d228dd1ca75a995e6fb7b8f4e807e9db34a74c45))
|
||||
* remove Cannabrands as brand - it's a company only ([59db949](https://code.cannabrands.com/Cannabrands/hub/commit/59db949ca0210f518b752c745841183b8c8321aa))
|
||||
* remove HtmlString label causing text overflow on toggles ([eed5e57](https://code.cannabrands.com/Cannabrands/hub/commit/eed5e570b930c3e9d4d91075b7608d794ea4a7b1))
|
||||
* remove images column conflict with images() relationship ([79dbb2b](https://code.cannabrands.com/Cannabrands/hub/commit/79dbb2b370fae2ce2eade352da57ab77e487fcf9)), closes [#5](https://code.cannabrands.com/Cannabrands/hub/issues/5)
|
||||
* remove inappropriate email availability checking from login form ([56b9a32](https://code.cannabrands.com/Cannabrands/hub/commit/56b9a32d1e5e8f3b0bb00b2133e44344e0d7d392))
|
||||
* remove invalid Tab::description() method call ([fac1f45](https://code.cannabrands.com/Cannabrands/hub/commit/fac1f459e478ca922d524b04ba79292fce98e7ad))
|
||||
* remove non-existent locations relationship from change fulfillment modal ([8174aad](https://code.cannabrands.com/Cannabrands/hub/commit/8174aadd273360a9e62a83d58070db2b320fa9f9))
|
||||
* replace $order->company with $order->business in buyer order view ([7751604](https://code.cannabrands.com/Cannabrands/hub/commit/775160457e1e708f929afa7f5beedd6cfccfe603)), closes [#6](https://code.cannabrands.com/Cannabrands/hub/issues/6)
|
||||
* resolve ambiguous column references in analytics queries ([c960754](https://code.cannabrands.com/Cannabrands/hub/commit/c96075473c99d072b6c971bcf26c3261f8cb3f17))
|
||||
* resolve double-encoding issue with vendor names containing apostrophes ([6b37783](https://code.cannabrands.com/Cannabrands/hub/commit/6b377837f77e4e6a3a4a3cee9de962c2a8fbdca3))
|
||||
* resolve JavaScript errors in analytics charts ([608c14a](https://code.cannabrands.com/Cannabrands/hub/commit/608c14a4ffa9206bee4b2908508153a23a2f5dbb))
|
||||
* restructure BrandResource to correct namespace and add navigation ([a6fe91a](https://code.cannabrands.com/Cannabrands/hub/commit/a6fe91a79ec10ca7fb15c70fe138e1915f27f2b1))
|
||||
* simplify notification widget by removing table actions ([059e220](https://code.cannabrands.com/Cannabrands/hub/commit/059e22085fee134a81e5279882493c299f8c534a))
|
||||
* target active state for Choices.js dropdown background ([712ce5b](https://code.cannabrands.com/Cannabrands/hub/commit/712ce5b25f35c4a7e8be891639ec914338c9a302))
|
||||
* update app layout theme and simplify DaisyUI theme config ([4c38493](https://code.cannabrands.com/Cannabrands/hub/commit/4c384938648a262aab3045e54239a775c2f45a6f))
|
||||
* update correct field when applying picked_qty changes ([d6d4ef1](https://code.cannabrands.com/Cannabrands/hub/commit/d6d4ef181c286d4f8d545c15c67cce968747aba2))
|
||||
* update notification view to use correct layout and styling ([dc5627e](https://code.cannabrands.com/Cannabrands/hub/commit/dc5627edc6ccf9cc31db2f8fbc90faae39fdb913))
|
||||
* update tests for new user model structure and disable Vite in testing ([c165bf9](https://code.cannabrands.com/Cannabrands/hub/commit/c165bf91f5f11615d3a458420ca791fcea59d125))
|
||||
* update to anime.js v4 API syntax ([cd80da1](https://code.cannabrands.com/Cannabrands/hub/commit/cd80da1cbd51ab95f45a33215c2e92cefe092f5b))
|
||||
* update UserApprovalService and phpunit config for new schema ([a617272](https://code.cannabrands.com/Cannabrands/hub/commit/a61727253109ae7dd57563d5ff4efb30aee64cba))
|
||||
* upgrade CI to PHP 8.3 to match composer.lock requirements ([8bddf54](https://code.cannabrands.com/Cannabrands/hub/commit/8bddf543041bce0877e19559fef0a389dc4a4b89))
|
||||
* use [@apply](https://code.cannabrands.com/apply) with DaisyUI utilities for Choices.js theming ([7aeb56d](https://code.cannabrands.com/Cannabrands/hub/commit/7aeb56d90e472f1f99fa1b9da542b5f1f4befb56))
|
||||
* use company as location (multi-location not yet implemented) ([6ddfab4](https://code.cannabrands.com/Cannabrands/hub/commit/6ddfab4bfa4fd03d315aa74139489956e61db475))
|
||||
* use computed primary color for chart gradient ([dd46a92](https://code.cannabrands.com/Cannabrands/hub/commit/dd46a92a3e96751688d51135d75f50f080aa3c4d)), closes [#3b82f6](https://code.cannabrands.com/Cannabrands/hub/issues/3b82f6)
|
||||
* use correct anime.js v4 two-argument API ([edd488d](https://code.cannabrands.com/Cannabrands/hub/commit/edd488d1aae658b2f25be852eedff0eb6620a93c))
|
||||
* use DaisyUI CSS variables with !important for Choices.js ([9a42e25](https://code.cannabrands.com/Cannabrands/hub/commit/9a42e2524caaef6de9bdc4b8012661ee5c6cbfa4))
|
||||
* use dropdown-top for table action menus (DaisyUI pattern) ([9a7ad53](https://code.cannabrands.com/Cannabrands/hub/commit/9a7ad531c86eaf56fb94282716ae30e30e8f4736))
|
||||
* use hardcoded theme colors instead of CSS variables ([57328fa](https://code.cannabrands.com/Cannabrands/hub/commit/57328fa4e3d67e4b1059b141c3b39ef78a58e652)), closes [#ffffff](https://code.cannabrands.com/Cannabrands/hub/issues/ffffff) [#dcdee0](https://code.cannabrands.com/Cannabrands/hub/issues/dcdee0) [#1e2328](https://code.cannabrands.com/Cannabrands/hub/issues/1e2328) [#181c20](https://code.cannabrands.com/Cannabrands/hub/issues/181c20) [#2c3034](https://code.cannabrands.com/Cannabrands/hub/issues/2c3034) [#f0f4f8](https://code.cannabrands.com/Cannabrands/hub/issues/f0f4f8)
|
||||
* use HtmlString for dotted underline tooltip on primary toggles ([b85e0a2](https://code.cannabrands.com/Cannabrands/hub/commit/b85e0a2a0141d30edc10d6060463cb2cbb757219))
|
||||
* use inline-flex for locations header to prevent icon floating ([e0e4094](https://code.cannabrands.com/Cannabrands/hub/commit/e0e4094dcfc97c3da62e2b30afa6139ecf4a1481))
|
||||
* use line_total column instead of non-existent subtotal in analytics ([0d5904c](https://code.cannabrands.com/Cannabrands/hub/commit/0d5904c816ac742bf9b48dbbf1c63f1927685a95))
|
||||
* use map syntax for environment variables in Woodpecker CI ([53d6f44](https://code.cannabrands.com/Cannabrands/hub/commit/53d6f443c256c720864298905d4c81952324209f))
|
||||
* use named export for anime.js v4 ([d6cf85c](https://code.cannabrands.com/Cannabrands/hub/commit/d6cf85c0532e435acd77415cea6a105217c3a86a))
|
||||
* use server-side Blade conditionals for empty data handling ([f31289b](https://code.cannabrands.com/Cannabrands/hub/commit/f31289be74e10395ec08214c4ed2bca15618ccd7))
|
||||
* use Storage::url() for product images in invoice show view ([0a0dbd1](https://code.cannabrands.com/Cannabrands/hub/commit/0a0dbd16415b371badab3f5ea38eb4f242867bb3))
|
||||
* use Storage::url() for product images in picking ticket view ([2a3e4f9](https://code.cannabrands.com/Cannabrands/hub/commit/2a3e4f988b458ab5f373458485c76fcab3c44c50))
|
||||
* use theme-aware colors for page headers and icons ([e68df40](https://code.cannabrands.com/Cannabrands/hub/commit/e68df403e93ac27db1a1a0292e977493a4f814f9))
|
||||
* use theme-aware colors in invoices stats cards and table ([102a2ed](https://code.cannabrands.com/Cannabrands/hub/commit/102a2ed927f63ce9ffbc41d805ef27113cf2560b))
|
||||
* add background color to Choices.js dropdown items ([fcf3689](https://code.cannabrands.app/Cannabrands/hub/commit/fcf36893d9750b33f087f750623f7a34468a5a15))
|
||||
* add background to nested choices list container ([36a66d6](https://code.cannabrands.app/Cannabrands/hub/commit/36a66d65e2124d830cb40011c5116e28b56b463b))
|
||||
* add high-specificity selectors for dropdown background ([c7de8d5](https://code.cannabrands.app/Cannabrands/hub/commit/c7de8d5bbe002b8c65bcfe51d279c1fc16e6476a))
|
||||
* add libpq-dev for PostgreSQL extension and show build output ([e800eab](https://code.cannabrands.app/Cannabrands/hub/commit/e800eab40ea75d6057db837dbca8c26a031001a3))
|
||||
* add missing Action import in LatestNotifications widget ([3b12568](https://code.cannabrands.app/Cannabrands/hub/commit/3b1256801805e608d7475fe5bbf0efd317663852))
|
||||
* add missing compliance document methods to Business model ([0c3a935](https://code.cannabrands.app/Cannabrands/hub/commit/0c3a9350d9b74ad18e18f3125c787bf3f521ee55))
|
||||
* add null user checks to OrderNotificationService ([a81c9a0](https://code.cannabrands.app/Cannabrands/hub/commit/a81c9a09ff71f0fb126220d18003bcff77e7e2fb))
|
||||
* add overflow-visible to parent containers for dropdown menus ([dd5b7d2](https://code.cannabrands.app/Cannabrands/hub/commit/dd5b7d2b97679ea93f99df9e629e910ba6a09163))
|
||||
* add z-index and opacity to dropdown to prevent see-through ([1991941](https://code.cannabrands.app/Cannabrands/hub/commit/1991941c4d8fc1a065188352cca0de3cdef08f1f))
|
||||
* apply conditional dropdown-top to invoices table last row ([8e41203](https://code.cannabrands.app/Cannabrands/hub/commit/8e412031ea84fa350b221f90290de90ed6108fb4))
|
||||
* broken product images on order show page ([7af3403](https://code.cannabrands.app/Cannabrands/hub/commit/7af34031872e873389a61e9889fc8959ebe0d2df))
|
||||
* center empty state icon above 'No Locations Yet' text ([5041fec](https://code.cannabrands.app/Cannabrands/hub/commit/5041fece20f7523dd30414950dedd1bceeb28c20))
|
||||
* change notification actions to redirect instead of returning JSON ([16621ce](https://code.cannabrands.app/Cannabrands/hub/commit/16621ce01c51436a3cdf2244623b559477bc1fac))
|
||||
* change notification ID type from int to string for UUID support ([a53ce10](https://code.cannabrands.app/Cannabrands/hub/commit/a53ce10864bcf43798a9b10207684f4622c6e5d0))
|
||||
* connect Analytics navigation link to actual route ([592a05d](https://code.cannabrands.app/Cannabrands/hub/commit/592a05d927715bcdc6acb72173a5165857ee73b5))
|
||||
* convert business license type to dropdown in Filament admin ([95764a1](https://code.cannabrands.app/Cannabrands/hub/commit/95764a1484b459aa345488d6ea025eafd39aa7ba))
|
||||
* correct anime.js import path for ES modules ([67e1ed2](https://code.cannabrands.app/Cannabrands/hub/commit/67e1ed270838fb8d67f1890f34e24d020be85ec7))
|
||||
* correct brand colors column name from brand_colors to colors ([ba4bd33](https://code.cannabrands.app/Cannabrands/hub/commit/ba4bd33924d81561e8fdc44428f3e27186d83479))
|
||||
* correct Business model relationships and remove non-existent fields ([911865e](https://code.cannabrands.app/Cannabrands/hub/commit/911865e48fe47df45b23d4917812bacd4dc165ee))
|
||||
* correct business relationship reference in buyer invoice view (Bug [#9](https://code.cannabrands.app/Cannabrands/hub/issues/9)) ([0f3939c](https://code.cannabrands.app/Cannabrands/hub/commit/0f3939c76a04b74195727a2b7ebf34bf1216d1df))
|
||||
* correct Company-Location relationship foreign key ([5b1969c](https://code.cannabrands.app/Cannabrands/hub/commit/5b1969c0e4d00dcbf615940bb92d9e5642d22be8))
|
||||
* correct data format for ApexCharts treemaps and Cal-heatmap ([080e1c5](https://code.cannabrands.app/Cannabrands/hub/commit/080e1c5859bf1b49e01a80a0ff738bb41f3fe3b6))
|
||||
* correct database column names and currency handling in analytics ([b0c7b2d](https://code.cannabrands.app/Cannabrands/hub/commit/b0c7b2d5559199574a559ef0046f3bed9212bfb8))
|
||||
* correct database references from MySQL to PostgreSQL in README ([3a4fe0a](https://code.cannabrands.app/Cannabrands/hub/commit/3a4fe0ae5fe4c6f518a499e75427e43881b75100))
|
||||
* correct invoice-brand relationship path (Bug [#9](https://code.cannabrands.app/Cannabrands/hub/issues/9)) ([b1dcb36](https://code.cannabrands.app/Cannabrands/hub/commit/b1dcb363cda527dca0284e61ae801b969e68405f))
|
||||
* correct picking ticket complete route name (Bug [#8](https://code.cannabrands.app/Cannabrands/hub/issues/8)) ([f0596b2](https://code.cannabrands.app/Cannabrands/hub/commit/f0596b2a839c0ca8be138c0189515d29cf8643d4))
|
||||
* correct picking ticket route name in seller order view ([a41cdf1](https://code.cannabrands.app/Cannabrands/hub/commit/a41cdf17586f1d65977744d7c7286906279c7ac9)), closes [#7](https://code.cannabrands.app/Cannabrands/hub/issues/7)
|
||||
* correct Section import for Filament 4 Schema API ([26583b1](https://code.cannabrands.app/Cannabrands/hub/commit/26583b13ddd67a692970934aad882c9ec56251a6))
|
||||
* correct user_type default and invoice business_id bug ([559d705](https://code.cannabrands.app/Cannabrands/hub/commit/559d7058507480b855f4a38d3b9dcadfdfac6254))
|
||||
* force Choices.js dropdown to stay closed on page load ([9ddc883](https://code.cannabrands.app/Cannabrands/hub/commit/9ddc88343e24b8d4b7fa42df9dd0eacff2c7d4cd))
|
||||
* handle null user in invoice show view for manual invoices ([ad2d7f8](https://code.cannabrands.app/Cannabrands/hub/commit/ad2d7f8fda1d27683158e1dd4edf0c01dbdedb5a))
|
||||
* handle null user in picking ticket view ([92cbc21](https://code.cannabrands.app/Cannabrands/hub/commit/92cbc219eb2dd9c75a70976109ed74f385f8a80c))
|
||||
* handle null user in seller invoices index view ([7e1c6a0](https://code.cannabrands.app/Cannabrands/hub/commit/7e1c6a0743a413b76b09ad9164134f13a0967d26))
|
||||
* handle null user in seller order show view for manual invoices ([a772502](https://code.cannabrands.app/Cannabrands/hub/commit/a7725025f987fb666f927bcfe4afbc56188ef3ad))
|
||||
* improve dark mode text readability with white text ([9264b8b](https://code.cannabrands.app/Cannabrands/hub/commit/9264b8bf1844ab2379067e9b3a6c17b90cc4ddea)), closes [#e3f1eb](https://code.cannabrands.app/Cannabrands/hub/issues/e3f1eb) [#ffffff](https://code.cannabrands.app/Cannabrands/hub/issues/ffffff)
|
||||
* improve UX when no contacts exist for invoice customer ([5c7d9e6](https://code.cannabrands.app/Cannabrands/hub/commit/5c7d9e6c18e9d82f08f3f789200d7ec4ac04cf67))
|
||||
* install PHP intl extension and dependencies for Filament ([0ef61eb](https://code.cannabrands.app/Cannabrands/hub/commit/0ef61eb4d10dc9af2c7228cbb8d7a8c216d0fe72))
|
||||
* make Choices.js vendor dropdown compatible with DaisyUI theme ([a032d98](https://code.cannabrands.app/Cannabrands/hub/commit/a032d98ab5e5d55614a0515deadca00663f6ca95))
|
||||
* make invoice creation page header theme-aware for dark mode ([5940ed1](https://code.cannabrands.app/Cannabrands/hub/commit/5940ed189f4be97b796395817c1d64338a53264f))
|
||||
* make locations page header theme-aware and add icon ([91fcf1e](https://code.cannabrands.app/Cannabrands/hub/commit/91fcf1e401a96d1f1fea6e309ddd38d441df5818))
|
||||
* make vendor dropdown reactive to dark mode toggle ([4b62fab](https://code.cannabrands.app/Cannabrands/hub/commit/4b62fab96f2589fa082a15be8fd4650d4dbb7fc1))
|
||||
* move postgres to services section and add Composer caching ([bef77df](https://code.cannabrands.app/Cannabrands/hub/commit/bef77df1f6a0d35f3ed170b37f41d3c022106b68))
|
||||
* only show 'awaiting buyer approval' after invoice is sent ([ea6d2ef](https://code.cannabrands.app/Cannabrands/hub/commit/ea6d2ef2d90feabead3bae5ad06a52198180b3dd))
|
||||
* prevent Choices.js dropdown auto-opening and clean up single-select display ([36b7e5e](https://code.cannabrands.app/Cannabrands/hub/commit/36b7e5e3d63c80645a732123b4b2a696224e89b8))
|
||||
* prevent dropdown menu clipping in table rows ([b831dd4](https://code.cannabrands.app/Cannabrands/hub/commit/b831dd4eb5a263ae8aa33132485fd4ced50814da))
|
||||
* remove [@apply](https://code.cannabrands.app/apply) directives causing invalid CSS variables ([6409436](https://code.cannabrands.app/Cannabrands/hub/commit/6409436f339c8fd329d73dccf1ed2775bcbad5ad))
|
||||
* remove Alpine x-for from vendor select to fix scope error ([5dc5379](https://code.cannabrands.app/Cannabrands/hub/commit/5dc5379f7023f95211116f723bc256883bc1fcdb))
|
||||
* remove Alpine.js code and restore working toggle implementation ([2a8f072](https://code.cannabrands.app/Cannabrands/hub/commit/2a8f07200f3de3eea61fe84f712ea0c01bdc2760))
|
||||
* remove Alpine.js code causing text rendering issues ([4e1706d](https://code.cannabrands.app/Cannabrands/hub/commit/4e1706dae032dc1acb17845494d3a2a8c098145b))
|
||||
* remove Canna brand - now 12 brands total ([d228dd1](https://code.cannabrands.app/Cannabrands/hub/commit/d228dd1ca75a995e6fb7b8f4e807e9db34a74c45))
|
||||
* remove Cannabrands as brand - it's a company only ([59db949](https://code.cannabrands.app/Cannabrands/hub/commit/59db949ca0210f518b752c745841183b8c8321aa))
|
||||
* remove HtmlString label causing text overflow on toggles ([eed5e57](https://code.cannabrands.app/Cannabrands/hub/commit/eed5e570b930c3e9d4d91075b7608d794ea4a7b1))
|
||||
* remove images column conflict with images() relationship ([79dbb2b](https://code.cannabrands.app/Cannabrands/hub/commit/79dbb2b370fae2ce2eade352da57ab77e487fcf9)), closes [#5](https://code.cannabrands.app/Cannabrands/hub/issues/5)
|
||||
* remove inappropriate email availability checking from login form ([56b9a32](https://code.cannabrands.app/Cannabrands/hub/commit/56b9a32d1e5e8f3b0bb00b2133e44344e0d7d392))
|
||||
* remove invalid Tab::description() method call ([fac1f45](https://code.cannabrands.app/Cannabrands/hub/commit/fac1f459e478ca922d524b04ba79292fce98e7ad))
|
||||
* remove non-existent locations relationship from change fulfillment modal ([8174aad](https://code.cannabrands.app/Cannabrands/hub/commit/8174aadd273360a9e62a83d58070db2b320fa9f9))
|
||||
* replace $order->company with $order->business in buyer order view ([7751604](https://code.cannabrands.app/Cannabrands/hub/commit/775160457e1e708f929afa7f5beedd6cfccfe603)), closes [#6](https://code.cannabrands.app/Cannabrands/hub/issues/6)
|
||||
* resolve ambiguous column references in analytics queries ([c960754](https://code.cannabrands.app/Cannabrands/hub/commit/c96075473c99d072b6c971bcf26c3261f8cb3f17))
|
||||
* resolve double-encoding issue with vendor names containing apostrophes ([6b37783](https://code.cannabrands.app/Cannabrands/hub/commit/6b377837f77e4e6a3a4a3cee9de962c2a8fbdca3))
|
||||
* resolve JavaScript errors in analytics charts ([608c14a](https://code.cannabrands.app/Cannabrands/hub/commit/608c14a4ffa9206bee4b2908508153a23a2f5dbb))
|
||||
* restructure BrandResource to correct namespace and add navigation ([a6fe91a](https://code.cannabrands.app/Cannabrands/hub/commit/a6fe91a79ec10ca7fb15c70fe138e1915f27f2b1))
|
||||
* simplify notification widget by removing table actions ([059e220](https://code.cannabrands.app/Cannabrands/hub/commit/059e22085fee134a81e5279882493c299f8c534a))
|
||||
* target active state for Choices.js dropdown background ([712ce5b](https://code.cannabrands.app/Cannabrands/hub/commit/712ce5b25f35c4a7e8be891639ec914338c9a302))
|
||||
* update app layout theme and simplify DaisyUI theme config ([4c38493](https://code.cannabrands.app/Cannabrands/hub/commit/4c384938648a262aab3045e54239a775c2f45a6f))
|
||||
* update correct field when applying picked_qty changes ([d6d4ef1](https://code.cannabrands.app/Cannabrands/hub/commit/d6d4ef181c286d4f8d545c15c67cce968747aba2))
|
||||
* update notification view to use correct layout and styling ([dc5627e](https://code.cannabrands.app/Cannabrands/hub/commit/dc5627edc6ccf9cc31db2f8fbc90faae39fdb913))
|
||||
* update tests for new user model structure and disable Vite in testing ([c165bf9](https://code.cannabrands.app/Cannabrands/hub/commit/c165bf91f5f11615d3a458420ca791fcea59d125))
|
||||
* update to anime.js v4 API syntax ([cd80da1](https://code.cannabrands.app/Cannabrands/hub/commit/cd80da1cbd51ab95f45a33215c2e92cefe092f5b))
|
||||
* update UserApprovalService and phpunit config for new schema ([a617272](https://code.cannabrands.app/Cannabrands/hub/commit/a61727253109ae7dd57563d5ff4efb30aee64cba))
|
||||
* upgrade CI to PHP 8.3 to match composer.lock requirements ([8bddf54](https://code.cannabrands.app/Cannabrands/hub/commit/8bddf543041bce0877e19559fef0a389dc4a4b89))
|
||||
* use [@apply](https://code.cannabrands.app/apply) with DaisyUI utilities for Choices.js theming ([7aeb56d](https://code.cannabrands.app/Cannabrands/hub/commit/7aeb56d90e472f1f99fa1b9da542b5f1f4befb56))
|
||||
* use company as location (multi-location not yet implemented) ([6ddfab4](https://code.cannabrands.app/Cannabrands/hub/commit/6ddfab4bfa4fd03d315aa74139489956e61db475))
|
||||
* use computed primary color for chart gradient ([dd46a92](https://code.cannabrands.app/Cannabrands/hub/commit/dd46a92a3e96751688d51135d75f50f080aa3c4d)), closes [#3b82f6](https://code.cannabrands.app/Cannabrands/hub/issues/3b82f6)
|
||||
* use correct anime.js v4 two-argument API ([edd488d](https://code.cannabrands.app/Cannabrands/hub/commit/edd488d1aae658b2f25be852eedff0eb6620a93c))
|
||||
* use DaisyUI CSS variables with !important for Choices.js ([9a42e25](https://code.cannabrands.app/Cannabrands/hub/commit/9a42e2524caaef6de9bdc4b8012661ee5c6cbfa4))
|
||||
* use dropdown-top for table action menus (DaisyUI pattern) ([9a7ad53](https://code.cannabrands.app/Cannabrands/hub/commit/9a7ad531c86eaf56fb94282716ae30e30e8f4736))
|
||||
* use hardcoded theme colors instead of CSS variables ([57328fa](https://code.cannabrands.app/Cannabrands/hub/commit/57328fa4e3d67e4b1059b141c3b39ef78a58e652)), closes [#ffffff](https://code.cannabrands.app/Cannabrands/hub/issues/ffffff) [#dcdee0](https://code.cannabrands.app/Cannabrands/hub/issues/dcdee0) [#1e2328](https://code.cannabrands.app/Cannabrands/hub/issues/1e2328) [#181c20](https://code.cannabrands.app/Cannabrands/hub/issues/181c20) [#2c3034](https://code.cannabrands.app/Cannabrands/hub/issues/2c3034) [#f0f4f8](https://code.cannabrands.app/Cannabrands/hub/issues/f0f4f8)
|
||||
* use HtmlString for dotted underline tooltip on primary toggles ([b85e0a2](https://code.cannabrands.app/Cannabrands/hub/commit/b85e0a2a0141d30edc10d6060463cb2cbb757219))
|
||||
* use inline-flex for locations header to prevent icon floating ([e0e4094](https://code.cannabrands.app/Cannabrands/hub/commit/e0e4094dcfc97c3da62e2b30afa6139ecf4a1481))
|
||||
* use line_total column instead of non-existent subtotal in analytics ([0d5904c](https://code.cannabrands.app/Cannabrands/hub/commit/0d5904c816ac742bf9b48dbbf1c63f1927685a95))
|
||||
* use map syntax for environment variables in Woodpecker CI ([53d6f44](https://code.cannabrands.app/Cannabrands/hub/commit/53d6f443c256c720864298905d4c81952324209f))
|
||||
* use named export for anime.js v4 ([d6cf85c](https://code.cannabrands.app/Cannabrands/hub/commit/d6cf85c0532e435acd77415cea6a105217c3a86a))
|
||||
* use server-side Blade conditionals for empty data handling ([f31289b](https://code.cannabrands.app/Cannabrands/hub/commit/f31289be74e10395ec08214c4ed2bca15618ccd7))
|
||||
* use Storage::url() for product images in invoice show view ([0a0dbd1](https://code.cannabrands.app/Cannabrands/hub/commit/0a0dbd16415b371badab3f5ea38eb4f242867bb3))
|
||||
* use Storage::url() for product images in picking ticket view ([2a3e4f9](https://code.cannabrands.app/Cannabrands/hub/commit/2a3e4f988b458ab5f373458485c76fcab3c44c50))
|
||||
* use theme-aware colors for page headers and icons ([e68df40](https://code.cannabrands.app/Cannabrands/hub/commit/e68df403e93ac27db1a1a0292e977493a4f814f9))
|
||||
* use theme-aware colors in invoices stats cards and table ([102a2ed](https://code.cannabrands.app/Cannabrands/hub/commit/102a2ed927f63ce9ffbc41d805ef27113cf2560b))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add AdminPanelProvider and dashboard_url helper function ([8628e44](https://code.cannabrands.com/Cannabrands/hub/commit/8628e447ce4f2eaaff5eed26cd973a7549038108))
|
||||
* add ApexCharts and anime.js for dashboard enhancements ([7fca649](https://code.cannabrands.com/Cannabrands/hub/commit/7fca649b37827f2d860e1b692438f2e2e1057e66))
|
||||
* add Arizona cannabis compliance info to invoice PDF ([25cc2e7](https://code.cannabrands.com/Cannabrands/hub/commit/25cc2e70f6d0cc52fa9e2aeb0ccf59d2651d4f03))
|
||||
* add batch selection UI to marketplace and cart ([1ccef51](https://code.cannabrands.com/Cannabrands/hub/commit/1ccef51cbb98e94248b2ab01f5818416e52380f0))
|
||||
* add batch tracking and MSRP support to invoicing ([f2cadb2](https://code.cannabrands.com/Cannabrands/hub/commit/f2cadb2d9336294250006e829a062d15de34a30a))
|
||||
* add BOM (Bill of Materials) management system ([f1d9684](https://code.cannabrands.com/Cannabrands/hub/commit/f1d96844c3e520365d84abcbe6befb5b0310164c))
|
||||
* add buyer UI for pickup selection and management ([57c9407](https://code.cannabrands.com/Cannabrands/hub/commit/57c940735aac92737b4b0cc13d08f456f02e6c6a))
|
||||
* add configurable tax rates for Arizona B2B cannabis compliance ([dd60485](https://code.cannabrands.com/Cannabrands/hub/commit/dd60485206fc8ae10802bb186d25b4f6b24802c2))
|
||||
* add data migration commands for legacy system import ([cf06bcb](https://code.cannabrands.com/Cannabrands/hub/commit/cf06bcb4d858ea68b95e4ef6a22b9548d1d60df5))
|
||||
* add database migrations for batch tracking system ([4f53fde](https://code.cannabrands.com/Cannabrands/hub/commit/4f53fde8b2897486fe0163711b79cca55a9bd3bf))
|
||||
* add Docker image building to CI pipeline + deployment docs ([c658193](https://code.cannabrands.com/Cannabrands/hub/commit/c658193909ed9f54387106a2b8eefa10b375a82d))
|
||||
* add drag-and-drop reordering to BOM components ([83a9f71](https://code.cannabrands.com/Cannabrands/hub/commit/83a9f713a21355626b360e96431a386d86fc0f07))
|
||||
* add Filament admin interface for batch management ([e002322](https://code.cannabrands.com/Cannabrands/hub/commit/e0023222ea89ad03d28c7d6c2358325c786133a8))
|
||||
* add finalize & send functionality for manual invoices ([05a0e74](https://code.cannabrands.com/Cannabrands/hub/commit/05a0e74c66599b12566d415ef3ca023c43b51e1a))
|
||||
* add gitlab ci starter ([4c3185c](https://code.cannabrands.com/Cannabrands/hub/commit/4c3185ca0a00fdf22f56cc528227bd9ea475bba1))
|
||||
* add license field validation and storage to location controller ([d77a138](https://code.cannabrands.com/Cannabrands/hub/commit/d77a138985b412655ebff78d984d6cb264def93d))
|
||||
* add license_type field and clean up Business model ([3b4da67](https://code.cannabrands.com/Cannabrands/hub/commit/3b4da67c6d063c6e1c653d26f4ee3849cc110863))
|
||||
* add light/dark mode support to ApexCharts ([c2dbf28](https://code.cannabrands.com/Cannabrands/hub/commit/c2dbf284609cf30a73b364cb098f08414fa54255))
|
||||
* add light/dark theme toggle with sun/moon icons ([5bf5b7c](https://code.cannabrands.com/Cannabrands/hub/commit/5bf5b7ced9d04c9c4c2ee2eadd3f93da2dbab609))
|
||||
* add location and contact selection to invoice creation form ([e5f2dda](https://code.cannabrands.com/Cannabrands/hub/commit/e5f2dda7f3b44bbd0ce4e64820936d91c3850adb))
|
||||
* add location license number to invoice PDF ([f6dda38](https://code.cannabrands.com/Cannabrands/hub/commit/f6dda38180431602b58e56113e2cf49ef26f3f3a))
|
||||
* add Makefiles ([5d3c5d5](https://code.cannabrands.com/Cannabrands/hub/commit/5d3c5d5fe93048c4b5227913af1a0b794fe00531))
|
||||
* add marketplace controllers and documentation for buyer/seller structure ([eb6db6d](https://code.cannabrands.com/Cannabrands/hub/commit/eb6db6dbdced3adede2fc7391e6d7ec5d94b6685))
|
||||
* Add multi-step signup onboarding process ([65c6149](https://code.cannabrands.com/Cannabrands/hub/commit/65c6149e3a68215d17c0c773531bb888f9adffac))
|
||||
* Add new blade component files for sidebar and layout components ([2f88152](https://code.cannabrands.com/Cannabrands/hub/commit/2f8815235c989f180fc78d84ccdf805255db4764))
|
||||
* Add new blade components for Filament panels ([d6ac894](https://code.cannabrands.com/Cannabrands/hub/commit/d6ac8941b804fbdc4c933c4c802529e53e8bc5b0))
|
||||
* Add new view files for authentication and dashboard pages ([80f008c](https://code.cannabrands.com/Cannabrands/hub/commit/80f008c2534dff04ceba04cd5150d10b6da3a589))
|
||||
* add notification infrastructure and policy documentation (Day 15 - Part 1) ([9e4d4de](https://code.cannabrands.com/Cannabrands/hub/commit/9e4d4de07c14950e42be92a02c028ba4f2a93cac))
|
||||
* Add onboarding confirmation page and file upload endpoint ([b843788](https://code.cannabrands.com/Cannabrands/hub/commit/b8437882d7fafee106a288bfa6a2ce3c79b29a40))
|
||||
* add optimized BOM UI with advanced features ([b0261b9](https://code.cannabrands.com/Cannabrands/hub/commit/b0261b9d4ab3ec548cb996e149b26b22adc52d9d))
|
||||
* add PDF export for BOM ([94bc897](https://code.cannabrands.com/Cannabrands/hub/commit/94bc897c86b294093271def8fe911a59e0d7849f))
|
||||
* add per-brand revenue visualization with color-coded legend ([b00155c](https://code.cannabrands.com/Cannabrands/hub/commit/b00155cf66ad9f7b17f36b3206b23cede13e9f85))
|
||||
* add picking workflow for manual invoices ([18685fa](https://code.cannabrands.com/Cannabrands/hub/commit/18685fafde9ee9340990f6d1322e6ac9d4a8568b))
|
||||
* add pickup integration - models and database layer ([a231b4a](https://code.cannabrands.com/Cannabrands/hub/commit/a231b4a4db38d396f9728616e74f3608905ee721))
|
||||
* add pre-commit hook for automatic code formatting ([8ec361f](https://code.cannabrands.com/Cannabrands/hub/commit/8ec361f49f609fcc7bfb946bf1b478e88864d5f3))
|
||||
* add product pricing and business enhancements ([0560d5a](https://code.cannabrands.com/Cannabrands/hub/commit/0560d5ac50f67c1bb72feb5a059cf0177fa23715))
|
||||
* add RelationManagers and actions to ProductResource ([fd1c27e](https://code.cannabrands.com/Cannabrands/hub/commit/fd1c27e066ef3a4a94c293f886b898879f7253ca))
|
||||
* Add RetailerResource functionality ([831f758](https://code.cannabrands.com/Cannabrands/hub/commit/831f75805d31f5c810e934012853c83e6eaf2f1f))
|
||||
* add roles, seeders, and new packages ([6c07ce9](https://code.cannabrands.com/Cannabrands/hub/commit/6c07ce92d56d298e8bea7b24e49d1da97342360d))
|
||||
* Add RolesOverview and UserStats widgets ([5ddb1c8](https://code.cannabrands.com/Cannabrands/hub/commit/5ddb1c840ec5a816b58d01b0a423c75d67881729))
|
||||
* add seller notification widget to Filament admin dashboard ([04fa99d](https://code.cannabrands.com/Cannabrands/hub/commit/04fa99df240d790adff5e4cc59ae08f9e35ea317))
|
||||
* add status and version routes ([6d72d88](https://code.cannabrands.com/Cannabrands/hub/commit/6d72d882f4defbb8908e932f08337ee910addedb))
|
||||
* add stock filter to products page ([392a8ea](https://code.cannabrands.com/Cannabrands/hub/commit/392a8eafe041a93b1d17535a4113a0f64322739b))
|
||||
* Add SuperAdminSeeder for initial super admin user ([645d26e](https://code.cannabrands.com/Cannabrands/hub/commit/645d26eb25c19849823fe8cb3ea6ba652f957a31))
|
||||
* add tax configuration UI and location license management ([87d3572](https://code.cannabrands.com/Cannabrands/hub/commit/87d35722ef0933b5f0d7c9cd10f8e9712ed20c79))
|
||||
* Add user menu and theme switcher components ([43426ed](https://code.cannabrands.com/Cannabrands/hub/commit/43426ed625cd9370c2d4abb0bdd25a0aeb6f8111))
|
||||
* Add UserResource and related classes and pages ([cdae421](https://code.cannabrands.com/Cannabrands/hub/commit/cdae42170fe8a2699dcdaf5959c60b9fc5bc6d87))
|
||||
* add utility commands for data cleanup and fixes ([52ecf0b](https://code.cannabrands.com/Cannabrands/hub/commit/52ecf0b3ffc4e831248c8907c1705ebffe4a04f6))
|
||||
* add vendor management and component images ([b04e2b6](https://code.cannabrands.com/Cannabrands/hub/commit/b04e2b6e6b6153766e70faec46c986fffbf2016b))
|
||||
* complete shipping manifest system with buyer access and PDF improvements ([031136d](https://code.cannabrands.com/Cannabrands/hub/commit/031136db7de0e398498c9d96ef4d9786012b4d0f))
|
||||
* comprehensive auth system improvements and user management enhancements ([766d416](https://code.cannabrands.com/Cannabrands/hub/commit/766d416100ad8f3b50f244e7c03c053664a7be2e))
|
||||
* comprehensive test data setup and account type analysis ([c8987b1](https://code.cannabrands.com/Cannabrands/hub/commit/c8987b1c95b69dd221294518906795e049e1a665))
|
||||
* convert license type to dropdown with official Arizona types ([246779b](https://code.cannabrands.com/Cannabrands/hub/commit/246779be3bb2ef0e37402492416aa051d018ec0a))
|
||||
* create fully onboarded test businesses in DevSeeder ([0d0334c](https://code.cannabrands.com/Cannabrands/hub/commit/0d0334c2e97ab200e8fd191d1f04dd229ef5dce7))
|
||||
* create separate authentication controllers and views for buyers and sellers ([684e947](https://code.cannabrands.com/Cannabrands/hub/commit/684e947496d8ac1daaa2b864ea43cf7bc3755e32))
|
||||
* create unified registration landing page with user type selection ([87b862c](https://code.cannabrands.com/Cannabrands/hub/commit/87b862c6357171d6524563171996b949c058589b))
|
||||
* dynamically pull seller business info on invoice PDF ([be0ef68](https://code.cannabrands.com/Cannabrands/hub/commit/be0ef682079eb8decde9d69145a74c5f3e8f56b2))
|
||||
* enforce single primary contact/user with info icon tooltip ([edb69ed](https://code.cannabrands.com/Cannabrands/hub/commit/edb69ed4c79b8f7d36438654b7e7a98023da47b4))
|
||||
* fix component images migration to use product_images table ([c1cb84f](https://code.cannabrands.com/Cannabrands/hub/commit/c1cb84fa9d141e517e2a7e0b97558b5d9cf81ccb))
|
||||
* implement batch tracking models and relationships ([8a753ad](https://code.cannabrands.com/Cannabrands/hub/commit/8a753ad3a624a09e31d6b3c2c1ae6f49f5f5a1fe))
|
||||
* implement Brand model with 14 actual brands ([49d4f11](https://code.cannabrands.com/Cannabrands/hub/commit/49d4f11102f83ce5d2fdf1add9c95f17d28d2005))
|
||||
* implement business management features (contacts, locations, users, profiles) ([38ac09e](https://code.cannabrands.com/Cannabrands/hub/commit/38ac09e9e7def2d4d4533b880eee0dfbdea1df4b))
|
||||
* implement buyer order and invoice management portal (Day 13) ([aaad277](https://code.cannabrands.com/Cannabrands/hub/commit/aaad277a49d80640e7b397d3df22adeda2a8ff7d))
|
||||
* implement buyer-specific Nexus dashboard with LeafLink-style navigation ([9b72a8f](https://code.cannabrands.com/Cannabrands/hub/commit/9b72a8f3ba97924e94eed806cc014af538110ec2))
|
||||
* implement buyer/seller routing structure with dual registration options ([5393969](https://code.cannabrands.com/Cannabrands/hub/commit/53939692c01669724372028f8056d9bfdf0bb92a))
|
||||
* implement buyer/seller user type separation in database layer ([4e15b3d](https://code.cannabrands.com/Cannabrands/hub/commit/4e15b3d15cc8fd046dd40d0a6de512ec8b878b84))
|
||||
* implement CalVer versioning system with sidebar display ([197d102](https://code.cannabrands.com/Cannabrands/hub/commit/197d10269004f758692cacb444a0e9698ec7e7f1))
|
||||
* implement Choices.js for vendor selection dropdown ([3c25bb4](https://code.cannabrands.com/Cannabrands/hub/commit/3c25bb49229767845456eb38ef45c41a015583d3))
|
||||
* implement CompanyResource with approval workflow ([08d3271](https://code.cannabrands.com/Cannabrands/hub/commit/08d32713c37b35535ce7d69021ba0bdaa41c472a))
|
||||
* implement complete business/company migration from old CRM ([2f163db](https://code.cannabrands.com/Cannabrands/hub/commit/2f163db73814ed336c191b075605b94d4c03a41f))
|
||||
* implement complete Docker containerization for development and production ([7629500](https://code.cannabrands.com/Cannabrands/hub/commit/7629500d2be40ae93ae1bab58c5f9cefc23c9b5d))
|
||||
* implement complete email and notification system (Day 15 - Part 2) ([3183c8b](https://code.cannabrands.com/Cannabrands/hub/commit/3183c8b9acb017a702d489064e04d650fc1d063e))
|
||||
* implement complete Nexus dashboard layout with Iconify integration ([9cb9fcc](https://code.cannabrands.com/Cannabrands/hub/commit/9cb9fcca724abb6c64f0ebb42e35d7655e485715))
|
||||
* implement complete order management system with picking workflow ([f9fa249](https://code.cannabrands.com/Cannabrands/hub/commit/f9fa2499da4e5bc919a8ee2aa668a3512c4322ae))
|
||||
* implement complete user approval and dashboard workflow ([7f51605](https://code.cannabrands.com/Cannabrands/hub/commit/7f51605e0d674bb2e6296addb32a51c412804ed1))
|
||||
* implement comprehensive analytics dashboard with 5 visualizations ([fdfe204](https://code.cannabrands.com/Cannabrands/hub/commit/fdfe204f8b981fc2350cca710f2cddc7f244b88d))
|
||||
* implement comprehensive business setup form with all CRM sections ([e39623d](https://code.cannabrands.com/Cannabrands/hub/commit/e39623dd08ebe14fb6f0b38690df6cd85128ee4f))
|
||||
* implement comprehensive buyer dashboard with real-time stats (Day 14) ([98284a6](https://code.cannabrands.com/Cannabrands/hub/commit/98284a6035f0f20a70e2a545a7d724010a204823))
|
||||
* implement comprehensive notification system and ZOD form validation ([138abc1](https://code.cannabrands.com/Cannabrands/hub/commit/138abc144bcfbb62b11fe5cea9d43c281745f11c))
|
||||
* implement comprehensive Profile and Settings pages ([8eeee6a](https://code.cannabrands.com/Cannabrands/hub/commit/8eeee6a164d49e9ad667d66fc5f6f0035cf1c939))
|
||||
* implement core Business model and multi-tenancy architecture ([3883719](https://code.cannabrands.com/Cannabrands/hub/commit/388371982c79ef7d019d3a7bb7ea2c9d375deaec))
|
||||
* implement CRM-inspired theme and remove logos from auth forms ([6bc6e3e](https://code.cannabrands.com/Cannabrands/hub/commit/6bc6e3eef1abe796214a46e2ed262c69c497ac44)), closes [#014342](https://code.cannabrands.com/Cannabrands/hub/issues/014342) [#015b59](https://code.cannabrands.com/Cannabrands/hub/issues/015b59) [#e3f1eb](https://code.cannabrands.com/Cannabrands/hub/issues/e3f1eb) [#172B4D](https://code.cannabrands.com/Cannabrands/hub/issues/172B4D) [#344767](https://code.cannabrands.com/Cannabrands/hub/issues/344767)
|
||||
* implement DaisyUI 5.x custom theme syntax ([433b712](https://code.cannabrands.com/Cannabrands/hub/commit/433b712c5dd568c5c33fc2f918447aa9ca538a9d))
|
||||
* implement fleet management system with delivery scheduling ([7781de5](https://code.cannabrands.com/Cannabrands/hub/commit/7781de51053d53e5dc75bc9152277097641324f7))
|
||||
* implement interactive dashboard with ApexCharts and anime.js ([3cf5c63](https://code.cannabrands.com/Cannabrands/hub/commit/3cf5c635a9508b134e3e87e8e6e3013d505f24b7))
|
||||
* implement Lab model and StrainSeeder for cannabis compliance ([a26c0fc](https://code.cannabrands.com/Cannabrands/hub/commit/a26c0fc520d0f3d879f0d74883809a1abaf6964f))
|
||||
* implement manual admin approval workflow for user registration ([ead8916](https://code.cannabrands.com/Cannabrands/hub/commit/ead8916567d72ccd92e6a89a1b199a3265c6640f))
|
||||
* implement payment term surcharges at checkout ([cf148a8](https://code.cannabrands.com/Cannabrands/hub/commit/cf148a833062eb1c70cf489cee5760ced4e91d5b))
|
||||
* implement product migration command with dry-run support ([ba2f434](https://code.cannabrands.com/Cannabrands/hub/commit/ba2f434bcb11eb04f15a50f94cf7023f181c65bb))
|
||||
* implement Product, Strain, and Component models with BOM system ([d4aebad](https://code.cannabrands.com/Cannabrands/hub/commit/d4aebadbc7d009aa28276051f65f503fcb02bbbd))
|
||||
* implement ProductResource in Filament with cannabis-specific features ([5d7f7d6](https://code.cannabrands.com/Cannabrands/hub/commit/5d7f7d681086a889c004a1ec53112a6ccc78dee8))
|
||||
* implement seamless primary toggle switching with Livewire ([fe790d2](https://code.cannabrands.com/Cannabrands/hub/commit/fe790d284ebf9868347ccfef5456e7bfe2a5efb8))
|
||||
* implement seller notification system (Day 16) ([c024cd8](https://code.cannabrands.com/Cannabrands/hub/commit/c024cd86fe5a2ddd413052facc295c4d966efae5))
|
||||
* implement unified single login for all user types ([c00e869](https://code.cannabrands.com/Cannabrands/hub/commit/c00e869a82492e555ceb8c3308ba57cd435d3ba4))
|
||||
* implement UUID-based storage paths for multi-tenant isolation ([2b6f2df](https://code.cannabrands.com/Cannabrands/hub/commit/2b6f2dfd379801063b9d2d048c2256d76e66d17e))
|
||||
* improve manual invoice workflow to skip picking stages ([ca0bda4](https://code.cannabrands.com/Cannabrands/hub/commit/ca0bda4e852add20992f471946bef8d69bb67b06))
|
||||
* integrate batch selection in marketplace and cart ([af96aba](https://code.cannabrands.com/Cannabrands/hub/commit/af96abaa1cf527e099d2a792a433edb37a73201b))
|
||||
* migrate business-level contact fields to Contact records ([d97534d](https://code.cannabrands.com/Cannabrands/hub/commit/d97534d4d3aaf00852fdda99d04f2fe80bfd955a))
|
||||
* migrate to first_name/last_name fields and fix authentication routing ([42a6f13](https://code.cannabrands.com/Cannabrands/hub/commit/42a6f1330c6ed54b667cc450e41efd9924a0bcff))
|
||||
* **models:** Add retailer_id field to User model and migration ([25d8e71](https://code.cannabrands.com/Cannabrands/hub/commit/25d8e71ef33e103840ffe087420570d94c1cfcfe))
|
||||
* optimize form field widths and add Alpine.js formatters ([e58c0ab](https://code.cannabrands.com/Cannabrands/hub/commit/e58c0abcf4d52315408367fa1256e8005f330510))
|
||||
* optimize theme colors for UX and accessibility ([ee8c7b8](https://code.cannabrands.com/Cannabrands/hub/commit/ee8c7b8e6237c8c61d7960089bc2fb7e556f44b8)), closes [#015b59](https://code.cannabrands.com/Cannabrands/hub/issues/015b59) [#e3f1eb](https://code.cannabrands.com/Cannabrands/hub/issues/e3f1eb)
|
||||
* remove standard /register route, use only /b/register ([f4de7da](https://code.cannabrands.com/Cannabrands/hub/commit/f4de7da08209ae3a2121b9d5817948d466ffddac))
|
||||
* rename businesses table and model to companies ([cd02403](https://code.cannabrands.com/Cannabrands/hub/commit/cd02403e025c2f88c4e8f3e26e78432ded1c5f6c))
|
||||
* replace Spatie Laravel PDF with DomPDF for all PDF generation ([e3cb8b2](https://code.cannabrands.com/Cannabrands/hub/commit/e3cb8b2dfe2277f80c3d9ba2d9fc0b8ca336bd86))
|
||||
* successfully upgrade Filament from v3 to v4 ([cd77339](https://code.cannabrands.com/Cannabrands/hub/commit/cd7733974293a5eb5a1f67b660dee84d9eb1a19f))
|
||||
* update business registration form to match main register styling ([b4b4324](https://code.cannabrands.com/Cannabrands/hub/commit/b4b432420bf18405746c779f78da4aa4d98b7478))
|
||||
* update dashboard page title and breadcrumb for multi-tenant future ([0f872b6](https://code.cannabrands.com/Cannabrands/hub/commit/0f872b6c6a2b7a5edfeab8b830030f80f4ead33c))
|
||||
* update invoice PDF to display contact and location information ([79fc856](https://code.cannabrands.com/Cannabrands/hub/commit/79fc856850b84fe6697a2f7d290127e2187453f2))
|
||||
* update manifest PDF template for pickup/delivery ([c7e9f95](https://code.cannabrands.com/Cannabrands/hub/commit/c7e9f952bf109de0096e6584dd8de14c7ea3b6fa))
|
||||
* update seller/admin views for pickup display ([9f98ab3](https://code.cannabrands.com/Cannabrands/hub/commit/9f98ab381b32953836af5bb6cd786d87d70f0af2))
|
||||
* upgrade Pest testing framework to v4.0 ([24c2eb2](https://code.cannabrands.com/Cannabrands/hub/commit/24c2eb29a8431e49f48f6d9ea6b9973af63f8f29))
|
||||
* Add AdminPanelProvider and dashboard_url helper function ([8628e44](https://code.cannabrands.app/Cannabrands/hub/commit/8628e447ce4f2eaaff5eed26cd973a7549038108))
|
||||
* add ApexCharts and anime.js for dashboard enhancements ([7fca649](https://code.cannabrands.app/Cannabrands/hub/commit/7fca649b37827f2d860e1b692438f2e2e1057e66))
|
||||
* add Arizona cannabis compliance info to invoice PDF ([25cc2e7](https://code.cannabrands.app/Cannabrands/hub/commit/25cc2e70f6d0cc52fa9e2aeb0ccf59d2651d4f03))
|
||||
* add batch selection UI to marketplace and cart ([1ccef51](https://code.cannabrands.app/Cannabrands/hub/commit/1ccef51cbb98e94248b2ab01f5818416e52380f0))
|
||||
* add batch tracking and MSRP support to invoicing ([f2cadb2](https://code.cannabrands.app/Cannabrands/hub/commit/f2cadb2d9336294250006e829a062d15de34a30a))
|
||||
* add BOM (Bill of Materials) management system ([f1d9684](https://code.cannabrands.app/Cannabrands/hub/commit/f1d96844c3e520365d84abcbe6befb5b0310164c))
|
||||
* add buyer UI for pickup selection and management ([57c9407](https://code.cannabrands.app/Cannabrands/hub/commit/57c940735aac92737b4b0cc13d08f456f02e6c6a))
|
||||
* add configurable tax rates for Arizona B2B cannabis compliance ([dd60485](https://code.cannabrands.app/Cannabrands/hub/commit/dd60485206fc8ae10802bb186d25b4f6b24802c2))
|
||||
* add data migration commands for legacy system import ([cf06bcb](https://code.cannabrands.app/Cannabrands/hub/commit/cf06bcb4d858ea68b95e4ef6a22b9548d1d60df5))
|
||||
* add database migrations for batch tracking system ([4f53fde](https://code.cannabrands.app/Cannabrands/hub/commit/4f53fde8b2897486fe0163711b79cca55a9bd3bf))
|
||||
* add Docker image building to CI pipeline + deployment docs ([c658193](https://code.cannabrands.app/Cannabrands/hub/commit/c658193909ed9f54387106a2b8eefa10b375a82d))
|
||||
* add drag-and-drop reordering to BOM components ([83a9f71](https://code.cannabrands.app/Cannabrands/hub/commit/83a9f713a21355626b360e96431a386d86fc0f07))
|
||||
* add Filament admin interface for batch management ([e002322](https://code.cannabrands.app/Cannabrands/hub/commit/e0023222ea89ad03d28c7d6c2358325c786133a8))
|
||||
* add finalize & send functionality for manual invoices ([05a0e74](https://code.cannabrands.app/Cannabrands/hub/commit/05a0e74c66599b12566d415ef3ca023c43b51e1a))
|
||||
* add gitlab ci starter ([4c3185c](https://code.cannabrands.app/Cannabrands/hub/commit/4c3185ca0a00fdf22f56cc528227bd9ea475bba1))
|
||||
* add license field validation and storage to location controller ([d77a138](https://code.cannabrands.app/Cannabrands/hub/commit/d77a138985b412655ebff78d984d6cb264def93d))
|
||||
* add license_type field and clean up Business model ([3b4da67](https://code.cannabrands.app/Cannabrands/hub/commit/3b4da67c6d063c6e1c653d26f4ee3849cc110863))
|
||||
* add light/dark mode support to ApexCharts ([c2dbf28](https://code.cannabrands.app/Cannabrands/hub/commit/c2dbf284609cf30a73b364cb098f08414fa54255))
|
||||
* add light/dark theme toggle with sun/moon icons ([5bf5b7c](https://code.cannabrands.app/Cannabrands/hub/commit/5bf5b7ced9d04c9c4c2ee2eadd3f93da2dbab609))
|
||||
* add location and contact selection to invoice creation form ([e5f2dda](https://code.cannabrands.app/Cannabrands/hub/commit/e5f2dda7f3b44bbd0ce4e64820936d91c3850adb))
|
||||
* add location license number to invoice PDF ([f6dda38](https://code.cannabrands.app/Cannabrands/hub/commit/f6dda38180431602b58e56113e2cf49ef26f3f3a))
|
||||
* add Makefiles ([5d3c5d5](https://code.cannabrands.app/Cannabrands/hub/commit/5d3c5d5fe93048c4b5227913af1a0b794fe00531))
|
||||
* add marketplace controllers and documentation for buyer/seller structure ([eb6db6d](https://code.cannabrands.app/Cannabrands/hub/commit/eb6db6dbdced3adede2fc7391e6d7ec5d94b6685))
|
||||
* Add multi-step signup onboarding process ([65c6149](https://code.cannabrands.app/Cannabrands/hub/commit/65c6149e3a68215d17c0c773531bb888f9adffac))
|
||||
* Add new blade component files for sidebar and layout components ([2f88152](https://code.cannabrands.app/Cannabrands/hub/commit/2f8815235c989f180fc78d84ccdf805255db4764))
|
||||
* Add new blade components for Filament panels ([d6ac894](https://code.cannabrands.app/Cannabrands/hub/commit/d6ac8941b804fbdc4c933c4c802529e53e8bc5b0))
|
||||
* Add new view files for authentication and dashboard pages ([80f008c](https://code.cannabrands.app/Cannabrands/hub/commit/80f008c2534dff04ceba04cd5150d10b6da3a589))
|
||||
* add notification infrastructure and policy documentation (Day 15 - Part 1) ([9e4d4de](https://code.cannabrands.app/Cannabrands/hub/commit/9e4d4de07c14950e42be92a02c028ba4f2a93cac))
|
||||
* Add onboarding confirmation page and file upload endpoint ([b843788](https://code.cannabrands.app/Cannabrands/hub/commit/b8437882d7fafee106a288bfa6a2ce3c79b29a40))
|
||||
* add optimized BOM UI with advanced features ([b0261b9](https://code.cannabrands.app/Cannabrands/hub/commit/b0261b9d4ab3ec548cb996e149b26b22adc52d9d))
|
||||
* add PDF export for BOM ([94bc897](https://code.cannabrands.app/Cannabrands/hub/commit/94bc897c86b294093271def8fe911a59e0d7849f))
|
||||
* add per-brand revenue visualization with color-coded legend ([b00155c](https://code.cannabrands.app/Cannabrands/hub/commit/b00155cf66ad9f7b17f36b3206b23cede13e9f85))
|
||||
* add picking workflow for manual invoices ([18685fa](https://code.cannabrands.app/Cannabrands/hub/commit/18685fafde9ee9340990f6d1322e6ac9d4a8568b))
|
||||
* add pickup integration - models and database layer ([a231b4a](https://code.cannabrands.app/Cannabrands/hub/commit/a231b4a4db38d396f9728616e74f3608905ee721))
|
||||
* add pre-commit hook for automatic code formatting ([8ec361f](https://code.cannabrands.app/Cannabrands/hub/commit/8ec361f49f609fcc7bfb946bf1b478e88864d5f3))
|
||||
* add product pricing and business enhancements ([0560d5a](https://code.cannabrands.app/Cannabrands/hub/commit/0560d5ac50f67c1bb72feb5a059cf0177fa23715))
|
||||
* add RelationManagers and actions to ProductResource ([fd1c27e](https://code.cannabrands.app/Cannabrands/hub/commit/fd1c27e066ef3a4a94c293f886b898879f7253ca))
|
||||
* Add RetailerResource functionality ([831f758](https://code.cannabrands.app/Cannabrands/hub/commit/831f75805d31f5c810e934012853c83e6eaf2f1f))
|
||||
* add roles, seeders, and new packages ([6c07ce9](https://code.cannabrands.app/Cannabrands/hub/commit/6c07ce92d56d298e8bea7b24e49d1da97342360d))
|
||||
* Add RolesOverview and UserStats widgets ([5ddb1c8](https://code.cannabrands.app/Cannabrands/hub/commit/5ddb1c840ec5a816b58d01b0a423c75d67881729))
|
||||
* add seller notification widget to Filament admin dashboard ([04fa99d](https://code.cannabrands.app/Cannabrands/hub/commit/04fa99df240d790adff5e4cc59ae08f9e35ea317))
|
||||
* add status and version routes ([6d72d88](https://code.cannabrands.app/Cannabrands/hub/commit/6d72d882f4defbb8908e932f08337ee910addedb))
|
||||
* add stock filter to products page ([392a8ea](https://code.cannabrands.app/Cannabrands/hub/commit/392a8eafe041a93b1d17535a4113a0f64322739b))
|
||||
* Add SuperAdminSeeder for initial super admin user ([645d26e](https://code.cannabrands.app/Cannabrands/hub/commit/645d26eb25c19849823fe8cb3ea6ba652f957a31))
|
||||
* add tax configuration UI and location license management ([87d3572](https://code.cannabrands.app/Cannabrands/hub/commit/87d35722ef0933b5f0d7c9cd10f8e9712ed20c79))
|
||||
* Add user menu and theme switcher components ([43426ed](https://code.cannabrands.app/Cannabrands/hub/commit/43426ed625cd9370c2d4abb0bdd25a0aeb6f8111))
|
||||
* Add UserResource and related classes and pages ([cdae421](https://code.cannabrands.app/Cannabrands/hub/commit/cdae42170fe8a2699dcdaf5959c60b9fc5bc6d87))
|
||||
* add utility commands for data cleanup and fixes ([52ecf0b](https://code.cannabrands.app/Cannabrands/hub/commit/52ecf0b3ffc4e831248c8907c1705ebffe4a04f6))
|
||||
* add vendor management and component images ([b04e2b6](https://code.cannabrands.app/Cannabrands/hub/commit/b04e2b6e6b6153766e70faec46c986fffbf2016b))
|
||||
* complete shipping manifest system with buyer access and PDF improvements ([031136d](https://code.cannabrands.app/Cannabrands/hub/commit/031136db7de0e398498c9d96ef4d9786012b4d0f))
|
||||
* comprehensive auth system improvements and user management enhancements ([766d416](https://code.cannabrands.app/Cannabrands/hub/commit/766d416100ad8f3b50f244e7c03c053664a7be2e))
|
||||
* comprehensive test data setup and account type analysis ([c8987b1](https://code.cannabrands.app/Cannabrands/hub/commit/c8987b1c95b69dd221294518906795e049e1a665))
|
||||
* convert license type to dropdown with official Arizona types ([246779b](https://code.cannabrands.app/Cannabrands/hub/commit/246779be3bb2ef0e37402492416aa051d018ec0a))
|
||||
* create fully onboarded test businesses in DevSeeder ([0d0334c](https://code.cannabrands.app/Cannabrands/hub/commit/0d0334c2e97ab200e8fd191d1f04dd229ef5dce7))
|
||||
* create separate authentication controllers and views for buyers and sellers ([684e947](https://code.cannabrands.app/Cannabrands/hub/commit/684e947496d8ac1daaa2b864ea43cf7bc3755e32))
|
||||
* create unified registration landing page with user type selection ([87b862c](https://code.cannabrands.app/Cannabrands/hub/commit/87b862c6357171d6524563171996b949c058589b))
|
||||
* dynamically pull seller business info on invoice PDF ([be0ef68](https://code.cannabrands.app/Cannabrands/hub/commit/be0ef682079eb8decde9d69145a74c5f3e8f56b2))
|
||||
* enforce single primary contact/user with info icon tooltip ([edb69ed](https://code.cannabrands.app/Cannabrands/hub/commit/edb69ed4c79b8f7d36438654b7e7a98023da47b4))
|
||||
* fix component images migration to use product_images table ([c1cb84f](https://code.cannabrands.app/Cannabrands/hub/commit/c1cb84fa9d141e517e2a7e0b97558b5d9cf81ccb))
|
||||
* implement batch tracking models and relationships ([8a753ad](https://code.cannabrands.app/Cannabrands/hub/commit/8a753ad3a624a09e31d6b3c2c1ae6f49f5f5a1fe))
|
||||
* implement Brand model with 14 actual brands ([49d4f11](https://code.cannabrands.app/Cannabrands/hub/commit/49d4f11102f83ce5d2fdf1add9c95f17d28d2005))
|
||||
* implement business management features (contacts, locations, users, profiles) ([38ac09e](https://code.cannabrands.app/Cannabrands/hub/commit/38ac09e9e7def2d4d4533b880eee0dfbdea1df4b))
|
||||
* implement buyer order and invoice management portal (Day 13) ([aaad277](https://code.cannabrands.app/Cannabrands/hub/commit/aaad277a49d80640e7b397d3df22adeda2a8ff7d))
|
||||
* implement buyer-specific Nexus dashboard with LeafLink-style navigation ([9b72a8f](https://code.cannabrands.app/Cannabrands/hub/commit/9b72a8f3ba97924e94eed806cc014af538110ec2))
|
||||
* implement buyer/seller routing structure with dual registration options ([5393969](https://code.cannabrands.app/Cannabrands/hub/commit/53939692c01669724372028f8056d9bfdf0bb92a))
|
||||
* implement buyer/seller user type separation in database layer ([4e15b3d](https://code.cannabrands.app/Cannabrands/hub/commit/4e15b3d15cc8fd046dd40d0a6de512ec8b878b84))
|
||||
* implement CalVer versioning system with sidebar display ([197d102](https://code.cannabrands.app/Cannabrands/hub/commit/197d10269004f758692cacb444a0e9698ec7e7f1))
|
||||
* implement Choices.js for vendor selection dropdown ([3c25bb4](https://code.cannabrands.app/Cannabrands/hub/commit/3c25bb49229767845456eb38ef45c41a015583d3))
|
||||
* implement CompanyResource with approval workflow ([08d3271](https://code.cannabrands.app/Cannabrands/hub/commit/08d32713c37b35535ce7d69021ba0bdaa41c472a))
|
||||
* implement complete business/company migration from old CRM ([2f163db](https://code.cannabrands.app/Cannabrands/hub/commit/2f163db73814ed336c191b075605b94d4c03a41f))
|
||||
* implement complete Docker containerization for development and production ([7629500](https://code.cannabrands.app/Cannabrands/hub/commit/7629500d2be40ae93ae1bab58c5f9cefc23c9b5d))
|
||||
* implement complete email and notification system (Day 15 - Part 2) ([3183c8b](https://code.cannabrands.app/Cannabrands/hub/commit/3183c8b9acb017a702d489064e04d650fc1d063e))
|
||||
* implement complete Nexus dashboard layout with Iconify integration ([9cb9fcc](https://code.cannabrands.app/Cannabrands/hub/commit/9cb9fcca724abb6c64f0ebb42e35d7655e485715))
|
||||
* implement complete order management system with picking workflow ([f9fa249](https://code.cannabrands.app/Cannabrands/hub/commit/f9fa2499da4e5bc919a8ee2aa668a3512c4322ae))
|
||||
* implement complete user approval and dashboard workflow ([7f51605](https://code.cannabrands.app/Cannabrands/hub/commit/7f51605e0d674bb2e6296addb32a51c412804ed1))
|
||||
* implement comprehensive analytics dashboard with 5 visualizations ([fdfe204](https://code.cannabrands.app/Cannabrands/hub/commit/fdfe204f8b981fc2350cca710f2cddc7f244b88d))
|
||||
* implement comprehensive business setup form with all CRM sections ([e39623d](https://code.cannabrands.app/Cannabrands/hub/commit/e39623dd08ebe14fb6f0b38690df6cd85128ee4f))
|
||||
* implement comprehensive buyer dashboard with real-time stats (Day 14) ([98284a6](https://code.cannabrands.app/Cannabrands/hub/commit/98284a6035f0f20a70e2a545a7d724010a204823))
|
||||
* implement comprehensive notification system and ZOD form validation ([138abc1](https://code.cannabrands.app/Cannabrands/hub/commit/138abc144bcfbb62b11fe5cea9d43c281745f11c))
|
||||
* implement comprehensive Profile and Settings pages ([8eeee6a](https://code.cannabrands.app/Cannabrands/hub/commit/8eeee6a164d49e9ad667d66fc5f6f0035cf1c939))
|
||||
* implement core Business model and multi-tenancy architecture ([3883719](https://code.cannabrands.app/Cannabrands/hub/commit/388371982c79ef7d019d3a7bb7ea2c9d375deaec))
|
||||
* implement CRM-inspired theme and remove logos from auth forms ([6bc6e3e](https://code.cannabrands.app/Cannabrands/hub/commit/6bc6e3eef1abe796214a46e2ed262c69c497ac44)), closes [#014342](https://code.cannabrands.app/Cannabrands/hub/issues/014342) [#015b59](https://code.cannabrands.app/Cannabrands/hub/issues/015b59) [#e3f1eb](https://code.cannabrands.app/Cannabrands/hub/issues/e3f1eb) [#172B4D](https://code.cannabrands.app/Cannabrands/hub/issues/172B4D) [#344767](https://code.cannabrands.app/Cannabrands/hub/issues/344767)
|
||||
* implement DaisyUI 5.x custom theme syntax ([433b712](https://code.cannabrands.app/Cannabrands/hub/commit/433b712c5dd568c5c33fc2f918447aa9ca538a9d))
|
||||
* implement fleet management system with delivery scheduling ([7781de5](https://code.cannabrands.app/Cannabrands/hub/commit/7781de51053d53e5dc75bc9152277097641324f7))
|
||||
* implement interactive dashboard with ApexCharts and anime.js ([3cf5c63](https://code.cannabrands.app/Cannabrands/hub/commit/3cf5c635a9508b134e3e87e8e6e3013d505f24b7))
|
||||
* implement Lab model and StrainSeeder for cannabis compliance ([a26c0fc](https://code.cannabrands.app/Cannabrands/hub/commit/a26c0fc520d0f3d879f0d74883809a1abaf6964f))
|
||||
* implement manual admin approval workflow for user registration ([ead8916](https://code.cannabrands.app/Cannabrands/hub/commit/ead8916567d72ccd92e6a89a1b199a3265c6640f))
|
||||
* implement payment term surcharges at checkout ([cf148a8](https://code.cannabrands.app/Cannabrands/hub/commit/cf148a833062eb1c70cf489cee5760ced4e91d5b))
|
||||
* implement product migration command with dry-run support ([ba2f434](https://code.cannabrands.app/Cannabrands/hub/commit/ba2f434bcb11eb04f15a50f94cf7023f181c65bb))
|
||||
* implement Product, Strain, and Component models with BOM system ([d4aebad](https://code.cannabrands.app/Cannabrands/hub/commit/d4aebadbc7d009aa28276051f65f503fcb02bbbd))
|
||||
* implement ProductResource in Filament with cannabis-specific features ([5d7f7d6](https://code.cannabrands.app/Cannabrands/hub/commit/5d7f7d681086a889c004a1ec53112a6ccc78dee8))
|
||||
* implement seamless primary toggle switching with Livewire ([fe790d2](https://code.cannabrands.app/Cannabrands/hub/commit/fe790d284ebf9868347ccfef5456e7bfe2a5efb8))
|
||||
* implement seller notification system (Day 16) ([c024cd8](https://code.cannabrands.app/Cannabrands/hub/commit/c024cd86fe5a2ddd413052facc295c4d966efae5))
|
||||
* implement unified single login for all user types ([c00e869](https://code.cannabrands.app/Cannabrands/hub/commit/c00e869a82492e555ceb8c3308ba57cd435d3ba4))
|
||||
* implement UUID-based storage paths for multi-tenant isolation ([2b6f2df](https://code.cannabrands.app/Cannabrands/hub/commit/2b6f2dfd379801063b9d2d048c2256d76e66d17e))
|
||||
* improve manual invoice workflow to skip picking stages ([ca0bda4](https://code.cannabrands.app/Cannabrands/hub/commit/ca0bda4e852add20992f471946bef8d69bb67b06))
|
||||
* integrate batch selection in marketplace and cart ([af96aba](https://code.cannabrands.app/Cannabrands/hub/commit/af96abaa1cf527e099d2a792a433edb37a73201b))
|
||||
* migrate business-level contact fields to Contact records ([d97534d](https://code.cannabrands.app/Cannabrands/hub/commit/d97534d4d3aaf00852fdda99d04f2fe80bfd955a))
|
||||
* migrate to first_name/last_name fields and fix authentication routing ([42a6f13](https://code.cannabrands.app/Cannabrands/hub/commit/42a6f1330c6ed54b667cc450e41efd9924a0bcff))
|
||||
* **models:** Add retailer_id field to User model and migration ([25d8e71](https://code.cannabrands.app/Cannabrands/hub/commit/25d8e71ef33e103840ffe087420570d94c1cfcfe))
|
||||
* optimize form field widths and add Alpine.js formatters ([e58c0ab](https://code.cannabrands.app/Cannabrands/hub/commit/e58c0abcf4d52315408367fa1256e8005f330510))
|
||||
* optimize theme colors for UX and accessibility ([ee8c7b8](https://code.cannabrands.app/Cannabrands/hub/commit/ee8c7b8e6237c8c61d7960089bc2fb7e556f44b8)), closes [#015b59](https://code.cannabrands.app/Cannabrands/hub/issues/015b59) [#e3f1eb](https://code.cannabrands.app/Cannabrands/hub/issues/e3f1eb)
|
||||
* remove standard /register route, use only /b/register ([f4de7da](https://code.cannabrands.app/Cannabrands/hub/commit/f4de7da08209ae3a2121b9d5817948d466ffddac))
|
||||
* rename businesses table and model to companies ([cd02403](https://code.cannabrands.app/Cannabrands/hub/commit/cd02403e025c2f88c4e8f3e26e78432ded1c5f6c))
|
||||
* replace Spatie Laravel PDF with DomPDF for all PDF generation ([e3cb8b2](https://code.cannabrands.app/Cannabrands/hub/commit/e3cb8b2dfe2277f80c3d9ba2d9fc0b8ca336bd86))
|
||||
* successfully upgrade Filament from v3 to v4 ([cd77339](https://code.cannabrands.app/Cannabrands/hub/commit/cd7733974293a5eb5a1f67b660dee84d9eb1a19f))
|
||||
* update business registration form to match main register styling ([b4b4324](https://code.cannabrands.app/Cannabrands/hub/commit/b4b432420bf18405746c779f78da4aa4d98b7478))
|
||||
* update dashboard page title and breadcrumb for multi-tenant future ([0f872b6](https://code.cannabrands.app/Cannabrands/hub/commit/0f872b6c6a2b7a5edfeab8b830030f80f4ead33c))
|
||||
* update invoice PDF to display contact and location information ([79fc856](https://code.cannabrands.app/Cannabrands/hub/commit/79fc856850b84fe6697a2f7d290127e2187453f2))
|
||||
* update manifest PDF template for pickup/delivery ([c7e9f95](https://code.cannabrands.app/Cannabrands/hub/commit/c7e9f952bf109de0096e6584dd8de14c7ea3b6fa))
|
||||
* update seller/admin views for pickup display ([9f98ab3](https://code.cannabrands.app/Cannabrands/hub/commit/9f98ab381b32953836af5bb6cd786d87d70f0af2))
|
||||
* upgrade Pest testing framework to v4.0 ([24c2eb2](https://code.cannabrands.app/Cannabrands/hub/commit/24c2eb29a8431e49f48f6d9ea6b9973af63f8f29))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* optimize animations for 60fps performance ([2180897](https://code.cannabrands.com/Cannabrands/hub/commit/2180897cb473a70e3cc8bc2212eebbf7cc343424))
|
||||
* optimize primary toggle switching for instant response ([ccbbd33](https://code.cannabrands.com/Cannabrands/hub/commit/ccbbd33386818a2a7acee300ec9837b04d2e6264))
|
||||
* optimize animations for 60fps performance ([2180897](https://code.cannabrands.app/Cannabrands/hub/commit/2180897cb473a70e3cc8bc2212eebbf7cc343424))
|
||||
* optimize primary toggle switching for instant response ([ccbbd33](https://code.cannabrands.app/Cannabrands/hub/commit/ccbbd33386818a2a7acee300ec9837b04d2e6264))
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "refactor: match products filter styling to components page" ([8578e36](https://code.cannabrands.com/Cannabrands/hub/commit/8578e3680da8b340002d1a989e75e70d105b4333))
|
||||
* display actual database values for component units ([239390b](https://code.cannabrands.com/Cannabrands/hub/commit/239390b13291cbc5efe6f70c29eed5940bd20564))
|
||||
* remove multi-brand chart implementation ([b51a4a0](https://code.cannabrands.com/Cannabrands/hub/commit/b51a4a002106a3578b21c5b2d123d6d62ae09814))
|
||||
* restore working primary toggle implementation ([02135f2](https://code.cannabrands.com/Cannabrands/hub/commit/02135f2b039b9192a3f345bacdeacd5e10ecf019))
|
||||
* Revert "refactor: match products filter styling to components page" ([8578e36](https://code.cannabrands.app/Cannabrands/hub/commit/8578e3680da8b340002d1a989e75e70d105b4333))
|
||||
* display actual database values for component units ([239390b](https://code.cannabrands.app/Cannabrands/hub/commit/239390b13291cbc5efe6f70c29eed5940bd20564))
|
||||
* remove multi-brand chart implementation ([b51a4a0](https://code.cannabrands.app/Cannabrands/hub/commit/b51a4a002106a3578b21c5b2d123d6d62ae09814))
|
||||
* restore working primary toggle implementation ([02135f2](https://code.cannabrands.app/Cannabrands/hub/commit/02135f2b039b9192a3f345bacdeacd5e10ecf019))
|
||||
|
||||
|
||||
|
||||
|
||||
184
CLAUDE.md
184
CLAUDE.md
@@ -1,85 +1,141 @@
|
||||
# Claude Code Context
|
||||
|
||||
## Important Documentation to Review
|
||||
## 📌 IMPORTANT: Check Personal Context Files
|
||||
|
||||
Before implementing any features, please review the following documentation:
|
||||
**ALWAYS read `claude.kelly.md` first** - Contains personal preferences and session tracking workflow
|
||||
|
||||
### URL Structure and Architecture
|
||||
- **ALWAYS** review `docs/URL_STRUCTURE.md` before implementing any routing changes
|
||||
- The application uses a three-tier user system: buyers, sellers, and admins
|
||||
- URL prefixes: `/b/` (buyers), `/s/` (sellers), `/admin` (super admins)
|
||||
---
|
||||
|
||||
### Additional Documentation
|
||||
- `docs/DATABASE.md` - Database schema and conventions
|
||||
- `docs/DEVELOPMENT.md` - Development environment setup
|
||||
- `docs/KUBERNETES_DEPLOYMENT.md` - Kubernetes deployment guide
|
||||
- `docs/REGISTRY_CLEANUP.md` - Docker registry cleanup policy
|
||||
- `CONTRIBUTING.md` - Git workflow and PR process (root level)
|
||||
- `k8s/KUBECTL_COMMANDS.md` - kubectl operations reference
|
||||
## 🚨 Critical Mistakes You Make
|
||||
|
||||
### User Types and Account Structure
|
||||
- **Buyers** (Retailers/Dispensaries): Browse marketplace, instant approval
|
||||
- **Sellers** (Brands/Manufacturers): Manage products, require approval
|
||||
- **Admins**: Platform management
|
||||
### 1. Business Isolation (MOST COMMON!)
|
||||
❌ **Wrong:** `Component::findOrFail($id)` then check business_id
|
||||
✅ **Right:** `Component::where('business_id', $business->id)->findOrFail($id)`
|
||||
**Why:** Prevents ID enumeration across tenants (see audit: BomController vulnerability)
|
||||
|
||||
### Development Guidelines
|
||||
1. Maintain Laravel Breeze compatibility for authentication
|
||||
2. Follow existing code conventions and patterns
|
||||
3. Always check existing components before creating new ones
|
||||
4. Use PostgreSQL-compatible migrations (no IF/ELSE logic)
|
||||
5. Test routes after implementation
|
||||
6. Create informative git commits with clear messages
|
||||
**Models needing business_id:** Component, Brand, Product, Driver, Vehicle, Contact, Invoice
|
||||
**Exception:** Orders span buyer + seller businesses - use `whereHas('items.product.brand')`
|
||||
|
||||
## What NOT to Do
|
||||
### 2. Route Prefixes
|
||||
Check `docs/URL_STRUCTURE.md` BEFORE route changes.
|
||||
- `/b/*` → Buyers only
|
||||
- `/s/*` → Sellers only
|
||||
- `/admin` → Super admins only
|
||||
|
||||
- ❌ **NEVER** commit directly to `develop` or `master` branches
|
||||
- ❌ **NEVER** use raw SQL (use Eloquent/Query Builder)
|
||||
- ❌ **NEVER** skip authentication middleware on protected routes
|
||||
- ❌ **NEVER** commit `.env` files or secrets
|
||||
- ❌ **NEVER** create migrations with IF/ELSE logic (PostgreSQL incompatible)
|
||||
- ❌ **NEVER** bypass CI/CD checks
|
||||
- ❌ **NEVER** skip writing down() methods in migrations
|
||||
### 3. Filament Usage Boundary
|
||||
**Filament = `/admin` ONLY** (super admin tools)
|
||||
**DO NOT** use Filament for `/b/` or `/s/` - use DaisyUI + Blade instead
|
||||
**Why:** Filament is admin panel framework, not customer-facing UI
|
||||
|
||||
## Testing Requirements
|
||||
### 4. Multi-Tenancy Architecture
|
||||
**We do NOT use spatie/laravel-multitenancy** - manual business_id scoping
|
||||
**Why:** Two-sided marketplace needs cross-business queries (buyers browse all sellers' products)
|
||||
Orders link TWO businesses: buyer's business_id + seller's product→brand→business_id
|
||||
|
||||
- **ALWAYS** run tests before committing: `php artisan test --parallel`
|
||||
- **ALWAYS** check code style: `./vendor/bin/pint`
|
||||
- Tests must pass in CI/CD pipeline before merge
|
||||
### 5. Middleware Protection
|
||||
ALL routes need auth + user type middleware except public pages
|
||||
**Pattern:** `->middleware(['auth', 'verified', 'buyer'])` or `['seller', 'approved']`
|
||||
**Caught in audit:** Unprotected `/onboarding/*` routes - now fixed
|
||||
|
||||
## Testing Credentials
|
||||
- Buyer: `dispensary@example.com` / `password`
|
||||
- Seller: `brand@example.com` / `password`
|
||||
- Admin: `admin@example.com` / `password`
|
||||
### 6. PostgreSQL Migrations
|
||||
❌ No IF/ELSE logic in migrations (not supported)
|
||||
✅ Use Laravel Schema builder or conditional PHP code
|
||||
|
||||
## Current Architecture Decisions
|
||||
- Dual registration flow with informative landing page at `/register`
|
||||
- Separate authentication controllers for buyers and sellers
|
||||
- Marketplace functionality under `/b/` prefix
|
||||
- Brand/seller CRM functionality under `/s/` prefix
|
||||
### 7. Styling - DaisyUI/Tailwind Only
|
||||
❌ **NEVER use inline `style=""` attributes** in Blade templates
|
||||
✅ **ALWAYS use DaisyUI/Tailwind utility classes**
|
||||
**Why:** Consistency, maintainability, theme switching, and better performance
|
||||
|
||||
## Commands to Run After Changes
|
||||
- Clear caches: `php artisan cache:clear && php artisan config:clear && php artisan route:clear`
|
||||
- Run migrations: `php artisan migrate`
|
||||
- Seed test data: `php artisan db:seed --class=DevSeeder`
|
||||
**Correct patterns:**
|
||||
- Colors: Use `bg-primary`, `text-primary`, `bg-success`, etc. (defined in `resources/css/app.css`)
|
||||
- Spacing: Use `p-4`, `m-2`, `gap-3` (Tailwind utilities)
|
||||
- Layout: Use `flex`, `grid`, `items-center` (Tailwind utilities)
|
||||
- Custom colors: Add to `resources/css/app.css` theme variables, NOT inline
|
||||
|
||||
## CI/CD Pipeline
|
||||
**Exception:** Only use inline styles for truly dynamic values from database (e.g., user-uploaded brand colors)
|
||||
|
||||
Woodpecker CI runs automatically on push to develop/master:
|
||||
1. PHP syntax check
|
||||
2. Code style check (Pint)
|
||||
3. PHPUnit tests
|
||||
4. Docker image build (only if all checks pass)
|
||||
---
|
||||
|
||||
**Do not merge Pull Requests if CI/CD fails.**
|
||||
## Tech Stack by Area
|
||||
|
||||
## Server Requirements
|
||||
| Area | Framework | Users | UI |
|
||||
|------|-----------|-------|-----|
|
||||
| `/admin` | Filament v3 | Super admins | Filament tables/forms |
|
||||
| `/b/` | Blade + DaisyUI | Buyers | Custom marketplace |
|
||||
| `/s/` | Blade + DaisyUI | Sellers | Custom CRM |
|
||||
|
||||
### PDF Generation (DomPDF)
|
||||
This application uses DomPDF (`barryvdh/laravel-dompdf`) for generating cannabis shipping manifests and invoices.
|
||||
---
|
||||
|
||||
**No special server requirements needed** - DomPDF is pure PHP and works out of the box on all platforms (Linux, macOS, Windows, ARM64, x86_64).
|
||||
## Business Types
|
||||
|
||||
**Configuration:**
|
||||
- Package: `barryvdh/laravel-dompdf`
|
||||
- Already installed via Composer
|
||||
- No additional system dependencies required
|
||||
- `'buyer'` - Dispensary (browses marketplace, places orders)
|
||||
- `'seller'` - Brand (manages products, fulfills orders)
|
||||
- `'both'` - Vertically integrated
|
||||
|
||||
Users have `user_type` matching their business type.
|
||||
|
||||
---
|
||||
|
||||
## Testing & Git
|
||||
|
||||
**Before commit:**
|
||||
```bash
|
||||
php artisan test --parallel # REQUIRED
|
||||
./vendor/bin/pint # REQUIRED
|
||||
```
|
||||
|
||||
**Commit Messages:**
|
||||
- ❌ **DO NOT** include Claude Code signature/attribution in commit messages
|
||||
- ❌ **DO NOT** add "🤖 Generated with Claude Code" or "Co-Authored-By: Claude"
|
||||
- ✅ Write clean, professional commit messages without AI attribution
|
||||
|
||||
**Credentials:** `{buyer,seller,admin}@example.com` / `password`
|
||||
|
||||
**Branches:** Never commit to `master`/`develop` directly - use feature branches
|
||||
|
||||
**CI/CD:** Woodpecker checks syntax → Pint → tests → Docker build
|
||||
|
||||
---
|
||||
|
||||
## Common Query Patterns
|
||||
|
||||
```php
|
||||
// Seller viewing their products
|
||||
Product::whereHas('brand', fn($q) => $q->where('business_id', $business->id))->get();
|
||||
|
||||
// Buyer viewing their orders
|
||||
Order::where('business_id', $business->id)->get();
|
||||
|
||||
// Seller viewing incoming orders
|
||||
Order::whereHas('items.product.brand', fn($q) => $q->where('business_id', $business->id))->get();
|
||||
|
||||
// Marketplace (cross-business - intentional!)
|
||||
Product::where('is_active', true)->get(); // No business_id filter!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture Docs (Read When Needed)
|
||||
|
||||
**Custom Architecture:**
|
||||
- `.claude/DEPARTMENTS.md` - Department system, permissions, access control
|
||||
- `.claude/ROUTING.md` - Business slug routing, subdivision architecture
|
||||
- `.claude/PROCESSING.md` - Processing operations (Solventless vs BHO, conversions, wash batches)
|
||||
- `.claude/MODELS.md` - Key models, relationships, query patterns
|
||||
|
||||
**Standard Docs:**
|
||||
- `docs/URL_STRUCTURE.md` - **READ BEFORE** routing changes
|
||||
- `docs/DATABASE.md` - **READ BEFORE** migrations
|
||||
- `docs/DEVELOPMENT.md` - Local setup
|
||||
- `CONTRIBUTING.md` - Detailed git workflow
|
||||
|
||||
---
|
||||
|
||||
## What You Often Forget
|
||||
|
||||
✅ Scope by business_id BEFORE finding by ID
|
||||
✅ Use Eloquent (never raw SQL)
|
||||
✅ Protect routes with middleware
|
||||
✅ DaisyUI for buyer/seller, Filament only for admin
|
||||
✅ NO inline styles - use Tailwind/DaisyUI classes only
|
||||
✅ Run tests before committing
|
||||
|
||||
@@ -1,891 +0,0 @@
|
||||
# Claude Code Collaboration Workflow Guide
|
||||
|
||||
**Project:** Cannabrands CRM → Filament Migration
|
||||
**Timeline:** 28 days
|
||||
**Developer:** Solo developer + Claude Code
|
||||
**Last Updated:** January 2025
|
||||
|
||||
---
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
This guide provides prompt templates, reference strategies, and workflows for effectively collaborating with Claude Code during the migration from the legacy Laravel 9 + VentureDrake CRM system to the new Laravel 12 + Filament 4 platform.
|
||||
|
||||
**Key Principle:** The old codebase (`/cannabrands_crm`) contains **business logic** that must be preserved, but **architectural patterns** that should NOT be replicated.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Prompt Templates
|
||||
|
||||
### Template 1: Implementing a New Feature
|
||||
|
||||
```
|
||||
I need to implement [FEATURE NAME] from the old system.
|
||||
|
||||
**Old System Reference:**
|
||||
- Location: [file path in /cannabrands_crm]
|
||||
- Key business rules: [specific rules to preserve]
|
||||
- Data involved: [models/tables]
|
||||
|
||||
**New System Requirements:**
|
||||
- Filament resource: [Yes/No]
|
||||
- Public-facing page: [Yes/No]
|
||||
- Special considerations: [any unique requirements]
|
||||
|
||||
Please:
|
||||
1. Read the old implementation to understand the business logic
|
||||
2. Implement using Filament 4 best practices
|
||||
3. Preserve all validation rules and business logic
|
||||
4. Do NOT copy the CRM-specific patterns
|
||||
|
||||
Reference documents:
|
||||
- FEATURE_IMPLEMENTATION_ROADMAP.md (Day X task)
|
||||
- FILAMENT_RESOURCES_SPEC.md (if applicable)
|
||||
```
|
||||
|
||||
**Example Usage:**
|
||||
```
|
||||
I need to implement the buyer application approval workflow.
|
||||
|
||||
**Old System Reference:**
|
||||
- Location: vendor/venturedrake/laravel-crm/src/Http/Controllers/CompaniesController.php (lines 520-790)
|
||||
- Key business rules:
|
||||
- Requires license document upload
|
||||
- Requires W9 tax form
|
||||
- Admin approval required before account activation
|
||||
- Email notification on approval/rejection
|
||||
- Data involved: companies table, documents, email_verifications
|
||||
|
||||
**New System Requirements:**
|
||||
- Filament resource: Yes (CompanyResource)
|
||||
- Public-facing page: Yes (buyer registration form)
|
||||
- Special considerations: Must integrate with existing email verification system
|
||||
|
||||
Please:
|
||||
1. Read the old implementation to understand document validation
|
||||
2. Implement using Filament 4 actions and notifications
|
||||
3. Preserve license/W9 validation logic
|
||||
4. Do NOT copy the CRM Organization model patterns
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Template 2: Understanding Business Logic
|
||||
|
||||
```
|
||||
I need to understand how [BUSINESS PROCESS] works in the old system.
|
||||
|
||||
**Process:** [name of workflow/calculation/rule]
|
||||
**Why:** [what you're trying to implement that needs this]
|
||||
|
||||
Please:
|
||||
1. Search the old codebase for relevant files
|
||||
2. Trace the workflow from start to finish
|
||||
3. Identify all business rules, validations, and side effects
|
||||
4. Explain the logic in plain English
|
||||
5. Recommend how to implement in new system
|
||||
|
||||
Do NOT implement yet - just analyze and explain.
|
||||
```
|
||||
|
||||
**Example Usage:**
|
||||
```
|
||||
I need to understand how orders convert to invoices in the old system.
|
||||
|
||||
**Process:** Order-to-Invoice conversion workflow
|
||||
**Why:** Need to implement this in new OrderResource and InvoiceResource
|
||||
|
||||
Please:
|
||||
1. Search the old codebase for order/invoice conversion logic
|
||||
2. Trace the workflow from order creation → invoice generation
|
||||
3. Identify all status transitions and triggers
|
||||
4. Explain when/why orders become invoices
|
||||
5. Recommend how to implement in Filament with separate Order/Invoice models
|
||||
|
||||
Do NOT implement yet - just analyze and explain.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Template 3: Creating a Migration Script
|
||||
|
||||
```
|
||||
I need to migrate [DATA TYPE] from old database to new database.
|
||||
|
||||
**Old Table(s):** [table names]
|
||||
**New Table(s):** [table names]
|
||||
**Record Count:** [approximate number]
|
||||
**Special Considerations:** [foreign keys, transformations, etc.]
|
||||
|
||||
Reference:
|
||||
- SCHEMA_TRANSFORMATION.md (Section: [section name])
|
||||
|
||||
Please:
|
||||
1. Create Laravel migration file
|
||||
2. Write data migration script (using DB facade or raw SQL)
|
||||
3. Include data transformation logic from SCHEMA_TRANSFORMATION.md
|
||||
4. Add verification queries to confirm successful migration
|
||||
5. Include rollback logic
|
||||
|
||||
Test with a small subset first (LIMIT 10).
|
||||
```
|
||||
|
||||
**Example Usage:**
|
||||
```
|
||||
I need to migrate products from old database to new database.
|
||||
|
||||
**Old Table(s):** crm_products, crm_brands, products
|
||||
**New Table(s):** products, brands
|
||||
**Record Count:** 883 products
|
||||
**Special Considerations:**
|
||||
- 552 products have NULL brand_id (assign to default "Cannabrands" brand)
|
||||
- Parent-child relationships for product varieties
|
||||
- BOM flags (isAssembly, isRaw, isSellable)
|
||||
|
||||
Reference:
|
||||
- SCHEMA_TRANSFORMATION.md (Section: Products Migration)
|
||||
|
||||
Please:
|
||||
1. Create Laravel migration file
|
||||
2. Write data migration script with brand assignment logic
|
||||
3. Handle NULL brand_id → default to Cannabrands
|
||||
4. Preserve parent_product_id relationships
|
||||
5. Include COUNT verification before/after
|
||||
|
||||
Test with LIMIT 10 first, then show me results before full migration.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Template 4: Debugging Behavior Mismatch
|
||||
|
||||
```
|
||||
The new implementation doesn't match old system behavior.
|
||||
|
||||
**Feature:** [what you implemented]
|
||||
**Expected Behavior:** [what should happen, based on old system]
|
||||
**Actual Behavior:** [what's happening in new system]
|
||||
**Old System Reference:** [file/line where expected behavior exists]
|
||||
|
||||
Please:
|
||||
1. Read the old implementation carefully
|
||||
2. Identify what business logic I missed
|
||||
3. Show me the discrepancy
|
||||
4. Fix the new implementation to match
|
||||
```
|
||||
|
||||
**Example Usage:**
|
||||
```
|
||||
The new implementation doesn't match old system behavior.
|
||||
|
||||
**Feature:** Order acceptance workflow
|
||||
**Expected Behavior:** When seller creates order, buyer must accept before it can be fulfilled. When buyer creates order, seller must accept.
|
||||
**Actual Behavior:** All orders can be immediately marked as accepted regardless of who created them.
|
||||
**Old System Reference:** app/Models/Invoice.php (acceptable() method, lines 45-52)
|
||||
|
||||
Please:
|
||||
1. Read the old Invoice model's acceptable() logic
|
||||
2. Identify the created_by and status checks I missed
|
||||
3. Show me what validation rules are missing
|
||||
4. Update OrderResource to match this business rule
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Reference Strategy
|
||||
|
||||
### ✅ WHEN to Read Old Codebase
|
||||
|
||||
**1. Business Rules & Validation**
|
||||
```php
|
||||
// OLD: vendor/venturedrake/laravel-crm/src/Http/Requests/StoreCompanyRequest.php
|
||||
'license_number' => 'required|string|max:50|unique:companies',
|
||||
'license_document' => 'required|file|mimes:pdf,jpg,png|max:5120',
|
||||
|
||||
// Extract these rules for new CompanyResource
|
||||
```
|
||||
|
||||
**2. Calculations & Formulas**
|
||||
```php
|
||||
// OLD: app/Models/Invoice.php
|
||||
public function calculateTotal() {
|
||||
return $this->lines->sum(function($line) {
|
||||
return $line->quantity * $line->unit_price * (1 - $line->discount/100);
|
||||
}) + $this->shipping_cost + $this->tax_amount;
|
||||
}
|
||||
|
||||
// Preserve exact calculation logic in new Order model
|
||||
```
|
||||
|
||||
**3. Status Workflows**
|
||||
```php
|
||||
// OLD: Check status transitions and conditions
|
||||
if ($order->status == 'new' && $order->created_by === 'seller') {
|
||||
// Buyer must accept
|
||||
} elseif ($order->status == 'accepted') {
|
||||
// Can be fulfilled
|
||||
}
|
||||
|
||||
// Replicate state machine logic in new system
|
||||
```
|
||||
|
||||
**4. Email Templates & Notifications**
|
||||
```php
|
||||
// OLD: resources/views/emails/order/accepted.blade.php
|
||||
// Copy branding, copy, and structure
|
||||
// Update to use new Filament notification patterns
|
||||
```
|
||||
|
||||
**5. Complex Queries**
|
||||
```php
|
||||
// OLD: Multi-table joins, aggregations, report logic
|
||||
$products = Product::with(['brand', 'parent'])
|
||||
->where('is_active', true)
|
||||
->whereHas('brand', fn($q) => $q->where('public', true))
|
||||
->get();
|
||||
|
||||
// Preserve query structure and business logic
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ WHAT NOT to Copy
|
||||
|
||||
**1. VentureDrake Model Patterns**
|
||||
```php
|
||||
// DON'T COPY THIS:
|
||||
namespace VentureDrake\LaravelCrm\Models;
|
||||
use VentureDrake\LaravelCrm\Traits\BelongsToTeams;
|
||||
|
||||
// Instead: Use clean Laravel models
|
||||
namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
```
|
||||
|
||||
**2. CRM-Specific Features**
|
||||
```php
|
||||
// DON'T COPY: Leads, Deals, Pipelines, Campaigns
|
||||
// These are unused CRM bloat - implement only what's needed
|
||||
```
|
||||
|
||||
**3. Vendor Overrides & Hacks**
|
||||
```php
|
||||
// DON'T COPY: Direct vendor file modifications
|
||||
// Use proper Laravel extension patterns instead
|
||||
```
|
||||
|
||||
**4. Inefficient Queries**
|
||||
```php
|
||||
// DON'T COPY: N+1 queries or missing eager loading
|
||||
foreach ($orders as $order) {
|
||||
echo $order->company->name; // N+1 problem
|
||||
}
|
||||
|
||||
// Instead: Optimize with eager loading
|
||||
Order::with('company')->get();
|
||||
```
|
||||
|
||||
**5. Inline Business Logic in Controllers**
|
||||
```php
|
||||
// DON'T COPY: Fat controllers
|
||||
public function store(Request $request) {
|
||||
// 200 lines of business logic...
|
||||
}
|
||||
|
||||
// Instead: Use Services, Actions, or Model methods
|
||||
public function store(Request $request, OrderService $service) {
|
||||
$service->createOrder($request->validated());
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Development Workflow
|
||||
|
||||
### Git Strategy
|
||||
|
||||
**Branch Naming:**
|
||||
```bash
|
||||
feature/[feature-name] # New features (e.g., feature/shopping-cart)
|
||||
fix/[bug-description] # Bug fixes (e.g., fix/order-total-calculation)
|
||||
migrate/[data-type] # Data migrations (e.g., migrate/products)
|
||||
```
|
||||
|
||||
**Commit Message Pattern:**
|
||||
```
|
||||
[type]: [concise description]
|
||||
|
||||
[Optional detailed explanation]
|
||||
[Reference to old system if applicable]
|
||||
|
||||
Examples:
|
||||
✅ feat: implement buyer application approval workflow
|
||||
- Add CompanyResource approval/rejection actions
|
||||
- Preserve license validation from old CompaniesController.php:520
|
||||
- Add email notifications on status change
|
||||
|
||||
✅ fix: correct order total calculation to match legacy system
|
||||
- Include tax and shipping in total
|
||||
- Reference: app/Models/Invoice.php:calculateTotal()
|
||||
|
||||
✅ migrate: import 883 products from old database
|
||||
- Assign 552 unbranded products to default Cannabrands brand
|
||||
- Preserve parent_product_id relationships
|
||||
- Verified: All products migrated successfully
|
||||
```
|
||||
|
||||
**Daily Workflow:**
|
||||
```bash
|
||||
# Morning: Start day's feature
|
||||
git checkout -b feature/[todays-feature]
|
||||
|
||||
# During: Commit frequently
|
||||
git add .
|
||||
git commit -m "feat: [incremental progress]"
|
||||
|
||||
# End of Day: Merge if complete and tested
|
||||
git checkout feature/migration-implementation
|
||||
git merge feature/[todays-feature] --no-ff
|
||||
git branch -d feature/[todays-feature]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Testing After Each Feature
|
||||
|
||||
**1. Filament Resource Testing**
|
||||
```
|
||||
After implementing [Resource]Resource:
|
||||
|
||||
1. Visual Test:
|
||||
- Visit /admin/[resources] in browser
|
||||
- Create new record
|
||||
- Edit existing record
|
||||
- Test filters and search
|
||||
- Test custom actions
|
||||
|
||||
2. Validation Test:
|
||||
- Try submitting empty form (should fail)
|
||||
- Try invalid data (should show errors)
|
||||
- Try valid data (should succeed)
|
||||
|
||||
3. Relationship Test:
|
||||
- Test relation managers (if any)
|
||||
- Verify related records display
|
||||
- Test creating related records
|
||||
|
||||
4. Permission Test (if roles implemented):
|
||||
- Test as admin (should have full access)
|
||||
- Test as regular user (should have limited access)
|
||||
```
|
||||
|
||||
**2. Public Page Testing**
|
||||
```
|
||||
After implementing public-facing page:
|
||||
|
||||
1. Guest Test:
|
||||
- Access page without login
|
||||
- Submit form with valid data
|
||||
- Submit form with invalid data
|
||||
|
||||
2. Email Test:
|
||||
- Check Mailpit (localhost:8025)
|
||||
- Verify email content and styling
|
||||
- Test email links
|
||||
|
||||
3. Flow Test:
|
||||
- Complete full user journey (register → verify → login)
|
||||
- Check database records created
|
||||
- Verify relationships created
|
||||
```
|
||||
|
||||
**3. Migration Script Testing**
|
||||
```
|
||||
After creating migration script:
|
||||
|
||||
1. Test Run (Small Subset):
|
||||
php artisan migrate:fresh --seed
|
||||
php artisan migrate:legacy:products --limit=10
|
||||
|
||||
2. Verification Queries:
|
||||
SELECT COUNT(*) FROM products;
|
||||
SELECT * FROM products WHERE brand_id IS NULL; -- Should be 0
|
||||
SELECT * FROM products WHERE parent_product_id IS NOT NULL; -- Varieties
|
||||
|
||||
3. Rollback Test:
|
||||
php artisan migrate:rollback
|
||||
-- Verify data removed cleanly
|
||||
|
||||
4. Full Migration (After test passes):
|
||||
php artisan migrate:legacy:products
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Debugging Patterns
|
||||
|
||||
### Pattern 1: Behavior Doesn't Match Old System
|
||||
|
||||
**Symptom:** "This works, but it's not doing what the old system did"
|
||||
|
||||
**Process:**
|
||||
```
|
||||
1. Find old implementation:
|
||||
Prompt: "Search old codebase for [feature] logic in controllers, models, and services"
|
||||
|
||||
2. Trace execution:
|
||||
Prompt: "Read [old file] and explain step-by-step what happens when [action occurs]"
|
||||
|
||||
3. Compare implementations:
|
||||
Prompt: "Here's my new implementation [paste code]. Compare to old system and identify missing business logic"
|
||||
|
||||
4. Fix discrepancies:
|
||||
Prompt: "Update new implementation to include [missing rule] from old system"
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
User: "Order totals in new system don't match old invoices"
|
||||
|
||||
Claude: Let me search for total calculation in old system
|
||||
[Reads app/Models/Invoice.php]
|
||||
|
||||
Claude: I found the issue. Old system includes:
|
||||
- Line items (quantity × price × discount)
|
||||
- Shipping cost
|
||||
- Tax amount
|
||||
|
||||
Your new Order model is missing shipping and tax. Here's the fix:
|
||||
[Provides corrected calculation method]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pattern 2: Data Migration Produces Incorrect Results
|
||||
|
||||
**Symptom:** "Migration ran, but data looks wrong"
|
||||
|
||||
**Process:**
|
||||
```
|
||||
1. Run diagnostic queries:
|
||||
SELECT COUNT(*), brand_id FROM products GROUP BY brand_id;
|
||||
SELECT * FROM products WHERE [expected_field] IS NULL LIMIT 10;
|
||||
|
||||
2. Compare to old system:
|
||||
-- Run same query on old database
|
||||
SELECT COUNT(*), brand_id FROM crm_products GROUP BY brand_id;
|
||||
|
||||
3. Identify transformation error:
|
||||
Prompt: "My migration produced [X results] but old system has [Y results].
|
||||
Here's my migration script [paste]. Find the bug."
|
||||
|
||||
4. Fix and re-run:
|
||||
php artisan migrate:rollback
|
||||
[Fix migration script]
|
||||
php artisan migrate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pattern 3: Filament Action Not Working
|
||||
|
||||
**Symptom:** "Button appears but clicking does nothing / shows error"
|
||||
|
||||
**Debugging Steps:**
|
||||
```
|
||||
1. Check browser console for JS errors
|
||||
2. Check Laravel logs: tail -f storage/logs/laravel.log
|
||||
3. Verify action requirements:
|
||||
- Is user authorized?
|
||||
- Are required fields present?
|
||||
- Is model state correct?
|
||||
|
||||
Prompt: "My Filament action [action name] in [Resource] isn't working.
|
||||
Here's the code [paste]. Here's the error [paste].
|
||||
Reference old system at [file path] to see how it should work."
|
||||
```
|
||||
|
||||
**Common Fixes:**
|
||||
```php
|
||||
// Missing authorization
|
||||
Tables\Actions\Action::make('approve')
|
||||
->visible(fn ($record) => $record->status === 'pending') // Add this
|
||||
|
||||
// Missing requiresConfirmation
|
||||
Tables\Actions\Action::make('delete')
|
||||
->requiresConfirmation() // Add this for destructive actions
|
||||
|
||||
// Missing success notification
|
||||
->action(function ($record) {
|
||||
$record->approve();
|
||||
Notification::make()->success()->title('Approved')->send(); // Add this
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Example Interactions
|
||||
|
||||
### Example 1: Day 1 - Company Model Implementation
|
||||
|
||||
**User:**
|
||||
```
|
||||
Starting Day 1 tasks. I need to:
|
||||
1. Rename businesses table to companies
|
||||
2. Add buyer application fields
|
||||
3. Create CompanyResource
|
||||
|
||||
Reference:
|
||||
- FEATURE_IMPLEMENTATION_ROADMAP.md (Day 1)
|
||||
- FILAMENT_RESOURCES_SPEC.md (CompanyResource)
|
||||
- Old system: vendor/venturedrake/laravel-crm/src/Models/Organisation.php
|
||||
|
||||
Please implement step 1 (rename migration) first.
|
||||
```
|
||||
|
||||
**Claude:**
|
||||
```
|
||||
I'll create a migration to rename the businesses table to companies and add buyer application fields.
|
||||
|
||||
[Creates migration file]
|
||||
[Shows migration code]
|
||||
|
||||
This migration:
|
||||
1. Renames businesses → companies
|
||||
2. Adds license_number, license_document_path, w9_document_path
|
||||
3. Adds buyer application fields (status, approved_at, rejected_at, rejection_reason)
|
||||
4. Preserves all existing data
|
||||
|
||||
Run: php artisan migrate
|
||||
|
||||
After confirming this works, I'll proceed to step 2 (CompanyResource).
|
||||
```
|
||||
|
||||
**User:**
|
||||
```
|
||||
Migration ran successfully. Proceed with CompanyResource.
|
||||
```
|
||||
|
||||
**Claude:**
|
||||
```
|
||||
I'll create the CompanyResource with approval workflow.
|
||||
|
||||
First, let me read the old buyer application logic:
|
||||
[Reads vendor/venturedrake/.../CompaniesController.php lines 520-790]
|
||||
|
||||
I see the approval workflow requires:
|
||||
- License document validation
|
||||
- W9 document validation
|
||||
- Admin approval action
|
||||
- Email notification on approval/rejection
|
||||
|
||||
Creating CompanyResource with these features...
|
||||
[Creates resource file]
|
||||
[Shows code]
|
||||
|
||||
Test by:
|
||||
1. Visit /admin/companies
|
||||
2. Click on a pending company
|
||||
3. Click "Approve" action
|
||||
4. Check email in Mailpit (localhost:8025)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Example 2: Understanding Order Workflow
|
||||
|
||||
**User:**
|
||||
```
|
||||
I need to understand how orders convert to invoices in the old system.
|
||||
|
||||
**Process:** Order-to-Invoice conversion workflow
|
||||
**Why:** Need to implement this in new OrderResource and InvoiceResource
|
||||
|
||||
Please analyze and explain - don't implement yet.
|
||||
```
|
||||
|
||||
**Claude:**
|
||||
```
|
||||
Let me trace the order-to-invoice workflow in the old system.
|
||||
|
||||
[Searches for 'invoice' AND 'order' in old codebase]
|
||||
[Reads app/Models/Invoice.php]
|
||||
[Reads vendor/venturedrake/.../InvoiceController.php]
|
||||
|
||||
Here's how it works:
|
||||
|
||||
**Data Structure:**
|
||||
- Old system uses SINGLE table: invoices
|
||||
- Column: type (values: 'order', 'invoice', 'quote', 'draft')
|
||||
- Column: status (values: 'new', 'accepted', 'create_manifest', 'invoiced', 'delivered')
|
||||
|
||||
**Workflow:**
|
||||
1. Order Created:
|
||||
- type = 'order'
|
||||
- status = 'new'
|
||||
- created_by = 'seller' OR 'buyer'
|
||||
|
||||
2. Acceptance:
|
||||
- If created_by = 'seller' → buyer must accept
|
||||
- If created_by = 'buyer' → seller must accept
|
||||
- Status changes: 'new' → 'accepted'
|
||||
|
||||
3. Fulfillment Prep:
|
||||
- Admin marks order for fulfillment
|
||||
- Status changes: 'accepted' → 'create_manifest'
|
||||
|
||||
4. **CONVERSION TRIGGER** (InvoiceController.php:434):
|
||||
- When status = 'create_manifest'
|
||||
- Type changes: 'order' → 'invoice'
|
||||
- Status changes: 'create_manifest' → 'invoiced'
|
||||
|
||||
5. Delivery:
|
||||
- Status changes: 'invoiced' → 'delivered'
|
||||
- Payment tracking begins
|
||||
|
||||
**Recommendation for New System:**
|
||||
- Use separate Order and Invoice models/tables
|
||||
- When Order.status = 'fulfilled', create Invoice record
|
||||
- Copy order data to invoice (preserve order_id foreign key)
|
||||
- Keep Order record for history (don't delete)
|
||||
|
||||
This gives you:
|
||||
- Cleaner separation of concerns
|
||||
- Better audit trail
|
||||
- Easier reporting
|
||||
|
||||
Would you like me to implement this workflow now?
|
||||
```
|
||||
|
||||
**User:**
|
||||
```
|
||||
Yes, implement this in OrderResource and InvoiceResource.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Example 3: Debugging Migration Discrepancy
|
||||
|
||||
**User:**
|
||||
```
|
||||
I ran the products migration but the counts are wrong.
|
||||
|
||||
Expected (from old system): 883 products
|
||||
Actual (new system): 1435 products
|
||||
|
||||
Here's my migration script: [pastes script]
|
||||
```
|
||||
|
||||
**Claude:**
|
||||
```
|
||||
Let me investigate. First, let me check if the old system has soft deletes:
|
||||
|
||||
[Reads old products table schema]
|
||||
|
||||
Found the issue! The old crm_products table has a deleted_at column.
|
||||
Your migration script is copying ALL records, including soft-deleted ones.
|
||||
|
||||
Old system:
|
||||
- Total records: 1435
|
||||
- Active (deleted_at IS NULL): 883
|
||||
- Soft-deleted: 552
|
||||
|
||||
Fix:
|
||||
[Shows updated migration script with WHERE deleted_at IS NULL]
|
||||
|
||||
Run this to fix:
|
||||
php artisan migrate:rollback
|
||||
php artisan migrate
|
||||
php artisan migrate:legacy:products
|
||||
|
||||
You should now see exactly 883 products.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Daily Checklist
|
||||
|
||||
**Every Morning:**
|
||||
```
|
||||
[ ] Review FEATURE_IMPLEMENTATION_ROADMAP.md for today's tasks
|
||||
[ ] Create feature branch: git checkout -b feature/[todays-feature]
|
||||
[ ] Identify which old files contain business logic for today's feature
|
||||
```
|
||||
|
||||
**During Implementation:**
|
||||
```
|
||||
[ ] Read old implementation first (understand before coding)
|
||||
[ ] Extract business rules (validations, calculations, workflows)
|
||||
[ ] Implement in Filament/Laravel best practices
|
||||
[ ] Do NOT copy CRM-specific patterns
|
||||
[ ] Commit incrementally (every 1-2 hours)
|
||||
```
|
||||
|
||||
**Before Marking Task Complete:**
|
||||
```
|
||||
[ ] Visual test in browser (if applicable)
|
||||
[ ] Validation test (try to break it)
|
||||
[ ] Check Laravel logs for errors
|
||||
[ ] Run relevant test suite (if tests exist)
|
||||
[ ] Commit final changes with descriptive message
|
||||
```
|
||||
|
||||
**End of Day:**
|
||||
```
|
||||
[ ] Merge feature branch if complete and tested
|
||||
[ ] Update progress in FEATURE_IMPLEMENTATION_ROADMAP.md (add checkmarks)
|
||||
[ ] Note any blockers or questions for tomorrow
|
||||
[ ] Push to remote: git push origin feature/migration-implementation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 When to Ask for Clarification
|
||||
|
||||
**ASK when:**
|
||||
- Old system has conflicting business rules in different files
|
||||
- Data migration produces unexpected results
|
||||
- You're unsure which old implementation is the "source of truth"
|
||||
- Performance implications of copying old approach (e.g., N+1 queries)
|
||||
- Security concerns with old implementation
|
||||
|
||||
**DON'T ASK when:**
|
||||
- You can find answer in old codebase (read it first)
|
||||
- It's covered in migration documents (SCHEMA_TRANSFORMATION.md, etc.)
|
||||
- It's a standard Laravel/Filament pattern (use best practices)
|
||||
- Old implementation is clearly wrong (use correct approach in new system)
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Common Pitfalls & Solutions
|
||||
|
||||
### Pitfall 1: Copying CRM Model Structure
|
||||
```
|
||||
❌ WRONG:
|
||||
namespace App\Models;
|
||||
use VentureDrake\LaravelCrm\Traits\BelongsToTeams;
|
||||
class Company extends Model {
|
||||
use BelongsToTeams; // Don't copy CRM traits
|
||||
}
|
||||
|
||||
✅ RIGHT:
|
||||
namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
class Company extends Model {
|
||||
// Clean Laravel model
|
||||
}
|
||||
```
|
||||
|
||||
### Pitfall 2: Assuming Table Names Match
|
||||
```
|
||||
❌ WRONG:
|
||||
Assuming old "organisations" = new "companies" directly
|
||||
|
||||
✅ RIGHT:
|
||||
Check SCHEMA_TRANSFORMATION.md for exact table mappings
|
||||
Old: companies (CRM Contact companies) + organisations (CRM Orgs)
|
||||
New: companies (unified, with type field)
|
||||
```
|
||||
|
||||
### Pitfall 3: Missing Business Rules
|
||||
```
|
||||
❌ WRONG:
|
||||
Creating generic CRUD without checking old validations
|
||||
|
||||
✅ RIGHT:
|
||||
Read old Request classes for validation rules:
|
||||
- StoreCompanyRequest.php
|
||||
- UpdateProductRequest.php
|
||||
Extract rules and apply to Filament resource
|
||||
```
|
||||
|
||||
### Pitfall 4: Over-Engineering Too Soon
|
||||
```
|
||||
❌ WRONG:
|
||||
Implementing features that don't exist in old system
|
||||
"Should I add multi-currency support?"
|
||||
|
||||
✅ RIGHT:
|
||||
Implement only what old system has (feature parity first)
|
||||
Note ideas for post-launch improvements
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Quick Reference
|
||||
|
||||
**Old Codebase Locations:**
|
||||
```
|
||||
Business Logic: app/Models/*.php
|
||||
Controllers: app/Http/Controllers/**/*.php
|
||||
Validations: app/Http/Requests/*.php
|
||||
CRM Controllers: vendor/venturedrake/.../Controllers/*.php
|
||||
CRM Models: vendor/venturedrake/.../Models/*.php
|
||||
Email Templates: resources/views/emails/**/*.blade.php
|
||||
Routes: routes/*.php
|
||||
```
|
||||
|
||||
**New Codebase Locations:**
|
||||
```
|
||||
Models: app/Models/*.php
|
||||
Filament Resources: app/Filament/Resources/*.php
|
||||
Services: app/Services/*.php
|
||||
Migrations: database/migrations/*.php
|
||||
Seeders: database/seeders/*.php
|
||||
Public Pages: app/Http/Controllers/**/*.php
|
||||
Views: resources/views/**/*.blade.php
|
||||
```
|
||||
|
||||
**Key Documents:**
|
||||
```
|
||||
Migration Strategy: MIGRATION_MASTER_PLAN.md
|
||||
Data Mappings: SCHEMA_TRANSFORMATION.md
|
||||
Day-by-Day Tasks: FEATURE_IMPLEMENTATION_ROADMAP.md
|
||||
Filament Code: FILAMENT_RESOURCES_SPEC.md
|
||||
This Guide: CLAUDE_COLLABORATION_WORKFLOW.md
|
||||
```
|
||||
|
||||
**Useful Commands:**
|
||||
```bash
|
||||
# Development
|
||||
php artisan serve
|
||||
php artisan migrate
|
||||
php artisan migrate:fresh --seed
|
||||
|
||||
# Testing
|
||||
php artisan tinker
|
||||
php artisan route:list
|
||||
tail -f storage/logs/laravel.log
|
||||
|
||||
# Email Testing
|
||||
docker-compose up mailpit -d
|
||||
# Visit: localhost:8025
|
||||
|
||||
# Database
|
||||
php artisan db:seed
|
||||
php artisan migrate:rollback
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Success Indicators
|
||||
|
||||
**You're on the right track when:**
|
||||
- New feature behavior matches old system exactly
|
||||
- Migration scripts produce expected record counts
|
||||
- Filament resources are cleaner than old CRM code
|
||||
- Business rules are preserved, but implementation is modern
|
||||
- You can explain why new approach is better than old
|
||||
|
||||
**Red flags:**
|
||||
- Copying vendor file structures
|
||||
- Implementing unused CRM features
|
||||
- Can't explain business rule origin
|
||||
- Migration counts don't match old system
|
||||
- New feature does something old system didn't
|
||||
|
||||
---
|
||||
|
||||
**End of Guide**
|
||||
|
||||
*This document evolves as you progress through the migration. Update it with new patterns, pitfalls, and solutions as you discover them.*
|
||||
266
CONTRIBUTING.md
266
CONTRIBUTING.md
@@ -44,7 +44,7 @@ Our workflow provides audit trails regulators love:
|
||||
|
||||
1. **Clone the repository**
|
||||
```bash
|
||||
git clone https://code.cannabrands.com/Cannabrands/hub.git
|
||||
git clone https://code.cannabrands.app/Cannabrands/hub.git
|
||||
cd hub
|
||||
```
|
||||
|
||||
@@ -68,21 +68,59 @@ Our workflow provides audit trails regulators love:
|
||||
|
||||
---
|
||||
|
||||
## Branch Protection & Pull Request Workflow
|
||||
|
||||
**IMPORTANT:** The `develop` and `master` branches are **protected** - you cannot push directly to them.
|
||||
|
||||
### Standard Workflow:
|
||||
|
||||
```bash
|
||||
# 1. Create a feature branch
|
||||
git checkout -b feature/my-feature-name
|
||||
|
||||
# 2. Make changes and commit
|
||||
git add .
|
||||
git commit -m "feat: add new feature"
|
||||
|
||||
# 3. Push to your feature branch
|
||||
git push origin feature/my-feature-name
|
||||
|
||||
# 4. Create Pull Request on Gitea
|
||||
# - Navigate to https://code.cannabrands.app
|
||||
# - Create PR to merge your branch into develop
|
||||
# - CI will run automatically
|
||||
# - Request review from team
|
||||
|
||||
# 5. After approval and passing CI
|
||||
# - Merge PR via Gitea interface
|
||||
# - Delete feature branch
|
||||
```
|
||||
|
||||
### Branch Naming Conventions:
|
||||
|
||||
- `feature/` - New features (e.g., `feature/bulk-import`)
|
||||
- `fix/` - Bug fixes (e.g., `fix/tax-calculation`)
|
||||
- `chore/` - Maintenance tasks (e.g., `chore/upgrade-php`)
|
||||
- `docs/` - Documentation changes (e.g., `docs/update-readme`)
|
||||
|
||||
---
|
||||
|
||||
## Real-World Team Scenarios
|
||||
|
||||
### Scenario 1: Normal Feature Development
|
||||
**Developer Jon adds bulk import feature**
|
||||
|
||||
```bash
|
||||
$ git checkout -b feature/bulk-import # Create feature branch
|
||||
$ vim app/Orders.php # Make changes
|
||||
$ git add .
|
||||
$ git commit -m "feat(orders): add bulk import"
|
||||
🎨 Pre-commit: Pint formats code (1s) ✅
|
||||
|
||||
$ git push origin master
|
||||
$ git push origin feature/bulk-import
|
||||
🧪 Pre-push: Tests run (30s) ✅
|
||||
✅ All tests passed! Pushing...
|
||||
🚀 CI: Full verification (5min) ✅
|
||||
🚀 Create PR → merge to develop → CI verifies (5min) ✅
|
||||
```
|
||||
|
||||
**Time cost: 31 seconds** (vs 5+ minutes if tests failed in CI)
|
||||
@@ -122,14 +160,15 @@ $ git push --no-verify # Skip tests intentionally
|
||||
**Developer Emma fixes production bug**
|
||||
|
||||
```bash
|
||||
$ vim app/Invoice.php # Critical bug fix
|
||||
$ git checkout -b fix/tax-calculation # Create hotfix branch
|
||||
$ vim app/Invoice.php # Critical bug fix
|
||||
$ git commit -m "fix(invoices): correct tax calculation"
|
||||
🎨 Pre-commit: Formats ✅
|
||||
|
||||
$ git push origin master
|
||||
$ git push origin fix/tax-calculation
|
||||
🧪 Pre-push: Tests run (30s) ✅
|
||||
🚀 CI: Passes (5min) ✅
|
||||
📦 Deploy: Safe to release ✅
|
||||
🚀 Create PR → fast-track review → merge to develop ✅
|
||||
📦 CI: Passes (5min) → Safe to release ✅
|
||||
```
|
||||
|
||||
**Safety: Tests caught regression** before it reached production
|
||||
@@ -138,7 +177,8 @@ $ git push origin master
|
||||
**Developer Alex updates dependencies**
|
||||
|
||||
```bash
|
||||
$ vim Dockerfile # Update PHP version
|
||||
$ git checkout -b chore/php-8.3-upgrade # Create branch
|
||||
$ vim Dockerfile # Update PHP version
|
||||
|
||||
# Test locally FIRST (best practice)
|
||||
$ docker build -t cannabrands:test .
|
||||
@@ -146,8 +186,8 @@ $ docker build -t cannabrands:test .
|
||||
|
||||
# Then push
|
||||
$ git commit -m "chore: upgrade PHP to 8.3"
|
||||
$ git push origin master
|
||||
🚀 CI: Rebuilds (8min) ✅
|
||||
$ git push origin chore/php-8.3-upgrade
|
||||
🚀 Create PR → CI rebuilds (8min) ✅
|
||||
```
|
||||
|
||||
**Time saved: 5 minutes** by catching Docker issues locally
|
||||
@@ -170,20 +210,27 @@ Layer 3: CI (REQUIRED) → Final verification (~5 minutes)
|
||||
|
||||
**For most changes:**
|
||||
```bash
|
||||
# 1. Make your changes
|
||||
# 1. Create feature branch
|
||||
git checkout -b feature/my-feature
|
||||
|
||||
# 2. Make your changes
|
||||
vim app/SomeFile.php
|
||||
|
||||
# 2. Commit (formatting happens automatically)
|
||||
# 3. Commit (formatting happens automatically)
|
||||
git add .
|
||||
git commit -m "feat(scope): description"
|
||||
→ Pre-commit runs Laravel Pint ✅
|
||||
→ Code formatted automatically ✅
|
||||
|
||||
# 3. Push (tests run automatically)
|
||||
git push origin master
|
||||
# 4. Push (tests run automatically)
|
||||
git push origin feature/my-feature
|
||||
→ Pre-push runs tests (30 seconds) ✅
|
||||
→ If tests pass, push continues ✅
|
||||
|
||||
# 5. Create Pull Request
|
||||
→ Open PR on Gitea to merge into develop
|
||||
→ CI verifies everything (5 minutes) ✅
|
||||
→ After review, merge PR
|
||||
```
|
||||
|
||||
**For quick documentation changes:**
|
||||
@@ -192,6 +239,163 @@ git push origin master
|
||||
git push --no-verify
|
||||
```
|
||||
|
||||
### Keeping Your Feature Branch Up-to-Date
|
||||
|
||||
**Best practice for teams:** Sync your feature branch with `develop` regularly to avoid large merge conflicts.
|
||||
|
||||
#### Daily Start-of-Work Routine
|
||||
|
||||
```bash
|
||||
# 1. Get latest changes from develop
|
||||
git checkout develop
|
||||
git pull origin develop
|
||||
|
||||
# 2. Update your feature branch
|
||||
git checkout feature/my-feature
|
||||
git merge develop
|
||||
|
||||
# 3. If there are conflicts (see below), resolve them
|
||||
# 4. Continue working
|
||||
```
|
||||
|
||||
**How often?**
|
||||
- Minimum: Once per day (start of work)
|
||||
- Better: Multiple times per day if develop is active
|
||||
- Always: Before creating your Pull Request
|
||||
|
||||
#### Merge vs Rebase: Which to Use?
|
||||
|
||||
**For teams of 5+ developers, use `merge` (not `rebase`):**
|
||||
|
||||
```bash
|
||||
git checkout feature/my-feature
|
||||
git merge develop
|
||||
```
|
||||
|
||||
**Why merge over rebase?**
|
||||
- ✅ Safer: Preserves your commit history
|
||||
- ✅ Collaborative: Works when multiple people work on the same feature branch
|
||||
- ✅ Transparent: Shows when you integrated upstream changes
|
||||
- ✅ No force-push: Once you've pushed to origin, merge won't require `--force`
|
||||
|
||||
**When to use rebase:**
|
||||
- ⚠️ Only if you haven't pushed yet
|
||||
- ⚠️ Only if you're the sole developer on the branch
|
||||
- ⚠️ You want a cleaner, linear history
|
||||
|
||||
```bash
|
||||
# Only do this if you haven't pushed yet!
|
||||
git checkout feature/my-feature
|
||||
git rebase develop
|
||||
```
|
||||
|
||||
**Never rebase after pushing** - it rewrites history and breaks collaboration.
|
||||
|
||||
#### Handling Merge Conflicts
|
||||
|
||||
When you run `git merge develop` and see conflicts:
|
||||
|
||||
```bash
|
||||
$ git merge develop
|
||||
Auto-merging app/Http/Controllers/OrderController.php
|
||||
CONFLICT (content): Merge conflict in app/Http/Controllers/OrderController.php
|
||||
Automatic merge failed; fix conflicts and then commit the result.
|
||||
```
|
||||
|
||||
**Step-by-step resolution:**
|
||||
|
||||
1. **See which files have conflicts:**
|
||||
```bash
|
||||
git status
|
||||
# Look for "both modified:" files
|
||||
```
|
||||
|
||||
2. **Open conflicted files** - look for conflict markers:
|
||||
```php
|
||||
<<<<<<< HEAD
|
||||
// Your code
|
||||
=======
|
||||
// Code from develop
|
||||
>>>>>>> develop
|
||||
```
|
||||
|
||||
3. **Resolve conflicts** - edit the file to keep what you need:
|
||||
```php
|
||||
// Choose your code, their code, or combine both
|
||||
// Remove the <<<, ===, >>> markers
|
||||
```
|
||||
|
||||
4. **Mark as resolved:**
|
||||
```bash
|
||||
git add app/Http/Controllers/OrderController.php
|
||||
```
|
||||
|
||||
5. **Complete the merge:**
|
||||
```bash
|
||||
git commit -m "merge: resolve conflicts with develop"
|
||||
```
|
||||
|
||||
6. **Run tests to ensure nothing broke:**
|
||||
```bash
|
||||
./vendor/bin/sail artisan test
|
||||
```
|
||||
|
||||
7. **Push the merge commit:**
|
||||
```bash
|
||||
git push origin feature/my-feature
|
||||
```
|
||||
|
||||
#### When Conflicts Are Too Complex
|
||||
|
||||
If conflicts are extensive or you're unsure:
|
||||
|
||||
1. **Abort the merge:**
|
||||
```bash
|
||||
git merge --abort
|
||||
```
|
||||
|
||||
2. **Ask for help** in #engineering Slack:
|
||||
- "I'm merging develop into feature/X and have conflicts in OrderController"
|
||||
- Someone might have context on the upstream changes
|
||||
|
||||
3. **Pair program the resolution** - screen share with the person who made the conflicting changes
|
||||
|
||||
4. **Alternative: Start fresh** (last resort):
|
||||
```bash
|
||||
# Create new branch from latest develop
|
||||
git checkout develop
|
||||
git pull origin develop
|
||||
git checkout -b feature/my-feature-v2
|
||||
|
||||
# Cherry-pick your commits
|
||||
git cherry-pick <commit-hash>
|
||||
```
|
||||
|
||||
#### Example: Multi-Day Feature Work
|
||||
|
||||
```bash
|
||||
# Monday morning
|
||||
git checkout develop && git pull origin develop
|
||||
git checkout feature/payment-integration
|
||||
git merge develop # Get latest changes
|
||||
# Work all day, make commits
|
||||
|
||||
# Tuesday morning
|
||||
git checkout develop && git pull origin develop
|
||||
git checkout feature/payment-integration
|
||||
git merge develop # Sync again (someone added auth changes)
|
||||
# Continue working
|
||||
|
||||
# Wednesday
|
||||
git checkout develop && git pull origin develop
|
||||
git checkout feature/payment-integration
|
||||
git merge develop # Final sync before PR
|
||||
git push origin feature/payment-integration
|
||||
# Create Pull Request
|
||||
```
|
||||
|
||||
**Result:** Small, manageable syncs instead of one huge conflict on PR day.
|
||||
|
||||
### When to Test Locally
|
||||
|
||||
**Always run tests before pushing if you:**
|
||||
@@ -251,7 +455,7 @@ git commit -m "chore: upgrade Laravel to 11.x"
|
||||
## When CI Fails
|
||||
|
||||
### Step 1: Check What Failed
|
||||
Visit: `https://ci.cannabrands.com/repos/1`
|
||||
Visit: `https://ci.cannabrands.app/repos/1`
|
||||
|
||||
### Step 2: Reproduce Locally
|
||||
```bash
|
||||
@@ -276,7 +480,7 @@ vim app/SomeFile.php
|
||||
# Push fix
|
||||
git add .
|
||||
git commit -m "fix: resolve test failure"
|
||||
git push origin master
|
||||
git push origin feature/my-feature # Push to your feature branch
|
||||
```
|
||||
|
||||
---
|
||||
@@ -304,9 +508,9 @@ git commit --no-verify # Skip formatting (fix in next commit)
|
||||
|
||||
**❌ Skipping because tests fail** → Fix the tests instead
|
||||
**❌ Skipping to avoid formatting** → Let Pint format it
|
||||
**❌ Skipping on master before deploy** → CI will block you anyway
|
||||
**❌ Skipping to merge PR to develop/master** → CI will block you anyway
|
||||
|
||||
**Remember:** CI can't be bypassed, so issues will be caught before production.
|
||||
**Remember:** CI can't be bypassed, and develop/master are protected branches requiring PRs and passing CI.
|
||||
|
||||
---
|
||||
|
||||
@@ -321,7 +525,7 @@ git config core.hooksPath .githooks
|
||||
|
||||
### How it works:
|
||||
```bash
|
||||
$ git push origin master
|
||||
$ git push origin feature/my-feature
|
||||
|
||||
🧪 Running tests before push...
|
||||
(Use 'git push --no-verify' to skip)
|
||||
@@ -347,8 +551,8 @@ docker build -t cannabrands:test .
|
||||
# If successful, test run it
|
||||
docker run --rm cannabrands:test php -v
|
||||
|
||||
# Then push
|
||||
git push origin master
|
||||
# Then push to feature branch
|
||||
git push origin feature/my-feature
|
||||
```
|
||||
|
||||
**Why?** Docker builds take 5-10 minutes in CI vs 2-3 minutes locally.
|
||||
@@ -383,17 +587,23 @@ If you're responsible for creating releases, see:
|
||||
# 1. Determine version (CalVer: YYYY.MM.MICRO)
|
||||
git tag -l "2025.11.*" | sort -V | tail -1 # Check latest
|
||||
|
||||
# 2. Create release tag
|
||||
# 2. Ensure you're on master and up-to-date
|
||||
git checkout master
|
||||
git pull origin master
|
||||
|
||||
# 3. Create release tag on master
|
||||
git tag -a 2025.11.1 -m "Release notes here"
|
||||
git push origin 2025.11.1
|
||||
|
||||
# 3. CI builds production image automatically
|
||||
# 4. CI builds production image automatically
|
||||
|
||||
# 4. Generate changelog
|
||||
# 5. Generate changelog (create PR for this)
|
||||
git checkout -b chore/changelog-2025.11.1
|
||||
npm run changelog
|
||||
git add CHANGELOG.md
|
||||
git commit -m "docs: update changelog for 2025.11.1"
|
||||
git push origin master
|
||||
git push origin chore/changelog-2025.11.1
|
||||
# Create PR to merge into master
|
||||
```
|
||||
|
||||
---
|
||||
@@ -419,8 +629,8 @@ git push origin master
|
||||
- Pair program for complex changes
|
||||
|
||||
### Services
|
||||
- **Woodpecker CI:** `https://ci.cannabrands.com`
|
||||
- **Gitea:** `https://code.cannabrands.com`
|
||||
- **Woodpecker CI:** `https://ci.cannabrands.app`
|
||||
- **Gitea:** `https://code.cannabrands.app`
|
||||
- **Production:** `https://app.cannabrands.com` (future)
|
||||
|
||||
---
|
||||
@@ -455,7 +665,7 @@ Trust the process, and the process will catch your mistakes before they reach pr
|
||||
|
||||
Consider adding:
|
||||
- Code review requirement for certain files
|
||||
- Protected branches (master requires PR)
|
||||
- ✅ Protected branches already in place (develop/master require PRs)
|
||||
- Mandatory tests on pre-push (harder to skip)
|
||||
|
||||
### If You Reach 50+ developers
|
||||
|
||||
36
Dockerfile
36
Dockerfile
@@ -7,6 +7,18 @@ FROM node:22-alpine AS node-builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Accept Vite environment variables as build arguments
|
||||
ARG VITE_REVERB_APP_KEY
|
||||
ARG VITE_REVERB_HOST
|
||||
ARG VITE_REVERB_PORT=443
|
||||
ARG VITE_REVERB_SCHEME=https
|
||||
|
||||
# Export as environment variables for Vite build
|
||||
ENV VITE_REVERB_APP_KEY=${VITE_REVERB_APP_KEY}
|
||||
ENV VITE_REVERB_HOST=${VITE_REVERB_HOST}
|
||||
ENV VITE_REVERB_PORT=${VITE_REVERB_PORT}
|
||||
ENV VITE_REVERB_SCHEME=${VITE_REVERB_SCHEME}
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
@@ -18,17 +30,22 @@ COPY resources ./resources
|
||||
COPY vite.config.js tailwind.config.js ./
|
||||
COPY public ./public
|
||||
|
||||
# Build frontend assets
|
||||
# Build frontend assets (Vite will inline VITE_* env vars)
|
||||
RUN npm run build
|
||||
|
||||
# ==================== Stage 2: Composer Builder ====================
|
||||
FROM composer:2 AS composer-builder
|
||||
# Pin to PHP 8.4 - composer:2 uses latest PHP which may not be supported by dependencies yet
|
||||
FROM php:8.4-cli-alpine AS composer-builder
|
||||
|
||||
# Install Composer
|
||||
COPY --from=composer:2.8 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install required PHP extensions for Filament
|
||||
RUN apk add --no-cache icu-dev \
|
||||
&& docker-php-ext-install intl
|
||||
# Install required PHP extensions for Filament and Horizon
|
||||
RUN apk add --no-cache icu-dev libpng-dev libjpeg-turbo-dev freetype-dev libzip-dev \
|
||||
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||
&& docker-php-ext-install intl gd pcntl zip
|
||||
|
||||
# Copy composer files
|
||||
COPY composer.json composer.lock ./
|
||||
@@ -56,6 +73,8 @@ RUN apk add --no-cache \
|
||||
libjpeg-turbo-dev \
|
||||
freetype-dev \
|
||||
libzip-dev \
|
||||
icu-dev \
|
||||
icu-data-full \
|
||||
zip \
|
||||
unzip \
|
||||
git \
|
||||
@@ -75,6 +94,7 @@ RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||
pgsql \
|
||||
gd \
|
||||
zip \
|
||||
intl \
|
||||
pcntl \
|
||||
bcmath \
|
||||
opcache
|
||||
@@ -100,10 +120,8 @@ COPY --from=node-builder --chown=www-data:www-data /app/public/build ./public/bu
|
||||
# Copy vendor from composer-builder
|
||||
COPY --from=composer-builder --chown=www-data:www-data /app/vendor ./vendor
|
||||
|
||||
# Publish vendor assets at build time (Laravel + Filament best practice)
|
||||
# This bakes static assets into the Docker image for immutable deployments
|
||||
RUN php artisan vendor:publish --tag=public --force --ansi \
|
||||
&& php artisan filament:assets --ansi
|
||||
# Note: Asset publishing runs in init container at runtime (not build time)
|
||||
# Artisan commands require environment variables which aren't available during build
|
||||
|
||||
# Create version metadata file
|
||||
RUN echo "VERSION=${APP_VERSION}" > /var/www/html/version.env && \
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,716 +0,0 @@
|
||||
# 🚀 Cannabrands CRM → New Platform Migration Master Plan
|
||||
|
||||
**Project**: Migration from Laravel 9 + VentureDrake CRM to Laravel 12 + Filament 4
|
||||
**Timeline**: 28 days (End of Month Launch)
|
||||
**Scope**: Full data migration with feature parity for core commerce features
|
||||
**Risk Level**: Medium (greenfield rebuild, but parallel operation possible)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Executive Summary
|
||||
|
||||
### Current State
|
||||
- **Old System**: Laravel 9, VentureDrake CRM, MySQL, ~1000 SKUs, 20 users, 5 brands
|
||||
- **Problem**: Vendor file modifications (100+ commits), blocked Laravel upgrades, maintenance nightmare
|
||||
- **Data**: 1+ year operational history, ~1000 products, 20 active users, ongoing orders
|
||||
|
||||
### Target State
|
||||
- **New System**: Laravel 12, Filament 4, PostgreSQL, clean architecture
|
||||
- **Architecture**: License → Company → Brands → Stores (LeafLink model)
|
||||
- **Launch Date**: End of month (~28 days)
|
||||
- **Cannabrands Brands**: Doobz, Thunderbud, High Expectations, Hash Factory, Twisties
|
||||
|
||||
### Migration Strategy
|
||||
**Two-track parallel development:**
|
||||
1. **New Platform Development**: Build features in new codebase (weeks 1-3)
|
||||
2. **Data Migration**: Import existing data from old system (week 4)
|
||||
3. **Parallel Operation**: Old system stays live until cutover
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Business Goals
|
||||
|
||||
### Primary Objectives (Launch Blockers)
|
||||
1. ✅ **Product Catalog**: 1000 SKUs with strains, lab results, varieties
|
||||
2. ✅ **Shopping Cart + Checkout**: Buyer portal commerce flow
|
||||
3. ✅ **Order Management**: Order lifecycle, statuses, fulfillment
|
||||
4. ✅ **Invoice Generation**: Invoice creation + payment tracking
|
||||
5. ✅ **Buyer Application**: Company signup with compliance approval
|
||||
6. ✅ **Company/Location Management**: Multi-location buyers
|
||||
7. ✅ **Component/BOM System**: Build SKUs from components
|
||||
|
||||
### Post-Launch Features (Deferred)
|
||||
- ⏸️ Raffles system
|
||||
- ⏸️ Sample requests
|
||||
- ⏸️ Feedback system
|
||||
|
||||
### Success Criteria
|
||||
- All 1000 SKUs migrated and searchable
|
||||
- All 20 users can log in with existing credentials
|
||||
- Historical orders visible and accessible
|
||||
- New orders can be placed and fulfilled
|
||||
- Invoices generate correctly
|
||||
- Zero data loss
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architecture Transformation
|
||||
|
||||
### Old System Architecture
|
||||
```
|
||||
User (Laravel Auth)
|
||||
└─> CrmContact (company_contacts)
|
||||
└─> CrmCompany (companies) - BUYER
|
||||
|
||||
Cannabrands (hardcoded seller)
|
||||
└─> CrmProduct (products)
|
||||
└─> CrmInvoice (type='order')
|
||||
```
|
||||
|
||||
### New System Architecture
|
||||
```
|
||||
Company (License Holder)
|
||||
├─> type: 'seller' (Cannabrands)
|
||||
│ └─> Brands (Stores)
|
||||
│ ├─> Doobz
|
||||
│ ├─> Thunderbud
|
||||
│ ├─> High Expectations
|
||||
│ ├─> Hash Factory
|
||||
│ └─> Twisties
|
||||
│ └─> Products (SKUs)
|
||||
│ └─> Components (BOM)
|
||||
│
|
||||
└─> type: 'buyer' (Dispensaries)
|
||||
├─> Locations (delivery addresses)
|
||||
└─> Contacts (users)
|
||||
└─> Orders → Invoices
|
||||
```
|
||||
|
||||
### Key Changes
|
||||
1. **Rename**: `businesses` table → `companies` table (better terminology)
|
||||
2. **Multi-brand**: Cannabrands operates 5 brands (stores) under one license
|
||||
3. **Separation**: Products belong to Brands, not Companies
|
||||
4. **Dual-purpose removed**: Split `crm_invoices` (type='order'|'invoice') into separate tables
|
||||
5. **Clean schema**: No VentureDrake CRM bloat (70% unused tables eliminated)
|
||||
|
||||
---
|
||||
|
||||
## 📅 4-Week Implementation Timeline
|
||||
|
||||
### Week 1: Foundation & Core Models (Days 1-7)
|
||||
**Goal**: Database schema + authentication ready
|
||||
|
||||
#### Days 1-2: Schema Refactoring
|
||||
- [ ] Rename `businesses` → `companies` (migration + models)
|
||||
- [ ] Create `brands` table and Brand model
|
||||
- [ ] Create `products` table (clean, no CRM dependency)
|
||||
- [ ] Create `components` table (BOM system)
|
||||
- [ ] Create `orders` table (separate from invoices)
|
||||
- [ ] Create `order_items` table (line items)
|
||||
- [ ] Update all relationships and foreign keys
|
||||
|
||||
#### Days 3-4: Authentication & Users
|
||||
- [ ] User migration strategy (preserve passwords)
|
||||
- [ ] Spatie Permission roles: admin, company-owner, company-user, buyer
|
||||
- [ ] Contact model (user relationships)
|
||||
- [ ] Location model (delivery addresses)
|
||||
- [ ] License model (compliance tracking)
|
||||
|
||||
#### Days 5-7: Filament Admin Panel Setup
|
||||
- [ ] CompanyResource (CRUD for license holders)
|
||||
- [ ] BrandResource (store management for sellers)
|
||||
- [ ] UserResource (user management)
|
||||
- [ ] Dashboard widgets (overview stats)
|
||||
- [ ] Navigation structure
|
||||
|
||||
**Milestone 1 Deliverable**: Can create companies, brands, users in Filament
|
||||
|
||||
---
|
||||
|
||||
### Week 2: Product Catalog & BOM (Days 8-14)
|
||||
**Goal**: 1000 SKUs ready to migrate
|
||||
|
||||
#### Days 8-10: Product System
|
||||
- [ ] ProductResource in Filament
|
||||
- Form: name, SKU, description, price, strain, lab
|
||||
- Table: searchable, filterable by brand
|
||||
- RelationManager: ProductImages, ProductPrices
|
||||
- [ ] StrainResource (cannabis strains)
|
||||
- [ ] LabResource (lab test results - THC/CBD)
|
||||
- [ ] ProductCategoryResource (taxonomy)
|
||||
- [ ] Product varieties system (parent-child products)
|
||||
|
||||
#### Days 11-12: Component/BOM System
|
||||
- [ ] ComponentResource (raw materials)
|
||||
- [ ] Product → Components relationship (junction table)
|
||||
- [ ] BOM calculator (cost calculation)
|
||||
- [ ] Inventory tracking (if needed)
|
||||
|
||||
#### Days 13-14: Product Import Preparation
|
||||
- [ ] Extract old product data structure
|
||||
- [ ] Create product mapping script (old SKU → new product)
|
||||
- [ ] Create component extraction script
|
||||
- [ ] Build product seeder from old database
|
||||
- [ ] Test import with 50 sample products
|
||||
|
||||
**Milestone 2 Deliverable**: Can create/manage products with components
|
||||
|
||||
---
|
||||
|
||||
### Week 3: Commerce & Orders (Days 15-21)
|
||||
**Goal**: Full buyer purchasing flow working
|
||||
|
||||
#### Days 15-16: Shopping Cart (Buyer Portal)
|
||||
- [ ] Cart model + session management
|
||||
- [ ] Add to cart functionality
|
||||
- [ ] Cart display page (`/b/cart`)
|
||||
- [ ] Update quantities, remove items
|
||||
- [ ] Cart persistence (logged-in users)
|
||||
|
||||
#### Days 17-18: Checkout Flow
|
||||
- [ ] Checkout page (`/b/checkout`)
|
||||
- [ ] Select delivery location
|
||||
- [ ] Payment terms selection (COD, Net 15/30/60/90)
|
||||
- [ ] Order preview and confirmation
|
||||
- [ ] Order creation from cart
|
||||
|
||||
#### Days 19-20: Order Management
|
||||
- [ ] OrderResource in Filament
|
||||
- Table: order number, buyer, total, status, date
|
||||
- Form: view order details, line items
|
||||
- Actions: Accept, Fulfill, Deliver, Cancel
|
||||
- [ ] Order status workflow (new → accepted → fulfilled → delivered)
|
||||
- [ ] Email notifications (order placed, status changes)
|
||||
- [ ] Buyer order history page (`/b/orders`)
|
||||
|
||||
#### Day 21: Invoice System
|
||||
- [ ] InvoiceResource in Filament
|
||||
- [ ] Generate invoice from order
|
||||
- [ ] Invoice PDF generation
|
||||
- [ ] Payment tracking (paid/unpaid status)
|
||||
- [ ] Invoice email delivery
|
||||
|
||||
**Milestone 3 Deliverable**: Can place order, fulfill, generate invoice
|
||||
|
||||
---
|
||||
|
||||
### Week 4: Data Migration & Launch (Days 22-28)
|
||||
**Goal**: Old data migrated, system live
|
||||
|
||||
#### Days 22-23: Data Migration - Phase 1 (Companies & Users)
|
||||
- [ ] Export old database schema
|
||||
- [ ] Create Cannabrands as Company #1 (type='seller')
|
||||
- [ ] Create 5 brands linked to Cannabrands
|
||||
- Brand 1: Doobz
|
||||
- Brand 2: Thunderbud
|
||||
- Brand 3: High Expectations
|
||||
- Brand 4: Hash Factory
|
||||
- Brand 5: Twisties
|
||||
- [ ] Migrate buyer companies (old `companies` → new `companies` type='buyer')
|
||||
- [ ] Migrate users (preserve password hashes)
|
||||
- [ ] Migrate contacts → users relationship
|
||||
- [ ] Migrate locations (delivery addresses)
|
||||
|
||||
#### Days 24-25: Data Migration - Phase 2 (Products & Catalog)
|
||||
- [ ] Migrate strains table (direct copy)
|
||||
- [ ] Migrate labs table (direct copy)
|
||||
- [ ] Migrate components (BOM data)
|
||||
- [ ] Migrate products (1000 SKUs)
|
||||
- Map old `crm_products` → new `products`
|
||||
- Assign products to appropriate brand (need brand mapping logic)
|
||||
- Migrate product images
|
||||
- Migrate product prices
|
||||
- Migrate product varieties
|
||||
- [ ] Verify product data integrity (spot checks)
|
||||
|
||||
#### Day 26: Data Migration - Phase 3 (Orders & Invoices)
|
||||
- [ ] Migrate historical orders (all time)
|
||||
- Old `crm_invoices` (type='order') → new `orders`
|
||||
- Old `crm_invoice_lines` → new `order_items`
|
||||
- Preserve order statuses and dates
|
||||
- [ ] Migrate invoices
|
||||
- Old `crm_invoices` (type='invoice') → new `invoices`
|
||||
- Link to corresponding orders
|
||||
- [ ] Migrate invoice payments
|
||||
- [ ] Verify order totals match
|
||||
|
||||
#### Day 27: Testing & Bug Fixes
|
||||
- [ ] End-to-end testing
|
||||
- Register new buyer account
|
||||
- Browse products by brand
|
||||
- Add to cart, checkout
|
||||
- Place order
|
||||
- Admin: accept, fulfill order
|
||||
- Generate invoice
|
||||
- Record payment
|
||||
- [ ] User acceptance testing (UAT) with Cannabrands team
|
||||
- [ ] Performance testing (1000 products, 20 concurrent users)
|
||||
- [ ] Fix critical bugs
|
||||
- [ ] Data integrity verification
|
||||
|
||||
#### Day 28: Launch & Cutover
|
||||
- [ ] Final data sync (if parallel operation)
|
||||
- [ ] DNS/domain cutover
|
||||
- [ ] SSL certificate setup
|
||||
- [ ] Monitor error logs
|
||||
- [ ] User training documentation
|
||||
- [ ] Announce launch to users
|
||||
- [ ] Post-launch support monitoring
|
||||
|
||||
**Milestone 4 Deliverable**: New system live, old system retired
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ Data Migration Detailed Plan
|
||||
|
||||
### Migration Tools
|
||||
- **Laravel Commands**: Custom Artisan commands for each data type
|
||||
- **Direct SQL**: For bulk operations (faster)
|
||||
- **Seeders**: For reference data (strains, categories)
|
||||
- **Validation**: Hash comparisons to verify data integrity
|
||||
|
||||
### Migration Order (Critical Dependencies)
|
||||
```
|
||||
1. Companies (license holders) - no dependencies
|
||||
2. Brands (stores) - depends on companies
|
||||
3. Users - depends on companies
|
||||
4. Contacts - depends on users + companies
|
||||
5. Locations - depends on companies
|
||||
6. Strains - no dependencies
|
||||
7. Labs - no dependencies
|
||||
8. Components - no dependencies
|
||||
9. Products - depends on brands, strains, labs
|
||||
10. Product Components - depends on products, components
|
||||
11. Orders - depends on companies (buyer), users, products
|
||||
12. Order Items - depends on orders, products
|
||||
13. Invoices - depends on orders
|
||||
14. Invoice Payments - depends on invoices
|
||||
```
|
||||
|
||||
### Data Transformation Scripts
|
||||
|
||||
**Location**: `/Users/jon/projects/cannabrands/cannabrands_new/database/migrations/data/`
|
||||
|
||||
**Scripts to Create**:
|
||||
1. `migrate_companies.php` - Companies + Cannabrands setup
|
||||
2. `migrate_brands.php` - 5 Cannabrands brands
|
||||
3. `migrate_users.php` - User accounts (preserve passwords)
|
||||
4. `migrate_products.php` - 1000 SKUs with relationships
|
||||
5. `migrate_orders.php` - Historical orders + invoices
|
||||
6. `verify_migration.php` - Data integrity checks
|
||||
|
||||
### Password Preservation
|
||||
```php
|
||||
// Old system uses Laravel Hash (bcrypt)
|
||||
// New system uses Laravel Hash (bcrypt)
|
||||
// Direct copy of password hashes works
|
||||
|
||||
User::create([
|
||||
'email' => $oldUser->email,
|
||||
'password' => $oldUser->password, // Direct copy - NO rehashing
|
||||
'email_verified_at' => $oldUser->email_verified_at,
|
||||
]);
|
||||
```
|
||||
|
||||
### Rollback Strategy
|
||||
- Keep old system running in parallel for 2 weeks post-launch
|
||||
- Database snapshots before each migration phase
|
||||
- Export old database to SQL dump (backup)
|
||||
- Document rollback commands for each migration script
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Filament Resources Specification
|
||||
|
||||
### 1. CompanyResource
|
||||
**Purpose**: Manage license holders (buyers + sellers)
|
||||
|
||||
**Table Columns**:
|
||||
- Name (searchable)
|
||||
- Type (badge: buyer/seller/both)
|
||||
- License # (searchable)
|
||||
- Status (badge: active/pending/suspended)
|
||||
- Created date
|
||||
|
||||
**Form Fields**:
|
||||
- Business Information: name, DBA, legal name, type
|
||||
- License: number, expiry, document upload
|
||||
- Contact: email, phone, address
|
||||
- Compliance: W9, insurance, cannabis license
|
||||
|
||||
**Relations**:
|
||||
- Brands (HasMany) - for sellers
|
||||
- Locations (HasMany)
|
||||
- Contacts (HasMany)
|
||||
- Users (BelongsToMany via pivot)
|
||||
|
||||
**Actions**:
|
||||
- Approve Company
|
||||
- Suspend Company
|
||||
- Download Compliance Docs
|
||||
|
||||
---
|
||||
|
||||
### 2. BrandResource
|
||||
**Purpose**: Manage stores (product catalogs under a company)
|
||||
|
||||
**Table Columns**:
|
||||
- Logo (image)
|
||||
- Name (searchable)
|
||||
- Company (relationship)
|
||||
- Product count
|
||||
- Status (active/inactive)
|
||||
|
||||
**Form Fields**:
|
||||
- Brand Identity: name, slug, logo, description
|
||||
- Company (BelongsTo selector)
|
||||
- Social Media: Instagram, website
|
||||
- Settings: active status
|
||||
|
||||
**Relations**:
|
||||
- Products (HasMany)
|
||||
- Company (BelongsTo)
|
||||
|
||||
**Actions**:
|
||||
- View Storefront
|
||||
- Clone Brand
|
||||
- Activate/Deactivate
|
||||
|
||||
---
|
||||
|
||||
### 3. ProductResource
|
||||
**Purpose**: Manage SKUs (products sold on platform)
|
||||
|
||||
**Table Columns**:
|
||||
- Image (thumbnail)
|
||||
- SKU (searchable)
|
||||
- Name (searchable)
|
||||
- Brand (relationship)
|
||||
- Strain (relationship)
|
||||
- Price (money format)
|
||||
- Stock status
|
||||
|
||||
**Form Fields**:
|
||||
- Product Info: name, SKU, description
|
||||
- Brand (BelongsTo selector)
|
||||
- Pricing: base price, quantity breaks
|
||||
- Cannabis: Strain (BelongsTo), Lab results (BelongsTo)
|
||||
- Media: Images (multiple upload)
|
||||
- Inventory: track stock, quantity
|
||||
|
||||
**Relations**:
|
||||
- Brand (BelongsTo)
|
||||
- Strain (BelongsTo)
|
||||
- Lab (BelongsTo)
|
||||
- Components (BelongsToMany) - BOM
|
||||
- ProductImages (HasMany)
|
||||
- ProductPrices (HasMany)
|
||||
- Varieties (HasMany) - parent/child products
|
||||
|
||||
**Actions**:
|
||||
- Clone Product
|
||||
- Generate Barcode
|
||||
- Export to CSV
|
||||
- Activate/Deactivate
|
||||
|
||||
---
|
||||
|
||||
### 4. OrderResource
|
||||
**Purpose**: Manage buyer purchase orders
|
||||
|
||||
**Table Columns**:
|
||||
- Order # (searchable)
|
||||
- Buyer Company (relationship)
|
||||
- Brand (relationship)
|
||||
- Total (money format)
|
||||
- Status (badge with colors)
|
||||
- Order date (sortable)
|
||||
|
||||
**Form Fields**:
|
||||
- Order Info: number, date, buyer, location
|
||||
- Line Items: Repeater (product, quantity, price)
|
||||
- Totals: subtotal, tax, total
|
||||
- Status: workflow selector
|
||||
- Notes: internal notes
|
||||
|
||||
**Relations**:
|
||||
- Company (BelongsTo) - buyer
|
||||
- User (BelongsTo) - who placed it
|
||||
- Location (BelongsTo) - delivery address
|
||||
- OrderItems (HasMany)
|
||||
- Invoice (HasOne)
|
||||
|
||||
**Actions**:
|
||||
- Accept Order
|
||||
- Mark as Fulfilled
|
||||
- Mark as Delivered
|
||||
- Cancel Order
|
||||
- Generate Invoice
|
||||
- Email Customer
|
||||
|
||||
**Status Workflow**:
|
||||
```
|
||||
new → accepted → fulfilled → delivered
|
||||
↓ ↓ ↓
|
||||
cancelled
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. ComponentResource (BOM System)
|
||||
**Purpose**: Manage raw materials used to build products
|
||||
|
||||
**Table Columns**:
|
||||
- Name (searchable)
|
||||
- Type (badge: flower/extract/packaging)
|
||||
- Unit cost (money)
|
||||
- Unit (oz/g/each)
|
||||
- Stock
|
||||
|
||||
**Form Fields**:
|
||||
- Component Info: name, description, type
|
||||
- Pricing: cost per unit, unit of measure
|
||||
- Inventory: current stock, reorder point
|
||||
- Supplier: supplier info (optional)
|
||||
|
||||
**Relations**:
|
||||
- Products (BelongsToMany via product_components)
|
||||
|
||||
**Actions**:
|
||||
- View Products Using This Component
|
||||
- Update Cost
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Security & Compliance
|
||||
|
||||
### Data Protection
|
||||
- **Password hashes**: Direct copy (bcrypt compatible)
|
||||
- **Sensitive documents**: Migrate file paths, verify file existence
|
||||
- **License data**: Encrypted at rest in new system
|
||||
- **Payment info**: PCI compliance (if storing cards)
|
||||
|
||||
### Access Control
|
||||
**Roles** (Spatie Permission):
|
||||
- `admin` - Platform administrators (you/your team)
|
||||
- `company-owner` - Company account owner (full company access)
|
||||
- `company-manager` - Can manage orders, products (limited)
|
||||
- `company-user` - Can place orders, view history (buyer role)
|
||||
|
||||
**Permissions**:
|
||||
- `companies.view`, `companies.create`, `companies.edit`, `companies.delete`
|
||||
- `brands.manage` - Create/edit brands (seller only)
|
||||
- `products.manage` - Manage product catalog (seller only)
|
||||
- `orders.place` - Place orders (buyer only)
|
||||
- `orders.manage` - Accept/fulfill orders (seller only)
|
||||
- `invoices.view`, `invoices.generate`
|
||||
|
||||
---
|
||||
|
||||
## 📈 Performance Optimization
|
||||
|
||||
### Database Indexes (Critical for 1000 SKUs)
|
||||
```sql
|
||||
-- Products table
|
||||
CREATE INDEX idx_products_brand_id ON products(brand_id);
|
||||
CREATE INDEX idx_products_sku ON products(sku);
|
||||
CREATE INDEX idx_products_strain_id ON products(strain_id);
|
||||
|
||||
-- Orders table
|
||||
CREATE INDEX idx_orders_company_id ON orders(company_id);
|
||||
CREATE INDEX idx_orders_status ON orders(status);
|
||||
CREATE INDEX idx_orders_created_at ON orders(created_at);
|
||||
|
||||
-- Companies table
|
||||
CREATE INDEX idx_companies_type ON companies(type);
|
||||
CREATE INDEX idx_companies_license ON companies(license_number);
|
||||
```
|
||||
|
||||
### Caching Strategy
|
||||
- **Product catalog**: Cache for 1 hour
|
||||
- **Brand data**: Cache for 24 hours
|
||||
- **Shopping cart**: Session-based (no DB queries)
|
||||
- **Order history**: Cache per user (invalidate on new order)
|
||||
|
||||
### Eager Loading (Prevent N+1 Queries)
|
||||
```php
|
||||
// Products with relationships
|
||||
Product::with(['brand', 'strain', 'lab', 'images', 'prices'])->get();
|
||||
|
||||
// Orders with line items
|
||||
Order::with(['items.product', 'company', 'location'])->get();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Strategy
|
||||
|
||||
### Unit Tests (Pest)
|
||||
- [ ] Product model relationships
|
||||
- [ ] Order total calculations
|
||||
- [ ] BOM cost calculations
|
||||
- [ ] User authentication
|
||||
- [ ] Permission checks
|
||||
|
||||
### Feature Tests
|
||||
- [ ] User registration flow
|
||||
- [ ] Company approval workflow
|
||||
- [ ] Product CRUD operations
|
||||
- [ ] Shopping cart functionality
|
||||
- [ ] Order placement end-to-end
|
||||
- [ ] Invoice generation
|
||||
|
||||
### Migration Verification Tests
|
||||
- [ ] User count matches (20 users)
|
||||
- [ ] Product count matches (~1000 SKUs)
|
||||
- [ ] Order totals match historical data
|
||||
- [ ] Company count matches
|
||||
- [ ] All file paths resolve (images, documents)
|
||||
|
||||
### Load Testing
|
||||
- [ ] 1000 products loaded in catalog page (<2s)
|
||||
- [ ] 20 concurrent users placing orders
|
||||
- [ ] Search performance with 1000 SKUs (<500ms)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Risk Mitigation
|
||||
|
||||
### High-Risk Items
|
||||
|
||||
**1. Password Migration**
|
||||
- **Risk**: Users can't log in if hashes incompatible
|
||||
- **Mitigation**: Test migration with 3 sample users first
|
||||
- **Rollback**: Keep old system for password resets
|
||||
|
||||
**2. Product-to-Brand Mapping**
|
||||
- **Risk**: Old system doesn't track which brand owns which product
|
||||
- **Mitigation**: Manual mapping CSV: product_id → brand_name
|
||||
- **Fallback**: Assign all to "Cannabrands General" brand initially
|
||||
|
||||
**3. Order Data Integrity**
|
||||
- **Risk**: Order totals don't match after migration
|
||||
- **Mitigation**: Checksum verification on order totals
|
||||
- **Testing**: Compare 10 random orders old vs new
|
||||
|
||||
**4. File Path Migration**
|
||||
- **Risk**: Product images, compliance docs not found
|
||||
- **Mitigation**: Copy entire storage directory
|
||||
- **Verification**: Script to check all file paths resolve
|
||||
|
||||
**5. Email Notifications**
|
||||
- **Risk**: Spamming users during migration testing
|
||||
- **Mitigation**: Use Mailpit, disable SMTP until launch
|
||||
- **Testing**: Check email queue, don't send during migration
|
||||
|
||||
### Medium-Risk Items
|
||||
- Component data incomplete (some products missing BOM)
|
||||
- Variety relationships complex (parent-child products)
|
||||
- Historical data too large (performance issues)
|
||||
|
||||
---
|
||||
|
||||
## 📋 Launch Checklist
|
||||
|
||||
### Pre-Launch (Day 27)
|
||||
- [ ] All 1000 products migrated and visible
|
||||
- [ ] All 20 users can log in successfully
|
||||
- [ ] Test order placement end-to-end (3 different buyers)
|
||||
- [ ] Filament admin panel accessible
|
||||
- [ ] Email notifications working (test mode)
|
||||
- [ ] SSL certificate installed
|
||||
- [ ] Database backups automated
|
||||
- [ ] Error monitoring setup (Sentry/Bugsnag)
|
||||
|
||||
### Launch Day (Day 28)
|
||||
- [ ] Final data sync from old database
|
||||
- [ ] DNS cutover to new system
|
||||
- [ ] Announce to users via email
|
||||
- [ ] Monitor error logs (first 2 hours)
|
||||
- [ ] Support team on standby
|
||||
- [ ] Old system in read-only mode (fallback)
|
||||
|
||||
### Post-Launch (Week 5)
|
||||
- [ ] Daily monitoring for 1 week
|
||||
- [ ] User feedback collection
|
||||
- [ ] Bug fix hot patches
|
||||
- [ ] Performance tuning based on real usage
|
||||
- [ ] Retire old system after 2 weeks stable operation
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Development Tools & Resources
|
||||
|
||||
### Required Packages (Already Installed)
|
||||
- ✅ Laravel 12
|
||||
- ✅ Filament 4
|
||||
- ✅ Spatie Permission v6
|
||||
- ✅ Laravel Breeze (auth scaffolding)
|
||||
- ✅ Pest (testing)
|
||||
|
||||
### Additional Packages Needed
|
||||
```bash
|
||||
composer require barryvdh/laravel-dompdf # Invoice PDFs
|
||||
composer require intervention/image # Image processing
|
||||
composer require maatwebsite/excel # CSV imports/exports
|
||||
```
|
||||
|
||||
### Development Commands
|
||||
```bash
|
||||
# New project
|
||||
cd /Users/jon/projects/cannabrands/cannabrands_new
|
||||
|
||||
# Old project (reference only)
|
||||
cd /Users/jon/projects/cannabrands/cannabrands_crm
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support & Communication
|
||||
|
||||
### Daily Standup (Recommended)
|
||||
- What was completed yesterday
|
||||
- What's planned for today
|
||||
- Any blockers
|
||||
|
||||
### Claude Code Collaboration
|
||||
- Reference old codebase: `/Users/jon/projects/cannabrands/cannabrands_crm`
|
||||
- Active development: `/Users/jon/projects/cannabrands/cannabrands_new`
|
||||
- Use CLAUDE_COLLABORATION_WORKFLOW.md for prompt templates
|
||||
|
||||
### Decision Log
|
||||
| Date | Decision | Rationale |
|
||||
|------|----------|-----------|
|
||||
| Today | Use "companies" not "businesses" | Better industry terminology |
|
||||
| Today | 5 brands under Cannabrands | Matches business structure |
|
||||
| Today | Separate orders/invoices tables | Cleaner than dual-purpose |
|
||||
| Today | PostgreSQL for new system | Better JSON support, scalability |
|
||||
|
||||
---
|
||||
|
||||
## ✅ Success Metrics
|
||||
|
||||
### Technical Metrics
|
||||
- Zero data loss (100% of records migrated)
|
||||
- <2s page load for product catalog
|
||||
- <500ms search response time
|
||||
- 99.9% uptime in first month
|
||||
|
||||
### Business Metrics
|
||||
- All 5 Cannabrands brands have active storefronts
|
||||
- 20/20 users successfully migrated and active
|
||||
- First order placed within 48 hours of launch
|
||||
- Invoice generation working for all brands
|
||||
|
||||
### User Satisfaction
|
||||
- User training completed (1 hour session)
|
||||
- <5 support tickets in first week
|
||||
- Positive feedback from Cannabrands team
|
||||
- Buyers can easily find products
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: December 2024
|
||||
**Owner**: Cannabrands Development Team
|
||||
**Status**: Ready for Execution
|
||||
**Next Step**: Begin Week 1 - Schema Refactoring
|
||||
168
Makefile
168
Makefile
@@ -1,8 +1,40 @@
|
||||
.PHONY: help dev dev-down dev-build dev-shell dev-logs dev-vite prod-build prod-up prod-down prod-logs prod-shell prod-vite prod-test prod-test-build prod-test-up prod-test-down prod-test-logs prod-test-shell prod-test-status prod-test-clean migrate test clean install
|
||||
.PHONY: help dev dev-down dev-build dev-shell dev-logs dev-vite k-setup k-dev k-down k-logs k-shell k-artisan k-composer k-vite k-status prod-build prod-up prod-down prod-logs prod-shell prod-vite prod-test prod-test-build prod-test-up prod-test-down prod-test-logs prod-test-shell prod-test-status prod-test-clean migrate test clean install
|
||||
|
||||
# Default target
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
# ==================== K8s Variables ====================
|
||||
# Detect if we're in a worktree or project root
|
||||
GIT_DIR := $(shell git rev-parse --git-dir 2>/dev/null)
|
||||
IS_WORKTREE := $(shell echo "$(GIT_DIR)" | grep -q ".worktrees" && echo "true" || echo "false")
|
||||
|
||||
# Find project root (handles both worktree and main repo)
|
||||
ifeq ($(IS_WORKTREE),true)
|
||||
# In a worktree - project root is two levels up
|
||||
PROJECT_ROOT := $(shell cd ../.. && pwd)
|
||||
WORKTREE_NAME := $(shell basename $(CURDIR))
|
||||
K8S_VOLUME_PATH := /worktrees/$(WORKTREE_NAME)
|
||||
HOST_WORKTREE_PATH := $(PROJECT_ROOT)/.worktrees
|
||||
else
|
||||
# In project root
|
||||
PROJECT_ROOT := $(shell pwd)
|
||||
WORKTREE_NAME := root
|
||||
K8S_VOLUME_PATH := /project-root
|
||||
HOST_WORKTREE_PATH := $(PROJECT_ROOT)/.worktrees
|
||||
endif
|
||||
|
||||
# Generate namespace from branch name (feat-branch-name)
|
||||
CURRENT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD)
|
||||
K8S_NS := $(shell echo "$(CURRENT_BRANCH)" | sed 's/feature\//feat-/' | sed 's/bugfix\//fix-/' | sed 's/\//-/g')
|
||||
# Generate sanitized branch name for database
|
||||
SANITIZED_BRANCH := $(shell echo "$(CURRENT_BRANCH)" | sed 's/[^a-zA-Z0-9]/_/g')
|
||||
# Generate host from branch
|
||||
K8S_HOST := $(shell echo "$(CURRENT_BRANCH)" | sed 's/feature\///' | sed 's/bugfix\///' | sed 's/\//-/g').cannabrands.test
|
||||
# Read database credentials from .env
|
||||
DB_USERNAME := $(shell grep '^DB_USERNAME=' .env 2>/dev/null | cut -d '=' -f2)
|
||||
DB_PASSWORD := $(shell grep '^DB_PASSWORD=' .env 2>/dev/null | cut -d '=' -f2)
|
||||
DB_DATABASE := $(shell grep '^DB_DATABASE=' .env 2>/dev/null | cut -d '=' -f2)
|
||||
|
||||
# ==================== Local Development (Sail) ====================
|
||||
dev: ## Start local development environment with Sail
|
||||
./vendor/bin/sail up -d
|
||||
@@ -31,6 +63,116 @@ dev-composer: ## Run composer command (usage: make dev-composer CMD="install")
|
||||
dev-vite: ## Start Vite dev server (run after 'make dev')
|
||||
./vendor/bin/sail npm run dev
|
||||
|
||||
# ==================== K8s Local Development ====================
|
||||
k-setup: ## One-time setup: Create K3d cluster with auto-detected volume mounts
|
||||
@echo "🔧 Setting up K3d cluster 'dev' with auto-detected paths"
|
||||
@echo " Project Root: $(PROJECT_ROOT)"
|
||||
@echo " Worktrees Path: $(HOST_WORKTREE_PATH)"
|
||||
@echo ""
|
||||
@# Check if cluster already exists
|
||||
@if k3d cluster list | grep -q "^dev "; then \
|
||||
echo "⚠️ Cluster 'dev' already exists!"; \
|
||||
echo " To recreate, run: k3d cluster delete dev && make k-setup"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@# Create cluster with dynamic volume mounts
|
||||
k3d cluster create dev \
|
||||
--api-port 6443 \
|
||||
--port "80:80@loadbalancer" \
|
||||
--port "443:443@loadbalancer" \
|
||||
--volume $(HOST_WORKTREE_PATH):/worktrees \
|
||||
--volume $(PROJECT_ROOT):/project-root
|
||||
@echo ""
|
||||
@echo "✅ K3d cluster created successfully!"
|
||||
@echo " Next step: Run 'make k-dev' to start your environment"
|
||||
|
||||
k-dev: ## Start k8s local environment (like Sail, but with namespace isolation)
|
||||
@echo "🚀 Starting k8s environment"
|
||||
@echo " Location: $(if $(filter true,$(IS_WORKTREE)),Worktree ($(WORKTREE_NAME)),Project Root)"
|
||||
@echo " Namespace: $(K8S_NS)"
|
||||
@echo " Branch: $(CURRENT_BRANCH)"
|
||||
@echo " URL: http://$(K8S_HOST)"
|
||||
@echo ""
|
||||
@# Create namespace
|
||||
@kubectl create ns $(K8S_NS) --dry-run=client -o yaml | kubectl apply -f -
|
||||
@# Create secrets from .env
|
||||
@kubectl -n $(K8S_NS) delete secret app-env --ignore-not-found
|
||||
@kubectl -n $(K8S_NS) create secret generic app-env --from-env-file=.env
|
||||
@# Create PostgreSQL auth secret (using credentials from .env)
|
||||
@kubectl -n $(K8S_NS) create secret generic pg-auth --dry-run=client -o yaml \
|
||||
--from-literal=POSTGRES_DB=$(DB_DATABASE) \
|
||||
--from-literal=POSTGRES_USER=$(DB_USERNAME) \
|
||||
--from-literal=POSTGRES_PASSWORD=$(DB_PASSWORD) | kubectl apply -f -
|
||||
@# Deploy PostgreSQL
|
||||
@export NS=$(K8S_NS) PG_DB=$(DB_DATABASE) PG_USER=$(DB_USERNAME) PG_PASS=$(DB_PASSWORD) && \
|
||||
envsubst < k8s/local/postgres.yaml | kubectl apply -f -
|
||||
@# Deploy Redis
|
||||
@export NS=$(K8S_NS) && \
|
||||
envsubst < k8s/local/redis.yaml | kubectl apply -f -
|
||||
@# Deploy Reverb (WebSocket server)
|
||||
@export NS=$(K8S_NS) K8S_VOLUME_PATH=$(K8S_VOLUME_PATH) K8S_HOST=$(K8S_HOST) && \
|
||||
envsubst < k8s/local/reverb.yaml | kubectl apply -f -
|
||||
@# Wait for DB
|
||||
@echo "⏳ Waiting for PostgreSQL..."
|
||||
@kubectl -n $(K8S_NS) wait --for=condition=ready pod -l app=postgres --timeout=60s
|
||||
@# Deploy app (with code volume mounted)
|
||||
@export NS=$(K8S_NS) K8S_VOLUME_PATH=$(K8S_VOLUME_PATH) K8S_HOST=$(K8S_HOST) && \
|
||||
envsubst < k8s/local/deployment.yaml | kubectl apply -f -
|
||||
@# Create service + ingress
|
||||
@export NS=$(K8S_NS) K8S_HOST=$(K8S_HOST) && \
|
||||
envsubst < k8s/local/service.yaml | kubectl apply -f - && \
|
||||
envsubst < k8s/local/ingress.yaml | kubectl apply -f -
|
||||
@echo ""
|
||||
@echo "✅ Ready! Visit: http://$(K8S_HOST)"
|
||||
@echo ""
|
||||
@echo "💡 Your code is volume-mounted - changes are instant!"
|
||||
@echo " Edit files → refresh browser → see changes"
|
||||
@echo ""
|
||||
@echo "📝 Useful commands:"
|
||||
@echo " make k-logs # View app logs"
|
||||
@echo " make k-shell # Open shell in pod"
|
||||
@echo " make k-vite # Start Vite dev server"
|
||||
@echo ""
|
||||
@echo "🔌 WebSocket (Reverb) available at: ws://reverb.$(K8S_HOST):8080"
|
||||
|
||||
k-down: ## Stop k8s environment
|
||||
@echo "🗑 Removing namespace: $(K8S_NS)"
|
||||
@kubectl delete ns $(K8S_NS) --ignore-not-found
|
||||
@echo "✅ Cleaned up"
|
||||
|
||||
k-logs: ## View app logs
|
||||
@kubectl -n $(K8S_NS) logs -f deploy/web --all-containers=true
|
||||
|
||||
k-shell: ## Shell into app container
|
||||
@kubectl -n $(K8S_NS) exec -it deploy/web -- /bin/bash
|
||||
|
||||
k-artisan: ## Run artisan command (usage: make k-artisan CMD="migrate")
|
||||
@kubectl -n $(K8S_NS) exec deploy/web -- php artisan $(CMD)
|
||||
|
||||
k-composer: ## Run composer (usage: make k-composer CMD="install")
|
||||
@kubectl -n $(K8S_NS) exec deploy/web -- composer $(CMD)
|
||||
|
||||
k-vite: ## Run Vite dev server in k8s pod
|
||||
@echo "🎨 Starting Vite dev server in pod..."
|
||||
@echo " Access at: http://vite.$(K8S_HOST)"
|
||||
@kubectl -n $(K8S_NS) exec deploy/web -- npm run dev
|
||||
|
||||
k-test: ## Run tests in k8s pod
|
||||
@echo "🧪 Running tests in k8s pod..."
|
||||
@kubectl -n $(K8S_NS) exec deploy/web -- php artisan test
|
||||
|
||||
k-seed: ## Run database seeders in k8s (usage: make k-seed SEEDER=DevSeeder)
|
||||
@kubectl -n $(K8S_NS) exec deploy/web -- php artisan db:seed --class=$(SEEDER)
|
||||
|
||||
k-migrate-fresh: ## Fresh database with seeding in k8s pod
|
||||
@echo "🔄 Running fresh migration with seeding..."
|
||||
@kubectl -n $(K8S_NS) exec deploy/web -- php artisan migrate:fresh --seed
|
||||
|
||||
k-status: ## Show k8s environment status
|
||||
@echo "📊 Status for namespace: $(K8S_NS)"
|
||||
@echo ""
|
||||
@kubectl -n $(K8S_NS) get pods,svc,ingress
|
||||
|
||||
# ==================== Production ====================
|
||||
prod-build: ## Build production Docker image
|
||||
docker build -t cannabrands/app:latest -f Dockerfile .
|
||||
@@ -129,13 +271,37 @@ install: ## Initial project setup
|
||||
@echo " 2. Run 'make dev' to start development environment"
|
||||
@echo " 3. Run 'make migrate' to set up database"
|
||||
|
||||
setup-hooks: ## Configure git hooks for code quality
|
||||
@git config core.hooksPath .githooks
|
||||
@chmod +x .githooks/*
|
||||
@echo "✅ Git hooks configured!"
|
||||
@echo " - pre-commit: Auto-formats code with Laravel Pint"
|
||||
@echo " - pre-push: Optionally runs tests before pushing"
|
||||
|
||||
mailpit: ## Open Mailpit web UI
|
||||
@open http://localhost:8025 || xdg-open http://localhost:8025 || echo "Open http://localhost:8025 in your browser"
|
||||
|
||||
new-worktree: ## Create new worktree (usage: make new-worktree BRANCH=feature/my-feature or make new-worktree BRANCH=feature/my-feature NEW=true)
|
||||
@if [ -z "$(BRANCH)" ]; then \
|
||||
echo "❌ Error: BRANCH parameter required"; \
|
||||
echo ""; \
|
||||
echo "Usage:"; \
|
||||
echo " make new-worktree BRANCH=feature/my-feature # Checkout existing branch"; \
|
||||
echo " make new-worktree BRANCH=feature/my-feature NEW=true # Create new branch"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ "$(NEW)" = "true" ]; then \
|
||||
./scripts/new-worktree.sh -b $(BRANCH); \
|
||||
else \
|
||||
./scripts/new-worktree.sh $(BRANCH); \
|
||||
fi
|
||||
|
||||
help: ## Show this help message
|
||||
@echo "\n📦 CannaBrands Docker Commands\n"
|
||||
@echo "Local Development (Sail):"
|
||||
@grep -E '^dev.*:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-25s\033[0m %s\n", $$1, $$2}'
|
||||
@echo "\nK8s Local Development:"
|
||||
@grep -E '^k-.*:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[35m%-25s\033[0m %s\n", $$1, $$2}'
|
||||
@echo "\nProduction Testing (Local):"
|
||||
@grep -E '^prod-test.*:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[33m%-25s\033[0m %s\n", $$1, $$2}'
|
||||
@echo "\nProduction (K8s/Deployment):"
|
||||
|
||||
237
NOTIFICATIONS.md
237
NOTIFICATIONS.md
@@ -1,237 +0,0 @@
|
||||
# Notification & Email Policy
|
||||
|
||||
**Last Updated**: January 2025
|
||||
**Status**: Initial Implementation (Day 15)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the notification and email strategy for the Cannabrands B2B platform. The system uses both email notifications (via Laravel Mail) and in-app notifications for real-time updates.
|
||||
|
||||
---
|
||||
|
||||
## Email Notifications
|
||||
|
||||
### Order Workflow Emails
|
||||
|
||||
| Event | Recipient | Template | Priority | Notes |
|
||||
|-------|-----------|----------|----------|-------|
|
||||
| **New Order Placed** | Seller (Admin) | `emails.orders.new-order` | High | Includes order summary, buyer info, items list |
|
||||
| **Order Accepted** | Buyer | `emails.orders.order-accepted` | Medium | Confirms acceptance, shows estimated timeline |
|
||||
| **Order Ready for Delivery** | Buyer | `emails.orders.ready-for-delivery` | High | Notifies order is packed and ready to ship |
|
||||
| **Order Delivered** | Buyer | `emails.orders.order-delivered` | Medium | Delivery confirmation with thank you message |
|
||||
| **Order Cancelled** | Buyer | `emails.orders.order-cancelled` | High | Cancellation notice with reason if provided |
|
||||
|
||||
### Invoice Workflow Emails
|
||||
|
||||
| Event | Recipient | Template | Priority | Notes |
|
||||
|-------|-----------|----------|----------|-------|
|
||||
| **Invoice Generated** | Buyer | `emails.invoices.invoice-ready` | High | Invoice ready for buyer approval after seller review |
|
||||
| **Invoice Overdue (3 days)** | Buyer | `emails.invoices.payment-reminder-3day` | High | First reminder |
|
||||
| **Invoice Overdue (7 days)** | Buyer | `emails.invoices.payment-reminder-7day` | High | Second reminder |
|
||||
| **Invoice Overdue (14 days)** | Buyer + Seller | `emails.invoices.payment-reminder-14day` | Critical | Final reminder, copied to seller |
|
||||
| **Payment Received** | Buyer | `emails.invoices.payment-received` | Low | Payment confirmation (future feature) |
|
||||
|
||||
### Picking Workflow Emails
|
||||
|
||||
| Event | Recipient | Template | Priority | Notes |
|
||||
|-------|-----------|----------|----------|-------|
|
||||
| **Picking Complete (100%)** | Seller | `emails.orders.picking-complete` | High | Alerts seller to review and generate invoice |
|
||||
|
||||
---
|
||||
|
||||
## In-App Notifications
|
||||
|
||||
### Notification Types
|
||||
|
||||
**For Buyers:**
|
||||
- 🛒 New order confirmation
|
||||
- ✅ Order accepted by seller
|
||||
- 📦 Order ready for delivery
|
||||
- 🚚 Order delivered
|
||||
- 📄 Invoice ready for approval
|
||||
- ⚠️ Payment due soon (3 days before due date)
|
||||
- 🔴 Payment overdue
|
||||
|
||||
**For Sellers (Admin Panel):**
|
||||
- 🛍️ New order received
|
||||
- 📋 Picking complete - ready for review
|
||||
- 💳 Payment received (future)
|
||||
|
||||
### Notification Bell Behavior
|
||||
|
||||
- **Unread Count Badge**: Shows count of unread notifications
|
||||
- **Auto-refresh**: Checks for new notifications every 30 seconds
|
||||
- **Mark as Read**: Clicking notification marks it as read
|
||||
- **Persistence**: Notifications stored in database, not deleted after reading
|
||||
- **Archive**: Users can manually dismiss notifications (future feature)
|
||||
|
||||
---
|
||||
|
||||
## Email Template Design
|
||||
|
||||
All emails follow the branded template pattern established in `emails.registration.verification`:
|
||||
|
||||
**Brand Elements:**
|
||||
- Logo: `https://hub.cannabrands.com/assets/images/canna_white.png`
|
||||
- Primary Color: `#014847` (teal gradient)
|
||||
- Font: DM Sans
|
||||
- Button Style: Teal gradient background, white text
|
||||
- Footer: Cannabrands © 2025, support contact
|
||||
|
||||
**Template Structure:**
|
||||
```blade
|
||||
@component('mail::message')
|
||||
# [Email Subject Line]
|
||||
|
||||
[Email body content - plain language, buyer/seller focused]
|
||||
|
||||
@component('mail::button', ['url' => $actionUrl, 'color' => 'primary'])
|
||||
[Call to Action Button]
|
||||
@endcomponent
|
||||
|
||||
[Additional information or next steps]
|
||||
|
||||
Thanks,<br>
|
||||
The Cannabrands Team
|
||||
@endcomponent
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notification Preferences (Future)
|
||||
|
||||
Allow users to control notification settings:
|
||||
|
||||
### Email Preferences
|
||||
- [ ] Order updates (placed, accepted, delivered)
|
||||
- [ ] Invoice notifications (generated, due soon, overdue)
|
||||
- [ ] Marketing emails (new products, promotions)
|
||||
- [ ] System announcements
|
||||
|
||||
### In-App Preferences
|
||||
- [ ] Real-time notifications
|
||||
- [ ] Desktop push notifications (future)
|
||||
- [ ] Sound alerts
|
||||
|
||||
### Frequency Settings
|
||||
- [ ] Immediate (default)
|
||||
- [ ] Daily digest
|
||||
- [ ] Weekly summary
|
||||
- [ ] Disabled (except critical)
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Current Status (Day 15)
|
||||
|
||||
**Implemented:**
|
||||
- Email infrastructure using Laravel Mail
|
||||
- Notification database table and model
|
||||
- In-app notification dropdown (frontend exists, needs backend)
|
||||
- Brand-consistent email templates
|
||||
|
||||
**Not Yet Implemented:**
|
||||
- Automated overdue payment reminders (requires scheduled task)
|
||||
- Email preference management
|
||||
- Notification archiving
|
||||
- Push notifications
|
||||
- SMS notifications (future consideration)
|
||||
|
||||
### Technical Details
|
||||
|
||||
**Email Service**: Mailpit (local), SMTP (production)
|
||||
|
||||
**Notification Storage**:
|
||||
- Table: `notifications`
|
||||
- Model: `App\Models\Notification`
|
||||
- Polymorphic relationship to User
|
||||
|
||||
**Queue System**:
|
||||
- Development: sync driver
|
||||
- Production: database/redis queue recommended for email sending
|
||||
|
||||
**Rate Limiting**:
|
||||
- Payment reminders: Max 1 per invoice per day
|
||||
- Marketing emails: Opt-in only, max 2 per week
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Email Testing
|
||||
- [ ] New order email arrives at seller email
|
||||
- [ ] Order acceptance email arrives at buyer email
|
||||
- [ ] Invoice ready email includes correct invoice number and amount
|
||||
- [ ] Overdue reminders send at correct intervals
|
||||
- [ ] All emails render correctly in Gmail, Outlook, Apple Mail
|
||||
- [ ] All CTA buttons link to correct pages
|
||||
- [ ] Unsubscribe links work (future)
|
||||
|
||||
### In-App Notification Testing
|
||||
- [ ] Notification bell shows unread count
|
||||
- [ ] Clicking notification marks it as read
|
||||
- [ ] Notification links to correct resource (order/invoice)
|
||||
- [ ] Notifications auto-refresh without page reload
|
||||
- [ ] Mark all as read functionality works
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Phase 2 (Post-MVP)
|
||||
1. **Digest Emails**: Daily/weekly summary of activity
|
||||
2. **Smart Notifications**: AI-powered suggestions based on buying patterns
|
||||
3. **SMS Notifications**: Critical alerts via Twilio
|
||||
4. **Webhook Support**: Allow third-party integrations
|
||||
5. **Notification Templates**: Customizable by company
|
||||
|
||||
### Phase 3 (Advanced)
|
||||
1. **Multi-channel**: Email + SMS + Push + Slack
|
||||
2. **Notification Analytics**: Track open rates, click-through
|
||||
3. **A/B Testing**: Test email subject lines and content
|
||||
4. **Scheduled Sends**: Time-zone aware delivery
|
||||
5. **Rich Notifications**: Images, action buttons in notifications
|
||||
|
||||
---
|
||||
|
||||
## Refinement Areas
|
||||
|
||||
**To be reviewed and potentially changed:**
|
||||
|
||||
1. **Overdue Reminder Intervals**: Currently 3/7/14 days - should it be 1/3/7 or 5/10/15?
|
||||
2. **Seller Notifications**: Should sellers get daily digest of new orders or immediate alerts?
|
||||
3. **Picking Alerts**: Should lab crew get notifications when orders are accepted?
|
||||
4. **Invoice Approval**: Should seller be notified when buyer approves invoice?
|
||||
5. **Marketing vs Transactional**: Clear separation needed for CAN-SPAM compliance
|
||||
6. **Notification Retention**: How long to keep old notifications? Auto-archive after 30 days?
|
||||
7. **Critical vs Non-Critical**: Which emails should bypass "unsubscribe" (transactional only)
|
||||
8. **CC Recipients**: Should account managers be CC'd on order emails?
|
||||
|
||||
---
|
||||
|
||||
## Compliance Notes
|
||||
|
||||
**CAN-SPAM Act Requirements:**
|
||||
- ✅ Clear "From" name (Cannabrands)
|
||||
- ✅ Accurate subject lines
|
||||
- ✅ Physical address in footer
|
||||
- ✅ Unsubscribe mechanism (for marketing only)
|
||||
- ⚠️ Transactional emails exempt from unsubscribe requirement
|
||||
|
||||
**GDPR Considerations** (if applicable):
|
||||
- User consent for marketing emails
|
||||
- Right to export notification history
|
||||
- Right to delete notification data
|
||||
- Data retention policies
|
||||
|
||||
---
|
||||
|
||||
## Contact for Policy Changes
|
||||
|
||||
**Product Owner**: [To be filled]
|
||||
**Technical Lead**: [To be filled]
|
||||
**Last Review Date**: January 2025
|
||||
**Next Review Date**: March 2025
|
||||
24
README.md
24
README.md
@@ -429,15 +429,37 @@ No setup required - just works!
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
### **Getting Started**
|
||||
- **[Development Guide](docs/DEVELOPMENT.md)**: Complete developer setup and workflow
|
||||
- ✅ **Standard/Hybrid Flow** (PHP on host + Docker services)
|
||||
- ✅ **Local Kubernetes Development** (k3d, minikube, kind, Docker Desktop K8s)
|
||||
- Covers all local development approaches
|
||||
- **[Docker & Sail Guide](DOCKER.md)**: Laravel Sail development environment
|
||||
- ✅ **Laravel Sail Flow** (All services in Docker)
|
||||
- Quick start for containerized development
|
||||
|
||||
### **Deployment & DevOps**
|
||||
- **[Deployment Workflow](docs/DEPLOYMENT_WORKFLOW.md)**: Branching strategy, CI/CD pipeline, and deployment guide
|
||||
- **[Kubernetes Deployment](docs/KUBERNETES_DEPLOYMENT.md)**: Complete Kubernetes deployment guide (for DevOps)
|
||||
- **[Kubernetes Deployment](docs/KUBERNETES_DEPLOYMENT.md)**: Production Kubernetes deployment guide (for DevOps/SRE)
|
||||
- Production/Staging/Dev K8s clusters
|
||||
- Not for local development (see DEVELOPMENT.md for local K8s)
|
||||
|
||||
### **Application Reference**
|
||||
- **[Setup Guide](docs/SETUP.md)**: Detailed installation and configuration
|
||||
- **[API Reference](docs/API.md)**: Complete API endpoint documentation
|
||||
- **[Database Schema](docs/DATABASE.md)**: Database structure and relationships
|
||||
- **[Notifications](docs/NOTIFICATIONS.md)**: Notification system guide
|
||||
- **[App Overview](docs/APP_OVERVIEW.md)**: Project roadmap and architecture
|
||||
|
||||
### **Development Flow Options Summary**
|
||||
|
||||
| Flow | Document | Best For |
|
||||
|------|----------|----------|
|
||||
| **Local PHP + Docker services** | [DEVELOPMENT.md](docs/DEVELOPMENT.md) | Daily development (fastest) |
|
||||
| **Laravel Sail (all Docker)** | [DOCKER.md](DOCKER.md) | Environment consistency |
|
||||
| **Local Kubernetes (k3d/minikube)** | [DEVELOPMENT.md](docs/DEVELOPMENT.md) | Testing K8s deployments |
|
||||
| **Production Kubernetes** | [KUBERNETES_DEPLOYMENT.md](docs/KUBERNETES_DEPLOYMENT.md) | Production/staging clusters |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Deployment
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,368 +0,0 @@
|
||||
# Invoice Approval & Modification System - Testing Guide
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Generate Test Data
|
||||
|
||||
Run this command to create a complete test order with invoice:
|
||||
|
||||
```bash
|
||||
php artisan test:invoice-approval
|
||||
```
|
||||
|
||||
Or specify a specific buyer:
|
||||
```bash
|
||||
php artisan test:invoice-approval --buyer-email=your-buyer@example.com
|
||||
```
|
||||
|
||||
This will:
|
||||
- ✅ Create a test order with 5 random products
|
||||
- ✅ Progress it through the workflow (accepted → in_progress → ready_for_invoice → invoiced)
|
||||
- ✅ Generate an invoice with `approval_status = 'pending_buyer_approval'`
|
||||
- ✅ Display test URLs and credentials
|
||||
|
||||
---
|
||||
|
||||
## Testing Scenarios
|
||||
|
||||
### Scenario 1: Approve Invoice Without Changes
|
||||
|
||||
**Steps:**
|
||||
1. Login as buyer
|
||||
2. Navigate to `/b/invoices/{invoice_id}`
|
||||
3. Click **"Approve Invoice"** button
|
||||
4. Confirm the action
|
||||
|
||||
**Expected Results:**
|
||||
- ✅ Invoice `approval_status` → `'buyer_approved'`
|
||||
- ✅ Invoice `approved_at` timestamp set
|
||||
- ✅ Invoice `approved_by` = current user ID
|
||||
- ✅ Order `status` → `'manifest_created'`
|
||||
- ✅ Order `manifest_created_at` timestamp set
|
||||
- ✅ Success message displayed
|
||||
- ✅ Page reloads with success alert
|
||||
|
||||
**Database Verification:**
|
||||
```sql
|
||||
-- Check invoice approval
|
||||
SELECT id, invoice_number, approval_status, approved_at, approved_by
|
||||
FROM invoices
|
||||
WHERE id = {invoice_id};
|
||||
|
||||
-- Check order status progression
|
||||
SELECT id, order_number, status, manifest_created_at
|
||||
FROM orders
|
||||
WHERE id = {order_id};
|
||||
|
||||
-- Should be no changes recorded (direct approval)
|
||||
SELECT COUNT(*) FROM order_changes WHERE order_id = {order_id};
|
||||
-- Expected: 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Scenario 2: Reject Invoice
|
||||
|
||||
**Steps:**
|
||||
1. Login as buyer
|
||||
2. Navigate to `/b/invoices/{invoice_id}`
|
||||
3. Click **"Reject Invoice"** button
|
||||
4. Modal opens
|
||||
5. Enter rejection reason: "Prices too high"
|
||||
6. Click **"Confirm Rejection"**
|
||||
|
||||
**Expected Results:**
|
||||
- ✅ Invoice `approval_status` → `'rejected'`
|
||||
- ✅ Invoice `rejected_at` timestamp set
|
||||
- ✅ Invoice `rejection_reason` = "Prices too high"
|
||||
- ✅ Order `status` → `'rejected'`
|
||||
- ✅ Order `rejected_at` timestamp set
|
||||
- ✅ Order `rejected_reason` = "Prices too high"
|
||||
- ✅ Redirect to invoices index with success message
|
||||
|
||||
**Database Verification:**
|
||||
```sql
|
||||
SELECT id, invoice_number, approval_status, rejected_at, rejection_reason
|
||||
FROM invoices
|
||||
WHERE id = {invoice_id};
|
||||
|
||||
SELECT id, order_number, status, rejected_at, rejected_reason
|
||||
FROM orders
|
||||
WHERE id = {order_id};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Scenario 3: Modify Invoice - Reduce Quantity (Auto-Approved)
|
||||
|
||||
**Steps:**
|
||||
1. Login as buyer
|
||||
2. Navigate to `/b/invoices/{invoice_id}`
|
||||
3. Click **"Modify Invoice"** button
|
||||
4. Edit mode activates
|
||||
5. Find a line item with quantity 10
|
||||
6. Change quantity to **9** (10% reduction - should auto-approve)
|
||||
7. Click **"Save Changes"**
|
||||
|
||||
**Expected Results:**
|
||||
- ✅ Invoice `approval_status` → `'buyer_modified'`
|
||||
- ✅ `order_changes` record created:
|
||||
- `change_type` = 'quantity_edit'
|
||||
- `old_value` = 10
|
||||
- `new_value` = 9
|
||||
- `status` = **'auto_approved'** (because <10% reduction)
|
||||
- `negotiation_round` = 1
|
||||
- `user_type` = 'buyer'
|
||||
- ✅ Success message: "Changes saved successfully..."
|
||||
- ✅ Page reloads
|
||||
|
||||
**Database Verification:**
|
||||
```sql
|
||||
-- Check the change was recorded
|
||||
SELECT * FROM order_changes
|
||||
WHERE order_id = {order_id}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1;
|
||||
|
||||
-- Verify auto-approval
|
||||
SELECT change_type, old_value, new_value, status
|
||||
FROM order_changes
|
||||
WHERE order_id = {order_id} AND status = 'auto_approved';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Scenario 4: Modify Invoice - Reduce Quantity (>10%, Needs Review)
|
||||
|
||||
**Steps:**
|
||||
1. Login as buyer
|
||||
2. Navigate to `/b/invoices/{invoice_id}`
|
||||
3. Click **"Modify Invoice"**
|
||||
4. Change a quantity from **10 to 5** (50% reduction)
|
||||
5. Click **"Save Changes"**
|
||||
|
||||
**Expected Results:**
|
||||
- ✅ Invoice `approval_status` → `'buyer_modified'`
|
||||
- ✅ `order_changes` record created with `status` = **'pending'** (not auto-approved)
|
||||
- ✅ Seller will need to review this change
|
||||
|
||||
**Database Verification:**
|
||||
```sql
|
||||
SELECT change_type, old_value, new_value, status
|
||||
FROM order_changes
|
||||
WHERE order_id = {order_id} AND status = 'pending';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Scenario 5: Delete Line Item
|
||||
|
||||
**Steps:**
|
||||
1. Login as buyer
|
||||
2. Navigate to `/b/invoices/{invoice_id}`
|
||||
3. Click **"Modify Invoice"**
|
||||
4. Click **"Remove"** button on a line item
|
||||
5. Item shows as deleted (opacity-50, crossed out)
|
||||
6. Click **"Save Changes"**
|
||||
|
||||
**Expected Results:**
|
||||
- ✅ Invoice `approval_status` → `'buyer_modified'`
|
||||
- ✅ `order_changes` record created:
|
||||
- `change_type` = 'item_delete'
|
||||
- `old_value` = {original_quantity}
|
||||
- `new_value` = 0
|
||||
- `status` = 'pending'
|
||||
- `order_item_id` = {deleted_item_id}
|
||||
|
||||
**Database Verification:**
|
||||
```sql
|
||||
SELECT * FROM order_changes
|
||||
WHERE order_id = {order_id} AND change_type = 'item_delete';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Scenario 6: Try to Increase Quantity (Should Fail)
|
||||
|
||||
**Steps:**
|
||||
1. Login as buyer
|
||||
2. Navigate to `/b/invoices/{invoice_id}`
|
||||
3. Click **"Modify Invoice"**
|
||||
4. Try to change quantity from **10 to 15**
|
||||
5. Input field should prevent this OR show validation error
|
||||
|
||||
**Expected Results:**
|
||||
- ✅ Validation prevents increase
|
||||
- ✅ Alert: "You can only reduce quantities, not increase them."
|
||||
- ✅ Quantity resets to original value
|
||||
- ✅ No change saved
|
||||
|
||||
---
|
||||
|
||||
### Scenario 7: Multiple Changes in One Modification
|
||||
|
||||
**Steps:**
|
||||
1. Login as buyer
|
||||
2. Click **"Modify Invoice"**
|
||||
3. Reduce item 1 quantity from 10 → 8
|
||||
4. Delete item 2 entirely
|
||||
5. Reduce item 3 quantity from 12 → 10
|
||||
6. Click **"Save Changes"**
|
||||
|
||||
**Expected Results:**
|
||||
- ✅ 3 separate `order_changes` records created
|
||||
- ✅ Each tracked independently
|
||||
- ✅ All have same `negotiation_round` (1)
|
||||
- ✅ All have same `created_at` (batch)
|
||||
|
||||
**Database Verification:**
|
||||
```sql
|
||||
SELECT id, change_type, order_item_id, old_value, new_value, status, negotiation_round
|
||||
FROM order_changes
|
||||
WHERE order_id = {order_id}
|
||||
ORDER BY created_at DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Scenario 8: View Change History
|
||||
|
||||
**Steps:**
|
||||
1. After making changes (Scenario 7)
|
||||
2. Scroll down to **"Change History"** section
|
||||
3. Review the table
|
||||
|
||||
**Expected Results:**
|
||||
- ✅ Shows recent changes (up to 5)
|
||||
- ✅ Displays: Round, Type, Product, Old Value, New Value, Changed By, Status
|
||||
- ✅ Status badges: success (approved/auto-approved), warning (pending), error (rejected)
|
||||
- ✅ "View All Changes" button visible
|
||||
|
||||
---
|
||||
|
||||
### Scenario 9: Cancel Edit Mode
|
||||
|
||||
**Steps:**
|
||||
1. Login as buyer
|
||||
2. Click **"Modify Invoice"**
|
||||
3. Make some changes (reduce qty, delete item)
|
||||
4. Click **"Cancel"** button
|
||||
|
||||
**Expected Results:**
|
||||
- ✅ All changes discarded
|
||||
- ✅ Quantities reset to original values
|
||||
- ✅ Deleted items restored
|
||||
- ✅ Edit mode exits
|
||||
- ✅ No changes saved to database
|
||||
|
||||
---
|
||||
|
||||
### Scenario 10: Negotiation Round Tracking
|
||||
|
||||
**This requires seller response - will test once seller UI is complete**
|
||||
|
||||
1. Buyer modifies invoice (Round 1)
|
||||
2. Seller counter-modifies (Round 2)
|
||||
3. Buyer modifies again (Round 3)
|
||||
4. Check `current_negotiation_round` field increments
|
||||
|
||||
**Expected:**
|
||||
- ✅ Max 3 rounds allowed
|
||||
- ✅ `invoice.current_negotiation_round` increments
|
||||
|
||||
---
|
||||
|
||||
## Browser Console Testing
|
||||
|
||||
Open browser console while testing to see:
|
||||
|
||||
```javascript
|
||||
// Alpine.js state inspection
|
||||
Alpine.store('invoice')
|
||||
|
||||
// Check reactive data
|
||||
$data.items
|
||||
$data.editMode
|
||||
$data.hasChanges
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Network Tab Verification
|
||||
|
||||
Monitor the following API calls:
|
||||
|
||||
### Approve Invoice
|
||||
```
|
||||
POST /b/invoices/{invoice}/approve
|
||||
Response: { success: true, message: "Invoice approved successfully." }
|
||||
```
|
||||
|
||||
### Reject Invoice
|
||||
```
|
||||
POST /b/invoices/{invoice}/reject
|
||||
Body: { reason: "..." }
|
||||
Redirect: /b/invoices
|
||||
```
|
||||
|
||||
### Modify Invoice
|
||||
```
|
||||
POST /b/invoices/{invoice}/modify
|
||||
Body: {
|
||||
items: [
|
||||
{ id: 1, quantity: 8, deleted: false },
|
||||
{ id: 2, quantity: 0, deleted: true }
|
||||
]
|
||||
}
|
||||
Response: { success: true, message: "Changes saved..." }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edge Cases to Test
|
||||
|
||||
### ❌ Unauthorized Access
|
||||
- Try accessing another company's invoice
|
||||
- Expected: 403 Forbidden
|
||||
|
||||
### ❌ Already Approved Invoice
|
||||
- Try modifying an invoice with `approval_status = 'buyer_approved'`
|
||||
- Expected: Buttons hidden, "This invoice cannot be modified" message
|
||||
|
||||
### ❌ Already Rejected Invoice
|
||||
- Try approving a rejected invoice
|
||||
- Expected: Buttons hidden, rejection reason displayed
|
||||
|
||||
### ❌ Empty Changes
|
||||
- Click "Modify Invoice"
|
||||
- Don't change anything
|
||||
- Click "Save Changes"
|
||||
- Expected: "No changes detected" error
|
||||
|
||||
---
|
||||
|
||||
## Post-Testing Cleanup
|
||||
|
||||
Remove test data:
|
||||
```sql
|
||||
-- Find test orders
|
||||
SELECT * FROM orders WHERE notes LIKE '%Test order for invoice approval%';
|
||||
|
||||
-- Delete test data (cascades to order_items, invoices, order_changes)
|
||||
DELETE FROM orders WHERE notes LIKE '%Test order for invoice approval%';
|
||||
```
|
||||
|
||||
Or keep for repeated testing!
|
||||
|
||||
---
|
||||
|
||||
## Next Steps: Seller Testing (Once UI Complete)
|
||||
|
||||
1. Seller receives notification of buyer modifications
|
||||
2. Seller reviews changes at `/seller/invoices/{invoice}`
|
||||
3. Seller can:
|
||||
- Approve buyer's changes (applies them, moves to amendment_in_progress)
|
||||
- Counter-modify (make own changes, increments negotiation round)
|
||||
- Reject outright
|
||||
|
||||
This will be tested once seller review UI is implemented.
|
||||
@@ -1,174 +0,0 @@
|
||||
# Testing Production Docker Image Locally
|
||||
|
||||
This guide helps you test the production Docker image locally before pushing to CI/CD.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Build and start the production image locally
|
||||
docker-compose -f docker-compose.prod-test.yml up --build
|
||||
|
||||
# Access the app at: http://localhost:8080
|
||||
# PostgreSQL accessible at: localhost:5433
|
||||
```
|
||||
|
||||
## Why Test Locally?
|
||||
|
||||
- ✅ **Faster feedback** - 2-5 min vs 10-15 min through CI/CD
|
||||
- ✅ **Save CI resources** - Don't waste build minutes
|
||||
- ✅ **Easier debugging** - Direct container access
|
||||
- ✅ **Catch issues early** - Before they hit version control
|
||||
|
||||
## Common Commands
|
||||
|
||||
### Build and Run
|
||||
```bash
|
||||
# Start everything (builds if needed)
|
||||
docker-compose -f docker-compose.prod-test.yml up
|
||||
|
||||
# Build with no cache (clean build)
|
||||
docker-compose -f docker-compose.prod-test.yml build --no-cache
|
||||
|
||||
# Run in background
|
||||
docker-compose -f docker-compose.prod-test.yml up -d
|
||||
|
||||
# View logs
|
||||
docker-compose -f docker-compose.prod-test.yml logs -f app
|
||||
```
|
||||
|
||||
### Debug Inside Container
|
||||
```bash
|
||||
# Execute shell in running container
|
||||
docker-compose -f docker-compose.prod-test.yml exec app /bin/sh
|
||||
|
||||
# Check supervisor status
|
||||
docker-compose -f docker-compose.prod-test.yml exec app supervisorctl status
|
||||
|
||||
# View nginx logs
|
||||
docker-compose -f docker-compose.prod-test.yml exec app cat /var/log/nginx/error.log
|
||||
|
||||
# View Laravel logs
|
||||
docker-compose -f docker-compose.prod-test.yml exec app tail -f /var/www/html/storage/logs/laravel.log
|
||||
```
|
||||
|
||||
### Cleanup
|
||||
```bash
|
||||
# Stop everything
|
||||
docker-compose -f docker-compose.prod-test.yml down
|
||||
|
||||
# Remove volumes too (fresh database)
|
||||
docker-compose -f docker-compose.prod-test.yml down -v
|
||||
```
|
||||
|
||||
## Testing Workflow
|
||||
|
||||
### Before Pushing to CI/CD:
|
||||
|
||||
1. **Make your changes** to Dockerfile, configs, etc.
|
||||
|
||||
2. **Test locally:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod-test.yml up --build
|
||||
```
|
||||
|
||||
3. **Verify it works:**
|
||||
- App accessible at http://localhost:8080
|
||||
- No errors in logs: `docker-compose -f docker-compose.prod-test.yml logs app`
|
||||
- All services running: `docker-compose -f docker-compose.prod-test.yml exec app supervisorctl status`
|
||||
|
||||
4. **If it works, push to develop:**
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "fix: your change"
|
||||
git push origin develop
|
||||
```
|
||||
|
||||
5. **If it doesn't work, debug:**
|
||||
```bash
|
||||
# Check logs
|
||||
docker-compose -f docker-compose.prod-test.yml logs app
|
||||
|
||||
# Exec into container
|
||||
docker-compose -f docker-compose.prod-test.yml exec app /bin/sh
|
||||
|
||||
# Make fixes and rebuild
|
||||
docker-compose -f docker-compose.prod-test.yml up --build
|
||||
```
|
||||
|
||||
## Differences from Production
|
||||
|
||||
This local test environment differs from production K8s in:
|
||||
|
||||
- Uses local PostgreSQL (not persistent volume)
|
||||
- Runs on localhost:8080 (not ingress with TLS)
|
||||
- Uses test APP_KEY (not secret from K8s)
|
||||
- Single replica (not multiple pods)
|
||||
- No load balancer or ingress
|
||||
|
||||
But it **does test**:
|
||||
- ✅ Dockerfile builds correctly
|
||||
- ✅ All directories exist and have correct permissions
|
||||
- ✅ Supervisor starts all services (nginx, php-fpm, workers, scheduler)
|
||||
- ✅ Laravel boots and connects to database
|
||||
- ✅ Migrations run successfully
|
||||
- ✅ Application responds to HTTP requests
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Port conflicts
|
||||
If 8080 or 5433 are already in use, edit `docker-compose.prod-test.yml` and change the ports.
|
||||
|
||||
### Build errors
|
||||
```bash
|
||||
# Clean build from scratch
|
||||
docker-compose -f docker-compose.prod-test.yml build --no-cache --pull
|
||||
```
|
||||
|
||||
### Container crashes immediately
|
||||
```bash
|
||||
# Check logs for error
|
||||
docker-compose -f docker-compose.prod-test.yml logs app
|
||||
|
||||
# Try running without detach to see output
|
||||
docker-compose -f docker-compose.prod-test.yml up
|
||||
```
|
||||
|
||||
### Need fresh database
|
||||
```bash
|
||||
# Remove volumes and recreate
|
||||
docker-compose -f docker-compose.prod-test.yml down -v
|
||||
docker-compose -f docker-compose.prod-test.yml up
|
||||
```
|
||||
|
||||
## Integration with Development Workflow
|
||||
|
||||
### Using alongside Laravel Sail
|
||||
|
||||
Sail and prod-test can run simultaneously:
|
||||
- Sail dev environment: http://localhost (port 80)
|
||||
- Production test: http://localhost:8080
|
||||
|
||||
Just make sure to use different database ports (Sail: 5432, prod-test: 5433).
|
||||
|
||||
### When to use each:
|
||||
|
||||
**Laravel Sail (docker-compose.yml):**
|
||||
- Daily development
|
||||
- Running artisan commands
|
||||
- Testing code changes
|
||||
- Hot reload with Vite
|
||||
|
||||
**Production Test (docker-compose.prod-test.yml):**
|
||||
- Before pushing to develop
|
||||
- Testing Dockerfile changes
|
||||
- Testing supervisor/nginx configs
|
||||
- Verifying production build process
|
||||
|
||||
## Next Steps After Local Success
|
||||
|
||||
Once your image works locally:
|
||||
|
||||
1. ✅ Commit and push to develop
|
||||
2. ✅ CI/CD builds the same image
|
||||
3. ✅ Deploy to Kubernetes with confidence
|
||||
4. ✅ Much faster iteration cycle!
|
||||
197
TEST_RESULTS.md
197
TEST_RESULTS.md
@@ -1,197 +0,0 @@
|
||||
# Business-Scoped Seller Routes - Test Results
|
||||
|
||||
**Test Date:** 2025-10-15
|
||||
**Tester:** Claude Code
|
||||
**Test Account:** jon@cannabrands.com
|
||||
**Business Slug:** cannabrands
|
||||
|
||||
## Test Data Summary
|
||||
- **Business:** Cannabrands (slug: `cannabrands`)
|
||||
- **Brands:** 13
|
||||
- **Products:** 312
|
||||
- **Orders:** 0 (none yet)
|
||||
- **Invoices:** 0 (none yet)
|
||||
- **Drivers:** 1
|
||||
- **Vehicles:** 2
|
||||
|
||||
---
|
||||
|
||||
## Route Tests
|
||||
|
||||
### 1. Business-Scoped Dashboard
|
||||
**URL:** `http://localhost:8000/s/cannabrands/dashboard`
|
||||
**Expected:** Dashboard with business context
|
||||
**Status:** ⏳ Testing...
|
||||
|
||||
### 2. Fleet Management - Drivers
|
||||
**URL:** `http://localhost:8000/s/cannabrands/fleet/drivers`
|
||||
**Expected:** Drivers list page with business context
|
||||
**Status:** ⏳ Testing...
|
||||
|
||||
### 3. Fleet Management - Vehicles
|
||||
**URL:** `http://localhost:8000/s/cannabrands/fleet/vehicles`
|
||||
**Expected:** Vehicles list page with business context
|
||||
**Status:** ⏳ Testing...
|
||||
|
||||
### 4. Orders List
|
||||
**URL:** `http://localhost:8000/s/cannabrands/orders`
|
||||
**Expected:** Orders list (empty state expected)
|
||||
**Status:** ✅ PASSED (Fixed query in OrderController)
|
||||
**Notes:** Fixed relationship chain to use `whereHas('items.product.brand')`
|
||||
|
||||
### 5. Products List
|
||||
**URL:** `http://localhost:8000/s/cannabrands/products`
|
||||
**Expected:** Products list with 312 products
|
||||
**Status:** ⏳ Testing...
|
||||
|
||||
### 6. Invoices List
|
||||
**URL:** `http://localhost:8000/s/cannabrands/invoices`
|
||||
**Expected:** Invoices list (empty state expected)
|
||||
**Status:** ✅ PASSED
|
||||
**Notes:** InvoiceController updated with business scope
|
||||
|
||||
### 7. Product Edit Page
|
||||
**URL:** `http://localhost:8000/s/cannabrands/products/225/edit`
|
||||
**Expected:** Product edit form with audit history
|
||||
**Status:** ✅ PASSED
|
||||
**Notes:** Enhanced UI, audit logging, checkbox handling fixed
|
||||
|
||||
---
|
||||
|
||||
## Access Control Tests
|
||||
|
||||
### Test 1: Business Slug Validation
|
||||
**Test:** Access non-existent business slug
|
||||
**URL:** `http://localhost:8000/s/invalid-slug/dashboard`
|
||||
**Expected:** 404 Not Found
|
||||
**Status:** ⏳ Testing...
|
||||
|
||||
### Test 2: Unauthorized Business Access
|
||||
**Test:** User attempts to access another user's business
|
||||
**Expected:** 403 Forbidden
|
||||
**Status:** ⏳ Testing...
|
||||
**Notes:** Requires second test account
|
||||
|
||||
### Test 3: Unauthenticated Access
|
||||
**Test:** Access business-scoped route without login
|
||||
**Expected:** Redirect to login page
|
||||
**Status:** ⏳ Testing...
|
||||
|
||||
---
|
||||
|
||||
## Route Model Binding Tests
|
||||
|
||||
### Test 1: Business by Slug
|
||||
**Binding:** `{business}` → Business model by slug
|
||||
**Test URL:** `/s/cannabrands/dashboard`
|
||||
**Expected:** Resolve to Business with slug 'cannabrands'
|
||||
**Status:** ⏳ Testing...
|
||||
|
||||
### Test 2: Order by Order Number
|
||||
**Binding:** `{order}` → Order model by order_number
|
||||
**Test URL:** `/s/cannabrands/orders/{order_number}`
|
||||
**Expected:** Resolve to Order by order_number field
|
||||
**Status:** ⏳ Testing... (requires order data)
|
||||
|
||||
### Test 3: Product by ID
|
||||
**Binding:** `{product}` → Product model by ID
|
||||
**Test URL:** `/s/cannabrands/products/225/edit`
|
||||
**Expected:** Resolve to Product with ID 225
|
||||
**Status:** ✅ PASSED
|
||||
|
||||
---
|
||||
|
||||
## Critical Routes Requiring Approval Middleware
|
||||
|
||||
These routes require `approved` middleware:
|
||||
- ✅ Orders: `/s/{business}/orders`
|
||||
- ✅ Invoices: `/s/{business}/invoices`
|
||||
- ✅ Products: `/s/{business}/products`
|
||||
- ✅ Components: `/s/{business}/components`
|
||||
- ✅ Customers: `/s/{business}/customers`
|
||||
|
||||
**Test Account Status:** jon@cannabrands.com is approved ✓
|
||||
|
||||
---
|
||||
|
||||
## Known Issues & Fixes Applied
|
||||
|
||||
### Issue 1: OrderController Query Error ✅ FIXED
|
||||
**Error:** `Column "seller_business_id" does not exist`
|
||||
**Fix:** Changed query to use correct relationship chain:
|
||||
```php
|
||||
->whereHas('items.product.brand', function ($query) use ($business) {
|
||||
$query->where('business_id', $business->id);
|
||||
})
|
||||
```
|
||||
**File:** `app/Http/Controllers/OrderController.php:26`
|
||||
|
||||
### Issue 2: Invoice Model Fillable Array ✅ FIXED
|
||||
**Error:** Using deprecated `company_id` instead of `business_id`
|
||||
**Fix:** Updated fillable array in Invoice model
|
||||
**File:** `app/Models/Invoice.php`
|
||||
|
||||
### Issue 3: Invoice Routes Not Business-Scoped ✅ FIXED
|
||||
**Error:** Invoice routes not accepting business parameter
|
||||
**Fix:** Updated InvoiceController to accept Business parameter and verify ownership
|
||||
**File:** `app/Http/Controllers/Seller/InvoiceController.php`
|
||||
|
||||
### Issue 4: Product Checkbox Not Unchecking ✅ FIXED
|
||||
**Error:** Featured checkbox stays checked when unchecked and saved
|
||||
**Fix:** Added explicit checkbox handling in ProductController:
|
||||
```php
|
||||
$validated['is_active'] = $request->has('is_active');
|
||||
$validated['is_featured'] = $request->has('is_featured');
|
||||
```
|
||||
**File:** `app/Http/Controllers/Seller/ProductController.php`
|
||||
|
||||
### Issue 5: Alpine.js FOUC on Sidebar ✅ FIXED
|
||||
**Error:** Brief flash of expanded menu items during page load
|
||||
**Fix:** Added `x-cloak` directive to sidebar menu container
|
||||
**File:** `resources/views/components/seller-sidebar.blade.php:35`
|
||||
|
||||
### Issue 6: Alpine.js FOUC on Notifications ✅ FIXED
|
||||
**Error:** Brief flash of notification dropdown during page load
|
||||
**Fix:** Added `x-cloak` directive to notification dropdown container
|
||||
**File:** `resources/views/layouts/app-with-sidebar.blade.php:67`
|
||||
|
||||
---
|
||||
|
||||
## Manual Testing Checklist
|
||||
|
||||
- [ ] Login as seller (jon@cannabrands.com)
|
||||
- [ ] Navigate to `/s/cannabrands/dashboard`
|
||||
- [ ] Test fleet management pages (drivers, vehicles)
|
||||
- [ ] Test orders page (empty state)
|
||||
- [ ] Test products list and edit
|
||||
- [ ] Test invoices page (empty state)
|
||||
- [ ] Test product audit history
|
||||
- [ ] Test checkbox toggles
|
||||
- [ ] Verify sidebar menu persistence
|
||||
- [ ] Verify notification dropdown works
|
||||
- [ ] Test accessing invalid business slug
|
||||
- [ ] Test logout and re-login flow
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
1. **Create test orders** to fully test the order management workflow
|
||||
2. **Add automated tests** for business-scoped routing and access control
|
||||
3. **Monitor production** for any route binding issues
|
||||
4. **Document** the business-scoped routing pattern for future development
|
||||
5. **Consider** adding middleware to verify business ownership on all routes
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Total Routes Tested:** 7
|
||||
**Passed:** 4
|
||||
**In Progress:** 3
|
||||
**Failed:** 0
|
||||
|
||||
**Critical Fixes Applied:** 6
|
||||
**Access Control:** ✅ Implemented via custom route model binding
|
||||
**Business Scope:** ✅ All routes accept business parameter
|
||||
**Database Queries:** ✅ Fixed to use correct relationship chains
|
||||
155
app/Console/Commands/CleanupPermissionAuditLogs.php
Normal file
155
app/Console/Commands/CleanupPermissionAuditLogs.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\PermissionAuditLog;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CleanupPermissionAuditLogs extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'permissions:cleanup-audit
|
||||
{--dry-run : Show what would be deleted without actually deleting}
|
||||
{--force : Skip confirmation prompt}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Delete expired permission audit logs (non-critical logs past their expiration date)';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$isDryRun = $this->option('dry-run');
|
||||
$isForced = $this->option('force');
|
||||
|
||||
$this->info('🔍 Scanning for expired permission audit logs...');
|
||||
$this->newLine();
|
||||
|
||||
// Find expired logs
|
||||
$expiredLogs = PermissionAuditLog::expired()->get();
|
||||
|
||||
if ($expiredLogs->isEmpty()) {
|
||||
$this->info('✅ No expired audit logs found. Everything is up to date!');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
// Statistics
|
||||
$totalCount = $expiredLogs->count();
|
||||
$oldestLog = $expiredLogs->sortBy('created_at')->first();
|
||||
$newestLog = $expiredLogs->sortByDesc('created_at')->first();
|
||||
|
||||
// Display summary
|
||||
$this->table(
|
||||
['Metric', 'Value'],
|
||||
[
|
||||
['Expired logs found', $totalCount],
|
||||
['Oldest expired log', $oldestLog->created_at->format('Y-m-d H:i:s')],
|
||||
['Newest expired log', $newestLog->created_at->format('Y-m-d H:i:s')],
|
||||
['Date range', $oldestLog->created_at->diffForHumans($newestLog->created_at, true)],
|
||||
]
|
||||
);
|
||||
|
||||
$this->newLine();
|
||||
|
||||
// Show sample of logs to be deleted
|
||||
$this->info('📋 Sample of logs to be deleted:');
|
||||
$sampleLogs = $expiredLogs->take(5);
|
||||
|
||||
foreach ($sampleLogs as $log) {
|
||||
$this->line(sprintf(
|
||||
' • [%s] %s - %s (expired %s)',
|
||||
$log->created_at->format('Y-m-d'),
|
||||
$log->action_name,
|
||||
$log->targetUser?->name ?? 'Unknown User',
|
||||
$log->expires_at->diffForHumans()
|
||||
));
|
||||
}
|
||||
|
||||
if ($totalCount > 5) {
|
||||
$this->line(" ... and {$totalCount} more");
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
|
||||
// Dry run mode
|
||||
if ($isDryRun) {
|
||||
$this->warn('🧪 DRY RUN MODE - No logs will be deleted');
|
||||
$this->info("Would delete {$totalCount} expired audit logs");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
// Confirmation prompt (unless forced)
|
||||
if (! $isForced) {
|
||||
$confirmed = $this->confirm(
|
||||
"Are you sure you want to delete {$totalCount} expired audit logs?",
|
||||
false
|
||||
);
|
||||
|
||||
if (! $confirmed) {
|
||||
$this->info('❌ Cleanup cancelled');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
// Perform deletion
|
||||
$this->info('🗑️ Deleting expired audit logs...');
|
||||
|
||||
$progressBar = $this->output->createProgressBar($totalCount);
|
||||
$progressBar->start();
|
||||
|
||||
$deletedCount = 0;
|
||||
$errorCount = 0;
|
||||
|
||||
foreach ($expiredLogs as $log) {
|
||||
try {
|
||||
$log->delete();
|
||||
$deletedCount++;
|
||||
} catch (\Exception $e) {
|
||||
$errorCount++;
|
||||
$this->error("Failed to delete log ID {$log->id}: {$e->getMessage()}");
|
||||
}
|
||||
|
||||
$progressBar->advance();
|
||||
}
|
||||
|
||||
$progressBar->finish();
|
||||
$this->newLine(2);
|
||||
|
||||
// Final summary
|
||||
if ($errorCount === 0) {
|
||||
$this->info("✅ Successfully deleted {$deletedCount} expired audit logs");
|
||||
} else {
|
||||
$this->warn("⚠️ Deleted {$deletedCount} logs with {$errorCount} errors");
|
||||
}
|
||||
|
||||
// Show remaining stats
|
||||
$remainingTotal = PermissionAuditLog::count();
|
||||
$remainingCritical = PermissionAuditLog::critical()->count();
|
||||
$remainingNonExpired = $remainingTotal - $remainingCritical;
|
||||
|
||||
$this->newLine();
|
||||
$this->info('📊 Database statistics after cleanup:');
|
||||
$this->table(
|
||||
['Category', 'Count'],
|
||||
[
|
||||
['Critical logs (kept forever)', $remainingCritical],
|
||||
['Non-critical logs (not yet expired)', $remainingNonExpired],
|
||||
['Total remaining logs', $remainingTotal],
|
||||
]
|
||||
);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -51,8 +51,10 @@ class CreateTestInvoiceForApproval extends Command
|
||||
|
||||
$this->info("✓ Company: {$company->name}");
|
||||
|
||||
// Get some products
|
||||
$products = Product::where('quantity_on_hand', '>', 10)->where('is_active', true)->take(5)->get();
|
||||
// Get some products that have inventory
|
||||
$products = Product::whereHas('inventoryItems', function ($q) {
|
||||
$q->where('quantity_on_hand', '>', 10);
|
||||
})->where('is_active', true)->take(5)->get();
|
||||
if ($products->isEmpty()) {
|
||||
$this->error('No products found. Please seed products first.');
|
||||
|
||||
|
||||
289
app/Console/Commands/ImportBrandFromMySQL.php
Normal file
289
app/Console/Commands/ImportBrandFromMySQL.php
Normal file
@@ -0,0 +1,289 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Brand;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Intervention\Image\Drivers\Gd\Driver;
|
||||
use Intervention\Image\ImageManager;
|
||||
|
||||
class ImportBrandFromMySQL extends Command
|
||||
{
|
||||
protected $signature = 'brand:import-from-mysql {remoteName? : Remote brand name} {localName? : Local brand name (if different)}';
|
||||
|
||||
protected $description = 'Import brand data and images from remote MySQL database';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$remoteBrandName = $this->argument('remoteName') ?? 'Canna';
|
||||
$localBrandName = $this->argument('localName') ?? $remoteBrandName;
|
||||
|
||||
$this->info('Connecting to remote MySQL database...');
|
||||
|
||||
try {
|
||||
// Connect to remote MySQL with latin1 charset (Windows-1252)
|
||||
$pdo = new \PDO(
|
||||
'mysql:host=sql1.creationshop.net;dbname=hub_cannabrands;charset=latin1',
|
||||
'claude',
|
||||
'claude'
|
||||
);
|
||||
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
$this->info('Connected successfully!');
|
||||
|
||||
// Fetch brand data from MySQL
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT brand_id, name, tagline, short_desc, `desc`, url,
|
||||
image, banner, address, unit_number, city, state, zip, phone,
|
||||
public, fb, insta, twitter, youtube
|
||||
FROM brands
|
||||
WHERE name = :name
|
||||
');
|
||||
$stmt->execute(['name' => $remoteBrandName]);
|
||||
$remoteBrand = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
|
||||
if (! $remoteBrand) {
|
||||
$this->error("Brand '{$remoteBrandName}' not found in remote database");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->info("Found remote brand: {$remoteBrand['name']}");
|
||||
|
||||
// Find local brand by name
|
||||
$localBrand = Brand::where('name', $localBrandName)->first();
|
||||
|
||||
if (! $localBrand) {
|
||||
$this->error("Brand '{$localBrandName}' not found in local database");
|
||||
$this->info('Available brands: '.Brand::pluck('name')->implode(', '));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->info("Found local brand: {$localBrand->name} (ID: {$localBrand->id})");
|
||||
|
||||
// Create brands directory if it doesn't exist
|
||||
if (! Storage::disk('public')->exists('brands')) {
|
||||
Storage::disk('public')->makeDirectory('brands');
|
||||
$this->info('Created brands directory');
|
||||
}
|
||||
|
||||
// Initialize Intervention Image
|
||||
$manager = new ImageManager(new Driver);
|
||||
|
||||
// Process logo image with thumbnails (save as PNG for transparency support)
|
||||
if ($remoteBrand['image']) {
|
||||
$logoPath = "brands/{$localBrand->slug}-logo.png";
|
||||
|
||||
// Read and process the original image
|
||||
$originalImage = $manager->read($remoteBrand['image']);
|
||||
|
||||
// Try to remove white background by making white pixels transparent
|
||||
// Sample corners to detect if background is white
|
||||
$width = $originalImage->width();
|
||||
$height = $originalImage->height();
|
||||
|
||||
// Use GD to manipulate pixels
|
||||
$gdImage = imagecreatefromstring($remoteBrand['image']);
|
||||
if ($gdImage !== false) {
|
||||
// Enable alpha blending
|
||||
imagealphablending($gdImage, false);
|
||||
imagesavealpha($gdImage, true);
|
||||
|
||||
// Make white and near-white pixels transparent
|
||||
for ($x = 0; $x < imagesx($gdImage); $x++) {
|
||||
for ($y = 0; $y < imagesy($gdImage); $y++) {
|
||||
$rgb = imagecolorat($gdImage, $x, $y);
|
||||
$colors = imagecolorsforindex($gdImage, $rgb);
|
||||
|
||||
// If pixel is white or very close to white (RGB > 245)
|
||||
if ($colors['red'] > 245 && $colors['green'] > 245 && $colors['blue'] > 245) {
|
||||
$transparent = imagecolorallocatealpha($gdImage, 255, 255, 255, 127);
|
||||
imagesetpixel($gdImage, $x, $y, $transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save as PNG
|
||||
ob_start();
|
||||
imagepng($gdImage);
|
||||
$processedData = ob_get_clean();
|
||||
imagedestroy($gdImage);
|
||||
|
||||
Storage::disk('public')->put($logoPath, $processedData);
|
||||
$originalImage = $manager->read($processedData);
|
||||
} else {
|
||||
// Fallback: save original as PNG
|
||||
Storage::disk('public')->put($logoPath, $originalImage->toPng());
|
||||
}
|
||||
|
||||
// Generate thumbnails optimized for retina displays (PNG for transparency)
|
||||
// Thumbnail (160x160) for list views (2x retina at 80px)
|
||||
$thumbRetina = clone $originalImage;
|
||||
$thumbRetina->scale(width: 160);
|
||||
Storage::disk('public')->put("brands/{$localBrand->slug}-logo-thumb.png", $thumbRetina->toPng());
|
||||
|
||||
// Medium (600x600) for product cards (2x retina at 300px)
|
||||
$mediumRetina = clone $originalImage;
|
||||
$mediumRetina->scale(width: 600);
|
||||
Storage::disk('public')->put("brands/{$localBrand->slug}-logo-medium.png", $mediumRetina->toPng());
|
||||
|
||||
// Large (1600x1600) for detail views
|
||||
$largeRetina = clone $originalImage;
|
||||
$largeRetina->scale(width: 1600);
|
||||
Storage::disk('public')->put("brands/{$localBrand->slug}-logo-large.png", $largeRetina->toPng());
|
||||
|
||||
$localBrand->logo_path = $logoPath;
|
||||
$this->info("✓ Saved logo + thumbnails: {$logoPath} (".strlen($remoteBrand['image']).' bytes)');
|
||||
}
|
||||
|
||||
// Process banner image with thumbnails
|
||||
if ($remoteBrand['banner']) {
|
||||
$bannerPath = "brands/{$localBrand->slug}-banner.jpg";
|
||||
|
||||
// Save original
|
||||
Storage::disk('public')->put($bannerPath, $remoteBrand['banner']);
|
||||
|
||||
// Generate banner thumbnails if banner is large enough
|
||||
if (strlen($remoteBrand['banner']) > 1000) {
|
||||
$image = $manager->read($remoteBrand['banner']);
|
||||
|
||||
// Medium banner (1344px wide) for retina displays at 672px
|
||||
$mediumBanner = clone $image;
|
||||
$mediumBanner->scale(width: 1344);
|
||||
Storage::disk('public')->put("brands/{$localBrand->slug}-banner-medium.jpg", $mediumBanner->toJpeg(quality: 92));
|
||||
|
||||
// Large banner (2560px wide) for full-width hero sections
|
||||
$largeBanner = clone $image;
|
||||
$largeBanner->scale(width: 2560);
|
||||
Storage::disk('public')->put("brands/{$localBrand->slug}-banner-large.jpg", $largeBanner->toJpeg(quality: 92));
|
||||
}
|
||||
|
||||
$localBrand->banner_path = $bannerPath;
|
||||
$this->info("✓ Saved banner + thumbnails: {$bannerPath} (".strlen($remoteBrand['banner']).' bytes)');
|
||||
}
|
||||
|
||||
// Helper function to sanitize text (convert Windows-1252 to UTF-8)
|
||||
$sanitize = function ($text) {
|
||||
if (! $text) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
// First, convert from Windows-1252/ISO-8859-1 to UTF-8
|
||||
$text = mb_convert_encoding($text, 'UTF-8', 'Windows-1252');
|
||||
|
||||
// Replace common Windows-1252 special characters with standard equivalents
|
||||
$replacements = [
|
||||
"\xE2\x80\x98" => "'", // Left single quote
|
||||
"\xE2\x80\x99" => "'", // Right single quote (apostrophe)
|
||||
"\xE2\x80\x9C" => '"', // Left double quote
|
||||
"\xE2\x80\x9D" => '"', // Right double quote
|
||||
"\xE2\x80\x93" => '-', // En dash
|
||||
"\xE2\x80\x94" => '-', // Em dash
|
||||
"\xE2\x80\x26" => '...', // Ellipsis
|
||||
];
|
||||
|
||||
$text = str_replace(array_keys($replacements), array_values($replacements), $text);
|
||||
|
||||
return trim($text);
|
||||
};
|
||||
|
||||
// Update other brand fields
|
||||
$updates = [];
|
||||
|
||||
if ($remoteBrand['tagline']) {
|
||||
$localBrand->tagline = $sanitize($remoteBrand['tagline']);
|
||||
$updates[] = 'tagline';
|
||||
}
|
||||
|
||||
if ($remoteBrand['short_desc']) {
|
||||
$localBrand->description = $sanitize($remoteBrand['short_desc']);
|
||||
$updates[] = 'description';
|
||||
}
|
||||
|
||||
if ($remoteBrand['desc']) {
|
||||
$localBrand->long_description = $sanitize($remoteBrand['desc']);
|
||||
$updates[] = 'long_description';
|
||||
}
|
||||
|
||||
if ($remoteBrand['url']) {
|
||||
$localBrand->website_url = $remoteBrand['url'];
|
||||
$updates[] = 'website_url';
|
||||
}
|
||||
|
||||
// Address fields
|
||||
if ($remoteBrand['address']) {
|
||||
$localBrand->address = $remoteBrand['address'];
|
||||
$updates[] = 'address';
|
||||
}
|
||||
|
||||
if ($remoteBrand['unit_number']) {
|
||||
$localBrand->unit_number = $remoteBrand['unit_number'];
|
||||
$updates[] = 'unit_number';
|
||||
}
|
||||
|
||||
if ($remoteBrand['city']) {
|
||||
$localBrand->city = $remoteBrand['city'];
|
||||
$updates[] = 'city';
|
||||
}
|
||||
|
||||
if ($remoteBrand['state']) {
|
||||
$localBrand->state = $remoteBrand['state'];
|
||||
$updates[] = 'state';
|
||||
}
|
||||
|
||||
if ($remoteBrand['zip']) {
|
||||
$localBrand->zip_code = $remoteBrand['zip'];
|
||||
$updates[] = 'zip_code';
|
||||
}
|
||||
|
||||
if ($remoteBrand['phone']) {
|
||||
$localBrand->phone = $remoteBrand['phone'];
|
||||
$updates[] = 'phone';
|
||||
}
|
||||
|
||||
// Social media
|
||||
if ($remoteBrand['fb']) {
|
||||
$localBrand->facebook_url = 'https://facebook.com/'.$remoteBrand['fb'];
|
||||
$updates[] = 'facebook_url';
|
||||
}
|
||||
|
||||
if ($remoteBrand['insta']) {
|
||||
$localBrand->instagram_handle = $remoteBrand['insta'];
|
||||
$updates[] = 'instagram_handle';
|
||||
}
|
||||
|
||||
if ($remoteBrand['twitter']) {
|
||||
$localBrand->twitter_handle = $remoteBrand['twitter'];
|
||||
$updates[] = 'twitter_handle';
|
||||
}
|
||||
|
||||
if ($remoteBrand['youtube']) {
|
||||
$localBrand->youtube_url = $remoteBrand['youtube'];
|
||||
$updates[] = 'youtube_url';
|
||||
}
|
||||
|
||||
// Visibility
|
||||
$localBrand->is_public = (bool) $remoteBrand['public'];
|
||||
$updates[] = 'is_public';
|
||||
|
||||
// Save the brand
|
||||
$localBrand->save();
|
||||
|
||||
$this->info("\n✓ Successfully imported brand data!");
|
||||
$this->info('Updated fields: '.implode(', ', $updates));
|
||||
|
||||
$this->newLine();
|
||||
$this->info('View the brand at:');
|
||||
$this->line("http://localhost/s/cannabrands/brands/{$localBrand->hashid}/edit");
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Error: '.$e->getMessage());
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
158
app/Console/Commands/SeedCoaData.php
Normal file
158
app/Console/Commands/SeedCoaData.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Batch;
|
||||
use App\Models\BatchCoaFile;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class SeedCoaData extends Command
|
||||
{
|
||||
protected $signature = 'seed:coa-data';
|
||||
|
||||
protected $description = 'Add COA files to existing batches for testing';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('Seeding COA data for testing...');
|
||||
|
||||
// Get all active products with batches
|
||||
$products = Product::with('batches')
|
||||
->where('is_active', true)
|
||||
->whereHas('batches')
|
||||
->get();
|
||||
|
||||
if ($products->isEmpty()) {
|
||||
$this->warn('No products with batches found. Run the main seeder first.');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->info("Found {$products->count()} products with batches");
|
||||
|
||||
$coaCount = 0;
|
||||
|
||||
foreach ($products as $product) {
|
||||
foreach ($product->batches as $batch) {
|
||||
// Skip if batch already has COAs
|
||||
if ($batch->coaFiles()->exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create 1-2 COA files per batch
|
||||
$numCoas = rand(1, 2);
|
||||
|
||||
for ($i = 1; $i <= $numCoas; $i++) {
|
||||
$isPrimary = ($i === 1);
|
||||
|
||||
// Create a dummy PDF file
|
||||
$fileName = "COA-{$batch->batch_number}-{$i}.pdf";
|
||||
$filePath = "businesses/{$product->brand->business->uuid}/batches/{$batch->id}/coas/{$fileName}";
|
||||
|
||||
// Create dummy PDF content (just for testing)
|
||||
$pdfContent = $this->generateDummyPdf($batch, $product);
|
||||
Storage::disk('local')->put($filePath, $pdfContent);
|
||||
|
||||
// Create COA file record
|
||||
BatchCoaFile::create([
|
||||
'batch_id' => $batch->id,
|
||||
'file_name' => $fileName,
|
||||
'file_path' => $filePath,
|
||||
'file_size' => strlen($pdfContent),
|
||||
'mime_type' => 'application/pdf',
|
||||
'is_primary' => $isPrimary,
|
||||
'display_order' => $i,
|
||||
]);
|
||||
|
||||
$coaCount++;
|
||||
}
|
||||
|
||||
$this->line(" Added {$numCoas} COA(s) for batch {$batch->batch_number}");
|
||||
}
|
||||
}
|
||||
|
||||
$this->info("✓ Created {$coaCount} COA files");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function generateDummyPdf(Batch $batch, Product $product): string
|
||||
{
|
||||
// Generate a simple text-based "PDF" for testing
|
||||
// In a real system, you'd use a PDF library
|
||||
return "%PDF-1.4
|
||||
1 0 obj
|
||||
<<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/Type /Pages
|
||||
/Kids [3 0 R]
|
||||
/Count 1
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 <<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Helvetica
|
||||
>>
|
||||
>>
|
||||
>>
|
||||
/MediaBox [0 0 612 792]
|
||||
/Contents 4 0 R
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/Length 250
|
||||
>>
|
||||
stream
|
||||
BT
|
||||
/F1 12 Tf
|
||||
50 700 Td
|
||||
(CERTIFICATE OF ANALYSIS) Tj
|
||||
0 -30 Td
|
||||
(Batch Number: {$batch->batch_number}) Tj
|
||||
0 -20 Td
|
||||
(Product: {$product->name}) Tj
|
||||
0 -20 Td
|
||||
(Test Date: ".now()->format('Y-m-d').') Tj
|
||||
0 -30 Td
|
||||
(THC: 25.5%) Tj
|
||||
0 -20 Td
|
||||
(CBD: 0.8%) Tj
|
||||
0 -20 Td
|
||||
(Status: PASSED) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 5
|
||||
0000000000 65535 f
|
||||
0000000009 00000 n
|
||||
0000000058 00000 n
|
||||
0000000115 00000 n
|
||||
0000000317 00000 n
|
||||
trailer
|
||||
<<
|
||||
/Size 5
|
||||
/Root 1 0 R
|
||||
>>
|
||||
startxref
|
||||
619
|
||||
%%EOF';
|
||||
}
|
||||
}
|
||||
225
app/Console/Commands/SeedTestOrders.php
Normal file
225
app/Console/Commands/SeedTestOrders.php
Normal file
@@ -0,0 +1,225 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Batch;
|
||||
use App\Models\Business;
|
||||
use App\Models\Location;
|
||||
use App\Models\Order;
|
||||
use App\Models\OrderItem;
|
||||
use App\Models\Product;
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class SeedTestOrders extends Command
|
||||
{
|
||||
protected $signature = 'seed:test-orders {--clean : Delete existing test orders first}';
|
||||
|
||||
protected $description = 'Create test orders at various statuses for testing the order flow';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
if ($this->option('clean')) {
|
||||
$this->info('Cleaning up existing test orders...');
|
||||
$testOrders = Order::where('order_number', 'like', 'TEST-%')->get();
|
||||
foreach ($testOrders as $order) {
|
||||
// Delete order items first, then the order
|
||||
$order->items()->delete();
|
||||
$order->delete();
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('Creating test orders at various statuses...');
|
||||
|
||||
// Get a buyer business (retailer) and location
|
||||
$buyerBusiness = Business::where('business_type', 'retailer')->first();
|
||||
if (! $buyerBusiness) {
|
||||
$this->error('No buyer business found. Run the main seeder first.');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$buyerLocation = Location::where('business_id', $buyerBusiness->id)->first();
|
||||
if (! $buyerLocation) {
|
||||
$this->error('No buyer location found. Run the main seeder first.');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Get a buyer user
|
||||
$buyerUser = User::where('user_type', 'buyer')->first();
|
||||
if (! $buyerUser) {
|
||||
$this->error('No buyer user found. Run the main seeder first.');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Get products with batches and COAs
|
||||
$products = Product::with(['brand.business', 'batches.coaFiles'])
|
||||
->where('is_active', true)
|
||||
->whereHas('batches.coaFiles')
|
||||
->limit(10)
|
||||
->get();
|
||||
|
||||
if ($products->isEmpty()) {
|
||||
$this->error('No products with COAs found. Run seed:coa-data first.');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$orders = [];
|
||||
|
||||
// 1. Order ready for pre-delivery review (after picking, before delivery)
|
||||
$orders[] = $this->createTestOrder(
|
||||
$buyerBusiness,
|
||||
$buyerLocation,
|
||||
$products->random(3),
|
||||
'ready_for_delivery',
|
||||
'TEST-PREDELIVERY-001',
|
||||
'Order ready for pre-delivery review (Review #1)'
|
||||
);
|
||||
|
||||
// 2. Order delivered and ready for post-delivery acceptance (Review #2)
|
||||
$orders[] = $this->createTestOrder(
|
||||
$buyerBusiness,
|
||||
$buyerLocation,
|
||||
$products->random(3),
|
||||
'delivered',
|
||||
'TEST-DELIVERED-001',
|
||||
'Order delivered and ready for acceptance (Review #2)'
|
||||
);
|
||||
|
||||
// 3. Order in progress (picking)
|
||||
$orders[] = $this->createTestOrder(
|
||||
$buyerBusiness,
|
||||
$buyerLocation,
|
||||
$products->random(2),
|
||||
'in_progress',
|
||||
'TEST-PICKING-001',
|
||||
'Order currently being picked'
|
||||
);
|
||||
|
||||
// 4. Order accepted and approved for delivery
|
||||
$orders[] = $this->createTestOrder(
|
||||
$buyerBusiness,
|
||||
$buyerLocation,
|
||||
$products->random(2),
|
||||
'approved_for_delivery',
|
||||
'TEST-APPROVED-001',
|
||||
'Order approved for delivery (passed Review #1)'
|
||||
);
|
||||
|
||||
// 5. Order out for delivery
|
||||
$orders[] = $this->createTestOrder(
|
||||
$buyerBusiness,
|
||||
$buyerLocation,
|
||||
$products->random(2),
|
||||
'out_for_delivery',
|
||||
'TEST-OUTDELIVERY-001',
|
||||
'Order out for delivery'
|
||||
);
|
||||
|
||||
$this->newLine();
|
||||
$this->info('✓ Created '.count($orders).' test orders');
|
||||
$this->newLine();
|
||||
|
||||
$this->table(
|
||||
['Order Number', 'Status', 'Items', 'Description'],
|
||||
collect($orders)->map(fn ($order) => [
|
||||
$order->order_number,
|
||||
$order->status,
|
||||
$order->items->count(),
|
||||
$this->getOrderDescription($order->order_number),
|
||||
])
|
||||
);
|
||||
|
||||
$this->newLine();
|
||||
$this->info('You can now test the order flow in the UI:');
|
||||
$this->line(' • Pre-delivery review: /b/'.$buyerBusiness->slug.'/orders/TEST-PREDELIVERY-001/pre-delivery-review');
|
||||
$this->line(' • Post-delivery acceptance: /b/'.$buyerBusiness->slug.'/orders/TEST-DELIVERED-001/acceptance');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function createTestOrder(
|
||||
Business $buyerBusiness,
|
||||
Location $buyerLocation,
|
||||
$products,
|
||||
string $status,
|
||||
string $orderNumber,
|
||||
string $description
|
||||
): Order {
|
||||
return DB::transaction(function () use ($buyerBusiness, $buyerLocation, $products, $status, $orderNumber) {
|
||||
// Get first product's seller business
|
||||
$sellerBusiness = $products->first()->brand->business;
|
||||
|
||||
// Calculate totals
|
||||
$subtotal = $products->sum(function ($product) {
|
||||
return $product->wholesale_price * 5; // 5 units each
|
||||
});
|
||||
|
||||
$surchargePercent = Order::getSurchargePercentage('net_30');
|
||||
$surcharge = $subtotal * ($surchargePercent / 100);
|
||||
$taxRate = $buyerBusiness->getTaxRate();
|
||||
$tax = ($subtotal + $surcharge) * $taxRate;
|
||||
$total = $subtotal + $surcharge + $tax;
|
||||
|
||||
// Create order
|
||||
$order = Order::create([
|
||||
'order_number' => $orderNumber,
|
||||
'business_id' => $buyerBusiness->id,
|
||||
'seller_business_id' => $sellerBusiness->id,
|
||||
'location_id' => $buyerLocation->id,
|
||||
'status' => $status,
|
||||
'fulfillment_method' => 'delivery',
|
||||
'payment_terms' => 'net_30',
|
||||
'subtotal' => $subtotal,
|
||||
'tax' => $tax,
|
||||
'surcharge' => $surcharge,
|
||||
'total' => $total,
|
||||
'notes' => 'Test order for flow testing',
|
||||
]);
|
||||
|
||||
// Create order items with batch allocation
|
||||
foreach ($products as $product) {
|
||||
$batch = $product->batches->first();
|
||||
$quantity = 5;
|
||||
|
||||
// Allocate inventory
|
||||
if ($batch) {
|
||||
$batch->allocate($quantity);
|
||||
}
|
||||
|
||||
OrderItem::create([
|
||||
'order_id' => $order->id,
|
||||
'product_id' => $product->id,
|
||||
'batch_id' => $batch?->id,
|
||||
'product_name' => $product->name,
|
||||
'product_sku' => $product->sku,
|
||||
'brand_name' => $product->brand->name,
|
||||
'batch_number' => $batch?->batch_number,
|
||||
'quantity' => $quantity,
|
||||
'unit_price' => $product->wholesale_price,
|
||||
'line_total' => $product->wholesale_price * $quantity,
|
||||
]);
|
||||
}
|
||||
|
||||
return $order->fresh(['items']);
|
||||
});
|
||||
}
|
||||
|
||||
private function getOrderDescription(string $orderNumber): string
|
||||
{
|
||||
return match (true) {
|
||||
str_contains($orderNumber, 'PREDELIVERY') => 'Order ready for pre-delivery review (Review #1)',
|
||||
str_contains($orderNumber, 'DELIVERED') => 'Order delivered and ready for acceptance (Review #2)',
|
||||
str_contains($orderNumber, 'PICKING') => 'Order currently being picked',
|
||||
str_contains($orderNumber, 'APPROVED') => 'Order approved for delivery (passed Review #1)',
|
||||
str_contains($orderNumber, 'OUTDELIVERY') => 'Order out for delivery',
|
||||
default => 'Test order',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,10 @@ class Kernel extends ConsoleKernel
|
||||
*/
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
// $schedule->command('inspire')->hourly();
|
||||
// Check for scheduled broadcasts every minute
|
||||
$schedule->job(new \App\Jobs\Marketing\ProcessScheduledBroadcastsJob)
|
||||
->everyMinute()
|
||||
->withoutOverlapping();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
68
app/Events/CartUpdated.php
Normal file
68
app/Events/CartUpdated.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
/**
|
||||
* Cart Updated Event
|
||||
*
|
||||
* Broadcasts real-time cart count updates to authenticated users.
|
||||
* Best practices:
|
||||
* - Uses ShouldBroadcast for automatic broadcasting
|
||||
* - Broadcasts on private channel (user-specific)
|
||||
* - Includes minimal data (just count, not full cart)
|
||||
* - Uses custom event name via broadcastAs()
|
||||
*/
|
||||
class CartUpdated implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public int $userId,
|
||||
public int $count
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return array<int, \Illuminate\Broadcasting\Channel>
|
||||
*/
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
// Private channel - only authenticated user can listen
|
||||
return [
|
||||
new PrivateChannel('user.'.$this->userId),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The event's broadcast name.
|
||||
*/
|
||||
public function broadcastAs(): string
|
||||
{
|
||||
return 'CartUpdated';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data to broadcast.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function broadcastWith(): array
|
||||
{
|
||||
return [
|
||||
'count' => $this->count,
|
||||
];
|
||||
}
|
||||
}
|
||||
56
app/Events/HighIntentBuyerDetected.php
Normal file
56
app/Events/HighIntentBuyerDetected.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\Analytics\BuyerEngagementScore;
|
||||
use App\Models\Analytics\IntentSignal;
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class HighIntentBuyerDetected implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public int $sellerBusinessId,
|
||||
public int $buyerBusinessId,
|
||||
public IntentSignal $signal,
|
||||
public ?BuyerEngagementScore $engagementScore = null
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*/
|
||||
public function broadcastOn(): Channel
|
||||
{
|
||||
return new Channel("business.{$this->sellerBusinessId}.analytics");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data to broadcast.
|
||||
*/
|
||||
public function broadcastWith(): array
|
||||
{
|
||||
return [
|
||||
'buyer_business_id' => $this->buyerBusinessId,
|
||||
'buyer_business_name' => $this->signal->buyerBusiness?->name,
|
||||
'signal_type' => $this->signal->signal_type,
|
||||
'signal_strength' => $this->signal->signal_strength,
|
||||
'product_id' => $this->signal->subject_type === 'App\Models\Product' ? $this->signal->subject_id : null,
|
||||
'total_engagement_score' => $this->engagementScore?->total_score,
|
||||
'detected_at' => $this->signal->detected_at->toIso8601String(),
|
||||
'context' => $this->signal->context,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The event's broadcast name.
|
||||
*/
|
||||
public function broadcastAs(): string
|
||||
{
|
||||
return 'high-intent-buyer-detected';
|
||||
}
|
||||
}
|
||||
78
app/Events/PickingProgressUpdated.php
Normal file
78
app/Events/PickingProgressUpdated.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
/**
|
||||
* Picking Progress Updated Event
|
||||
*
|
||||
* Broadcasts real-time picking progress updates to all workers on the same picking ticket.
|
||||
* Use case: Multiple warehouse workers picking different items from the same order simultaneously.
|
||||
*
|
||||
* Best practices:
|
||||
* - Uses ShouldBroadcast for automatic broadcasting
|
||||
* - Broadcasts on private channel (order-specific)
|
||||
* - Includes minimal data (item ID, picked quantity, progress)
|
||||
* - Uses custom event name via broadcastAs()
|
||||
*/
|
||||
class PickingProgressUpdated implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param int $orderId The order ID
|
||||
* @param int $itemId The order item ID that was updated
|
||||
* @param int $pickedQty The new picked quantity for this item
|
||||
* @param float $progress Overall picking progress percentage (0-100)
|
||||
*/
|
||||
public function __construct(
|
||||
public int $orderId,
|
||||
public int $itemId,
|
||||
public int $pickedQty,
|
||||
public float $progress
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return array<int, \Illuminate\Broadcasting\Channel>
|
||||
*/
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
// Private channel - only authorized users (sellers on this order) can listen
|
||||
return [
|
||||
new PrivateChannel('picking-ticket.'.$this->orderId),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The event's broadcast name.
|
||||
*/
|
||||
public function broadcastAs(): string
|
||||
{
|
||||
return 'PickingProgressUpdated';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data to broadcast.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function broadcastWith(): array
|
||||
{
|
||||
return [
|
||||
'itemId' => $this->itemId,
|
||||
'pickedQty' => $this->pickedQty,
|
||||
'progress' => $this->progress,
|
||||
];
|
||||
}
|
||||
}
|
||||
192
app/Filament/Pages/NotificationSettings.php
Normal file
192
app/Filament/Pages/NotificationSettings.php
Normal file
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
|
||||
class NotificationSettings extends Page
|
||||
{
|
||||
protected static \BackedEnum|string|null $navigationIcon = 'heroicon-o-envelope';
|
||||
|
||||
protected string $view = 'filament.pages.notification-settings';
|
||||
|
||||
protected static \UnitEnum|string|null $navigationGroup = 'System';
|
||||
|
||||
protected static ?string $navigationLabel = 'Notification Settings';
|
||||
|
||||
protected static ?int $navigationSort = 98;
|
||||
|
||||
public ?array $data = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->form->fill([
|
||||
// Mail settings
|
||||
'mail_driver' => config('mail.default'),
|
||||
'mail_host' => config('mail.mailers.smtp.host'),
|
||||
'mail_port' => config('mail.mailers.smtp.port'),
|
||||
'mail_username' => config('mail.mailers.smtp.username'),
|
||||
'mail_password' => config('mail.mailers.smtp.password'),
|
||||
'mail_encryption' => config('mail.mailers.smtp.encryption'),
|
||||
'mail_from_address' => config('mail.from.address'),
|
||||
'mail_from_name' => config('mail.from.name'),
|
||||
|
||||
// SMS settings (Twilio example)
|
||||
'sms_enabled' => env('SMS_ENABLED', false),
|
||||
'sms_provider' => env('SMS_PROVIDER', 'twilio'),
|
||||
'twilio_sid' => env('TWILIO_SID'),
|
||||
'twilio_auth_token' => env('TWILIO_AUTH_TOKEN'),
|
||||
'twilio_phone_number' => env('TWILIO_PHONE_NUMBER'),
|
||||
|
||||
// WhatsApp settings
|
||||
'whatsapp_enabled' => env('WHATSAPP_ENABLED', false),
|
||||
'whatsapp_provider' => env('WHATSAPP_PROVIDER', 'twilio'),
|
||||
'whatsapp_business_number' => env('WHATSAPP_BUSINESS_NUMBER'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Tabs::make('Notification Providers')
|
||||
->tabs([
|
||||
Forms\Components\Tabs\Tab::make('Email')
|
||||
->icon('heroicon-o-envelope')
|
||||
->schema([
|
||||
Forms\Components\Section::make('Email Provider Configuration')
|
||||
->description('Configure your email provider for sending transactional emails')
|
||||
->schema([
|
||||
Forms\Components\Select::make('mail_driver')
|
||||
->label('Mail Driver')
|
||||
->options([
|
||||
'smtp' => 'SMTP',
|
||||
'sendmail' => 'Sendmail',
|
||||
'mailgun' => 'Mailgun',
|
||||
'ses' => 'Amazon SES',
|
||||
'postmark' => 'Postmark',
|
||||
])
|
||||
->required()
|
||||
->reactive(),
|
||||
Forms\Components\Grid::make(2)
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('mail_host')
|
||||
->label('SMTP Host')
|
||||
->required()
|
||||
->visible(fn ($get) => $get('mail_driver') === 'smtp'),
|
||||
Forms\Components\TextInput::make('mail_port')
|
||||
->label('SMTP Port')
|
||||
->required()
|
||||
->numeric()
|
||||
->visible(fn ($get) => $get('mail_driver') === 'smtp'),
|
||||
Forms\Components\TextInput::make('mail_username')
|
||||
->label('Username')
|
||||
->visible(fn ($get) => $get('mail_driver') === 'smtp'),
|
||||
Forms\Components\TextInput::make('mail_password')
|
||||
->label('Password')
|
||||
->password()
|
||||
->revealable()
|
||||
->visible(fn ($get) => $get('mail_driver') === 'smtp'),
|
||||
Forms\Components\Select::make('mail_encryption')
|
||||
->label('Encryption')
|
||||
->options([
|
||||
'tls' => 'TLS',
|
||||
'ssl' => 'SSL',
|
||||
'' => 'None',
|
||||
])
|
||||
->visible(fn ($get) => $get('mail_driver') === 'smtp'),
|
||||
Forms\Components\TextInput::make('mail_from_address')
|
||||
->label('From Address')
|
||||
->email()
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('mail_from_name')
|
||||
->label('From Name')
|
||||
->required(),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Forms\Components\Tabs\Tab::make('SMS')
|
||||
->icon('heroicon-o-device-phone-mobile')
|
||||
->schema([
|
||||
Forms\Components\Section::make('SMS Provider Configuration')
|
||||
->description('Configure your SMS provider for sending text messages')
|
||||
->schema([
|
||||
Forms\Components\Toggle::make('sms_enabled')
|
||||
->label('Enable SMS Notifications')
|
||||
->reactive(),
|
||||
Forms\Components\Select::make('sms_provider')
|
||||
->label('SMS Provider')
|
||||
->options([
|
||||
'twilio' => 'Twilio',
|
||||
'nexmo' => 'Vonage (Nexmo)',
|
||||
'aws_sns' => 'AWS SNS',
|
||||
])
|
||||
->required()
|
||||
->reactive()
|
||||
->visible(fn ($get) => $get('sms_enabled')),
|
||||
Forms\Components\Grid::make(2)
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('twilio_sid')
|
||||
->label('Twilio Account SID')
|
||||
->required()
|
||||
->visible(fn ($get) => $get('sms_enabled') && $get('sms_provider') === 'twilio'),
|
||||
Forms\Components\TextInput::make('twilio_auth_token')
|
||||
->label('Twilio Auth Token')
|
||||
->password()
|
||||
->revealable()
|
||||
->required()
|
||||
->visible(fn ($get) => $get('sms_enabled') && $get('sms_provider') === 'twilio'),
|
||||
Forms\Components\TextInput::make('twilio_phone_number')
|
||||
->label('Twilio Phone Number')
|
||||
->tel()
|
||||
->required()
|
||||
->visible(fn ($get) => $get('sms_enabled') && $get('sms_provider') === 'twilio'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Forms\Components\Tabs\Tab::make('WhatsApp')
|
||||
->icon('heroicon-o-chat-bubble-left-right')
|
||||
->schema([
|
||||
Forms\Components\Section::make('WhatsApp Configuration')
|
||||
->description('Configure WhatsApp Business API for sending messages')
|
||||
->schema([
|
||||
Forms\Components\Toggle::make('whatsapp_enabled')
|
||||
->label('Enable WhatsApp Notifications')
|
||||
->reactive(),
|
||||
Forms\Components\Select::make('whatsapp_provider')
|
||||
->label('WhatsApp Provider')
|
||||
->options([
|
||||
'twilio' => 'Twilio WhatsApp',
|
||||
'whatsapp_cloud' => 'WhatsApp Cloud API',
|
||||
])
|
||||
->required()
|
||||
->reactive()
|
||||
->visible(fn ($get) => $get('whatsapp_enabled')),
|
||||
Forms\Components\TextInput::make('whatsapp_business_number')
|
||||
->label('WhatsApp Business Number')
|
||||
->tel()
|
||||
->required()
|
||||
->visible(fn ($get) => $get('whatsapp_enabled')),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->statePath('data');
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
// TODO: Save settings to environment file or database
|
||||
// For now, this would require implementing a settings storage system
|
||||
|
||||
Notification::make()
|
||||
->title('Settings saved')
|
||||
->success()
|
||||
->body('Note: These settings are read from .env file. To persist changes, update your .env file.')
|
||||
->send();
|
||||
}
|
||||
}
|
||||
160
app/Filament/Resources/BatchResource.php
Normal file
160
app/Filament/Resources/BatchResource.php
Normal file
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\Batches\Schemas\BatchForm;
|
||||
use App\Filament\Resources\Batches\Tables\BatchesTable;
|
||||
use App\Filament\Resources\BatchResource\Pages;
|
||||
use App\Models\Batch;
|
||||
use App\Services\QrCodeService;
|
||||
use BackedEnum;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\BulkAction;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use UnitEnum;
|
||||
|
||||
class BatchResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Batch::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-archive-box';
|
||||
|
||||
protected static ?string $navigationLabel = 'Batches';
|
||||
|
||||
protected static UnitEnum|string|null $navigationGroup = 'Inventory';
|
||||
|
||||
protected static ?int $navigationSort = 2;
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return BatchForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
$table = BatchesTable::configure($table);
|
||||
|
||||
// Add custom QR and COA actions
|
||||
return $table
|
||||
->recordActions(array_merge(
|
||||
$table->getRecordActions(),
|
||||
[
|
||||
Action::make('generate_qr')
|
||||
->label('Generate QR')
|
||||
->icon('heroicon-o-qr-code')
|
||||
->action(function (Batch $record) {
|
||||
$qrService = app(QrCodeService::class);
|
||||
$result = $qrService->generateForBatch($record);
|
||||
|
||||
if ($result['success']) {
|
||||
Notification::make()
|
||||
->title('QR Code Generated')
|
||||
->body($result['message'])
|
||||
->success()
|
||||
->send();
|
||||
} else {
|
||||
Notification::make()
|
||||
->title('Failed to generate QR code')
|
||||
->body($result['message'])
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
})
|
||||
->visible(fn (Batch $record) => ! $record->qr_code_path),
|
||||
|
||||
Action::make('download_qr')
|
||||
->label('Download QR')
|
||||
->icon('heroicon-o-arrow-down-tray')
|
||||
->url(fn (Batch $record) => route('seller.business.manufacturing.batches.qr-code.download', [
|
||||
'business' => $record->business->slug,
|
||||
'batch' => $record->id,
|
||||
]))
|
||||
->openUrlInNewTab()
|
||||
->visible(fn (Batch $record) => $record->qr_code_path),
|
||||
|
||||
Action::make('regenerate_qr')
|
||||
->label('Regenerate QR')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->action(function (Batch $record) {
|
||||
$qrService = app(QrCodeService::class);
|
||||
$result = $qrService->regenerate($record);
|
||||
|
||||
if ($result['success']) {
|
||||
Notification::make()
|
||||
->title('QR Code Regenerated')
|
||||
->success()
|
||||
->send();
|
||||
} else {
|
||||
Notification::make()
|
||||
->title('Failed to regenerate QR code')
|
||||
->body($result['message'])
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
})
|
||||
->requiresConfirmation()
|
||||
->visible(fn (Batch $record) => $record->qr_code_path),
|
||||
|
||||
Action::make('view_coa')
|
||||
->label('View COA')
|
||||
->icon('heroicon-o-document-text')
|
||||
->url(fn (Batch $record) => route('public.coa.show', ['batchNumber' => $record->batch_number]))
|
||||
->openUrlInNewTab()
|
||||
->visible(fn (Batch $record) => $record->lab !== null),
|
||||
]
|
||||
))
|
||||
->bulkActions(array_merge(
|
||||
$table->getBulkActions(),
|
||||
[
|
||||
BulkAction::make('generate_qr_codes')
|
||||
->label('Generate QR Codes')
|
||||
->icon('heroicon-o-qr-code')
|
||||
->action(function (Collection $records) {
|
||||
$qrService = app(QrCodeService::class);
|
||||
$batchIds = $records->pluck('id')->toArray();
|
||||
$result = $qrService->bulkGenerate($batchIds);
|
||||
|
||||
Notification::make()
|
||||
->title("Generated {$result['successful']} QR codes")
|
||||
->body("Failed: {$result['failed']}")
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
]
|
||||
));
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
$query = parent::getEloquentQuery();
|
||||
|
||||
// Scope to user's business unless they're a super admin
|
||||
if (! auth()->user()->hasRole('super_admin')) {
|
||||
$query->where('business_id', auth()->user()->business_id);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListBatches::route('/'),
|
||||
'create' => Pages\CreateBatch::route('/create'),
|
||||
'view' => Pages\ViewBatch::route('/{record}'),
|
||||
'edit' => Pages\EditBatch::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
18
app/Filament/Resources/BatchResource/Pages/CreateBatch.php
Normal file
18
app/Filament/Resources/BatchResource/Pages/CreateBatch.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\BatchResource\Pages;
|
||||
|
||||
use App\Filament\Resources\BatchResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateBatch extends CreateRecord
|
||||
{
|
||||
protected static string $resource = BatchResource::class;
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$data['business_id'] = auth()->user()->business_id;
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
20
app/Filament/Resources/BatchResource/Pages/EditBatch.php
Normal file
20
app/Filament/Resources/BatchResource/Pages/EditBatch.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\BatchResource\Pages;
|
||||
|
||||
use App\Filament\Resources\BatchResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditBatch extends EditRecord
|
||||
{
|
||||
protected static string $resource = BatchResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\ViewAction::make(),
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
19
app/Filament/Resources/BatchResource/Pages/ListBatches.php
Normal file
19
app/Filament/Resources/BatchResource/Pages/ListBatches.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\BatchResource\Pages;
|
||||
|
||||
use App\Filament\Resources\BatchResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListBatches extends ListRecords
|
||||
{
|
||||
protected static string $resource = BatchResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
19
app/Filament/Resources/BatchResource/Pages/ViewBatch.php
Normal file
19
app/Filament/Resources/BatchResource/Pages/ViewBatch.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\BatchResource\Pages;
|
||||
|
||||
use App\Filament\Resources\BatchResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewBatch extends ViewRecord
|
||||
{
|
||||
protected static string $resource = BatchResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\EditAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Filament\Resources\Batches\Schemas;
|
||||
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
@@ -18,84 +19,144 @@ class BatchForm
|
||||
->components([
|
||||
Section::make('Batch Information')
|
||||
->schema([
|
||||
TextInput::make('batch_number')
|
||||
->label('Batch Number')
|
||||
->placeholder('Auto-generated if left blank')
|
||||
->maxLength(255)
|
||||
->helperText('Unique identifier for this batch'),
|
||||
|
||||
Select::make('product_id')
|
||||
->label('Product')
|
||||
->relationship('product', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->required()
|
||||
->columnSpan(2),
|
||||
TextInput::make('batch_number')
|
||||
->required()
|
||||
->unique(ignoreRecord: true)
|
||||
->helperText('Unique identifier for this batch (e.g., TB-AM-240315)'),
|
||||
TextInput::make('internal_code')
|
||||
->helperText('Internal production/tracking code (optional)'),
|
||||
])
|
||||
->columns(2),
|
||||
->required(),
|
||||
|
||||
Section::make('Production Dates')
|
||||
->schema([
|
||||
DatePicker::make('production_date')
|
||||
->helperText('Date the batch was produced/manufactured'),
|
||||
DatePicker::make('harvest_date')
|
||||
->helperText('Harvest date (for flower products)'),
|
||||
DatePicker::make('package_date')
|
||||
->helperText('Date the batch was packaged'),
|
||||
DatePicker::make('expiration_date')
|
||||
->helperText('Expiration/best-by date'),
|
||||
Select::make('batch_type')
|
||||
->label('Batch Type')
|
||||
->options([
|
||||
'intake' => 'Intake',
|
||||
'production' => 'Production',
|
||||
'finished' => 'Finished',
|
||||
])
|
||||
->default('finished')
|
||||
->required()
|
||||
->helperText('Type of batch in the production process'),
|
||||
|
||||
Select::make('lab_id')
|
||||
->label('Lab Test')
|
||||
->relationship('lab', 'lab_name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText('Associated lab test results'),
|
||||
|
||||
Select::make('parent_batch_id')
|
||||
->label('Parent Batch')
|
||||
->relationship('parentBatch', 'batch_number')
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText('Parent batch if this was produced from another batch'),
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
Section::make('Inventory Management')
|
||||
->schema([
|
||||
TextInput::make('quantity_produced')
|
||||
->label('Quantity Produced')
|
||||
->required()
|
||||
->numeric()
|
||||
->default(0)
|
||||
->helperText('Total units produced in this batch'),
|
||||
|
||||
TextInput::make('quantity_available')
|
||||
->label('Quantity Available')
|
||||
->required()
|
||||
->numeric()
|
||||
->default(0)
|
||||
->helperText('Units currently available for sale'),
|
||||
|
||||
TextInput::make('quantity_allocated')
|
||||
->label('Quantity Allocated')
|
||||
->numeric()
|
||||
->default(0)
|
||||
->disabled()
|
||||
->dehydrated(false)
|
||||
->helperText('Units reserved in pending orders (auto-calculated)'),
|
||||
|
||||
TextInput::make('quantity_sold')
|
||||
->label('Quantity Sold')
|
||||
->numeric()
|
||||
->default(0)
|
||||
->disabled()
|
||||
->dehydrated(false)
|
||||
->helperText('Units already sold (auto-calculated)'),
|
||||
])
|
||||
->columns(2)
|
||||
->columns(4)
|
||||
->description('Allocated and sold quantities are automatically managed by the system.'),
|
||||
|
||||
Section::make('Status & Compliance')
|
||||
Section::make('Dates')
|
||||
->schema([
|
||||
Toggle::make('is_active')
|
||||
->default(true)
|
||||
->helperText('Is this batch available for sale?'),
|
||||
Toggle::make('is_tested')
|
||||
->default(false)
|
||||
->helperText('Has this batch passed lab testing?'),
|
||||
Toggle::make('is_quarantined')
|
||||
->default(false)
|
||||
->helperText('Is this batch quarantined pending results?'),
|
||||
])
|
||||
->columns(3),
|
||||
DatePicker::make('production_date')
|
||||
->label('Production Date')
|
||||
->helperText('Date the batch was produced/manufactured'),
|
||||
|
||||
Section::make('Additional Information')
|
||||
DatePicker::make('intake_date')
|
||||
->label('Intake Date')
|
||||
->helperText('Date the batch was received/intake'),
|
||||
|
||||
DatePicker::make('expiration_date')
|
||||
->label('Expiration Date')
|
||||
->helperText('Expiration/best-by date'),
|
||||
|
||||
DatePicker::make('test_date')
|
||||
->label('Test Date')
|
||||
->helperText('Date of lab testing'),
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
Section::make('Warehouse & Location')
|
||||
->schema([
|
||||
TextInput::make('warehouse_location')
|
||||
->label('Warehouse Location')
|
||||
->placeholder('e.g., Shelf A-15')
|
||||
->maxLength(255)
|
||||
->helperText('Physical location in warehouse'),
|
||||
|
||||
TextInput::make('container_type')
|
||||
->label('Container Type')
|
||||
->placeholder('e.g., Turkey Bag, Box')
|
||||
->maxLength(255)
|
||||
->helperText('Type of container batch is stored in'),
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
Section::make('Quality & Compliance')
|
||||
->schema([
|
||||
Toggle::make('is_quarantined')
|
||||
->label('Quarantined')
|
||||
->default(false)
|
||||
->helperText('Is this batch quarantined?')
|
||||
->reactive(),
|
||||
|
||||
Textarea::make('quarantine_reason')
|
||||
->label('Quarantine Reason')
|
||||
->rows(2)
|
||||
->helperText('Reason for quarantine')
|
||||
->visible(fn (Forms\Get $get) => $get('is_quarantined'))
|
||||
->columnSpanFull(),
|
||||
|
||||
Toggle::make('is_released_for_sale')
|
||||
->label('Released for Sale')
|
||||
->default(false)
|
||||
->helperText('Has this batch been released for sale?'),
|
||||
|
||||
Textarea::make('notes')
|
||||
->label('Notes')
|
||||
->rows(3)
|
||||
->helperText('Production notes, special handling instructions, etc.')
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->collapsible(),
|
||||
->columns(2),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,18 +23,35 @@ class BatchesTable
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('batch_number')
|
||||
->label('Batch #')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->copyable()
|
||||
->weight('bold'),
|
||||
TextColumn::make('product.name')
|
||||
->label('Product')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->description(fn ($record) => $record->product->sku ?? null),
|
||||
->description(fn ($record) => $record->product->sku ?? null)
|
||||
->limit(30),
|
||||
TextColumn::make('batch_type')
|
||||
->label('Type')
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'intake' => 'info',
|
||||
'production' => 'warning',
|
||||
'finished' => 'success',
|
||||
default => 'gray',
|
||||
}),
|
||||
TextColumn::make('warehouse_location')
|
||||
->label('Location')
|
||||
->searchable()
|
||||
->toggleable(),
|
||||
TextColumn::make('production_date')
|
||||
->label('Produced')
|
||||
->date()
|
||||
->sortable()
|
||||
->toggleable(),
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('expiration_date')
|
||||
->date()
|
||||
->sortable()
|
||||
@@ -60,14 +77,13 @@ class BatchesTable
|
||||
->label('Status')
|
||||
->badge()
|
||||
->getStateUsing(fn ($record) => $record->is_quarantined ? 'Quarantined' :
|
||||
(! $record->is_active ? 'Inactive' :
|
||||
(! $record->is_tested ? 'Pending Test' : 'Active'))
|
||||
(! $record->is_released_for_sale ? 'Not Released' : 'Released')
|
||||
)
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'Active' => Color::Green,
|
||||
'Pending Test' => Color::Yellow,
|
||||
'Released' => Color::Green,
|
||||
'Not Released' => Color::Yellow,
|
||||
'Quarantined' => Color::Red,
|
||||
'Inactive' => Color::Gray,
|
||||
default => Color::Gray,
|
||||
}),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
@@ -80,19 +96,23 @@ class BatchesTable
|
||||
])
|
||||
->defaultSort('created_at', 'desc')
|
||||
->filters([
|
||||
SelectFilter::make('batch_type')
|
||||
->options([
|
||||
'intake' => 'Intake',
|
||||
'production' => 'Production',
|
||||
'finished' => 'Finished',
|
||||
]),
|
||||
SelectFilter::make('product')
|
||||
->relationship('product', 'name')
|
||||
->searchable()
|
||||
->preload(),
|
||||
Filter::make('active')
|
||||
->query(fn (Builder $query): Builder => $query->where('is_active', true))
|
||||
Filter::make('released')
|
||||
->label('Released for Sale')
|
||||
->query(fn (Builder $query): Builder => $query->where('is_released_for_sale', true))
|
||||
->toggle(),
|
||||
Filter::make('available')
|
||||
->query(fn (Builder $query): Builder => $query->where('quantity_available', '>', 0))
|
||||
->toggle(),
|
||||
Filter::make('tested')
|
||||
->query(fn (Builder $query): Builder => $query->where('is_tested', true))
|
||||
->toggle(),
|
||||
Filter::make('quarantined')
|
||||
->query(fn (Builder $query): Builder => $query->where('is_quarantined', true))
|
||||
->toggle(),
|
||||
|
||||
@@ -33,7 +33,10 @@ class BrandResource extends Resource
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count();
|
||||
// Cache brand count for 60 seconds to reduce database queries on every page load
|
||||
return cache()->remember('brand_count', 60, function () {
|
||||
return static::getModel()::count() ?: null;
|
||||
});
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
@@ -54,7 +57,7 @@ class BrandResource extends Resource
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
\Tapp\FilamentAuditing\RelationManagers\AuditsRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ class BrandsTable
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->modifyQueryUsing(fn ($query) => $query->with(['business']))
|
||||
->columns([
|
||||
TextColumn::make('business.name')
|
||||
->searchable(),
|
||||
|
||||
77
app/Filament/Resources/BusinessModuleResource.php
Normal file
77
app/Filament/Resources/BusinessModuleResource.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\BusinessModuleResource\Pages\CreateBusinessModule;
|
||||
use App\Filament\Resources\BusinessModuleResource\Pages\EditBusinessModule;
|
||||
use App\Filament\Resources\BusinessModuleResource\Pages\ListBusinessModules;
|
||||
use App\Filament\Resources\BusinessModuleResource\Pages\ViewBusinessModule;
|
||||
use App\Filament\Resources\BusinessModuleResource\Schemas\BusinessModuleForm;
|
||||
use App\Filament\Resources\BusinessModuleResource\Schemas\BusinessModuleInfolist;
|
||||
use App\Filament\Resources\BusinessModuleResource\Tables\BusinessModulesTable;
|
||||
use App\Models\BusinessModule;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
use UnitEnum;
|
||||
|
||||
class BusinessModuleResource extends Resource
|
||||
{
|
||||
protected static ?string $model = BusinessModule::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedCog;
|
||||
|
||||
protected static UnitEnum|string|null $navigationGroup = 'System';
|
||||
|
||||
protected static ?int $navigationSort = 11;
|
||||
|
||||
protected static ?string $navigationLabel = 'Business Modules';
|
||||
|
||||
protected static ?string $modelLabel = 'Business Module';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return cache()->remember('business_module_count', 60, function () {
|
||||
return static::getModel()::active()->count() ?: null;
|
||||
});
|
||||
}
|
||||
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
return auth()->check() && auth()->user()->user_type === 'admin';
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return BusinessModuleForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function infolist(Schema $schema): Schema
|
||||
{
|
||||
return BusinessModuleInfolist::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return BusinessModulesTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListBusinessModules::route('/'),
|
||||
'create' => CreateBusinessModule::route('/create'),
|
||||
'view' => ViewBusinessModule::route('/{record}'),
|
||||
'edit' => EditBusinessModule::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\BusinessModuleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\BusinessModuleResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateBusinessModule extends CreateRecord
|
||||
{
|
||||
protected static string $resource = BusinessModuleResource::class;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\BusinessModuleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\BusinessModuleResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditBusinessModule extends EditRecord
|
||||
{
|
||||
protected static string $resource = BusinessModuleResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
ViewAction::make(),
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\BusinessModuleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\BusinessModuleResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListBusinessModules extends ListRecords
|
||||
{
|
||||
protected static string $resource = BusinessModuleResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\BusinessModuleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\BusinessModuleResource;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewBusinessModule extends ViewRecord
|
||||
{
|
||||
protected static string $resource = BusinessModuleResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
EditAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\BusinessModuleResource\Schemas;
|
||||
|
||||
use App\Models\Module;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class BusinessModuleForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Section::make('Business Module Configuration')
|
||||
->schema([
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
Select::make('business_id')
|
||||
->relationship('business', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->required()
|
||||
->live()
|
||||
->helperText('Select the business'),
|
||||
|
||||
Select::make('module_key')
|
||||
->options(Module::active()->pluck('name', 'key'))
|
||||
->searchable()
|
||||
->required()
|
||||
->live()
|
||||
->helperText('Select the module to enable')
|
||||
->disabled(fn (?string $operation): bool => $operation === 'edit'),
|
||||
]),
|
||||
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
Toggle::make('enabled')
|
||||
->default(true)
|
||||
->helperText('Enable or disable this module'),
|
||||
|
||||
Select::make('activated_by')
|
||||
->relationship('activatedBy', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText('User who activated this module'),
|
||||
]),
|
||||
]),
|
||||
|
||||
Section::make('Pricing & Plan')
|
||||
->schema([
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
TextInput::make('plan')
|
||||
->maxLength(255)
|
||||
->helperText('Plan name (e.g., basic, pro, enterprise)'),
|
||||
|
||||
TextInput::make('monthly_price')
|
||||
->numeric()
|
||||
->prefix('$')
|
||||
->step(0.01)
|
||||
->helperText('Monthly subscription price'),
|
||||
]),
|
||||
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
DateTimePicker::make('activated_at')
|
||||
->default(now())
|
||||
->helperText('When the module was activated'),
|
||||
|
||||
DateTimePicker::make('expires_at')
|
||||
->helperText('Module expiration date (optional)'),
|
||||
]),
|
||||
])
|
||||
->collapsible(),
|
||||
|
||||
Section::make('Configuration & Limits')
|
||||
->schema([
|
||||
KeyValue::make('config')
|
||||
->label('Module Configuration')
|
||||
->keyLabel('Config Key')
|
||||
->valueLabel('Config Value')
|
||||
->helperText('Custom configuration for this business')
|
||||
->reorderable(),
|
||||
|
||||
KeyValue::make('limits')
|
||||
->label('Usage Limits')
|
||||
->keyLabel('Metric')
|
||||
->valueLabel('Limit')
|
||||
->helperText('Usage limits for this business (overrides defaults)')
|
||||
->default(function (Get $get) {
|
||||
$moduleKey = $get('module_key');
|
||||
if (! $moduleKey) {
|
||||
return [];
|
||||
}
|
||||
$module = Module::where('key', $moduleKey)->first();
|
||||
|
||||
return $module?->default_limits ?? [];
|
||||
})
|
||||
->reorderable(),
|
||||
])
|
||||
->collapsible(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\BusinessModuleResource\Schemas;
|
||||
|
||||
use Filament\Infolists\Components\Grid;
|
||||
use Filament\Infolists\Components\IconEntry;
|
||||
use Filament\Infolists\Components\KeyValueEntry;
|
||||
use Filament\Infolists\Components\Section;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class BusinessModuleInfolist
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Section::make('Business Module Details')
|
||||
->schema([
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
TextEntry::make('business.name')
|
||||
->label('Business'),
|
||||
|
||||
TextEntry::make('module_key')
|
||||
->label('Module')
|
||||
->badge()
|
||||
->color('info')
|
||||
->formatStateUsing(fn (string $state): string => \App\Models\Module::where('key', $state)->value('name') ?? $state
|
||||
),
|
||||
]),
|
||||
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
IconEntry::make('enabled')
|
||||
->boolean(),
|
||||
|
||||
IconEntry::make('is_active')
|
||||
->label('Active')
|
||||
->getStateUsing(fn ($record) => $record->isActive())
|
||||
->boolean(),
|
||||
]),
|
||||
]),
|
||||
|
||||
Section::make('Pricing & Plan')
|
||||
->schema([
|
||||
Grid::make(3)
|
||||
->schema([
|
||||
TextEntry::make('plan')
|
||||
->badge()
|
||||
->color('success')
|
||||
->default('N/A'),
|
||||
|
||||
TextEntry::make('monthly_price')
|
||||
->money('USD'),
|
||||
|
||||
TextEntry::make('activatedBy.name')
|
||||
->label('Activated By')
|
||||
->default('N/A'),
|
||||
]),
|
||||
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
TextEntry::make('activated_at')
|
||||
->dateTime(),
|
||||
|
||||
TextEntry::make('expires_at')
|
||||
->dateTime()
|
||||
->color(fn ($state) => $state && $state->isPast() ? 'danger' : null)
|
||||
->icon(fn ($state) => $state && $state->isPast() ? 'heroicon-o-exclamation-triangle' : null)
|
||||
->default('Never'),
|
||||
]),
|
||||
])
|
||||
->collapsible(),
|
||||
|
||||
Section::make('Configuration')
|
||||
->schema([
|
||||
KeyValueEntry::make('config')
|
||||
->label('Module Configuration'),
|
||||
|
||||
KeyValueEntry::make('limits')
|
||||
->label('Usage Limits'),
|
||||
])
|
||||
->collapsible(),
|
||||
|
||||
Section::make('Timestamps')
|
||||
->schema([
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
TextEntry::make('created_at')
|
||||
->dateTime(),
|
||||
|
||||
TextEntry::make('updated_at')
|
||||
->dateTime(),
|
||||
]),
|
||||
])
|
||||
->collapsible()
|
||||
->collapsed(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\BusinessModuleResource\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Support\Enums\FontWeight;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Filters\TernaryFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class BusinessModulesTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('business.name')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->weight(FontWeight::Bold),
|
||||
|
||||
TextColumn::make('module_key')
|
||||
->label('Module')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->badge()
|
||||
->color('info')
|
||||
->formatStateUsing(fn (string $state): string => \App\Models\Module::where('key', $state)->value('name') ?? $state
|
||||
),
|
||||
|
||||
IconColumn::make('enabled')
|
||||
->boolean()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('plan')
|
||||
->searchable()
|
||||
->badge()
|
||||
->color('success')
|
||||
->default('N/A'),
|
||||
|
||||
TextColumn::make('monthly_price')
|
||||
->money('USD')
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('activated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('activatedBy.name')
|
||||
->label('Activated By')
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('expires_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable()
|
||||
->color(fn ($state) => $state && $state->isPast() ? 'danger' : null)
|
||||
->icon(fn ($state) => $state && $state->isPast() ? 'heroicon-o-exclamation-triangle' : null),
|
||||
|
||||
IconColumn::make('is_active')
|
||||
->label('Active')
|
||||
->getStateUsing(fn ($record) => $record->isActive())
|
||||
->boolean(),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('business_id')
|
||||
->relationship('business', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->label('Business'),
|
||||
|
||||
SelectFilter::make('module_key')
|
||||
->options(\App\Models\Module::pluck('name', 'key'))
|
||||
->label('Module')
|
||||
->searchable(),
|
||||
|
||||
TernaryFilter::make('enabled')
|
||||
->label('Enabled')
|
||||
->boolean()
|
||||
->trueLabel('Enabled only')
|
||||
->falseLabel('Disabled only')
|
||||
->native(false),
|
||||
|
||||
TernaryFilter::make('expired')
|
||||
->label('Expired')
|
||||
->queries(
|
||||
true: fn ($query) => $query->whereNotNull('expires_at')->where('expires_at', '<=', now()),
|
||||
false: fn ($query) => $query->where(function ($q) {
|
||||
$q->whereNull('expires_at')->orWhere('expires_at', '>', now());
|
||||
}),
|
||||
)
|
||||
->trueLabel('Expired only')
|
||||
->falseLabel('Active only')
|
||||
->native(false),
|
||||
])
|
||||
->actions([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
])
|
||||
->defaultSort('created_at', 'desc');
|
||||
}
|
||||
}
|
||||
@@ -52,8 +52,10 @@ class BusinessResource extends Resource
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
// Count businesses pending approval
|
||||
return static::getModel()::where('status', 'submitted')->count() ?: null;
|
||||
// Count businesses pending approval (cached for 60 seconds)
|
||||
return cache()->remember('business_pending_count', 60, function () {
|
||||
return static::getModel()::where('status', 'submitted')->count() ?: null;
|
||||
});
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
@@ -452,6 +454,58 @@ class BusinessResource extends Resource
|
||||
->columns(2),
|
||||
]),
|
||||
|
||||
Tab::make('Modules')
|
||||
->schema([
|
||||
Section::make('Premium Feature Modules')
|
||||
->description('Enable optional premium features for this business. Modules are activated on a per-business basis.')
|
||||
->schema([
|
||||
Grid::make(1)
|
||||
->schema([
|
||||
Toggle::make('has_analytics')
|
||||
->label('Buyer Intelligence Module')
|
||||
->helperText('Premium analytics: Buyer engagement tracking, intent signals, RFDI scoring, email campaign analytics')
|
||||
->default(false)
|
||||
->inline(false),
|
||||
|
||||
Toggle::make('has_marketing')
|
||||
->label('Marketing Module')
|
||||
->helperText('Email campaigns, marketing automation, broadcast messages')
|
||||
->default(false)
|
||||
->inline(false),
|
||||
|
||||
Toggle::make('has_manufacturing')
|
||||
->label('Manufacturing Module')
|
||||
->helperText('Production tracking, batch management, quality control')
|
||||
->default(false)
|
||||
->inline(false),
|
||||
]),
|
||||
]),
|
||||
|
||||
Section::make('Module Information')
|
||||
->description('Module activation status and billing information')
|
||||
->schema([
|
||||
Forms\Components\Placeholder::make('active_modules_count')
|
||||
->label('Active Modules')
|
||||
->content(function ($record) {
|
||||
if (! $record) {
|
||||
return '0';
|
||||
}
|
||||
$count = 0;
|
||||
if ($record->has_analytics) {
|
||||
$count++;
|
||||
}
|
||||
if ($record->has_marketing) {
|
||||
$count++;
|
||||
}
|
||||
if ($record->has_manufacturing) {
|
||||
$count++;
|
||||
}
|
||||
|
||||
return $count.' module'.($count !== 1 ? 's' : '').' enabled';
|
||||
}),
|
||||
])
|
||||
->columns(1),
|
||||
]),
|
||||
Tab::make('Status & Settings')
|
||||
->schema([
|
||||
Grid::make(2)
|
||||
@@ -528,6 +582,7 @@ class BusinessResource extends Resource
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->modifyQueryUsing(fn ($query) => $query->with(['owner', 'users']))
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->label('Name')
|
||||
@@ -544,6 +599,24 @@ class BusinessResource extends Resource
|
||||
})
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('modules')
|
||||
->label('Active Modules')
|
||||
->formatStateUsing(function ($record) {
|
||||
$modules = [];
|
||||
if ($record->has_analytics) {
|
||||
$modules[] = 'Analytics';
|
||||
}
|
||||
if ($record->has_marketing) {
|
||||
$modules[] = 'Marketing';
|
||||
}
|
||||
if ($record->has_manufacturing) {
|
||||
$modules[] = 'Manufacturing';
|
||||
}
|
||||
|
||||
return empty($modules) ? 'None' : implode(', ', $modules);
|
||||
})
|
||||
->badge()
|
||||
->color(fn ($record) => ($record->has_analytics || $record->has_marketing || $record->has_manufacturing) ? 'success' : 'gray'),
|
||||
BadgeColumn::make('status')
|
||||
->label('Status')
|
||||
->formatStateUsing(fn (string $state): string => ucfirst(str_replace('_', ' ', $state)))
|
||||
|
||||
@@ -33,7 +33,10 @@ class ComponentResource extends Resource
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count();
|
||||
// Cache component count for 60 seconds to reduce database queries on every page load
|
||||
return cache()->remember('component_count', 60, function () {
|
||||
return static::getModel()::count() ?: null;
|
||||
});
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
@@ -49,7 +52,7 @@ class ComponentResource extends Resource
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
\Tapp\FilamentAuditing\RelationManagers\AuditsRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
63
app/Filament/Resources/EmailTemplateResource.php
Normal file
63
app/Filament/Resources/EmailTemplateResource.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\EmailTemplateResource\Pages\CreateEmailTemplate;
|
||||
use App\Filament\Resources\EmailTemplateResource\Pages\EditEmailTemplate;
|
||||
use App\Filament\Resources\EmailTemplateResource\Pages\ListEmailTemplates;
|
||||
use App\Filament\Resources\EmailTemplateResource\Pages\ViewEmailTemplate;
|
||||
use App\Filament\Resources\EmailTemplateResource\Schemas\EmailTemplateForm;
|
||||
use App\Filament\Resources\EmailTemplateResource\Schemas\EmailTemplateInfolist;
|
||||
use App\Filament\Resources\EmailTemplateResource\Tables\EmailTemplatesTable;
|
||||
use App\Models\EmailTemplate;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class EmailTemplateResource extends Resource
|
||||
{
|
||||
protected static ?string $model = EmailTemplate::class;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-envelope';
|
||||
|
||||
protected static \UnitEnum|string|null $navigationGroup = 'System';
|
||||
|
||||
protected static ?int $navigationSort = 10;
|
||||
|
||||
protected static ?string $navigationLabel = 'Email Templates';
|
||||
|
||||
protected static ?string $modelLabel = 'Email Template';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'Email Templates';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
// Count inactive templates
|
||||
return static::getModel()::where('is_active', false)->count() ?: null;
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return EmailTemplateForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function infolist(Schema $schema): Schema
|
||||
{
|
||||
return EmailTemplateInfolist::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return EmailTemplatesTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListEmailTemplates::route('/'),
|
||||
'create' => CreateEmailTemplate::route('/create'),
|
||||
'view' => ViewEmailTemplate::route('/{record}'),
|
||||
'edit' => EditEmailTemplate::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\EmailTemplateResource\Pages;
|
||||
|
||||
use App\Filament\Resources\EmailTemplateResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateEmailTemplate extends CreateRecord
|
||||
{
|
||||
protected static string $resource = EmailTemplateResource::class;
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\EmailTemplateResource\Pages;
|
||||
|
||||
use App\Filament\Resources\EmailTemplateResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditEmailTemplate extends EditRecord
|
||||
{
|
||||
protected static string $resource = EmailTemplateResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
ViewAction::make(),
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\EmailTemplateResource\Pages;
|
||||
|
||||
use App\Filament\Resources\EmailTemplateResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListEmailTemplates extends ListRecords
|
||||
{
|
||||
protected static string $resource = EmailTemplateResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\EmailTemplateResource\Pages;
|
||||
|
||||
use App\Filament\Resources\EmailTemplateResource;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewEmailTemplate extends ViewRecord
|
||||
{
|
||||
protected static string $resource = EmailTemplateResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
EditAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\EmailTemplateResource\Schemas;
|
||||
|
||||
use App\Models\EmailTemplate;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class EmailTemplateForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->columns(1)
|
||||
->components([
|
||||
Section::make('Template Details')
|
||||
->schema([
|
||||
TextInput::make('key')
|
||||
->label('Template Key')
|
||||
->required()
|
||||
->unique(ignoreRecord: true)
|
||||
->regex('/^[a-z0-9_-]+$/')
|
||||
->helperText('Lowercase alphanumeric characters, hyphens and underscores only')
|
||||
->disabled(fn ($context) => $context === 'edit')
|
||||
->dehydrated(fn ($context) => $context === 'create')
|
||||
->columnSpanFull(),
|
||||
|
||||
TextInput::make('name')
|
||||
->label('Template Name')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->columnSpanFull(),
|
||||
|
||||
TextInput::make('subject')
|
||||
->label('Email Subject')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->columnSpanFull(),
|
||||
|
||||
Textarea::make('description')
|
||||
->label('Description')
|
||||
->helperText('Describe when this template is used')
|
||||
->rows(3)
|
||||
->columnSpanFull(),
|
||||
|
||||
TextInput::make('available_variables')
|
||||
->label('Available Variables')
|
||||
->helperText('Comma-separated list (e.g., verification_url, email, logo_url)')
|
||||
->afterStateHydrated(function (TextInput $component, $state) {
|
||||
if (is_array($state)) {
|
||||
$component->state(implode(', ', $state));
|
||||
}
|
||||
})
|
||||
->dehydrateStateUsing(function ($state) {
|
||||
if (empty($state)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_map('trim', explode(',', $state));
|
||||
})
|
||||
->columnSpanFull(),
|
||||
|
||||
Checkbox::make('is_active')
|
||||
->label('Template is Active')
|
||||
->default(true)
|
||||
->inline(false),
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
Section::make('Email Content')
|
||||
->schema([
|
||||
Textarea::make('body_html')
|
||||
->label('HTML Body')
|
||||
->required()
|
||||
->rows(25)
|
||||
->helperText('Use {{ $variable }} syntax for dynamic content')
|
||||
->columnSpanFull()
|
||||
->extraAttributes(['style' => 'font-family: monospace; font-size: 13px;']),
|
||||
|
||||
Textarea::make('body_text')
|
||||
->label('Plain Text Body (Optional)')
|
||||
->rows(15)
|
||||
->helperText('Plain text fallback for email clients that don\'t support HTML')
|
||||
->columnSpanFull()
|
||||
->extraAttributes(['style' => 'font-family: monospace; font-size: 13px;']),
|
||||
]),
|
||||
|
||||
Section::make('Metadata')
|
||||
->schema([
|
||||
Placeholder::make('created_at')
|
||||
->label('Created At')
|
||||
->content(fn (?EmailTemplate $record): string => $record?->created_at?->diffForHumans() ?? '-'),
|
||||
|
||||
Placeholder::make('updated_at')
|
||||
->label('Last Updated')
|
||||
->content(fn (?EmailTemplate $record): string => $record?->updated_at?->diffForHumans() ?? '-'),
|
||||
])
|
||||
->columns(2)
|
||||
->hidden(fn ($context) => $context === 'create'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\EmailTemplateResource\Schemas;
|
||||
|
||||
use Filament\Infolists\Components\IconEntry;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Infolists\Components\ViewEntry;
|
||||
use Filament\Schemas\Schema;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class EmailTemplateInfolist
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextEntry::make('name')
|
||||
->label('Template Name')
|
||||
->columnSpan(1),
|
||||
|
||||
TextEntry::make('key')
|
||||
->label('Template Key')
|
||||
->badge()
|
||||
->copyable()
|
||||
->copyMessage('Key copied!')
|
||||
->copyMessageDuration(1500)
|
||||
->columnSpan(1),
|
||||
|
||||
TextEntry::make('subject')
|
||||
->label('Email Subject')
|
||||
->columnSpan(2),
|
||||
|
||||
TextEntry::make('description')
|
||||
->label('Description')
|
||||
->columnSpan(2)
|
||||
->placeholder('No description provided'),
|
||||
|
||||
TextEntry::make('available_variables')
|
||||
->label('Available Variables')
|
||||
->badge()
|
||||
->separator(',')
|
||||
->columnSpan(2)
|
||||
->placeholder('No variables defined'),
|
||||
|
||||
IconEntry::make('is_active')
|
||||
->label('Status')
|
||||
->boolean()
|
||||
->trueIcon('heroicon-o-check-circle')
|
||||
->falseIcon('heroicon-o-x-circle')
|
||||
->trueColor('success')
|
||||
->falseColor('danger')
|
||||
->columnSpan(1),
|
||||
|
||||
TextEntry::make('created_at')
|
||||
->label('Created')
|
||||
->dateTime()
|
||||
->since()
|
||||
->columnSpan(1),
|
||||
|
||||
TextEntry::make('updated_at')
|
||||
->label('Last Updated')
|
||||
->dateTime()
|
||||
->since()
|
||||
->columnSpan(1),
|
||||
|
||||
ViewEntry::make('preview')
|
||||
->label('HTML Preview')
|
||||
->viewData(fn ($record) => [
|
||||
'html' => $record->body_html,
|
||||
])
|
||||
->view('filament.email-template-preview')
|
||||
->columnSpan(2),
|
||||
|
||||
TextEntry::make('body_html')
|
||||
->label('HTML Source')
|
||||
->formatStateUsing(fn ($state) => new HtmlString('<pre class="text-xs font-mono bg-gray-100 dark:bg-gray-900 p-4 rounded overflow-x-auto whitespace-pre-wrap">'.htmlspecialchars($state).'</pre>'))
|
||||
->columnSpan(2),
|
||||
|
||||
TextEntry::make('body_text')
|
||||
->label('Plain Text Version')
|
||||
->formatStateUsing(fn ($state) => new HtmlString('<pre class="text-xs font-mono bg-gray-100 dark:bg-gray-900 p-4 rounded overflow-x-auto whitespace-pre-wrap">'.htmlspecialchars($state ?: 'No plain text version').'</pre>'))
|
||||
->columnSpan(2)
|
||||
->hidden(fn ($record) => empty($record->body_text)),
|
||||
])
|
||||
->columns(2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\EmailTemplateResource\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class EmailTemplatesTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->label('Template Name')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->weight('bold'),
|
||||
|
||||
TextColumn::make('key')
|
||||
->label('Key')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->fontFamily('mono')
|
||||
->size('sm')
|
||||
->copyable()
|
||||
->copyMessage('Key copied!')
|
||||
->copyMessageDuration(1500),
|
||||
|
||||
TextColumn::make('subject')
|
||||
->label('Subject')
|
||||
->searchable()
|
||||
->limit(50)
|
||||
->wrap(),
|
||||
|
||||
IconColumn::make('is_active')
|
||||
->label('Status')
|
||||
->boolean()
|
||||
->trueIcon('heroicon-o-check-circle')
|
||||
->falseIcon('heroicon-o-x-circle')
|
||||
->trueColor('success')
|
||||
->falseColor('danger')
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('updated_at')
|
||||
->label('Last Updated')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->since()
|
||||
->size('sm'),
|
||||
])
|
||||
->defaultSort('name')
|
||||
->filters([
|
||||
SelectFilter::make('is_active')
|
||||
->label('Status')
|
||||
->options([
|
||||
true => 'Active',
|
||||
false => 'Inactive',
|
||||
]),
|
||||
])
|
||||
->recordActions([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
168
app/Filament/Resources/FailedJobResource.php
Normal file
168
app/Filament/Resources/FailedJobResource.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\FailedJobResource\Pages;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class FailedJobResource extends Resource
|
||||
{
|
||||
protected static ?string $model = null;
|
||||
|
||||
protected static \BackedEnum|string|null $navigationIcon = 'heroicon-o-exclamation-triangle';
|
||||
|
||||
protected static ?string $navigationLabel = 'Failed Jobs';
|
||||
|
||||
protected static \UnitEnum|string|null $navigationGroup = 'System';
|
||||
|
||||
protected static ?int $navigationSort = 99;
|
||||
|
||||
public static function getModel(): string
|
||||
{
|
||||
return config('queue.failed.database') ?? 'failed_jobs';
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
return DB::table('failed_jobs')->orderBy('failed_at', 'desc');
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->query(fn () => DB::table('failed_jobs')->orderBy('failed_at', 'desc'))
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('id')
|
||||
->label('ID')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('queue')
|
||||
->badge()
|
||||
->color('info')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('connection')
|
||||
->badge()
|
||||
->color('gray')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('payload')
|
||||
->label('Job Type')
|
||||
->getStateUsing(function ($record) {
|
||||
$payload = json_decode($record->payload, true);
|
||||
$displayName = $payload['displayName'] ?? 'Unknown';
|
||||
// Extract just the class name
|
||||
if (str_contains($displayName, '\\')) {
|
||||
return class_basename($displayName);
|
||||
}
|
||||
|
||||
return $displayName;
|
||||
})
|
||||
->badge()
|
||||
->color('warning')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('exception')
|
||||
->label('Error')
|
||||
->limit(100)
|
||||
->tooltip(fn ($record) => $record->exception)
|
||||
->wrap()
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('failed_at')
|
||||
->label('Failed At')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->since()
|
||||
->description(fn ($record) => $record->failed_at),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('queue')
|
||||
->options(function () {
|
||||
return DB::table('failed_jobs')
|
||||
->distinct()
|
||||
->pluck('queue', 'queue')
|
||||
->toArray();
|
||||
}),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\Action::make('retry')
|
||||
->label('Retry')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->color('success')
|
||||
->requiresConfirmation()
|
||||
->action(function ($record) {
|
||||
Artisan::call('queue:retry', ['id' => [$record->id]]);
|
||||
})
|
||||
->successNotificationTitle('Job queued for retry'),
|
||||
Tables\Actions\Action::make('view_details')
|
||||
->label('View Details')
|
||||
->icon('heroicon-o-eye')
|
||||
->modalHeading('Failed Job Details')
|
||||
->modalContent(function ($record) {
|
||||
$payload = json_decode($record->payload, true);
|
||||
|
||||
return view('filament.resources.failed-job.view-details', [
|
||||
'record' => $record,
|
||||
'payload' => $payload,
|
||||
]);
|
||||
})
|
||||
->modalSubmitAction(false)
|
||||
->modalCancelActionLabel('Close'),
|
||||
Tables\Actions\DeleteAction::make()
|
||||
->label('Delete')
|
||||
->action(fn ($record) => DB::table('failed_jobs')->where('id', $record->id)->delete())
|
||||
->successNotificationTitle('Failed job deleted'),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkAction::make('retry_selected')
|
||||
->label('Retry Selected')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->color('success')
|
||||
->requiresConfirmation()
|
||||
->action(function ($records) {
|
||||
foreach ($records as $record) {
|
||||
Artisan::call('queue:retry', ['id' => [$record->id]]);
|
||||
}
|
||||
})
|
||||
->deselectRecordsAfterCompletion()
|
||||
->successNotificationTitle('Selected jobs queued for retry'),
|
||||
Tables\Actions\BulkAction::make('delete_selected')
|
||||
->label('Delete Selected')
|
||||
->icon('heroicon-o-trash')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->action(function ($records) {
|
||||
$ids = collect($records)->pluck('id')->toArray();
|
||||
DB::table('failed_jobs')->whereIn('id', $ids)->delete();
|
||||
})
|
||||
->deselectRecordsAfterCompletion()
|
||||
->successNotificationTitle('Selected jobs deleted'),
|
||||
])
|
||||
->defaultSort('failed_at', 'desc')
|
||||
->poll('30s')
|
||||
->emptyStateHeading('No Failed Jobs')
|
||||
->emptyStateDescription('All jobs are processing successfully!')
|
||||
->emptyStateIcon('heroicon-o-check-circle');
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListFailedJobs::route('/'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function canEdit(Model $record): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\FailedJobResource\Pages;
|
||||
|
||||
use App\Filament\Resources\FailedJobResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ListFailedJobs extends ListRecords
|
||||
{
|
||||
protected static string $resource = FailedJobResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\Action::make('retry_all')
|
||||
->label('Retry All Failed Jobs')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->color('success')
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Retry All Failed Jobs?')
|
||||
->modalDescription('This will attempt to retry all failed jobs in the queue.')
|
||||
->action(function () {
|
||||
Artisan::call('queue:retry', ['id' => ['all']]);
|
||||
})
|
||||
->successNotificationTitle('All failed jobs queued for retry')
|
||||
->visible(fn () => DB::table('failed_jobs')->count() > 0),
|
||||
Actions\Action::make('flush_all')
|
||||
->label('Delete All Failed Jobs')
|
||||
->icon('heroicon-o-trash')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Delete All Failed Jobs?')
|
||||
->modalDescription('This will permanently delete all failed job records. This action cannot be undone.')
|
||||
->action(function () {
|
||||
Artisan::call('queue:flush');
|
||||
})
|
||||
->successNotificationTitle('All failed jobs deleted')
|
||||
->visible(fn () => DB::table('failed_jobs')->count() > 0),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderWidgets(): array
|
||||
{
|
||||
return [
|
||||
FailedJobResource\Widgets\FailedJobsStatsWidget::class,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\FailedJobResource\Widgets;
|
||||
|
||||
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
|
||||
use Filament\Widgets\StatsOverviewWidget\Stat;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class FailedJobsStatsWidget extends BaseWidget
|
||||
{
|
||||
protected function getStats(): array
|
||||
{
|
||||
$totalFailed = DB::table('failed_jobs')->count();
|
||||
$failedToday = DB::table('failed_jobs')
|
||||
->whereDate('failed_at', today())
|
||||
->count();
|
||||
$failedThisWeek = DB::table('failed_jobs')
|
||||
->where('failed_at', '>=', now()->startOfWeek())
|
||||
->count();
|
||||
|
||||
// Get most common failed job type
|
||||
$commonFailure = DB::table('failed_jobs')
|
||||
->select('payload')
|
||||
->get()
|
||||
->map(function ($job) {
|
||||
$payload = json_decode($job->payload, true);
|
||||
|
||||
return $payload['displayName'] ?? 'Unknown';
|
||||
})
|
||||
->countBy()
|
||||
->sortDesc()
|
||||
->first();
|
||||
|
||||
return [
|
||||
Stat::make('Total Failed Jobs', $totalFailed)
|
||||
->description('All time')
|
||||
->descriptionIcon('heroicon-m-exclamation-triangle')
|
||||
->color($totalFailed > 0 ? 'danger' : 'success'),
|
||||
Stat::make('Failed Today', $failedToday)
|
||||
->description(now()->format('M d, Y'))
|
||||
->descriptionIcon('heroicon-m-calendar')
|
||||
->color($failedToday > 0 ? 'warning' : 'success'),
|
||||
Stat::make('Failed This Week', $failedThisWeek)
|
||||
->description('Since '.now()->startOfWeek()->format('M d'))
|
||||
->descriptionIcon('heroicon-m-chart-bar')
|
||||
->color($failedThisWeek > 0 ? 'warning' : 'success'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getPollingInterval(): ?string
|
||||
{
|
||||
return '30s';
|
||||
}
|
||||
}
|
||||
@@ -31,8 +31,10 @@ class InvoiceResource extends Resource
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
// Count unpaid invoices
|
||||
return static::getModel()::where('payment_status', 'unpaid')->count() ?: null;
|
||||
// Cache unpaid invoice count for 60 seconds to reduce database queries on every page load
|
||||
return cache()->remember('invoice_unpaid_count', 60, function () {
|
||||
return static::getModel()::where('payment_status', 'unpaid')->count() ?: null;
|
||||
});
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
|
||||
@@ -16,6 +16,7 @@ class InvoicesTable
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->modifyQueryUsing(fn ($query) => $query->with(['order', 'business']))
|
||||
->columns([
|
||||
TextColumn::make('invoice_number')
|
||||
->searchable(),
|
||||
|
||||
90
app/Filament/Resources/LabResource.php
Normal file
90
app/Filament/Resources/LabResource.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\LabResource\Pages;
|
||||
use App\Filament\Resources\LabResource\Schemas\LabForm;
|
||||
use App\Filament\Resources\LabResource\Tables\LabsTable;
|
||||
use App\Models\Lab;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use UnitEnum;
|
||||
|
||||
class LabResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Lab::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-beaker';
|
||||
|
||||
protected static ?string $navigationLabel = 'Lab Tests';
|
||||
|
||||
protected static UnitEnum|string|null $navigationGroup = 'Inventory';
|
||||
|
||||
protected static ?int $navigationSort = 3;
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return LabForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return LabsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
$query = parent::getEloquentQuery();
|
||||
|
||||
// Scope to user's business products and batches unless they're a super admin
|
||||
if (auth()->check() && ! auth()->user()->hasRole('super_admin')) {
|
||||
$businessId = auth()->user()->business_id;
|
||||
|
||||
$query->where(function ($q) use ($businessId) {
|
||||
// Include labs for products owned by this business
|
||||
$q->whereHas('product', function ($productQuery) use ($businessId) {
|
||||
$productQuery->whereHas('brand', function ($brandQuery) use ($businessId) {
|
||||
$brandQuery->where('business_id', $businessId);
|
||||
});
|
||||
})
|
||||
// OR labs for batches owned by this business
|
||||
->orWhereHas('batch', function ($batchQuery) use ($businessId) {
|
||||
$batchQuery->where('business_id', $businessId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListLabs::route('/'),
|
||||
'create' => Pages\CreateLab::route('/create'),
|
||||
'view' => Pages\ViewLab::route('/{record}'),
|
||||
'edit' => Pages\EditLab::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
// Show count of recent lab tests (last 30 days)
|
||||
return cache()->remember('recent_lab_tests_count', 300, function () {
|
||||
$query = static::getEloquentQuery();
|
||||
|
||||
return $query->where('test_date', '>=', now()->subDays(30))
|
||||
->count() ?: null;
|
||||
});
|
||||
}
|
||||
}
|
||||
11
app/Filament/Resources/LabResource/Pages/CreateLab.php
Normal file
11
app/Filament/Resources/LabResource/Pages/CreateLab.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\LabResource\Pages;
|
||||
|
||||
use App\Filament\Resources\LabResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateLab extends CreateRecord
|
||||
{
|
||||
protected static string $resource = LabResource::class;
|
||||
}
|
||||
20
app/Filament/Resources/LabResource/Pages/EditLab.php
Normal file
20
app/Filament/Resources/LabResource/Pages/EditLab.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\LabResource\Pages;
|
||||
|
||||
use App\Filament\Resources\LabResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditLab extends EditRecord
|
||||
{
|
||||
protected static string $resource = LabResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\ViewAction::make(),
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
19
app/Filament/Resources/LabResource/Pages/ListLabs.php
Normal file
19
app/Filament/Resources/LabResource/Pages/ListLabs.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\LabResource\Pages;
|
||||
|
||||
use App\Filament\Resources\LabResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListLabs extends ListRecords
|
||||
{
|
||||
protected static string $resource = LabResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
19
app/Filament/Resources/LabResource/Pages/ViewLab.php
Normal file
19
app/Filament/Resources/LabResource/Pages/ViewLab.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\LabResource\Pages;
|
||||
|
||||
use App\Filament\Resources\LabResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewLab extends ViewRecord
|
||||
{
|
||||
protected static string $resource = LabResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\EditAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
298
app/Filament/Resources/LabResource/Schemas/LabForm.php
Normal file
298
app/Filament/Resources/LabResource/Schemas/LabForm.php
Normal file
@@ -0,0 +1,298 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\LabResource\Schemas;
|
||||
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class LabForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Tabs::make('Lab Test Information')
|
||||
->tabs([
|
||||
Tab::make('Basic Information')
|
||||
->schema([
|
||||
Section::make('Test Details')
|
||||
->schema([
|
||||
Select::make('product_id')
|
||||
->label('Product')
|
||||
->relationship('product', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText('Product this test is for'),
|
||||
|
||||
Select::make('batch_id')
|
||||
->label('Batch')
|
||||
->relationship('batch', 'batch_number')
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText('Specific batch tested'),
|
||||
|
||||
TextInput::make('lab_name')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->helperText('Testing laboratory name'),
|
||||
|
||||
TextInput::make('lab_license_number')
|
||||
->label('Lab License #')
|
||||
->maxLength(255)
|
||||
->helperText('State license number'),
|
||||
|
||||
DatePicker::make('test_date')
|
||||
->required()
|
||||
->default(now())
|
||||
->helperText('Date test was performed'),
|
||||
|
||||
TextInput::make('batch_number')
|
||||
->label('Lab Batch Number')
|
||||
->maxLength(255)
|
||||
->helperText('Internal lab tracking number'),
|
||||
|
||||
TextInput::make('sample_id')
|
||||
->label('Sample ID')
|
||||
->maxLength(255)
|
||||
->helperText('Sample identification'),
|
||||
])
|
||||
->columns(2),
|
||||
]),
|
||||
|
||||
Tab::make('Cannabinoids')
|
||||
->schema([
|
||||
Section::make('Primary Cannabinoids')
|
||||
->schema([
|
||||
TextInput::make('thc_percentage')
|
||||
->label('THC %')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(100)
|
||||
->step(0.01)
|
||||
->suffix('%'),
|
||||
|
||||
TextInput::make('thca_percentage')
|
||||
->label('THCA %')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(100)
|
||||
->step(0.01)
|
||||
->suffix('%'),
|
||||
|
||||
TextInput::make('cbd_percentage')
|
||||
->label('CBD %')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(100)
|
||||
->step(0.01)
|
||||
->suffix('%'),
|
||||
|
||||
TextInput::make('cbda_percentage')
|
||||
->label('CBDA %')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(100)
|
||||
->step(0.01)
|
||||
->suffix('%'),
|
||||
])
|
||||
->columns(4),
|
||||
|
||||
Section::make('Minor Cannabinoids')
|
||||
->schema([
|
||||
TextInput::make('cbg_percentage')
|
||||
->label('CBG %')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(100)
|
||||
->step(0.01)
|
||||
->suffix('%'),
|
||||
|
||||
TextInput::make('cbn_percentage')
|
||||
->label('CBN %')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(100)
|
||||
->step(0.01)
|
||||
->suffix('%'),
|
||||
|
||||
TextInput::make('thcv_percentage')
|
||||
->label('THCV %')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(100)
|
||||
->step(0.01)
|
||||
->suffix('%'),
|
||||
|
||||
TextInput::make('cbdv_percentage')
|
||||
->label('CBDV %')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(100)
|
||||
->step(0.01)
|
||||
->suffix('%'),
|
||||
])
|
||||
->columns(4),
|
||||
|
||||
Section::make('Calculated Totals')
|
||||
->schema([
|
||||
TextInput::make('total_thc')
|
||||
->label('Total THC')
|
||||
->numeric()
|
||||
->disabled()
|
||||
->dehydrated(false)
|
||||
->suffix('%')
|
||||
->helperText('Auto-calculated from THC + (THCA × 0.877)'),
|
||||
|
||||
TextInput::make('total_cbd')
|
||||
->label('Total CBD')
|
||||
->numeric()
|
||||
->disabled()
|
||||
->dehydrated(false)
|
||||
->suffix('%')
|
||||
->helperText('Auto-calculated from CBD + (CBDA × 0.877)'),
|
||||
|
||||
TextInput::make('total_cannabinoids')
|
||||
->label('Total Cannabinoids')
|
||||
->numeric()
|
||||
->disabled()
|
||||
->dehydrated(false)
|
||||
->suffix('%')
|
||||
->helperText('Sum of all cannabinoids'),
|
||||
])
|
||||
->columns(3)
|
||||
->description('These values are automatically calculated on save'),
|
||||
]),
|
||||
|
||||
Tab::make('Terpenes')
|
||||
->schema([
|
||||
Repeater::make('terpenes')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->required()
|
||||
->helperText('Terpene name (e.g., Myrcene)'),
|
||||
|
||||
TextInput::make('percentage')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->step(0.001)
|
||||
->suffix('%')
|
||||
->helperText('Percentage'),
|
||||
])
|
||||
->columns(2)
|
||||
->collapsible()
|
||||
->helperText('Add terpene profile data'),
|
||||
]),
|
||||
|
||||
Tab::make('Compliance Tests')
|
||||
->schema([
|
||||
Section::make('Safety Tests')
|
||||
->schema([
|
||||
Toggle::make('pesticides_pass')
|
||||
->label('Pesticides Pass')
|
||||
->default(true)
|
||||
->inline(false),
|
||||
|
||||
Toggle::make('heavy_metals_pass')
|
||||
->label('Heavy Metals Pass')
|
||||
->default(true)
|
||||
->inline(false),
|
||||
|
||||
Toggle::make('microbials_pass')
|
||||
->label('Microbials Pass')
|
||||
->default(true)
|
||||
->inline(false),
|
||||
|
||||
Toggle::make('mycotoxins_pass')
|
||||
->label('Mycotoxins Pass')
|
||||
->default(true)
|
||||
->inline(false),
|
||||
|
||||
Toggle::make('residual_solvents_pass')
|
||||
->label('Residual Solvents Pass')
|
||||
->default(true)
|
||||
->inline(false),
|
||||
|
||||
Toggle::make('foreign_material_pass')
|
||||
->label('Foreign Material Pass')
|
||||
->default(true)
|
||||
->inline(false),
|
||||
])
|
||||
->columns(3)
|
||||
->description('All tests must pass for overall compliance'),
|
||||
|
||||
Section::make('Additional Tests')
|
||||
->schema([
|
||||
TextInput::make('moisture_content')
|
||||
->label('Moisture Content %')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(100)
|
||||
->step(0.01)
|
||||
->suffix('%'),
|
||||
|
||||
Toggle::make('compliance_pass')
|
||||
->label('Overall Compliance Pass')
|
||||
->default(true)
|
||||
->disabled()
|
||||
->dehydrated(false)
|
||||
->helperText('Auto-calculated from all safety tests'),
|
||||
])
|
||||
->columns(2),
|
||||
]),
|
||||
|
||||
Tab::make('COA Files')
|
||||
->schema([
|
||||
Section::make('Certificate of Analysis Files')
|
||||
->schema([
|
||||
Repeater::make('coaFiles')
|
||||
->relationship()
|
||||
->schema([
|
||||
FileUpload::make('file_path')
|
||||
->label('File')
|
||||
->required()
|
||||
->directory('compliance/coas')
|
||||
->acceptedFileTypes(['application/pdf', 'image/*'])
|
||||
->maxSize(10240),
|
||||
|
||||
TextInput::make('description')
|
||||
->maxLength(255)
|
||||
->helperText('Optional description'),
|
||||
|
||||
Toggle::make('is_primary')
|
||||
->label('Primary COA')
|
||||
->inline(false),
|
||||
])
|
||||
->columns(3)
|
||||
->collapsible()
|
||||
->helperText('Upload COA files (PDF or images)'),
|
||||
|
||||
TextInput::make('certificate_url')
|
||||
->label('External COA URL')
|
||||
->url()
|
||||
->maxLength(255)
|
||||
->helperText('Link to COA on external site (optional)'),
|
||||
]),
|
||||
]),
|
||||
|
||||
Tab::make('Notes')
|
||||
->schema([
|
||||
Textarea::make('notes')
|
||||
->rows(5)
|
||||
->columnSpanFull()
|
||||
->helperText('Additional notes about this test'),
|
||||
]),
|
||||
])
|
||||
->columnSpanFull(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
136
app/Filament/Resources/LabResource/Tables/LabsTable.php
Normal file
136
app/Filament/Resources/LabResource/Tables/LabsTable.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\LabResource\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Support\Colors\Color;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\Filter;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Filters\TernaryFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class LabsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('lab_name')
|
||||
->label('Lab')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->weight('bold'),
|
||||
|
||||
TextColumn::make('product.name')
|
||||
->label('Product')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->limit(30),
|
||||
|
||||
TextColumn::make('batch.batch_number')
|
||||
->label('Batch')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('test_date')
|
||||
->date('M d, Y')
|
||||
->sortable()
|
||||
->color(fn ($record) => $record->test_date < now()->subDays(90) ? Color::Orange : null),
|
||||
|
||||
TextColumn::make('total_thc')
|
||||
->label('THC')
|
||||
->numeric(decimalPlaces: 2)
|
||||
->suffix('%')
|
||||
->sortable()
|
||||
->color(fn ($state) => $state > 20 ? Color::Green : ($state > 15 ? Color::Amber : Color::Gray)),
|
||||
|
||||
TextColumn::make('total_cbd')
|
||||
->label('CBD')
|
||||
->numeric(decimalPlaces: 2)
|
||||
->suffix('%')
|
||||
->sortable()
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('total_cannabinoids')
|
||||
->label('Total')
|
||||
->numeric(decimalPlaces: 2)
|
||||
->suffix('%')
|
||||
->sortable()
|
||||
->toggleable(),
|
||||
|
||||
IconColumn::make('compliance_pass')
|
||||
->label('Compliance')
|
||||
->boolean()
|
||||
->trueIcon('heroicon-o-check-circle')
|
||||
->falseIcon('heroicon-o-x-circle')
|
||||
->trueColor(Color::Green)
|
||||
->falseColor(Color::Red)
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('terpene_profile')
|
||||
->label('Top Terpenes')
|
||||
->limit(40)
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->defaultSort('test_date', 'desc')
|
||||
->filters([
|
||||
SelectFilter::make('lab_name')
|
||||
->options(function () {
|
||||
return \App\Models\Lab::query()
|
||||
->distinct('lab_name')
|
||||
->pluck('lab_name', 'lab_name')
|
||||
->toArray();
|
||||
})
|
||||
->searchable(),
|
||||
|
||||
SelectFilter::make('product')
|
||||
->relationship('product', 'name')
|
||||
->searchable()
|
||||
->preload(),
|
||||
|
||||
SelectFilter::make('batch')
|
||||
->relationship('batch', 'batch_number')
|
||||
->searchable()
|
||||
->preload(),
|
||||
|
||||
TernaryFilter::make('compliance_pass')
|
||||
->label('Compliant'),
|
||||
|
||||
Filter::make('recent')
|
||||
->label('Recent (Last 30 days)')
|
||||
->query(fn (Builder $query): Builder => $query->where('test_date', '>=', now()->subDays(30)))
|
||||
->toggle(),
|
||||
|
||||
Filter::make('high_thc')
|
||||
->label('High THC (>20%)')
|
||||
->query(fn (Builder $query): Builder => $query->where('total_thc', '>', 20))
|
||||
->toggle(),
|
||||
|
||||
Filter::make('high_cbd')
|
||||
->label('High CBD (>10%)')
|
||||
->query(fn (Builder $query): Builder => $query->where('total_cbd', '>', 10))
|
||||
->toggle(),
|
||||
])
|
||||
->recordActions([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
75
app/Filament/Resources/ModuleResource.php
Normal file
75
app/Filament/Resources/ModuleResource.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\ModuleResource\Pages\CreateModule;
|
||||
use App\Filament\Resources\ModuleResource\Pages\EditModule;
|
||||
use App\Filament\Resources\ModuleResource\Pages\ListModules;
|
||||
use App\Filament\Resources\ModuleResource\Pages\ViewModule;
|
||||
use App\Filament\Resources\ModuleResource\Schemas\ModuleForm;
|
||||
use App\Filament\Resources\ModuleResource\Schemas\ModuleInfolist;
|
||||
use App\Filament\Resources\ModuleResource\Tables\ModulesTable;
|
||||
use App\Models\Module;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
use UnitEnum;
|
||||
|
||||
class ModuleResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Module::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedPuzzlePiece;
|
||||
|
||||
protected static UnitEnum|string|null $navigationGroup = 'System';
|
||||
|
||||
protected static ?int $navigationSort = 10;
|
||||
|
||||
protected static ?string $navigationLabel = 'Modules';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return cache()->remember('module_count', 60, function () {
|
||||
return static::getModel()::active()->count() ?: null;
|
||||
});
|
||||
}
|
||||
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
return auth()->check() && auth()->user()->user_type === 'admin';
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return ModuleForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function infolist(Schema $schema): Schema
|
||||
{
|
||||
return ModuleInfolist::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return ModulesTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListModules::route('/'),
|
||||
'create' => CreateModule::route('/create'),
|
||||
'view' => ViewModule::route('/{record}'),
|
||||
'edit' => EditModule::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
11
app/Filament/Resources/ModuleResource/Pages/CreateModule.php
Normal file
11
app/Filament/Resources/ModuleResource/Pages/CreateModule.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ModuleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ModuleResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateModule extends CreateRecord
|
||||
{
|
||||
protected static string $resource = ModuleResource::class;
|
||||
}
|
||||
21
app/Filament/Resources/ModuleResource/Pages/EditModule.php
Normal file
21
app/Filament/Resources/ModuleResource/Pages/EditModule.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ModuleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ModuleResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditModule extends EditRecord
|
||||
{
|
||||
protected static string $resource = ModuleResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
ViewAction::make(),
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
19
app/Filament/Resources/ModuleResource/Pages/ListModules.php
Normal file
19
app/Filament/Resources/ModuleResource/Pages/ListModules.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ModuleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ModuleResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListModules extends ListRecords
|
||||
{
|
||||
protected static string $resource = ModuleResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
19
app/Filament/Resources/ModuleResource/Pages/ViewModule.php
Normal file
19
app/Filament/Resources/ModuleResource/Pages/ViewModule.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ModuleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ModuleResource;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewModule extends ViewRecord
|
||||
{
|
||||
protected static string $resource = ModuleResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
EditAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
115
app/Filament/Resources/ModuleResource/Schemas/ModuleForm.php
Normal file
115
app/Filament/Resources/ModuleResource/Schemas/ModuleForm.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ModuleResource\Schemas;
|
||||
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class ModuleForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Section::make('Module Information')
|
||||
->schema([
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
TextInput::make('key')
|
||||
->required()
|
||||
->unique(ignoreRecord: true)
|
||||
->maxLength(255)
|
||||
->helperText('Unique identifier for the module (e.g., sms_gateway)')
|
||||
->disabled(fn (?string $operation): bool => $operation === 'edit'),
|
||||
|
||||
TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
]),
|
||||
|
||||
Textarea::make('description')
|
||||
->columnSpanFull()
|
||||
->rows(3),
|
||||
|
||||
Grid::make(3)
|
||||
->schema([
|
||||
Select::make('category')
|
||||
->options([
|
||||
'Communication' => 'Communication',
|
||||
'Sales' => 'Sales',
|
||||
'Operations' => 'Operations',
|
||||
'Finance' => 'Finance',
|
||||
'Marketing' => 'Marketing',
|
||||
'Support' => 'Support',
|
||||
'Analytics' => 'Analytics',
|
||||
])
|
||||
->searchable()
|
||||
->preload(),
|
||||
|
||||
TextInput::make('icon')
|
||||
->helperText('Heroicon name (e.g., heroicon-o-cube)'),
|
||||
|
||||
TextInput::make('sort_order')
|
||||
->numeric()
|
||||
->default(0)
|
||||
->helperText('Lower numbers appear first'),
|
||||
]),
|
||||
]),
|
||||
|
||||
Section::make('Module Settings')
|
||||
->schema([
|
||||
Fieldset::make('Status & Features')
|
||||
->schema([
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
Toggle::make('is_active')
|
||||
->label('Active')
|
||||
->default(true)
|
||||
->helperText('Is this module available for use?'),
|
||||
|
||||
Toggle::make('is_premium')
|
||||
->label('Premium Module')
|
||||
->default(false)
|
||||
->helperText('Requires payment or subscription'),
|
||||
]),
|
||||
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
Toggle::make('requires_approval')
|
||||
->label('Requires Approval')
|
||||
->default(false)
|
||||
->helperText('Admin must approve before activation'),
|
||||
|
||||
Toggle::make('enabled_by_default')
|
||||
->label('Enabled by Default')
|
||||
->default(false)
|
||||
->helperText('Automatically enabled for new businesses'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
||||
Section::make('Configuration & Limits')
|
||||
->schema([
|
||||
KeyValue::make('config')
|
||||
->label('Module Configuration')
|
||||
->keyLabel('Config Key')
|
||||
->valueLabel('Config Value')
|
||||
->helperText('JSON configuration for module features'),
|
||||
|
||||
KeyValue::make('default_limits')
|
||||
->label('Default Limits')
|
||||
->keyLabel('Metric')
|
||||
->valueLabel('Limit')
|
||||
->helperText('Default usage limits (e.g., sms_per_month: 1000)'),
|
||||
])
|
||||
->collapsible(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ModuleResource\Schemas;
|
||||
|
||||
use Filament\Infolists\Components\Grid;
|
||||
use Filament\Infolists\Components\IconEntry;
|
||||
use Filament\Infolists\Components\KeyValueEntry;
|
||||
use Filament\Infolists\Components\Section;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class ModuleInfolist
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Section::make('Module Information')
|
||||
->schema([
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
TextEntry::make('name'),
|
||||
TextEntry::make('key')
|
||||
->badge()
|
||||
->color('gray'),
|
||||
]),
|
||||
|
||||
TextEntry::make('description')
|
||||
->columnSpanFull(),
|
||||
|
||||
Grid::make(3)
|
||||
->schema([
|
||||
TextEntry::make('category')
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'Communication' => 'info',
|
||||
'Sales' => 'success',
|
||||
'Operations' => 'warning',
|
||||
'Finance' => 'danger',
|
||||
default => 'gray',
|
||||
}),
|
||||
|
||||
TextEntry::make('icon'),
|
||||
|
||||
TextEntry::make('sort_order')
|
||||
->label('Sort Order'),
|
||||
]),
|
||||
]),
|
||||
|
||||
Section::make('Module Status')
|
||||
->schema([
|
||||
Grid::make(4)
|
||||
->schema([
|
||||
IconEntry::make('is_active')
|
||||
->label('Active')
|
||||
->boolean(),
|
||||
|
||||
IconEntry::make('is_premium')
|
||||
->label('Premium')
|
||||
->boolean(),
|
||||
|
||||
IconEntry::make('requires_approval')
|
||||
->label('Requires Approval')
|
||||
->boolean(),
|
||||
|
||||
IconEntry::make('enabled_by_default')
|
||||
->label('Enabled by Default')
|
||||
->boolean(),
|
||||
]),
|
||||
]),
|
||||
|
||||
Section::make('Configuration')
|
||||
->schema([
|
||||
KeyValueEntry::make('config')
|
||||
->label('Module Configuration'),
|
||||
|
||||
KeyValueEntry::make('default_limits')
|
||||
->label('Default Limits'),
|
||||
])
|
||||
->collapsible(),
|
||||
|
||||
Section::make('Timestamps')
|
||||
->schema([
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
TextEntry::make('created_at')
|
||||
->dateTime(),
|
||||
|
||||
TextEntry::make('updated_at')
|
||||
->dateTime(),
|
||||
]),
|
||||
])
|
||||
->collapsible()
|
||||
->collapsed(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
127
app/Filament/Resources/ModuleResource/Tables/ModulesTable.php
Normal file
127
app/Filament/Resources/ModuleResource/Tables/ModulesTable.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ModuleResource\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Support\Enums\FontWeight;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Filters\TernaryFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ModulesTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->weight(FontWeight::Bold),
|
||||
|
||||
TextColumn::make('key')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->badge()
|
||||
->color('gray'),
|
||||
|
||||
TextColumn::make('category')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'Communication' => 'info',
|
||||
'Sales' => 'success',
|
||||
'Operations' => 'warning',
|
||||
'Finance' => 'danger',
|
||||
default => 'gray',
|
||||
}),
|
||||
|
||||
TextColumn::make('description')
|
||||
->limit(50)
|
||||
->tooltip(function (TextColumn $column): ?string {
|
||||
$state = $column->getState();
|
||||
|
||||
if (strlen($state) <= 50) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $state;
|
||||
}),
|
||||
|
||||
IconColumn::make('is_active')
|
||||
->label('Active')
|
||||
->boolean()
|
||||
->sortable(),
|
||||
|
||||
IconColumn::make('is_premium')
|
||||
->label('Premium')
|
||||
->boolean()
|
||||
->sortable(),
|
||||
|
||||
IconColumn::make('requires_approval')
|
||||
->label('Requires Approval')
|
||||
->boolean(),
|
||||
|
||||
IconColumn::make('enabled_by_default')
|
||||
->label('Default')
|
||||
->boolean(),
|
||||
|
||||
TextColumn::make('sort_order')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
TernaryFilter::make('is_active')
|
||||
->label('Active')
|
||||
->boolean()
|
||||
->trueLabel('Active only')
|
||||
->falseLabel('Inactive only')
|
||||
->native(false),
|
||||
|
||||
TernaryFilter::make('is_premium')
|
||||
->label('Premium')
|
||||
->boolean()
|
||||
->trueLabel('Premium only')
|
||||
->falseLabel('Free only')
|
||||
->native(false),
|
||||
|
||||
SelectFilter::make('category')
|
||||
->options([
|
||||
'Communication' => 'Communication',
|
||||
'Sales' => 'Sales',
|
||||
'Operations' => 'Operations',
|
||||
'Finance' => 'Finance',
|
||||
'Marketing' => 'Marketing',
|
||||
'Support' => 'Support',
|
||||
'Analytics' => 'Analytics',
|
||||
])
|
||||
->native(false),
|
||||
])
|
||||
->actions([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
])
|
||||
->defaultSort('sort_order');
|
||||
}
|
||||
}
|
||||
@@ -34,8 +34,10 @@ class OrderResource extends Resource
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
// Count pending and processing orders
|
||||
return static::getModel()::whereIn('status', ['pending', 'processing', 'confirmed'])->count() ?: null;
|
||||
// Cache active order count for 60 seconds to reduce database queries on every page load
|
||||
return cache()->remember('order_active_count', 60, function () {
|
||||
return static::getModel()::whereIn('status', ['pending', 'processing', 'confirmed'])->count() ?: null;
|
||||
});
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
@@ -57,6 +59,7 @@ class OrderResource extends Resource
|
||||
{
|
||||
return [
|
||||
RelationManagers\ItemsRelationManager::class,
|
||||
\Tapp\FilamentAuditing\RelationManagers\AuditsRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ class OrdersTable
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->modifyQueryUsing(fn ($query) => $query->with(['business']))
|
||||
->columns([
|
||||
TextColumn::make('order_number')
|
||||
->label('Order #')
|
||||
|
||||
@@ -36,7 +36,10 @@ class ProductResource extends Resource
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count();
|
||||
// Cache product count for 60 seconds to reduce database queries on every page load
|
||||
return cache()->remember('product_count', 60, function () {
|
||||
return static::getModel()::count() ?: null;
|
||||
});
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
@@ -60,6 +63,7 @@ class ProductResource extends Resource
|
||||
BatchesRelationManager::class,
|
||||
ComponentsRelationManager::class,
|
||||
VarietiesRelationManager::class,
|
||||
\Tapp\FilamentAuditing\RelationManagers\AuditsRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ class ProductsTable
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->modifyQueryUsing(fn ($query) => $query->with(['brand', 'strain']))
|
||||
->columns([
|
||||
ImageColumn::make('image_path')
|
||||
->label('Image')
|
||||
|
||||
@@ -36,8 +36,10 @@ class UserResource extends Resource
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
// Count inactive and suspended users
|
||||
return static::getModel()::whereIn('status', ['inactive', 'suspended'])->count() ?: null;
|
||||
// Cache inactive/suspended user count for 60 seconds to reduce database queries on every page load
|
||||
return cache()->remember('user_inactive_count', 60, function () {
|
||||
return static::getModel()::whereIn('status', ['inactive', 'suspended'])->count() ?: null;
|
||||
});
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
@@ -83,6 +85,22 @@ class UserResource extends Resource
|
||||
'suspended' => 'Suspended',
|
||||
])
|
||||
->default('active'),
|
||||
TextInput::make('password')
|
||||
->label('Password')
|
||||
->password()
|
||||
->required(fn ($record) => $record === null)
|
||||
->dehydrated(fn ($state) => filled($state))
|
||||
->minLength(8)
|
||||
->maxLength(255)
|
||||
->helperText('Leave blank to keep current password when editing')
|
||||
->visible(fn ($livewire) => $livewire instanceof CreateUser),
|
||||
TextInput::make('password_confirmation')
|
||||
->label('Confirm Password')
|
||||
->password()
|
||||
->required(fn ($record) => $record === null && filled($record?->password))
|
||||
->dehydrated(false)
|
||||
->same('password')
|
||||
->visible(fn ($livewire) => $livewire instanceof CreateUser),
|
||||
])->columns(2),
|
||||
|
||||
Section::make('Business Association')
|
||||
@@ -236,6 +254,13 @@ class UserResource extends Resource
|
||||
EditAction::make()
|
||||
->label('View/Modify')
|
||||
->icon('heroicon-o-pencil'),
|
||||
Action::make('impersonate')
|
||||
->label('Impersonate')
|
||||
->icon('heroicon-o-user-circle')
|
||||
->color('warning')
|
||||
->visible(fn (User $record) => auth()->user()->canImpersonate() && $record->canBeImpersonated())
|
||||
->url(fn (User $record) => route('admin.impersonate.perform', $record))
|
||||
->openUrlInNewTab(false),
|
||||
Action::make('suspend')
|
||||
->label('Suspend')
|
||||
->icon('heroicon-o-no-symbol')
|
||||
|
||||
@@ -4,8 +4,18 @@ namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Resources\UserResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class CreateUser extends CreateRecord
|
||||
{
|
||||
protected static string $resource = UserResource::class;
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
if (isset($data['password'])) {
|
||||
$data['password'] = Hash::make($data['password']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
143
app/Helpers/BusinessHelper.php
Normal file
143
app/Helpers/BusinessHelper.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use App\Models\Business;
|
||||
use App\Services\PermissionService;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class BusinessHelper
|
||||
{
|
||||
/**
|
||||
* Get current business context from session or user's primary business
|
||||
*/
|
||||
public static function current(): ?Business
|
||||
{
|
||||
if (! Auth::check()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$businessId = session('current_business_id');
|
||||
|
||||
if ($businessId) {
|
||||
return Business::find($businessId);
|
||||
}
|
||||
|
||||
// Fall back to user's primary business if no session is set
|
||||
return Auth::user()->primaryBusiness();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has a permission for current business
|
||||
*
|
||||
* This method now uses PermissionService internally for better architecture
|
||||
* while maintaining backward compatibility with existing code.
|
||||
*
|
||||
* @param string $permission Permission key (e.g. 'analytics.overview')
|
||||
*/
|
||||
public static function hasPermission(string $permission): bool
|
||||
{
|
||||
if (! Auth::check()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
$business = self::current();
|
||||
|
||||
if (! $business) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use PermissionService for permission checking
|
||||
$permissionService = app(PermissionService::class);
|
||||
|
||||
return $permissionService->check($user, $permission, $business);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is owner or admin for current business
|
||||
*/
|
||||
public static function isOwnerOrAdmin(): bool
|
||||
{
|
||||
if (! Auth::check()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
$business = self::current();
|
||||
|
||||
if (! $business) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Super admin
|
||||
if ($user->user_type === 'admin') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Business owner
|
||||
return $business->owner_user_id === $user->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's role template for current business
|
||||
*/
|
||||
public static function getRoleTemplate(): ?string
|
||||
{
|
||||
if (! Auth::check()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
$business = self::current();
|
||||
|
||||
if (! $business) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$businessUser = $user->businesses()
|
||||
->where('businesses.id', $business->id)
|
||||
->first();
|
||||
|
||||
return $businessUser?->pivot->role_template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's permissions array for current business
|
||||
*/
|
||||
public static function getPermissions(): array
|
||||
{
|
||||
if (! Auth::check()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
$business = self::current();
|
||||
|
||||
if (! $business) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Use PermissionService for cached permission retrieval
|
||||
$permissionService = app(PermissionService::class);
|
||||
|
||||
return $permissionService->getUserPermissions($user, $business);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current business has a specific module enabled
|
||||
*
|
||||
* @param string $module Module name (sales, manufacturing, compliance)
|
||||
*/
|
||||
public static function hasModule(string $module): bool
|
||||
{
|
||||
$business = self::current();
|
||||
|
||||
return match ($module) {
|
||||
'sales' => true, // Sales is always enabled (base product)
|
||||
'manufacturing' => $business?->has_manufacturing ?? false,
|
||||
'compliance' => $business?->has_compliance ?? false,
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
24
app/Helpers/helpers.php
Normal file
24
app/Helpers/helpers.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use App\Helpers\BusinessHelper;
|
||||
|
||||
if (! function_exists('currentBusiness')) {
|
||||
function currentBusiness()
|
||||
{
|
||||
return BusinessHelper::current();
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('currentBusinessId')) {
|
||||
function currentBusinessId()
|
||||
{
|
||||
return BusinessHelper::currentId();
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('hasBusinessPermission')) {
|
||||
function hasBusinessPermission(string $permission): bool
|
||||
{
|
||||
return BusinessHelper::hasPermission($permission);
|
||||
}
|
||||
}
|
||||
110
app/Http/Controllers/Admin/QuickSwitchController.php
Normal file
110
app/Http/Controllers/Admin/QuickSwitchController.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class QuickSwitchController extends Controller
|
||||
{
|
||||
/**
|
||||
* Ensure only admins can access
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
$this->middleware(function ($request, $next) {
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $user) {
|
||||
abort(403, 'Not authenticated');
|
||||
}
|
||||
|
||||
$manager = app(\Lab404\Impersonate\Services\ImpersonateManager::class);
|
||||
|
||||
// If impersonating, check if the impersonator can impersonate
|
||||
// Otherwise check if the current user can impersonate
|
||||
$canAccess = $manager->isImpersonating()
|
||||
? $manager->getImpersonator()->canImpersonate()
|
||||
: $user->canImpersonate();
|
||||
|
||||
if (! $canAccess) {
|
||||
abort(403, 'Only administrators can access this feature. Please login as an admin.');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show quick switch menu for testing
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// Get all seller users for quick switching
|
||||
$users = User::where('user_type', 'seller')
|
||||
->with('businesses')
|
||||
->orderBy('email')
|
||||
->get();
|
||||
|
||||
return view('admin.quick-switch', compact('users'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick switch to user using impersonation (maintains admin session)
|
||||
*/
|
||||
public function switch(Request $request, User $user)
|
||||
{
|
||||
$currentUser = auth()->user();
|
||||
$manager = app(\Lab404\Impersonate\Services\ImpersonateManager::class);
|
||||
|
||||
// Get the actual admin user (might be the impersonator)
|
||||
$admin = $manager->isImpersonating()
|
||||
? $manager->getImpersonator()
|
||||
: $currentUser;
|
||||
|
||||
if (! $user->canBeImpersonated()) {
|
||||
abort(403, 'This user cannot be impersonated');
|
||||
}
|
||||
|
||||
// If already impersonating someone, leave that impersonation first
|
||||
if ($manager->isImpersonating()) {
|
||||
$manager->leave();
|
||||
}
|
||||
|
||||
// Use impersonation instead of session replacement
|
||||
// This allows multiple tabs with different impersonated users
|
||||
$manager->take($admin, $user, 'web');
|
||||
|
||||
// Redirect based on user type and business
|
||||
$business = $user->primaryBusiness();
|
||||
|
||||
if ($business && $business->isParentCompany()) {
|
||||
return redirect()->route('seller.business.executive.dashboard', $business->slug);
|
||||
} elseif ($business) {
|
||||
return redirect()->route('seller.business.dashboard', $business->slug);
|
||||
}
|
||||
|
||||
return redirect()->route('seller.dashboard');
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch back to admin (leave impersonation)
|
||||
*/
|
||||
public function backToAdmin()
|
||||
{
|
||||
$manager = app(\Lab404\Impersonate\Services\ImpersonateManager::class);
|
||||
|
||||
if (! $manager->isImpersonating()) {
|
||||
return redirect()->route('filament.admin.pages.dashboard')
|
||||
->with('info', 'You are not currently impersonating anyone');
|
||||
}
|
||||
|
||||
// Leave impersonation
|
||||
$manager->leave();
|
||||
|
||||
return redirect()->route('filament.admin.pages.dashboard')
|
||||
->with('success', 'Returned to admin panel');
|
||||
}
|
||||
}
|
||||
101
app/Http/Controllers/Analytics/AnalyticsDashboardController.php
Normal file
101
app/Http/Controllers/Analytics/AnalyticsDashboardController.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Analytics;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Analytics\AnalyticsEvent;
|
||||
use App\Models\Analytics\BuyerEngagementScore;
|
||||
use App\Models\Analytics\IntentSignal;
|
||||
use App\Models\Analytics\ProductView;
|
||||
use App\Models\Analytics\UserSession;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AnalyticsDashboardController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
if (! hasBusinessPermission('analytics.overview')) {
|
||||
abort(403, 'Unauthorized to view analytics');
|
||||
}
|
||||
|
||||
$business = currentBusiness();
|
||||
$period = $request->input('period', '30'); // days
|
||||
|
||||
$startDate = now()->subDays((int) $period);
|
||||
|
||||
// Key metrics
|
||||
$metrics = [
|
||||
'total_sessions' => UserSession::forBusiness($business->id)->where('started_at', '>=', $startDate)->count(),
|
||||
'total_page_views' => UserSession::forBusiness($business->id)->where('started_at', '>=', $startDate)->sum('page_views'),
|
||||
'total_product_views' => ProductView::forBusiness($business->id)->where('viewed_at', '>=', $startDate)->count(),
|
||||
'unique_products_viewed' => ProductView::forBusiness($business->id)->where('viewed_at', '>=', $startDate)
|
||||
->distinct('product_id')
|
||||
->count('product_id'),
|
||||
'high_intent_signals' => IntentSignal::forBusiness($business->id)->where('detected_at', '>=', $startDate)
|
||||
->where('signal_strength', '>=', IntentSignal::STRENGTH_HIGH)
|
||||
->count(),
|
||||
'active_buyers' => BuyerEngagementScore::forBusiness($business->id)->where('last_interaction_at', '>=', $startDate)->count(),
|
||||
];
|
||||
|
||||
// Traffic trend (daily breakdown)
|
||||
$trafficTrend = AnalyticsEvent::forBusiness($business->id)->where('created_at', '>=', $startDate)
|
||||
->select(
|
||||
DB::raw('DATE(created_at) as date'),
|
||||
DB::raw('COUNT(*) as total_events'),
|
||||
DB::raw('COUNT(DISTINCT session_id) as unique_sessions')
|
||||
)
|
||||
->groupBy('date')
|
||||
->orderBy('date')
|
||||
->get();
|
||||
|
||||
// Top products by views
|
||||
$topProducts = ProductView::forBusiness($business->id)->where('viewed_at', '>=', $startDate)
|
||||
->select('product_id', DB::raw('COUNT(*) as view_count'))
|
||||
->groupBy('product_id')
|
||||
->orderByDesc('view_count')
|
||||
->limit(10)
|
||||
->with('product')
|
||||
->get();
|
||||
|
||||
// High-value buyers
|
||||
$highValueBuyers = BuyerEngagementScore::forBusiness($business->id)->highValue()
|
||||
->active()
|
||||
->orderByDesc('score')
|
||||
->limit(10)
|
||||
->with('buyerBusiness')
|
||||
->get();
|
||||
|
||||
// Recent high-intent signals
|
||||
$recentIntentSignals = IntentSignal::forBusiness($business->id)->highIntent()
|
||||
->where('detected_at', '>=', now()->subHours(24))
|
||||
->orderByDesc('detected_at')
|
||||
->limit(10)
|
||||
->with(['buyerBusiness', 'user'])
|
||||
->get();
|
||||
|
||||
// Engagement score distribution
|
||||
$engagementDistribution = BuyerEngagementScore::forBusiness($business->id)->select(
|
||||
DB::raw('CASE
|
||||
WHEN score >= 80 THEN \'Very High\'
|
||||
WHEN score >= 60 THEN \'High\'
|
||||
WHEN score >= 40 THEN \'Medium\'
|
||||
ELSE \'Low\'
|
||||
END as score_range'),
|
||||
DB::raw('COUNT(*) as count')
|
||||
)
|
||||
->groupBy('score_range')
|
||||
->get();
|
||||
|
||||
return view('seller.analytics.dashboard', compact(
|
||||
'business',
|
||||
'period',
|
||||
'metrics',
|
||||
'trafficTrend',
|
||||
'topProducts',
|
||||
'highValueBuyers',
|
||||
'recentIntentSignals',
|
||||
'engagementDistribution'
|
||||
));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user