Compare commits

...

65 Commits

Author SHA1 Message Date
7be161f4e4 resolving
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 5m14s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 17s
Build, Push and Deploy / deploy-staging (push) Successful in 41s
Build, Push and Deploy / deploy-production (push) Has been skipped
2026-04-27 08:58:08 +07:00
7beac673c8 pull from web 2026-04-27 08:56:13 +07:00
triagungbiantoro
417b7b7453 harga pada beranda terhubung ke manage plans di dashboard admin
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m27s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Successful in 31s
Build, Push and Deploy / deploy-production (push) Has been skipped
2026-04-25 21:57:25 +07:00
f25542ea0d update grafik revenuw di dashboard
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m58s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Successful in 42s
Build, Push and Deploy / deploy-production (push) Has been skipped
2026-04-23 22:28:02 +07:00
f2cd16a2a8 update querry report profit n lost
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m48s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 15s
Build, Push and Deploy / deploy-staging (push) Successful in 40s
Build, Push and Deploy / deploy-production (push) Has been skipped
2026-04-23 22:17:41 +07:00
4f530ff301 perbaikan product string 1 bugs
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m51s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 15s
Build, Push and Deploy / deploy-staging (push) Successful in 41s
Build, Push and Deploy / deploy-production (push) Has been skipped
2026-04-18 23:08:39 +07:00
6319d4c6cb update on psosale string 1 bugs
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m15s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 14s
Build, Push and Deploy / deploy-staging (push) Successful in 36s
Build, Push and Deploy / deploy-production (push) Has been skipped
2026-04-18 22:47:24 +07:00
2173c3ee81 update gitignore composer.js
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m23s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 14s
Build, Push and Deploy / deploy-staging (push) Successful in 37s
Build, Push and Deploy / deploy-production (push) Has been skipped
2026-04-18 21:41:31 +07:00
dcaf267458 update lock clucknut
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 14s
Build, Push and Deploy / build-and-push (push) Successful in 3m14s
Build, Push and Deploy / deploy-staging (push) Successful in 25s
Build, Push and Deploy / deploy-production (push) Has been skipped
2026-04-18 20:32:18 +07:00
4554035227 fix: enforce php 8.2.30 platform for docker compatibility
Some checks failed
Build, Push and Deploy / deploy-staging (push) Has been cancelled
Build, Push and Deploy / deploy-production (push) Has been cancelled
Build, Push and Deploy / build-and-push (push) Has been cancelled
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
2026-04-18 20:29:54 +07:00
f459f03d0c perbaikan bug dashboard
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m20s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 14s
Build, Push and Deploy / deploy-staging (push) Successful in 26s
Build, Push and Deploy / deploy-production (push) Has been skipped
2026-04-18 19:55:00 +07:00
ef902b2604 update bug daashboards 2026-04-18 19:47:13 +07:00
dd60899d9e updater
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m29s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 14s
Build, Push and Deploy / deploy-staging (push) Successful in 26s
Build, Push and Deploy / deploy-production (push) Has been skipped
2026-04-18 19:07:27 +07:00
1e252a0e4c update git pus
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m35s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 14s
Build, Push and Deploy / deploy-staging (push) Successful in 30s
Build, Push and Deploy / deploy-production (push) Has been skipped
2026-04-18 16:22:32 +07:00
ed9654271b fix: sync composer.lock with composer.json after adding scribe and missing extensions 2026-04-18 16:21:59 +07:00
1228c52538 update after stop build
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m36s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Successful in 42s
Build, Push and Deploy / deploy-production (push) Has been skipped
2026-04-18 16:10:57 +07:00
e449146306 update dockerfile
Some checks failed
Build, Push and Deploy / build-and-push (push) Failing after 2m15s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 15s
Build, Push and Deploy / deploy-staging (push) Has been skipped
Build, Push and Deploy / deploy-production (push) Has been skipped
2026-04-18 16:02:16 +07:00
62417c44be Update bunines bugs
Some checks failed
Build, Push and Deploy / build-and-push (push) Failing after 2m57s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Has been skipped
Build, Push and Deploy / deploy-production (push) Has been skipped
2026-04-18 15:50:39 +07:00
e2fa4d2d7b create insert db querry
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m31s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 15s
Build, Push and Deploy / deploy-staging (push) Successful in 37s
Build, Push and Deploy / deploy-production (push) Has been skipped
2026-04-16 05:04:06 +07:00
75dfca9128 update
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m17s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 15s
Build, Push and Deploy / deploy-staging (push) Successful in 37s
Build, Push and Deploy / deploy-production (push) Has been skipped
2026-04-15 22:26:07 +07:00
58edd89bd1 refressh database
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 4m1s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Successful in 35s
Build, Push and Deploy / deploy-production (push) Has been skipped
2026-04-15 20:34:37 +07:00
de510a05f1 seeder db
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m46s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 18s
Build, Push and Deploy / deploy-staging (push) Successful in 45s
Build, Push and Deploy / deploy-production (push) Has been skipped
2026-04-13 09:24:39 +07:00
4e58905bf7 migrate
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 15s
Build, Push and Deploy / build-and-push (push) Successful in 3m0s
Build, Push and Deploy / deploy-staging (push) Successful in 36s
Build, Push and Deploy / deploy-production (push) Has been skipped
2026-04-12 23:49:02 +07:00
2e2b0ee178 test migrate 2026-04-12 23:46:56 +07:00
eko
d9988d59a4 fix entrypoint
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 2m56s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 15s
Build, Push and Deploy / deploy-staging (push) Successful in 25s
Build, Push and Deploy / deploy-production (push) Has been skipped
2026-04-12 22:38:52 +07:00
eko
afc31bc825 fix entrypoint
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m25s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 17s
Build, Push and Deploy / deploy-staging (push) Successful in 29s
Build, Push and Deploy / deploy-production (push) Has been skipped
2026-04-12 22:23:56 +07:00
eko
ac2ec6e659 add swagger
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m28s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 15s
Build, Push and Deploy / deploy-staging (push) Successful in 38s
Build, Push and Deploy / deploy-production (push) Has been skipped
2026-04-12 22:06:46 +07:00
eko
75ece668cc fix vald
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m2s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 14s
Build, Push and Deploy / deploy-staging (push) Successful in 35s
Build, Push and Deploy / deploy-production (push) Has been skipped
2026-04-05 00:57:34 +07:00
eko
eafa8d362f fix vald
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m3s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 14s
Build, Push and Deploy / deploy-staging (push) Successful in 36s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 0s
2026-04-05 00:39:10 +07:00
eko
2d2cb95600 fix val
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m0s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 14s
Build, Push and Deploy / deploy-staging (push) Successful in 36s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
2026-04-05 00:18:48 +07:00
eko
1d63c28356 remove route::cache production
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m12s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 14s
Build, Push and Deploy / deploy-staging (push) Successful in 37s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 0s
2026-04-04 23:51:47 +07:00
eko
4398aee449 fix production
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m9s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 15s
Build, Push and Deploy / deploy-staging (push) Successful in 38s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
2026-04-04 23:34:45 +07:00
eko
481b8fba7e fix pipeline production still use port 8000
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m15s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Successful in 39s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
2026-04-04 23:12:57 +07:00
cdd7fbaecb csrf
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m10s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 14s
Build, Push and Deploy / deploy-staging (push) Successful in 41s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
2026-04-03 21:06:42 +07:00
ff60e63ece crsf bugs
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m37s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 14s
Build, Push and Deploy / deploy-staging (push) Successful in 37s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
2026-04-03 20:41:20 +07:00
e5d8df803f hapus
Some checks failed
Build, Push and Deploy / build-and-push (push) Failing after 2m58s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 15s
Build, Push and Deploy / deploy-staging (push) Has been skipped
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 0s
2026-04-02 23:20:02 +07:00
df323c1c70 gdockerignore cache
Some checks failed
Build, Push and Deploy / build-and-push (push) Failing after 3m10s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 13s
Build, Push and Deploy / deploy-staging (push) Has been skipped
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 0s
2026-04-02 23:12:48 +07:00
c0d1714543 docker ignore cache
Some checks failed
Build, Push and Deploy / build-and-push (push) Failing after 3m24s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 15s
Build, Push and Deploy / deploy-staging (push) Has been skipped
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
2026-04-02 23:05:21 +07:00
2f17c3abdd update pull
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m13s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 15s
Build, Push and Deploy / deploy-staging (push) Successful in 37s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 0s
2026-04-02 22:00:30 +07:00
ee73c81dde Merge branch 'main' of https://kode.sadateknologi.site/yanto/kulakpos_web 2026-04-02 21:39:22 +07:00
78dd794f71 pulldata 2026-04-02 21:38:46 +07:00
triagungbiantoro
1ab56d68b3 perbaikan tombol e-wallet
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m59s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 15s
Build, Push and Deploy / deploy-staging (push) Successful in 38s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
2026-04-02 21:26:01 +07:00
triagungbiantoro
ac43e83b38 Perbaikan Button Pilih Basic dan Pilih Pro
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m13s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 15s
Build, Push and Deploy / deploy-staging (push) Successful in 43s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
2026-04-02 09:12:29 +07:00
triagungbiantoro
80f4d8f046 perbaikan mobile view + no Whatsapp
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m57s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 15s
Build, Push and Deploy / deploy-staging (push) Successful in 38s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
2026-04-01 18:06:09 +07:00
triagungbiantoro
8fafbccb66 ganti logo dan link tombol Jadwalkan Demo
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m47s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 15s
Build, Push and Deploy / deploy-staging (push) Successful in 40s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
2026-04-01 09:19:43 +07:00
f2c19ec025 update git
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m8s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 15s
Build, Push and Deploy / deploy-staging (push) Successful in 26s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 0s
2026-03-31 08:27:42 +07:00
8fb1995913 folder
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 2m38s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Successful in 31s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 0s
2026-03-31 01:32:38 +07:00
d346b24299 update docker file
Some checks failed
Build, Push and Deploy / build-and-push (push) Failing after 2m0s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Has been skipped
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
2026-03-31 01:25:48 +07:00
1c730d154d test second dockeerfile
Some checks failed
Build, Push and Deploy / build-and-push (push) Failing after 1m55s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 14s
Build, Push and Deploy / deploy-staging (push) Has been skipped
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
2026-03-31 01:17:24 +07:00
94a70c95c0 remove composer lock
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 2m47s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 15s
Build, Push and Deploy / deploy-staging (push) Successful in 28s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
2026-03-30 18:07:58 +07:00
a5638095f1 update git
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 2m40s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 15s
Build, Push and Deploy / deploy-staging (push) Successful in 29s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
2026-03-30 17:50:31 +07:00
b5e3a778ce allow vendord
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m3s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Successful in 32s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
2026-03-30 14:54:57 +07:00
66aed7c4e8 update bug docker file
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m7s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 8s
Build, Push and Deploy / deploy-staging (push) Successful in 37s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 0s
2026-03-30 14:32:56 +07:00
327dceee4e update bugs dockerfile
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m27s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 9s
Build, Push and Deploy / deploy-staging (push) Successful in 37s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
2026-03-30 14:18:41 +07:00
eko
d722295036 fix /plan
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 2m57s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 7s
Build, Push and Deploy / deploy-staging (push) Successful in 32s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
2026-03-29 19:35:21 +07:00
eko
74e6666da5 fix docker
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 8s
Build, Push and Deploy / build-and-push (push) Successful in 2m39s
Build, Push and Deploy / deploy-staging (push) Successful in 22s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
2026-03-29 15:42:51 +07:00
eko
a234bc497e fix docker
Some checks failed
Build, Push and Deploy / build-and-push (push) Failing after 1m56s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 8s
Build, Push and Deploy / deploy-staging (push) Has been skipped
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 0s
2026-03-29 15:33:07 +07:00
eko
957352e257 fix docker
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 2m35s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 9s
Build, Push and Deploy / deploy-staging (push) Successful in 21s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
2026-03-29 15:12:24 +07:00
eko
6b34fcb87a fix docker
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 2m36s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 8s
Build, Push and Deploy / deploy-staging (push) Successful in 30s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 0s
2026-03-29 14:53:39 +07:00
eko
46b1a4f74d fix docker
Some checks failed
Build, Push and Deploy / build-and-push (push) Failing after 1m51s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 9s
Build, Push and Deploy / deploy-staging (push) Has been skipped
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
2026-03-29 14:45:23 +07:00
eko
b0a929684a fix dockerfile
Some checks failed
Build, Push and Deploy / build-and-push (push) Failing after 1m52s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 8s
Build, Push and Deploy / deploy-staging (push) Has been skipped
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
2026-03-29 14:37:23 +07:00
eko
30a3f05cbb fix dockerfile
Some checks failed
Build, Push and Deploy / build-and-push (push) Failing after 1m49s
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 8s
Build, Push and Deploy / deploy-staging (push) Has been skipped
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 0s
2026-03-29 14:32:03 +07:00
eko
0be65d8013 adapt version
Some checks failed
Build, Push and Deploy / build-and-push (push) Successful in 2m28s
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 8s
Build, Push and Deploy / deploy-staging (push) Successful in 28s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 0s
2026-03-29 14:11:26 +07:00
eko
bd85604fb4 using vite: ^8.0.3
Some checks failed
Build, Push and Deploy / build-and-push (push) Failing after 1m40s
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 8s
Build, Push and Deploy / deploy-staging (push) Has been skipped
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 0s
2026-03-29 14:01:39 +07:00
eko
f1b383fbc7 add vite
Some checks failed
Build, Push and Deploy / build-and-push (push) Failing after 1m33s
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 8s
Build, Push and Deploy / deploy-staging (push) Has been skipped
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
2026-03-29 13:48:35 +07:00
22005 changed files with 2955019 additions and 1616 deletions

30
.dockerignore Normal file
View File

@@ -0,0 +1,30 @@
# **/node_modules
# **/vendor
# #.env
# .env.backup
# .git
# .phpunit.result.cache
# Homestead.json
# Homestead.yaml
# auth.json
# npm-debug.log
# yarn-error.log
# .idea
# .vscode
# storage/app/public
# storage/app/temp
# storage/app/uploads
# storage/debugbar
# storage/framework/views/*
# public/modules/*
# .qodo
# .camel-jbang
# # Ignore large archives that slow down docker build context
# *.zip
# *.tar.gz
# *.sqlstorage/framework/sessions/*
storage/framework/sessions/*
storage/framework/views/*
storage/framework/cache/*
bootstrap/cache/*

View File

@@ -84,7 +84,7 @@ jobs:
echo "Starting new container..."
docker run -d \
--restart unless-stopped \
--health-cmd="curl -f http://localhost:8000 || exit 1" \
--health-cmd="curl -f http://127.0.0.1:8000 || exit 1" \
--health-interval=30s \
--health-timeout=10s \
--health-retries=3 \
@@ -98,7 +98,7 @@ jobs:
# Check if container is running
sleep 5
if docker ps | grep -q kulakpos_web_staging; then
if docker ps | grep kulakpos_web_staging; then
echo "✅ Staging deployment successful!"
docker logs --tail 20 kulakpos_web_staging
else
@@ -183,7 +183,13 @@ jobs:
docker ps --filter name=kulakpos_web_prod
# Create backup of current deployment info
docker inspect kulakpos_web_prod > /tmp/kulakpos_backup_$(date +%Y%m%d_%H%M%S).json || true
mkdir -p /opt/kulakpos/backups
docker inspect kulakpos_web_prod > /opt/kulakpos/backups/kulakpos_backup_$(date +%Y%m%d_%H%M%S).json || true
# Backup .env file
if [ -f "/opt/kulakpos/.env" ]; then
cp /opt/kulakpos/.env /opt/kulakpos/backups/.env_backup_$(date +%Y%m%d_%H%M%S)
fi
# Pull the specified image
echo "Pulling image: kode.sadateknologi.site/${{ gitea.repository_owner }}/kulakpos_web:$IMAGE_TAG"
@@ -194,11 +200,11 @@ jobs:
docker stop kulakpos_web_prod || true
docker rm kulakpos_web_prod || true
# Run new container
echo "Starting new container..."
# Run new container (using PHP's built-in server on port 8000 like staging)
echo "Starting new production container on port 8000..."
docker run -d \
--restart unless-stopped \
--health-cmd="pgrep php-fpm" \
--health-cmd="curl -f http://127.0.0.1:8000 || exit 1" \
--health-interval=30s \
--health-timeout=10s \
--health-start-period=40s \
@@ -206,15 +212,23 @@ jobs:
--name kulakpos_web_prod \
--network="host" \
-v /opt/kulakpos/.env:/var/www/html/.env \
-p 9000:9000 \
-e MODE=prod \
-e MODE=production \
-e APP_ENV=production \
-e APP_DEBUG=false \
-e AUTO_MIGRATE=false \
kode.sadateknologi.site/${{ gitea.repository_owner }}/kulakpos_web:$IMAGE_TAG
# Check if container is running
# Wait for container to be ready
echo "Waiting for container to be ready..."
sleep 10
if docker ps | grep -q kulakpos_web_prod; then
# Check if container is running
if docker ps | grep kulakpos_web_prod; then
echo "✅ Production deployment successful!"
echo ""
echo "Container status:"
docker ps --filter name=kulakpos_web_prod --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
echo ""
echo "Container logs (last 20 lines):"
docker logs --tail 20 kulakpos_web_prod
else
@@ -222,50 +236,31 @@ jobs:
docker logs kulakpos_web_prod || echo "No logs available"
# Attempt rollback to previous version if available
echo ""
echo "Attempting rollback to previous version..."
PREVIOUS_IMAGE=$(docker inspect kulakpos_web_prod --format='{{.Config.Image}}' 2>/dev/null || echo "")
if [ -n "$PREVIOUS_IMAGE" ] && [ "$PREVIOUS_IMAGE" != "kode.sadateknologi.site/${{ gitea.repository_owner }}/kulakpos_web:$IMAGE_TAG" ]; then
docker run -d \
--restart unless-stopped \
--name kulakpos_web_prod \
--network="host" \
-v /opt/kulakpos/.env:/var/www/html/.env \
-p 8000:8000 \
-e MODE=prod \
-e APP_ENV=production \
$PREVIOUS_IMAGE
echo "Rollback attempted"
PREVIOUS_BACKUP=$(ls -t /opt/kulakpos/backups/kulakpos_backup_*.json 2>/dev/null | head -1)
if [ -f "$PREVIOUS_BACKUP" ]; then
PREVIOUS_IMAGE=$(jq -r '.[0].Config.Image' $PREVIOUS_BACKUP 2>/dev/null || echo "")
if [ -n "$PREVIOUS_IMAGE" ] && [ "$PREVIOUS_IMAGE" != "kode.sadateknologi.site/${{ gitea.repository_owner }}/kulakpos_web:$IMAGE_TAG" ]; then
echo "Rolling back to: $PREVIOUS_IMAGE"
docker run -d \
--restart unless-stopped \
--health-cmd="curl -f http://127.0.0.1:8000 || exit 1" \
--health-interval=30s \
--health-timeout=10s \
--health-start-period=40s \
--health-retries=3 \
--name kulakpos_web_prod \
--network="host" \
-v /opt/kulakpos/.env:/var/www/html/.env \
-e MODE=production \
-e APP_ENV=production \
-e APP_DEBUG=false \
-e AUTO_MIGRATE=false \
$PREVIOUS_IMAGE
echo "Rollback completed"
fi
fi
exit 1
fi
EOF
# - name: Verify Production Deployment
# run: |
# ssh kulakpos_prod "curl -f http://localhost:8000/health || curl -f http://localhost:8000 || echo 'Service is running but health check endpoint not configured'"
# echo "✅ Production deployment verified!"
- name: Verify Production Deployment
run: |
ssh kulakpos_prod "docker ps --filter 'name=kulakpos_web_prod' --format '{{.Status}}' | grep -q healthy"
echo "✅ Production deployment verified!"
- name: Send Deployment Notification
if: always()
run: |
STATUS="${{ job.status }}"
if [ "$STATUS" = "success" ]; then
echo "✅ Production deployment completed successfully!"
else
echo "❌ Production deployment failed!"
exit 1
fi
cleanup:
runs-on: ubuntu-latest
needs: [deploy-staging, deploy-production]
if: always()
steps:
- name: Cleanup old Docker images on runners
run: |
docker system prune -f --filter "until=24h"

View File

@@ -25,7 +25,6 @@ jobs:
- run: head -n 5 app/Helpers/Helper.php
- run: ls -la app/Helpers/Helper.php
- run: file app/Helpers/Helper.php
- run: nl -ba app/Helpers/Helper.php | head -10
- run: pwd
- run: whoami

7
.gitignore vendored
View File

@@ -1,7 +1,7 @@
/node_modules
/public/build
/public/hot
/vendor
#/vendor
.env
.env.backup
.phpunit.result.cache
@@ -19,4 +19,7 @@ yarn-error.log
/storage/framework/views/*
/public/modules/*
.qodo
.camel-jbang
.camel-jbang
dump/
db/
composer.lock

4
.scribe/.filehashes Normal file
View File

@@ -0,0 +1,4 @@
# GENERATED. YOU SHOULDN'T MODIFY OR DELETE THIS FILE.
# Scribe uses this file to know when you change something manually in your docs.
.scribe/intro.md=4cce32d41499a35efb2ff125bcc4d59c
.scribe/auth.md=9bee2b1ef8a238b2e58613fa636d5f39

3
.scribe/auth.md Normal file
View File

@@ -0,0 +1,3 @@
# Authenticating requests
This API is not authenticated.

File diff suppressed because it is too large Load Diff

10920
.scribe/endpoints/00.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
# To include an endpoint that isn't a part of your Laravel app (or belongs to a vendor package),
# you can define it in a custom.*.yaml file, like this one.
# Each custom file should contain an array of endpoints. Here's an example:
# See https://scribe.knuckles.wtf/laravel/documenting/custom-endpoints#extra-sorting-groups-in-custom-endpoint-files for more options
#- httpMethods:
# - POST
# uri: api/doSomething/{param}
# metadata:
# groupName: The group the endpoint belongs to. Can be a new group or an existing group.
# groupDescription: A description for the group. You don't need to set this for every endpoint; once is enough.
# subgroup: You can add a subgroup, too.
# title: Do something
# description: 'This endpoint allows you to do something.'
# authenticated: false
# headers:
# Content-Type: application/json
# Accept: application/json
# urlParameters:
# param:
# name: param
# description: A URL param for no reason.
# required: true
# example: 2
# type: integer
# queryParameters:
# speed:
# name: speed
# description: How fast the thing should be done. Can be `slow` or `fast`.
# required: false
# example: fast
# type: string
# bodyParameters:
# something:
# name: something
# description: The things we should do.
# required: true
# example:
# - string 1
# - string 2
# type: 'string[]'
# responses:
# - status: 200
# description: 'When the thing was done smoothly.'
# content: # Your response content can be an object, an array, a string or empty.
# {
# "hey": "ho ho ho"
# }
# responseFields:
# hey:
# name: hey
# description: Who knows?
# type: string # This is optional

13
.scribe/intro.md Normal file
View File

@@ -0,0 +1,13 @@
# Introduction
<aside>
<strong>Base URL</strong>: <code>http://localhost:8080</code>
</aside>
This documentation aims to provide all the information you need to work with our API.
<aside>As you scroll, you'll see code examples for working with the API in different programming languages in the dark area to the right (or as part of the content on mobile).
You can switch the language used with the tabs at the top right (or from the nav menu at the top left on mobile).</aside>

View File

@@ -9,49 +9,90 @@ RUN apk add --no-cache \
libpng-dev \
libjpeg-turbo-dev \
freetype-dev \
libxml2-dev \
icu-dev \
oniguruma-dev \
git \
curl \
postgresql-client \
nodejs \
npm \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) \
pdo_pgsql \
pgsql \
opcache \
zip \
bcmath \
gd \
exif \
pcntl
pdo_pgsql \
pgsql \
opcache \
zip \
bcmath \
gd \
exif \
pcntl \
xml \
dom \
intl
# Configure git to allow safe directory
RUN git config --global --add safe.directory /var/www/html
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
# Copy application files
COPY --chown=www-data:www-data . .
# Copy ONLY composer files first
COPY composer.json ./
# Remove any nested vendor directories to avoid conflicts
RUN find . -type d -name "vendor" -not -path "./vendor*" -exec rm -rf {} + 2>/dev/null || true
# Create all necessary directories
RUN mkdir -p storage/framework/{cache,sessions,views} \
# Create storage and bootstrap directories needed for composer scripts
RUN mkdir -p storage/framework/cache \
&& mkdir -p storage/framework/sessions \
&& mkdir -p storage/framework/views \
&& mkdir -p storage/framework/testing \
&& mkdir -p storage/logs \
&& mkdir -p bootstrap/cache \
&& chmod -R 777 storage \
&& chmod -R 777 bootstrap/cache
# Install dependencies
RUN composer install --no-dev --no-interaction --no-progress --optimize-autoloader --no-scripts \
&& composer run-script post-autoload-dump \
&& php artisan package:discover --ansi || true
# Configure Composer for stability on unstable networks
ENV COMPOSER_ALLOW_SUPERUSER=1 \
COMPOSER_MEMORY_LIMIT=-1 \
COMPOSER_PROCESS_TIMEOUT=2000
# Generate application key
RUN php artisan key:generate --no-interaction --force || true
# Install PHP dependencies (this layer will be cached)
RUN composer config -g repo.packagist composer https://packagist.org \
&& composer install --no-interaction --no-progress --no-scripts --prefer-dist --ignore-platform-reqs
# Isolate the clean vendor directory
#RUN mv vendor /vendor_clean
# Copy the rest of the application files (will ignore vendor because of .dockerignore)
COPY --chown=www-data:www-data . .
# Restore the clean vendor directory and set proper permissions
#RUN rm -rf vendor && mv /vendor_clean vendor && chown -R www-data:www-data vendor
# Final build verification
RUN if [ ! -f vendor/laravel/prompts/src/helpers.php ]; then \
echo "ERROR: laravel/prompts/src/helpers.php missing!" && exit 1; \
fi
# Final check and debug listing
RUN ls -l vendor/laravel/prompts/src/helpers.php \
&& du -sh vendor/laravel/prompts
# Copy package files for NPM
COPY package*.json ./
COPY vite.config.js ./
# Install NPM dependencies
RUN npm install --legacy-peer-deps --no-audit
# Build Vite assets
RUN npm run build
# Copy entrypoint script and ensure it's executable
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Let the entrypoint handle artisan commands at runtime
EXPOSE 8000 9000
ENTRYPOINT ["/entrypoint.sh"]

51
Dockerfile2 Normal file
View File

@@ -0,0 +1,51 @@
FROM php:8.4-fpm
# 1. Setup NodeSource untuk instalasi Node.js v20 (Cepat & Stabil)
RUN apt-get update && apt-get install -y ca-certificates curl gnupg \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
# 2. Install dependencies utama dengan apt-get
RUN apt-get update && apt-get install -y \
git \
libpng-dev \
libonig-dev \
libxml2-dev \
libpq-dev \
libfreetype6-dev \
libjpeg62-turbo-dev \
libzip-dev \
zip \
unzip \
nodejs \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# 3. Install PHP Extensions (TERMASUK pgsql / pdo_pgsql)
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install pdo_mysql pdo_pgsql pgsql mbstring exif pcntl bcmath gd zip
# 4. Get latest Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /var/www
# 5. Copy existing application directory contents
COPY . /var/www
# 6. Install dependencies
# (Hapus --no-dev sementara jika Anda mau testing dengan Error Page Laravel yang rapi)
RUN composer install --no-interaction --optimize-autoloader
# 7. Install & Build Frontend
RUN npm install
RUN npm run build
# 8. Set permissions
RUN chown -R www-data:www-data /var/www/storage /var/www/bootstrap/cache \
&& chmod -R 775 /var/www/storage /var/www/bootstrap/cache
# 9. Expose port 8000 and start php artisan serve
EXPOSE 8000
# artisan serve berjalan langsung
CMD ["php", "artisan", "serve", "--host=0.0.0.0", "--port=8000"]

View File

@@ -24,8 +24,8 @@ public function view(): View
})
->select(
'product_id',
DB::raw('SUM(CASE WHEN lossProfit > 0 THEN lossProfit ELSE 0 END) as profit'),
DB::raw('SUM(CASE WHEN lossProfit < 0 THEN lossProfit ELSE 0 END) as loss')
DB::raw('SUM(CASE WHEN "lossProfit" > 0 THEN "lossProfit" ELSE 0 END) as profit'),
DB::raw('SUM(CASE WHEN "lossProfit" < 0 THEN "lossProfit" ELSE 0 END) as loss')
)
->groupBy('product_id')
->get();

View File

@@ -28,15 +28,15 @@ public function index(Request $request)
$salesQuery = DB::table('sales')
->select(
DB::raw('DATE(saleDate) as date'),
DB::raw('"saleDate"::date as date'),
DB::raw('SUM(actual_total_amount) as total_sales'),
DB::raw('SUM(lossProfit) as total_sale_income')
DB::raw('SUM("lossProfit") as total_sale_income')
)
->where('business_id', $businessId)
->when($branchId, fn ($q) =>
$q->where('branch_id', $branchId)
)
->groupBy(DB::raw('DATE(saleDate)'));
->groupBy(DB::raw('"saleDate"::date'));
$this->applyDateFilter($salesQuery, $duration, 'saleDate', $request->from_date, $request->to_date);
$dailySales = $salesQuery->get();
@@ -50,14 +50,14 @@ public function index(Request $request)
$incomeQuery = DB::table('incomes')
->select(
DB::raw('DATE(incomeDate) as date'),
DB::raw('"incomeDate"::date as date'),
DB::raw('SUM(amount) as total_incomes')
)
->where('business_id', $businessId)
->when($branchId, fn ($q) =>
$q->where('branch_id', $branchId)
)
->groupBy(DB::raw('DATE(incomeDate)'));
->groupBy(DB::raw('"incomeDate"::date'));
$this->applyDateFilter($incomeQuery, $duration, 'incomeDate', $request->from_date, $request->to_date);
$dailyIncomes = $incomeQuery->get();
@@ -105,14 +105,14 @@ public function index(Request $request)
$expenseQuery = DB::table('expenses')
->select(
DB::raw('DATE(expenseDate) as date'),
DB::raw('"expenseDate"::date as date'),
DB::raw('SUM(amount) as total_expenses_only')
)
->where('business_id', $businessId)
->when($branchId, fn ($q) =>
$q->where('branch_id', $branchId)
)
->groupBy(DB::raw('DATE(expenseDate)'));
->groupBy(DB::raw('"expenseDate"::date'));
$this->applyDateFilter($expenseQuery, $duration, 'expenseDate', $request->from_date, $request->to_date);
$dailyExpenses = $expenseQuery->get();
@@ -212,13 +212,13 @@ public function exportPdf()
// SALES
$dailySales = DB::table('sales')
->select(
DB::raw('DATE(saleDate) as date'),
DB::raw('"saleDate"::date as date'),
DB::raw('SUM(actual_total_amount) as total_sales'),
DB::raw('SUM(lossProfit) as total_sale_income')
DB::raw('SUM("lossProfit") as total_sale_income')
)
->where('business_id', $businessId)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->groupBy(DB::raw('DATE(saleDate)'))
->groupBy(DB::raw('"saleDate"::date'))
->get();
$sale_datas = $dailySales->map(fn ($sale) => (object)[
@@ -231,12 +231,12 @@ public function exportPdf()
// INCOME
$dailyIncomes = DB::table('incomes')
->select(
DB::raw('DATE(incomeDate) as date'),
DB::raw('"incomeDate"::date as date'),
DB::raw('SUM(amount) as total_incomes')
)
->where('business_id', $businessId)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->groupBy(DB::raw('DATE(incomeDate)'))
->groupBy(DB::raw('"incomeDate"::date'))
->get();
$income_datas = $dailyIncomes->map(fn ($income) => (object)[
@@ -274,12 +274,12 @@ public function exportPdf()
// EXPENSES
$dailyExpenses = DB::table('expenses')
->select(
DB::raw('DATE(expenseDate) as date'),
DB::raw('"expenseDate"::date as date'),
DB::raw('SUM(amount) as total_expenses_only')
)
->where('business_id', $businessId)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->groupBy(DB::raw('DATE(expenseDate)'))
->groupBy(DB::raw('"expenseDate"::date'))
->get();
$mergedExpenseData = collect();

View File

@@ -25,15 +25,15 @@ public function index(Request $request)
$salesQuery = DB::table('sales')
->select(
DB::raw('DATE(saleDate) as date'),
DB::raw('"saleDate"::date as date'),
DB::raw('SUM(actual_total_amount) as total_sales'),
DB::raw('SUM(lossProfit) as total_sale_income')
DB::raw('SUM("lossProfit") as total_sale_income')
)
->where('business_id', $businessId)
->when($branchId, fn ($q) =>
$q->where('branch_id', $branchId)
)
->groupBy(DB::raw('DATE(saleDate)'));
->groupBy(DB::raw('"saleDate"::date'));
$this->applyDateFilter($salesQuery, $duration, 'saleDate', $request->from_date, $request->to_date);
$dailySales = $salesQuery->get();
@@ -47,14 +47,14 @@ public function index(Request $request)
$incomeQuery = DB::table('incomes')
->select(
DB::raw('DATE(incomeDate) as date'),
DB::raw('"incomeDate"::date as date'),
DB::raw('SUM(amount) as total_incomes')
)
->where('business_id', $businessId)
->when($branchId, fn ($q) =>
$q->where('branch_id', $branchId)
)
->groupBy(DB::raw('DATE(incomeDate)'));
->groupBy(DB::raw('"incomeDate"::date'));
$this->applyDateFilter($incomeQuery, $duration, 'incomeDate', $request->from_date, $request->to_date);
$dailyIncomes = $incomeQuery->get();
@@ -102,14 +102,14 @@ public function index(Request $request)
$expenseQuery = DB::table('expenses')
->select(
DB::raw('DATE(expenseDate) as date'),
DB::raw('"expenseDate"::date as date'),
DB::raw('SUM(amount) as total_expenses_only')
)
->where('business_id', $businessId)
->when($branchId, fn ($q) =>
$q->where('branch_id', $branchId)
)
->groupBy(DB::raw('DATE(expenseDate)'));
->groupBy(DB::raw('"expenseDate"::date'));
$this->applyDateFilter($expenseQuery, $duration, 'expenseDate', $request->from_date, $request->to_date);
$dailyExpenses = $expenseQuery->get();

View File

@@ -45,8 +45,8 @@ public function index(Request $request)
->with('product:id,productName,productCode')
->select(
'product_id',
DB::raw('SUM(CASE WHEN lossProfit > 0 THEN lossProfit ELSE 0 END) AS profit'),
DB::raw('SUM(CASE WHEN lossProfit < 0 THEN lossProfit ELSE 0 END) AS loss')
DB::raw('SUM(CASE WHEN "lossProfit" > 0 THEN "lossProfit" ELSE 0 END) AS profit'),
DB::raw('SUM(CASE WHEN "lossProfit" < 0 THEN "lossProfit" ELSE 0 END) AS loss')
)
->groupBy('product_id')
->paginate($request->per_page ?? 20)
@@ -93,8 +93,8 @@ public function exportPdf()
})
->select(
'product_id',
DB::raw('SUM(CASE WHEN lossProfit > 0 THEN lossProfit ELSE 0 END) as profit'),
DB::raw('SUM(CASE WHEN lossProfit < 0 THEN lossProfit ELSE 0 END) as loss')
DB::raw('SUM(CASE WHEN "lossProfit" > 0 THEN "lossProfit" ELSE 0 END) as profit'),
DB::raw('SUM(CASE WHEN "lossProfit" < 0 THEN "lossProfit" ELSE 0 END) as loss')
)
->groupBy('product_id')
->get();

View File

@@ -196,7 +196,7 @@ public function revenue()
$data['loss'] = Sale::where('business_id', auth()->user()->business_id)
->whereYear('created_at', request('year') ?? date('Y'))
->where('lossProfit', '<', 0)
->selectRaw('MONTHNAME(created_at) as month, SUM(ABS(lossProfit)) as total')
->selectRaw("TO_CHAR(created_at, 'Month') as month, SUM(ABS(\"lossProfit\")) as total")
->orderBy('created_at')
->groupBy('created_at')
->get();
@@ -204,7 +204,7 @@ public function revenue()
$data['profit'] = Sale::where('business_id', auth()->user()->business_id)
->whereYear('created_at', request('year') ?? date('Y'))
->where('lossProfit', '>=', 0)
->selectRaw('MONTHNAME(created_at) as month, SUM(ABS(lossProfit)) as total')
->selectRaw("TO_CHAR(created_at, 'Month') as month, SUM(ABS(\"lossProfit\")) as total")
->orderBy('created_at')
->groupBy('created_at')
->get();

View File

@@ -241,7 +241,7 @@ public function incomeExpense(Request $request)
$data['expenses'] = Sale::where('business_id', auth()->user()->business_id)
->whereYear('created_at', request('year') ?? date('Y'))
->where('lossProfit', '<', 0)
->selectRaw('MONTH(created_at) as month_number, MONTHNAME(created_at) as month, SUM(ABS(lossProfit)) as total')
->selectRaw("EXTRACT(MONTH FROM created_at) as month_number, TO_CHAR(created_at, 'Month') as month, SUM(ABS(lossProfit)) as total")
->groupBy('month_number', 'month')
->orderBy('month_number')
->get();
@@ -249,7 +249,7 @@ public function incomeExpense(Request $request)
$data['incomes'] = Sale::where('business_id', auth()->user()->business_id)
->whereYear('created_at', request('year') ?? date('Y'))
->where('lossProfit', '>=', 0)
->selectRaw('MONTH(created_at) as month_number, MONTHNAME(created_at) as month, SUM(ABS(lossProfit)) as total')
->selectRaw("EXTRACT(MONTH FROM created_at) as month_number, TO_CHAR(created_at, 'Month') as month, SUM(ABS(lossProfit)) as total")
->groupBy('month_number', 'month')
->orderBy('month_number')
->get();

View File

@@ -68,3 +68,8 @@ ## Authenticate
email : admin@gmail.com <br>
pass : password
docker build -f Dockerfile2 -t kulakpos-app:latest .
docker run -d --name kulakpos-testing --network kulakpos-net -p 8000:8000 --env-file .env kulakpos-app:latest

View File

@@ -10,6 +10,11 @@ trait HasUploader
private function upload(Request $request, $input, $oldFile = null, $disk = null)
{
$file = $request->file($input);
if (!$file) {
return $oldFile;
}
$ext = $file->getClientOriginalExtension();
$filename = now()->timestamp . '-' . rand(1, 1000) . '.' . $ext;
@@ -29,6 +34,11 @@ private function upload(Request $request, $input, $oldFile = null, $disk = null)
private function uploadWithFileName(Request $request, $input, $oldFile = null, $disk = null)
{
$file = $request->file($input);
if (!$file) {
return $oldFile;
}
$filename = $file->getClientOriginalName();
$path = 'files/';
@@ -47,8 +57,13 @@ private function uploadWithFileName(Request $request, $input, $oldFile = null, $
private function multipleUpload(Request $request, $input, $oldFiles = [], $disk = null)
{
$uploadedFiles = [];
$files = $request->file($input);
foreach ($request->file($input) as $file) {
if (!$files) {
return $uploadedFiles;
}
foreach ($files as $file) {
$ext = $file->getClientOriginalExtension();
$filename = now()->timestamp . '_' . uniqid() . '.' . $ext;

View File

@@ -34,7 +34,7 @@ public function getDashboardData()
public function yearlySubscriptions()
{
$subscriptions = PlanSubscribe::whereYear('created_at', request('year') ?? date('Y'))
->selectRaw('MONTHNAME(created_at) as month, SUM(price) as total_amount')
->selectRaw("TO_CHAR(created_at, 'Month') as month, SUM(price) as total_amount")
->groupBy('month')
->get();

View File

@@ -33,13 +33,13 @@ public function lossProfit(Request $request)
$salesQuery = DB::table('sales')
->select(
DB::raw('DATE(saleDate) as date'),
DB::raw('"saleDate"::date as date'),
DB::raw('SUM(actual_total_amount) as total_sales'),
DB::raw('SUM(lossProfit) as total_sale_income')
DB::raw('SUM("lossProfit") as total_sale_income')
)
->where('business_id', $businessId)
->when($branchId, fn($q) => $q->where('branch_id', $branchId))
->groupBy(DB::raw('DATE(saleDate)'));
->groupBy(DB::raw('"saleDate"::date'));
$this->applyDateFilter($salesQuery, $duration, 'saleDate', $request->from_date, $request->to_date);
@@ -56,12 +56,12 @@ public function lossProfit(Request $request)
$incomeQuery = DB::table('incomes')
->select(
DB::raw('DATE(incomeDate) as date'),
DB::raw('"incomeDate"::date as date'),
DB::raw('SUM(amount) as total_incomes')
)
->where('business_id', $businessId)
->when($branchId, fn($q) => $q->where('branch_id', $branchId))
->groupBy(DB::raw('DATE(incomeDate)'));
->groupBy(DB::raw('"incomeDate"::date'));
$this->applyDateFilter($incomeQuery, $duration, 'incomeDate', $request->from_date, $request->to_date);
$dailyIncomes = $incomeQuery->get();
@@ -92,12 +92,12 @@ public function lossProfit(Request $request)
if (moduleCheck('HrmAddon')) {
$payrollQuery = DB::table('payrolls')
->select(
DB::raw('DATE(date) as date'),
DB::raw('date::date as date'),
DB::raw('SUM(amount) as total_payrolls')
)
->where('business_id', $businessId)
->when($branchId, fn($q) => $q->where('branch_id', $branchId))
->groupBy(DB::raw('DATE(date)'));
->groupBy(DB::raw('date::date'));
$this->applyDateFilter($payrollQuery, $duration, 'date', $request->from_date, $request->to_date);
$dailyPayrolls = $payrollQuery->get();
@@ -105,12 +105,12 @@ public function lossProfit(Request $request)
$expenseQuery = DB::table('expenses')
->select(
DB::raw('DATE(expenseDate) as date'),
DB::raw('"expenseDate"::date as date'),
DB::raw('SUM(amount) as total_expenses_only')
)
->where('business_id', $businessId)
->when($branchId, fn($q) => $q->where('branch_id', $branchId))
->groupBy(DB::raw('DATE(expenseDate)'));
->groupBy(DB::raw('"expenseDate"::date'));
$this->applyDateFilter($expenseQuery, $duration, 'expenseDate', $request->from_date, $request->to_date);
$dailyExpenses = $expenseQuery->get();

View File

@@ -25,6 +25,19 @@ public function index()
{
$business_id = auth()->user()->business_id;
if (is_null($business_id)) {
$business = Business::with('category', 'enrolled_plan')->first();
if (!$business) {
return response()->json([
'status' => 'error',
'message' => 'No business found'
], 404);
}
$business_id = $business->id;
} else {
$business = Business::with('category:id,name', 'enrolled_plan:id,plan_id,business_id,price,duration,allow_multibranch', 'enrolled_plan.plan:id,subscriptionName')->findOrFail($business_id);
}
// Ensure currency exists
$business_currency = UserCurrency::select('id', 'name', 'code', 'symbol', 'position')
->where('business_id', $business_id)
@@ -60,16 +73,31 @@ public function index()
->get()
->firstWhere('value.business_id', $business_id);
$invoice_logo = $option->value['invoice_logo'] ?? null;
$a4_invoice_logo = $option->value['a4_invoice_logo'] ?? null;
$thermal_invoice_logo = $option->value['thermal_invoice_logo'] ?? null;
$invoice_scanner_logo = $option->value['invoice_scanner_logo'] ?? null;
$sale_rounding_option = $option->value['sale_rounding_option'] ?? 'none';
$note_label = $option->value['note_label'] ?? null;
$note = $option->value['note'] ?? null;
$gratitude_message = $option->value['gratitude_message'] ?? null;
$warranty_void_label = $option->value['warranty_void_label'] ?? null;
$warranty_void = $option->value['warranty_void'] ?? null;
if (!$option) {
$invoice_logo = null;
$a4_invoice_logo = null;
$thermal_invoice_logo = null;
$invoice_scanner_logo = null;
$sale_rounding_option = 'none';
$note_label = null;
$note = null;
$gratitude_message = null;
$warranty_void_label = null;
$warranty_void = null;
$option_value = [];
} else {
$option_value = $option->value;
$invoice_logo = $option_value['invoice_logo'] ?? null;
$a4_invoice_logo = $option_value['a4_invoice_logo'] ?? null;
$thermal_invoice_logo = $option_value['thermal_invoice_logo'] ?? null;
$invoice_scanner_logo = $option_value['invoice_scanner_logo'] ?? null;
$sale_rounding_option = $option_value['sale_rounding_option'] ?? 'none';
$note_label = $option_value['note_label'] ?? null;
$note = $option_value['note'] ?? null;
$gratitude_message = $option_value['gratitude_message'] ?? null;
$warranty_void_label = $option_value['warranty_void_label'] ?? null;
$warranty_void = $option_value['warranty_void'] ?? null;
}
$data = array_merge(
$business->toArray(),
@@ -87,12 +115,12 @@ public function index()
['gratitude_message' => $gratitude_message],
['warranty_void_label' => $warranty_void_label],
['warranty_void' => $warranty_void],
['show_note' => (int) ($option->value['show_note'] ?? 0)],
['show_gratitude_msg' => (int) ($option->value['show_gratitude_msg'] ?? 0)],
['show_invoice_scanner_logo' => (int) ($option->value['show_invoice_scanner_logo'] ?? 0)],
['show_a4_invoice_logo' => (int) ($option->value['show_a4_invoice_logo'] ?? 0)],
['show_thermal_invoice_logo' => (int) ($option->value['show_thermal_invoice_logo'] ?? 0)],
['show_warranty' => (int) ($option->value['show_warranty'] ?? 0)],
['show_note' => (int) ($option_value['show_note'] ?? 0)],
['show_gratitude_msg' => (int) ($option_value['show_gratitude_msg'] ?? 0)],
['show_invoice_scanner_logo' => (int) ($option_value['show_invoice_scanner_logo'] ?? 0)],
['show_a4_invoice_logo' => (int) ($option_value['show_a4_invoice_logo'] ?? 0)],
['show_thermal_invoice_logo' => (int) ($option_value['show_thermal_invoice_logo'] ?? 0)],
['show_warranty' => (int) ($option_value['show_warranty'] ?? 0)],
['develop_by_level' => $develop_by_level],
['develop_by' => $develop_by],
['develop_by_link' => $develop_by_link],

View File

@@ -106,16 +106,16 @@ public function dashboard()
return response()->json(['error' => 'Invalid duration'], 400);
}
// SQL date format for grouping
// SQL date format for grouping (PostgreSQL TO_CHAR format)
$dateFormatSQL = match ($format) {
'H' => '%H',
'd' => '%Y-%m-%d',
'M' => '%Y-%m',
default => '%Y-%m-%d',
'H' => 'HH24',
'd' => 'YYYY-MM-DD',
'M' => 'YYYY-MM',
default => 'YYYY-MM-DD',
};
// Sales data fetch and map
$sales_data = Sale::selectRaw("DATE_FORMAT(created_at, '$dateFormatSQL') as date, SUM(totalAmount) as amount")
$sales_data = Sale::selectRaw("TO_CHAR(created_at, '$dateFormatSQL') as date, SUM(\"totalAmount\") as amount")
->where('business_id', $business_id)
->whereBetween('created_at', [$start, $end])
->groupBy('date')
@@ -128,7 +128,7 @@ public function dashboard()
->keyBy('date');
// Purchase data fetch and map
$purchase_data = Purchase::selectRaw("DATE_FORMAT(created_at, '$dateFormatSQL') as date, SUM(totalAmount) as amount")
$purchase_data = Purchase::selectRaw("TO_CHAR(created_at, '$dateFormatSQL') as date, SUM(\"totalAmount\") as amount")
->where('business_id', $business_id)
->whereBetween('created_at', [$start, $end])
->groupBy('date')
@@ -187,9 +187,9 @@ public function dashboard()
$data = [
'total_expense' => (float) $expense_amount,
'total_income' => (float) $income_amount + $sale_profit,
'total_items' => Product::where('business_id', $business_id)->count(),
'total_categories' => Category::where('business_id', $business_id)->count(),
'stock_value' => $total_stock_value,
'total_items' => (int) Product::where('business_id', $business_id)->count(),
'total_categories' => (int) Category::where('business_id', $business_id)->count(),
'stock_value' => (float) $total_stock_value,
'total_due' => (float) Sale::where('business_id', $business_id)->whereBetween('saleDate', [$start, $end])->sum('dueAmount'),
'total_profit' => (float) Sale::where('business_id', $business_id)->whereBetween('created_at', [$start, $end])->where('lossProfit', '>', 0)->sum('lossProfit'),
'total_loss' => (float) Sale::where('business_id', $business_id)->whereBetween('created_at', [$start, $end])->where('lossProfit', '<', 0)->sum('lossProfit'),

View File

@@ -13,6 +13,7 @@ class WebController extends Controller
{
public function index()
{
return view('index');
$plans = Plan::where('status', 1)->get();
return view('index', compact('plans'));
}
}

View File

@@ -12,7 +12,7 @@ class TrustProxies extends Middleware
*
* @var array<int, string>|string|null
*/
protected $proxies;
protected $proxies = '*';
/**
* The headers that should be used to detect proxies.

View File

@@ -98,6 +98,14 @@ public function dueCollect()
'credit_limit' => 'double',
'loyalty_points' => 'double',
'opening_balance' => 'double',
'total_sale_amount' => 'double',
'total_sale_paid' => 'double',
'total_sale_profit' => 'double',
'total_sale_loss' => 'double',
'total_purchase_amount' => 'double',
'total_purchase_paid' => 'double',
'sales_count' => 'integer',
'purchases_count' => 'integer',
'billing_address' => 'json',
'shipping_address' => 'json',
];

View File

@@ -146,6 +146,8 @@ public function batch()
'profit_percent' => 'double',
'alert_qty' => 'double',
'stocks_sum_product_stock' => 'double',
'sale_details_sum_quantities' => 'double',
'purchase_details_sum_quantities' => 'double',
'meta' => 'json',
'variation_ids' => 'json',
'warranty_guarantee_info' => 'json',

View File

@@ -34,7 +34,7 @@ protected static function boot()
static::creating(function ($transfer) {
$lastNumber = (int) self::whereNotNull('invoice_no')
->orderByDesc('id')
->value(DB::raw("CAST(SUBSTRING(invoice_no, 4) AS UNSIGNED)")) ?? 0;
->value(DB::raw("CAST(SUBSTRING(invoice_no, 4) AS INTEGER)")) ?? 0;
$transfer->invoice_no = str_pad($lastNumber + 1, 5, '0', STR_PAD_LEFT);
});

253
build.log Normal file
View File

@@ -0,0 +1,253 @@
#0 building with "default" instance using docker driver
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 2.31kB done
#1 DONE 0.0s
#2 [internal] load metadata for docker.io/library/php:8.2-fpm-alpine
#2 ...
#3 [internal] load metadata for docker.io/library/composer:latest
#3 DONE 6.0s
#2 [internal] load metadata for docker.io/library/php:8.2-fpm-alpine
#2 DONE 6.6s
#4 [internal] load .dockerignore
#4 transferring context: 412B done
#4 DONE 0.0s
#5 [stage-0 1/17] FROM docker.io/library/php:8.2-fpm-alpine@sha256:6363baa3186e5bd43794c9612f8a34fb88657179de3da3ebc37b8618ba7bfffc
#5 DONE 0.0s
#6 FROM docker.io/library/composer:latest@sha256:743aebe48ca67097c36819040633ea77e44a561eca135e4fc84c002e63a1ba07
#6 DONE 0.0s
#7 [internal] load build context
#7 transferring context: 357.31kB 0.1s done
#7 DONE 0.1s
#8 [stage-0 3/17] RUN git config --global --add safe.directory /var/www/html
#8 CACHED
#9 [stage-0 4/17] COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
#9 CACHED
#10 [stage-0 5/17] WORKDIR /var/www/html
#10 CACHED
#11 [stage-0 6/17] COPY composer.json composer.lock ./
#11 CACHED
#12 [stage-0 2/17] RUN apk add --no-cache vim xxd libzip-dev postgresql-dev libpng-dev libjpeg-turbo-dev freetype-dev oniguruma-dev git curl nodejs npm && docker-php-ext-configure gd --with-freetype --with-jpeg && docker-php-ext-install -j$(nproc) pdo_pgsql pgsql opcache zip bcmath gd exif pcntl
#12 CACHED
#13 [stage-0 7/17] RUN mkdir -p storage/framework/cache && mkdir -p storage/framework/sessions && mkdir -p storage/framework/views && mkdir -p storage/framework/testing && mkdir -p storage/logs && mkdir -p bootstrap/cache && chmod -R 777 storage && chmod -R 777 bootstrap/cache
#13 CACHED
#14 [stage-0 8/17] RUN composer install --no-interaction --no-progress --no-scripts
#14 0.298 Installing dependencies from lock file (including require-dev)
#14 0.303 Verifying lock file contents can be installed on current platform.
#14 0.325 Package operations: 202 installs, 0 updates, 0 removals
#14 0.343 - Downloading php-http/discovery (1.20.0)
#14 0.343 - Downloading spatie/color (1.8.0)
#14 0.344 - Downloading voku/portable-ascii (2.0.3)
#14 0.344 - Downloading symfony/polyfill-php80 (v1.33.0)
#14 0.344 - Downloading symfony/polyfill-mbstring (v1.33.0)
#14 0.344 - Downloading symfony/polyfill-ctype (v1.33.0)
#14 0.344 - Downloading phpoption/phpoption (1.9.5)
#14 0.344 - Downloading graham-campbell/result-type (v1.1.4)
#14 0.344 - Downloading vlucas/phpdotenv (v5.6.3)
#14 0.344 - Downloading symfony/css-selector (v7.4.0)
#14 0.345 - Downloading tijsverkoyen/css-to-inline-styles (v2.4.0)
#14 0.345 - Downloading symfony/deprecation-contracts (v3.6.0)
#14 0.345 - Downloading symfony/var-dumper (v6.4.26)
#14 0.345 - Downloading symfony/polyfill-uuid (v1.33.0)
#14 0.345 - Downloading symfony/uid (v6.4.24)
#14 0.345 - Downloading symfony/routing (v6.4.30)
#14 0.345 - Downloading symfony/process (v6.4.31)
#14 0.345 - Downloading symfony/polyfill-intl-normalizer (v1.33.0)
#14 0.345 - Downloading symfony/polyfill-intl-idn (v1.33.0)
#14 0.345 - Downloading symfony/mime (v6.4.30)
#14 0.346 - Downloading psr/container (2.0.2)
#14 0.346 - Downloading symfony/service-contracts (v3.6.1)
#14 0.346 - Downloading psr/event-dispatcher (1.0.0)
#14 0.346 - Downloading symfony/event-dispatcher-contracts (v3.6.0)
#14 0.346 - Downloading symfony/event-dispatcher (v7.4.0)
#14 0.346 - Downloading psr/log (3.0.2)
#14 0.346 - Downloading doctrine/deprecations (1.1.5)
#14 0.346 - Downloading doctrine/lexer (2.1.1)
#14 0.346 - Downloading egulias/email-validator (4.0.4)
#14 0.346 - Downloading symfony/mailer (v6.4.31)
#14 0.346 - Downloading symfony/polyfill-php83 (v1.33.0)
#14 0.346 - Downloading symfony/http-foundation (v6.4.31)
#14 0.346 - Downloading symfony/error-handler (v6.4.26)
#14 0.346 - Downloading symfony/http-kernel (v6.4.31)
#14 0.347 - Downloading symfony/finder (v6.4.31)
#14 0.347 - Downloading symfony/polyfill-intl-grapheme (v1.33.0)
#14 0.347 - Downloading symfony/string (v7.4.0)
#14 0.347 - Downloading symfony/console (v6.4.31)
#14 0.347 - Downloading ramsey/collection (2.1.1)
#14 0.347 - Downloading brick/math (0.12.3)
#14 0.347 - Downloading ramsey/uuid (4.9.2)
#14 0.347 - Downloading psr/simple-cache (3.0.0)
#14 0.347 - Downloading nunomaduro/termwind (v1.17.0)
#14 0.347 - Downloading symfony/translation-contracts (v3.6.1)
#14 0.347 - Downloading symfony/translation (v6.4.31)
#14 0.347 - Downloading psr/clock (1.0.0)
#14 0.347 - Downloading carbonphp/carbon-doctrine-types (2.1.0)
#14 0.348 - Downloading nesbot/carbon (2.73.0)
#14 0.348 - Downloading monolog/monolog (3.10.0)
#14 0.348 - Downloading league/mime-type-detection (1.16.0)
#14 0.348 - Downloading league/flysystem (3.30.2)
#14 0.348 - Downloading league/flysystem-local (3.30.2)
#14 0.348 - Downloading nette/utils (v4.1.1)
#14 0.348 - Downloading nette/schema (v1.3.3)
#14 0.348 - Downloading dflydev/dot-access-data (v3.0.3)
#14 0.348 - Downloading league/config (v1.2.0)
#14 0.348 - Downloading league/commonmark (2.8.0)
#14 0.348 - Downloading laravel/serializable-closure (v1.3.7)
#14 0.348 - Downloading laravel/framework (v10.50.0)
#14 0.349 - Downloading laravel/prompts (v0.1.25)
#14 0.349 - Downloading guzzlehttp/uri-template (v1.0.5)
#14 0.349 - Downloading fruitcake/php-cors (v1.4.0)
#14 0.349 - Downloading dragonmantank/cron-expression (v3.6.0)
#14 0.349 - Downloading doctrine/inflector (2.1.0)
#14 0.349 - Downloading ageekdev/laravel-barcode (v1.3.0)
#14 0.349 - Downloading anandsiddharth/laravel-paytm-wallet (v2.0.0)
#14 0.349 - Downloading symfony/filesystem (v7.4.0)
#14 0.349 - Downloading psr/http-message (2.0)
#14 0.349 - Downloading mtdowling/jmespath.php (2.8.0)
#14 0.349 - Downloading ralouphie/getallheaders (3.0.3)
#14 0.349 - Downloading psr/http-factory (1.1.0)
#14 0.349 - Downloading guzzlehttp/psr7 (2.8.0)
#14 0.350 - Downloading guzzlehttp/promises (2.3.0)
#14 0.350 - Downloading psr/http-client (1.0.3)
#14 0.350 - Downloading guzzlehttp/guzzle (7.10.0)
#14 0.350 - Downloading aws/aws-crt-php (v1.2.7)
#14 0.350 - Downloading aws/aws-sdk-php (3.369.10)
#14 0.350 - Downloading masterminds/html5 (2.10.0)
#14 0.350 - Downloading thecodingmachine/safe (v3.3.0)
#14 0.350 - Downloading sabberworm/php-css-parser (v9.1.0)
#14 0.350 - Downloading dompdf/php-svg-lib (1.0.2)
#14 0.350 - Downloading dompdf/php-font-lib (1.0.1)
#14 0.350 - Downloading dompdf/dompdf (v3.1.4)
#14 0.350 - Downloading barryvdh/laravel-dompdf (v3.1.1)
#14 0.351 - Downloading dasprid/enum (1.0.7)
#14 0.351 - Downloading dipesh79/laravel-phonepe (1.0.1)
#14 0.351 - Downloading psr/cache (3.0.0)
#14 0.351 - Downloading doctrine/event-manager (2.0.1)
#14 0.351 - Downloading doctrine/persistence (4.1.1)
#14 0.351 - Downloading fakerphp/faker (v1.24.1)
#14 0.351 - Downloading fedapay/fedapay-php (0.4.7)
#14 0.351 - Downloading rize/uri-template (0.4.1)
#14 0.351 - Downloading grpc/grpc (1.74.0)
#14 0.351 - Downloading google/protobuf (v4.33.3)
#14 0.351 - Downloading google/longrunning (0.6.0)
#14 0.351 - Downloading firebase/php-jwt (v6.11.1)
#14 0.352 - Downloading google/auth (v1.50.0)
#14 0.352 - Downloading google/grpc-gcp (v0.4.1)
#14 0.352 - Downloading google/common-protos (4.12.4)
#14 0.352 - Downloading google/gax (v1.40.0)
#14 0.352 - Downloading google/cloud-core (v1.69.0)
#14 0.352 - Downloading hardevine/shoppingcart (3.4)
#14 0.352 - Downloading karim007/laravel-bkash-tokenize (v2.4)
#14 0.352 - Downloading kavenegar/php (v1.2.2)
#14 0.352 - Downloading lcobucci/jwt (5.6.0)
#14 0.352 - Downloading fig/http-message-util (1.1.5)
#14 0.352 - Downloading beste/clock (3.0.0)
#14 0.352 - Downloading kreait/firebase-tokens (5.3.0)
#14 0.353 - Downloading google/cloud-storage (v1.49.0)
#14 0.353 - Downloading cuyz/valinor (2.3.1)
#14 0.353 - Downloading beste/json (1.7.0)
#14 0.353 - Downloading beste/in-memory-cache (1.4.0)
#14 0.353 - Downloading kreait/firebase-php (7.24.0)
#14 0.353 - Downloading laravel/breeze (v1.29.1)
#14 0.353 - Downloading laravel/pint (v1.27.0)
#14 0.353 - Downloading symfony/yaml (v7.4.1)
#14 0.353 - Downloading laravel/sail (v1.52.0)
#14 0.353 - Downloading laravel/sanctum (v3.3.3)
#14 0.353 - Downloading paragonie/random_compat (v9.99.100)
#14 0.353 - Downloading paragonie/constant_time_encoding (v3.1.3)
#14 0.353 - Downloading phpseclib/phpseclib (3.0.48)
#14 0.354 - Downloading league/oauth1-client (v1.11.0)
#14 0.354 - Downloading laravel/socialite (v5.24.1)
#14 0.354 - Downloading nikic/php-parser (v5.7.0)
#14 0.354 - Downloading psy/psysh (v0.12.18)
#14 0.354 - Downloading laravel/tinker (v2.11.0)
#14 0.354 - Downloading php-http/promise (1.3.1)
#14 0.354 - Downloading php-http/httplug (2.4.1)
#14 0.354 - Downloading php-http/guzzle7-adapter (1.1.0)
#14 0.354 - Downloading php-http/message-factory (1.1.0)
#14 0.354 - Downloading clue/stream-filter (v1.7.0)
#14 0.354 - Downloading php-http/message (1.16.2)
#14 0.354 - Downloading moneyphp/money (v4.8.0)
#14 0.355 - Downloading omnipay/common (v3.4.0)
#14 0.355 - Downloading markbaker/matrix (3.0.1)
#14 0.355 - Downloading markbaker/complex (3.0.2)
#14 0.355 - Downloading maennchen/zipstream-php (3.1.2)
#14 0.355 - Downloading ezyang/htmlpurifier (v4.19.0)
#14 0.355 - Downloading composer/pcre (3.3.2)
#14 0.355 - Downloading phpoffice/phpspreadsheet (1.30.0)
#14 0.355 - Downloading composer/semver (3.4.4)
#14 0.355 - Downloading maatwebsite/excel (3.1.67)
#14 0.356 - Downloading mediaburst/clockworksms (2.0.0)
#14 0.356 - Downloading melipayamak/php (1.0.0)
#14 0.356 - Downloading doctrine/common (3.5.0)
#14 0.356 - Downloading doctrine/annotations (1.14.4)
#14 0.356 - Downloading mercadopago/dx-php (2.6.2)
#14 0.356 - Downloading hamcrest/hamcrest-php (v2.1.1)
#14 0.356 - Downloading mockery/mockery (1.6.12)
#14 0.356 - Downloading composer/ca-bundle (1.5.10)
#14 0.356 - Downloading mollie/mollie-api-php (v2.79.1)
#14 0.356 - Downloading setasign/fpdi (v2.6.4)
#14 0.356 - Downloading myclabs/deep-copy (1.13.4)
#14 0.357 - Downloading mpdf/psr-log-aware-trait (v3.0.0)
#14 0.357 - Downloading mpdf/psr-http-message-shim (v2.0.1)
#14 0.357 - Downloading mpdf/mpdf (v8.2.7)
#14 0.357 - Downloading filp/whoops (2.18.4)
#14 0.357 - Downloading nunomaduro/collision (v7.12.0)
#14 0.357 - Downloading nwidart/laravel-modules (10.0.6)
#14 0.357 - Downloading omnipay/paypal (v3.0.2)
#14 0.357 - Downloading omnipay/stripe (v3.2.0)
#14 0.357 - Downloading paytm/paytmchecksum (v1.1.0)
#14 0.357 - Downloading sebastian/version (4.0.1)
#14 0.357 - Downloading sebastian/type (4.0.0)
#14 0.357 - Downloading sebastian/recursion-context (5.0.1)
#14 0.358 - Downloading sebastian/object-reflector (3.0.0)
#14 0.358 - Downloading sebastian/object-enumerator (5.0.0)
#14 0.358 - Downloading sebastian/global-state (6.0.2)
#14 0.358 - Downloading sebastian/exporter (5.1.4)
#14 0.358 - Downloading sebastian/environment (6.1.0)
#14 0.358 - Downloading sebastian/diff (5.1.1)
#14 0.358 - Downloading sebastian/comparator (5.0.4)
#14 0.358 - Downloading sebastian/code-unit (2.0.0)
#14 0.358 - Downloading sebastian/cli-parser (2.0.1)
#14 0.358 - Downloading phpunit/php-timer (6.0.0)
#14 0.358 - Downloading phpunit/php-text-template (3.0.1)
#14 0.358 - Downloading phpunit/php-invoker (4.0.0)
#14 0.358 - Downloading phpunit/php-file-iterator (4.1.0)
#14 0.358 - Downloading theseer/tokenizer (1.3.1)
#14 0.359 - Downloading sebastian/lines-of-code (2.0.2)
#14 0.359 - Downloading sebastian/complexity (3.2.0)
#14 0.359 - Downloading sebastian/code-unit-reverse-lookup (3.0.0)
#14 0.359 - Downloading phpunit/php-code-coverage (10.1.16)
#14 0.359 - Downloading phar-io/version (3.2.1)
#14 0.359 - Downloading phar-io/manifest (2.0.4)
#14 0.359 - Downloading phpunit/phpunit (10.5.60)
#14 0.359 - Downloading rmccue/requests (v2.0.17)
#14 0.359 - Downloading razorpay/razorpay (2.9.2)
#14 0.359 - Downloading safiull/laravel-installer (3.5.2)
#14 0.359 - Downloading bacon/bacon-qr-code (2.0.8)
#14 0.359 - Downloading simplesoftwareio/simple-qrcode (4.2.0)
#14 0.360 - Downloading smsgatewayme/client (v0.0.1)
#14 0.360 - Downloading spatie/error-solutions (1.1.3)
#14 0.360 - Downloading spatie/backtrace (1.8.1)
#14 0.360 - Downloading spatie/flare-client-php (1.10.1)
#14 0.360 - Downloading spatie/ignition (1.15.1)
#14 0.360 - Downloading spatie/laravel-ignition (2.9.1)
#14 0.360 - Downloading spatie/laravel-permission (6.24.0)
#14 0.360 - Downloading stripe/stripe-php (v13.18.0)
#14 0.360 - Downloading twilio/sdk (8.10.1)
#14 0.360 - Downloading tzsk/sms (9.0.0)
#14 10.38 A connection timeout was encountered. If you intend to run Composer without connecting to the internet, run the command again prefixed with COMPOSER_DISABLE_NETWORK=1 to make Composer run in offline mode.

View File

@@ -53,7 +53,8 @@
"mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^7.0",
"phpunit/phpunit": "^10.1",
"spatie/laravel-ignition": "^2.0"
"spatie/laravel-ignition": "^2.0",
"knuckleswtf/scribe": "5.9.0"
},
"autoload": {
"psr-4": {
@@ -92,6 +93,9 @@
}
},
"config": {
"platform": {
"php": "8.2.30"
},
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,

1489
composer.lock generated

File diff suppressed because it is too large Load Diff

259
config/scribe.php Normal file
View File

@@ -0,0 +1,259 @@
<?php
use Knuckles\Scribe\Config\AuthIn;
use Knuckles\Scribe\Config\Defaults;
use Knuckles\Scribe\Extracting\Strategies;
use function Knuckles\Scribe\Config\configureStrategy;
use function Knuckles\Scribe\Config\removeStrategies;
// Only the most common configs are shown. See the https://scribe.knuckles.wtf/laravel/reference/config for all.
return [
// The HTML <title> for the generated documentation.
'title' => config('app.name').' API Documentation',
// A short description of your API. Will be included in the docs webpage, Postman collection and OpenAPI spec.
'description' => '',
// Text to place in the "Introduction" section, right after the `description`. Markdown and HTML are supported.
'intro_text' => <<<'INTRO'
This documentation aims to provide all the information you need to work with our API.
<aside>As you scroll, you'll see code examples for working with the API in different programming languages in the dark area to the right (or as part of the content on mobile).
You can switch the language used with the tabs at the top right (or from the nav menu at the top left on mobile).</aside>
INTRO,
// The base URL displayed in the docs.
// If you're using `laravel` type, you can set this to a dynamic string, like '{{ config("app.tenant_url") }}' to get a dynamic base URL.
'base_url' => config('app.url'),
// Routes to include in the docs
'routes' => [
[
'match' => [
// Match only routes whose paths match this pattern (use * as a wildcard to match any characters). Example: 'users/*'.
'prefixes' => ['api/*'],
// Match only routes whose domains match this pattern (use * as a wildcard to match any characters). Example: 'api.*'.
'domains' => ['*'],
],
// Include these routes even if they did not match the rules above.
'include' => [
// 'users.index', 'POST /new', '/auth/*'
],
// Exclude these routes even if they matched the rules above.
'exclude' => [
// 'GET /health', 'admin.*'
],
],
],
// The type of documentation output to generate.
// - "static" will generate a static HTMl page in the /public/docs folder,
// - "laravel" will generate the documentation as a Blade view, so you can add routing and authentication.
// - "external_static" and "external_laravel" do the same as above, but pass the OpenAPI spec as a URL to an external UI template
'type' => 'laravel',
// See https://scribe.knuckles.wtf/laravel/reference/config#theme for supported options
'theme' => 'default',
'static' => [
// HTML documentation, assets and Postman collection will be generated to this folder.
// Source Markdown will still be in resources/docs.
'output_path' => 'public/docs',
],
'laravel' => [
// Whether to automatically create a docs route for you to view your generated docs. You can still set up routing manually.
'add_routes' => true,
// URL path to use for the docs endpoint (if `add_routes` is true).
// By default, `/docs` opens the HTML page, `/docs.postman` opens the Postman collection, and `/docs.openapi` the OpenAPI spec.
'docs_url' => '/docs',
// Directory within `public` in which to store CSS and JS assets.
// By default, assets are stored in `public/vendor/scribe`.
// If set, assets will be stored in `public/{{assets_directory}}`
'assets_directory' => null,
// Middleware to attach to the docs endpoint (if `add_routes` is true).
'middleware' => [],
],
'external' => [
'html_attributes' => [],
],
'try_it_out' => [
// Add a Try It Out button to your endpoints so consumers can test endpoints right from their browser.
// Don't forget to enable CORS headers for your endpoints.
'enabled' => true,
// The base URL to use in the API tester. Leave as null to be the same as the displayed URL (`scribe.base_url`).
'base_url' => null,
// [Laravel Sanctum] Fetch a CSRF token before each request, and add it as an X-XSRF-TOKEN header.
'use_csrf' => false,
// The URL to fetch the CSRF token from (if `use_csrf` is true).
'csrf_url' => '/sanctum/csrf-cookie',
],
// How is your API authenticated? This information will be used in the displayed docs, generated examples and response calls.
'auth' => [
// Set this to true if ANY endpoints in your API use authentication.
'enabled' => false,
// Set this to true if your API should be authenticated by default. If so, you must also set `enabled` (above) to true.
// You can then use @unauthenticated or @authenticated on individual endpoints to change their status from the default.
'default' => false,
// Where is the auth value meant to be sent in a request?
'in' => AuthIn::BEARER->value,
// The name of the auth parameter (e.g. token, key, apiKey) or header (e.g. Authorization, Api-Key).
'name' => 'key',
// The value of the parameter to be used by Scribe to authenticate response calls.
// This will NOT be included in the generated documentation. If empty, Scribe will use a random value.
'use_value' => env('SCRIBE_AUTH_KEY'),
// Placeholder your users will see for the auth parameter in the example requests.
// Set this to null if you want Scribe to use a random value as placeholder instead.
'placeholder' => '{YOUR_AUTH_KEY}',
// Any extra authentication-related info for your users. Markdown and HTML are supported.
'extra_info' => 'You can retrieve your token by visiting your dashboard and clicking <b>Generate API token</b>.',
],
// Example requests for each endpoint will be shown in each of these languages.
// Supported options are: bash, javascript, php, python
// To add a language of your own, see https://scribe.knuckles.wtf/laravel/advanced/example-requests
// Note: does not work for `external` docs types
'example_languages' => [
'bash',
'javascript',
],
// Generate a Postman collection (v2.1.0) in addition to HTML docs.
// For 'static' docs, the collection will be generated to public/docs/collection.json.
// For 'laravel' docs, it will be generated to storage/app/scribe/collection.json.
// Setting `laravel.add_routes` to true (above) will also add a route for the collection.
'postman' => [
'enabled' => true,
'overrides' => [
// 'info.version' => '2.0.0',
],
],
// Generate an OpenAPI spec in addition to docs webpage.
// For 'static' docs, the collection will be generated to public/docs/openapi.yaml.
// For 'laravel' docs, it will be generated to storage/app/scribe/openapi.yaml.
// Setting `laravel.add_routes` to true (above) will also add a route for the spec.
'openapi' => [
'enabled' => true,
// The OpenAPI spec version to generate. Supported versions: '3.0.3', '3.1.0'.
// OpenAPI 3.1 is more compatible with JSON Schema and is becoming the dominant version.
// See https://spec.openapis.org/oas/v3.1.0 for details on 3.1 changes.
'version' => '3.0.3',
'overrides' => [
// 'info.version' => '2.0.0',
],
// Additional generators to use when generating the OpenAPI spec.
// Should extend `Knuckles\Scribe\Writing\OpenApiSpecGenerators\OpenApiGenerator`.
'generators' => [],
],
'groups' => [
// Endpoints which don't have a @group will be placed in this default group.
'default' => 'Endpoints',
// By default, Scribe will sort groups alphabetically, and endpoints in the order their routes are defined.
// You can override this by listing the groups, subgroups and endpoints here in the order you want them.
// See https://scribe.knuckles.wtf/blog/laravel-v4#easier-sorting and https://scribe.knuckles.wtf/laravel/reference/config#order for details
// Note: does not work for `external` docs types
'order' => [],
],
// Custom logo path. This will be used as the value of the src attribute for the <img> tag,
// so make sure it points to an accessible URL or path. Set to false to not use a logo.
// For example, if your logo is in public/img:
// - 'logo' => '../img/logo.png' // for `static` type (output folder is public/docs)
// - 'logo' => 'img/logo.png' // for `laravel` type
'logo' => false,
// Customize the "Last updated" value displayed in the docs by specifying tokens and formats.
// Examples:
// - {date:F j Y} => March 28, 2022
// - {git:short} => Short hash of the last Git commit
// Available tokens are `{date:<format>}` and `{git:<format>}`.
// The format you pass to `date` will be passed to PHP's `date()` function.
// The format you pass to `git` can be either "short" or "long".
// Note: does not work for `external` docs types
'last_updated' => 'Last updated: {date:F j, Y}',
'examples' => [
// Set this to any number to generate the same example values for parameters on each run,
'faker_seed' => 1234,
// With API resources and transformers, Scribe tries to generate example models to use in your API responses.
// By default, Scribe will try the model's factory, and if that fails, try fetching the first from the database.
// You can reorder or remove strategies here.
'models_source' => ['factoryCreate', 'factoryMake', 'databaseFirst'],
],
// The strategies Scribe will use to extract information about your routes at each stage.
// Use configureStrategy() to specify settings for a strategy in the list.
// Use removeStrategies() to remove an included strategy.
'strategies' => [
'metadata' => [
...Defaults::METADATA_STRATEGIES,
],
'headers' => [
...Defaults::HEADERS_STRATEGIES,
Strategies\StaticData::withSettings(data: [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
]),
],
'urlParameters' => [
...Defaults::URL_PARAMETERS_STRATEGIES,
],
'queryParameters' => [
...Defaults::QUERY_PARAMETERS_STRATEGIES,
],
'bodyParameters' => [
...Defaults::BODY_PARAMETERS_STRATEGIES,
],
'responses' => configureStrategy(
Defaults::RESPONSES_STRATEGIES,
Strategies\Responses\ResponseCalls::withSettings(
only: ['GET *'],
// Recommended: disable debug mode in response calls to avoid error stack traces in responses
config: [
'app.debug' => false,
]
)
),
'responseFields' => [
...Defaults::RESPONSE_FIELDS_STRATEGIES,
],
],
// For response calls, API resource responses and transformer responses,
// Scribe will try to start database transactions, so no changes are persisted to your database.
// Tell Scribe which connections should be transacted here. If you only use one db connection, you can leave this as is.
'database_connections_to_transact' => [config('database.default')],
'fractal' => [
// If you are using a custom serializer with league/fractal, you can specify it here.
'serializer' => null,
],
];

1210
converted.sql Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,201 @@
<?php
namespace Database\Seeders;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
class InitialDataSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$sqlPath = base_path('db/kulakposmysql.sql');
if (!File::exists($sqlPath)) {
$this->command->error("SQL file not found at: {$sqlPath}");
return;
}
$this->command->info("Starting initial data import from: {$sqlPath}");
// Disable foreign key checks for PostgreSQL
if (DB::getDriverName() === 'pgsql') {
DB::statement("SET session_replication_role = 'replica';");
} else {
Schema::disableForeignKeyConstraints();
}
$handle = fopen($sqlPath, "r");
if ($handle) {
$currentTable = null;
$insertCount = 0;
$tableColumnMap = [];
while (($line = fgets($handle)) !== false) {
$line = trim($line);
// Detect CREATE TABLE statement to extract column order
if (preg_match('/^CREATE TABLE `(.+?)`/i', $line, $matches)) {
$currentTable = $matches[1];
$tableColumnMap[$currentTable] = [];
continue;
}
// If we are inside a CREATE TABLE block, look for column definitions
if ($currentTable && preg_match('/^\s*`(.+?)`/i', $line, $colMatches)) {
$tableColumnMap[$currentTable][] = $colMatches[1];
}
// End of CREATE TABLE block
if ($currentTable && strpos($line, ') ENGINE') !== false) {
$currentTable = null;
}
// Detect INSERT INTO statement
if (preg_match('/^INSERT INTO `(.+?)` VALUES\s*\((.*)\);$/i', $line, $matches)) {
$tableName = $matches[1];
$valuesStr = $matches[2];
$values = $this->parseValues($valuesStr);
$sqlColumns = $tableColumnMap[$tableName] ?? [];
try {
$data = $this->mapValuesToTable($tableName, $values, $sqlColumns);
if (!empty($data)) {
DB::table($tableName)->insert($data);
$insertCount++;
if ($insertCount % 100 === 0) {
$this->command->info("Imported {$insertCount} records...");
}
}
} catch (\Exception $e) {
$this->command->error("Error inserting into {$tableName}: " . $e->getMessage());
}
}
}
fclose($handle);
$this->command->info("Finished! Total records imported: {$insertCount}");
}
// Re-enable foreign key checks
if (DB::getDriverName() === 'pgsql') {
DB::statement("SET session_replication_role = 'origin';");
// Update sequences for PostgreSQL
$this->updatePostgresSequences();
}
Schema::enableForeignKeyConstraints();
}
/**
* Update PostgreSQL sequences so they don't clash with imported IDs.
*/
private function updatePostgresSequences()
{
$this->command->info("Updating PostgreSQL sequences...");
$tables = DB::select("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'");
foreach ($tables as $table) {
$tableName = $table->table_name;
try {
$maxId = DB::table($tableName)->max('id');
if ($maxId) {
$seqName = "{$tableName}_id_seq";
// Check if sequence exists
$seqExists = DB::select("SELECT 1 FROM pg_class WHERE relname = ?", [$seqName]);
if ($seqExists) {
DB::statement("SELECT setval(?, ?, true)", [$seqName, $maxId]);
}
}
} catch (\Exception $e) {
// Ignore if table doesn't have id column or sequence
}
}
}
/**
* Basic parser for MySQL values string: (val1, val2, ...)
*/
private function parseValues($str)
{
$values = [];
$i = 0;
$len = strlen($str);
$current = '';
$inString = false;
$stringChar = '';
while ($i < $len) {
$char = $str[$i];
if ($inString) {
if ($char === $stringChar && ($i === 0 || $str[$i - 1] !== '\\')) {
$inString = false;
$values[] = str_replace(['\\\'', '\\"'], ["'", '"'], $current);
$current = '';
} else {
$current .= $char;
}
} else {
if ($char === "'" || $char === '"') {
$inString = true;
$stringChar = $char;
} elseif ($char === ',') {
$val = trim($current);
if ($val !== '') {
$values[] = $this->castValue($val);
}
$current = '';
} else {
$current .= $char;
}
}
$i++;
}
// Last value
$val = trim($current);
if ($val !== '') {
$values[] = $this->castValue($val);
}
return $values;
}
private function castValue($val)
{
if (strtoupper($val) === 'NULL') return null;
if (is_numeric($val) && (strpos($val, '0') !== 0 || $val === '0')) {
return $val + 0; // Convert to int or float
}
return trim($val, "'\"");
}
/**
* Map numerical values to column names based on SQL dump schema and filter by DB schema.
*/
private function mapValuesToTable($table, $values, $sqlColumns)
{
if (empty($sqlColumns) || count($sqlColumns) !== count($values)) {
return []; // Can't reliably map
}
// Combine using SQL column names
$mapped = array_combine($sqlColumns, $values);
// Get actual columns in the database
static $dbColumnsCache = [];
if (!isset($dbColumnsCache[$table])) {
$dbColumnsCache[$table] = DB::getSchemaBuilder()->getColumnListing($table);
}
$dbColumns = $dbColumnsCache[$table];
// Filter out columns that don't exist in the current DB
return array_intersect_key($mapped, array_flip($dbColumns));
}
}

1096
db/kulakpg.sql Normal file

File diff suppressed because it is too large Load Diff

60
docker-compose.yml Normal file
View File

@@ -0,0 +1,60 @@
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: kulakpos_app
restart: unless-stopped
tty: true
environment:
SERVICE_NAME: app
SERVICE_TAGS: dev
volumes:
- .:/var/www/html
- ./entrypoint.sh:/entrypoint.sh
networks:
- kulakpos-network
depends_on:
- db
db:
image: postgres:15-alpine
container_name: kulakpos_db
restart: unless-stopped
tty: true
ports:
- "5433:5432"
environment:
POSTGRES_DB: kulakpos
POSTGRES_USER: postgres
POSTGRES_PASSWORD: gaspolvr46
volumes:
- dbdata:/var/lib/postgresql/data
- ./converted.sql:/docker-entrypoint-initdb.d/init.sql:ro
networks:
- kulakpos-network
nginx:
image: nginx:alpine
container_name: kulakpos_nginx
restart: unless-stopped
tty: true
ports:
- "8080:80"
volumes:
- .:/var/www/html
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- kulakpos-network
depends_on:
- app
networks:
kulakpos-network:
driver: bridge
volumes:
dbdata:
driver: local

22
docker/nginx/default.conf Normal file
View File

@@ -0,0 +1,22 @@
server {
listen 80;
index index.php index.html;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /var/www/html/public;
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass app:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
gzip_static on;
}
}

92
dockerfile.txt Normal file
View File

@@ -0,0 +1,92 @@
FROM php:8.2-fpm-alpine
# Install system dependencies
RUN apk add --no-cache \
vim \
xxd \
libzip-dev \
postgresql-dev \
libpng-dev \
libjpeg-turbo-dev \
freetype-dev \
oniguruma-dev \
git \
curl \
nodejs \
npm \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) \
pdo_pgsql \
pgsql \
opcache \
zip \
bcmath \
gd \
exif \
pcntl
# Configure git to allow safe directory
RUN git config --global --add safe.directory /var/www/html
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
# Copy ONLY composer files first
COPY composer.json ./
# Create storage and bootstrap directories needed for composer scripts
RUN mkdir -p storage/framework/cache \
&& mkdir -p storage/framework/sessions \
&& mkdir -p storage/framework/views \
&& mkdir -p storage/framework/testing \
&& mkdir -p storage/logs \
&& mkdir -p bootstrap/cache \
&& chmod -R 777 storage \
&& chmod -R 777 bootstrap/cache
# Configure Composer for stability on unstable networks
ENV COMPOSER_ALLOW_SUPERUSER=1 \
COMPOSER_MEMORY_LIMIT=-1 \
COMPOSER_PROCESS_TIMEOUT=2000
# Install PHP dependencies (this layer will be cached)
RUN composer config -g repo.packagist composer https://packagist.org \
&& composer install --no-interaction --no-progress --no-scripts --prefer-dist
# Isolate the clean vendor directory
#RUN mv vendor /vendor_clean
# Copy the rest of the application files (will ignore vendor because of .dockerignore)
COPY --chown=www-data:www-data . .
# Restore the clean vendor directory and set proper permissions
#RUN rm -rf vendor && mv /vendor_clean vendor && chown -R www-data:www-data vendor
# Final build verification
RUN if [ ! -f vendor/laravel/prompts/src/helpers.php ]; then \
echo "ERROR: laravel/prompts/src/helpers.php missing!" && exit 1; \
fi
# Final check and debug listing
RUN ls -l vendor/laravel/prompts/src/helpers.php \
&& du -sh vendor/laravel/prompts
# Copy package files for NPM
COPY package*.json ./
COPY vite.config.js ./
# Install NPM dependencies
RUN npm install --legacy-peer-deps --no-audit
# Build Vite assets
RUN npm run build
# Copy entrypoint script and ensure it's executable
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Let the entrypoint handle artisan commands at runtime
EXPOSE 8000 9000
ENTRYPOINT ["/entrypoint.sh"]

60
dockerfile2.txt Normal file
View File

@@ -0,0 +1,60 @@
FROM php:8.4-fpm
# 1. Setup NodeSource untuk instalasi Node.js v20 (Cepat & Stabil)
RUN apt-get update && apt-get install -y ca-certificates curl gnupg \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
# 2. Install dependencies utama dengan apt-get
RUN apt-get update && apt-get install -y \
git \
libpng-dev \
libonig-dev \
libxml2-dev \
libpq-dev \
libfreetype6-dev \
libjpeg62-turbo-dev \
libzip-dev \
zip \
unzip \
nodejs \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# 3. Install PHP Extensions (TERMASUK pgsql / pdo_pgsql)
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install pdo_mysql pdo_pgsql pgsql mbstring exif pcntl bcmath gd zip
# 4. Get latest Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /var/www
# 5. Copy existing application directory contents
COPY . /var/www
# 5.5 PASTIKAN FOLDER CACHE & STORAGE TERBUAT SEBELUM COMPOSER RUN
RUN mkdir -p /var/www/storage/framework/cache/data \
/var/www/storage/framework/sessions \
/var/www/storage/framework/views \
/var/www/storage/framework/testing \
/var/www/storage/logs \
/var/www/bootstrap/cache
# 6. Install dependencies
ENV COMPOSER_ALLOW_SUPERUSER=1 \
COMPOSER_MEMORY_LIMIT=-1
RUN composer install --no-interaction --optimize-autoloader
# 7. Install & Build Frontend
RUN npm install
RUN npm run build
# 8. Set permissions
RUN chown -R www-data:www-data /var/www/storage /var/www/bootstrap/cache \
&& chmod -R 775 /var/www/storage /var/www/bootstrap/cache
# 9. Expose port 8000 and start php artisan serve
EXPOSE 8000
# artisan serve berjalan langsung
CMD ["php", "artisan", "serve", "--host=0.0.0.0", "--port=8000"]

217
entrypoint.sh Normal file → Executable file
View File

@@ -9,6 +9,15 @@ echo "========================================"
echo "Current user: $(whoami)"
echo "Current directory: $(pwd)"
# Set debug based on environment
if [ "$APP_ENV" = "production" ]; then
export APP_DEBUG=false
export LOG_LEVEL=error
else
export APP_DEBUG=true
export LOG_LEVEL=debug
fi
# Create ALL required Laravel directories
echo "Creating storage directories..."
mkdir -p storage/framework/cache
@@ -54,33 +63,205 @@ EOF
fi
fi
# Generate key if not present
if ! grep -q "APP_KEY=" .env || grep -q "APP_KEY=$" .env; then
echo "Generating application key..."
php artisan key:generate --no-interaction --force || echo "Key generation failed"
# LOAD environment variables from .env for use in this script
if [ -f .env ]; then
echo "Loading environment variables from .env..."
# Read .env file line by line and export each variable
while IFS= read -r line || [ -n "$line" ]; do
# Remove carriage return if present (Windows style)
line=$(echo "$line" | sed 's/\r$//')
# Skip comments and empty lines
case "$line" in
"#"*) continue ;;
"") continue ;;
esac
# Remove quotes from value and export
export "$(echo "$line" | sed 's/["'\'']//g')" 2>/dev/null || true
done < .env
fi
# Clear all caches to ensure fresh state
echo "Clearing caches..."
php artisan config:clear || true
php artisan cache:clear || true
php artisan view:clear || true
php artisan route:clear || true
# Optional: Run migrations if needed
# php artisan migrate --force
# Generate key if not present (only if APP_KEY is empty or missing)
echo "Checking .env for APP_KEY..."
if ! grep -E "APP_KEY=.{10,}" .env; then
echo "APP_KEY is empty or missing. Generating..."
# If the file doesn't have APP_KEY= at all, add it so artisan can replace it
if ! grep -q "APP_KEY=" .env; then
echo "Appending APP_KEY= placeholder..."
echo "" >> .env
echo "APP_KEY=" >> .env
fi
php artisan key:generate --no-interaction --force || echo "❌ Key generation failed!"
fi
echo "Verifying APP_KEY after generation:"
grep "APP_KEY" .env
# Ensure vendor directory matches composer.lock exactly, syncing missing packages
echo "Verifying and synchronizing Composer dependencies..."
# Check if scribe is in composer.json but not installed
if grep -q "knuckleswtf/scribe" composer.json; then
if ! composer show --locked knuckleswtf/scribe --no-ansi 2>/dev/null | grep -q "name"; then
echo "📦 knuckleswtf/scribe found in composer.json but not in lock file. Installing..."
composer require --dev knuckleswtf/scribe --no-interaction --no-progress --update-with-dependencies
else
echo "✅ knuckleswtf/scribe is already in lock file"
composer install --no-interaction --optimize-autoloader
fi
else
# Normal install
composer install --no-interaction --optimize-autoloader
fi
# If install fails, try update
if [ $? -ne 0 ]; then
echo "⚠️ Composer install failed, attempting update..."
composer update --no-interaction --optimize-autoloader --with-all-dependencies
fi
# Install and publish Scribe assets
echo "Setting up Scribe/OpenAPI documentation..."
# Publish Scribe config (if not already published)
if [ ! -f config/scribe.php ]; then
echo "Publishing Scribe configuration..."
php artisan vendor:publish --tag=scribe-config --force
fi
# Skip Scribe generation if docs already exist to save time on restart
if [ -d resources/views/scribe ] && [ "$FORCE_REGENERATE_DOCS" != "true" ]; then
echo "✅ Scribe docs already exist, skipping generation..."
SKIP_SCRIBE=true
fi
# Generate OpenAPI/Swagger documentation
if [ "$APP_ENV" != "production" ] || [ "$AUTO_GENERATE_API_DOCS" = "true" ]; then
echo "Generating API documentation..."
# Generate Scribe documentation
if [ "$SKIP_SCRIBE" != "true" ]; then
echo "Generating Scribe documentation..."
php artisan scribe:generate --no-interaction || echo "⚠️ Scribe generation failed, check your API routes"
fi
# For OpenAPI/Swagger UI specifically
if [ -f public/docs/openapi.yaml ] || [ -f public/docs/collection.json ]; then
echo "✅ Documentation generated successfully"
echo "📚 Swagger UI available at: /docs (if configured)"
echo "📄 OpenAPI spec available at: /docs/openapi.yaml"
else
echo "⚠️ Documentation files not found after generation"
fi
fi
# Create symbolic link for Scribe assets if needed
if [ -d vendor/knuckleswtf/scribe/dist ]; then
echo "Creating Scribe assets symlink..."
mkdir -p public/vendor/scribe
cp -r vendor/knuckleswtf/scribe/dist/* public/vendor/scribe/ 2>/dev/null || \
ln -sf ../vendor/knuckleswtf/scribe/dist public/vendor/scribe 2>/dev/null || true
fi
# FIX: Ensure storage and bootstrap are writable (these are small)
echo "Setting correct ownership for storage and bootstrap..."
chown -R www-data:www-data storage bootstrap/cache
chmod -R 775 storage bootstrap/cache
# Note: We skip recursive chown on vendor because it's very slow on mounted volumes
# and usually not necessary for runtime.
# Verify vendor integrity at PHP level
echo "Checking vendor visibility and READABILITY for PHP..."
ls -la /var/www/html/vendor/laravel/prompts/src/helpers.php
php -r 'if (file_exists("/var/www/html/vendor/laravel/prompts/src/helpers.php")) { echo "✅ file_exists true\n"; }'
php -r 'require "/var/www/html/vendor/laravel/prompts/src/helpers.php"; echo "✅ PHP successfully REQUIRED helpers.php\n";'
# Run production optimizations for production environment
if [ "$APP_ENV" = "production" ] || [ "$MODE" = "production" ]; then
echo "Running production optimizations..."
php artisan config:cache
php artisan view:cache
php artisan event:cache
else
# Clear all caches to ensure fresh state for non-production
echo "Clearing caches..."
php artisan config:clear || true
php artisan view:clear || true
php artisan route:clear || true
php artisan cache:clear || true
php artisan optimize:clear || true
fi
# Verify Vite manifest exists
if [ -f public/build/manifest.json ]; then
echo "✅ Vite manifest found at public/build/manifest.json"
else
echo "⚠️ Vite manifest not found! Attempting to build assets..."
if command -v npm >/dev/null 2>&1; then
if [ ! -d node_modules ]; then
echo "📦 node_modules missing. Installing..."
npm install --legacy-peer-deps --no-audit || echo "❌ npm install failed!"
fi
npm run build || echo "❌ npm run build failed!"
else
echo "❌ npm not found, cannot build assets automatically."
fi
if [ ! -f public/build/manifest.json ]; then
echo "⚠️ Still no Vite manifest! Assets may not load correctly."
echo "Current contents of public/build:"
ls -la public/build/ 2>/dev/null || echo "public/build directory does not exist"
fi
fi
# Optional: Run migrations if needed (with safety for production)
if [ "$AUTO_MIGRATE" = "true" ] || [ "$APP_ENV" != "production" ]; then
echo "Running migrations..."
php artisan migrate --force
# Optional: Seed if needed, but we have converted.sql
# php artisan db:seed --force
# Check if database has data (e.g., check users table)
# We use PGPASSWORD to avoid interactive prompt
export PGPASSWORD=$DB_PASSWORD
echo "Checking if database needs data import..."
USER_COUNT=$(psql -h $DB_HOST -U $DB_USERNAME -d $DB_DATABASE -t -c "SELECT count(*) FROM users;" 2>/dev/null | tr -d '[:space:]' || echo "0")
if [ "$USER_COUNT" = "0" ] || [ -z "$USER_COUNT" ]; then
if [ -f converted.sql ]; then
echo "📦 Database is empty. Importing data from converted.sql..."
psql -h $DB_HOST -U $DB_USERNAME -d $DB_DATABASE -f converted.sql > /dev/null
echo "✅ Data import complete."
else
echo "⚠️ converted.sql not found, skipping data import."
fi
else
echo "✅ Database already has data ($USER_COUNT users)."
fi
unset PGPASSWORD
fi
echo "========================================"
echo "Setup complete. Starting application..."
echo "========================================"
# Start the application with proper error handling
if [ "$MODE" = "staging" ]; then
echo "Starting PHP built-in server on port 8000..."
# Use exec with error capture
exec php artisan serve --host=0.0.0.0 --port=8000 2>&1 | tee -a /var/log/php-server.log
# Start the application based on environment
if [ "$APP_ENV" = "production" ] || [ "$MODE" = "production" ]; then
echo "Starting PHP built-in server on port 8000 (Production mode)..."
echo "Document root: /var/www/html/public"
echo "Workers: Single-threaded PHP server (use Nginx for production scaling)"
# Use direct php -S for production (optimized)
exec php -d opcache.enable=1 -d opcache.memory_consumption=128 -d opcache.max_accelerated_files=10000 -S 0.0.0.0:8000 -t public
elif [ "$MODE" = "staging" ]; then
echo "Starting PHP built-in server on port 8000 (Staging mode)..."
# Use direct php -S to eliminate any artisan wrapper issues
exec php -d opcache.enable=0 -S 0.0.0.0:8000 -t public
else
echo "Starting PHP-FPM on port 9000..."
echo "Starting PHP-FPM on port 9000 (Development mode)..."
# Ensure PHP-FPM is properly configured
exec php-fpm -F
fi

52
entrypoint2.sh Normal file
View File

@@ -0,0 +1,52 @@
#!/bin/sh
set -e
echo "========================================"
echo "Starting KulakPOS Container Setup..."
echo "========================================"
# Tampilkan info dasar untuk debugging
echo "Current user: $(whoami)"
echo "Current directory: $(pwd)"
# 1. Fallback untuk kelengkapan env (agar container minimal bisa booting)
if [ ! -f .env ]; then
echo "⚠️ .env tidak ditemukan!"
if [ -f .env.example ]; then
echo "Menyalin dari .env.example..."
cp .env.example .env
fi
fi
# 2. Pastikan permission path krusial aman (berguna jika terkena Volume Mount di Docker Compose)
echo "Menyesuaikan izin (permissions) untuk storage & bootstrap/cache..."
mkdir -p storage/framework/cache/data storage/framework/sessions storage/framework/views storage/framework/testing storage/logs bootstrap/cache
chown -R www-data:www-data storage bootstrap/cache
chmod -R 775 storage bootstrap/cache
# 3. Bersihkan cache instalasi/config lama
echo "Membersihkan cache Laravel..."
php artisan config:clear || true
# 4. Verifikasi Vite Build
if [ -f public/build/manifest.json ]; then
echo "✅ Vite manifest ditemukan."
else
echo "⚠️ Peringatan: Vite manifest public/build/manifest.json tidak ditemukan."
fi
# ========================================
# PENTING: Dihilangkan dari script ini:
# - composer install (Sudah dijamin ada pada Dockerfile, bukan di runtime)
# - chmod 777 (Diganti menjadi 775 dan chown www-data)
# - php -S (Kita standarkan aplikasi menggunakan php-fpm murni dari CMD Dockerfile)
# - Paksaan export APP_DEBUG
# ========================================
echo "========================================"
echo "Selesai. Menjalankan proses utama ($@)..."
echo "========================================"
# Serahkan eksekusi ke CMD milik Dockerfile (yaitu php-fpm)
exec "$@"

2052
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,19 +6,19 @@
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.2",
"@vitejs/plugin-react": "^6.0.1",
"@vitejs/plugin-react": "^4.3.0",
"alpinejs": "^3.4.2",
"autoprefixer": "^10.4.2",
"axios": "^1.1.2",
"laravel-vite-plugin": "^2.0.0",
"laravel-vite-plugin": "^1.2.0",
"lodash": "^4.17.19",
"postcss": "^8.4.6",
"tailwindcss": "^3.1.0",
"vite": "^8.0.3"
"vite": "^6.0.0"
},
"dependencies": {
"lucide-react": "^1.7.0",
"react": "^19.2.4",
"react-dom": "^19.2.4"
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
}

12
public/images/gopay.svg Normal file
View File

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="63" height="16" viewBox="0 0 63 16">
<g fill="none" fill-rule="evenodd">
<path fill="#FFF" fill-opacity=".01" d="M0 0h63v16H0z"/>
<g transform="translate(0 1.143)">
<ellipse cx="6.811" cy="6.857" fill="#00AED6" fill-rule="nonzero" rx="6.811" ry="6.857"/>
<path fill="#FFF" d="M10.778 6.644a1.587 1.587 0 0 0-1.652-1.5H4.824a.285.285 0 0 1-.284-.286c0-.158.127-.286.284-.286h4.359a1.362 1.362 0 0 0-.993-1.26 10.97 10.97 0 0 0-3.84 0 1.82 1.82 0 0 0-1.362 1.526 13.711 13.711 0 0 0 0 4.06 1.92 1.92 0 0 0 1.552 1.526 19.13 19.13 0 0 0 4.748 0 1.669 1.669 0 0 0 1.317-1.44c.14-.772.199-1.556.173-2.34zm-1.413.96v.254a.285.285 0 0 1-.284.286.285.285 0 0 1-.284-.286v-.254a.427.427 0 0 1 .284-.746.427.427 0 0 1 .284.746z"/>
</g>
<g fill="#000" fill-rule="nonzero">
<path d="M18.937 11.414a2.921 2.921 0 0 0 2.545 1.252c1.187 0 2.059-.763 2.059-1.8v-.547h-.029c-.65.64-1.537.974-2.444.922a3.955 3.955 0 0 1-3.513-1.94 4.012 4.012 0 0 1-.037-4.033 3.956 3.956 0 0 1 3.478-2.002 3.39 3.39 0 0 1 2.516.892h.029V3.41h2.03v7.428c0 2.159-1.7 3.656-4.089 3.656a4.87 4.87 0 0 1-4.06-1.814l1.515-1.266zm4.519-4.622c0-.863-.973-1.655-2.059-1.655-1.373 0-2.288.835-2.288 2.087-.04.594.18 1.175.605 1.588a1.995 1.995 0 0 0 1.597.557c1.187 0 2.145-.748 2.145-1.684v-.893zM30.916 3.194c2.474 0 4.276 1.77 4.276 4.03 0 2.26-1.802 4.031-4.276 4.031a4.005 4.005 0 0 1-3.692-1.935 4.063 4.063 0 0 1 0-4.191 4.005 4.005 0 0 1 3.692-1.935zm0 1.87a2.152 2.152 0 0 0-2.13 2.17 2.152 2.152 0 0 0 2.15 2.15 2.152 2.152 0 0 0 2.14-2.16 2.075 2.075 0 0 0-.605-1.562 2.045 2.045 0 0 0-1.555-.597zM36.29 3.41h2.03v.676h.03a3.359 3.359 0 0 1 2.444-.892c2.18.04 3.928 1.828 3.932 4.023.004 2.196-1.738 3.99-3.918 4.038-.86.02-1.7-.265-2.373-.806h-.029v3.829H36.29V3.41zm4.176 1.67c-1.116 0-2.06.791-2.06 1.655v.964c0 .922.916 1.684 2.073 1.684a2.145 2.145 0 0 0 2.131-2.158 2.145 2.145 0 0 0-2.144-2.146zM48.803 6.49c1.387-.187 1.802-.388 1.802-.777 0-.504-.53-.806-1.344-.806a1.79 1.79 0 0 0-1.888 1.367l-2.002-.417c.286-1.555 1.874-2.663 3.832-2.663 2.216 0 3.59 1.137 3.59 2.993v4.852H50.89v-.835h-.03a3.117 3.117 0 0 1-2.559 1.051c-1.673 0-2.83-.921-2.83-2.275 0-1.425.943-2.159 3.331-2.49zm1.973.806h-.028c-.187.274-.587.432-1.616.62-1.244.23-1.687.474-1.687.92 0 .461.372.663 1.172.663 1.216 0 2.16-.562 2.16-1.296v-.907zM56.82 10.622L53.317 3.41h2.331l2.302 4.98h.028l2.274-4.98h2.345L57.35 14.278h-2.331z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

BIN
public/images/ovo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
public/images/shopeepay.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

View File

@@ -0,0 +1,393 @@
/* Copied from https://github.com/slatedocs/slate/blob/c4b4c0b8f83e891ca9fab6bbe9a1a88d5fe41292/stylesheets/print.css and unminified */
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
html {
font-family: sans-serif;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%
}
body {
margin: 0
}
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
display: block
}
audio,
canvas,
progress,
video {
display: inline-block;
vertical-align: baseline
}
audio:not([controls]) {
display: none;
height: 0
}
[hidden],
template {
display: none
}
a {
background-color: transparent
}
a:active,
a:hover {
outline: 0
}
abbr[title] {
border-bottom: 1px dotted
}
b,
strong {
font-weight: bold
}
dfn {
font-style: italic
}
h1 {
font-size: 2em;
margin: 0.67em 0
}
mark {
background: #ff0;
color: #000
}
small {
font-size: 80%
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline
}
sup {
top: -0.5em
}
sub {
bottom: -0.25em
}
img {
border: 0
}
svg:not(:root) {
overflow: hidden
}
figure {
margin: 1em 40px
}
hr {
box-sizing: content-box;
height: 0
}
pre {
overflow: auto
}
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em
}
button,
input,
optgroup,
select,
textarea {
color: inherit;
font: inherit;
margin: 0
}
button {
overflow: visible
}
button,
select {
text-transform: none
}
button,
html input[type="button"],
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button;
cursor: pointer
}
button[disabled],
html input[disabled] {
cursor: default
}
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0
}
input {
line-height: normal
}
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box;
padding: 0
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto
}
input[type="search"] {
-webkit-appearance: textfield;
box-sizing: content-box
}
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none
}
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em
}
legend {
border: 0;
padding: 0
}
textarea {
overflow: auto
}
optgroup {
font-weight: bold
}
table {
border-collapse: collapse;
border-spacing: 0
}
td,
th {
padding: 0
}
.content h1,
.content h2,
.content h3,
.content h4,
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 14px
}
.content h1,
.content h2,
.content h3,
.content h4 {
font-weight: bold
}
.content pre,
.content code {
font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace, serif;
font-size: 12px;
line-height: 1.5
}
.content pre,
.content code {
word-break: break-all;
-webkit-hyphens: auto;
-ms-hyphens: auto;
hyphens: auto
}
@font-face {
font-family: 'slate';
src: url(../fonts/slate.eot?-syv14m);
src: url(../fonts/slate.eot?#iefix-syv14m) format("embedded-opentype"), url(../fonts/slate.woff2?-syv14m) format("woff2"), url(../fonts/slate.woff?-syv14m) format("woff"), url(../fonts/slate.ttf?-syv14m) format("truetype"), url(../fonts/slate.svg?-syv14m#slate) format("svg");
font-weight: normal;
font-style: normal
}
.content aside.warning:before,
.content aside.notice:before,
.content aside.success:before {
font-family: 'slate';
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1
}
.content aside.warning:before {
content: "\e600"
}
.content aside.notice:before {
content: "\e602"
}
.content aside.success:before {
content: "\e606"
}
.tocify,
.toc-footer,
.lang-selector,
.search,
#nav-button {
display: none
}
.tocify-wrapper>img {
margin: 0 auto;
display: block
}
.content {
font-size: 12px
}
.content pre,
.content code {
border: 1px solid #999;
border-radius: 5px;
font-size: 0.8em
}
.content pre code {
border: 0
}
.content pre {
padding: 1.3em
}
.content code {
padding: 0.2em
}
.content table {
border: 1px solid #999
}
.content table tr {
border-bottom: 1px solid #999
}
.content table td,
.content table th {
padding: 0.7em
}
.content p {
line-height: 1.5
}
.content a {
text-decoration: none;
color: #000
}
.content h1 {
font-size: 2.5em;
padding-top: 0.5em;
padding-bottom: 0.5em;
margin-top: 1em;
margin-bottom: 21px;
border: 2px solid #ccc;
border-width: 2px 0;
text-align: center
}
.content h2 {
font-size: 1.8em;
margin-top: 2em;
border-top: 2px solid #ccc;
padding-top: 0.8em
}
.content h1+h2,
.content h1+div+h2 {
border-top: none;
padding-top: 0;
margin-top: 0
}
.content h3,
.content h4 {
font-size: 0.8em;
margin-top: 1.5em;
margin-bottom: 0.8em;
text-transform: uppercase
}
.content h5,
.content h6 {
text-transform: uppercase
}
.content aside {
padding: 1em;
border: 1px solid #ccc;
border-radius: 5px;
margin-top: 1.5em;
margin-bottom: 1.5em;
line-height: 1.6
}
.content aside:before {
vertical-align: middle;
padding-right: 0.5em;
font-size: 14px
}

File diff suppressed because it is too large Load Diff

BIN
public/vendor/scribe/images/navbar.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

View File

@@ -0,0 +1,149 @@
document.addEventListener('DOMContentLoaded', function() {
const updateHash = function (id) {
window.location.hash = `#${id}`;
};
const navButton = document.getElementById('nav-button');
const menuWrapper = document.querySelector('.tocify-wrapper');
function toggleSidebar(event) {
event.preventDefault();
if (menuWrapper) {
menuWrapper.classList.toggle('open');
navButton.classList.toggle('open');
}
}
function closeSidebar() {
if (menuWrapper) {
menuWrapper.classList.remove('open');
navButton.classList.remove('open');
}
}
navButton.addEventListener('click', toggleSidebar);
window.hljs.highlightAll();
const wrapper = document.getElementById('toc');
// https://jets.js.org/
window.jets = new window.Jets({
// *OR - Selects elements whose values contains at least one part of search substring
searchSelector: '*OR',
searchTag: '#input-search',
contentTag: '#toc li',
didSearch: function(term) {
wrapper.classList.toggle('jets-searching', String(term).length > 0)
},
// map these accent keys to plain values
diacriticsMap: {
a: 'ÀÁÂÃÄÅàáâãäåĀāąĄ',
c: 'ÇçćĆčČ',
d: 'đĐďĎ',
e: 'ÈÉÊËèéêëěĚĒēęĘ',
i: 'ÌÍÎÏìíîïĪī',
l: 'łŁ',
n: 'ÑñňŇńŃ',
o: 'ÒÓÔÕÕÖØòóôõöøŌō',
r: 'řŘ',
s: 'ŠšśŚ',
t: 'ťŤ',
u: 'ÙÚÛÜùúûüůŮŪū',
y: 'ŸÿýÝ',
z: 'ŽžżŻźŹ'
}
});
function hashChange() {
const currentItems = document.querySelectorAll('.tocify-subheader.visible, .tocify-item.tocify-focus');
Array.from(currentItems).forEach((elem) => {
elem.classList.remove('visible', 'tocify-focus');
});
const currentTag = document.querySelector(`a[href="${window.location.hash}"]`);
if (currentTag) {
const parent = currentTag.closest('.tocify-subheader');
if (parent) {
parent.classList.add('visible');
}
const siblings = currentTag.closest('.tocify-header');
if (siblings) {
Array.from(siblings.querySelectorAll('.tocify-subheader')).forEach((elem) => {
elem.classList.add('visible');
});
}
currentTag.parentElement.classList.add('tocify-focus');
// wait for dom changes to be done
setTimeout(() => {
currentTag.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' });
// only close the sidebar on level-2 events
if (currentTag.parentElement.classList.contains('level-2')) {
closeSidebar();
}
}, 1500);
}
}
let languages = JSON.parse(document.body.getAttribute('data-languages'));
// Support a key => value object where the key is the name, or an array of strings where the value is the name
if (!Array.isArray(languages)) {
languages = Object.values(languages);
}
// if there is no language use the first one
const currentLanguage = window.localStorage.getItem('language') || languages[0];
const languageStyle = document.getElementById('language-style');
const langSelector = document.querySelectorAll('.lang-selector button.lang-button');
function setActiveLanguage(newLanguage) {
window.localStorage.setItem('language', newLanguage);
if (!languageStyle) {
return;
}
const newStyle = languages.map((language) => {
return language === newLanguage
// the current one should be visible
? `body .content .${language}-example pre { display: block; }`
// the inactive one should be hidden
: `body .content .${language}-example pre { display: none; }`;
}).join(`\n`);
Array.from(langSelector).forEach((elem) => {
elem.classList.toggle('active', elem.getAttribute('data-language-name') === newLanguage);
});
const activeHash = window.location.hash.slice(1);
languageStyle.innerHTML = newStyle;
setTimeout(() => {
updateHash(activeHash);
}, 200);
}
setActiveLanguage(currentLanguage);
Array.from(langSelector).forEach((elem) => {
elem.addEventListener('click', () => {
const newLanguage = elem.getAttribute('data-language-name');
setActiveLanguage(newLanguage);
});
});
window.addEventListener('hashchange', hashChange, false);
const divs = document.querySelectorAll('.content h1[id], .content h2[id]');
document.addEventListener('scroll', () => {
divs.forEach(item => {
const rect = item.getBoundingClientRect();
if (rect.top > 0 && rect.top < 150) {
const location = window.location.toString().split('#')[0];
history.replaceState(null, null, location + '#' + item.id);
hashChange();
}
});
});
hashChange();
});

View File

@@ -0,0 +1,289 @@
window.abortControllers = {};
function cacheAuthValue() {
// Whenever the auth header is set for one endpoint, cache it for the others
window.lastAuthValue = '';
let authInputs = document.querySelectorAll(`.auth-value`)
authInputs.forEach(el => {
el.addEventListener('input', (event) => {
window.lastAuthValue = event.target.value;
authInputs.forEach(otherInput => {
if (otherInput === el) return;
// Don't block the main thread
setTimeout(() => {
otherInput.value = window.lastAuthValue;
}, 0);
});
});
});
}
window.addEventListener('DOMContentLoaded', cacheAuthValue);
function getCookie(name) {
if (!document.cookie) {
return null;
}
const cookies = document.cookie.split(';')
.map(c => c.trim())
.filter(c => c.startsWith(name + '='));
if (cookies.length === 0) {
return null;
}
return decodeURIComponent(cookies[0].split('=')[1]);
}
function tryItOut(endpointId) {
document.querySelector(`#btn-tryout-${endpointId}`).hidden = true;
document.querySelector(`#btn-canceltryout-${endpointId}`).hidden = false;
const executeBtn = document.querySelector(`#btn-executetryout-${endpointId}`).hidden = false;
executeBtn.disabled = false;
// Show all input fields
document.querySelectorAll(`input[data-endpoint=${endpointId}],label[data-endpoint=${endpointId}]`)
.forEach(el => el.style.display = 'block');
if (document.querySelector(`#form-${endpointId}`).dataset.authed === "1") {
const authElement = document.querySelector(`#auth-${endpointId}`);
authElement && (authElement.hidden = false);
}
// Expand all nested fields
document.querySelectorAll(`#form-${endpointId} details`)
.forEach(el => el.open = true);
}
function cancelTryOut(endpointId) {
if (window.abortControllers[endpointId]) {
window.abortControllers[endpointId].abort();
delete window.abortControllers[endpointId];
}
document.querySelector(`#btn-tryout-${endpointId}`).hidden = false;
const executeBtn = document.querySelector(`#btn-executetryout-${endpointId}`);
executeBtn.hidden = true;
executeBtn.textContent = executeBtn.dataset.initialText;
document.querySelector(`#btn-canceltryout-${endpointId}`).hidden = true;
// Hide inputs
document.querySelectorAll(`input[data-endpoint=${endpointId}],label[data-endpoint=${endpointId}]`)
.forEach(el => el.style.display = 'none');
document.querySelectorAll(`#form-${endpointId} details`)
.forEach(el => el.open = false);
const authElement = document.querySelector(`#auth-${endpointId}`);
authElement && (authElement.hidden = true);
document.querySelector('#execution-results-' + endpointId).hidden = true;
document.querySelector('#execution-error-' + endpointId).hidden = true;
// Revert to sample code blocks
document.querySelector('#example-requests-' + endpointId).hidden = false;
document.querySelector('#example-responses-' + endpointId).hidden = false;
}
function makeAPICall(method, path, body = {}, query = {}, headers = {}, endpointId = null) {
console.log({endpointId, path, body, query, headers});
if (!(body instanceof FormData) && typeof body !== "string") {
body = JSON.stringify(body)
}
const url = new URL(window.tryItOutBaseUrl + '/' + path.replace(/^\//, ''));
// We need this function because if you try to set an array or object directly to a URLSearchParams object,
// you'll get [object Object] or the array.toString()
function addItemToSearchParamsObject(key, value, searchParams) {
if (Array.isArray(value)) {
value.forEach((v, i) => {
// Append {filters: [first, second]} as filters[0]=first&filters[1]second
addItemToSearchParamsObject(key + '[' + i + ']', v, searchParams);
})
} else if (typeof value === 'object' && value !== null) {
Object.keys(value).forEach((i) => {
// Append {filters: {name: first}} as filters[name]=first
addItemToSearchParamsObject(key + '[' + i + ']', value[i], searchParams);
});
} else {
searchParams.append(key, value);
}
}
Object.keys(query)
.forEach(key => addItemToSearchParamsObject(key, query[key], url.searchParams));
window.abortControllers[endpointId] = new AbortController();
return fetch(url, {
method,
headers,
body: method === 'GET' ? undefined : body,
signal: window.abortControllers[endpointId].signal,
referrer: window.tryItOutBaseUrl,
mode: 'cors',
credentials: 'same-origin',
})
.then(response => Promise.all([response.status, response.statusText, response.text(), response.headers]));
}
function hideCodeSamples(endpointId) {
document.querySelector('#example-requests-' + endpointId).hidden = true;
document.querySelector('#example-responses-' + endpointId).hidden = true;
}
function handleResponse(endpointId, response, status, headers) {
hideCodeSamples(endpointId);
// Hide error views
document.querySelector('#execution-error-' + endpointId).hidden = true;
const responseContentEl = document.querySelector('#execution-response-content-' + endpointId);
// Check if the response contains Laravel's dd() default dump output
const isLaravelDump = response.includes('Sfdump');
// If it's a Laravel dd() dump, use innerHTML to render it safely
if (isLaravelDump) {
responseContentEl.innerHTML = response === '' ? responseContentEl.dataset.emptyResponseText : response;
} else {
// Otherwise, stick to textContent for regular responses
responseContentEl.textContent = response === '' ? responseContentEl.dataset.emptyResponseText : response;
}
// Prettify it if it's JSON
let isJson = false;
try {
const jsonParsed = JSON.parse(response);
if (jsonParsed !== null) {
isJson = true;
response = JSON.stringify(jsonParsed, null, 4);
responseContentEl.textContent = response;
}
} catch (e) {
}
isJson && window.hljs.highlightElement(responseContentEl);
const statusEl = document.querySelector('#execution-response-status-' + endpointId);
statusEl.textContent = ` (${status})`;
document.querySelector('#execution-results-' + endpointId).hidden = false;
statusEl.scrollIntoView({behavior: "smooth", block: "center"});
}
function handleError(endpointId, err) {
hideCodeSamples(endpointId);
// Hide response views
document.querySelector('#execution-results-' + endpointId).hidden = true;
// Show error views
let errorMessage = err.message || err;
const $errorMessageEl = document.querySelector('#execution-error-message-' + endpointId);
$errorMessageEl.textContent = errorMessage + $errorMessageEl.textContent;
const errorEl = document.querySelector('#execution-error-' + endpointId);
errorEl.hidden = false;
errorEl.scrollIntoView({behavior: "smooth", block: "center"});
}
async function executeTryOut(endpointId, form) {
const executeBtn = document.querySelector(`#btn-executetryout-${endpointId}`);
executeBtn.textContent = executeBtn.dataset.loadingText;
executeBtn.disabled = true;
executeBtn.scrollIntoView({behavior: "smooth", block: "center"});
let body;
let setter;
if (form.dataset.hasfiles === "1") {
body = new FormData();
setter = (name, value) => body.append(name, value);
} else if (form.dataset.isarraybody === "1") {
body = [];
setter = (name, value) => _.set(body, name, value);
} else {
body = {};
setter = (name, value) => _.set(body, name, value);
}
const bodyParameters = form.querySelectorAll('input[data-component=body]');
bodyParameters.forEach(el => {
let value = el.value;
if (el.type === 'number' && typeof value === 'string') {
value = parseFloat(value);
}
if (el.type === 'file' && el.files[0]) {
setter(el.name, el.files[0]);
return;
}
if (el.type !== 'radio') {
if (value === "" && el.required === false) {
// Don't include empty optional values in the request
return;
}
setter(el.name, value);
return;
}
if (el.checked) {
value = (value === 'false') ? false : true;
setter(el.name, value);
}
});
const query = {};
const queryParameters = form.querySelectorAll('input[data-component=query]');
queryParameters.forEach(el => {
if (el.type !== 'radio' || (el.type === 'radio' && el.checked)) {
if (el.value === '') {
// Don't include empty values in the request
return;
}
_.set(query, el.name, el.value);
}
});
let path = form.dataset.path;
const urlParameters = form.querySelectorAll('input[data-component=url]');
urlParameters.forEach(el => (path = path.replace(new RegExp(`\\{${el.name}\\??}`), el.value)));
const headers = Object.fromEntries(Array.from(form.querySelectorAll('input[data-component=header]'))
.map(el => [el.name, el.value]));
// When using FormData, the browser sets the correct content-type + boundary
let method = form.dataset.method;
if (body instanceof FormData) {
delete headers['Content-Type'];
// When using FormData with PUT or PATCH, use method spoofing so PHP can access the post body
if (['PUT', 'PATCH'].includes(form.dataset.method)) {
method = 'POST';
setter('_method', form.dataset.method);
}
}
let preflightPromise = Promise.resolve();
if (window.useCsrf && window.csrfUrl) {
preflightPromise = makeAPICall('GET', window.csrfUrl).then(() => {
headers['X-XSRF-TOKEN'] = getCookie('XSRF-TOKEN');
});
}
return preflightPromise.then(() => makeAPICall(method, path, body, query, headers, endpointId))
.then(([responseStatus, statusText, responseContent, responseHeaders]) => {
handleResponse(endpointId, responseContent, responseStatus, responseHeaders)
})
.catch(err => {
if (err.name === "AbortError") {
console.log("Request cancelled");
return;
}
console.log("Error while making request: ", err);
handleError(endpointId, err);
})
.finally(() => {
executeBtn.disabled = false;
executeBtn.textContent = executeBtn.dataset.initialText;
});
}

View File

@@ -6,13 +6,16 @@ import {
MonitorSmartphone, Printer, ShieldCheck, Wrench, Sparkles,
Laptop, Tablet, ArrowRight, Bot, PieChart, TrendingUp, BrainCircuit,
Package, FileText, ShoppingCart, Warehouse, Users, GitMerge, ArrowLeft,
Utensils, Zap, Clock, UserCheck, CalendarDays, History
Utensils, Zap, Clock, UserCheck, CalendarDays, History, Lock, Wallet
} from 'lucide-react';
export default function App() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [activeIndustryTab, setActiveIndustryTab] = useState('fnb');
const [currentPage, setCurrentPage] = useState('home'); // 'home' or 'industry-detail'
const [currentPage, setCurrentPage] = useState('home'); // 'home', 'industry-detail', or 'checkout'
const [selectedPlan, setSelectedPlan] = useState(null);
const [paymentMethod, setPaymentMethod] = useState('credit_card');
const [selectedEWallet, setSelectedEWallet] = useState('gopay');
// Scroll to top when changing page
useEffect(() => {
@@ -80,34 +83,40 @@ export default function App() {
const activeIndustry = industries.find(i => i.id === activeIndustryTab);
// Generate Date for Next Month
const getNextMonthDate = () => {
const nextMonth = new Date();
nextMonth.setMonth(nextMonth.getMonth() + 1);
return nextMonth.toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' });
};
const renderNavbar = () => (
<nav className="fixed top-0 w-full bg-white/95 backdrop-blur-md z-50 border-b border-slate-200 transition-all shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-20">
<div className="flex-shrink-0 flex items-center gap-2 cursor-pointer" onClick={() => setCurrentPage('home')}>
<div className="w-10 h-10 bg-teal-600 rounded-lg flex items-center justify-center shadow-md">
<Store className="text-white w-6 h-6" />
</div>
<span className="font-extrabold text-2xl text-slate-900 tracking-tight hidden sm:block">
Kulak<span className="text-teal-600">pos</span>
</span>
<div className="flex-shrink-0 flex items-center gap-2 cursor-pointer" onClick={() => { setCurrentPage('home'); window.location.href = '/'; }}>
<img src="/images/logokulakpos.png" alt="Kulakpos" className="h-10 w-auto" />
</div>
<div className="hidden md:flex space-x-8 items-center">
<a href="#fitur" onClick={(e) => { e.preventDefault(); setCurrentPage('home'); setTimeout(() => document.getElementById('fitur')?.scrollIntoView({behavior: 'smooth'}), 100); }} className="text-slate-600 hover:text-teal-600 font-semibold transition-colors text-sm">Fitur POS</a>
<a href="#ai" onClick={(e) => { e.preventDefault(); setCurrentPage('home'); setTimeout(() => document.getElementById('ai')?.scrollIntoView({behavior: 'smooth'}), 100); }} className="text-teal-600 hover:text-teal-700 font-bold transition-colors text-sm flex items-center gap-1.5 bg-teal-50 px-3 py-1.5 rounded-full border border-teal-100">
<a href="/#fitur" onClick={(e) => { e.preventDefault(); setCurrentPage('home'); setTimeout(() => document.getElementById('fitur')?.scrollIntoView({behavior: 'smooth'}), 100); }} className="text-slate-600 hover:text-teal-600 font-semibold transition-colors text-sm">Fitur POS</a>
<a href="/#ai" onClick={(e) => { e.preventDefault(); setCurrentPage('home'); setTimeout(() => document.getElementById('ai')?.scrollIntoView({behavior: 'smooth'}), 100); }} className="text-teal-600 hover:text-teal-700 font-bold transition-colors text-sm flex items-center gap-1.5 bg-teal-50 px-3 py-1.5 rounded-full border border-teal-100">
<Bot className="w-4 h-4" /> AI Assistance
</a>
<a href="#perangkat" onClick={(e) => { e.preventDefault(); setCurrentPage('home'); setTimeout(() => document.getElementById('perangkat')?.scrollIntoView({behavior: 'smooth'}), 100); }} className="text-slate-600 hover:text-teal-600 font-semibold transition-colors text-sm">Perangkat</a>
<a href="#solusi" onClick={(e) => { e.preventDefault(); setCurrentPage('home'); setTimeout(() => document.getElementById('solusi')?.scrollIntoView({behavior: 'smooth'}), 100); }} className="text-slate-600 hover:text-teal-600 font-semibold transition-colors text-sm">Solusi Industri</a>
<a href="#harga" onClick={(e) => { e.preventDefault(); setCurrentPage('home'); setTimeout(() => document.getElementById('harga')?.scrollIntoView({behavior: 'smooth'}), 100); }} className="text-slate-600 hover:text-teal-600 font-semibold transition-colors text-sm">Harga</a>
<a href="/#perangkat" onClick={(e) => { e.preventDefault(); setCurrentPage('home'); setTimeout(() => document.getElementById('perangkat')?.scrollIntoView({behavior: 'smooth'}), 100); }} className="text-slate-600 hover:text-teal-600 font-semibold transition-colors text-sm">Perangkat</a>
<a href="/#solusi" onClick={(e) => { e.preventDefault(); setCurrentPage('home'); setTimeout(() => document.getElementById('solusi')?.scrollIntoView({behavior: 'smooth'}), 100); }} className="text-slate-600 hover:text-teal-600 font-semibold transition-colors text-sm">Solusi Industri</a>
<a href="/#harga" onClick={(e) => { e.preventDefault(); setCurrentPage('home'); setTimeout(() => document.getElementById('harga')?.scrollIntoView({behavior: 'smooth'}), 100); }} className="text-slate-600 hover:text-teal-600 font-semibold transition-colors text-sm">Harga</a>
</div>
<div className="hidden md:flex items-center space-x-4">
<a href="/login" className="text-slate-600 font-semibold hover:text-red-600 transition-colors text-sm">Masuk</a>
<a href="/plans" className="bg-teal-600 hover:bg-teal-700 text-white px-5 py-2.5 rounded-lg font-semibold transition-all shadow-md text-sm flex items-center gap-2">
<button
type="button"
onClick={() => { setSelectedPlan({ name: 'Pro Business', price: 100000 }); setCurrentPage('checkout'); window.scrollTo(0, 0); }}
className="bg-teal-600 hover:bg-teal-700 text-white px-5 py-2.5 rounded-lg font-semibold transition-all shadow-md text-sm flex items-center gap-2"
>
Coba Gratis <ArrowRight className="w-4 h-4" />
</a>
</button>
</div>
<div className="md:hidden flex items-center">
@@ -117,6 +126,7 @@ export default function App() {
</div>
</div>
</div>
</nav>
);
@@ -125,13 +135,8 @@ export default function App() {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-12 lg:gap-8 mb-12">
<div className="lg:col-span-2">
<div className="flex items-center gap-2 mb-6 cursor-pointer" onClick={() => setCurrentPage('home')}>
<div className="w-10 h-10 bg-teal-600 rounded-lg flex items-center justify-center shadow-md">
<Store className="text-white w-6 h-6" />
</div>
<span className="font-extrabold text-2xl text-slate-900 tracking-tight">
Kulak<span className="text-teal-600">pos</span>
</span>
<div className="flex items-center gap-2 mb-6 cursor-pointer" onClick={() => { setCurrentPage('home'); window.location.href = '/'; }}>
<img src="/images/logokulakpos.png" alt="Kulakpos" className="h-10 w-auto" />
</div>
<p className="text-sm mb-6 max-w-sm leading-relaxed text-slate-500 font-medium">
Penyedia layanan perangkat lunak kasir (SaaS POS) terkemuka di Indonesia. Misi kami adalah memberdayakan UMKM untuk naik kelas melalui teknologi kasir yang cerdas dan mudah.
@@ -176,7 +181,7 @@ export default function App() {
const renderHomePage = () => (
<>
{/* HERO SECTION */}
<section className="pt-32 pb-20 lg:pt-40 lg:pb-24 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto relative overflow-hidden">
<section className="pt-28 pb-16 lg:pt-40 lg:pb-24 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto relative overflow-hidden">
<div className="absolute top-0 right-0 -translate-y-12 translate-x-1/3 w-96 h-96 bg-teal-50 rounded-full blur-3xl opacity-50 z-0"></div>
<div className="absolute top-40 left-0 -translate-x-1/2 w-64 h-64 bg-red-50 rounded-full blur-3xl opacity-50 z-0"></div>
@@ -185,7 +190,7 @@ export default function App() {
<Sparkles className="w-4 h-4" />
Mitra Digital #1 untuk UMKM Naik Kelas
</div>
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-extrabold text-slate-900 leading-[1.15] mb-6 tracking-tight">
<h1 className="text-3xl sm:text-5xl lg:text-6xl font-extrabold text-slate-900 leading-tight md:leading-[1.15] mb-6 tracking-tight">
Sistem Kasir Pintar dengan <br className="hidden md:block" />
Teknologi <span className="text-teal-600">AI Assistance</span>
</h1>
@@ -193,12 +198,16 @@ export default function App() {
Tinggalkan cara manual. Kendalikan penjualan, pantau stok bahan baku, hingga analisis laba rugi secara real-time dari satu dashboard profesional Kulakpos.id.
</p>
<div className="flex flex-col sm:flex-row items-center gap-4 justify-center">
<a href="/plans" className="w-full sm:w-auto bg-teal-600 hover:bg-teal-700 text-white px-8 py-4 rounded-lg text-base font-bold transition-all shadow-lg shadow-teal-600/20 flex items-center justify-center gap-2 group decoration-transparent">
<button
type="button"
onClick={() => { setSelectedPlan({ name: 'Pro Business', price: 100000 }); setCurrentPage('checkout'); window.scrollTo(0, 0); }}
className="w-full sm:w-auto bg-teal-600 hover:bg-teal-700 text-white px-8 py-4 rounded-lg text-base font-bold transition-all shadow-lg shadow-teal-600/20 flex items-center justify-center gap-2 group decoration-transparent"
>
Coba Gratis Sekarang <ChevronRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
</a>
<button className="w-full sm:w-auto bg-white border-2 border-slate-200 hover:border-red-600 hover:text-red-600 text-slate-700 px-8 py-4 rounded-lg text-base font-bold transition-all">
Jadwalkan Demo
</button>
<a href="/contact-us" className="w-full sm:w-auto bg-white border-2 border-slate-200 hover:border-red-600 hover:text-red-600 text-slate-700 px-8 py-4 rounded-lg text-base font-bold transition-all text-center decoration-transparent">
Jadwalkan Demo
</a>
</div>
</div>
@@ -449,43 +458,98 @@ export default function App() {
<h2 className="text-3xl font-extrabold text-slate-900 mb-4 tracking-tight">Investasi Transparan UMKM</h2>
<p className="text-lg text-slate-600">Skema harga yang ramah di kantong tanpa biaya tersembunyi. Skalakan sesuai pertumbuhan bisnis Anda.</p>
</div>
<div className="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto">
{/* Basic */}
<div className="bg-white rounded-3xl p-8 border border-slate-200 flex flex-col shadow-sm">
<h3 className="text-xl font-extrabold text-slate-900 mb-2">Basic</h3>
<div className="text-4xl font-black text-slate-900 mb-6 pb-6 border-b">Rp 50.k <span className="text-sm font-medium">/bln</span></div>
<ul className="space-y-4 mb-8 flex-1">
{["POS Full Features", "Inventory Management", "Finance & Accounting", "Purchase Management", "Warehouse Management", "3rd Party Management"].map(f => (
<li key={f} className="flex gap-2 text-sm font-medium"><CheckCircle className="w-5 h-5 text-teal-500" />{f}</li>
))}
<li className="flex gap-2 text-sm font-medium italic opacity-50"><X className="w-5 h-5" /> Multi-branch Allowed</li>
</ul>
<button className="w-full py-3 rounded-xl border-2 font-bold hover:border-teal-600 transition-colors">Pilih Basic</button>
</div>
{/* Pro */}
<div className="bg-teal-900 rounded-3xl p-8 border border-teal-800 flex flex-col transform md:-translate-y-4 shadow-2xl text-white">
<div className="bg-red-600 text-white px-4 py-1.5 rounded-full text-[10px] font-black uppercase w-max mb-4">Paling Laris</div>
<h3 className="text-xl font-extrabold mb-2">Pro Business</h3>
<div className="text-4xl font-black mb-6 pb-6 border-b text-teal-400">Rp 100.k <span className="text-sm font-medium text-white">/bln</span></div>
<ul className="space-y-4 mb-8 flex-1">
<li className="flex gap-2 text-sm font-bold text-teal-300"><Bot className="w-5 h-5" /> Akses AI Assistance</li>
{["POS Full Features", "Inventory Management", "Finance & Accounting", "Purchase Management", "Warehouse Management", "3rd Party Management", "Multi-branch Allowed"].map(f => (
<li key={f} className="flex gap-2 text-sm font-medium"><CheckCircle className="w-5 h-5 text-teal-400" />{f}</li>
))}
</ul>
<button className="w-full py-3 rounded-xl bg-teal-500 text-teal-950 font-black hover:bg-teal-400 transition-all">Pilih Pro</button>
</div>
{/* Enterprise */}
<div className="bg-white rounded-3xl p-8 border border-slate-200 flex flex-col shadow-sm">
<h3 className="text-xl font-extrabold text-slate-900 mb-2">Enterprise</h3>
<div className="text-2xl font-black text-slate-900 mb-6 pb-6 border-b">Hubungi Kami</div>
<ul className="space-y-4 mb-8 flex-1">
{["Custom AI Model", "Unlimited Outlets", "SLA Support 24/7", "Dedicated Manager"].map(f => (
<li key={f} className="flex gap-2 text-sm font-medium"><CheckCircle className="w-5 h-5 text-teal-500" />{f}</li>
))}
</ul>
<button className="w-full py-3 rounded-xl border-2 font-bold hover:bg-slate-50 transition-colors">Konsultasi Sales</button>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-6xl mx-auto">
{(window.plans && window.plans.length > 0) ? (
window.plans.map((plan, idx) => {
const isFeatured = window.plans.length === 3 ? idx === 1 : idx === window.plans.length - 1;
const features = typeof plan.features === 'string' ? JSON.parse(plan.features) : (plan.features || {});
return (
<div key={plan.id} className={`${isFeatured ? 'bg-teal-900 text-white transform md:-translate-y-4 shadow-2xl' : 'bg-white text-slate-900 border-slate-200 shadow-sm'} rounded-3xl p-8 border flex flex-col relative`}>
{isFeatured && <div className="bg-red-600 text-white px-4 py-1.5 rounded-full text-[10px] font-black uppercase w-max mb-4">Paling Laris</div>}
<h3 className={`text-xl font-extrabold mb-2 ${isFeatured ? 'text-white' : 'text-slate-900'}`}>{plan.subscriptionName}</h3>
<div className={`text-4xl font-black mb-6 pb-6 border-b ${isFeatured ? 'text-teal-400 border-teal-800' : 'text-slate-900 border-slate-100'}`}>
Rp {Number(plan.offerPrice || plan.subscriptionPrice).toLocaleString('id-ID')}
<span className={`text-sm font-medium ${isFeatured ? 'text-white' : 'text-slate-500'}`}> /{plan.duration === 'Monthly' ? 'bln' : (plan.duration === 'Yearly' ? 'thn' : plan.duration)}</span>
</div>
<ul className="space-y-4 mb-8 flex-1">
{Object.values(features).map((feat, fidx) => (
<li key={fidx} className="flex gap-2 text-sm font-medium">
{feat[1] === 'True' || feat[1] === true || feat[1] === '1' ? (
<CheckCircle className={`w-5 h-5 ${isFeatured ? 'text-teal-400' : 'text-teal-500'}`} />
) : (
<X className="w-5 h-5 opacity-50" />
)}
{feat[0]}
</li>
))}
</ul>
<button
type="button"
onClick={() => {
setSelectedPlan({ name: plan.subscriptionName, price: Number(plan.offerPrice || plan.subscriptionPrice) });
setCurrentPage('checkout');
window.scrollTo(0, 0);
}}
className={`w-full py-3 rounded-xl font-bold transition-all ${isFeatured ? 'bg-teal-500 text-teal-950 hover:bg-teal-400' : 'border-2 hover:border-teal-600 text-slate-700 hover:text-teal-600'}`}
>
Pilih {plan.subscriptionName}
</button>
</div>
);
})
) : (
/* Fallback if no plans in DB */
<>
{/* Basic */}
<div className="bg-white rounded-3xl p-8 border border-slate-200 flex flex-col shadow-sm">
<h3 className="text-xl font-extrabold text-slate-900 mb-2">Basic</h3>
<div className="text-4xl font-black text-slate-900 mb-6 pb-6 border-b">Rp 50.k <span className="text-sm font-medium">/bln</span></div>
<ul className="space-y-4 mb-8 flex-1">
{["POS Full Features", "Inventory Management", "Finance & Accounting", "Purchase Management", "Warehouse Management", "3rd Party Management"].map(f => (
<li key={f} className="flex gap-2 text-sm font-medium"><CheckCircle className="w-5 h-5 text-teal-500" />{f}</li>
))}
</ul>
<button
type="button"
onClick={() => { setSelectedPlan({ name: 'Basic', price: 50000 }); setCurrentPage('checkout'); window.scrollTo(0, 0); }}
className="w-full py-3 rounded-xl border-2 font-bold hover:border-teal-600 text-slate-700 hover:text-teal-600 transition-colors"
>
Pilih Basic
</button>
</div>
{/* Pro */}
<div className="bg-teal-900 rounded-3xl p-8 border border-teal-800 flex flex-col transform md:-translate-y-4 shadow-2xl text-white">
<div className="bg-red-600 text-white px-4 py-1.5 rounded-full text-[10px] font-black uppercase w-max mb-4">Paling Laris</div>
<h3 className="text-xl font-extrabold mb-2">Pro Business</h3>
<div className="text-4xl font-black mb-6 pb-6 border-b text-teal-400">Rp 100.k <span className="text-sm font-medium text-white">/bln</span></div>
<ul className="space-y-4 mb-8 flex-1">
<li className="flex gap-2 text-sm font-bold text-teal-300"><Bot className="w-5 h-5" /> Akses AI Assistance</li>
{["POS Full Features", "Inventory Management", "Finance & Accounting", "Purchase Management", "Warehouse Management", "3rd Party Management", "Multi-branch Allowed"].map(f => (
<li key={f} className="flex gap-2 text-sm font-medium"><CheckCircle className="w-5 h-5 text-teal-400" />{f}</li>
))}
</ul>
<button
type="button"
onClick={() => { setSelectedPlan({ name: 'Pro Business', price: 100000 }); setCurrentPage('checkout'); window.scrollTo(0, 0); }}
className="w-full py-3 rounded-xl bg-teal-500 text-teal-950 font-black hover:bg-teal-400 transition-all"
>
Pilih Pro
</button>
</div>
{/* Enterprise */}
<div className="bg-white rounded-3xl p-8 border border-slate-200 flex flex-col shadow-sm">
<h3 className="text-xl font-extrabold text-slate-900 mb-2">Enterprise</h3>
<div className="text-2xl font-black text-slate-900 mb-6 pb-6 border-b">Hubungi Kami</div>
<ul className="space-y-4 mb-8 flex-1">
{["Custom AI Model", "Unlimited Outlets", "SLA Support 24/7", "Dedicated Manager"].map(f => (
<li key={f} className="flex gap-2 text-sm font-medium"><CheckCircle className="w-5 h-5 text-teal-500" />{f}</li>
))}
</ul>
<button className="w-full py-3 rounded-xl border-2 font-bold hover:bg-slate-50 transition-colors">Konsultasi Sales</button>
</div>
</>
)}
</div>
</div>
</section>
@@ -580,19 +644,222 @@ export default function App() {
</div>
);
const renderCheckoutPage = () => {
// Default fallback if directly opened without selecting
const plan = selectedPlan || { name: 'Pro Business', price: 100000 };
return (
<div className="min-h-screen bg-slate-50 pt-28 pb-20">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<button
onClick={() => setCurrentPage('home')}
className="flex items-center gap-2 text-slate-500 hover:text-teal-600 font-bold mb-8 transition-colors"
>
<ArrowLeft className="w-5 h-5" /> Kembali
</button>
<div className="grid lg:grid-cols-3 gap-8">
{/* Left Column: Form & Payment Methods */}
<div className="lg:col-span-2 space-y-8">
<div className="bg-white p-8 rounded-3xl border border-slate-200 shadow-sm">
<h2 className="text-2xl font-extrabold text-slate-900 mb-6">Detail Pendaftaran</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">Nama Lengkap</label>
<input type="text" className="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-teal-500 focus:ring-2 focus:ring-teal-200 outline-none transition-all" placeholder="John Doe" />
</div>
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">Email Perusahaan</label>
<input type="email" className="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-teal-500 focus:ring-2 focus:ring-teal-200 outline-none transition-all" placeholder="john@bisnis.com" />
</div>
<div className="md:col-span-2">
<label className="block text-sm font-bold text-slate-700 mb-2">Nama Toko / Bisnis</label>
<input type="text" className="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-teal-500 focus:ring-2 focus:ring-teal-200 outline-none transition-all" placeholder="Kedai Kopi Nusantara" />
</div>
</div>
</div>
<div className="bg-white p-8 rounded-3xl border border-slate-200 shadow-sm">
<div className="flex flex-col sm:flex-row justify-between sm:items-center gap-4 mb-6">
<div>
<h2 className="text-2xl font-extrabold text-slate-900">Metode Pembayaran</h2>
<p className="text-sm text-slate-500 font-medium mt-1">Untuk penagihan bulan ke-2</p>
</div>
<div className="flex items-center gap-2 bg-slate-50 px-3 py-1.5 rounded-lg border border-slate-100 w-max">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Powered by</span>
<span className="font-black text-[#013550] text-lg tracking-tighter">xendit</span>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mb-8">
<div
onClick={() => setPaymentMethod('credit_card')}
className={`p-4 rounded-xl border-2 cursor-pointer flex flex-col items-center gap-3 transition-all ${paymentMethod === 'credit_card' ? 'border-teal-500 bg-teal-50 text-teal-700' : 'border-slate-200 bg-white hover:border-slate-300'}`}
>
<CreditCard className={`w-8 h-8 ${paymentMethod === 'credit_card' ? 'text-teal-600' : 'text-slate-400'}`} />
<span className="font-bold text-sm">Kartu Kredit</span>
</div>
<div
onClick={() => setPaymentMethod('ewallet')}
className={`p-4 rounded-xl border-2 cursor-pointer flex flex-col items-center gap-3 transition-all ${paymentMethod === 'ewallet' ? 'border-teal-500 bg-teal-50 text-teal-700' : 'border-slate-200 bg-white hover:border-slate-300'}`}
>
<Wallet className={`w-8 h-8 ${paymentMethod === 'ewallet' ? 'text-teal-600' : 'text-slate-400'}`} />
<span className="font-bold text-sm">E-Wallet</span>
</div>
</div>
{/* Conditional Form Inputs */}
{paymentMethod === 'credit_card' ? (
<div className="space-y-6 animate-fade-in-up">
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">Nomor Kartu</label>
<div className="relative">
<input type="text" className="w-full px-4 py-3 pl-12 rounded-xl border border-slate-200 focus:border-teal-500 focus:ring-2 focus:ring-teal-200 outline-none transition-all" placeholder="0000 0000 0000 0000" />
<CreditCard className="w-5 h-5 text-slate-400 absolute left-4 top-1/2 -translate-y-1/2" />
</div>
</div>
<div className="grid grid-cols-2 gap-6">
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">Masa Berlaku (MM/YY)</label>
<input type="text" className="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-teal-500 focus:ring-2 focus:ring-teal-200 outline-none transition-all" placeholder="12/26" />
</div>
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">CVC</label>
<input type="text" className="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-teal-500 focus:ring-2 focus:ring-teal-200 outline-none transition-all" placeholder="123" />
</div>
</div>
</div>
) : (
<div className="space-y-6 animate-fade-in-up p-6 bg-slate-50 rounded-xl border border-slate-200">
<label className="block text-sm font-bold text-slate-700 mb-2 text-center">Pilih E-Wallet</label>
<div className="grid grid-cols-3 gap-4 mb-2">
<div
onClick={() => setSelectedEWallet('gopay')}
className={`p-3 rounded-xl border-2 cursor-pointer flex items-center justify-center bg-white transition-all ${selectedEWallet === 'gopay' ? 'border-teal-500 ring-2 ring-teal-200' : 'border-slate-200 hover:border-teal-300'}`}
>
<img src="/images/gopay.svg" alt="GoPay" className="h-6 object-contain" />
</div>
<div
onClick={() => setSelectedEWallet('ovo')}
className={`p-3 rounded-xl border-2 cursor-pointer flex items-center justify-center bg-white transition-all ${selectedEWallet === 'ovo' ? 'border-teal-500 ring-2 ring-teal-200' : 'border-slate-200 hover:border-teal-300'}`}
>
<img src="/images/ovo.png" alt="OVO" className="h-6 object-contain" />
</div>
<div
onClick={() => setSelectedEWallet('shopeepay')}
className={`p-3 rounded-xl border-2 cursor-pointer flex items-center justify-center bg-white transition-all ${selectedEWallet === 'shopeepay' ? 'border-teal-500 ring-2 ring-teal-200' : 'border-slate-200 hover:border-teal-300'}`}
>
<img src="/images/shopeepay.png" alt="ShopeePay" className="h-6 object-contain" />
</div>
</div>
<p className="text-sm text-slate-600 font-medium text-center mt-4">
Anda akan diarahkan ke aplikasi <strong className="text-slate-900 capitalize">{selectedEWallet === 'gopay' ? 'Gojek' : selectedEWallet === 'ovo' ? 'OVO' : 'Shopee'}</strong> untuk menghubungkan akun {selectedEWallet === 'gopay' ? 'GoPay' : selectedEWallet === 'ovo' ? 'OVO' : 'ShopeePay'} Anda sebagai metode pembayaran langganan.
</p>
</div>
)}
<div className="mt-8 flex items-start gap-3 bg-teal-50 p-4 rounded-xl border border-teal-100">
<Lock className="w-5 h-5 text-teal-600 flex-shrink-0 mt-0.5" />
<p className="text-xs text-teal-800 leading-relaxed font-medium">
Kami menggunakan sistem enkripsi tingkat bank yang diproses secara aman oleh <strong>Xendit</strong>. Detail pembayaran Anda tersimpan aman. Anda tidak akan dikenakan biaya apapun hari ini.
</p>
</div>
</div>
</div>
{/* Right Column: Order Summary */}
<div className="lg:col-span-1">
<div className="bg-white p-6 rounded-3xl border border-slate-200 shadow-xl sticky top-28">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-red-50 text-red-600 font-bold text-xs mb-6 border border-red-100">
<Sparkles className="w-4 h-4" /> 1 Bulan Gratis
</div>
<h3 className="text-xl font-extrabold text-slate-900 mb-6 pb-6 border-b border-slate-100">Ringkasan Pesanan</h3>
<div className="space-y-4 mb-6">
<div className="flex justify-between items-center">
<span className="font-bold text-slate-700">Paket {plan.name}</span>
<span className="font-medium text-slate-600">Rp {(plan.price).toLocaleString('id-ID')}</span>
</div>
<div className="flex justify-between items-center text-red-600">
<span className="font-bold">Promo 1 Bulan Gratis</span>
<span className="font-bold">- Rp {(plan.price).toLocaleString('id-ID')}</span>
</div>
</div>
<div className="pt-6 border-t border-slate-100 mb-6">
<div className="flex justify-between items-end mb-2">
<span className="text-sm font-bold text-slate-500 uppercase tracking-widest">Total Hari Ini</span>
<span className="text-3xl font-black text-teal-600">Rp 0</span>
</div>
</div>
<div className="bg-slate-50 p-4 rounded-xl border border-slate-200 mb-8">
<p className="text-xs text-slate-600 leading-relaxed font-medium">
Penagihan normal sebesar <strong className="text-slate-900">Rp {(plan.price).toLocaleString('id-ID')} / bulan</strong> akan dimulai pada tanggal <strong className="text-slate-900">{getNextMonthDate()}</strong>. Anda dapat membatalkan langganan kapan saja sebelum tanggal tersebut.
</p>
</div>
<button className="w-full bg-teal-600 text-white py-4 rounded-xl font-black text-lg shadow-lg hover:bg-teal-700 transition-all flex justify-center items-center gap-2">
Mulai Uji Coba Gratis
</button>
</div>
</div>
</div>
</div>
</div>
);
};
return (
<div className="min-h-screen font-sans text-slate-800 bg-white">
{renderNavbar()}
<main>
{currentPage === 'home' ? renderHomePage() : renderIndustryDetail()}
</main>
<main className="flex-1">
{currentPage === 'home' && renderHomePage()}
{currentPage === 'industry-detail' && renderIndustryDetail()}
{currentPage === 'checkout' && renderCheckoutPage()}
{/* MOBILE MENU DRAWER OUTSIDE NAVBAR */}
{isMenuOpen && (
<div
className="fixed inset-0 z-[10000] bg-white flex flex-col md:hidden"
style={{ backgroundColor: 'white', opacity: 1 }}
>
<div className="flex justify-between items-center h-20 px-4 border-b border-slate-100 flex-shrink-0">
<div className="flex items-center gap-2">
<img src="/images/logokulakpos.png" alt="Kulakpos" className="h-8 w-auto" />
</div>
<button onClick={() => setIsMenuOpen(false)} className="text-slate-600 p-2 border border-slate-200 rounded-lg">
<X className="h-7 w-7" />
</button>
</div>
<div className="flex-1 overflow-y-auto py-4 px-6 bg-white">
<div className="flex flex-col gap-4 font-bold text-base text-slate-800">
<a href="/#fitur" onClick={() => { setIsMenuOpen(false); setCurrentPage('home'); setTimeout(() => document.getElementById('fitur')?.scrollIntoView({behavior: 'smooth'}), 100); }} className="pb-3 border-b border-slate-100 flex items-center justify-between">Fitur POS <ChevronRight className="w-4 h-4 text-slate-300" /></a>
<a href="/#ai" onClick={() => { setIsMenuOpen(false); setCurrentPage('home'); setTimeout(() => document.getElementById('ai')?.scrollIntoView({behavior: 'smooth'}), 100); }} className="pb-3 border-b border-slate-100 flex items-center justify-between text-teal-600 font-extrabold">AI Assistance <Bot className="w-4 h-4" /></a>
<a href="/#perangkat" onClick={() => { setIsMenuOpen(false); setCurrentPage('home'); setTimeout(() => document.getElementById('perangkat')?.scrollIntoView({behavior: 'smooth'}), 100); }} className="pb-3 border-b border-slate-100 flex items-center justify-between">Perangkat <ChevronRight className="w-4 h-4 text-slate-300" /></a>
<a href="/#solusi" onClick={() => { setIsMenuOpen(false); setCurrentPage('home'); setTimeout(() => document.getElementById('solusi')?.scrollIntoView({behavior: 'smooth'}), 100); }} className="pb-3 border-b border-slate-100 flex items-center justify-between">Solusi Industri <ChevronRight className="w-4 h-4 text-slate-300" /></a>
<a href="/#harga" onClick={() => { setIsMenuOpen(false); setCurrentPage('home'); setTimeout(() => document.getElementById('harga')?.scrollIntoView({behavior: 'smooth'}), 100); }} className="pb-3 border-b border-slate-100 flex items-center justify-between">Harga <ChevronRight className="w-4 h-4 text-slate-300" /></a>
<div className="grid grid-cols-2 gap-3 mt-2">
<a href="/login" className="text-center py-3 rounded-lg border border-slate-200 text-slate-700 text-sm">Masuk</a>
<a href="/plans" className="text-center py-3 rounded-lg bg-teal-600 text-white shadow-md text-sm">Coba Gratis</a>
</div>
</div>
</div>
<div className="p-6 border-t border-slate-100 bg-slate-50 text-center text-xs text-slate-500 font-medium flex-shrink-0">
© {new Date().getFullYear()} PT Sada Teknologi Indonesia.
</div>
</div>
)}
</main>
{renderFooter()}
{/* WHATSAPP WIDGET */}
<a
href="https://wa.me/6281234567890?text=Halo%20Kulakpos.id,%20saya%20tertarik%20dengan%20solusi%20POS%20dan%20AI%20Assistance%20Anda."
href="https://wa.me/6287783275818?text=Halo%20Kulakpos.id,%20saya%20tertarik%20dengan%20solusi%20POS%20dan%20AI%20Assistance%20Anda."
target="_blank"
rel="noopener noreferrer"
className="fixed bottom-6 right-6 z-[60] group flex items-center gap-3"

View File

@@ -62,7 +62,7 @@ class="form-control" placeholder="{{ __('Enter Link') }}">
<label>{{ __('WhatsApp Number (For Floating Button)') }}</label>
<input type="text" name="whatsapp_number"
value="{{ $general->value['whatsapp_number'] ?? '' }}" class="form-control"
placeholder="{{ __('e.g., 6289620324323') }}">
placeholder="{{ __('e.g., 6287783275818') }}">
</div>
<div class="col-lg-6">

View File

@@ -2,8 +2,14 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>KulakPOS</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Kulakpos.id - Solusi Kasir Pintar (SaaS POS) Terlengkap dengan Teknologi AI Assistance untuk UMKM Naik Kelas.">
<title>KulakPOS | Solusi Kasir Pintar Berbasis AI</title>
@vite('resources/js/app.jsx')
<script>
window.plans = {!! json_encode($plans) !!};
</script>
<meta name="diagnostic" content="v2-dynamic-plans">
</head>
<body>
<div id="app"></div>

View File

@@ -4,7 +4,7 @@
<div class="row mt-5">
<div class="col-md-6 col-lg-5">
<a href="{{ route('home') }}">
<img src="{{ asset($general->value['footer_logo'] ?? '') }}" alt="footer-logo" class="footer-logo" />
<img src="/images/logokulakpos.png" alt="footer-logo" class="footer-logo" style="height: 40px; width: auto;" />
</a>
<p class="mt-3 footer-details">
{{ $page_data['headings']['footer_short_title'] ?? '' }}

View File

@@ -114,12 +114,8 @@
<nav class="navbar navbar-expand-lg h-100" style="min-height:80px;">
<div class="container" style="max-width:80rem;">
<!-- Logo -->
<a class="navbar-brand d-flex align-items-center m-0 p-0" href="{{ route('home') }}">
<div class="rh-logo-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m2 7 4.41-4.41A2 2 0 0 1 7.83 2h8.34a2 2 0 0 1 1.42.59L22 7"/><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><path d="M15 22v-4a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4"/><path d="M2 7h20"/><path d="M22 7v3a2 2 0 0 1-2 2v0a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 16 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 12 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 8 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 4 12v0a2 2 0 0 1-2-2V7"/></svg>
</div>
<!-- Hide text on mobile using d-none d-sm-block -->
<span class="rh-logo-text d-none d-md-block">Kulak<span>pos</span></span>
<a href="{{ route('home') }}">
<img src="/images/logokulakpos.png" alt="footer-logo" class="footer-logo" style="height: 40px; width: auto;" />
</a>
<!-- Mobile Toggle -->

View File

@@ -113,7 +113,7 @@ class="form-select w-auto country-input" required>
</select>
<input type="text" name="phone" id="phone"
class="form-control number-input w-100"
placeholder="{{ __('Enter your phone number') }}" required>
placeholder="{{ __('e.g., 6287783275818') }}" required>
</div>
</td>
</tr>
@@ -165,7 +165,7 @@ class="btn btn-md payment-btn">{{ __('Pay Now') }}</button>
<!-- Floating WhatsApp Button -->
@php
$whatsapp_number = \App\Models\Option::where('key', 'general')->first()->value['whatsapp_number'] ?? '6289620324323';
$whatsapp_number = \App\Models\Option::where('key', 'general')->first()->value['whatsapp_number'] ?? '6287783275818';
@endphp
@if($whatsapp_number)
<a href="https://wa.me/{{ $whatsapp_number }}?text=Halo%20Admin,%20saya%20butuh%20bantuan%20terkait%20pembayaran"

File diff suppressed because it is too large Load Diff

21
server.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
/**
* Laravel - A PHP Framework For Web Artisans
*
* @package Laravel
* @author Taylor Otwell <taylor@laravel.com>
*/
$uri = urldecode(
parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)
);
// This file allows us to emulate Apache's "mod_rewrite" functionality from the
// built-in PHP web server. This provides a convenient way to test a Laravel
// application without having installed a "real" web server software here.
if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) {
return false;
}
require_once __DIR__.'/public/index.php';

12
truncate.txt Normal file
View File

@@ -0,0 +1,12 @@
-- Option 1: Truncate All Tables Dynamically (Recommended)
DO $$
DECLARE
r RECORD;
BEGIN
FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public') LOOP
EXECUTE 'TRUNCATE TABLE ' || quote_ident(r.tablename) || ' RESTART IDENTITY CASCADE;';
END LOOP;
END $$;
-- Option 2: Truncate Tables Explicitly
TRUNCATE TABLE "public"."users", "public"."currencies", "public"."transfers", "public"."options", "public"."parties", "public"."sales", "public"."notifications", "public"."variations", "public"."payment_types", "public"."purchase_details", "public"."warehouses", "public"."due_collects", "public"."transfer_products", "public"."permissions", "public"."user_currencies", "public"."expenses", "public"."messages", "public"."model_has_roles", "public"."purchases", "public"."pos_app_interfaces", "public"."gateways", "public"."migrations", "public"."plan_subscribes", "public"."testimonials", "public"."categories", "public"."blogs", "public"."vats", "public"."roles", "public"."businesses", "public"."languages", "public"."income_categories", "public"."personal_access_tokens", "public"."product_settings", "public"."features", "public"."stocks", "public"."transactions", "public"."sale_details", "public"."role_has_permissions", "public"."branches", "public"."business_categories", "public"."units", "public"."expense_categories", "public"."plans", "public"."combo_products", "public"."incomes", "public"."brands", "public"."banners", "public"."products" RESTART IDENTITY CASCADE;

21
vendor/ageekdev/laravel-barcode/LICENSE.md vendored Executable file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) AgeekDev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

74
vendor/ageekdev/laravel-barcode/composer.json vendored Executable file
View File

@@ -0,0 +1,74 @@
{
"name": "ageekdev/laravel-barcode",
"description": "laravel barcode generator",
"keywords": [
"ageekdev",
"laravel",
"barcode"
],
"homepage": "https://github.com/ageekdev/laravel-barcode",
"license": "MIT",
"authors": [
{
"name": "Tint Naing Win",
"email": "amigo.k8@gmail.com",
"role": "Developer"
}
],
"require": {
"php": "^8.1",
"ext-bcmath": "*",
"illuminate/contracts": "^9.0|^10.0|^11.0|^12.0",
"spatie/color": "^1.5"
},
"require-dev": {
"laravel/pint": "^1.5",
"larastan/larastan": "^2.0|^3.0",
"orchestra/testbench": "^7.31|^8.11|^9.0|^10.0",
"pestphp/pest": "^1.21|^2.0|^3.0",
"pestphp/pest-plugin-laravel": "^1.4|^2.0|^3.0",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-deprecation-rules": "^1.0|^2.0",
"phpstan/phpstan-phpunit": "^1.0|^2.0",
"roave/security-advisories": "dev-latest"
},
"autoload": {
"psr-4": {
"AgeekDev\\Barcode\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"AgeekDev\\Barcode\\Tests\\": "tests"
}
},
"suggest": {
"ext-gd": "For JPG and PNG generators, GD or Imagick is required",
"ext-imagick": "For JPG and PNG generators, GD or Imagick is required"
},
"scripts": {
"analyse": "vendor/bin/phpstan analyse",
"test": "vendor/bin/pest",
"test-coverage": "vendor/bin/pest --coverage",
"format": "vendor/bin/pint"
},
"config": {
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"phpstan/extension-installer": true
}
},
"extra": {
"laravel": {
"providers": [
"AgeekDev\\Barcode\\BarcodeServiceProvider"
],
"aliases": {
"Barcode": "AgeekDev\\Barcode\\Facades\\Barcode"
}
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@@ -0,0 +1,47 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Barcode Image Type
|--------------------------------------------------------------------------
|
| This option controls the default barcode generator that will be used
| to generate barcode. Alternative barcode generators may be setup and used as
| needed; however, this generator will be used by default.
|
| Supported: "dynamic_html", "html", "jpg", "png", "svg"
|
*/
'image_type' => env('BARCODE_DRIVER', 'png'),
/*
|----------------------------------------------------------------------------------------------
| Accepted Barcode Types
|----------------------------------------------------------------------------------------------
| Supported: TYPE_CODE_32, TYPE_CODE_39, TYPE_CODE_39_CHECKSUM, TYPE_CODE_39E,
| TYPE_CODE_39E_CHECKSUM, TYPE_CODE_93, TYPE_STANDARD_2_5, TYPE_STANDARD_2_5_CHECKSUM,
| TYPE_INTERLEAVED_2_5, TYPE_INTERLEAVED_2_5_CHECKSUM, TYPE_CODE_128, TYPE_CODE_128_A,
| TYPE_CODE_128_B, TYPE_CODE_128_C, TYPE_EAN_2, TYPE_EAN_5, TYPE_EAN_8, TYPE_EAN_13,
| TYPE_UPC_A, TYPE_UPC_E, TYPE_MSI, TYPE_MSI_CHECKSUM, TYPE_POSTNET, TYPE_PLANET, TYPE_RMS4CC,
| TYPE_KIX, TYPE_IMB, TYPE_CODABAR, TYPE_CODE_11, TYPE_PHARMA_CODE, TYPE_PHARMA_CODE_TWO_TRACKS
|
*/
'type' => \AgeekDev\Barcode\Enums\BarcodeType::CODE_128,
/*
* Foreground color of the barcode
*/
'foreground_color' => '#000000',
/*
* width factor of barcode
*/
'width_factor' => 2,
/*
* Height of image barcode
*/
'height' => 30,
];

View File

@@ -0,0 +1,68 @@
<?php
namespace AgeekDev\Barcode;
use AgeekDev\Barcode\Contracts\ImageType;
use AgeekDev\Barcode\Enums\BarcodeType;
use AgeekDev\Barcode\Enums\Type;
use AgeekDev\Barcode\Types\TypeInterface;
abstract class AbstractGenerator implements ImageType
{
/**
* The foreground color of the barcode.
*/
protected string|array $foregroundColor = '#000000';
protected int $height;
protected int $widthFactor;
protected BarcodeType|Type $type;
public function __construct()
{
$this->foregroundColor = config('barcode.foreground_color');
$this->widthFactor = config('barcode.width_factor');
$this->height = config('barcode.height');
$this->type = config('barcode.type');
}
protected function getBarcodeData(string $code, BarcodeType|Type $type): Barcode
{
return $this->createDataBuilderForType($type)->getBarcodeData($code);
}
protected function createDataBuilderForType(BarcodeType|Type $type): TypeInterface
{
return $type->class();
}
public function type(BarcodeType|Type $type): static
{
$this->type = $type;
return $this;
}
public function foregroundColor(string $foregroundColor): static
{
$this->foregroundColor = $foregroundColor;
return $this;
}
public function height(int $height): static
{
$this->height = $height;
return $this;
}
public function widthFactor(int $widthFactor): static
{
$this->widthFactor = $widthFactor;
return $this;
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace AgeekDev\Barcode;
class Barcode
{
protected string $barcode;
protected int $width = 0;
protected int $height = 0;
protected array $bars = [];
public function __construct(string $barcode)
{
$this->barcode = $barcode;
}
/**
* Add bar.
*/
public function addBar(BarcodeBar $bar): void
{
$this->bars[] = $bar;
$this->width += $bar->getWidth();
$this->height = max($this->height, $bar->getHeight());
}
/**
* Get the barcode.
*/
public function getBarcode(): string
{
return $this->barcode;
}
/**
* Get the width.
*/
public function getWidth(): int
{
return $this->width;
}
/**
* Get the height.
*/
public function getHeight(): int
{
return $this->height;
}
/**
* Get the bars.
*/
public function getBars(): array
{
return $this->bars;
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace AgeekDev\Barcode;
class BarcodeBar
{
protected int $width;
protected int $height;
protected int $positionVertical;
protected int $type;
public const TYPE_BAR = 1;
public const TYPE_SPACING = 0;
public function __construct(int $width, int $height, bool $drawBar = true, int $positionVertical = 0)
{
$this->width = $width;
$this->height = $height;
$this->positionVertical = $positionVertical;
$this->type = $drawBar ? self::TYPE_BAR : self::TYPE_SPACING;
}
/**
* Get the width.
*/
public function getWidth(): int
{
return $this->width;
}
/**
* Get the height.
*/
public function getHeight(): int
{
return $this->height;
}
/**
* Get position vertical.
*/
public function getPositionVertical(): int
{
return $this->positionVertical;
}
/**
* Bar or not.
*/
public function isBar(): bool
{
return $this->type === self::TYPE_BAR;
}
}

View File

@@ -0,0 +1,277 @@
<?php
namespace AgeekDev\Barcode;
use AgeekDev\Barcode\Contracts\Factory;
use AgeekDev\Barcode\Contracts\ImageType;
use AgeekDev\Barcode\Drivers\DynamicHTML;
use AgeekDev\Barcode\Drivers\HTML;
use AgeekDev\Barcode\Drivers\JPG;
use AgeekDev\Barcode\Drivers\PNG;
use AgeekDev\Barcode\Drivers\SVG;
use AgeekDev\Barcode\Enums\BarcodeType;
use AgeekDev\Barcode\Enums\Type;
use AgeekDev\Barcode\Exceptions\BarcodeException;
use Closure;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Str;
use InvalidArgumentException;
class BarcodeManager implements Factory
{
/**
* The container instance.
*/
protected Container $container;
/**
* The configuration repository instance.
*
* @var \Illuminate\Contracts\Config\Repository
*/
protected mixed $config;
/**
* The registered custom driver creators.
*/
protected array $customCreators = [];
/**
* The array of created "drivers".
*/
protected array $imageTypes = [];
/**
* Create a new manager instance.
*
* @throws BindingResolutionException
*/
public function __construct(Container $container)
{
$this->container = $container;
$this->config = $container->make('config');
}
/**
* Get a image type instance.
*/
public function imageType(?string $name = null): ImageType
{
$name = $name ?: $this->getDefaultDriver();
// If the given driver has not been created before, we will create the instances
// here and cache it, so we can return it next time very quickly. If there is
// already a driver created by this name, we'll just return that instance.
if (! isset($this->imageTypes[$name])) {
$this->imageTypes[$name] = $this->createDriver($name);
}
return $this->imageTypes[$name];
}
/**
* Create a new driver instance.
*
* @throws InvalidArgumentException
*/
protected function createDriver(string $driver): ImageType
{
// First, we will determine if a custom driver creator exists for the given driver and
// if it does not we will check for a creator method for the driver. Custom creator
// callbacks allow developers to build their own "drivers" easily using Closures.
if (isset($this->customCreators[$driver])) {
return $this->callCustomCreator($driver);
}
$method = 'create'.Str::studly($driver).'Driver';
if (method_exists($this, $method)) {
return $this->$method();
}
throw new InvalidArgumentException("ImageType [$driver] not supported.");
}
/**
* Call a custom driver creator.
*/
protected function callCustomCreator(string $driver): ImageType
{
return $this->customCreators[$driver]($this->container);
}
/**
* Create an HTML instance.
*/
public function createHtmlDriver(): ImageType
{
return new HTML;
}
/**
* Create an DynamicHTML instance.
*/
public function createDynamicHtmlDriver(): ImageType
{
return new DynamicHTML;
}
/**
* Create an JPG instance.
*/
public function createJpgDriver(): ImageType
{
return new JPG;
}
/**
* Create an PNG instance.
*/
public function createPngDriver(): ImageType
{
return new PNG;
}
/**
* Create an SVG instance.
*/
public function createSvgDriver(): ImageType
{
return new SVG;
}
/**
* Generate Barcode.
*/
public function generate(string $text): string
{
return $this->imageType()->generate($text);
}
/**
* Set the barcode type.
*/
public function type(BarcodeType|Type $type): ImageType
{
return $this->imageType()->type($type);
}
/**
* Set the barcode foreground color.
*/
public function foregroundColor(string $foregroundColor): ImageType
{
return $this->imageType()->foregroundColor($foregroundColor);
}
/**
* Set the barcode height.
*/
public function height(int $height): ImageType
{
return $this->imageType()->height($height);
}
/**
* Set the barcode width factor.
*/
public function widthFactor(int $widthFactor): ImageType
{
return $this->imageType()->widthFactor($widthFactor);
}
/**
* Force the use of Imagick image extension
*
* @throws BarcodeException
*/
public function useImagick(): ImageType
{
if (method_exists($this->imageType(), 'useImagick')) {
if (! extension_loaded('imagick')) {
throw new BarcodeException('The imagick is not installed!');
}
return $this->imageType()->useImagick();
}
throw new BarcodeException('This image type does not support useImagick function.');
}
/**
* Force the use of the GD image library
*
* @throws BarcodeException
*/
public function useGd(): ImageType
{
if (method_exists($this->imageType(), 'useGd')) {
if (! function_exists('imagecreate')) {
throw new BarcodeException('The GD is not installed!');
}
return $this->imageType()->useGd();
}
throw new BarcodeException('This image type does not support useGd function.');
}
/**
* Get the default driver name.
*/
public function getDefaultDriver(): string
{
return $this->config->get('barcode.image_type', 'png');
}
/**
* Register a custom driver creator Closure.
*/
public function extend(string $imageType, Closure $callback): self
{
$this->customCreators[$imageType] = $callback;
return $this;
}
/**
* Get the container instance used by the manager.
*/
public function getContainer(): Container
{
return $this->container;
}
/**
* Set the container instance used by the manager.
*/
public function setContainer(Container $container): self
{
$this->container = $container;
return $this;
}
/**
* Unset the given disk instances.
*/
public function forgetImageType(array|string $imageType): self
{
foreach ((array) $imageType as $imageTypeName) {
unset($this->imageTypes[$imageTypeName]);
}
return $this;
}
/**
* Dynamically call the default driver instance.
*
* @return mixed
*/
public function __call(string $method, array $parameters)
{
return $this->imageType()->$method(...$parameters);
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace AgeekDev\Barcode;
use Illuminate\Support\ServiceProvider;
class BarcodeServiceProvider extends ServiceProvider
{
/**
* Register the service provider.
*/
public function register(): void
{
$this->mergeConfigFrom(__DIR__.'/../config/barcode.php', 'barcode');
$this->app->bind('laravel-barcode', function ($app) {
return new BarcodeManager($app);
});
}
public function boot(): void
{
$this->publishes([
__DIR__.'/../config/barcode.php' => config_path('barcode.php'),
], 'laravel-barcode-config');
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace AgeekDev\Barcode\Contracts;
interface Factory
{
/**
* Get image type implementation.
*/
public function imageType(?string $name = null): ImageType;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace AgeekDev\Barcode\Contracts;
use AgeekDev\Barcode\Enums\BarcodeType;
use AgeekDev\Barcode\Enums\Type;
interface ImageType
{
public function generate(string $text): string;
public function type(BarcodeType|Type $type): static;
public function foregroundColor(string $foregroundColor): static;
public function height(int $height): static;
public function widthFactor(int $widthFactor): static;
}

View File

@@ -0,0 +1,44 @@
<?php
namespace AgeekDev\Barcode\Drivers;
use AgeekDev\Barcode\AbstractGenerator;
use AgeekDev\Barcode\BarcodeBar;
class DynamicHTML extends AbstractGenerator
{
private const WIDTH_PRECISION = 6;
/**
* Return an HTML representation of barcode.
* This 'dynamic' version uses percentage based widths and heights, resulting in a vector-y qualitative result.
*
* @param string $text code to print
*/
public function generate(string $text): string
{
$barcodeData = $this->getBarcodeData($text, $this->type);
$html = '<div style="font-size:0;position:relative;width:100%;height:100%">'.PHP_EOL;
$positionHorizontal = 0;
/** @var BarcodeBar $bar */
foreach ($barcodeData->getBars() as $bar) {
$barWidth = $bar->getWidth() / $barcodeData->getWidth() * 100;
$barHeight = round(($bar->getHeight() / $barcodeData->getHeight() * 100), 3);
if ($barWidth > 0 && $bar->isBar()) {
$positionVertical = round(($bar->getPositionVertical() / $barcodeData->getHeight() * 100), 3);
// draw a vertical bar
$html .= '<div style="background-color:'.$this->foregroundColor.';width:'.round($barWidth, self::WIDTH_PRECISION).'%;height:'.$barHeight.'%;position:absolute;left:'.round($positionHorizontal, self::WIDTH_PRECISION).'%;top:'.$positionVertical.(($positionVertical > 0) ? '%' : '').'">&nbsp;</div>'.PHP_EOL;
}
$positionHorizontal += $barWidth;
}
$html .= '</div>'.PHP_EOL;
return $html;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace AgeekDev\Barcode\Drivers;
use AgeekDev\Barcode\AbstractGenerator;
use AgeekDev\Barcode\BarcodeBar;
class HTML extends AbstractGenerator
{
/**
* Return an HTML representation of barcode.
* This original version uses pixel based widths and heights. Use Dynamic HTML version for better quality representation.
*/
public function generate(string $text): string
{
$barcodeData = $this->getBarcodeData($text, $this->type);
$html = '<div style="font-size:0;position:relative;width:'.($barcodeData->getWidth() * $this->widthFactor).'px;height:'.($this->height).'px;">'.PHP_EOL;
$positionHorizontal = 0;
/** @var BarcodeBar $bar */
foreach ($barcodeData->getBars() as $bar) {
$barWidth = round(($bar->getWidth() * $this->widthFactor), 3);
$barHeight = round(($bar->getHeight() * $this->height / $barcodeData->getHeight()), 3);
if ($barWidth > 0 && $bar->isBar()) {
$positionVertical = round(($bar->getPositionVertical() * $this->height / $barcodeData->getHeight()), 3);
$html .= '<div style="background-color:'.$this->foregroundColor.';width:'.$barWidth.'px;height:'.$barHeight.'px;position:absolute;left:'.$positionHorizontal.'px;top:'.$positionVertical.(($positionVertical > 0) ? 'px' : '').'">&nbsp;</div>'.PHP_EOL;
}
$positionHorizontal += $barWidth;
}
$html .= '</div>'.PHP_EOL;
return $html;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace AgeekDev\Barcode\Drivers;
use Imagick;
class JPG extends PNG
{
protected function createImagickImageObject(int $width, int $height): Imagick
{
$image = new Imagick;
$image->newImage($width, $height, 'white', 'JPG');
return $image;
}
protected function generateGdImage($image): void
{
imagejpeg($image);
imagedestroy($image);
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace AgeekDev\Barcode\Drivers;
use AgeekDev\Barcode\AbstractGenerator;
use AgeekDev\Barcode\BarcodeBar;
use AgeekDev\Barcode\Exceptions\BarcodeException;
use Imagick;
use imagickdraw;
use imagickpixel;
use Spatie\Color\Hex;
class PNG extends AbstractGenerator
{
protected bool $useImagick = true;
/**
* @throws BarcodeException
*/
public function __construct()
{
parent::__construct();
// Auto switch between GD and Imagick based on what is installed
if (extension_loaded('imagick')) {
$this->useImagick = true;
} elseif (function_exists('imagecreate')) {
$this->useImagick = false;
} else {
throw new BarcodeException('Neither gd-lib or imagick are installed!');
}
}
/**
* Force the use of Imagick image extension.
*/
public function useImagick(): self
{
$this->useImagick = true;
return $this;
}
/**
* Force the use of the GD image library.
*/
public function useGd(): self
{
$this->useImagick = false;
return $this;
}
/**
* @param string $text code to print
*
* @throws \ImagickDrawException
* @throws \ImagickException
* @throws \ImagickPixelException
*/
public function generate(string $text): string
{
$barcodeData = $this->getBarcodeData($text, $this->type);
$width = round($barcodeData->getWidth() * $this->widthFactor);
$foregroundColor = $this->getForegroundColor();
if ($this->useImagick) {
$imagickBarsShape = new imagickdraw;
$imagickBarsShape->setFillColor(new imagickpixel('rgb('.implode(',', $foregroundColor).')'));
} else {
$image = $this->createGdImageObject($width, $this->height);
$gdForegroundColor = imagecolorallocate($image, $foregroundColor[0], $foregroundColor[1], $foregroundColor[2]);
}
// print bars
$positionHorizontal = 0;
/** @var BarcodeBar $bar */
foreach ($barcodeData->getBars() as $bar) {
$barWidth = round(($bar->getWidth() * $this->widthFactor), 3);
if ($barWidth > 0 && $bar->isBar()) {
$y = round(($bar->getPositionVertical() * $this->height / $barcodeData->getHeight()), 3);
$barHeight = round(($bar->getHeight() * $this->height / $barcodeData->getHeight()), 3);
// draw a vertical bar
if ($this->useImagick) {
$imagickBarsShape->rectangle($positionHorizontal, $y, ($positionHorizontal + $barWidth - 1), ($y + $barHeight));
} else {
imagefilledrectangle($image, $positionHorizontal, $y, ($positionHorizontal + $barWidth - 1), ($y + $barHeight), $gdForegroundColor);
}
}
$positionHorizontal += $barWidth;
}
if ($this->useImagick) {
$image = $this->createImagickImageObject($width, $this->height);
$image->drawImage($imagickBarsShape);
return $image->getImageBlob();
}
ob_start();
$this->generateGdImage($image);
return ob_get_clean();
}
public function getForegroundColor(): string|array
{
$color = Hex::fromString($this->foregroundColor)->toRgba();
return [$color->red(), $color->blue(), $color->green()];
}
protected function createGdImageObject(int $width, int $height)
{
$image = imagecreate($width, $height);
$colorBackground = imagecolorallocate($image, 255, 255, 255);
imagecolortransparent($image, $colorBackground);
return $image;
}
/**
* @throws \ImagickException
*/
protected function createImagickImageObject(int $width, int $height): Imagick
{
$image = new Imagick;
$image->newImage($width, $height, 'none', 'PNG');
return $image;
}
protected function generateGdImage($image): void
{
imagepng($image);
imagedestroy($image);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace AgeekDev\Barcode\Drivers;
use AgeekDev\Barcode\AbstractGenerator;
use AgeekDev\Barcode\BarcodeBar;
class SVG extends AbstractGenerator
{
/**
* Return SVG string representation of barcode.
*
* @param string $text code to print
*/
public function generate(string $text): string
{
$barcodeData = $this->getBarcodeData($text, $this->type);
// replace table for special characters
$repstr = ["\0" => '', '&' => '&amp;', '<' => '&lt;', '>' => '&gt;'];
$width = round(($barcodeData->getWidth() * $this->widthFactor), 3);
$svg = '<?xml version="1.0" standalone="no" ?>'.PHP_EOL;
$svg .= '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">'.PHP_EOL;
$svg .= '<svg width="'.$width.'" height="'.$this->height.'" viewBox="0 0 '.$width.' '.$this->height.'" version="1.1" xmlns="http://www.w3.org/2000/svg">'.PHP_EOL;
$svg .= "\t".'<desc>'.strtr($barcodeData->getBarcode(), $repstr).'</desc>'.PHP_EOL;
$svg .= "\t".'<g id="bars" fill="'.$this->foregroundColor.'" stroke="none">'.PHP_EOL;
// print bars
$positionHorizontal = 0;
/** @var BarcodeBar $bar */
foreach ($barcodeData->getBars() as $bar) {
$barWidth = round(($bar->getWidth() * $this->widthFactor), 3);
$barHeight = round(($bar->getHeight() * $this->height / $barcodeData->getHeight()), 3);
if ($bar->isBar() && $barWidth > 0) {
$positionVertical = round(($bar->getPositionVertical() * $this->height / $barcodeData->getHeight()), 3);
// draw a vertical bar
$svg .= "\t\t".'<rect x="'.$positionHorizontal.'" y="'.$positionVertical.'" width="'.$barWidth.'" height="'.$barHeight.'" />'.PHP_EOL;
}
$positionHorizontal += $barWidth;
}
$svg .= "\t</g>".PHP_EOL;
$svg .= '</svg>'.PHP_EOL;
return $svg;
}
}

View File

@@ -0,0 +1,168 @@
<?php
namespace AgeekDev\Barcode\Enums;
use AgeekDev\Barcode\Types\TypeCodabar;
use AgeekDev\Barcode\Types\TypeCode11;
use AgeekDev\Barcode\Types\TypeCode128;
use AgeekDev\Barcode\Types\TypeCode128A;
use AgeekDev\Barcode\Types\TypeCode128B;
use AgeekDev\Barcode\Types\TypeCode128C;
use AgeekDev\Barcode\Types\TypeCode32;
use AgeekDev\Barcode\Types\TypeCode39;
use AgeekDev\Barcode\Types\TypeCode39Checksum;
use AgeekDev\Barcode\Types\TypeCode39Extended;
use AgeekDev\Barcode\Types\TypeCode39ExtendedChecksum;
use AgeekDev\Barcode\Types\TypeCode93;
use AgeekDev\Barcode\Types\TypeEan13;
use AgeekDev\Barcode\Types\TypeEan8;
use AgeekDev\Barcode\Types\TypeIntelligentMailBarcode;
use AgeekDev\Barcode\Types\TypeInterface;
use AgeekDev\Barcode\Types\TypeInterleaved25;
use AgeekDev\Barcode\Types\TypeInterleaved25Checksum;
use AgeekDev\Barcode\Types\TypeKix;
use AgeekDev\Barcode\Types\TypeMsi;
use AgeekDev\Barcode\Types\TypeMsiChecksum;
use AgeekDev\Barcode\Types\TypePharmacode;
use AgeekDev\Barcode\Types\TypePharmacodeTwoCode;
use AgeekDev\Barcode\Types\TypePlanet;
use AgeekDev\Barcode\Types\TypePostnet;
use AgeekDev\Barcode\Types\TypeRms4cc;
use AgeekDev\Barcode\Types\TypeStandard2of5;
use AgeekDev\Barcode\Types\TypeStandard2of5Checksum;
use AgeekDev\Barcode\Types\TypeUpcA;
use AgeekDev\Barcode\Types\TypeUpcE;
use AgeekDev\Barcode\Types\TypeUpcExtension2;
use AgeekDev\Barcode\Types\TypeUpcExtension5;
enum BarcodeType: string
{
case CODE_32 = 'C32';
case CODE_39 = 'C39';
case CODE_39_CHECKSUM = 'C39+';
case CODE_39E = 'C39E'; // CODE 39 EXTENDED
case CODE_39E_CHECKSUM = 'C39E+'; // CODE 39 EXTENDED + CHECKSUM
case CODE_93 = 'C93';
case STANDARD_2_5 = 'S25';
case STANDARD_2_5_CHECKSUM = 'S25+';
case INTERLEAVED_2_5 = 'I25';
case INTERLEAVED_2_5_CHECKSUM = 'I25+';
case CODE_128 = 'C128';
case CODE_128_A = 'C128A';
case CODE_128_B = 'C128B';
case CODE_128_C = 'C128C';
case EAN_2 = 'EAN2'; // 2-Digits UPC-Based Extention
case EAN_5 = 'EAN5'; // 5-Digits UPC-Based Extention
case EAN_8 = 'EAN8';
case EAN_13 = 'EAN13';
case UPC_A = 'UPCA';
case UPC_E = 'UPCE';
case MSI = 'MSI'; // MSI (Variation of Plessey code)
case MSI_CHECKSUM = 'MSI+'; // MSI + CHECKSUM (modulo 11)
case POSTNET = 'POSTNET';
case PLANET = 'PLANET';
case RMS4CC = 'RMS4CC'; // RMS4CC (Royal Mail 4-state Customer Code) - CBC (Customer Bar Code)
case KIX = 'KIX'; // KIX (Klant index - Customer index)
case IMB = 'IMB'; // IMB - Intelligent Mail Barcode - Onecode - USPS-B-3200
case CODABAR = 'CODABAR';
case CODE_11 = 'CODE11';
case PHARMA_CODE = 'PHARMA';
case PHARMA_CODE_TWO_TRACKS = 'PHARMA2T';
public function class(): TypeInterface
{
return match ($this) {
self::CODE_32 => new TypeCode32,
self::CODE_39 => new TypeCode39,
self::CODE_39_CHECKSUM => new TypeCode39Checksum,
self::CODE_39E => new TypeCode39Extended,
self::CODE_39E_CHECKSUM => new TypeCode39ExtendedChecksum,
self::CODE_93 => new TypeCode93,
self::STANDARD_2_5 => new TypeStandard2of5,
self::STANDARD_2_5_CHECKSUM => new TypeStandard2of5Checksum,
self::INTERLEAVED_2_5 => new TypeInterleaved25,
self::INTERLEAVED_2_5_CHECKSUM => new TypeInterleaved25Checksum,
self::CODE_128 => new TypeCode128,
self::CODE_128_A => new TypeCode128A,
self::CODE_128_B => new TypeCode128B,
self::CODE_128_C => new TypeCode128C,
self::EAN_2 => new TypeUpcExtension2,
self::EAN_5 => new TypeUpcExtension5,
self::EAN_8 => new TypeEan8,
self::EAN_13 => new TypeEan13,
self::UPC_A => new TypeUpcA,
self::UPC_E => new TypeUpcE,
self::MSI => new TypeMsi,
self::MSI_CHECKSUM => new TypeMsiChecksum,
self::POSTNET => new TypePostnet,
self::PLANET => new TypePlanet,
self::RMS4CC => new TypeRms4cc,
self::KIX => new TypeKix,
self::IMB => new TypeIntelligentMailBarcode,
self::CODABAR => new TypeCodabar,
self::CODE_11 => new TypeCode11,
self::PHARMA_CODE => new TypePharmacode,
self::PHARMA_CODE_TWO_TRACKS => new TypePharmacodeTwoCode,
};
}
}

View File

@@ -0,0 +1,171 @@
<?php
namespace AgeekDev\Barcode\Enums;
use AgeekDev\Barcode\Types\TypeCodabar;
use AgeekDev\Barcode\Types\TypeCode11;
use AgeekDev\Barcode\Types\TypeCode128;
use AgeekDev\Barcode\Types\TypeCode128A;
use AgeekDev\Barcode\Types\TypeCode128B;
use AgeekDev\Barcode\Types\TypeCode128C;
use AgeekDev\Barcode\Types\TypeCode32;
use AgeekDev\Barcode\Types\TypeCode39;
use AgeekDev\Barcode\Types\TypeCode39Checksum;
use AgeekDev\Barcode\Types\TypeCode39Extended;
use AgeekDev\Barcode\Types\TypeCode39ExtendedChecksum;
use AgeekDev\Barcode\Types\TypeCode93;
use AgeekDev\Barcode\Types\TypeEan13;
use AgeekDev\Barcode\Types\TypeEan8;
use AgeekDev\Barcode\Types\TypeIntelligentMailBarcode;
use AgeekDev\Barcode\Types\TypeInterface;
use AgeekDev\Barcode\Types\TypeInterleaved25;
use AgeekDev\Barcode\Types\TypeInterleaved25Checksum;
use AgeekDev\Barcode\Types\TypeKix;
use AgeekDev\Barcode\Types\TypeMsi;
use AgeekDev\Barcode\Types\TypeMsiChecksum;
use AgeekDev\Barcode\Types\TypePharmacode;
use AgeekDev\Barcode\Types\TypePharmacodeTwoCode;
use AgeekDev\Barcode\Types\TypePlanet;
use AgeekDev\Barcode\Types\TypePostnet;
use AgeekDev\Barcode\Types\TypeRms4cc;
use AgeekDev\Barcode\Types\TypeStandard2of5;
use AgeekDev\Barcode\Types\TypeStandard2of5Checksum;
use AgeekDev\Barcode\Types\TypeUpcA;
use AgeekDev\Barcode\Types\TypeUpcE;
use AgeekDev\Barcode\Types\TypeUpcExtension2;
use AgeekDev\Barcode\Types\TypeUpcExtension5;
/**
* @deprecated see BarcodeType
*/
enum Type: string
{
case TYPE_CODE_32 = 'C32';
case TYPE_CODE_39 = 'C39';
case TYPE_CODE_39_CHECKSUM = 'C39+';
case TYPE_CODE_39E = 'C39E'; // CODE 39 EXTENDED
case TYPE_CODE_39E_CHECKSUM = 'C39E+'; // CODE 39 EXTENDED + CHECKSUM
case TYPE_CODE_93 = 'C93';
case TYPE_STANDARD_2_5 = 'S25';
case TYPE_STANDARD_2_5_CHECKSUM = 'S25+';
case TYPE_INTERLEAVED_2_5 = 'I25';
case TYPE_INTERLEAVED_2_5_CHECKSUM = 'I25+';
case TYPE_CODE_128 = 'C128';
case TYPE_CODE_128_A = 'C128A';
case TYPE_CODE_128_B = 'C128B';
case TYPE_CODE_128_C = 'C128C';
case TYPE_EAN_2 = 'EAN2'; // 2-Digits UPC-Based Extention
case TYPE_EAN_5 = 'EAN5'; // 5-Digits UPC-Based Extention
case TYPE_EAN_8 = 'EAN8';
case TYPE_EAN_13 = 'EAN13';
case TYPE_UPC_A = 'UPCA';
case TYPE_UPC_E = 'UPCE';
case TYPE_MSI = 'MSI'; // MSI (Variation of Plessey code)
case TYPE_MSI_CHECKSUM = 'MSI+'; // MSI + CHECKSUM (modulo 11)
case TYPE_POSTNET = 'POSTNET';
case TYPE_PLANET = 'PLANET';
case TYPE_RMS4CC = 'RMS4CC'; // RMS4CC (Royal Mail 4-state Customer Code) - CBC (Customer Bar Code)
case TYPE_KIX = 'KIX'; // KIX (Klant index - Customer index)
case TYPE_IMB = 'IMB'; // IMB - Intelligent Mail Barcode - Onecode - USPS-B-3200
case TYPE_CODABAR = 'CODABAR';
case TYPE_CODE_11 = 'CODE11';
case TYPE_PHARMA_CODE = 'PHARMA';
case TYPE_PHARMA_CODE_TWO_TRACKS = 'PHARMA2T';
public function class(): TypeInterface
{
return match ($this) {
self::TYPE_CODE_32 => new TypeCode32,
self::TYPE_CODE_39 => new TypeCode39,
self::TYPE_CODE_39_CHECKSUM => new TypeCode39Checksum,
self::TYPE_CODE_39E => new TypeCode39Extended,
self::TYPE_CODE_39E_CHECKSUM => new TypeCode39ExtendedChecksum,
self::TYPE_CODE_93 => new TypeCode93,
self::TYPE_STANDARD_2_5 => new TypeStandard2of5,
self::TYPE_STANDARD_2_5_CHECKSUM => new TypeStandard2of5Checksum,
self::TYPE_INTERLEAVED_2_5 => new TypeInterleaved25,
self::TYPE_INTERLEAVED_2_5_CHECKSUM => new TypeInterleaved25Checksum,
self::TYPE_CODE_128 => new TypeCode128,
self::TYPE_CODE_128_A => new TypeCode128A,
self::TYPE_CODE_128_B => new TypeCode128B,
self::TYPE_CODE_128_C => new TypeCode128C,
self::TYPE_EAN_2 => new TypeUpcExtension2,
self::TYPE_EAN_5 => new TypeUpcExtension5,
self::TYPE_EAN_8 => new TypeEan8,
self::TYPE_EAN_13 => new TypeEan13,
self::TYPE_UPC_A => new TypeUpcA,
self::TYPE_UPC_E => new TypeUpcE,
self::TYPE_MSI => new TypeMsi,
self::TYPE_MSI_CHECKSUM => new TypeMsiChecksum,
self::TYPE_POSTNET => new TypePostnet,
self::TYPE_PLANET => new TypePlanet,
self::TYPE_RMS4CC => new TypeRms4cc,
self::TYPE_KIX => new TypeKix,
self::TYPE_IMB => new TypeIntelligentMailBarcode,
self::TYPE_CODABAR => new TypeCodabar,
self::TYPE_CODE_11 => new TypeCode11,
self::TYPE_PHARMA_CODE => new TypePharmacode,
self::TYPE_PHARMA_CODE_TWO_TRACKS => new TypePharmacodeTwoCode,
};
}
}

View File

@@ -0,0 +1,5 @@
<?php
namespace AgeekDev\Barcode\Exceptions;
class BarcodeException extends \Exception {}

View File

@@ -0,0 +1,5 @@
<?php
namespace AgeekDev\Barcode\Exceptions;
class InvalidCharacterException extends BarcodeException {}

View File

@@ -0,0 +1,5 @@
<?php
namespace AgeekDev\Barcode\Exceptions;
class InvalidCheckDigitException extends BarcodeException {}

View File

@@ -0,0 +1,5 @@
<?php
namespace AgeekDev\Barcode\Exceptions;
class InvalidFormatException extends BarcodeException {}

View File

@@ -0,0 +1,5 @@
<?php
namespace AgeekDev\Barcode\Exceptions;
class InvalidLengthException extends BarcodeException {}

View File

@@ -0,0 +1,5 @@
<?php
namespace AgeekDev\Barcode\Exceptions;
class UnknownTypeException extends BarcodeException {}

View File

@@ -0,0 +1,28 @@
<?php
namespace AgeekDev\Barcode\Facades;
use AgeekDev\Barcode\Contracts\ImageType;
use AgeekDev\Barcode\Enums\BarcodeType;
use AgeekDev\Barcode\Enums\Type;
use Illuminate\Support\Facades\Facade;
/**
* @method static ImageType foregroundColor(string $foregroundColor)
* @method static ImageType height(int $height)
* @method static ImageType widthFactor(int $widthFactor)
* @method static ImageType type(BarcodeType|Type $type)
* @method static ImageType imageType(?string $driver)
* @method static ImageType useImagick()
* @method static ImageType useGd()
* @method static string generate(string $text)
*
* @see \AgeekDev\Barcode\BarcodeManager
*/
class Barcode extends Facade
{
protected static function getFacadeAccessor(): string
{
return 'laravel-barcode';
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace AgeekDev\Barcode\Helpers;
class BarcodeHelper
{
public static function getChecksum(string $code): int
{
$len = strlen($code);
$sum = 0;
for ($i = 0; $i < $len; $i += 2) {
$sum .= $code[$i];
}
$sum *= 3;
for ($i = 1; $i < $len; $i += 2) {
$sum .= ($code[$i]);
}
$r = $sum % 10;
if ($r > 0) {
$r = (10 - $r);
}
return $r;
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace AgeekDev\Barcode\Helpers;
use AgeekDev\Barcode\Barcode;
use AgeekDev\Barcode\BarcodeBar;
/**
* Convert binary barcode sequence string to Barcode representation.
*/
class BinarySequenceConverter
{
public static function convert(string $code, string $sequence): Barcode
{
$barcode = new Barcode($code);
return self::generate($sequence, $barcode);
}
public static function generate(string $sequence, Barcode $barcode): Barcode
{
$len = strlen($sequence);
$barWidth = 0;
for ($i = 0; $i < $len; $i++) {
$barWidth++;
if (($i === ($len - 1)) || (($i < ($len - 1)) && ($sequence[$i] !== $sequence[($i + 1)]))) {
if ($sequence[$i] === '1') {
$drawBar = true;
} else {
$drawBar = false;
}
$barcode->addBar(new BarcodeBar($barWidth, 1, $drawBar));
$barWidth = 0;
}
}
return $barcode;
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace AgeekDev\Barcode\Types;
use AgeekDev\Barcode\Barcode;
use AgeekDev\Barcode\BarcodeBar;
use AgeekDev\Barcode\Exceptions\InvalidCharacterException;
/*
* CODABAR barcodes.
* Older code often used in library systems, sometimes in blood banks
*/
class TypeCodabar implements TypeInterface
{
protected array $conversionTable = [
'0' => '11111221',
'1' => '11112211',
'2' => '11121121',
'3' => '22111111',
'4' => '11211211',
'5' => '21111211',
'6' => '12111121',
'7' => '12112111',
'8' => '12211111',
'9' => '21121111',
'-' => '11122111',
'$' => '11221111',
':' => '21112121',
'/' => '21211121',
'.' => '21212111',
'+' => '11222221',
'A' => '11221211',
'B' => '12121121',
'C' => '11121221',
'D' => '11122211',
];
/**
* @throws InvalidCharacterException
*/
public function getBarcodeData(string $code): Barcode
{
$barcode = new Barcode($code);
$code = 'A'.strtoupper($code).'A';
for ($i = 0, $iMax = strlen($code); $i < $iMax; $i++) {
if (! isset($this->conversionTable[$code[$i]])) {
throw new InvalidCharacterException('Char '.$code[$i].' is unsupported');
}
$seq = $this->conversionTable[$code[$i]];
for ($j = 0; $j < 8; $j++) {
if (($j % 2) === 0) {
$drawBar = true;
} else {
$drawBar = false;
}
$barWidth = $seq[$j];
$barcode->addBar(new BarcodeBar($barWidth, 1, $drawBar));
}
}
return $barcode;
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace AgeekDev\Barcode\Types;
use AgeekDev\Barcode\Barcode;
use AgeekDev\Barcode\BarcodeBar;
use AgeekDev\Barcode\Exceptions\InvalidCharacterException;
/*
* CODE11 barcodes.
* Used primarily for labeling telecommunications equipment
*/
class TypeCode11 implements TypeInterface
{
protected array $conversionTable = [
'0' => '111121',
'1' => '211121',
'2' => '121121',
'3' => '221111',
'4' => '112121',
'5' => '212111',
'6' => '122111',
'7' => '111221',
'8' => '211211',
'9' => '211111',
'-' => '112111',
'S' => '112211',
];
/**
* @throws InvalidCharacterException
*/
public function getBarcodeData(string $code): Barcode
{
$barcode = new Barcode($code);
$code .= $this->getCheckDigitC($code);
$code .= $this->getCheckDigitK($code);
$code = 'S'.$code.'S';
for ($i = 0, $iMax = strlen($code); $i < $iMax; $i++) {
if (! isset($this->conversionTable[$code[$i]])) {
throw new InvalidCharacterException('Char '.$code[$i].' is unsupported');
}
$seq = $this->conversionTable[$code[$i]];
for ($j = 0, $jMax = strlen($seq); $j < $jMax; $j++) {
if (($j % 2) === 0) {
$drawBar = true;
} else {
$drawBar = false;
}
$barWidth = $seq[$j];
$barcode->addBar(new BarcodeBar($barWidth, 1, $drawBar));
}
}
return $barcode;
}
private function getCheckDigitC(string $code): string
{
$p = 1;
$check = 0;
for ($i = (strlen($code) - 1); $i >= 0; $i--) {
$digit = $code[$i];
if ($digit === '-') {
$dval = 10;
} else {
$dval = (int) $digit;
}
$check += ($dval * $p);
$p++;
if ($p > 10) {
$p = 1;
}
}
$check %= 11;
if ($check === 10) {
$check = '-';
}
return $check;
}
private function getCheckDigitK(string $code): string
{
if (strlen($code) <= 10) {
return '';
}
$p = 1;
$check = 0;
for ($i = (strlen($code) - 1); $i >= 0; $i--) {
$digit = $code[$i];
if ($digit === '-') {
$dval = 10;
} else {
$dval = (int) $digit;
}
$check += ($dval * $p);
$p++;
if ($p > 9) {
$p = 1;
}
}
$check %= 11;
return (string) $check;
}
}

View File

@@ -0,0 +1,415 @@
<?php
namespace AgeekDev\Barcode\Types;
use AgeekDev\Barcode\Barcode;
use AgeekDev\Barcode\BarcodeBar;
use AgeekDev\Barcode\Exceptions\InvalidCharacterException;
use AgeekDev\Barcode\Exceptions\InvalidLengthException;
/*
* C128 barcodes.
* Very capable code, excellent density, high reliability; in very wide use world-wide
*
* @param $code (string) code to represent.
* @param $type (string) barcode type: A, B, C or empty for automatic switch (AUTO mode)
*/
class TypeCode128 implements TypeInterface
{
protected ?string $type = null;
protected array $conversionTable = [
'212222', /* 00 */
'222122', /* 01 */
'222221', /* 02 */
'121223', /* 03 */
'121322', /* 04 */
'131222', /* 05 */
'122213', /* 06 */
'122312', /* 07 */
'132212', /* 08 */
'221213', /* 09 */
'221312', /* 10 */
'231212', /* 11 */
'112232', /* 12 */
'122132', /* 13 */
'122231', /* 14 */
'113222', /* 15 */
'123122', /* 16 */
'123221', /* 17 */
'223211', /* 18 */
'221132', /* 19 */
'221231', /* 20 */
'213212', /* 21 */
'223112', /* 22 */
'312131', /* 23 */
'311222', /* 24 */
'321122', /* 25 */
'321221', /* 26 */
'312212', /* 27 */
'322112', /* 28 */
'322211', /* 29 */
'212123', /* 30 */
'212321', /* 31 */
'232121', /* 32 */
'111323', /* 33 */
'131123', /* 34 */
'131321', /* 35 */
'112313', /* 36 */
'132113', /* 37 */
'132311', /* 38 */
'211313', /* 39 */
'231113', /* 40 */
'231311', /* 41 */
'112133', /* 42 */
'112331', /* 43 */
'132131', /* 44 */
'113123', /* 45 */
'113321', /* 46 */
'133121', /* 47 */
'313121', /* 48 */
'211331', /* 49 */
'231131', /* 50 */
'213113', /* 51 */
'213311', /* 52 */
'213131', /* 53 */
'311123', /* 54 */
'311321', /* 55 */
'331121', /* 56 */
'312113', /* 57 */
'312311', /* 58 */
'332111', /* 59 */
'314111', /* 60 */
'221411', /* 61 */
'431111', /* 62 */
'111224', /* 63 */
'111422', /* 64 */
'121124', /* 65 */
'121421', /* 66 */
'141122', /* 67 */
'141221', /* 68 */
'112214', /* 69 */
'112412', /* 70 */
'122114', /* 71 */
'122411', /* 72 */
'142112', /* 73 */
'142211', /* 74 */
'241211', /* 75 */
'221114', /* 76 */
'413111', /* 77 */
'241112', /* 78 */
'134111', /* 79 */
'111242', /* 80 */
'121142', /* 81 */
'121241', /* 82 */
'114212', /* 83 */
'124112', /* 84 */
'124211', /* 85 */
'411212', /* 86 */
'421112', /* 87 */
'421211', /* 88 */
'212141', /* 89 */
'214121', /* 90 */
'412121', /* 91 */
'111143', /* 92 */
'111341', /* 93 */
'131141', /* 94 */
'114113', /* 95 */
'114311', /* 96 */
'411113', /* 97 */
'411311', /* 98 */
'113141', /* 99 */
'114131', /* 100 */
'311141', /* 101 */
'411131', /* 102 */
'211412', /* 103 START A */
'211214', /* 104 START B */
'211232', /* 105 START C */
'233111', /* STOP */
'200000', /* END */
];
/**
* @throws InvalidCharacterException
* @throws InvalidLengthException
*/
public function getBarcodeData(string $code): Barcode
{
if (trim($code) === '') {
throw new InvalidLengthException('You should provide a barcode string.');
}
// ASCII characters for code A (ASCII 00 - 95)
$keys_a = ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_';
$keys_a .= chr(0).chr(1).chr(2).chr(3).chr(4).chr(5).chr(6).chr(7).chr(8).chr(9);
$keys_a .= chr(10).chr(11).chr(12).chr(13).chr(14).chr(15).chr(16).chr(17).chr(18).chr(19);
$keys_a .= chr(20).chr(21).chr(22).chr(23).chr(24).chr(25).chr(26).chr(27).chr(28).chr(29);
$keys_a .= chr(30).chr(31);
// ASCII characters for code B (ASCII 32 - 127)
$keys_b = ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'.chr(127);
// special codes
$fnc_a = [241 => 102, 242 => 97, 243 => 96, 244 => 101];
$fnc_b = [241 => 102, 242 => 97, 243 => 96, 244 => 100];
// array of symbols
$code_data = [];
// length of the code
$len = strlen($code);
switch (strtoupper($this->type ?? '')) {
case 'A':
$startid = 103;
for ($i = 0; $i < $len; $i++) {
$char = $code[$i];
$char_id = ord($char);
if (($char_id <= 244) && ($char_id >= 241)) {
$code_data[] = $fnc_a[$char_id];
} elseif ($char_id <= 95) {
$code_data[] = strpos($keys_a, $char);
} else {
throw new InvalidCharacterException('Char '.$char.' is unsupported');
}
}
break;
case 'B':
$startid = 104;
for ($i = 0; $i < $len; $i++) {
$char = $code[$i];
$char_id = ord($char);
if (($char_id >= 241) && ($char_id <= 244)) {
$code_data[] = $fnc_b[$char_id];
} elseif (($char_id >= 32) && ($char_id <= 127)) {
$code_data[] = strpos($keys_b, $char);
} else {
throw new InvalidCharacterException('Char '.$char.' is unsupported');
}
}
break;
case 'C':
$startid = 105;
if (ord($code[0]) === 241) {
$code_data[] = 102;
$code = substr($code, 1);
$len--;
}
if (($len % 2) !== 0) {
throw new InvalidLengthException('Length must be even');
}
for ($i = 0; $i < $len; $i += 2) {
$chrnum = $code[$i].$code[$i + 1];
if (preg_match('/(\d{2})/', $chrnum) > 0) {
$code_data[] = (int) $chrnum;
} else {
throw new InvalidCharacterException;
}
}
break;
default:
// split code into sequences
$sequence = [];
// get numeric sequences (if any)
$numseq = [];
preg_match_all('/(\d{4,})/', $code, $numseq, PREG_OFFSET_CAPTURE);
if (! empty($numseq[1])) {
$end_offset = 0;
foreach ($numseq[1] as $val) {
$offset = $val[1];
// numeric sequence
$slen = strlen($val[0]);
if (($slen % 2) != 0) {
// the length must be even
$offset++;
$val[0] = substr($val[0], 1);
}
if ($offset > $end_offset) {
// non numeric sequence
$sequence = array_merge($sequence, $this->get128ABsequence(substr($code, $end_offset, ($offset - $end_offset))));
}
// numeric sequence fallback
$slen = strlen($val[0]);
if (($slen % 2) != 0) {
// the length must be even
$slen--;
}
$sequence[] = ['C', substr($code, $offset, $slen), $slen];
$end_offset = $offset + $slen;
}
if ($end_offset < $len) {
$sequence = array_merge($sequence, $this->get128ABsequence(substr($code, $end_offset)));
}
} else {
// text code (non C mode)
$sequence = array_merge($sequence, $this->get128ABsequence($code));
}
$startid = 0;
// process the sequence
foreach ($sequence as $key => $seq) {
switch ($seq[0]) {
case 'A':
if ($key === 0) {
$startid = 103;
} elseif ($sequence[($key - 1)][0] !== 'A') {
if (($seq[2] === 1) && ($key > 0) && ($sequence[($key - 1)][0] === 'B') && (! isset($sequence[($key - 1)][3]))) {
// single character shift
$code_data[] = 98;
// mark shift
$sequence[$key][3] = true;
} elseif (! isset($sequence[($key - 1)][3])) {
$code_data[] = 101;
}
}
for ($i = 0; $i < $seq[2]; $i++) {
$char = $seq[1][$i];
$char_id = ord($char);
if (($char_id >= 241) && ($char_id <= 244)) {
$code_data[] = $fnc_a[$char_id];
} else {
$code_data[] = strpos($keys_a, $char);
}
}
break;
case 'B':
if ($key === 0) {
$tmpchr = ord($seq[1][0]);
if (($seq[2] === 1) && ($tmpchr >= 241) && ($tmpchr <= 244) && isset($sequence[($key + 1)]) && ($sequence[($key + 1)][0] !== 'B')) {
switch ($sequence[($key + 1)][0]) {
case 'A':
$startid = 103;
$sequence[$key][0] = 'A';
$code_data[] = $fnc_a[$tmpchr];
break;
case 'C':
$startid = 105;
$sequence[$key][0] = 'C';
$code_data[] = $fnc_a[$tmpchr];
break;
}
break;
}
$startid = 104;
} elseif ($sequence[($key - 1)][0] !== 'B') {
if (($seq[2] === 1) && ($key > 0) && ($sequence[($key - 1)][0] === 'A') && (! isset($sequence[($key - 1)][3]))) {
// single character shift
$code_data[] = 98;
// mark shift
$sequence[$key][3] = true;
} elseif (! isset($sequence[($key - 1)][3])) {
$code_data[] = 100;
}
}
for ($i = 0; $i < $seq[2]; $i++) {
$char = $seq[1][$i];
$char_id = ord($char);
if (($char_id >= 241) && ($char_id <= 244)) {
$code_data[] = $fnc_b[$char_id];
} else {
$code_data[] = strpos($keys_b, $char);
}
}
break;
case 'C':
if ($key === 0) {
$startid = 105;
} elseif ($sequence[($key - 1)][0] !== 'C') {
$code_data[] = 99;
}
for ($i = 0; $i < $seq[2]; $i += 2) {
$chrnum = $seq[1][$i].$seq[1][$i + 1];
$code_data[] = (int) $chrnum;
}
break;
}
}
}
// calculate check character
$sum = $startid;
foreach ($code_data as $key => $val) {
$sum += ($val * ($key + 1));
}
// add check character
$code_data[] = ($sum % 103);
// add stop sequence
$code_data[] = 106;
$code_data[] = 107;
// add start code at the beginning
array_unshift($code_data, $startid);
// build barcode array
$barcode = new Barcode($code);
foreach ($code_data as $val) {
$seq = $this->conversionTable[$val];
for ($j = 0; $j < 6; $j++) {
if (($j % 2) === 0) {
$t = true; // bar
} else {
$t = false; // space
}
$w = $seq[$j];
$barcode->addBar(new BarcodeBar($w, 1, $t));
}
}
return $barcode;
}
/**
* Split text code in A/B sequence for 128 code
*
* @param $code (string) code to split.
*/
protected function get128ABsequence(string $code): array
{
$len = strlen($code);
$sequence = [];
// get A sequences (if any)
$numseq = [];
preg_match_all('/([\x00-\x1f])/', $code, $numseq, PREG_OFFSET_CAPTURE);
if (! empty($numseq[1])) {
$end_offset = 0;
foreach ($numseq[1] as $val) {
$offset = $val[1];
if ($offset > $end_offset) {
// B sequence
$sequence[] = [
'B',
substr($code, $end_offset, ($offset - $end_offset)),
($offset - $end_offset),
];
}
// A sequence
$slen = strlen($val[0]);
$sequence[] = ['A', substr($code, $offset, $slen), $slen];
$end_offset = $offset + $slen;
}
if ($end_offset < $len) {
$sequence[] = ['B', substr($code, $end_offset), ($len - $end_offset)];
}
} else {
// only B sequence
$sequence[] = ['B', $code, $len];
}
return $sequence;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace AgeekDev\Barcode\Types;
/*
* C128 barcodes.
* Very capable code, excellent density, high reliability; in very wide use world-wide
*
* @param $code (string) code to represent.
* @param $type (string) barcode type: A, B, C or empty for automatic switch (AUTO mode)
*/
class TypeCode128A extends TypeCode128
{
protected ?string $type = 'A';
}

View File

@@ -0,0 +1,16 @@
<?php
namespace AgeekDev\Barcode\Types;
/*
* C128 barcodes.
* Very capable code, excellent density, high reliability; in very wide use world-wide
*
* @param $code (string) code to represent.
* @param $type (string) barcode type: A, B, C or empty for automatic switch (AUTO mode)
*/
class TypeCode128B extends TypeCode128
{
protected ?string $type = 'B';
}

View File

@@ -0,0 +1,16 @@
<?php
namespace AgeekDev\Barcode\Types;
/*
* C128 barcodes.
* Very capable code, excellent density, high reliability; in very wide use world-wide
*
* @param $code (string) code to represent.
* @param $type (string) barcode type: A, B, C or empty for automatic switch (AUTO mode)
*/
class TypeCode128C extends TypeCode128
{
protected ?string $type = 'C';
}

View File

@@ -0,0 +1,60 @@
<?php
namespace AgeekDev\Barcode\Types;
use AgeekDev\Barcode\Barcode;
/*
* CODE 32 - italian pharmaceutical
* General-purpose code in very wide use world-wide
*/
class TypeCode32 extends TypeCode39
{
protected array $conversionTable32 = [
'0' => '0',
'1' => '1',
'2' => '2',
'3' => '3',
'4' => '4',
'5' => '5',
'6' => '6',
'7' => '7',
'8' => '8',
'9' => '9',
'10' => 'B',
'11' => 'C',
'12' => 'D',
'13' => 'F',
'14' => 'G',
'15' => 'H',
'16' => 'J',
'17' => 'K',
'18' => 'L',
'19' => 'M',
'20' => 'N',
'21' => 'P',
'22' => 'Q',
'23' => 'R',
'24' => 'S',
'25' => 'T',
'26' => 'U',
'27' => 'V',
'28' => 'W',
'29' => 'X',
'30' => 'Y',
'31' => 'Z',
];
public function getBarcodeData(string $code): Barcode
{
$code39 = '';
$codeElab = $code;
for ($e = 5; $e >= 0; $e--) {
$code39 .= $this->conversionTable32[(int) ($codeElab / (32 ** $e))];
$codeElab %= 32 ** $e;
}
return parent::getBarcodeData($code39);
}
}

View File

@@ -0,0 +1,330 @@
<?php
namespace AgeekDev\Barcode\Types;
use AgeekDev\Barcode\Barcode;
use AgeekDev\Barcode\BarcodeBar;
use AgeekDev\Barcode\Exceptions\InvalidCharacterException;
use AgeekDev\Barcode\Exceptions\InvalidLengthException;
/*
* CODE 39 - ANSI MH10.8M-1983 - USD-3 - 3 of 9.
* General-purpose code in very wide use world-wide
*/
class TypeCode39 implements TypeInterface
{
protected bool $extended = false;
protected bool $checksum = false;
protected array $conversionTable = [
'0' => '111331311',
'1' => '311311113',
'2' => '113311113',
'3' => '313311111',
'4' => '111331113',
'5' => '311331111',
'6' => '113331111',
'7' => '111311313',
'8' => '311311311',
'9' => '113311311',
'A' => '311113113',
'B' => '113113113',
'C' => '313113111',
'D' => '111133113',
'E' => '311133111',
'F' => '113133111',
'G' => '111113313',
'H' => '311113311',
'I' => '113113311',
'J' => '111133311',
'K' => '311111133',
'L' => '113111133',
'M' => '313111131',
'N' => '111131133',
'O' => '311131131',
'P' => '113131131',
'Q' => '111111333',
'R' => '311111331',
'S' => '113111331',
'T' => '111131331',
'U' => '331111113',
'V' => '133111113',
'W' => '333111111',
'X' => '131131113',
'Y' => '331131111',
'Z' => '133131111',
'-' => '131111313',
'.' => '331111311',
' ' => '133111311',
'$' => '131313111',
'/' => '131311131',
'+' => '131113131',
'%' => '111313131',
'*' => '131131311',
];
/**
* @throws InvalidCharacterException
* @throws InvalidLengthException
*/
public function getBarcodeData(string $code): Barcode
{
if (trim($code) === '') {
throw new InvalidLengthException('You should provide a barcode string.');
}
if ($this->extended) {
// extended mode
$code = $this->encode_code39_ext($code);
}
if ($this->checksum) {
// checksum
$code .= $this->checksum_code39($code);
}
// add start and stop codes
$code = '*'.$code.'*';
$barcode = new Barcode($code);
for ($i = 0, $iMax = strlen($code); $i < $iMax; $i++) {
$char = $code[$i];
if (! isset($this->conversionTable[$char])) {
throw new InvalidCharacterException('Char '.$char.' is unsupported');
}
for ($j = 0; $j < 9; $j++) {
if (($j % 2) === 0) {
$drawBar = true;
} else {
$drawBar = false;
}
$barWidth = $this->conversionTable[$char][$j];
$barcode->addBar(new BarcodeBar($barWidth, 1, $drawBar));
}
// inter character gap
$barcode->addBar(new BarcodeBar(1, 1, false));
}
return $barcode;
}
/**
* Encode a string to be used for CODE 39 Extended mode.
*
* @param string $code code to represent.
*
* @throws InvalidCharacterException
*/
protected function encode_code39_ext(string $code): string
{
$encode = [
chr(0) => '%U',
chr(1) => '$A',
chr(2) => '$B',
chr(3) => '$C',
chr(4) => '$D',
chr(5) => '$E',
chr(6) => '$F',
chr(7) => '$G',
chr(8) => '$H',
chr(9) => '$I',
chr(10) => '$J',
chr(11) => '$K',
chr(12) => '$L',
chr(13) => '$M',
chr(14) => '$N',
chr(15) => '$O',
chr(16) => '$P',
chr(17) => '$Q',
chr(18) => '$R',
chr(19) => '$S',
chr(20) => '$T',
chr(21) => '$U',
chr(22) => '$V',
chr(23) => '$W',
chr(24) => '$X',
chr(25) => '$Y',
chr(26) => '$Z',
chr(27) => '%A',
chr(28) => '%B',
chr(29) => '%C',
chr(30) => '%D',
chr(31) => '%E',
chr(32) => ' ',
chr(33) => '/A',
chr(34) => '/B',
chr(35) => '/C',
chr(36) => '/D',
chr(37) => '/E',
chr(38) => '/F',
chr(39) => '/G',
chr(40) => '/H',
chr(41) => '/I',
chr(42) => '/J',
chr(43) => '/K',
chr(44) => '/L',
chr(45) => '-',
chr(46) => '.',
chr(47) => '/O',
chr(48) => '0',
chr(49) => '1',
chr(50) => '2',
chr(51) => '3',
chr(52) => '4',
chr(53) => '5',
chr(54) => '6',
chr(55) => '7',
chr(56) => '8',
chr(57) => '9',
chr(58) => '/Z',
chr(59) => '%F',
chr(60) => '%G',
chr(61) => '%H',
chr(62) => '%I',
chr(63) => '%J',
chr(64) => '%V',
chr(65) => 'A',
chr(66) => 'B',
chr(67) => 'C',
chr(68) => 'D',
chr(69) => 'E',
chr(70) => 'F',
chr(71) => 'G',
chr(72) => 'H',
chr(73) => 'I',
chr(74) => 'J',
chr(75) => 'K',
chr(76) => 'L',
chr(77) => 'M',
chr(78) => 'N',
chr(79) => 'O',
chr(80) => 'P',
chr(81) => 'Q',
chr(82) => 'R',
chr(83) => 'S',
chr(84) => 'T',
chr(85) => 'U',
chr(86) => 'V',
chr(87) => 'W',
chr(88) => 'X',
chr(89) => 'Y',
chr(90) => 'Z',
chr(91) => '%K',
chr(92) => '%L',
chr(93) => '%M',
chr(94) => '%N',
chr(95) => '%O',
chr(96) => '%W',
chr(97) => '+A',
chr(98) => '+B',
chr(99) => '+C',
chr(100) => '+D',
chr(101) => '+E',
chr(102) => '+F',
chr(103) => '+G',
chr(104) => '+H',
chr(105) => '+I',
chr(106) => '+J',
chr(107) => '+K',
chr(108) => '+L',
chr(109) => '+M',
chr(110) => '+N',
chr(111) => '+O',
chr(112) => '+P',
chr(113) => '+Q',
chr(114) => '+R',
chr(115) => '+S',
chr(116) => '+T',
chr(117) => '+U',
chr(118) => '+V',
chr(119) => '+W',
chr(120) => '+X',
chr(121) => '+Y',
chr(122) => '+Z',
chr(123) => '%P',
chr(124) => '%Q',
chr(125) => '%R',
chr(126) => '%S',
chr(127) => '%T',
];
$code_ext = '';
for ($i = 0, $iMax = strlen($code); $i < $iMax; $i++) {
if (ord($code[$i]) > 127) {
throw new InvalidCharacterException('Only supports till char 127');
}
$code_ext .= $encode[$code[$i]];
}
return $code_ext;
}
/**
* Calculate CODE 39 checksum (modulo 43).
*
* @param string $code code to represent.
*/
protected function checksum_code39(string $code): string
{
$chars = [
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
'-',
'.',
' ',
'$',
'/',
'+',
'%',
];
$sum = 0;
for ($i = 0, $iMax = strlen($code); $i < $iMax; $i++) {
$k = array_keys($chars, $code[$i]);
$sum += $k[0];
}
$j = ($sum % 43);
return $chars[$j];
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace AgeekDev\Barcode\Types;
/*
* CODE 39 - ANSI MH10.8M-1983 - USD-3 - 3 of 9.
* General-purpose code in very wide use world-wide
*/
class TypeCode39Checksum extends TypeCode39
{
protected bool $extended = false;
protected bool $checksum = true;
}

View File

@@ -0,0 +1,15 @@
<?php
namespace AgeekDev\Barcode\Types;
/*
* CODE 39 - ANSI MH10.8M-1983 - USD-3 - 3 of 9.
* General-purpose code in very wide use world-wide
*/
class TypeCode39Extended extends TypeCode39
{
protected bool $extended = true;
protected bool $checksum = false;
}

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