Compare commits

..

207 Commits

Author SHA1 Message Date
arifal
fc4b419878 fix pbg task payment 2025-09-11 13:17:55 +07:00
arifal hidayat
53d12d6798 add column full pbg task payments 2025-09-11 02:06:34 +07:00
arifal
809eb85255 fix menu seeder 2025-09-10 18:20:21 +07:00
arifal
8a513460bb remove log folder 2025-09-10 17:51:52 +07:00
arifal
fc74875cce ingore folder log 2025-09-10 17:49:18 +07:00
arifal
beb7d935c9 partial update fix redirect and add note in public, quick and pbg data 2025-09-10 17:24:44 +07:00
arifal
5c4cebd2b3 remove update target pad from spreadsheet 2025-09-08 13:07:17 +07:00
arifal hidayat
cbe3d00c96 fix type uid 2025-08-31 01:34:35 +07:00
arifal hidayat
65d9247b46 fix new hit endpoint pbg status 2025-08-31 01:22:51 +07:00
arifal
63310f2748 fix no data and handle default page load no data 2025-08-26 13:59:36 +07:00
arifal hidayat
c6257b79bf create public search 2025-08-26 02:33:09 +07:00
arifal hidayat
38493063c4 fix same column for quick-search table with pbg 2025-08-23 22:25:06 +07:00
arifal
954b2d8716 using bigdata 168 2025-08-21 11:31:01 +07:00
arifal
41cfce589b bigdata fix 169 2025-08-21 11:28:33 +07:00
arifal
8de1b51fea fix fix bapenda 2025-08-20 05:43:38 +07:00
arifal
fef6ae7522 fix data count and sum for showing 2025-08-20 05:06:45 +07:00
arifal
6f1cb4195a fix handle big svg 2025-08-20 02:37:42 +07:00
arifal
6a060f5dac fix handle inside system 2025-08-20 02:30:46 +07:00
arifal
844fbdfa89 fix line dashbaord 2025-08-20 02:14:50 +07:00
arifal
e18c0cb3b6 fix get current year 2025-08-20 00:34:26 +07:00
arifal
0a9d9071e4 fix update dashboard external 2025-08-20 00:23:30 +07:00
arifal
4b28bebcc2 fix pbg payments 2025-08-19 22:00:20 +07:00
arifal
1bcd2023da fix non business where unit is more than one 2025-08-19 19:02:54 +07:00
arifal
1b084ed485 add status spatial plannings 2025-08-19 18:15:58 +07:00
arifal
71ca8dc553 fix handle redirect rab circle 2025-08-19 14:33:40 +07:00
arifal
3cddd271c8 fix exclude is_valid false and clickable dashboard simbg 2025-08-19 14:06:07 +07:00
arifal
e9a70a827c add local backupdb 2025-08-19 13:22:20 +07:00
arifal
2cbc4172da fix pbg task add toggle and rab krk dlh 2025-08-19 13:00:40 +07:00
arifal hidayat
d7e9f44b20 fix render data and show count rab krk and dlh 2025-08-19 04:02:08 +07:00
arifal hidayat
7c7aa0e2a5 partial update filter data year now pbg 2025-08-19 02:25:03 +07:00
arifal hidayat
0111ab14e1 fix search like in quick search and pbg data 2025-08-19 01:40:28 +07:00
arifal
68e9d5eebf fix call scraping scheduler 2025-08-15 17:51:13 +07:00
arifal
209ef07f9c add new url scraping data and create tab data lists 2025-08-15 17:25:20 +07:00
arifal
6896fd62a3 fix handle duplicate data 2025-08-15 12:33:38 +07:00
root
b73183becf backupdb 2025-08-15 05:14:42 +00:00
arifal
9f9c3758ed try to fix count and show data 2025-08-15 12:06:27 +07:00
arifal hidayat
48a340d684 revised 2025-08-15 03:58:17 +07:00
arifal hidayat
3ff3dc8f17 fix verified 2025-08-15 03:53:42 +07:00
arifal hidayat
7936eb1dbf fix potention 2025-08-15 03:26:10 +07:00
arifal hidayat
ec047821a1 fix potention 2025-08-15 03:19:49 +07:00
arifal hidayat
e0ed007a39 fix business data count and show 2025-08-15 02:57:19 +07:00
arifal hidayat
bb63ea8084 fix business data 2025-08-15 02:51:17 +07:00
arifal hidayat
7a19d9f39d fix syncrone 2025-08-15 02:27:05 +07:00
arifal hidayat
93af7ab2a1 fix vite 2025-08-15 01:59:24 +07:00
arifal hidayat
6158903260 fix handle vite simple validator 2025-08-15 01:52:10 +07:00
arifal hidayat
09e7d41ddc fix asset 2025-08-15 01:41:36 +07:00
arifal hidayat
2f4ef6cb56 fix assets call 2025-08-15 01:37:52 +07:00
arifal
4f94e9d8f7 add dump backupdb 2025-08-14 16:26:46 +00:00
arifal hidayat
fa6a0079dc fix handle redirect and add filter data pbg task same with dashboard 2025-08-14 23:11:36 +07:00
arifal
f7497cbec8 partial update count and sum dashboard 2025-08-14 20:00:23 +07:00
arifal
b5f7bf39b2 add spatial planning count and sum on dashboard simbg 2025-08-08 15:11:32 +07:00
arifal hidayat
ef3c9d6fc3 fix total non business count and sum 2025-08-08 03:53:38 +07:00
arifal hidayat
1288ab509d fix data and dashboard count and sum 2025-08-08 03:17:04 +07:00
arifal hidayat
588e3ad5e2 add backup success sync 2025-08-08 00:12:22 +07:00
arifal
3902a486f7 partial update fix sync data pbg 2025-08-07 22:04:40 +07:00
arifal hidayat
dd1cd72450 add build 2025-08-07 00:53:32 +07:00
arifal hidayat
af05a39a82 fix menu tax in data and fix session when multiple user login 2025-08-07 00:51:46 +07:00
arifal hidayat
0abf278aa3 add edit and delete data tax 2025-08-06 00:36:56 +07:00
arifal
c2cb1b99f2 fix searching using registration number only and zip build frontend 2025-08-05 14:51:56 +07:00
arifal hidayat
7135876ebc create page tax with data, upload and export group by subdistrict 2025-08-05 01:30:37 +07:00
arifal
456eec83dc fix index default sheet Bagan 2025 on spreadsheet 2025-07-18 14:00:20 +07:00
arifal
6a22b55a1c fix calculated and truncate calculation spatial plannings 2025-07-03 19:19:20 +07:00
arifal
5aab6fa3d1 add 10% 2025-07-03 15:34:02 +07:00
arifal
a1e302a56d add 30% 2025-07-03 15:28:54 +07:00
arifal
a7f578ca3d add docker for server demo 2025-06-26 18:28:26 +07:00
arifal
c33193d5f0 add docker 2025-06-24 15:09:21 +07:00
arifal
2c7c99bcf1 add backup db from demo server 2025-06-24 13:45:04 +07:00
arifal
a01b6f5611 fix error sync google sheet 2025-06-24 13:00:48 +07:00
arifal
2f43ebe97e build 2025-06-24 12:05:00 +07:00
arifal
e5baf5318f add separator in js for handle showing value is number 2025-06-24 12:02:17 +07:00
arifal
b895f61701 fix duplicate calculation ids 2025-06-23 18:20:16 +07:00
arifal
5dd92aa323 create new command for insert init spatial plannings dan remove unused retribution tables 2025-06-23 17:52:55 +07:00
arifal
7eb5a850c2 add new build vite 2025-06-23 13:56:42 +07:00
arifal
200b398868 fix handle null on scraping google sheet data and add detail data building 2025-06-23 13:54:26 +07:00
arifal
ccff82bd22 add build js 2025-06-19 13:57:28 +07:00
arifal
285e89d5d0 done restructure calculation retribution 2025-06-19 13:48:35 +07:00
arifal hidayat
4c3443c2d6 restructure retribution calculations table 2025-06-18 22:53:44 +07:00
arifal
df70a47bd1 add build 2025-06-18 15:45:12 +07:00
arifal
e71dd7d213 count spatial plannings business and non business and create pbg task detail and add to syncrone daily 2025-06-18 13:45:35 +07:00
arifal hidayat
f2eb998ac5 build and add init spatial 2025-06-18 02:57:30 +07:00
arifal hidayat
fc54e20fa4 add spatial plannings retribution calculations 2025-06-18 02:54:41 +07:00
arifal
6946fa7074 update retribution calculation spatial plannings 2025-06-17 17:58:37 +07:00
arifal
236b6f9bfc add build frontend 2025-06-17 12:04:55 +07:00
arifal
285ff46c2b fix data setting get datatable using api token 2025-06-17 11:54:02 +07:00
arifal hidayat
a8b02afad9 add build production manifest 2025-06-14 04:22:57 +07:00
arifal hidayat
a0666e78d2 fix value dashboard verified component 2025-06-14 04:20:26 +07:00
arifal hidayat
799e409ce2 create build script 2025-06-14 02:48:00 +07:00
arifal hidayat
780ba60224 add sync to leader dashboard new from google spreadsheet 2025-06-14 02:15:49 +07:00
arifal
baed8cc487 fix 401 hit api 2025-06-13 20:43:16 +07:00
arifal
e17f5beaf0 production code 2025-06-13 13:36:27 +00:00
arifal
766e1a430c fix permision deploy 2025-06-13 20:08:14 +07:00
arifal
6677c320fc build and create deploy production 2025-06-13 19:53:23 +07:00
arifal hidayat
9437eb949f add docker 2025-06-06 22:42:41 +07:00
arifal
6f77120c33 backup database 2025-06-02 15:41:03 +07:00
arifal
f8d0573e5c remove description body response on datatable 2025-06-02 15:30:43 +07:00
arifal
ca74d0143f hot fix show all chart growth 2025-05-20 11:59:50 +07:00
arifal
34e082c31b fix bug sync google sheet and handle to long data address on pbg task 2025-05-20 11:13:21 +07:00
arifal
c4d865bf2b fix get menu id and height auto chart growth 2025-05-15 18:09:28 +07:00
arifal
a103b38265 create page growth report 2025-05-15 17:02:02 +07:00
arifal
9aa3d32b6e add link in potential dashboard 2025-05-15 14:18:27 +07:00
arifal
99e2c214b6 create redirect link for dashboard pimpinan 2025-05-14 20:56:36 +07:00
arifal
501a76bc81 partial update circle chart can click 2025-05-06 17:29:39 +07:00
arifal
460267992e handle null value on detail quick search 2025-05-06 15:32:03 +07:00
arifal
becc368069 last update feat quick search 2025-05-06 15:06:59 +07:00
arifal
2618ac06d0 partial update create page quick search 2025-05-05 18:36:56 +07:00
arifal
d95676d477 fix show page document pbg 2025-04-10 15:44:51 +07:00
arifal
7737fee30f add validation topic on send prompt if not relevan 2025-04-10 01:59:57 +07:00
arifal
48293386c7 fix change chatbot to top 2025-04-09 21:22:26 +07:00
arifal
84870b95b1 add feat upload pbg task 2025-04-09 21:10:20 +07:00
arifal
6294d2f950 add new users on seeder for role user and superadmin 2025-03-27 17:19:27 +07:00
arifal
7b70591be5 add readme and backup schema local 2025-03-27 15:28:21 +07:00
arifal
3b67bfd1fb remove duplicate use timeout when running queue 2025-03-26 15:20:03 +07:00
arifal
45e22096ed add column to model import datasources 2025-03-26 11:51:57 +07:00
arifal
c7a8d6d249 add condition show button retry 2025-03-25 22:46:34 +07:00
arifal
091b1f305e fix handle retry button 2025-03-25 15:54:02 +07:00
arifal
e7950e22f2 add flatpicker for edit pbg task data 2025-03-24 21:16:05 +07:00
arifal
b68641db03 backup local db and add global setting seeder to run default 2025-03-24 11:08:10 +07:00
arifal
fefb85ac7a remove get sample data from service 2025-03-21 19:10:17 +07:00
arifal
d28a08a24c fix add bigdata resume after syncronize 2025-03-21 19:08:18 +07:00
arifal
654d2efe19 fix syncronize using worker and add duration syncronize 2025-03-21 18:44:28 +07:00
arifal
0a080763cd try catch handle refresh token fail to login again 2025-03-21 16:03:10 +07:00
arifal
f3ef21d1be fix style error message when failed login 2025-03-21 15:10:28 +07:00
arifal
d7bff86741 fix menu with parent data and remove action google sheet not available feature 2025-03-21 14:54:18 +07:00
arifal
f36f250700 fix supervisorctl syntax on deploy 2025-03-20 20:19:38 +07:00
arifal
2c5da87856 fix reload php service on deploy 2025-03-20 20:18:09 +07:00
arifal
a195559c4b fix running syntax seeder on automation deploy syntax 2025-03-20 20:15:30 +07:00
arifal
088f173fec fix add params filter date on grid js, export excel and pdf payment recaps 2025-03-20 20:12:42 +07:00
arifal
eadfddb3a4 add export excel and pdf district payment recaps 2025-03-19 18:55:51 +07:00
arifal
47a9fb1dfb add export excel and pdf report director 2025-03-19 18:22:00 +07:00
arifal
1713e32b67 add export pdf report tourisms and report ptsp 2025-03-19 17:15:10 +07:00
arifal
cf998455e0 add export excel report tourisms 2025-03-19 16:16:10 +07:00
arifal
5e1c9f3a2e fix dashboard pbg, add soft delete users, fix js create pbg task 2025-03-19 14:06:10 +07:00
arifal
e940b8d6c7 fix handle sync using button 2025-03-18 08:10:06 +07:00
arifal
5e139bc29c change command scraping 2025-03-18 06:47:53 +07:00
arifal
f9e1aa1604 fix service scraping data 2025-03-18 06:42:42 +07:00
arifal
2e385f80cd fix optimize syncronize 2025-03-14 19:10:28 +07:00
arifal
e2c26e0eff fix reklame route index 2025-03-13 16:23:17 +07:00
arifal
ca5b8ad403 fix deploy code and readme 2025-03-13 15:59:11 +07:00
arifal
e97b7eb70d fix redirect back spatial plannings 2025-03-13 15:48:53 +07:00
arifal
0258ca9f04 fix redirect back umkm 2025-03-13 15:39:32 +07:00
arifal
0116147e06 fix redirect back reklame and tourisms 2025-03-13 15:24:59 +07:00
arifal
7787db02a3 fix redirect back business or industries 2025-03-13 14:13:06 +07:00
arifal
e47ab36d5e fix pdam redirect back and advertisement partial update redirect 2025-03-13 13:57:53 +07:00
arifal
e5db2294b4 fix redirect back data settings 2025-03-12 21:39:52 +07:00
arifal
4db457d7bd fix redirect back users crud 2025-03-12 21:17:49 +07:00
arifal
7a82ad5302 fix redirect back roles 2025-03-12 20:30:16 +07:00
arifal
b0d4d4c23b fix redirect back with params menu id 2025-03-12 17:52:11 +07:00
arifal
68ffc1c090 fix 503 page 2025-03-12 15:57:31 +07:00
arifal
a1f4bd7f81 fix seeder menu role and user assign, create 503 page and fix redirect home 2025-03-12 15:37:22 +07:00
arifal
238aaba96c fix seeder users role menu 2025-03-12 12:01:28 +07:00
arifal
c7152d9dbe fix resources report payment recaps 2025-03-12 11:50:56 +07:00
arifal
2a4b96d0b2 fix vite resources 2025-03-12 11:43:11 +07:00
arifal
dd940ebdb6 fix inside and outside dashboard 2025-03-12 11:38:07 +07:00
arifal
dce5409248 done all view dummy new modul 2025-03-12 00:32:50 +07:00
arifal
b8f7d7f655 fix back button 2025-03-11 01:17:16 +07:00
arifal
65600f1b4f add view list data from google sheet 2025-03-11 00:26:48 +07:00
arifal
b0f15a9221 fix sidebar permission user 2025-03-10 16:33:53 +07:00
arifal
bf55eb228e fix width dashboard outside sy system and loading when load maps pariwisata, backup localdb 2025-03-07 18:00:30 +07:00
arifal
755720bac9 fix failed build images from leaflet 2025-03-07 16:41:52 +07:00
arifal
098b4c605b fix missing scss on vite 2025-03-07 15:39:26 +07:00
arifal
4632e102eb add use package missing 2025-03-07 15:00:26 +07:00
arifal
0431945a42 fix conflict 2025-03-07 14:52:51 +07:00
@jamaludinarifrohman6661
fbfa2a37bb feature: set role previledge access 2025-03-07 14:39:46 +07:00
arifal
c529a5d511 add readme 2025-03-07 14:39:23 +07:00
arifal
ff244039ff add readme and env example 2025-03-07 14:37:26 +07:00
arifal
55902042f4 add readme 2025-03-07 14:27:49 +07:00
arifal
c67aa979c2 change column type expertise and fix syncronize simbg service 2025-03-07 14:04:37 +07:00
arifal
fbaa33ae13 fix height scrollbar table grid js 2025-03-07 01:55:46 +07:00
arifal
9516b6f575 fix task assignment table on pbg task 2025-03-07 01:03:22 +07:00
arifal
ffc08f26cc add dashboard inside and outside system and fix timeout when search filter 2025-03-06 23:33:31 +07:00
@jamaludinarifrohman6661
dceb46ab86 change-request: add custom geo layer 2025-03-06 16:50:13 +07:00
arifal
e0c35b8897 fix search filter on page big data resume 2025-03-06 14:39:36 +07:00
arifal
22ee7502ad Merge remote-tracking branch 'origin/feature/chatbot-sidebar' into fix/sync-task-assignment 2025-03-06 11:45:34 +07:00
arifal
2f3bc172eb add search filter 2025-03-06 11:42:59 +07:00
@jamaludinarifrohman6661
bba932b2ba Merge remote-tracking branch 'origin/dev' into feature/chatbot-sidebar 2025-03-06 11:20:45 +07:00
@jamaludinarifrohman6661
3f5d0eb1cd fix: inserting chat history into the answer generation process 2025-03-06 11:06:45 +07:00
arifal
1f33d0de4e add sync task assignment pbg 2025-03-06 00:13:13 +07:00
arifal
86d694bcac add new menu chat bedas and view 2025-03-04 17:56:30 +07:00
arifal
cb5a3243fc Merge remote-tracking branch 'origin/feature/chatbot-sidebar' into dev 2025-03-04 17:32:37 +07:00
@jamaludinarifrohman6661
15210a56ee feature: chatbot pimpinan 2025-03-04 17:31:40 +07:00
arifal
a08f2cb2b7 fix optimizing deployment 2025-03-04 16:45:32 +07:00
arifal
632433c496 fix routing spatial-plannings 2025-03-04 16:23:00 +07:00
arifal
5b4780495e fix routing spatial-plannings 2025-03-04 16:17:47 +07:00
arifal
0a7012a57c fix tourisms routing 2025-03-04 16:13:17 +07:00
arifal
435a19346b fix route umkm 2025-03-04 16:05:21 +07:00
arifal
8fcf8859d6 hot fix advertisement route conflict 2025-03-04 15:58:30 +07:00
arifal
43a246d234 add permission deployment file 2025-03-04 08:49:51 +00:00
arifal
d6d0acf8fb hot fix conflict routing web and api advertisements 2025-03-04 15:46:35 +07:00
arifal
b4ec7a9d25 merge chatbot sidebar 2025-03-04 15:19:11 +07:00
arifal
5203babe11 Merge remote-tracking branch 'origin/feature/chatbot-sidebar' into dev 2025-03-04 15:15:32 +07:00
arifal
c0faafdbd7 hot fix add time midnight scheduler 2025-03-04 15:13:33 +07:00
arifal
c5e3fdd915 create file automation deployment using sh 2025-03-04 15:05:13 +07:00
@jamaludinarifrohman6661
572b86299c add:setting main chatbot
fix:chatbot ui
2025-03-04 14:46:29 +07:00
arifal
cdd84d02da fix parsing separator 2025-03-04 14:30:41 +07:00
arifal
ee1a395c75 add per page 50 and add some column onn laporan pimpinan 2025-03-04 10:41:52 +07:00
arifal
3bfcaddba4 fix queue execute scraping syncronize simbg add new column to resume 2025-03-03 22:55:57 +07:00
arifal
9ea7e96af1 add vite and merge chatbot jamal 2025-02-28 18:39:23 +07:00
arifal
e0d11af7d2 Merge remote-tracking branch 'origin/feature/chatbot' into dev 2025-02-28 18:14:29 +07:00
arifal
f5790cda94 fix filter year dashboard 2025-02-28 15:46:31 +07:00
arifal
f3db3783f9 deployed 2025-02-28 2025-02-28 04:49:28 +00:00
arifal
101e76c0fa add js to vite 2025-02-28 11:45:55 +07:00
arifal
4cc698a623 fix handle disable button when upload business industries 2025-02-28 10:37:41 +07:00
arifal
544ad1db46 fix upload business and industry with chunk 2025-02-28 00:35:09 +07:00
arifal
30ca819aa1 fix color on maps 2025-02-27 20:14:37 +07:00
arifal
b0bab784d1 partial update add page laporan pimpinan and fix upload big data excel pdam 2025-02-27 19:23:06 +07:00
arifal
01fda22c89 partial update add loading on maps and add color for zone type maps 2025-02-26 23:52:48 +07:00
arifal
de300c2c32 fix dashboard style and resume bigdata 2025-02-26 21:52:23 +07:00
590 changed files with 33557 additions and 9631 deletions

0
.editorconfig Executable file → Normal file
View File

6
.env.example Executable file → Normal file
View File

@@ -2,7 +2,7 @@ APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_TIMEZONE=UTC
APP_TIMEZONE=Asia/Jakarta
APP_URL=http://localhost
API_URL=http://localhost:8000
@@ -65,6 +65,8 @@ AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
VITE_APP_HOST=localhost
API_KEY_GOOGLE="xxxxx"
SPREAD_SHEET_ID="xxxxx"
SPREAD_SHEET_ID="xxxxx"
OPENAI_API_KEY="xxxxx"

0
.gitattributes vendored Executable file → Normal file
View File

3
.gitignore vendored Executable file → Normal file
View File

@@ -21,3 +21,6 @@ yarn-error.log
/.nova
/.vscode
/.zed
/.composer
/.config
/.npm

1
.lesshst Normal file
View File

@@ -0,0 +1 @@
.less-history-file:

115
Dockerfile Normal file
View File

@@ -0,0 +1,115 @@
FROM node:18 AS node-base
# Development stage
FROM node-base AS development
WORKDIR /var/www
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
# Local development stage for PHP
FROM php:8.2-fpm AS local
WORKDIR /var/www
# Install PHP extensions
RUN apt-get update && apt-get install -y \
git curl zip unzip libpng-dev libonig-dev libxml2-dev libzip-dev \
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip
# Override PHP memory limit
COPY docker/php/memory-limit.ini /usr/local/etc/php/conf.d/memory-limit.ini
# Install Node.js
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get install -y nodejs
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Create www-data user with same UID/GID as host user (1000:1000 is common for first user)
RUN usermod -u 1000 www-data && groupmod -g 1000 www-data
# Copy application files
COPY . .
# Install dependencies
RUN composer install
# Create storage directories and set proper permissions
RUN mkdir -p storage/framework/{sessions,views,cache} \
&& mkdir -p storage/logs \
&& mkdir -p bootstrap/cache \
&& chown -R www-data:www-data /var/www \
&& chmod -R 775 /var/www/storage \
&& chmod -R 775 /var/www/bootstrap/cache
# Create entrypoint script to fix permissions on startup
RUN echo '#!/bin/bash\n\
chown -R www-data:www-data /var/www/storage /var/www/bootstrap/cache\n\
chmod -R 775 /var/www/storage /var/www/bootstrap/cache\n\
exec "$@"' > /entrypoint.sh && chmod +x /entrypoint.sh
USER www-data
EXPOSE 9000
ENTRYPOINT ["/entrypoint.sh"]
CMD ["php-fpm"]
# Production stage
FROM php:8.2-fpm AS production
WORKDIR /var/www
# Install PHP extensions
RUN apt-get update && apt-get install -y \
git curl zip unzip libpng-dev libonig-dev libxml2-dev libzip-dev \
supervisor \
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip
# Override PHP memory limit
COPY docker/php/memory-limit.ini /usr/local/etc/php/conf.d/memory-limit.ini
# Install Node.js
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get install -y nodejs
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Copy application files
COPY . .
# Install dependencies
RUN composer install --no-dev --optimize-autoloader
# Install and build frontend assets
RUN npm install \
&& npm run build \
&& ls -la public/build \
&& mkdir -p public/assets \
&& cp -r public/build/* public/assets/ \
&& ls -la public/assets \
&& rm -rf node_modules \
&& rm -rf public/build
# Laravel caches
RUN php artisan config:clear \
&& php artisan route:clear \
&& php artisan view:clear \
&& php artisan optimize
RUN php artisan storage:link
# Create supervisor directories
RUN mkdir -p /var/log/supervisor /var/run/supervisor
# Copy supervisor configuration
COPY docker/supervisor/supervisord.conf /etc/supervisor/supervisord.conf
COPY docker/supervisor/laravel-production.conf /etc/supervisor/conf.d/laravel-production.conf
# Permissions
RUN chown -R www-data:www-data /var/www && chmod -R 755 /var/www/storage /var/www/public
EXPOSE 9000
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]

191
README.md Executable file → Normal file
View File

@@ -1,66 +1,167 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
# Sibedas PBG Web
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
Aplikasi web untuk manajemen data PBG (Pendidikan Berkelanjutan Guru) dengan fitur integrasi Google Sheets.
## About Laravel
## 🚀 Quick Start
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
### Prerequisites
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
- Docker & Docker Compose
- Domain name (untuk production)
Laravel is accessible, powerful, and provides tools required for large, robust applications.
### Local Development
## Learning Laravel
```bash
git clone <repository-url>
cd sibedas-pbg-web
./scripts/setup-local.sh
# Access: http://localhost:8000
```
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
### Production Deployment
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
```bash
# 1. Setup environment
cp env.production.example .env
nano .env
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
# 2. Deploy with SSL (Recommended)
./scripts/setup-reverse-proxy.sh setup
## Laravel Sponsors
# 3. Check status
./scripts/setup-reverse-proxy.sh status
# Access: https://yourdomain.com
```
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
## 🏗️ Architecture
### Premium Partners
### Local Development
- **[Vehikl](https://vehikl.com/)**
- **[Tighten Co.](https://tighten.co)**
- **[WebReinvent](https://webreinvent.com/)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
- **[Cyber-Duck](https://cyber-duck.co.uk)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Jump24](https://jump24.co.uk)**
- **[Redberry](https://redberry.international/laravel/)**
- **[Active Logic](https://activelogic.com)**
- **[byte5](https://byte5.de)**
- **[OP.GG](https://op.gg)**
```
Browser → Port 8000 → Nginx → PHP-FPM → MariaDB
```
## Contributing
### Production dengan Reverse Proxy
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
```
Internet → Reverse Proxy (80/443) → Internal Nginx → PHP-FPM → MariaDB
```
## Code of Conduct
## 🔧 Configuration
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
### Environment Variables
## Security Vulnerabilities
```bash
# Domain & SSL
DOMAIN=sibedas.yourdomain.com
EMAIL=admin@yourdomain.com
SSL_TYPE=self-signed # atau letsencrypt
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
# Database
DB_PASSWORD=your_secure_password
MYSQL_ROOT_PASSWORD=your_root_password
## License
# Laravel
APP_KEY=base64:your_app_key_here
APP_URL=https://sibedas.yourdomain.com
```
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
## 🚀 Production Deployment Steps
### 1. Server Preparation
```bash
# Install Docker & Docker Compose
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
# Install Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
```
### 2. Clone & Setup
```bash
git clone <repository-url>
cd sibedas-pbg-web
chmod +x scripts/*.sh
cp env.production.example .env
nano .env
```
### 3. Deploy
```bash
# Full deployment with SSL
./scripts/setup-reverse-proxy.sh setup
# Or step by step
./scripts/deploy-production.sh deploy
./scripts/setup-ssl.sh letsencrypt
```
### 4. Verify
```bash
docker-compose ps
./scripts/setup-reverse-proxy.sh status
curl -f http://localhost/health-check
```
## 📊 Monitoring
```bash
# Check status
./scripts/setup-reverse-proxy.sh status
# View logs
docker-compose logs [service]
# Check SSL certificate
./scripts/setup-ssl.sh check
```
## 🛠️ Common Commands
```bash
# Start services
docker-compose up -d
# Stop services
docker-compose down
# Restart services
docker-compose restart
# Execute Laravel commands
docker-compose exec app php artisan [command]
# Backup database
docker exec sibedas_db mysqldump -u root -p sibedas > backup.sql
```
## 📁 Scripts
### Essential Scripts
- `scripts/setup-reverse-proxy.sh` - Setup lengkap reverse proxy dan SSL
- `scripts/deploy-production.sh` - Deployment production
- `scripts/setup-ssl.sh` - Setup SSL certificates
### Optional Scripts
- `scripts/setup-local.sh` - Setup local development
- `scripts/import-sibedas-database.sh` - Manual database import
## 📚 Documentation
Untuk dokumentasi lengkap, lihat [docs/README.md](docs/README.md)
## 🆘 Support
1. Check logs: `docker-compose logs [service]`
2. Check status: `./scripts/setup-reverse-proxy.sh status`
3. Restart services: `docker-compose restart`
4. Review documentation di folder `docs/`

View File

@@ -0,0 +1,482 @@
<?php
namespace App\Console\Commands;
use App\Models\SpatialPlanning;
use App\Models\RetributionCalculation;
use App\Models\BuildingType;
use App\Services\RetributionCalculatorService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class AssignSpatialPlanningsToCalculation extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'spatial-planning:assign-calculations
{--force : Force assign even if already has calculation}
{--recalculate : Recalculate existing calculations with new values}
{--chunk=100 : Process in chunks}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Assign retribution calculations to spatial plannings (recalculate mode recalculates with current values)';
protected $calculatorService;
public function __construct(RetributionCalculatorService $calculatorService)
{
parent::__construct();
$this->calculatorService = $calculatorService;
}
/**
* Execute the console command.
*/
public function handle()
{
$this->info('🏗️ Starting spatial planning calculation assignment...');
// Get processing options
$force = $this->option('force');
$recalculate = $this->option('recalculate');
$chunkSize = (int) $this->option('chunk');
// Get spatial plannings query
$query = SpatialPlanning::query();
if ($recalculate) {
// Recalculate mode: only process those WITH active calculations
$query->whereHas('retributionCalculations', function ($q) {
$q->where('is_active', true);
});
$this->info('🔄 Recalculate mode: Processing spatial plannings with existing calculations');
$this->warn('⚠️ NOTE: Recalculate mode will recalculate all existing calculations with current values');
} elseif (!$force) {
// Normal mode: only process those without active calculations
$query->whereDoesntHave('retributionCalculations', function ($q) {
$q->where('is_active', true);
});
$this->info(' Normal mode: Processing spatial plannings without calculations');
} else {
// Force mode: process all
$this->info('🔥 Force mode: Processing ALL spatial plannings');
}
$totalRecords = $query->count();
if ($totalRecords === 0) {
$this->warn('No spatial plannings found to process.');
return 0;
}
$this->info("Found {$totalRecords} spatial planning(s) to process");
if (!$this->confirm('Do you want to continue?')) {
$this->info('Operation cancelled.');
return 0;
}
// Process in chunks
$processed = 0;
$errors = 0;
$reused = 0;
$created = 0;
$buildingTypeStats = [];
$progressBar = $this->output->createProgressBar($totalRecords);
$progressBar->start();
$recalculated = 0;
$query->chunk($chunkSize, function ($spatialPlannings) use (&$processed, &$errors, &$reused, &$created, &$recalculated, &$buildingTypeStats, $progressBar, $recalculate) {
foreach ($spatialPlannings as $spatialPlanning) {
try {
$result = $this->assignCalculationToSpatialPlanning($spatialPlanning, $recalculate);
if ($result['reused']) {
$reused++;
} elseif (isset($result['recalculated']) && $result['recalculated']) {
$recalculated++;
} else {
$created++;
}
// Track building type statistics
$buildingTypeName = $result['building_type_name'] ?? 'Unknown';
if (!isset($buildingTypeStats[$buildingTypeName])) {
$buildingTypeStats[$buildingTypeName] = 0;
}
$buildingTypeStats[$buildingTypeName]++;
$processed++;
} catch (\Exception $e) {
$errors++;
$this->error("Error processing ID {$spatialPlanning->id}: " . $e->getMessage());
}
$progressBar->advance();
}
});
$progressBar->finish();
// Show summary
$this->newLine(2);
$this->info('✅ Assignment completed!');
if ($recalculate) {
$this->table(
['Metric', 'Count'],
[
['Total Processed', $processed],
['Recalculated (Changed)', $recalculated],
['Unchanged', $reused],
['Errors', $errors],
]
);
$this->info('📊 Recalculate mode recalculated all existing calculations with current values');
} else {
$this->table(
['Metric', 'Count'],
[
['Total Processed', $processed],
['Calculations Created', $created],
['Calculations Reused', $reused],
['Errors', $errors],
]
);
}
// Show building type statistics
if (!empty($buildingTypeStats)) {
$this->newLine();
$this->info('📊 Building Type Distribution:');
$statsRows = [];
arsort($buildingTypeStats); // Sort by count descending
foreach ($buildingTypeStats as $typeName => $count) {
$percentage = round(($count / $processed) * 100, 1);
$statsRows[] = [$typeName, $count, $percentage . '%'];
}
$this->table(['Building Type', 'Count', 'Percentage'], $statsRows);
}
return 0;
}
/**
* Assign calculation to a spatial planning
*/
private function assignCalculationToSpatialPlanning(SpatialPlanning $spatialPlanning, bool $recalculate = false): array
{
// 1. Detect building type
$buildingType = $this->detectBuildingType($spatialPlanning->building_function);
// 2. Get calculation parameters (round to 2 decimal places)
$floorNumber = $spatialPlanning->number_of_floors ?: 1;
$buildingArea = round($spatialPlanning->getCalculationArea(), 2);
if ($buildingArea <= 0) {
throw new \Exception("Invalid building area: {$buildingArea}");
}
$reused = false;
$isRecalculated = false;
if ($recalculate) {
// Recalculate mode: Always create new calculation
$calculationResult = $this->performCalculation($spatialPlanning, $buildingType, true);
// Check if spatial planning has existing active calculation
$currentActiveCalculation = $spatialPlanning->activeRetributionCalculation;
if ($currentActiveCalculation) {
$oldAmount = $currentActiveCalculation->retributionCalculation->retribution_amount;
$oldArea = $currentActiveCalculation->retributionCalculation->building_area;
$newAmount = $calculationResult['amount'];
// Check if there's a significant difference (more than 1 rupiah)
if (abs($oldAmount - $newAmount) > 1) {
// Create new calculation
$calculation = RetributionCalculation::create([
'building_type_id' => $buildingType->id,
'floor_number' => $floorNumber,
'building_area' => $buildingArea,
'retribution_amount' => $calculationResult['amount'],
'calculation_detail' => $calculationResult['detail'],
]);
// Assign new calculation
$spatialPlanning->assignRetributionCalculation(
$calculation,
"Recalculated: Original area {$oldArea}m² → New area {$buildingArea}m², Amount {$oldAmount}{$newAmount}"
);
$isRecalculated = true;
} else {
// No significant difference, keep existing
$calculation = $currentActiveCalculation->retributionCalculation;
$reused = true;
}
} else {
// No existing calculation, create new
$calculation = RetributionCalculation::create([
'building_type_id' => $buildingType->id,
'floor_number' => $floorNumber,
'building_area' => $buildingArea,
'retribution_amount' => $calculationResult['amount'],
'calculation_detail' => $calculationResult['detail'],
]);
$spatialPlanning->assignRetributionCalculation(
$calculation,
'Recalculated (new calculation with current values)'
);
}
} else {
// Normal mode: Check if calculation already exists with same parameters
$existingCalculation = RetributionCalculation::where([
'building_type_id' => $buildingType->id,
'floor_number' => $floorNumber,
])
->whereBetween('building_area', [
$buildingArea * 0.99, // 1% tolerance
$buildingArea * 1.01
])
->first();
if ($existingCalculation) {
// Reuse existing calculation
$calculation = $existingCalculation;
$reused = true;
} else {
// Create new calculation
$calculationResult = $this->performCalculation($spatialPlanning, $buildingType, false);
$calculation = RetributionCalculation::create([
'building_type_id' => $buildingType->id,
'floor_number' => $floorNumber,
'building_area' => $buildingArea,
'retribution_amount' => $calculationResult['amount'],
'calculation_detail' => $calculationResult['detail'],
]);
}
// Assign to spatial planning
$spatialPlanning->assignRetributionCalculation(
$calculation,
$reused ? 'Auto-assigned (reused calculation)' : 'Auto-assigned (new calculation)'
);
}
return [
'calculation' => $calculation,
'reused' => $reused,
'recalculated' => $isRecalculated,
'building_type_name' => $buildingType->name,
'building_type_code' => $buildingType->code,
];
}
/**
* Detect building type based on building function using database
*/
private function detectBuildingType(string $buildingFunction = null): BuildingType
{
$function = strtolower($buildingFunction ?? '');
// Mapping building functions to building type codes from database
$mappings = [
// Religious
'masjid' => 'KEAGAMAAN',
'gereja' => 'KEAGAMAAN',
'vihara' => 'KEAGAMAAN',
'pura' => 'KEAGAMAAN',
'keagamaan' => 'KEAGAMAAN',
'religious' => 'KEAGAMAAN',
// Residential/Housing
'rumah' => 'HUN_SEDH', // Default to simple housing
'perumahan' => 'HUN_SEDH',
'hunian' => 'HUN_SEDH',
'residential' => 'HUN_SEDH',
'tinggal' => 'HUN_SEDH',
'mbr' => 'MBR', // Specifically for MBR
'masyarakat berpenghasilan rendah' => 'MBR',
// Commercial/Business - default to UMKM
'toko' => 'UMKM',
'warung' => 'UMKM',
'perdagangan' => 'UMKM',
'dagang' => 'UMKM',
'usaha' => 'UMKM',
'komersial' => 'UMKM',
'commercial' => 'UMKM',
'pasar' => 'UMKM',
'kios' => 'UMKM',
// Large commercial
'mall' => 'USH_BESAR',
'plaza' => 'USH_BESAR',
'supermarket' => 'USH_BESAR',
'department' => 'USH_BESAR',
'hotel' => 'USH_BESAR',
'resort' => 'USH_BESAR',
// Office
'kantor' => 'UMKM', // Can be UMKM or USH_BESAR depending on size
'perkantoran' => 'UMKM',
'office' => 'UMKM',
// Industry (usually big business)
'industri' => 'USH_BESAR',
'pabrik' => 'USH_BESAR',
'gudang' => 'USH_BESAR',
'warehouse' => 'USH_BESAR',
'manufacturing' => 'USH_BESAR',
// Social/Cultural
'sekolah' => 'SOSBUDAYA',
'pendidikan' => 'SOSBUDAYA',
'universitas' => 'SOSBUDAYA',
'kampus' => 'SOSBUDAYA',
'rumah sakit' => 'SOSBUDAYA',
'klinik' => 'SOSBUDAYA',
'kesehatan' => 'SOSBUDAYA',
'puskesmas' => 'SOSBUDAYA',
'museum' => 'SOSBUDAYA',
'perpustakaan' => 'SOSBUDAYA',
'gedung olahraga' => 'SOSBUDAYA',
// Mixed use
'campuran' => 'CAMP_KECIL', // Default to small mixed
'mixed' => 'CAMP_KECIL',
];
// Try to match building function
$detectedCode = null;
foreach ($mappings as $keyword => $code) {
if (str_contains($function, $keyword)) {
$detectedCode = $code;
break;
}
}
// Find building type in database by code
if ($detectedCode) {
$buildingType = BuildingType::where('code', $detectedCode)
->whereHas('indices') // Only types with indices
->first();
if ($buildingType) {
return $buildingType;
}
}
// Default to "UMKM" type if not detected (most common business type)
$defaultType = BuildingType::where('code', 'UMKM')
->whereHas('indices')
->first();
if ($defaultType) {
return $defaultType;
}
// Fallback to any available type with indices
$fallbackType = BuildingType::whereHas('indices')
->where('is_active', true)
->first();
if (!$fallbackType) {
throw new \Exception('No building types with indices found in database. Please run: php artisan db:seed --class=RetributionDataSeeder');
}
return $fallbackType;
}
/**
* Perform calculation using RetributionCalculatorService
*/
private function performCalculation(SpatialPlanning $spatialPlanning, BuildingType $buildingType, bool $recalculate = false): array
{
// Round area to 2 decimal places to match database storage format
$buildingArea = round($spatialPlanning->getCalculationArea(), 2);
// For recalculate mode, use the current area without any adjustment
if ($recalculate) {
$this->info("Recalculate mode: Using current area {$buildingArea}");
}
$floorNumber = $spatialPlanning->number_of_floors ?: 1;
try {
// Use the same calculation service as TestRetributionCalculation
$result = $this->calculatorService->calculate(
$buildingType->id,
$floorNumber,
$buildingArea,
false // Don't save to database, we'll handle that separately
);
return [
'amount' => $result['total_retribution'],
'detail' => [
'building_type_id' => $buildingType->id,
'building_type_name' => $buildingType->name,
'building_type_code' => $buildingType->code,
'coefficient' => $result['indices']['coefficient'],
'ip_permanent' => $result['indices']['ip_permanent'],
'ip_complexity' => $result['indices']['ip_complexity'],
'locality_index' => $result['indices']['locality_index'],
'height_index' => $result['input_parameters']['height_index'],
'infrastructure_factor' => $result['indices']['infrastructure_factor'],
'building_area' => $buildingArea,
'floor_number' => $floorNumber,
'building_function' => $spatialPlanning->building_function,
'calculation_steps' => $result['calculation_detail'],
'base_value' => $result['input_parameters']['base_value'],
'is_free' => $buildingType->is_free,
'calculation_date' => now()->toDateTimeString(),
'total' => $result['total_retribution'],
'is_recalculated' => $recalculate,
]
];
} catch (\Exception $e) {
// Fallback to basic calculation if service fails
$this->warn("Calculation service failed for {$spatialPlanning->name}: {$e->getMessage()}. Using fallback calculation.");
// Basic fallback calculation
$totalAmount = $buildingType->is_free ? 0 : ($buildingArea * 50000);
// For recalculate mode in fallback, use current amount without adjustment
if ($recalculate) {
$this->warn("Fallback recalculate: Using current amount Rp{$totalAmount}");
}
return [
'amount' => $totalAmount,
'detail' => [
'building_type_id' => $buildingType->id,
'building_type_name' => $buildingType->name,
'building_type_code' => $buildingType->code,
'building_area' => $buildingArea,
'floor_number' => $floorNumber,
'building_function' => $spatialPlanning->building_function,
'calculation_method' => 'fallback',
'error_message' => $e->getMessage(),
'is_free' => $buildingType->is_free,
'calculation_date' => now()->toDateTimeString(),
'total' => $totalAmount,
'is_recalculated' => $recalculate,
]
];
}
}
}

View File

@@ -1,40 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Services\ServiceSIMBG;
use Illuminate\Console\Command;
use \Illuminate\Support\Facades\Log;
class ExecuteScraping extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:execute-scraping';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Execure scraping service daily every 12 pm';
/**
* Execute the console command.
*/
private $service_simbg;
public function __construct(ServiceSIMBG $service_simbg){
$this->service_simbg = $service_simbg;
parent::__construct();
}
public function handle()
{
Log::info("running scheduler daily scraping");
$this->service_simbg->syncTaskList();
}
}

View File

@@ -0,0 +1,790 @@
<?php
namespace App\Console\Commands;
use App\Models\SpatialPlanning;
use Illuminate\Console\Command;
use PhpOffice\PhpSpreadsheet\IOFactory;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use Exception;
class InjectSpatialPlanningsData extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'spatial-planning:inject
{--file=storage/app/public/templates/2025.xlsx : Path to Excel file}
{--sheet=0 : Sheet index to read from}
{--dry-run : Run without actually inserting data}
{--debug : Show Excel content for debugging}
{--truncate : Clear existing data before import}
{--no-truncate : Skip truncation (keep existing data)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Inject spatial planning data from Excel file with BCR area calculation';
/**
* Execute the console command.
*/
public function handle()
{
try {
$filePath = $this->option('file');
$sheetIndex = (int) $this->option('sheet');
$isDryRun = $this->option('dry-run');
$isDebug = $this->option('debug');
$shouldTruncate = $this->option('truncate');
$noTruncate = $this->option('no-truncate');
if (!file_exists($filePath)) {
$this->error("File not found: {$filePath}");
return 1;
}
$this->info("Reading Excel file: {$filePath}");
$this->info("Sheet index: {$sheetIndex}");
if ($isDryRun) {
$this->warn("DRY RUN MODE - No data will be inserted");
}
// Check existing data
$existingCount = DB::table('spatial_plannings')->count();
if ($existingCount > 0) {
$this->info("Found {$existingCount} existing spatial planning records");
} else {
$this->info('No existing spatial planning data found');
}
// Handle truncation logic
$willTruncate = false;
if ($shouldTruncate) {
$willTruncate = true;
$this->info('Truncation requested via --truncate option');
} elseif ($noTruncate) {
$willTruncate = false;
$this->info('Truncation skipped via --no-truncate option');
} else {
// Default behavior: ask user if not in dry run mode
if (!$isDryRun) {
$willTruncate = $this->confirm('Do you want to clear existing spatial planning data before import?');
} else {
$willTruncate = false;
$this->info('DRY RUN MODE - Truncation will be skipped');
}
}
// Confirm truncation if not in dry run mode and truncation is requested
if ($willTruncate && !$isDryRun) {
if (!$this->confirm('This will delete all existing spatial planning data and related retribution calculations. Continue?')) {
$this->info('Operation cancelled.');
return 0;
}
}
// Truncate all related data properly
if ($willTruncate && !$isDryRun) {
$this->info('Truncating spatial planning data and related retribution calculations...');
try {
// Disable foreign key checks for safe truncation
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
// 1. Delete calculable retributions for spatial plannings (polymorphic relationship)
$deletedCalculableRetributions = DB::table('calculable_retributions')
->where('calculable_type', 'App\\Models\\SpatialPlanning')
->count();
if ($deletedCalculableRetributions > 0) {
DB::table('calculable_retributions')
->where('calculable_type', 'App\\Models\\SpatialPlanning')
->delete();
$this->info("Deleted {$deletedCalculableRetributions} calculable retributions for spatial plannings.");
}
// 2. Truncate spatial plannings table
DB::table('spatial_plannings')->truncate();
$this->info('Spatial plannings table truncated successfully.');
// Re-enable foreign key checks
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
$this->info('All spatial planning data and related retribution calculations cleared successfully.');
} catch (Exception $e) {
// Make sure to re-enable foreign key checks even on error
try {
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
} catch (Exception $fkError) {
$this->error('Failed to re-enable foreign key checks: ' . $fkError->getMessage());
}
$this->error('Failed to truncate spatial planning data: ' . $e->getMessage());
return 1;
}
} elseif ($willTruncate && $isDryRun) {
$this->info('DRY RUN MODE - Would truncate spatial planning data and related retribution calculations');
} else {
$this->info('Keeping existing data (no truncation)');
}
$spreadsheet = IOFactory::load($filePath);
$worksheet = $spreadsheet->getSheet($sheetIndex);
$rows = $worksheet->toArray(null, true, true, true);
if ($isDebug) {
$this->info("=== EXCEL CONTENT DEBUG ===");
foreach (array_slice($rows, 0, 20) as $index => $row) {
if (!empty(array_filter($row))) {
$this->line("Row $index: " . json_encode($row));
}
}
$this->info("=== END DEBUG ===");
}
// Find BCR percentages from last rows (columns D and E)
$bcrPercentages = $this->findBcrPercentages($rows);
$this->info("Found BCR Percentages: " . json_encode($bcrPercentages));
// Process data by sections
$sections = $this->processSections($rows, $bcrPercentages, $isDebug);
$this->info("Found " . count($sections) . " sections");
$totalInserted = 0;
foreach ($sections as $sectionIndex => $section) {
$this->info("Processing Section " . ($sectionIndex + 1) . ": " . $section['applicant_name']);
// Gudang/pergudangan keywords successfully added to Fungsi Usaha classification
if (!$isDryRun) {
$inserted = $this->insertSpatialPlanningData($section);
$totalInserted += $inserted;
$this->info("Inserted {$inserted} record for this section");
} else {
$this->info("Would insert 1 record for this section");
}
}
if (!$isDryRun) {
$this->info("Successfully inserted {$totalInserted} spatial planning records");
// Show summary of what was done
$finalCount = DB::table('spatial_plannings')->count();
$this->info("Final spatial planning records count: {$finalCount}");
if ($willTruncate) {
$this->info("✅ Data import completed with truncation");
} else {
$this->info("✅ Data import completed (existing data preserved)");
}
} else {
$this->info("Dry run completed. Total records that would be inserted: " . count($sections));
if ($willTruncate) {
$this->info("Would truncate existing data before import");
} else {
$this->info("Would preserve existing data during import");
}
}
return 0;
} catch (Exception $e) {
$this->error("Error: " . $e->getMessage());
Log::error("InjectSpatialPlanningsData failed", ['error' => $e->getMessage()]);
return 1;
}
}
/**
* Find BCR percentages from last rows in columns D and E
*/
private function findBcrPercentages(array $rows): array
{
$bcrPercentages = [];
// Look for BCR percentages in the last few rows
$totalRows = count($rows);
$searchRows = max(1, $totalRows - 10); // Search last 10 rows
for ($i = $totalRows; $i >= $searchRows; $i--) {
if (isset($rows[$i]['D']) && isset($rows[$i]['E'])) {
$valueD = $this->cleanNumericValue($rows[$i]['D']);
$valueE = $this->cleanNumericValue($rows[$i]['E']);
// Check if these look like percentages (between 0 and 100)
if ($valueD > 0 && $valueD <= 100 && $valueE > 0 && $valueE <= 100) {
$bcrPercentages['D'] = $valueD;
$bcrPercentages['E'] = $valueE;
break;
}
}
}
// Default values if not found
if (empty($bcrPercentages)) {
$bcrPercentages = ['D' => 60, 'E' => 40]; // Default BCR percentages
}
return $bcrPercentages;
}
/**
* Process data by sections (each applicant)
*/
private function processSections(array $rows, array $bcrPercentages, bool $isDebug): array
{
$sections = [];
$currentSection = null;
$currentSectionNumber = null;
$sectionData = [];
foreach ($rows as $rowIndex => $row) {
// Skip empty rows
if (empty(array_filter($row))) {
continue;
}
if ($isDebug) {
$this->line("Checking row $rowIndex: " . substr(json_encode($row), 0, 100) . "...");
}
// Check if this is a new section (applicant)
if ($this->isNewSection($row)) {
if ($isDebug) {
$this->info("Found new section at row $rowIndex");
}
// Save previous section if exists
if ($currentSection && !empty($sectionData)) {
$sections[] = [
'applicant_name' => $currentSection,
'section_number' => $currentSectionNumber,
'data' => $sectionData
];
if ($isDebug) {
$this->info("Saved section: $currentSection with " . count($sectionData) . " data rows");
}
}
// Start new section
$currentSectionNumber = trim($row['A'] ?? ''); // Store section number
$currentSection = $this->extractApplicantName($row);
$sectionData = [];
// Also process the header row itself for F, G, H data
$headerRow = $this->processDataRow($row, $bcrPercentages);
if ($headerRow) {
$sectionData[] = $headerRow;
}
if ($isDebug) {
$this->info("Starting new section: $currentSection");
$this->line(" Header F: " . ($row['F'] ?? 'null'));
$this->line(" Header G: " . ($row['G'] ?? 'null'));
$this->line(" Header H: " . ($row['H'] ?? 'null'));
}
} elseif ($currentSection && $this->isDataRow($row)) {
if ($isDebug) {
$this->line("Found data row for section: $currentSection");
$this->line(" Column D: " . ($row['D'] ?? 'null'));
$this->line(" Column E: " . ($row['E'] ?? 'null'));
$this->line(" Column F: " . ($row['F'] ?? 'null'));
$this->line(" Column G: " . ($row['G'] ?? 'null'));
$this->line(" Column H: " . ($row['H'] ?? 'null'));
}
// Add data to current section
$processedRow = $this->processDataRow($row, $bcrPercentages);
if ($processedRow) {
$sectionData[] = $processedRow;
}
}
}
// Add last section
if ($currentSection && !empty($sectionData)) {
$sections[] = [
'applicant_name' => $currentSection,
'section_number' => $currentSectionNumber,
'data' => $sectionData
];
}
return $sections;
}
/**
* Check if row indicates a new section/applicant
*/
private function isNewSection(array $row): bool
{
// Look for patterns that indicate a new applicant
$firstCell = trim($row['A'] ?? '');
// Check for pattern like "55 / 1565", "56 / 1543", etc.
return !empty($firstCell) && preg_match('/^\d+\s*\/\s*\d+$/', $firstCell);
}
/**
* Extract applicant name from section header
*/
private function extractApplicantName(array $row): string
{
// Row A contains number like "55 / 1565", Row B contains name and phone
$numberPart = trim($row['A'] ?? '');
$namePart = trim($row['B'] ?? '');
// Extract name from column B (remove phone number part)
if (!empty($namePart)) {
// Remove phone number pattern "No Telpon : xxxxx"
$name = preg_replace('/\s*No Telpon\s*:\s*[\d\s\-\+\(\)]+.*$/i', '', $namePart);
$name = trim($name);
return !empty($name) ? $name : $numberPart;
}
return $numberPart ?: 'Unknown Applicant';
}
/**
* Check if row contains data
*/
private function isDataRow(array $row): bool
{
// Check if row has data we're interested in
$columnD = trim($row['D'] ?? '');
$columnE = trim($row['E'] ?? '');
$columnF = trim($row['F'] ?? '');
$columnG = trim($row['G'] ?? '');
$columnH = trim($row['H'] ?? '');
// Look for important data patterns in column D
$importantPatterns = [
'A. Total luas lahan',
'Total luas lahan',
'Total Luas Lahan',
'BCR Kawasan',
'E. BCR Kawasan',
'D. BCR Kawasan',
'KWT',
'Total KWT',
'KWT Perumahan',
'D. KWT Perumahan',
'E. KWT Perumahan',
'BCR',
'Koefisien Wilayah Terbangun'
];
foreach ($importantPatterns as $pattern) {
if (stripos($columnD, $pattern) !== false && !empty($columnE)) {
return true;
}
}
// Also check for location data
if (stripos($columnD, 'Desa') !== false) {
return true;
}
// Check if any of the important columns (F, G, H) have data
// We want to capture ALL non-empty data in these columns within a section
if (!empty($columnF) && trim($columnF) !== '') {
return true;
}
if (!empty($columnG) && trim($columnG) !== '') {
return true;
}
if (!empty($columnH) && trim($columnH) !== '') {
return true;
}
return false;
}
/**
* Process a data row and calculate area using BCR formula
*/
private function processDataRow(array $row, array $bcrPercentages): ?array
{
try {
$columnD = trim($row['D'] ?? '');
$columnE = trim($row['E'] ?? '');
$columnF = trim($row['F'] ?? '');
$columnG = trim($row['G'] ?? '');
$columnH = trim($row['H'] ?? '');
$landArea = 0;
$bcrPercentage = $bcrPercentages['D'] ?? 60; // Default BCR percentage
$location = '';
// Extract land area if this is a "Total luas lahan" row
if (stripos($columnD, 'Total luas lahan') !== false ||
stripos($columnD, 'A. Total luas lahan') !== false) {
$landArea = $this->cleanNumericValue($columnE);
}
// Extract BCR percentage if this is a BCR row - comprehensive detection
if (stripos($columnD, 'BCR Kawasan') !== false ||
stripos($columnD, 'E. BCR Kawasan') !== false ||
stripos($columnD, 'D. BCR Kawasan') !== false ||
stripos($columnD, 'KWT Perumahan') !== false ||
stripos($columnD, 'D. KWT Perumahan') !== false ||
stripos($columnD, 'E. KWT Perumahan') !== false ||
stripos($columnD, 'KWT') !== false ||
(stripos($columnD, 'BCR') !== false && stripos($columnE, '%') !== false) ||
stripos($columnD, 'Koefisien Wilayah Terbangun') !== false) {
$bcrValue = $this->cleanNumericValue($columnE);
if ($bcrValue > 0 && $bcrValue <= 100) {
$bcrPercentage = $bcrValue;
}
}
// Get location from village/subdistrict info (previous rows in the section)
if (stripos($columnD, 'Desa') !== false) {
$location = $columnD;
}
// Calculate area: total luas lahan dikali persentase BCR
$calculatedArea = $landArea > 0 && $bcrPercentage > 0 ?
round($landArea * ($bcrPercentage / 100), 2) : 0;
return [
'data_type' => $columnD,
'value' => $columnE,
'land_area' => $landArea,
'bcr_percentage' => $bcrPercentage,
'calculated_area' => $calculatedArea,
'location' => $location,
'no_tapak' => !empty($columnF) ? $columnF : null,
'no_skkl' => !empty($columnG) ? $columnG : null,
'no_ukl' => !empty($columnH) ? $columnH : null,
'raw_data' => $row
];
} catch (Exception $e) {
Log::warning("Error processing row", ['row' => $row, 'error' => $e->getMessage()]);
return null;
}
}
/**
* Insert spatial planning data
*/
private function insertSpatialPlanningData(array $section): int
{
try {
// Process section data to extract key values
$sectionData = $this->consolidateSectionData($section);
if (empty($sectionData) || !$sectionData['has_valid_data']) {
$this->warn("No valid data found for section: " . $section['applicant_name']);
return 0;
}
SpatialPlanning::create([
'name' => $section['applicant_name'],
'number' => $section['section_number'], // Kolom A - section number
'location' => $sectionData['location'], // Column C from header row
'land_area' => $sectionData['land_area'],
'area' => $sectionData['calculated_area'],
'building_function' => $sectionData['building_function'], // Determined from activities
'sub_building_function' => $sectionData['sub_building_function'], // UMKM or Usaha Besar
'activities' => $sectionData['activities'], // Activities from column D of first row
'site_bcr' => $sectionData['bcr_percentage'],
'no_tapak' => $sectionData['no_tapak'],
'no_skkl' => $sectionData['no_skkl'],
'no_ukl' => $sectionData['no_ukl'],
'date' => now()->format('Y-m-d'),
'created_at' => now(),
'updated_at' => now()
]);
return 1;
} catch (Exception $e) {
Log::error("Error inserting spatial planning data", [
'section' => $section['applicant_name'],
'error' => $e->getMessage()
]);
$this->warn("Failed to insert record for: " . $section['applicant_name']);
return 0;
}
}
/**
* Consolidate section data into a single record
*/
private function consolidateSectionData(array $section): array
{
$landArea = 0;
$bcrPercentage = 60; // Default from Excel file
$location = '';
$activities = ''; // Activities from column D of first row
$villages = [];
$noTapakValues = [];
$noSKKLValues = [];
$noUKLValues = [];
// Get activities from first row (header row) column D
if (!empty($section['data']) && !empty($section['data'][0]['data_type'])) {
$activities = $section['data'][0]['data_type']; // Column D data
}
// Get location from first row (header row) column C (alamat)
// We need to get this from raw data since processDataRow doesn't capture column C
if (!empty($section['data']) && !empty($section['data'][0]['raw_data']['C'])) {
$location = trim($section['data'][0]['raw_data']['C']);
}
foreach ($section['data'] as $dataRow) {
// Extract land area
if ($dataRow['land_area'] > 0) {
$landArea = $dataRow['land_area'];
}
// Extract BCR percentage - prioritize specific BCR from this section
// Always use section-specific BCR if found, regardless of value
if ($dataRow['bcr_percentage'] > 0 && $dataRow['bcr_percentage'] <= 100) {
$bcrPercentage = $dataRow['bcr_percentage'];
}
// Extract additional location info from village/subdistrict data if main location is empty
if (empty($location) && !empty($dataRow['location'])) {
$villages[] = trim(str_replace('Desa ', '', $dataRow['location']));
}
// Collect no_tapak values
if (!empty($dataRow['no_tapak']) && !in_array($dataRow['no_tapak'], $noTapakValues)) {
$noTapakValues[] = $dataRow['no_tapak'];
}
// Collect no_skkl values
if (!empty($dataRow['no_skkl']) && !in_array($dataRow['no_skkl'], $noSKKLValues)) {
$noSKKLValues[] = $dataRow['no_skkl'];
}
// Collect no_ukl values
if (!empty($dataRow['no_ukl']) && !in_array($dataRow['no_ukl'], $noUKLValues)) {
$noUKLValues[] = $dataRow['no_ukl'];
}
}
// Use first village as fallback location if main location is empty
if (empty($location)) {
$location = !empty($villages) ? $villages[0] : 'Unknown Location';
}
// Merge multiple values with | separator
$noTapak = !empty($noTapakValues) ? implode('|', $noTapakValues) : null;
$noSKKL = !empty($noSKKLValues) ? implode('|', $noSKKLValues) : null;
$noUKL = !empty($noUKLValues) ? implode('|', $noUKLValues) : null;
// Calculate area using BCR formula: land_area * (bcr_percentage / 100)
$calculatedArea = $landArea > 0 && $bcrPercentage > 0 ?
round($landArea * ($bcrPercentage / 100), 2) : 0;
// Determine building_function and sub_building_function based on activities and applicant name
$buildingFunction = 'Mixed Development'; // Default
$subBuildingFunction = null;
// Get applicant name for PT validation
$applicantName = $section['applicant_name'] ?? '';
$isCompany = (strpos($applicantName, 'PT ') === 0 || strpos($applicantName, 'PT.') === 0);
// Activity-based classification (priority over PT validation for specific activities)
if (!empty($activities)) {
$activitiesLower = strtolower($activities);
// 1. FUNGSI KEAGAMAAN
if (strpos($activitiesLower, 'masjid') !== false ||
strpos($activitiesLower, 'gereja') !== false ||
strpos($activitiesLower, 'pura') !== false ||
strpos($activitiesLower, 'vihara') !== false ||
strpos($activitiesLower, 'klenteng') !== false ||
strpos($activitiesLower, 'tempat ibadah') !== false ||
strpos($activitiesLower, 'keagamaan') !== false ||
strpos($activitiesLower, 'mushola') !== false) {
$buildingFunction = 'Fungsi Keagamaan';
$subBuildingFunction = 'Fungsi Keagamaan';
}
// 2. FUNGSI HUNIAN (PERUMAHAN) - PRIORITY HIGHER THAN PT VALIDATION
elseif (strpos($activitiesLower, 'perumahan') !== false ||
strpos($activitiesLower, 'perumhan') !== false ||
strpos($activitiesLower, 'perum') !== false ||
strpos($activitiesLower, 'rumah') !== false ||
strpos($activitiesLower, 'hunian') !== false ||
strpos($activitiesLower, 'residence') !== false ||
strpos($activitiesLower, 'residential') !== false ||
strpos($activitiesLower, 'housing') !== false ||
strpos($activitiesLower, 'town') !== false) {
$buildingFunction = 'Fungsi Hunian';
// Determine housing type based on area and keywords
if (strpos($activitiesLower, 'mbr') !== false ||
strpos($activitiesLower, 'masyarakat berpenghasilan rendah') !== false ||
strpos($activitiesLower, 'sederhana') !== false ||
($landArea > 0 && $landArea < 2000)) { // Small area indicates MBR
$subBuildingFunction = 'Rumah Tinggal Deret (MBR) dan Rumah Tinggal Tunggal (MBR)';
}
elseif ($landArea > 0 && $landArea < 100) {
$subBuildingFunction = 'Sederhana <100';
}
elseif ($landArea > 0 && $landArea > 100) {
$subBuildingFunction = 'Tidak Sederhana >100';
}
else {
$subBuildingFunction = 'Tidak Sederhana >100'; // Default for housing
}
}
// 3. FUNGSI SOSIAL BUDAYA
elseif (strpos($activitiesLower, 'sekolah') !== false ||
strpos($activitiesLower, 'rumah sakit') !== false ||
strpos($activitiesLower, 'puskesmas') !== false ||
strpos($activitiesLower, 'klinik') !== false ||
strpos($activitiesLower, 'universitas') !== false ||
strpos($activitiesLower, 'kampus') !== false ||
strpos($activitiesLower, 'pendidikan') !== false ||
strpos($activitiesLower, 'kesehatan') !== false ||
strpos($activitiesLower, 'sosial') !== false ||
strpos($activitiesLower, 'budaya') !== false ||
strpos($activitiesLower, 'museum') !== false ||
strpos($activitiesLower, 'tower') !== false ||
strpos($activitiesLower, 'perpustakaan') !== false) {
$buildingFunction = 'Fungsi Sosial Budaya';
$subBuildingFunction = 'Fungsi Sosial Budaya';
}
// 4. FUNGSI USAHA
elseif (strpos($activitiesLower, 'perdagangan') !== false ||
strpos($activitiesLower, 'dagang') !== false ||
strpos($activitiesLower, 'toko') !== false ||
strpos($activitiesLower, 'usaha') !== false ||
strpos($activitiesLower, 'komersial') !== false ||
strpos($activitiesLower, 'pabrik') !== false ||
strpos($activitiesLower, 'industri') !== false ||
strpos($activitiesLower, 'manufaktur') !== false ||
strpos($activitiesLower, 'bisnis') !== false ||
strpos($activitiesLower, 'resto') !== false ||
strpos($activitiesLower, 'villa') !== false ||
strpos($activitiesLower, 'vila') !== false ||
strpos($activitiesLower, 'gudang') !== false ||
strpos($activitiesLower, 'pergudangan') !== false ||
strpos($activitiesLower, 'kolam renang') !== false ||
strpos($activitiesLower, 'minimarket') !== false ||
strpos($activitiesLower, 'supermarket') !== false ||
strpos($activitiesLower, 'perdaganagan') !== false ||
strpos($activitiesLower, 'waterpark') !== false ||
strpos($activitiesLower, 'pasar') !== false ||
strpos($activitiesLower, 'kantor') !== false) {
$buildingFunction = 'Fungsi Usaha';
// Determine business size based on land area for non-PT businesses
if ($landArea > 0 && $landArea > 500) { // > 500 m² considered large business
$subBuildingFunction = 'Usaha Besar (Non-Mikro)';
} else {
$subBuildingFunction = 'UMKM'; // For small individual businesses
}
}
// 5. FUNGSI CAMPURAN
elseif (strpos($activitiesLower, 'campuran') !== false ||
strpos($activitiesLower, 'mixed') !== false ||
strpos($activitiesLower, 'mix') !== false ||
strpos($activitiesLower, 'multi') !== false) {
$buildingFunction = 'Fungsi Campuran (lebih dari 1)';
// Determine mixed use size
if ($landArea > 0 && $landArea > 3000) { // > 3000 m² considered large mixed use
$subBuildingFunction = 'Campuran Besar';
} else {
$subBuildingFunction = 'Campuran Kecil';
}
}
// If no specific activity detected, fall back to PT validation
else {
// PT Company validation - PT/PT. automatically classified as Fungsi Usaha
if ($isCompany) {
$buildingFunction = 'Fungsi Usaha';
// For PT companies: area-based classification
if ($landArea > 0 && $landArea < 500) { // < 500 m² for PT = Non-Mikro (since PT is already established business)
$subBuildingFunction = 'Usaha Besar (Non-Mikro)';
} elseif ($landArea >= 500) { // >= 500 m² for PT = Large Business
$subBuildingFunction = 'Usaha Besar (Non-Mikro)';
} else {
$subBuildingFunction = 'Usaha Besar (Non-Mikro)'; // Default for PT
}
}
}
}
// If no activities, fall back to PT validation
else {
// PT Company validation - PT/PT. automatically classified as Fungsi Usaha
if ($isCompany) {
$buildingFunction = 'Fungsi Usaha';
// For PT companies: area-based classification
if ($landArea > 0 && $landArea < 500) { // < 500 m² for PT = Non-Mikro (since PT is already established business)
$subBuildingFunction = 'Usaha Besar (Non-Mikro)';
} elseif ($landArea >= 500) { // >= 500 m² for PT = Large Business
$subBuildingFunction = 'Usaha Besar (Non-Mikro)';
} else {
$subBuildingFunction = 'Usaha Besar (Non-Mikro)'; // Default for PT
}
}
}
return [
'land_area' => $landArea,
'bcr_percentage' => $bcrPercentage,
'calculated_area' => $calculatedArea,
'location' => $location,
'activities' => $activities, // Activities from column D of first row
'building_function' => $buildingFunction,
'sub_building_function' => $subBuildingFunction,
'no_tapak' => $noTapak,
'no_skkl' => $noSKKL,
'no_ukl' => $noUKL,
'has_valid_data' => $landArea > 0 // Flag untuk validasi
];
}
/**
* Clean and convert string to numeric value
*/
private function cleanNumericValue($value): float
{
if (is_numeric($value)) {
return (float) $value;
}
// Remove non-numeric characters except decimal points and commas
$cleaned = preg_replace('/[^0-9.,]/', '', $value);
// Handle different decimal separators
if (strpos($cleaned, ',') !== false && strpos($cleaned, '.') !== false) {
// Both comma and dot present, assume comma is thousands separator
$cleaned = str_replace(',', '', $cleaned);
} elseif (strpos($cleaned, ',') !== false) {
// Only comma present, assume it's decimal separator
$cleaned = str_replace(',', '.', $cleaned);
}
return is_numeric($cleaned) ? (float) $cleaned : 0;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Console\Commands;
use App\Services\ServiceGoogleSheet;
use Illuminate\Console\Command;
class ScrapingLeaderData extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:scraping-leader-data';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Scraping leader data from google spreadsheet and save to database';
/**
* Execute the console command.
*/
public function handle()
{
$service_google_sheet = app(ServiceGoogleSheet::class);
$service_google_sheet->sync_leader_data();
$this->info('Leader data synced successfully');
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace App\Console\Commands;
use App\Jobs\ScrapingDataJob;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class StartScrapingData extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:start-scraping-data
{--confirm : Skip confirmation prompt}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Start the optimized scraping data job (Google Sheet -> PBG Task -> Details)';
/**
* Execute the console command.
*/
public function handle()
{
$this->info('🚀 Starting Optimized Scraping Data Job');
$this->info('=====================================');
if (!$this->option('confirm')) {
$this->warn('⚠️ This will start a comprehensive data scraping process:');
$this->line(' 1. Google Sheet data scraping');
$this->line(' 2. PBG Task parent data scraping');
$this->line(' 3. Detailed task information scraping');
$this->line(' 4. BigData resume generation');
$this->newLine();
if (!$this->confirm('Do you want to continue?')) {
$this->info('Operation cancelled.');
return 0;
}
}
try {
// Dispatch the optimized job
$job = new ScrapingDataJob();
dispatch($job);
Log::info('ScrapingDataJob dispatched via command', [
'command' => $this->signature,
'user' => $this->option('confirm') ? 'auto' : 'manual'
]);
$this->info('✅ Scraping Data Job has been dispatched to the scraping queue!');
$this->newLine();
$this->info('📊 Monitor the job with:');
$this->line(' php artisan queue:monitor scraping');
$this->newLine();
$this->info('📜 View detailed logs with:');
$this->line(' tail -f /var/log/supervisor/sibedas-queue-scraping.log | grep "SCRAPING DATA JOB"');
$this->newLine();
$this->info('🔍 Check ImportDatasource status:');
$this->line(' docker-compose -f docker-compose.local.yml exec app php artisan tinker --execute="App\\Models\\ImportDatasource::latest()->first();"');
} catch (\Exception $e) {
$this->error('❌ Failed to dispatch ScrapingDataJob: ' . $e->getMessage());
Log::error('Failed to dispatch ScrapingDataJob via command', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return 1;
}
return 0;
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\ServiceGoogleSheet;
use App\Models\BigdataResume;
use App\Models\ImportDatasource;
use Illuminate\Support\Facades\Log;
class SyncDashboardPbg extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:sync-dashboard-pbg';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*/
public function handle()
{
$import_datasource = ImportDatasource::create([
'message' => 'Initiating sync dashboard pbg...',
'response_body' => null,
'status' => 'processing',
'start_time' => now(),
'failed_uuid' => null
]);
try {
BigdataResume::generateResumeData($import_datasource->id, date('Y'), "simbg");
$import_datasource->update([
'status' => 'success',
'message' => 'Sync dashboard pbg completed successfully.',
'finish_time' => now()
]);
} catch (\Exception $e) {
Log::error('Sync dashboard pbg failed: ' . $e->getMessage(), ['trace' => $e->getTraceAsString()]);
// Update status to failed
if (isset($import_datasource)) {
$import_datasource->update([
'status' => 'failed',
'message' => 'Sync dashboard pbg failed.',
'finish_time' => now()
]);
}
}
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace App\Console\Commands;
use App\Services\ServiceGoogleSheet;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Exception;
class SyncGoogleSheetData extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'sync:google-sheet {--type=all : Specify sync type (all, google-sheet, big-data, leader)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sync data from Google Sheets to database';
/**
* Execute the console command.
*/
public function handle()
{
$type = $this->option('type');
$this->info('Starting Google Sheet data synchronization...');
$this->info("Sync type: {$type}");
try {
$service = new ServiceGoogleSheet();
switch ($type) {
case 'google-sheet':
$this->info('Syncing Google Sheet data...');
$service->sync_google_sheet_data();
$this->info('✅ Google Sheet data synchronized successfully!');
break;
case 'big-data':
$this->info('Syncing Big Data...');
$service->sync_big_data();
$this->info('✅ Big Data synchronized successfully!');
break;
case 'leader':
$this->info('Syncing Leader data...');
$result = $service->sync_leader_data();
$this->info('✅ Leader data synchronized successfully!');
$this->table(['Section', 'Total', 'Nominal'], collect($result)->map(function($item, $key) {
// Convert nominal to numeric before formatting
$nominal = $item['nominal'] ?? 0;
if (is_string($nominal)) {
// Remove dots and convert to float
$nominal = (float) str_replace('.', '', $nominal);
}
return [
$key,
$item['total'] ?? 'N/A',
number_format((float) $nominal, 0, ',', '.')
];
})->toArray());
break;
case 'all':
default:
$this->info('Syncing all data (Google Sheet + Big Data)...');
$service->run_service();
$this->info('✅ All data synchronized successfully!');
break;
}
$this->newLine();
$this->info('🚀 Synchronization completed successfully!');
} catch (Exception $e) {
$this->error('❌ Synchronization failed!');
$this->error("Error: {$e->getMessage()}");
Log::error('Google Sheet sync command failed', [
'error' => $e->getMessage(),
'type' => $type,
'trace' => $e->getTraceAsString()
]);
return Command::FAILURE;
}
return Command::SUCCESS;
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Console\Commands;
use App\Services\ServiceGoogleSheet;
use Illuminate\Console\Command;
class SyncPbgTaskPayments extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'sync:pbg-payments';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sync PBG task payments from Google Sheets Sheet Data';
/**
* Execute the console command.
*/
public function handle()
{
$this->info('🚀 Starting PBG Task Payments sync...');
$this->newLine();
try {
$service = new ServiceGoogleSheet();
// Show progress bar
$this->info('📊 Fetching data from Google Sheets...');
$result = $service->sync_pbg_task_payments();
// Display results
$this->newLine();
$this->info('✅ Sync completed successfully!');
$this->newLine();
$this->table(
['Metric', 'Value'],
[
['Inserted rows', $result['inserted'] ?? 0],
['Success', ($result['success'] ?? false) ? 'Yes' : 'No'],
]
);
$this->newLine();
$this->info('📝 Check Laravel logs for detailed information.');
return Command::SUCCESS;
} catch (\Exception $e) {
$this->newLine();
$this->error('❌ Sync failed!');
$this->error('Error: ' . $e->getMessage());
return Command::FAILURE;
}
}
}

View File

@@ -0,0 +1,265 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\RetributionCalculatorService;
use App\Models\BuildingType;
class TestRetributionCalculation extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'retribution:test
{--area= : Luas bangunan dalam m2}
{--floor= : Jumlah lantai (1-6)}
{--type= : ID atau kode building type}
{--all : Test semua building types}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Test perhitungan retribusi PBG dengan input luas bangunan dan tinggi lantai';
protected $calculatorService;
public function __construct(RetributionCalculatorService $calculatorService)
{
parent::__construct();
$this->calculatorService = $calculatorService;
}
/**
* Execute the console command.
*/
public function handle()
{
$this->info('🏢 SISTEM TEST PERHITUNGAN RETRIBUSI PBG');
$this->info('=' . str_repeat('=', 50));
// Test all building types if --all flag is used
if ($this->option('all')) {
return $this->testAllBuildingTypes();
}
// Get input parameters
$area = $this->getArea();
$floor = $this->getFloor();
$buildingTypeId = $this->getBuildingType();
if (!$area || !$floor || !$buildingTypeId) {
$this->error('❌ Parameter tidak lengkap!');
return 1;
}
// Perform calculation
$this->performCalculation($buildingTypeId, $floor, $area);
return 0;
}
protected function getArea()
{
$area = $this->option('area');
if (!$area) {
$area = $this->ask('📐 Masukkan luas bangunan (m²)');
}
if (!is_numeric($area) || $area <= 0) {
$this->error('❌ Luas bangunan harus berupa angka positif!');
return null;
}
return (float) $area;
}
protected function getFloor()
{
$floor = $this->option('floor');
if (!$floor) {
$floor = $this->ask('🏗️ Masukkan jumlah lantai (1-6)');
}
if (!is_numeric($floor) || $floor < 1 || $floor > 6) {
$this->error('❌ Jumlah lantai harus antara 1-6!');
return null;
}
return (int) $floor;
}
protected function getBuildingType()
{
$type = $this->option('type');
if (!$type) {
$this->showBuildingTypes();
$type = $this->ask('🏢 Masukkan ID atau kode building type');
}
// Try to find by ID first, then by code
$buildingType = null;
if (is_numeric($type)) {
$buildingType = BuildingType::find($type);
} else {
$buildingType = BuildingType::where('code', strtoupper($type))->first();
}
if (!$buildingType) {
$this->error('❌ Building type tidak ditemukan!');
return null;
}
return $buildingType->id;
}
protected function showBuildingTypes()
{
$this->info('📋 DAFTAR BUILDING TYPES:');
$this->line('');
$buildingTypes = BuildingType::with('indices')
->whereHas('indices') // Only types that have indices
->get();
$headers = ['ID', 'Kode', 'Nama', 'Coefficient', 'Free'];
$rows = [];
foreach ($buildingTypes as $type) {
$rows[] = [
$type->id,
$type->code,
$type->name,
$type->indices ? number_format($type->indices->coefficient, 4) : 'N/A',
$type->is_free ? '✅' : '❌'
];
}
$this->table($headers, $rows);
$this->line('');
}
protected function performCalculation($buildingTypeId, $floor, $area)
{
try {
// Round area to 2 decimal places to match database storage format
$roundedArea = round($area, 2);
$result = $this->calculatorService->calculate($buildingTypeId, $floor, $roundedArea, false);
$this->displayResults($result, $roundedArea, $floor);
} catch (\Exception $e) {
$this->error('❌ Error: ' . $e->getMessage());
return 1;
}
}
protected function displayResults($result, $area, $floor)
{
$this->info('');
$this->info('📊 HASIL PERHITUNGAN RETRIBUSI');
$this->info('=' . str_repeat('=', 40));
// Building info
$this->line('🏢 <fg=cyan>Building Type:</> ' . $result['building_type']['name']);
$this->line('📐 <fg=cyan>Luas Bangunan:</> ' . number_format($area, 0) . ' m²');
$this->line('🏗️ <fg=cyan>Jumlah Lantai:</> ' . $floor);
if (isset($result['building_type']['is_free']) && $result['building_type']['is_free']) {
$this->line('');
$this->info('🎉 GRATIS - Building type ini tidak dikenakan retribusi');
$this->line('💰 <fg=green>Total Retribusi: Rp 0</fg=green>');
return;
}
$this->line('');
// Parameters
$this->info('📋 PARAMETER PERHITUNGAN:');
$indices = $result['indices'];
$this->line('• Coefficient: ' . number_format($indices['coefficient'], 4));
$this->line('• IP Permanent: ' . number_format($indices['ip_permanent'], 4));
$this->line('• IP Complexity: ' . number_format($indices['ip_complexity'], 4));
$this->line('• Locality Index: ' . number_format($indices['locality_index'], 4));
$this->line('• Height Index: ' . number_format($result['input_parameters']['height_index'], 4));
$this->line('');
// Calculation steps
$this->info('🔢 LANGKAH PERHITUNGAN:');
$detail = $result['calculation_detail'];
$this->line('1. H5 Raw: ' . number_format($detail['h5_raw'], 6));
$this->line('2. H5 Rounded: ' . number_format($detail['h5'], 4));
$this->line('3. Main Calculation: Rp ' . number_format($detail['main'], 2));
$this->line('4. Infrastructure (50%): Rp ' . number_format($detail['infrastructure'], 2));
$this->line('');
// Final result
$this->info('💰 <fg=green>TOTAL RETRIBUSI: ' . $result['formatted_amount'] . '</fg=green>');
$this->line('📈 <fg=yellow>Per m²: Rp ' . number_format($result['total_retribution'] / $area, 2) . '</fg=yellow>');
}
protected function testAllBuildingTypes()
{
$area = round($this->option('area') ?: 100, 2);
$floor = $this->option('floor') ?: 2;
$this->info("🧪 TESTING SEMUA BUILDING TYPES");
$this->info("📐 Luas: {$area} m² | 🏗️ Lantai: {$floor}");
$this->info('=' . str_repeat('=', 60));
$buildingTypes = BuildingType::with('indices')
->whereHas('indices') // Only types that have indices
->orderBy('level')
->orderBy('name')
->get();
$headers = ['Kode', 'Nama', 'Coefficient', 'Total Retribusi', 'Per m²'];
$rows = [];
foreach ($buildingTypes as $type) {
try {
$result = $this->calculatorService->calculate($type->id, $floor, $area, false);
if ($type->is_free) {
$rows[] = [
$type->code,
$type->name,
'FREE',
'Rp 0',
'Rp 0'
];
} else {
$rows[] = [
$type->code,
$type->name,
number_format($result['indices']['coefficient'], 4),
'Rp ' . number_format($result['total_retribution'], 0),
'Rp ' . number_format($result['total_retribution'] / $area, 0)
];
}
} catch (\Exception $e) {
$rows[] = [
$type->code,
$type->name,
'ERROR',
$e->getMessage(),
'-'
];
}
}
$this->table($headers, $rows);
return 0;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Console\Commands;
use App\Models\PbgTask;
use App\Models\PbgTaskDetail;
use App\Models\PbgTaskIndexIntegrations;
use App\Models\PbgTaskPrasarana;
use App\Models\PbgTaskRetributions;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class TruncatePBGTable extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:truncate-pbg-table';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*/
public function handle()
{
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
PbgTask::truncate();
PbgTaskRetributions::truncate();
PbgTaskDetail::truncate();
PbgTaskIndexIntegrations::truncate();
PbgTaskPrasarana::truncate();
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
}
}

View File

@@ -0,0 +1,266 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Exception;
class TruncateSpatialPlanningData extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'spatial-planning:truncate
{--force : Force truncate without confirmation}
{--backup : Create backup before truncate}
{--dry-run : Show what would be truncated without actually doing it}
{--only-active : Only truncate active calculations}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Truncate spatial planning data with related calculable retributions and retribution calculations';
/**
* Execute the console command.
*/
public function handle()
{
try {
$force = $this->option('force');
$backup = $this->option('backup');
$dryRun = $this->option('dry-run');
$onlyActive = $this->option('only-active');
if ($dryRun) {
$this->warn('DRY RUN MODE - No data will be truncated');
}
// Check existing data
$this->showDataStatistics();
// Confirm truncation if not in force mode
if (!$force && !$dryRun) {
if (!$this->confirm('This will permanently delete all spatial planning data and related calculations. Continue?')) {
$this->info('Operation cancelled.');
return 0;
}
}
// Create backup if requested
if ($backup && !$dryRun) {
$this->createBackup();
}
// Perform truncation
if ($dryRun) {
$this->performDryRun();
} else {
$this->performTruncation($onlyActive);
}
// Show final statistics
if (!$dryRun) {
$this->showDataStatistics('AFTER');
}
return 0;
} catch (Exception $e) {
$this->error("Error: " . $e->getMessage());
Log::error("TruncateSpatialPlanningData failed", ['error' => $e->getMessage()]);
return 1;
}
}
/**
* Show data statistics
*/
private function showDataStatistics(string $prefix = 'BEFORE'): void
{
$this->info("=== {$prefix} TRUNCATION ===");
$spatialCount = DB::table('spatial_plannings')->count();
$calculableCount = DB::table('calculable_retributions')->count();
$activeCalculableCount = DB::table('calculable_retributions')->where('is_active', true)->count();
$calculationCount = DB::table('retribution_calculations')->count();
$this->table(
['Table', 'Total Records', 'Active Records'],
[
['spatial_plannings', $spatialCount, '-'],
['calculable_retributions', $calculableCount, $activeCalculableCount],
['retribution_calculations', $calculationCount, '-'],
]
);
// Show breakdown by building function
$buildingFunctionStats = DB::table('spatial_plannings')
->select('building_function', DB::raw('count(*) as total'))
->groupBy('building_function')
->get();
if ($buildingFunctionStats->isNotEmpty()) {
$this->info('Building Function Breakdown:');
foreach ($buildingFunctionStats as $stat) {
$this->line(" - {$stat->building_function}: {$stat->total} records");
}
}
$this->newLine();
}
/**
* Create backup of data
*/
private function createBackup(): void
{
$this->info('Creating backup...');
$timestamp = date('Y-m-d_H-i-s');
$backupPath = storage_path("backups/spatial_planning_backup_{$timestamp}");
// Create backup directory
if (!file_exists($backupPath)) {
mkdir($backupPath, 0755, true);
}
// Backup spatial plannings
$spatialData = DB::table('spatial_plannings')->get();
file_put_contents(
"{$backupPath}/spatial_plannings.json",
$spatialData->toJson(JSON_PRETTY_PRINT)
);
// Backup calculable retributions
$calculableData = DB::table('calculable_retributions')->get();
file_put_contents(
"{$backupPath}/calculable_retributions.json",
$calculableData->toJson(JSON_PRETTY_PRINT)
);
// Backup retribution calculations
$calculationData = DB::table('retribution_calculations')->get();
file_put_contents(
"{$backupPath}/retribution_calculations.json",
$calculationData->toJson(JSON_PRETTY_PRINT)
);
$this->info("Backup created at: {$backupPath}");
}
/**
* Perform dry run
*/
private function performDryRun(): void
{
$this->info('DRY RUN - Would truncate the following:');
$spatialCount = DB::table('spatial_plannings')->count();
$calculableCount = DB::table('calculable_retributions')->count();
$calculationCount = DB::table('retribution_calculations')->count();
$this->line(" - spatial_plannings: {$spatialCount} records");
$this->line(" - calculable_retributions: {$calculableCount} records");
$this->line(" - retribution_calculations: {$calculationCount} records");
$this->warn('No actual data was truncated (dry run mode)');
}
/**
* Perform actual truncation
*/
private function performTruncation(bool $onlyActive = false): void
{
$this->info('Starting truncation...');
try {
// Disable foreign key checks for safe truncation
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
if ($onlyActive) {
// Only truncate active calculations
$this->truncateActiveOnly();
} else {
// Truncate all data
$this->truncateAllData();
}
// Re-enable foreign key checks
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
$this->info('✅ Truncation completed successfully!');
} catch (Exception $e) {
// Make sure to re-enable foreign key checks even on error
try {
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
} catch (Exception $fkError) {
$this->error('Failed to re-enable foreign key checks: ' . $fkError->getMessage());
}
throw $e;
}
}
/**
* Truncate only active calculations
*/
private function truncateActiveOnly(): void
{
$this->info('Truncating only active calculations...');
// Delete active calculable retributions
$deletedActive = DB::table('calculable_retributions')
->where('is_active', true)
->delete();
$this->info("Deleted {$deletedActive} active calculable retributions");
// Delete orphaned retribution calculations
$deletedOrphaned = DB::table('retribution_calculations')
->whereNotExists(function ($query) {
$query->select(DB::raw(1))
->from('calculable_retributions')
->whereColumn('calculable_retributions.retribution_calculation_id', 'retribution_calculations.id');
})
->delete();
$this->info("Deleted {$deletedOrphaned} orphaned retribution calculations");
// Keep spatial plannings but remove their calculation relationships
$this->info('Spatial plannings data preserved (only calculations removed)');
}
/**
* Truncate all data
*/
private function truncateAllData(): void
{
$this->info('Truncating all data...');
// Get counts before truncation
$spatialCount = DB::table('spatial_plannings')->count();
$calculableCount = DB::table('calculable_retributions')->count();
$calculationCount = DB::table('retribution_calculations')->count();
// Truncate tables in correct order
DB::table('calculable_retributions')->truncate();
$this->info("Truncated calculable_retributions table ({$calculableCount} records)");
DB::table('retribution_calculations')->truncate();
$this->info("Truncated retribution_calculations table ({$calculationCount} records)");
DB::table('spatial_plannings')->truncate();
$this->info("Truncated spatial_plannings table ({$spatialCount} records)");
// Reset auto increment
DB::statement('ALTER TABLE calculable_retributions AUTO_INCREMENT = 1');
DB::statement('ALTER TABLE retribution_calculations AUTO_INCREMENT = 1');
DB::statement('ALTER TABLE spatial_plannings AUTO_INCREMENT = 1');
$this->info('Reset auto increment counters');
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Enums;
enum PbgTaskApplicationTypes: string
{
case PERSETUJUAN_BG = '1';
case PERUBAHAN_BG = '2';
case SLF_BB = '4';
case SLF = '5';
public static function labels(): array
{
return [
null => "Pilih Application Type",
self::PERSETUJUAN_BG->value => 'Persetujuan Bangunan Gedung',
self::PERUBAHAN_BG->value => 'Perubahan Bangunan Gedung',
self::SLF_BB->value => 'Sertifikat Laik Fungsi - Bangunan Baru',
self::SLF->value => 'Sertifikat Laik Fungsi',
];
}
public static function getLabel(?string $status): ?string
{
return self::labels()[$status] ?? null;
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Enums;
enum PbgTaskFilterData : string
{
case non_business = 'non-business';
case business = 'business';
case verified = 'verified';
case non_verified = 'non-verified';
case all = 'all';
case potention = 'potention';
case issuance_realization_pbg = 'issuance-realization-pbg';
case process_in_technical_office = 'process-in-technical-office';
case waiting_click_dpmptsp = 'waiting-click-dpmptsp';
case non_business_rab = 'non-business-rab';
case non_business_krk = 'non-business-krk';
case business_rab = 'business-rab';
case business_krk = 'business-krk';
case business_dlh = 'business-dlh';
public static function getAllOptions() : array {
return [
self::all->value => 'Semua Berkas',
self::business->value => 'Usaha',
self::non_business->value => 'Bukan Usaha',
self::verified->value => 'Terverifikasi',
self::non_verified->value => 'Belum Terverifikasi',
self::potention->value => 'Potensi',
self::issuance_realization_pbg->value => 'Realisasi PBG',
self::process_in_technical_office->value => 'Proses Di Dinas Teknis',
self::waiting_click_dpmptsp->value => 'Menunggu Klik DPMPTSP',
self::non_business_rab->value => 'Non Usaha - RAB',
self::non_business_krk->value => 'Non Usaha - KRK',
self::business_rab->value => 'Usaha - RAB',
self::business_krk->value => 'Usaha - KRK',
self::business_dlh->value => 'Usaha - DLH',
];
}
}

145
app/Enums/PbgTaskStatus.php Normal file
View File

@@ -0,0 +1,145 @@
<?php
namespace App\Enums;
enum PbgTaskStatus: int
{
case VERIFIKASI_KELENGKAPAN = 1;
case PERBAIKAN_DOKUMEN = 2;
case PERMOHONAN_DIBATALKAN = 3;
case MENUNGGU_PENUGASAN_TPT_TPA = 4;
case MENUNGGU_JADWAL_KONSULTASI = 5;
case PELAKSANAAN_KONSULTASI = 6;
case PERBAIKAN_DOKUMEN_KONSULTASI = 8;
case PERMOHONAN_DITOLAK = 9;
case PERHITUNGAN_RETRIBUSI = 10;
case MENUNGGU_PEMBAYARAN_RETRIBUSI = 14;
case VERIFIKASI_PEMBAYARAN_RETRIBUSI = 15;
case RETRIBUSI_TIDAK_SESUAI = 16;
case VERIFIKASI_SK_PBG = 18;
case PENERBITAN_SK_PBG = 19;
case SK_PBG_TERBIT = 20;
case PENERBITAN_SPPST = 24;
case PROSES_PENERBITAN_SKRD = 25;
case MENUNGGU_PENUGASAN_TPT = 26;
case VERIFIKASI_DATA_TPT = 27;
case SERTIFIKAT_SLF_TERBIT = 28;
public static function getStatuses(): array
{
return [
null => "Pilih Status",
self::VERIFIKASI_KELENGKAPAN->value => "Verifikasi Kelengkapan Dokumen",
self::PERBAIKAN_DOKUMEN->value => "Perbaikan Dokumen",
self::PERMOHONAN_DIBATALKAN->value => "Permohonan Dibatalkan",
self::MENUNGGU_PENUGASAN_TPT_TPA->value => "Menunggu Penugasan TPT/TPA",
self::MENUNGGU_JADWAL_KONSULTASI->value => "Menunggu Jadwal Konsultasi",
self::PELAKSANAAN_KONSULTASI->value => "Pelaksanaan Konsultasi",
self::PERBAIKAN_DOKUMEN_KONSULTASI->value => "Perbaikan Dokumen Konsultasi",
self::PERMOHONAN_DITOLAK->value => "Permohonan Ditolak",
self::PERHITUNGAN_RETRIBUSI->value => "Perhitungan Retribusi",
self::MENUNGGU_PEMBAYARAN_RETRIBUSI->value => "Menunggu Pembayaran Retribusi",
self::VERIFIKASI_PEMBAYARAN_RETRIBUSI->value => "Verifikasi Pembayaran Retribusi",
self::RETRIBUSI_TIDAK_SESUAI->value => "Retribusi Tidak Sesuai",
self::VERIFIKASI_SK_PBG->value => "Verifikasi SK PBG",
self::PENERBITAN_SK_PBG->value => "Penerbitan SK PBG",
self::SK_PBG_TERBIT->value => "SK PBG Terbit",
self::PENERBITAN_SPPST->value => "Penerbitan SPPST",
self::PROSES_PENERBITAN_SKRD->value => "Proses Penerbitan SKRD",
self::MENUNGGU_PENUGASAN_TPT->value => "Menunggu Penugasan TPT",
self::VERIFIKASI_DATA_TPT->value => "Verifikasi Data TPT",
self::SERTIFIKAT_SLF_TERBIT->value => "Sertifikat SLF Terbit",
];
}
public static function getLabel(?int $status): ?string
{
return self::getStatuses()[$status] ?? null;
}
public static function getWaitingClickDpmptsp(): array
{
return [
self::MENUNGGU_PEMBAYARAN_RETRIBUSI->value,
self::PROSES_PENERBITAN_SKRD->value,
self::VERIFIKASI_PEMBAYARAN_RETRIBUSI->value
];
}
public static function getIssuanceRealizationPbg(): array
{
return [
self::PENERBITAN_SK_PBG->value,
self::SK_PBG_TERBIT->value,
self::VERIFIKASI_SK_PBG->value
];
}
public static function getProcessInTechnicalOffice(): array
{
return [
self::PENERBITAN_SPPST->value,
self::PERHITUNGAN_RETRIBUSI->value,
self::RETRIBUSI_TIDAK_SESUAI->value,
self::MENUNGGU_JADWAL_KONSULTASI->value,
self::MENUNGGU_PENUGASAN_TPT_TPA->value,
self::PELAKSANAAN_KONSULTASI->value
];
}
public static function getVerified(): array
{
return [
self::MENUNGGU_PEMBAYARAN_RETRIBUSI->value,
self::PROSES_PENERBITAN_SKRD->value,
self::VERIFIKASI_PEMBAYARAN_RETRIBUSI->value,
self::PENERBITAN_SK_PBG->value,
self::SK_PBG_TERBIT->value,
self::VERIFIKASI_SK_PBG->value,
self::PENERBITAN_SPPST->value,
self::PERHITUNGAN_RETRIBUSI->value,
self::RETRIBUSI_TIDAK_SESUAI->value,
self::MENUNGGU_JADWAL_KONSULTASI->value,
self::MENUNGGU_PENUGASAN_TPT_TPA->value,
self::PELAKSANAAN_KONSULTASI->value
];
}
public static function getNonVerified(): array
{
return [
self::VERIFIKASI_KELENGKAPAN->value,
self::PERBAIKAN_DOKUMEN->value,
self::PERBAIKAN_DOKUMEN_KONSULTASI->value,
];
}
public static function getPotention(): array
{
return [
self::MENUNGGU_PEMBAYARAN_RETRIBUSI->value,
self::PROSES_PENERBITAN_SKRD->value,
self::VERIFIKASI_PEMBAYARAN_RETRIBUSI->value,
self::PENERBITAN_SK_PBG->value,
self::SK_PBG_TERBIT->value,
self::VERIFIKASI_SK_PBG->value,
self::PENERBITAN_SPPST->value,
self::PERHITUNGAN_RETRIBUSI->value,
self::RETRIBUSI_TIDAK_SESUAI->value,
self::MENUNGGU_JADWAL_KONSULTASI->value,
self::MENUNGGU_PENUGASAN_TPT_TPA->value,
self::PELAKSANAAN_KONSULTASI->value,
self::VERIFIKASI_KELENGKAPAN->value,
self::PERBAIKAN_DOKUMEN->value,
self::PERBAIKAN_DOKUMEN_KONSULTASI->value,
];
}
public static function getRejected(): array
{
return [
self::PERMOHONAN_DITOLAK->value,
self::PERMOHONAN_DIBATALKAN->value
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Exports;
use App\Models\PbgTaskGoogleSheet;
use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
class DistrictPaymentRecapExport implements FromCollection, WithHeadings
{
/**
* @return \Illuminate\Support\Collection
*/
public function collection()
{
return PbgTaskGoogleSheet::select(
'kecamatan',
DB::raw('SUM(nilai_retribusi_keseluruhan_simbg) as total')
)
->groupBy('kecamatan')->get();
}
public function headings(): array{
return [
'Kecamatan',
'Total'
];
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace App\Exports;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use App\Models\PbgTask;
use App\Enums\PbgTaskFilterData;
class PbgTaskExport implements FromCollection, WithHeadings
{
protected $category;
protected $year;
public function __construct(string $category, int $year)
{
$this->category = $category;
$this->year = $year;
}
/**
* @return \Illuminate\Support\Collection
*/
public function collection()
{
$query = PbgTask::query()
->whereYear('task_created_at', $this->year);
// Menggunakan switch case karena lebih readable dan maintainable
// untuk multiple conditions yang berbeda
switch ($this->category) {
case PbgTaskFilterData::all->value:
// Tidak ada filter tambahan, ambil semua data
break;
case PbgTaskFilterData::business->value:
$query->where('application_type', 'business');
break;
case PbgTaskFilterData::non_business->value:
$query->where('application_type', 'non-business');
break;
case PbgTaskFilterData::verified->value:
$query->where('is_valid', true);
break;
case PbgTaskFilterData::non_verified->value:
$query->where('is_valid', false);
break;
case PbgTaskFilterData::potention->value:
$query->where('status', 'potention');
break;
case PbgTaskFilterData::issuance_realization_pbg->value:
$query->where('status', 'issuance-realization-pbg');
break;
case PbgTaskFilterData::process_in_technical_office->value:
$query->where('status', 'process-in-technical-office');
break;
case PbgTaskFilterData::waiting_click_dpmptsp->value:
$query->where('status', 'waiting-click-dpmptsp');
break;
case PbgTaskFilterData::non_business_rab->value:
$query->where('application_type', 'non-business')
->where('consultation_type', 'rab');
break;
case PbgTaskFilterData::non_business_krk->value:
$query->where('application_type', 'non-business')
->where('consultation_type', 'krk');
break;
case PbgTaskFilterData::business_rab->value:
$query->where('application_type', 'business')
->where('consultation_type', 'rab');
break;
case PbgTaskFilterData::business_krk->value:
$query->where('application_type', 'business')
->where('consultation_type', 'krk');
break;
case PbgTaskFilterData::business_dlh->value:
$query->where('application_type', 'business')
->where('consultation_type', 'dlh');
break;
default:
// Jika category tidak dikenali, return empty collection
return collect();
}
return $query->select([
'registration_number',
'document_number',
'owner_name',
'address',
'name as building_name',
'function_type'
])->get();
}
public function headings(): array{
return [
'Nomor Registrasi',
'Nomor Dokumen',
'Nama Pemilik',
'Alamat Pemilik',
'Nama Bangunan',
'Fungsi Bangunan',
];
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace App\Exports;
use App\Models\BigdataResume;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
class ReportDirectorExport implements FromCollection, WithHeadings, WithMapping
{
/**
* @return \Illuminate\Support\Collection
*/
public function collection()
{
return BigdataResume::select(
'potention_count',
'potention_sum',
'non_verified_count',
'non_verified_sum',
'verified_count',
'verified_sum',
'business_count',
'business_sum',
'non_business_count',
'non_business_sum',
'spatial_count',
'spatial_sum',
'waiting_click_dpmptsp_count',
'waiting_click_dpmptsp_sum',
'issuance_realization_pbg_count',
'issuance_realization_pbg_sum',
'process_in_technical_office_count',
'process_in_technical_office_sum',
'year',
'created_at'
)->orderBy('id', 'desc')->get();
}
public function headings(): array{
return [
"Jumlah Potensi" ,
"Total Potensi" ,
"Jumlah Berkas Belum Terverifikasi" ,
"Total Berkas Belum Terverifikasi" ,
"Jumlah Berkas Terverifikasi" ,
"Total Berkas Terverifikasi" ,
"Jumlah Usaha" ,
"Total Usaha" ,
"Jumlah Non Usaha" ,
"Total Non Usaha" ,
"Jumlah Tata Ruang" ,
"Total Tata Ruang" ,
"Jumlah Menunggu Klik DPMPTSP" ,
"Total Menunggu Klik DPMPTSP" ,
"Jumlah Realisasi Terbit PBG" ,
"Total Realisasi Terbit PBG" ,
"Jumlah Proses Dinas Teknis" ,
"Total Proses Dinas Teknis",
"Tahun",
"Created"
];
}
public function map($row): array
{
return [
$row->potention_count,
$row->potention_sum,
$row->non_verified_count,
$row->non_verified_sum,
$row->verified_count,
$row->verified_sum,
$row->business_count,
$row->business_sum,
$row->non_business_count,
$row->non_business_sum,
$row->spatial_count,
$row->spatial_sum,
$row->waiting_click_dpmptsp_count,
$row->waiting_click_dpmptsp_sum,
$row->issuance_realization_pbg_count,
$row->issuance_realization_pbg_sum,
$row->process_in_technical_office_count,
$row->process_in_technical_office_sum,
$row->year,
$row->created_at ? $row->created_at->format('Y-m-d H:i:s') : null, // Format created_at as Y-m-d
];
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Exports;
use App\Models\BigdataResume;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
class ReportPaymentRecapExport implements FromCollection, WithHeadings
{
/**
* @return \Illuminate\Support\Collection
*/
protected $startDate;
protected $endDate;
public function __construct($startDate, $endDate){
$this->startDate = $startDate;
$this->endDate = $endDate;
}
public function collection()
{
$query = BigdataResume::query()->orderBy('id', 'desc');
if ($this->startDate && $this->endDate) {
$query->whereBetween('created_at', [$this->startDate, $this->endDate]);
}
$items = $query->get();
$categoryMap = [
'potention_sum' => 'Potensi',
'non_verified_sum' => 'Belum Terverifikasi',
'verified_sum' => 'Terverifikasi',
'business_sum' => 'Usaha',
'non_business_sum' => 'Non Usaha',
'spatial_sum' => 'Tata Ruang',
'waiting_click_dpmptsp_sum' => 'Menunggu Klik DPMPTSP',
'issuance_realization_pbg_sum' => 'Realisasi Terbit PBG',
'process_in_technical_office_sum' => 'Proses Di Dinas Teknis',
];
// Restructure response
$data = [];
foreach ($items as $item) {
$createdAt = $item->created_at;
$id = $item->id;
foreach ($item->toArray() as $key => $value) {
// Only include columns with "sum" in their names
if (strpos($key, 'sum') !== false) {
$data[] = [
'category' => $categoryMap[$key] ?? $key, // Map category
'nominal' => number_format($value, 0, ',', '.'), // Format number
'created_at' => $createdAt->format('Y-m-d H:i:s'), // Format date
];
}
}
}
return collect($data);
}
public function headings(): array{
return [
'Kategori',
'Nominal',
'Created'
];
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Exports;
use App\Models\PbgTask;
use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
class ReportPbgPtspExport implements FromCollection, WithHeadings
{
/**
* @return \Illuminate\Support\Collection
*/
public function collection()
{
return PbgTask::select(
'status_name',
DB::raw('COUNT(*) as total')
)
->groupBy('status', 'status_name')
->get();
}
public function headings(): array
{
return [
'Status Name',
'Total'
];
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Exports;
use App\Models\TourismBasedKBLI;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
class ReportTourismExport implements FromCollection, WithHeadings
{
/**
* @return \Illuminate\Support\Collection
*/
public function collection()
{
return TourismBasedKBLI::select('kbli_title', 'total_records')->get();
}
public function headings(): array{
return [
'Jenis Bisnis Pariwisata',
'Jumlah Total'
];
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Exports;
use App\Models\Tax;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithTitle;
use Maatwebsite\Excel\Concerns\WithHeadings;
class TaxSubdistrictSheetExport implements FromCollection, WithTitle, WithHeadings
{
protected $subdistrict;
public function __construct(string $subdistrict)
{
$this->subdistrict = $subdistrict;
}
public function collection()
{
return Tax::where('subdistrict', $this->subdistrict)
->select(
'tax_code',
'tax_no',
'npwpd',
'wp_name',
'business_name',
'address',
'start_validity',
'end_validity',
'tax_value',
'subdistrict',
'village'
)->get();
}
public function headings(): array
{
return [
'Kode',
'No',
'NPWPD',
'Nama WP',
'Nama Usaha',
'Alamat Usaha',
'Tanggal Mulai Berlaku',
'Tanggal Berakhir Berlaku',
'Nilai Pajak',
'Kecamatan',
'Desa'
];
}
public function title(): string
{
return mb_substr($this->subdistrict, 0, 31);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Exports;
use App\Models\Tax;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
class TaxationsExport implements WithMultipleSheets
{
public function sheets(): array
{
$sheets = [];
// Ambil semua subdistrict unik
$subdistricts = Tax::select('subdistrict')->distinct()->pluck('subdistrict');
foreach ($subdistricts as $subdistrict) {
$sheets[] = new TaxSubdistrictSheetExport($subdistrict);
}
return $sheets;
}
}

View File

@@ -2,11 +2,19 @@
namespace App\Http\Controllers\Api;
use App\Exports\ReportDirectorExport;
use App\Exports\ReportPaymentRecapExport;
use App\Http\Controllers\Controller;
use App\Http\Resources\BigdataResumeResource;
use App\Models\BigdataResume;
use App\Models\DataSetting;
use App\Models\SpatialPlanning;
use App\Models\PbgTaskPayment;
use Barryvdh\DomPDF\Facade\Pdf;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Maatwebsite\Excel\Facades\Excel;
class BigDataResumeController extends Controller
{
@@ -17,13 +25,16 @@ class BigDataResumeController extends Controller
{
try{
$filterDate = $request->get("filterByDate");
$type = trim($request->get("type"));
// If filterByDate is "latest" or empty, get the most recent record
if (!$filterDate || $filterDate === "latest") {
$big_data_resume = BigdataResume::latest()->first();
$big_data_resume = BigdataResume::where('resume_type', $type)->latest()->first();
if (!$big_data_resume) {
return $this->response_empty_resume();
}
} else {
// Filter by specific date
$big_data_resume = BigdataResume::whereDate('created_at', $filterDate)
->where('resume_type', $type)
->orderBy('id', 'desc')
->first();
@@ -33,19 +44,28 @@ class BigDataResumeController extends Controller
}
$data_settings = DataSetting::all();
if($data_settings->isEmpty()){
return response()->json(['message' => 'No data setting found']);
$target_pad = 0;
if($data_settings->where('key', 'TARGET_PAD')->first()){
$target_pad = floatval($data_settings->where('key', 'TARGET_PAD')->first()->value ?? 0);
}
$target_pad = floatval(optional($data_settings->where('key', 'TARGET_PAD')->first())->value);
$tata_ruang = floatval(optional($data_settings->where('key', 'TATA_RUANG')->first())->value);
$realisasi_terbit_pbg_sum = floatval(optional($data_settings->where('key', 'REALISASI_TERBIT_PBG_SUM')->first())->value);
$realisasi_terbit_pbg_count = floatval(optional($data_settings->where('key', 'REALISASI_TERBIT_PBG_COUNT')->first())->value);
$menuggu_klik_dpmptsp_sum = floatval(optional($data_settings->where('key', 'MENUNGGU_KLIK_DPMPTSP_SUM')->first())->value);
$menuggu_klik_dpmptsp_count = floatval(optional($data_settings->where('key', 'MENUNGGU_KLIK_DPMPTSP_COUNT')->first())->value);
$proses_dinas_teknis_sum = floatval(optional($data_settings->where('key', 'PROSES_DINAS_TEKNIS_SUM')->first())->value);
$proses_dinas_teknis_count = floatval(optional($data_settings->where('key', 'PROSES_DINAS_TEKNIS_COUNT')->first())->value);
$realisasi_terbit_pbg_sum = $big_data_resume->issuance_realization_pbg_sum;
$realisasi_terbit_pbg_count = $big_data_resume->issuance_realization_pbg_count;
$menunggu_klik_dpmptsp_sum = $big_data_resume->waiting_click_dpmptsp_sum;
$menunggu_klik_dpmptsp_count = $big_data_resume->waiting_click_dpmptsp_count;
$proses_dinas_teknis_sum = $big_data_resume->process_in_technical_office_sum;
$proses_dinas_teknis_count = $big_data_resume->process_in_technical_office_count;
// Get real-time spatial planning data using new calculation formula
$spatialData = $this->getSpatialPlanningData();
$tata_ruang = $spatialData['sum'];
$tata_ruang_count = $spatialData['count'];
// Get real-time PBG Task Payments data
$pbgPaymentsData = $this->getPbgTaskPaymentsData();
$pbg_task_payments_sum = $pbgPaymentsData['sum'];
$pbg_task_payments_count = $pbgPaymentsData['count'];
$kekurangan_potensi = $target_pad - $big_data_resume->potention_sum;
// percentage kekurangan potensi
@@ -56,38 +76,54 @@ class BigDataResumeController extends Controller
$total_potensi_percentage = $big_data_resume->potention_sum > 0 && $target_pad > 0
? round(($big_data_resume->potention_sum / $target_pad) * 100, 2) : 0;
// percentage verified document
$verified_percentage = $big_data_resume->verified_sum > 0 && $big_data_resume->potention_sum > 0
// percentage verified document (verified_sum / potention_sum) - by value/amount
$verified_percentage = $big_data_resume->potention_sum > 0 && $big_data_resume->verified_sum >= 0
? round(($big_data_resume->verified_sum / $big_data_resume->potention_sum) * 100, 2) : 0;
// percentage non-verified document
$non_verified_percentage = $big_data_resume->non_verified_sum > 0 && $big_data_resume->potention_sum > 0
// percentage non-verified document (non_verified_sum / potention_sum) - by value/amount
$non_verified_percentage = $big_data_resume->potention_sum > 0 && $big_data_resume->non_verified_sum >= 0
? round(($big_data_resume->non_verified_sum / $big_data_resume->potention_sum) * 100, 2) : 0;
// percentage business document
$business_percentage = $big_data_resume->business_sum > 0 && $big_data_resume->non_verified_sum > 0
// Alternative: percentage by count (if needed)
// $verified_count_percentage = $big_data_resume->potention_count > 0
// ? round(($big_data_resume->verified_count / $big_data_resume->potention_count) * 100, 2) : 0;
// $non_verified_count_percentage = $big_data_resume->potention_count > 0
// ? round(($big_data_resume->non_verified_count / $big_data_resume->potention_count) * 100, 2) : 0;
// percentage business document (business / non_verified)
$business_percentage = $big_data_resume->non_verified_sum > 0 && $big_data_resume->business_sum >= 0
? round(($big_data_resume->business_sum / $big_data_resume->non_verified_sum) * 100, 2) : 0;
// percentage non-business document
$non_business_percentage = $big_data_resume->non_business_sum > 0 && $big_data_resume->potention_sum > 0
? round(($big_data_resume->non_business_sum / $big_data_resume->potention_sum) * 100, 2) : 0;
// percentage non-business document (non_business / non_verified)
$non_business_percentage = $big_data_resume->non_verified_sum > 0 && $big_data_resume->non_business_sum >= 0
? round(($big_data_resume->non_business_sum / $big_data_resume->non_verified_sum) * 100, 2) : 0;
// percentage tata ruang
$tata_ruang_percentage = $tata_ruang > 0 && $big_data_resume->potention_sum > 0
// percentage tata ruang (spatial / potention)
$tata_ruang_percentage = $big_data_resume->potention_sum > 0 && $tata_ruang >= 0
? round(($tata_ruang / $big_data_resume->potention_sum) * 100, 2) : 0;
// percentage realisasi terbit pbg
$realisasi_terbit_percentage = $big_data_resume->verified_sum > 0 && $realisasi_terbit_pbg_sum > 0
// percentage realisasi terbit pbg (issuance / verified)
$realisasi_terbit_percentage = $big_data_resume->verified_sum > 0 && $realisasi_terbit_pbg_sum >= 0
? round(($realisasi_terbit_pbg_sum / $big_data_resume->verified_sum) * 100, 2) : 0;
// percentage menunggu klik dpmptsp
$menunggu_klik_dpmptsp_percentage = $big_data_resume->verified_sum > 0 && $menuggu_klik_dpmptsp_sum > 0
? round(($menuggu_klik_dpmptsp_sum / $big_data_resume->verified_sum) * 100, 2) : 0;
// percentage menunggu klik dpmptsp (waiting / verified)
$menunggu_klik_dpmptsp_percentage = $big_data_resume->verified_sum > 0 && $menunggu_klik_dpmptsp_sum >= 0
? round(($menunggu_klik_dpmptsp_sum / $big_data_resume->verified_sum) * 100, 2) : 0;
// percentage proses_dinas_teknis
$proses_dinas_teknis_percentage = $big_data_resume->verified_sum > 0 && $proses_dinas_teknis_sum > 0
// percentage proses_dinas_teknis (process / verified)
$proses_dinas_teknis_percentage = $big_data_resume->verified_sum > 0 && $proses_dinas_teknis_sum >= 0
? round(($proses_dinas_teknis_sum / $big_data_resume->verified_sum) * 100, 2) : 0;
// percentage pbg_task_payments (payments / verified)
$pbg_task_payments_percentage = $realisasi_terbit_pbg_sum > 0 && $pbg_task_payments_sum >= 0
? round(($pbg_task_payments_sum / $realisasi_terbit_pbg_sum) * 100, 2) : 0;
$business_rab_count = $big_data_resume->business_rab_count;
$business_krk_count = $big_data_resume->business_krk_count;
$non_business_rab_count = $big_data_resume->non_business_rab_count;
$non_business_krk_count = $big_data_resume->non_business_krk_count;
$business_dlh_count = $big_data_resume->business_dlh_count;
$result = [
'target_pad' => [
'sum' => $target_pad,
@@ -95,6 +131,7 @@ class BigDataResumeController extends Controller
],
'tata_ruang' => [
'sum' => $tata_ruang,
'count' => $tata_ruang_count,
'percentage' => $tata_ruang_percentage,
],
'kekurangan_potensi' => [
@@ -132,14 +169,24 @@ class BigDataResumeController extends Controller
'percentage' => $realisasi_terbit_percentage
],
'menunggu_klik_dpmptsp' => [
'sum' => $menuggu_klik_dpmptsp_sum,
'count' => $menuggu_klik_dpmptsp_count,
'sum' => $menunggu_klik_dpmptsp_sum,
'count' => $menunggu_klik_dpmptsp_count,
'percentage' => $menunggu_klik_dpmptsp_percentage
],
'proses_dinas_teknis' => [
'sum' => $proses_dinas_teknis_sum,
'count' => $proses_dinas_teknis_count,
'percentage' => $proses_dinas_teknis_percentage
],
'business_rab_count' => $business_rab_count,
'business_krk_count' => $business_krk_count,
'non_business_rab_count' => $non_business_rab_count,
'non_business_krk_count' => $non_business_krk_count,
'business_dlh_count' => $business_dlh_count,
'pbg_task_payments' => [
'sum' => (float) $pbg_task_payments_sum,
'count' => $pbg_task_payments_count,
'percentage' => $pbg_task_payments_percentage
]
];
return response()->json($result);
@@ -148,42 +195,176 @@ class BigDataResumeController extends Controller
}
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
public function bigdata_report(Request $request){
try{
$query = BigdataResume::query()->orderBy('id', 'desc');
if($request->filled('search')){
$query->where('year', 'LIKE', '%'.$request->input('search').'%');
}
$query = $query->paginate(config('app.paginate_per_page', 50));
return BigdataResumeResource::collection($query)->response()->getData(true);
}catch(\Exception $e){
Log::error($e->getMessage());
return response()->json(['message' => 'Error when fetching data'], 500);
}
}
/**
* Display the specified resource.
*/
public function show(string $id)
public function payment_recaps(Request $request)
{
//
try {
$query = BigdataResume::query()->orderBy('id', 'desc');
if ($request->filled('start_date') && $request->filled('end_date')) {
$startDate = Carbon::parse($request->input('start_date'))->startOfDay();
$endDate = Carbon::parse($request->input('end_date'))->endOfDay();
$query->whereBetween('created_at', [$startDate, $endDate]);
}
$data = $query->paginate(50);
// Restructure response
$transformedData = [];
foreach ($data as $item) {
$createdAt = $item->created_at;
$id = $item->id;
foreach ($item->toArray() as $key => $value) {
// Only include columns with "sum" in their names
if (strpos($key, 'sum') !== false) {
$transformedData[] = [
'id' => $id,
'category' => $key,
'nominal' => $value,
'created_at' => $createdAt,
];
}
}
}
return response()->json([
'data' => $transformedData, // Flat array
'pagination' => [
'total' => count($transformedData),
'per_page' => $data->perPage(),
'current_page' => $data->currentPage(),
'last_page' => $data->lastPage(),
]
]);
} catch (\Exception $e) {
Log::error($e->getMessage());
return response()->json(['message' => 'Error when fetching data'], 500);
}
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
public function export_excel_payment_recaps(Request $request)
{
//
$startDate = null;
$endDate = null;
if ($request->filled('start_date') && $request->filled('end_date')) {
$startDate = Carbon::parse($request->input('start_date'))->startOfDay();
$endDate = Carbon::parse($request->input('end_date'))->endOfDay();
}
return Excel::download(new ReportPaymentRecapExport($startDate, $endDate), 'laporan-rekap-pembayaran.xlsx');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
{
//
public function export_pdf_payment_recaps(Request $request){
$query = BigdataResume::query()->orderBy('id', 'desc');
if ($request->filled('start_date') && $request->filled('end_date')) {
$startDate = Carbon::parse($request->input('start_date'))->startOfDay();
$endDate = Carbon::parse($request->input('end_date'))->endOfDay();
$query->whereBetween('created_at', [$startDate, $endDate]);
}
$items = $query->get();
// Define category mapping
$categoryMap = [
'potention_sum' => 'Potensi',
'non_verified_sum' => 'Belum Terverifikasi',
'verified_sum' => 'Terverifikasi',
'business_sum' => 'Usaha',
'non_business_sum' => 'Non Usaha',
'spatial_sum' => 'Tata Ruang',
'waiting_click_dpmptsp_sum' => 'Menunggu Klik DPMPTSP',
'issuance_realization_pbg_sum' => 'Realisasi Terbit PBG',
'process_in_technical_office_sum' => 'Proses Di Dinas Teknis',
];
// Restructure response
$data = [];
foreach ($items as $item) {
$createdAt = $item->created_at;
$id = $item->id;
foreach ($item->toArray() as $key => $value) {
// Only include columns with "sum" in their names
if (strpos($key, 'sum') !== false) {
$data[] = [
'id' => $id,
'category' => $categoryMap[$key] ?? $key, // Map category
'nominal' => $value, // Format number
'created_at' => $createdAt->format('Y-m-d H:i:s'), // Format date
];
}
}
}
$pdf = Pdf::loadView('exports.payment_recaps_report', compact('data'));
return $pdf->download('laporan-rekap-pembayaran.pdf');
}
public function export_excel_report_director(){
return Excel::download(new ReportDirectorExport, 'laporan-pimpinan.xlsx');
}
public function export_pdf_report_director(){
$data = BigdataResume::select(
'potention_count',
'potention_sum',
'non_verified_count',
'non_verified_sum',
'verified_count',
'verified_sum',
'business_count',
'business_sum',
'non_business_count',
'non_business_sum',
'spatial_count',
'spatial_sum',
'waiting_click_dpmptsp_count',
'waiting_click_dpmptsp_sum',
'issuance_realization_pbg_count',
'issuance_realization_pbg_sum',
'process_in_technical_office_count',
'process_in_technical_office_sum',
'year',
'created_at'
)->orderBy('id', 'desc')->get();
$pdf = Pdf::loadView('exports.director_report', compact('data'))->setPaper('a4', 'landscape');
return $pdf->download('laporan-pimpinan.pdf');
}
private function response_empty_resume(){
$data_settings = DataSetting::all();
$target_pad = 0;
if($data_settings->where('key', 'TARGET_PAD')->first()){
$target_pad = floatval($data_settings->where('key', 'TARGET_PAD')->first()->value ?? 0);
}
$result = [
'target_pad' => [
'sum' => 0,
'sum' => $target_pad,
'percentage' => 100,
],
'tata_ruang' => [
@@ -233,9 +414,97 @@ class BigDataResumeController extends Controller
'sum' => 0,
'count' => 0,
'percentage' => 0
],
'pbg_task_payments' => [
'sum' => 0,
'count' => 0,
'percentage' => 0
]
];
return response()->json($result);
}
/**
* Get spatial planning data using new calculation formula
*/
private function getSpatialPlanningData(): array
{
try {
// Get spatial plannings that are not yet issued (is_terbit = false) and have valid data
$spatialPlannings = SpatialPlanning::where('land_area', '>', 0)
->where('site_bcr', '>', 0)
->where('is_terbit', false)
->get();
$totalSum = 0;
$businessCount = 0;
$nonBusinessCount = 0;
foreach ($spatialPlannings as $spatialPlanning) {
// Use new calculation formula: LUAS LAHAN × BCR × HARGA SATUAN
$calculatedAmount = $spatialPlanning->calculated_retribution;
$totalSum += $calculatedAmount;
// Count business types
if ($spatialPlanning->is_business_type) {
$businessCount++;
} else {
$nonBusinessCount++;
}
}
Log::info("Real-time Spatial Planning Data (is_terbit = false only)", [
'total_records' => $spatialPlannings->count(),
'business_count' => $businessCount,
'non_business_count' => $nonBusinessCount,
'total_sum' => $totalSum,
'filtered_by' => 'is_terbit = false'
]);
return [
'count' => $spatialPlannings->count(),
'sum' => (float) $totalSum,
'business_count' => $businessCount,
'non_business_count' => $nonBusinessCount,
];
} catch (\Exception $e) {
Log::error("Error getting spatial planning data", ['error' => $e->getMessage()]);
return [
'count' => 0,
'sum' => 0.0,
'business_count' => 0,
'non_business_count' => 0,
];
}
}
/**
* Get PBG Task Payments data from database
*/
private function getPbgTaskPaymentsData(): array
{
try {
// Get sum and count from PbgTaskPayment model
$stats = PbgTaskPayment::whereNotNull('payment_date_raw')
->whereNotNull('retribution_total_pad')
->whereYear('payment_date_raw', date('Y'))
->selectRaw('SUM(retribution_total_pad) as total_sum, COUNT(*) as total_count')
->first();
$totalSum = $stats->total_sum ?? 0;
$totalCount = $stats->total_count ?? 0;
return [
'sum' => (float) $totalSum,
'count' => (int) $totalCount,
];
} catch (\Exception $e) {
Log::error("Error getting PBG task payments data", ['error' => $e->getMessage()]);
return [
'sum' => 0.0,
'count' => 0,
];
}
}
}

View File

@@ -9,6 +9,7 @@ use App\Models\BusinessOrIndustry;
use Illuminate\Http\Request;
use Maatwebsite\Excel\Facades\Excel;
use \Illuminate\Support\Facades\Validator;
use App\Http\Requests\ExcelUploadRequest;
class BusinessOrIndustriesController extends Controller
{
/**
@@ -30,7 +31,7 @@ class BusinessOrIndustriesController extends Controller
});
}
return response()->json($query->paginate());
return response()->json($query->paginate(config('app.paginate_per_page', 50)));
}
/**
@@ -79,29 +80,15 @@ class BusinessOrIndustriesController extends Controller
}
}
public function upload(Request $request){
if ($request->hasFile('file')) {
$file = $request->file('file');
}
// Validasi file
$validator = Validator::make($request->all(), [
'file' => 'required|mimes:xlsx,xls|max:102400', // Max 100MB
]);
if ($validator->fails()) {
return response()->json([
'message' => 'File validation failed.',
'errors' => $validator->errors()
], 400);
}
public function upload(ExcelUploadRequest $request){
try {
// Ambil file dari request
$file = $request->file('file');
if(!$request->hasFile('file')){
return response()->json([
'error' => 'No file provided'
], 400);
}
// Menggunakan Laravel Excel untuk mengimpor file
$file = $request->file('file');
Excel::import(new BusinessIndustriesImport, $file);
// Jika sukses, kembalikan respons sukses

View File

@@ -7,6 +7,7 @@ use App\Services\OpenAIService;
use App\Http\Controllers\Controller;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class ChatbotController extends Controller
{
@@ -19,7 +20,6 @@ class ChatbotController extends Controller
public function generateText(Request $request)
{
info($request);
$request->validate([
'tab_active' => 'required|string',
'prompt' => 'required|string',
@@ -33,52 +33,85 @@ class ChatbotController extends Controller
default => "UNKNOWN",
};
$chatHistory = $request->input('chatHistory');
Log::info('Chat history sebelum disimpan:', ['history' => $chatHistory]);
if ($main_content === "UNKNOWN") {
return response()->json(['response' => 'Invalid tab_active value.'], 400);
}
info($main_content);
// info($main_content);
// Klasifikasi apakah pertanyaan butuh database atau bisa dijawab langsung
$classifyResponse = $this->openAIService->generateClassifyMainContent($request->input('prompt'), $main_content);
$queryResponse = $this->openAIService->generateQueryBasedMainContent($request->input('prompt'), $main_content, $chatHistory);
if ($classifyResponse === "DATABASE") {
$queryResponse = $this->openAIService->generateQueryBasedMainContent($request->input('prompt'), $main_content);
if (is_array($queryResponse)) {
info('Query Response is an array: ', $queryResponse);
} else {
info('Query Response is a string: ' . $queryResponse);
if (str_contains($queryResponse, 'tidak relevan') || str_contains($queryResponse, 'tidak valid') || str_starts_with($queryResponse, 'Prompt')) {
return response()->json(['response' => $queryResponse], 400);
}
$formattedResultQuery = "[]";
$queryResponse = str_replace(['```sql', '```'], '', $queryResponse);
$resultQuery = DB::select($queryResponse);
$formattedResultQuery = json_encode($resultQuery, JSON_PRETTY_PRINT);
info($formattedResultQuery);
$nlpResult = $this->openAIService->generateNLPFromQuery($request->input('prompt'), $formattedResultQuery);
$finalGeneratedText =$this->openAIService->generateFinalText($nlpResult);
return response()->json(['response' => $finalGeneratedText, 'nlpResponse' => $queryResponse]);
}
public function mainGenerateText(Request $request)
{
// Log hanya data yang relevan
info("Received prompt: " . $request->input('prompt'));
// Validasi input
$request->validate([
'prompt' => 'required|string',
]);
try {
// Panggil service untuk generate text
$classifyResponse = $this->openAIService->classifyMainGenerateText($request->input('prompt'));
info($classifyResponse);
// Pastikan hasil klasifikasi valid sebelum melanjutkan
$validCategories = [
'reklame', 'business_or_industries', 'customers',
'pbg', 'retribusi', 'spatial_plannings',
'tourisms', 'umkms'
];
if (!in_array($classifyResponse, $validCategories)) {
return response()->json([
'error' => ''
], 400);
}
$chatHistory = $request->input('chatHistory');
Log::info('Chat history sebelum disimpan:', ['history' => $chatHistory]);
$queryResponse = $this->openAIService->createMainQuery($classifyResponse, $request->input('prompt'), $chatHistory);
info($queryResponse);
$formattedResultQuery = "[]";
// Validasi query dua kali sebelum eksekusi
if (
$this->openAIService->validateSyntaxQuery($queryResponse) === "VALID" &&
$this->openAIService->validateSyntaxQuery($queryResponse) === "VALID"
) {
info($queryResponse);
$queryResponse = str_replace(['```sql', '```'], '', $queryResponse);
$resultQuery = DB::select($queryResponse);
$formattedResultQuery = json_encode($resultQuery, JSON_PRETTY_PRINT);
$nlpResult = $this->openAIService->generateNLPFromQuery($request->input('prompt'), $formattedResultQuery);
$finalGeneratedText =$this->openAIService->generateFinalText($nlpResult);
return response()->json(['response' => $finalGeneratedText]);
}
return response()->json(['response' => ''], 400);
}
if ($classifyResponse === "GENERAL") {
$nlpResult = $this->openAIService->generateGeneralText($request->input('prompt'), $main_content);
$queryResponse = str_replace(['```sql', '```'], '', $queryResponse);
$queryResult = DB::select($queryResponse);
$formattedResultQuery = json_encode($queryResult, JSON_PRETTY_PRINT);
$nlpResult = $this->openAIService->generateNLPFromQuery($request->input('prompt'), $formattedResultQuery);
$finalGeneratedText =$this->openAIService->generateFinalText($nlpResult);
return response()->json(['response' => $finalGeneratedText]);
return response()->json(['response' => $finalGeneratedText, 'nlpResponse' => $queryResponse]);
} catch (\Exception $e) {
// Tangani error dan log exception
\Log::error("Error generating text: " . $e->getMessage());
return response()->json([
'error' => ''
], 500);
}
return response()->json(['response' => ''], 500);
}
private function classifyContent(string $prompt) {
$classifyResponse = $this->openAIService->generateClassifyContent($prompt);
return $classifyResponse;
}
}

View File

@@ -9,6 +9,7 @@ use App\Http\Resources\CustomersResource;
use App\Imports\CustomersImport;
use App\Models\Customer;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Maatwebsite\Excel\Facades\Excel;
class CustomersController extends Controller
@@ -22,9 +23,9 @@ class CustomersController extends Controller
if ($request->has('search') &&!empty($request->get('search'))) {
$query = $query->where('nomor_pelanggan', 'LIKE', '%'.$request->get('search').'%')
->orWhere('nama', 'LIKE', '%'.$request->get('search').'%')
->orWhere('kota_palayanan', 'LIKE', '%'.$request->get('search').'%');
->orWhere('kota_pelayanan', 'LIKE', '%'.$request->get('search').'%');
}
return CustomersResource::collection($query->paginate());
return CustomersResource::collection($query->paginate(config('app.paginate_per_page', 50)));
}
/**
@@ -120,7 +121,7 @@ class CustomersController extends Controller
'message' => 'File uploaded successfully',
]);
}catch(\Exception $e){
\Log::info($e->getMessage());
Log::info($e->getMessage());
return response()->json([
'error' => 'Failed to upload file',
'message' => $e->getMessage()

View File

@@ -34,7 +34,7 @@ class GlobalSettingsController extends Controller
try {
$data = GlobalSetting::create($request->validated());
return new GlobalSettingResource($data);
} catch (\Exception $e) {
} catch (Exception $e) {
return $this->resError($e->getMessage(), null, $e->getCode());
}
}

View File

@@ -3,33 +3,15 @@
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Services\GoogleSheetService;
use Illuminate\Http\Request;
class GoogleSheetController extends Controller
{
protected $googleSheetService;
public function __construct(GoogleSheetService $googleSheetService){
$this->googleSheetService = $googleSheetService;
}
/**
* Display a listing of the resource.
*/
public function index()
public function index(Request $request)
{
$dataCollection = $this->googleSheetService->getSheetDataCollection();
$result = [
"last_row" => $this->googleSheetService->getLastRowByColumn("C"),
"last_column" => $this->googleSheetService->getLastColumn(),
"header" => $this->googleSheetService->getHeader(),
"data_collection" => $dataCollection
];
return response()->json($result);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//

View File

@@ -0,0 +1,91 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\BigdataResume;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class GrowthReportAPIController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
// Get current date
$today = Carbon::today();
// Define default range: 1 month back from today
$defaultStart = $today->copy()->subMonth();
$defaultEnd = $today;
// Use request values if provided, else use defaults
// $startDate = $request->input('start_date', $defaultStart->toDateString());
// $endDate = $request->input('end_date', $defaultEnd->toDateString());
// Optional year filter (used if specified)
$year = $request->input('year', now()->year);
// $query = BigdataResume::selectRaw("
// DATE(created_at) as date,
// SUM(potention_sum) as potention_sum,
// SUM(verified_sum) as verified_sum,
// SUM(non_verified_sum) as non_verified_sum
// ")
// ->whereBetween('created_at', [$startDate, $endDate]);
$query = BigdataResume::selectRaw("
DATE(created_at) as date,
SUM(potention_sum) as potention_sum,
SUM(verified_sum) as verified_sum,
SUM(non_verified_sum) as non_verified_sum
");
$query->whereNotNull('year')
->where('year', '!=', 'all');
$data = $query->groupBy(DB::raw('DATE(created_at)'))
->orderBy(DB::raw('DATE(created_at)'))
->get()
->map(function ($item) {
$item->date = Carbon::parse($item->date)->format('d M Y');
return $item;
});
return response()->json($data);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(string $id)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
{
//
}
}

View File

@@ -24,7 +24,7 @@ class ImportDatasourceController extends Controller
$search = $request->get("search");
$query->where('status', 'like', "%".$search."%");
}
return ImportDatasourceResource::collection($query->paginate());
return ImportDatasourceResource::collection($query->paginate(config('app.paginate_per_page', 50)));
}
public function checkImportDatasource(){

View File

@@ -7,6 +7,9 @@ use App\Models\Advertisement;
use App\Models\Customer;
use App\Models\SpatialPlanning;
use Illuminate\Http\Request;
use App\Models\TourismBasedKBLI;
use App\Models\Tax;
use Illuminate\Support\Facades\Log;
class LackOfPotentialController extends Controller
{
@@ -16,11 +19,28 @@ class LackOfPotentialController extends Controller
$total_reklame = Advertisement::count();
$total_pdam = Customer::count();
$total_tata_ruang = SpatialPlanning::count();
$total_tata_ruang_usaha = SpatialPlanning::where('building_function','like', '%usaha%')->count();
$total_tata_ruang_non_usaha = SpatialPlanning::where('building_function','not like', '%usaha%')->count();
$data_report_tourism = TourismBasedKBLI::all();
$data_pajak_reklame = Tax::where('tax_code','Reklame')->distinct('business_name')->count();
$data_pajak_restoran = Tax::where('tax_code','Restoran')->distinct('business_name')->count();
$data_pajak_hiburan = Tax::where('tax_code','Hiburan')->distinct('business_name')->count();
$data_pajak_hotel = Tax::where('tax_code','Hotel')->distinct('business_name')->count();
$data_pajak_parkir = Tax::where('tax_code','Parkir')->distinct('business_name')->count();
return response()->json([
'total_reklame' => $total_reklame,
'total_pdam' => $total_pdam,
'total_tata_ruang' => $total_tata_ruang
'total_tata_ruang' => $total_tata_ruang,
'total_tata_ruang_usaha' => $total_tata_ruang_usaha,
'total_tata_ruang_non_usaha' => $total_tata_ruang_non_usaha,
'data_report' => $data_report_tourism,
'data_pajak_reklame' => $data_pajak_reklame,
'data_pajak_restoran' => $data_pajak_restoran,
'data_pajak_hiburan' => $data_pajak_hiburan,
'data_pajak_hotel' => $data_pajak_hotel,
'data_pajak_parkir' => $data_pajak_parkir,
'tata_ruang' => $this->getSpatialPlanningData()
], 200);
}catch(\Exception $e){
return response()->json([
@@ -28,4 +48,63 @@ class LackOfPotentialController extends Controller
], 500);
}
}
private function getSpatialPlanningData(): array
{
try {
// Get spatial plannings that are not yet issued (is_terbit = false) and have valid data
$spatialPlannings = SpatialPlanning::where('land_area', '>', 0)
->where('site_bcr', '>', 0)
->where('is_terbit', false)
->get();
$totalSum = 0;
$businessCount = 0;
$nonBusinessCount = 0;
$businessSum = 0;
$nonBusinessSum = 0;
foreach ($spatialPlannings as $spatialPlanning) {
// Use new calculation formula: LUAS LAHAN × BCR × HARGA SATUAN
$calculatedAmount = $spatialPlanning->calculated_retribution;
$totalSum += $calculatedAmount;
// Count business types
if ($spatialPlanning->is_business_type) {
$businessCount++;
$businessSum += $calculatedAmount;
} else {
$nonBusinessCount++;
$nonBusinessSum += $calculatedAmount;
}
}
Log::info("Real-time Spatial Planning Data (is_terbit = false only)", [
'total_records' => $spatialPlannings->count(),
'business_count' => $businessCount,
'non_business_count' => $nonBusinessCount,
'total_sum' => $totalSum,
'filtered_by' => 'is_terbit = false'
]);
return [
'count' => $spatialPlannings->count(),
'sum' => (float) $totalSum,
'business_count' => $businessCount,
'non_business_count' => $nonBusinessCount,
'business_sum' => (float) $businessSum,
'non_business_sum' => (float) $nonBusinessSum,
];
} catch (\Exception $e) {
Log::error("Error getting spatial planning data", ['error' => $e->getMessage()]);
return [
'count' => 0,
'sum' => 0.0,
'business_count' => 0,
'non_business_count' => 0,
'business_sum' => 0.0,
'non_business_sum' => 0.0,
];
}
}
}

View File

@@ -22,7 +22,8 @@ class MenusController extends Controller
$query = $query->where("name", "like", "%".$request->get("search")."%");
}
return response()->json($query->paginate());
// return response()->json($query->paginate(config('app.paginate_per_page', 50)));
return MenuResource::collection($query->paginate(config('app.paginate_per_page',50)));
}
/**

View File

@@ -0,0 +1,109 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\PbgTaskAttachment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\Response;
class PbgTaskAttachmentsController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request, $pbg_task_id)
{
try{
$request->validate([
'file' => 'required|file|mimes:jpg,png,pdf|max:5120',
'pbg_type' => 'string'
]);
$attachment = PbgTaskAttachment::create([
'pbg_task_id' => $pbg_task_id,
'file_name' => $request->file('file')->getClientOriginalName(),
'file_path' => '', // empty path initially
'pbg_type' => $request->pbg_type == 'bukti_bayar' ? 'bukti_bayar' : 'berita_acara'
]);
$file = $request->file('file');
$path = $file->store("uploads/pbg-tasks/{$pbg_task_id}/{$attachment->id}", "public");
$attachment->update([
'file_path' => $path,
]);
return response()->json([
'message' => 'File uploaded successfully.',
'attachment' => [
'id' => $attachment->id,
'file_name' => $attachment->file_name,
'file_url' => Storage::url($attachment->file_path),
'pbg_type' => $attachment->pbg_type
]
]);
}catch(\Exception $e){
\Log::error($e->getMessage());
return response()->json([
"success" => false,
"message" => $e->getTraceAsString()
]);
}
}
/**
* Display the specified resource.
*/
public function show(string $id)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
{
//
}
public function download(string $id)
{
try {
$data = PbgTaskAttachment::findOrFail($id);
$filePath = $data->file_path; // already relative to 'public' disk
if (!Storage::disk('public')->exists($filePath)) {
return response()->json([
"success" => false,
"message" => "File not found on server"
], Response::HTTP_NOT_FOUND);
}
return Storage::disk('public')->download($filePath, $data->file_name);
} catch (\Exception $e) {
return response()->json([
"success" => false,
"message" => $e->getMessage()
]);
}
}
}

View File

@@ -3,6 +3,8 @@
namespace App\Http\Controllers\Api;
use App\Enums\ImportDatasourceStatus;
use App\Enums\PbgTaskApplicationTypes;
use App\Enums\PbgTaskStatus;
use App\Http\Controllers\Controller;
use App\Http\Requests\PbgTaskMultiStepRequest;
use App\Http\Resources\PbgTaskResource;
@@ -14,6 +16,7 @@ use App\Services\GoogleSheetService;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rules\Enum;
class PbgTaskController extends Controller
{
@@ -116,9 +119,72 @@ class PbgTaskController extends Controller
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
public function update(Request $request, string $task_uuid)
{
//
try{
$pbg_task = PbgTask::where('uuid',$task_uuid)->first();
if(!$pbg_task){
return response()->json([
"success"=> false,
"message"=> "Data PBG Task tidak ditemukan",
], 404);
}
$validated = $request->validate([
'name' => 'nullable|string|max:255',
'owner_name' => 'nullable|string|max:255',
'application_type' => ['nullable', new Enum(PbgTaskApplicationTypes::class)],
'condition' => 'nullable|string|max:255',
'registration_number' => 'nullable|string|max:255',
'document_number' => 'nullable|string|max:255',
'status' => ['nullable', new Enum(PbgTaskStatus::class)],
'address' => 'nullable|string|max:255',
'slf_status_name' => 'nullable|string|max:255',
'function_type' => 'nullable|string|max:255',
'consultation_type' => 'nullable|string|max:255',
'due_date' => 'nullable|date',
'is_valid' => 'nullable|boolean',
]);
$statusLabel = $validated['status'] !== null ? PbgTaskStatus::getLabel($validated['status']) : null;
$applicationLabel = $validated['application_type'] !== null ? PbgTaskApplicationTypes::getLabel($validated['application_type']) : null;
// Prepare update data - only include fields that are actually provided
$updateData = [];
foreach ($validated as $key => $value) {
if ($value !== null || $request->has($key)) {
$updateData[$key] = $value;
}
}
// Handle special cases for labels
if (isset($updateData['status'])) {
$updateData['status_name'] = $statusLabel;
}
if (isset($updateData['application_type'])) {
$updateData['application_type_name'] = $applicationLabel;
}
// Handle is_valid specifically
if ($request->has('is_valid')) {
$updateData['is_valid'] = $validated['is_valid'];
}
$pbg_task->update($updateData);
return response()->json([
"success"=> true,
"message"=> "Data berhasil diubah",
"data"=> $pbg_task
]);
}catch(\Exception $e){
return response()->json([
"success"=> false,
"message"=> $e->getMessage(),
]);
}
}
/**
@@ -135,255 +201,4 @@ class PbgTaskController extends Controller
]);
}
public function syncPbgFromGoogleSheet(){
$import_datasource = ImportDatasource::create([
"message" => "initialization",
"response_body" => null,
"status" => ImportDatasourceStatus::Processing->value,
]);
try{
$totalRowCount = $this->googleSheetService->getLastRowByColumn("C");
$sheetData = $this->googleSheetService->getSheetDataCollection($totalRowCount);
$sheet_big_data = $this->googleSheetService->get_data_by_sheet();
$data_setting_result = []; // Initialize result storage
$found_section = null; // Track which section is found
foreach ($sheet_big_data as $row) {
// Check for section headers
if (in_array("•PROSES PENERBITAN:", $row)) {
$found_section = "MENUNGGU_KLIK_DPMPTSP";
} elseif (in_array("•BERKAS AKTUAL TERVERIFIKASI DINAS TEKNIS 2024:", $row)) {
$found_section = "REALISASI_TERBIT_PBG";
} elseif (in_array("•TERPROSES DI DPUTR: belum selesai rekomtek'", $row)) {
$found_section = "PROSES_DINAS_TEKNIS";
}
// If a section is found and we reach "Grand Total", save the corresponding values
if ($found_section && isset($row[0]) && trim($row[0]) === "Grand Total") {
if ($found_section === "MENUNGGU_KLIK_DPMPTSP") {
$data_setting_result["MENUNGGU_KLIK_DPMPTSP_COUNT"] = $row[2] ?? null;
$data_setting_result["MENUNGGU_KLIK_DPMPTSP_SUM"] = $row[3] ?? null;
} elseif ($found_section === "REALISASI_TERBIT_PBG") {
$data_setting_result["REALISASI_TERBIT_PBG_COUNT"] = $row[2] ?? null;
$data_setting_result["REALISASI_TERBIT_PBG_SUM"] = $row[4] ?? null;
} elseif ($found_section === "PROSES_DINAS_TEKNIS") {
$data_setting_result["PROSES_DINAS_TEKNIS_COUNT"] = $row[2] ?? null;
$data_setting_result["PROSES_DINAS_TEKNIS_SUM"] = $row[3] ?? null;
}
// Reset section tracking after capturing "Grand Total"
$found_section = null;
}
}
foreach ($data_setting_result as $key => $value) {
DataSetting::updateOrInsert(
["key" => $key], // Find by key
["value" => $value] // Update or insert value
);
}
$mapToUpsert = [];
$count = 0;
foreach($sheetData as $data){
$mapToUpsert[] =
[
'no_registrasi' => $data['no__registrasi'] ?? null,
'jenis_konsultasi' => $data['jenis_konsultasi'] ?? null,
'fungsi_bg' => $data['fungsi_bg'] ?? null,
'tgl_permohonan' => $this->convertToDate($data['tgl_permohonan']),
'status_verifikasi' => $data['status_verifikasi'] ?? null,
'status_permohonan' => $this->convertToDate($data['status_permohonan']),
'alamat_pemilik' => $data['alamat_pemilik'] ?? null,
'no_hp' => $data['no__hp'] ?? null,
'email' => $data['e_mail'] ?? null,
'tanggal_catatan' => $this->convertToDate($data['tanggal_catatan']),
'catatan_kekurangan_dokumen' => $data['catatan_kekurangan_dokumen'] ?? null,
'gambar' => $data['gambar'] ?? null,
'krk_kkpr' => $data['krk_kkpr'] ?? null,
'no_krk' => $data['no__krk'] ?? null,
'lh' => $data['lh'] ?? null,
'ska' => $data['ska'] ?? null,
'keterangan' => $data['keterangan'] ?? null,
'helpdesk' => $data['helpdesk'] ?? null,
'pj' => $data['pj'] ?? null,
'kepemilikan' => $data['kepemilikan'] ?? null,
'potensi_taru' => $data['potensi_taru'] ?? null,
'validasi_dinas' => $data['validasi_dinas'] ?? null,
'kategori_retribusi' => $data['kategori_retribusi'] ?? null,
'no_urut_ba_tpt' => $data['no__urut_ba_tpt__2024_0001_'] ?? null,
'tanggal_ba_tpt' => $this->convertToDate($data['tanggal_ba_tpt']),
'no_urut_ba_tpa' => $data['no__urut_ba_tpa'] ?? null,
'tanggal_ba_tpa' => $this->convertToDate($data['tanggal_ba_tpa']),
'no_urut_skrd' => $data['no__urut_skrd__2024_0001_'] ?? null,
'tanggal_skrd' => $this->convertToDate($data['tanggal_skrd']),
'ptsp' => $data['ptsp'] ?? null,
'selesai_terbit' => $data['selesai_terbit'] ?? null,
'tanggal_pembayaran' => $this->convertToDate($data['tanggal_pembayaran__yyyy_mm_dd_']),
'format_sts' => $data['format_sts'] ?? null,
'tahun_terbit' => (int) $data['tahun_terbit'] ?? null,
'tahun_berjalan' => (int) $data['tahun_berjalan'] ?? null,
'kelurahan' => $data['kelurahan'] ?? null,
'kecamatan' => $data['kecamatan'] ?? null,
'lb' => $this->convertToDecimal($data['lb']) ?? null,
'tb' => $this->convertToDecimal($data['tb']) ?? null,
'jlb' => (int) $data['jlb'] ?? null,
'unit' => (int) $data['unit'] ?? null,
'usulan_retribusi' => (int) $data['usulan_retribusi'] ?? null,
'nilai_retribusi_keseluruhan_simbg' => $this->convertToDecimal($data['nilai_retribusi_keseluruhan__simbg_']) ?? null,
'nilai_retribusi_keseluruhan_pad' => $this->convertToDecimal($data['nilai_retribusi_keseluruhan__pad_']) ?? null,
'denda' => $this->convertToDecimal($data['denda']) ?? null,
'latitude' => $data['latitude'] ?? null,
'longitude' => $data['longitude'] ?? null,
'nik_nib' => $data['nik_nib'] ?? null,
'dok_tanah' => $data['dok__tanah'] ?? null,
'temuan' => $data['temuan'] ?? null,
];
}
DB::beginTransaction();
$batchSize = 1000;
$chunks = array_chunk($mapToUpsert, $batchSize);
foreach($chunks as $chunk){
PbgTaskGoogleSheet::upsert($chunk, ["no_registrasi"],[
'jenis_konsultasi',
'nama_pemilik',
'lokasi_bg',
'fungsi_bg',
'nama_bangunan',
'tgl_permohonan',
'status_verifikasi',
'status_permohonan',
'alamat_pemilik',
'no_hp',
'email',
'tanggal_catatan',
'catatan_kekurangan_dokumen',
'gambar',
'krk_kkpr',
'no_krk',
'lh',
'ska',
'keterangan',
'helpdesk',
'pj',
'kepemilikan',
'potensi_taru',
'validasi_dinas',
'kategori_retribusi',
'no_urut_ba_tpt',
'tanggal_ba_tpt',
'no_urut_ba_tpa',
'tanggal_ba_tpa',
'no_urut_skrd',
'tanggal_skrd',
'ptsp',
'selesai_terbit',
'tanggal_pembayaran',
'format_sts',
'tahun_terbit',
'tahun_berjalan',
'kelurahan',
'kecamatan',
'lb',
'tb',
'jlb',
'unit',
'usulan_retribusi',
'nilai_retribusi_keseluruhan_simbg',
'nilai_retribusi_keseluruhan_pad',
'denda',
'latitude',
'longitude',
'nik_nib',
'dok_tanah',
'temuan',
]);
}
$total_data = count($mapToUpsert);
$import_datasource->update([
"message" => "Successfully processed: {$total_data}",
"status" => ImportDatasourceStatus::Success->value,
]);
DB::commit();
return response()->json([
"success" => true,
"message" => "Data berhasil disimpan ke database"
], 200);
}catch(\Exception $ex){
DB::rollBack();
$import_datasource->update([
"message" => "Failed to importing",
"response_body" => $ex->getMessage(),
"status" => ImportDatasourceStatus::Failed->value,
]);
return response()->json([
"success" => false,
"message" => "Gagal menyimpan data",
"error" => $ex->getMessage()
], 500);
}
}
protected function convertToDecimal(?string $value): ?float
{
if (empty($value)) {
return null; // Return null if the input is empty
}
// Remove all non-numeric characters except comma and dot
$value = preg_replace('/[^0-9,\.]/', '', $value);
// If the number contains both dot (.) and comma (,)
if (strpos($value, '.') !== false && strpos($value, ',') !== false) {
$value = str_replace('.', '', $value); // Remove thousands separator
$value = str_replace(',', '.', $value); // Convert decimal separator to dot
}
// If only a dot is present (assumed as thousands separator)
elseif (strpos($value, '.') !== false) {
$value = str_replace('.', '', $value); // Remove all dots (treat as thousands separators)
}
// If only a comma is present (assumed as decimal separator)
elseif (strpos($value, ',') !== false) {
$value = str_replace(',', '.', $value); // Convert comma to dot (decimal separator)
}
// Ensure the value is numeric before returning
return is_numeric($value) ? (float) number_format((float) $value, 2, '.', '') : null;
}
protected function convertToInteger($value) {
// Check if the value is an empty string, and return null if true
if (trim($value) === "") {
return null;
}
// Otherwise, cast to integer
return (int) $value;
}
protected function convertToDate($dateString)
{
try {
// Check if the string is empty
if (empty($dateString)) {
return null;
}
// Try to parse the date string
$date = Carbon::parse($dateString);
// Return the Carbon instance
return $date->format('Y-m-d');
} catch (\Exception $e) {
// Return null if an error occurs during parsing
return null;
}
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\PbgTaskGoogleSheetResource;
use App\Models\PbgTaskGoogleSheet;
use Illuminate\Http\Request;
class PbgTaskGoogleSheetsController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
$query = PbgTaskGoogleSheet::query()->orderBy('id', 'desc');
if ($request->filled('search')) {
$query->where('no_registrasi', 'like', "%{$request->get('search')}%");
}
return PbgTaskGoogleSheetResource::collection($query->paginate(config('app.paginate_per_page', 50)));
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(string $id)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
{
try{
$data = PbgTaskGoogleSheet::find($id);
$data->delete();
return response()->json(['message' => 'Data deleted successfully'], 200);
}catch(\Exception $e){
return response()->json(['message' => 'Failed to delete data'], 500);
}
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Api;
use App\Exports\ReportPbgPtspExport;
use App\Http\Controllers\Controller;
use App\Models\PbgTask;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Facades\Excel;
class ReportPbgPtspController extends Controller
{
public function export_excel(){
return Excel::download(new ReportPbgPtspExport, 'laporan-ptsp.xlsx');
}
public function export_pdf(){
$data = PbgTask::select(
'status',
'status_name', // Keeping this column
DB::raw('COUNT(*) as total')
)
->groupBy('status', 'status_name')
->get();
$pdf = Pdf::loadView('exports.ptsp_report', compact('data'));
return $pdf->download('laporan-ptsp.pdf');
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\Api;
use App\Exports\ReportTourismExport;
use App\Http\Controllers\Controller;
use App\Models\TourismBasedKBLI;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Http\Request;
use Maatwebsite\Excel\Facades\Excel;
class ReportTourismsController extends Controller
{
public function export_excel(){
return Excel::download(new ReportTourismExport, 'laporan-pariwisata.xlsx');
}
public function export_pdf(){
$data = TourismBasedKBLI::all();
$pdf = Pdf::loadView('exports.tourisms_report', compact('data'));
return $pdf->download('laporan-pariwisata.pdf');
}
}

View File

@@ -2,10 +2,18 @@
namespace App\Http\Controllers\Api;
use App\Exports\DistrictPaymentRecapExport;
use App\Http\Controllers\Controller;
use App\Http\Resources\RequestAssignmentResouce;
use App\Models\PbgTask;
use App\Models\PbgTaskGoogleSheet;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Support\Facades\DB;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Maatwebsite\Excel\Facades\Excel;
use App\Enums\PbgTaskStatus;
class RequestAssignmentController extends Controller
{
@@ -14,14 +22,425 @@ class RequestAssignmentController extends Controller
*/
public function index(Request $request)
{
$query = PbgTask::query()->orderBy('id', 'desc');
if($request->has('search') && !empty($request->get("search"))){
$query->where('name', 'LIKE', '%'.$request->get('search').'%')
->orWhere('registration_number', 'LIKE', '%'.$request->get('search').'%');
// Build base query for counting (without relationships to avoid duplicates)
$baseQuery = PbgTask::query();
// Always filter only valid data (is_valid = true)
$baseQuery->where('is_valid', true);
// Apply year filter if provided (to match BigdataResume behavior)
if ($request->has('year') && !empty($request->get('year'))) {
$year = $request->get('year');
$baseQuery->where('due_date', '>=', $year.'-02-01');
Log::info('RequestAssignmentController year filter applied', ['year' => $year]);
}
return RequestAssignmentResouce::collection($query->paginate());
// Get filter value, default to 'all' if not provided or empty
$filter = $request->has('filter') && !empty($request->get('filter'))
? strtolower(trim($request->get('filter')))
: 'all';
// Log filter for debugging
Log::info('RequestAssignmentController filter applied', ['filter' => $filter, 'original' => $request->get('filter')]);
// Apply filters to base query using single consolidated method
$this->applyFilter($baseQuery, $filter);
// Get accurate count from base query (without relationships)
$accurateCount = $baseQuery->count();
// Clone the base query for data fetching with relationships
$dataQuery = clone $baseQuery;
$dataQuery->with([
'attachments' => function ($q) {
$q->whereIn('pbg_type', ['berita_acara', 'bukti_bayar']);
},
'pbg_task_retributions',
'pbg_task_detail',
'pbg_status'
])->orderBy('id', 'desc');
// Log final query count for debugging
Log::info('RequestAssignmentController final result', [
'filter' => $filter,
'search' => $request->get('search'),
'year' => $request->get('year'),
'accurate_count' => $accurateCount,
'request_url' => $request->fullUrl(),
'all_params' => $request->all()
]);
// Cross-validation with BigdataResume logic (for debugging consistency)
if ($filter !== 'all' && $request->has('year') && !empty($request->get('year'))) {
$this->validateConsistencyWithBigdataResume($filter, $request->get('year'), $accurateCount);
}
// Apply search to data query
if ($request->has('search') && !empty($request->get("search"))) {
$this->applySearch($dataQuery, $request->get('search'));
}
// Additional logging for potention filter
if ($filter === 'potention') {
$rejectedCount = PbgTask::whereIn('status', PbgTaskStatus::getRejected())->count();
Log::info('Potention filter details', [
'potention_count' => $accurateCount,
'rejected_count' => $rejectedCount,
'total_all_records' => PbgTask::count(),
'note' => 'Potention filter excludes rejected data'
]);
}
// Also log to console for immediate debugging
if ($filter !== 'all') {
error_log('RequestAssignment Filter Debug: ' . $filter . ' -> Count: ' . $accurateCount);
}
// Get paginated results with relationships
$paginatedResults = $dataQuery->paginate();
// Append query parameters to pagination
$paginatedResults->appends($request->query());
return RequestAssignmentResouce::collection($paginatedResults);
}
/**
* Apply filter logic to the query
*/
private function applyFilter($query, string $filter)
{
switch ($filter) {
case 'all':
// No additional filters, just return all valid records
break;
case 'non-business':
// Non-business: function_type NOT LIKE usaha AND (unit IS NULL OR unit <= 1)
$query->where(function ($q) {
$q->where(function ($q2) {
$q2->where(function ($q3) {
$q3->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
})
->orWhereNull('function_type');
})
->whereIn("status", PbgTaskStatus::getNonVerified())
// Additional condition: unit IS NULL OR unit <= 1
->where(function ($q3) {
$q3->whereDoesntHave('pbg_task_detail', function ($q4) {
$q4->where('unit', '>', 1);
})
->orWhereDoesntHave('pbg_task_detail');
});
});
break;
case 'business':
// Business: function_type LIKE usaha OR (non-business with unit > 1)
$query->where(function ($q) {
$q->where(function ($q2) {
// Traditional business: function_type LIKE usaha
$q2->where(function ($q3) {
$q3->whereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%fungsi usaha%'])
->orWhereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%sebagai tempat usaha%']);
})
// OR non-business with unit > 1 (becomes business)
->orWhere(function ($q3) {
$q3->where(function ($q4) {
$q4->where(function ($q5) {
$q5->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
})
->orWhereNull('function_type');
})
->whereHas('pbg_task_detail', function ($q4) {
$q4->where('unit', '>', 1);
});
});
})
->whereIn("status", PbgTaskStatus::getNonVerified());
});
break;
case 'verified':
// Match BigdataResume verified logic exactly
$query->whereIn("status", PbgTaskStatus::getVerified());
break;
case 'non-verified':
// Match BigdataResume non-verified logic exactly
$query->whereIn("status", PbgTaskStatus::getNonVerified());
break;
case 'potention':
// Match BigdataResume potention logic exactly
$query->whereIn("status", PbgTaskStatus::getPotention());
break;
case 'issuance-realization-pbg':
// Match BigdataResume issuance realization logic exactly
$query->whereIn("status", PbgTaskStatus::getIssuanceRealizationPbg());
break;
case 'process-in-technical-office':
// Match BigdataResume process in technical office logic exactly
$query->whereIn("status", PbgTaskStatus::getProcessInTechnicalOffice());
break;
case 'waiting-click-dpmptsp':
// Match BigdataResume waiting click DPMPTSP logic exactly
$query->whereIn("status", PbgTaskStatus::getWaitingClickDpmptsp());
break;
case 'non-business-rab':
// Non-business tasks: function_type NOT LIKE usaha AND (unit IS NULL OR unit <= 1)
$query->where(function ($q) {
$q->where(function ($q2) {
$q2->where(function ($q3) {
$q3->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
})
->orWhereNull('function_type');
})
->whereIn("status", PbgTaskStatus::getNonVerified())
// Additional condition: unit IS NULL OR unit <= 1
->where(function ($q3) {
$q3->whereDoesntHave('pbg_task_detail', function ($q4) {
$q4->where('unit', '>', 1);
})
->orWhereDoesntHave('pbg_task_detail');
});
})
->whereExists(function ($query) {
$query->select(DB::raw(1))
->from('pbg_task_detail_data_lists')
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
->where('pbg_task_detail_data_lists.data_type', 3)
->where('pbg_task_detail_data_lists.status', '!=', 1);
});
break;
case 'non-business-krk':
// Non-business tasks: function_type NOT LIKE usaha AND (unit IS NULL OR unit <= 1)
$query->where(function ($q) {
$q->where(function ($q2) {
$q2->where(function ($q3) {
$q3->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
})
->orWhereNull('function_type');
})
->whereIn("status", PbgTaskStatus::getNonVerified())
// Additional condition: unit IS NULL OR unit <= 1
->where(function ($q3) {
$q3->whereDoesntHave('pbg_task_detail', function ($q4) {
$q4->where('unit', '>', 1);
})
->orWhereDoesntHave('pbg_task_detail');
});
})
->whereExists(function ($query) {
$query->select(DB::raw(1))
->from('pbg_task_detail_data_lists')
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
->where('pbg_task_detail_data_lists.data_type', 2)
->where('pbg_task_detail_data_lists.status', '!=', 1);
});
break;
case 'business-rab':
// Business tasks: function_type LIKE usaha OR (non-business with unit > 1)
$query->where(function ($q) {
$q->where(function ($q2) {
// Traditional business: function_type LIKE usaha
$q2->where(function ($q3) {
$q3->whereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%fungsi usaha%'])
->orWhereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%sebagai tempat usaha%']);
})
// OR non-business with unit > 1 (becomes business)
->orWhere(function ($q3) {
$q3->where(function ($q4) {
$q4->where(function ($q5) {
$q5->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
})
->orWhereNull('function_type');
})
->whereHas('pbg_task_detail', function ($q4) {
$q4->where('unit', '>', 1);
});
});
})
->whereIn("status", PbgTaskStatus::getNonVerified());
})
->whereExists(function ($query) {
$query->select(DB::raw(1))
->from('pbg_task_detail_data_lists')
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
->where('pbg_task_detail_data_lists.data_type', 3)
->where('pbg_task_detail_data_lists.status', '!=', 1);
});
break;
case 'business-krk':
// Business tasks: function_type LIKE usaha OR (non-business with unit > 1)
$query->where(function ($q) {
$q->where(function ($q2) {
// Traditional business: function_type LIKE usaha
$q2->where(function ($q3) {
$q3->whereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%fungsi usaha%'])
->orWhereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%sebagai tempat usaha%']);
})
// OR non-business with unit > 1 (becomes business)
->orWhere(function ($q3) {
$q3->where(function ($q4) {
$q4->where(function ($q5) {
$q5->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
})
->orWhereNull('function_type');
})
->whereHas('pbg_task_detail', function ($q4) {
$q4->where('unit', '>', 1);
});
});
})
->whereIn("status", PbgTaskStatus::getNonVerified());
})
->whereExists(function ($query) {
$query->select(DB::raw(1))
->from('pbg_task_detail_data_lists')
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
->where('pbg_task_detail_data_lists.data_type', 2)
->where('pbg_task_detail_data_lists.status', '!=', 1);
});
break;
case 'business-dlh':
// Business tasks: function_type LIKE usaha OR (non-business with unit > 1)
$query->where(function ($q) {
$q->where(function ($q2) {
// Traditional business: function_type LIKE usaha
$q2->where(function ($q3) {
$q3->whereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%fungsi usaha%'])
->orWhereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%sebagai tempat usaha%']);
})
// OR non-business with unit > 1 (becomes business)
->orWhere(function ($q3) {
$q3->where(function ($q4) {
$q4->where(function ($q5) {
$q5->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
})
->orWhereNull('function_type');
})
->whereHas('pbg_task_details', function ($q4) {
$q4->where('unit', '>', 1);
});
});
})
->whereIn("status", PbgTaskStatus::getNonVerified());
})
->whereExists(function ($query) {
$query->select(DB::raw(1))
->from('pbg_task_detail_data_lists')
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
->where('pbg_task_detail_data_lists.data_type', 5)
->where('pbg_task_detail_data_lists.status', '!=', 1);
});
break;
default:
// Log unrecognized filter for debugging
Log::warning('Unrecognized filter value', ['filter' => $filter]);
break;
}
}
/**
* Apply search logic to the query
*/
private function applySearch($query, string $search)
{
// Search in pbg_task columns
$query->where(function ($q) use ($search) {
$q->where('name', 'LIKE', "%$search%")
->orWhere('registration_number', 'LIKE', "%$search%")
->orWhere('owner_name', 'LIKE', "%$search%")
->orWhere('address', 'LIKE', "%$search%");
});
// If search term exists, also find UUIDs from name_building search
$namesBuildingUuids = DB::table('pbg_task_details')
->where('name_building', 'LIKE', "%$search%")
->pluck('pbg_task_uid')
->toArray();
// If we found matching name_building records, include them in the search
if (!empty($namesBuildingUuids)) {
$query->orWhereIn('uuid', $namesBuildingUuids);
}
}
public function report_payment_recaps(Request $request)
{
try {
// Query dengan group by kecamatan dan sum nilai_retribusi_keseluruhan_simbg
$query = PbgTaskGoogleSheet::select(
'kecamatan',
DB::raw('SUM(nilai_retribusi_keseluruhan_simbg) as total')
)
->groupBy('kecamatan')
->paginate(10);
// Return hasil dalam JSON format
return response()->json([
'success' => true,
'data' => $query
]);
} catch (Exception $e) {
Log::error($e->getMessage());
return response()->json(['message' => 'Terjadi kesalahan: ' . $e->getMessage()], 500);
}
}
public function export_excel_district_payment_recaps(){
return Excel::download(new DistrictPaymentRecapExport, 'laporan-rekap-data-pembayaran.xlsx');
}
public function export_pdf_district_payment_recaps(){
$data = PbgTaskGoogleSheet::select(
'kecamatan',
DB::raw('SUM(nilai_retribusi_keseluruhan_simbg) as total')
)
->groupBy('kecamatan')->get();
$pdf = Pdf::loadView('exports.district_payment_report', compact('data'));
return $pdf->download('laporan-rekap-data-pembayaran.pdf');
}
public function report_pbg_ptsp()
{
try {
// Query dengan group by status dan count total per status
$query = PbgTask::select(
'status',
'status_name',
DB::raw('COUNT(*) as total')
)
->groupBy('status', 'status_name')
->paginate(10);
// Return hasil dalam JSON format
return response()->json([
'success' => true,
'data' => $query
]);
} catch (Exception $e) {
Log::error($e->getMessage());
return response()->json(['message' => 'Terjadi kesalahan: ' . $e->getMessage()], 500);
}
}
/**
* Store a newly created resource in storage.
*/
@@ -53,4 +472,270 @@ class RequestAssignmentController extends Controller
{
//
}
/**
* Validate consistency with BigdataResume logic for debugging
*/
private function validateConsistencyWithBigdataResume(?string $filter, $year, int $actualCount)
{
try {
// Validate input parameters
if (empty($filter) || empty($year)) {
Log::info('Skipping consistency validation - empty filter or year', [
'filter' => $filter,
'year' => $year
]);
return;
}
// Convert year to integer
$year = (int) $year;
if ($year <= 0) {
Log::warning('Invalid year provided for consistency validation', ['year' => $year]);
return;
}
$bigdataResumeCount = null;
// Calculate expected count using BigdataResume logic
switch ($filter) {
case 'verified':
$bigdataResumeCount = PbgTask::whereIn('status', PbgTaskStatus::getVerified())
->where('is_valid', true)
->whereYear('task_created_at', $year)
->count();
break;
case 'non-verified':
$bigdataResumeCount = PbgTask::whereIn('status', PbgTaskStatus::getNonVerified())
->where('is_valid', true)
->whereYear('task_created_at', $year)
->count();
break;
case 'business':
$bigdataResumeCount = PbgTask::where(function ($q) {
$q->where(function ($q2) {
// Traditional business: function_type LIKE usaha
$q2->where(function ($q3) {
$q3->whereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%fungsi usaha%'])
->orWhereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%sebagai tempat usaha%']);
})
// OR non-business with unit > 1 (becomes business)
->orWhere(function ($q3) {
$q3->where(function ($q4) {
$q4->where(function ($q5) {
$q5->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
})
->orWhereNull('function_type');
})
->whereHas('pbg_task_details', function ($q4) {
$q4->where('unit', '>', 1);
});
});
})
->whereIn("status", PbgTaskStatus::getNonVerified());
})
->where('is_valid', true)
->whereYear('task_created_at', $year)
->count();
break;
case 'non-business':
$bigdataResumeCount = PbgTask::where(function ($q) {
$q->where(function ($q2) {
$q2->where(function ($q3) {
$q3->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
})
->orWhereNull('function_type');
})
->whereIn("status", PbgTaskStatus::getNonVerified())
// Additional condition: unit IS NULL OR unit <= 1
->where(function ($q3) {
$q3->whereDoesntHave('pbg_task_details', function ($q4) {
$q4->where('unit', '>', 1);
})
->orWhereDoesntHave('pbg_task_details');
});
})
->where('is_valid', true)
->whereYear('task_created_at', $year)
->count();
break;
case 'potention':
$bigdataResumeCount = PbgTask::whereIn('status', PbgTaskStatus::getPotention())
->where('is_valid', true)
->whereYear('task_created_at', $year)
->count();
break;
case 'waiting-click-dpmptsp':
$bigdataResumeCount = PbgTask::whereIn('status', PbgTaskStatus::getWaitingClickDpmptsp())
->where('is_valid', true)
->whereYear('task_created_at', $year)
->count();
break;
case 'issuance-realization-pbg':
$bigdataResumeCount = PbgTask::whereIn('status', PbgTaskStatus::getIssuanceRealizationPbg())
->where('is_valid', true)
->whereYear('task_created_at', $year)
->count();
break;
case 'process-in-technical-office':
$bigdataResumeCount = PbgTask::whereIn('status', PbgTaskStatus::getProcessInTechnicalOffice())
->where('is_valid', true)
->whereYear('task_created_at', $year)
->count();
break;
case 'non-business-rab':
$bigdataResumeCount = PbgTask::where(function ($q) {
$q->where(function ($q2) {
$q2->where(function ($q3) {
$q3->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
})
->orWhereNull('function_type');
})
->whereIn("status", PbgTaskStatus::getNonVerified());
})
->where('is_valid', true)
->whereYear('task_created_at', $year)
->whereExists(function ($query) {
$query->select(DB::raw(1))
->from('pbg_task_detail_data_lists')
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
->where('pbg_task_detail_data_lists.data_type', 3)
->where('pbg_task_detail_data_lists.status', '!=', 1);
})
->count();
break;
case 'non-business-krk':
$bigdataResumeCount = PbgTask::where(function ($q) {
$q->where(function ($q2) {
$q2->where(function ($q3) {
$q3->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%fungsi usaha%'])
->whereRaw("LOWER(TRIM(function_type)) NOT LIKE ?", ['%sebagai tempat usaha%']);
})
->orWhereNull('function_type');
})
->whereIn("status", PbgTaskStatus::getNonVerified());
})
->where('is_valid', true)
->whereYear('task_created_at', $year)
->whereExists(function ($query) {
$query->select(DB::raw(1))
->from('pbg_task_detail_data_lists')
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
->where('pbg_task_detail_data_lists.data_type', 2)
->where('pbg_task_detail_data_lists.status', '!=', 1);
})
->count();
break;
case 'business-rab':
$bigdataResumeCount = PbgTask::where(function ($q) {
$q->where(function ($q2) {
$q2->whereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%fungsi usaha%'])
->orWhereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%sebagai tempat usaha%']);
})
->whereIn("status", PbgTaskStatus::getNonVerified());
})
->where('is_valid', true)
->whereYear('task_created_at', $year)
->whereExists(function ($query) {
$query->select(DB::raw(1))
->from('pbg_task_detail_data_lists')
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
->where('pbg_task_detail_data_lists.data_type', 3)
->where('pbg_task_detail_data_lists.status', '!=', 1);
})
->count();
break;
case 'business-krk':
$bigdataResumeCount = PbgTask::where(function ($q) {
$q->where(function ($q2) {
$q2->whereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%fungsi usaha%'])
->orWhereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%sebagai tempat usaha%']);
})
->whereIn("status", PbgTaskStatus::getNonVerified());
})
->where('is_valid', true)
->whereYear('task_created_at', $year)
->whereExists(function ($query) {
$query->select(DB::raw(1))
->from('pbg_task_detail_data_lists')
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
->where('pbg_task_detail_data_lists.data_type', 2)
->where('pbg_task_detail_data_lists.status', '!=', 1);
})
->count();
break;
case 'business-dlh':
$bigdataResumeCount = PbgTask::where(function ($q) {
$q->where(function ($q2) {
$q2->whereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%fungsi usaha%'])
->orWhereRaw("LOWER(TRIM(function_type)) LIKE ?", ['%sebagai tempat usaha%']);
})
->whereIn("status", PbgTaskStatus::getNonVerified());
})
->where('is_valid', true)
->whereYear('task_created_at', $year)
->whereExists(function ($query) {
$query->select(DB::raw(1))
->from('pbg_task_detail_data_lists')
->whereColumn('pbg_task_detail_data_lists.pbg_task_uuid', 'pbg_task.uuid')
->where('pbg_task_detail_data_lists.data_type', 5)
->where('pbg_task_detail_data_lists.status', '!=', 1);
})
->count();
break;
default:
Log::info('Unknown filter for consistency validation', [
'filter' => $filter,
'year' => $year
]);
return;
}
if ($bigdataResumeCount !== null) {
$isConsistent = ($actualCount === $bigdataResumeCount);
Log::info('RequestAssignment vs BigdataResume consistency check', [
'filter' => $filter,
'year' => $year,
'request_assignment_count' => $actualCount,
'bigdata_resume_count' => $bigdataResumeCount,
'is_consistent' => $isConsistent,
'difference' => $actualCount - $bigdataResumeCount
]);
if (!$isConsistent) {
Log::warning('INCONSISTENCY DETECTED between RequestAssignment and BigdataResume', [
'filter' => $filter,
'year' => $year,
'request_assignment_count' => $actualCount,
'bigdata_resume_count' => $bigdataResumeCount,
'difference' => $actualCount - $bigdataResumeCount
]);
}
}
} catch (\Exception $e) {
Log::error('Error in consistency validation', [
'error' => $e->getMessage(),
'filter' => $filter,
'year' => $year
]);
}
}
}

View File

@@ -20,7 +20,7 @@ class RolesController extends Controller
$query = $query->where('name', 'like', '%'. $request->get('search') . '%');
}
return response()->json($query->paginate());
return response()->json($query->paginate(config('app.paginate_per_page', 50)));
}
/**

View File

@@ -4,8 +4,16 @@ namespace App\Http\Controllers\Api;
use App\Enums\ImportDatasourceStatus;
use App\Http\Controllers\Controller;
use App\Jobs\RetrySyncronizeJob;
use App\Jobs\ScrapingDataJob;
use App\Jobs\SyncronizeSIMBG;
use App\Models\ImportDatasource;
use App\Traits\GlobalApiResponse;
use App\Services\ServiceTokenSIMBG;
use GuzzleHttp\Client;
use App\Services\ServiceGoogleSheet;
use App\Services\ServicePbgTask;
use App\Services\ServiceTabPbgTask;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Http\Request;
@@ -22,40 +30,33 @@ class ScrapingController extends Controller
return $this->resError("Failed to execute while processing another scraping");
}
// run service artisan command
Artisan::call("app:execute-scraping");
return $this->resSuccess("Success execute scraping service please wait");
// use ole schema synchronization
// dispatch(new SyncronizeSIMBG());
// use new schema synchronization
dispatch(new ScrapingDataJob());
return $this->resSuccess(["message" => "Success execute scraping service on background, check status for more"]);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
public function retry_syncjob(string $import_datasource_id){
try{
/**
* Display the specified resource.
*/
public function show(string $id)
{
//
}
$import_datasource = ImportDatasource::find($import_datasource_id);
if(!$import_datasource){
return $this->resError("Invalid import datasource id", null, 404);
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
{
//
dispatch(new RetrySyncronizeJob($import_datasource->id));
return response()->json([
"success" => true,
"message" => "Retrying scrape job on background, check status for more"
]);
}catch(\Exception $e){
return response()->json([
"success" => false,
"message" => "Failed to retry sync job",
"error" => $e->getMessage()
]);
}
}
}

View File

@@ -26,6 +26,10 @@ class SpatialPlanningController extends Controller
$search = $request->input('search', '');
$query = SpatialPlanning::query();
// Only include spatial plannings that are not yet issued (is_terbit = false)
$query->where('is_terbit', false);
if ($search) {
$query->where(function ($q) use ($search) {
$q->where('name', 'like', "%$search%")
@@ -42,9 +46,11 @@ class SpatialPlanningController extends Controller
// Menambhakan nomor urut (No)
$start = ($spatialPlannings->currentPage()-1) * $perPage + 1;
// Tambahkan nomor urut ke dalam data
// Tambahkan nomor urut ke dalam data (calculated_retribution sudah auto-append)
$data = $spatialPlannings->map(function ($item, $index) use ($start) {
return array_merge($item->toArray(), ['no' => $start + $index]);
$itemArray = $item->toArray();
$itemArray['no'] = $start + $index;
return $itemArray;
});
info($data);
@@ -104,9 +110,10 @@ class SpatialPlanningController extends Controller
/**
* Display the specified resource.
*/
public function show(SpatialPlanning $spatialPlanning): SpatialPlanning
public function show(SpatialPlanning $spatialPlanning): array
{
return $spatialPlanning;
// calculated_retribution and formatted_retribution are already appended via $appends
return $spatialPlanning->toArray();
}
/**

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\TaskAssignmentsResource;
use App\Models\TaskAssignment;
use Illuminate\Http\Request;
class TaskAssignmentsController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request, $uuid)
{
try{
$query = TaskAssignment::query()
->where('pbg_task_uid', $uuid)
->orderBy('id', 'desc');
if ($request->filled('search')) {
$query->where('name', 'like', "%{$request->get('search')}%")
->orWhere('email', 'like', "%{$request->get('search')}%");
}
return TaskAssignmentsResource::collection($query->paginate(config('app.paginate_per_page', 50)));
}catch(\Exception $exception){
return response()->json(['message' => $exception->getMessage()], 500);
}
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(string $id)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
{
//
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace App\Http\Controllers\Api;
use App\Exports\TaxationsExport;
use App\Http\Controllers\Controller;
use App\Http\Requests\ExcelUploadRequest;
use App\Http\Requests\TaxationsRequest;
use App\Http\Resources\TaxationsResource;
use App\Imports\TaxationsImport;
use Illuminate\Http\Request;
use App\Models\Tax;
use Illuminate\Support\Facades\Log;
use Maatwebsite\Excel\Facades\Excel;
class TaxationsController extends Controller
{
public function index(Request $request)
{
try{
$query = Tax::query()->orderBy('id', 'desc');
if($request->has('search') && !empty($request->get('search'))){
$query->where('tax_no', 'like', '%'. $request->get('search') . '%')
->orWhere('wp_name', 'like', '%'. $request->get('search') . '%')
->orWhere('business_name', 'like', '%'. $request->get('search') . '%');
}
return TaxationsResource::collection($query->paginate(config('app.paginate_per_page', 50)));
}catch(\Exception $e){
Log::info($e->getMessage());
return response()->json([
'error' => 'Failed to get data',
'message' => $e->getMessage()
], 500);
}
}
public function upload(ExcelUploadRequest $request)
{
try{
if(!$request->hasFile('file')){
return response()->json([
'error' => 'No file provided'
], 400);
}
$file = $request->file('file');
Excel::import(new TaxationsImport, $file);
return response()->json(['message' => 'File uploaded successfully'], 200);
}catch(\Exception $e){
Log::info($e->getMessage());
return response()->json([
'error' => 'Failed to upload file',
'message' => $e->getMessage()
], 500);
}
}
public function export(Request $request)
{
return Excel::download(new TaxationsExport, 'pajak_per_kecamatan.xlsx');
}
public function delete(Request $request)
{
try{
$tax = Tax::find($request->id);
$tax->delete();
return response()->json(['message' => 'Data deleted successfully'], 200);
}catch(\Exception $e){
Log::info($e->getMessage());
return response()->json([
'error' => 'Failed to delete data',
'message' => $e->getMessage()
], 500);
}
}
public function update(TaxationsRequest $request, string $id)
{
try{
$tax = Tax::find($id);
if($tax){
$tax->update($request->validated());
return response()->json(['message' => 'Successfully updated', new TaxationsResource($tax)]);
} else {
return response()->json(['message' => 'Tax not found'], 404);
}
}catch(\Exception $e){
Log::info($e->getMessage());
return response()->json([
'error' => 'Failed to update tax',
'message' => $e->getMessage()
], 500);
}
}
}

View File

@@ -39,7 +39,7 @@ class TourismController extends Controller
$tourisms->village_name = $village ? $village->village_name : null;
$district = DB::table('districts')->where('district_code', $tourisms->district_code)->first();
$tourisms->district_name = $village ? $village->village_name : null;
$tourisms->district_name = $district ? $district->district_name : null;
return $tourisms;
});

View File

@@ -11,6 +11,7 @@ use App\Traits\GlobalApiResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class UsersController extends Controller
{
@@ -29,12 +30,15 @@ class UsersController extends Controller
public function index(Request $request){
$query = User::query();
if($request->has('search') && !empty($request->get("search"))){
$query->where('name', 'LIKE', '%'.$request->get('search').'%');
$query->where('name', 'LIKE', '%'.$request->get('search').'%')
->orWhere('email', 'LIKE', '%'.$request->get('search').'%');
}
return UserResource::collection($query->paginate());
return UserResource::collection($query->paginate(config('app.paginate_per_page', 50)));
}
public function logout(Request $request){
$request->user()->tokens()->delete();
\Laravel\Sanctum\PersonalAccessToken::where('tokenable_id', $request->user()->id)
->where('tokenable_type', get_class($request->user()))
->delete();
return response()->json(['message' => 'logged out successfully']);
}
public function store(UsersRequest $request){
@@ -83,4 +87,17 @@ class UsersController extends Controller
return response()->json(['message' => $e->getMessage()],500);
}
}
public function destroy($id){
try{
$user = User::findOrFail($id);
DB::beginTransaction();
$user->delete();
DB::commit();
return response()->json(['message' => 'Successfully deleted'], 200);
}catch(\Exception $e){
Log::error('Failed to delete user: '. $e->getMessage());
return response()->json(['message' => 'Failed to delete user'],500);
}
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Http\Controllers\Approval;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class ApprovalController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
return view('approval.index');
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(string $id)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(string $id)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
{
//
}
}

View File

@@ -36,13 +36,77 @@ class AuthenticatedSessionController extends Controller
// Ambil user yang sedang login
$user = Auth::user();
// Buat token untuk API
$token = $user->createToken(env('APP_KEY'))->plainTextToken;
// Hapus token lama jika ada
\Laravel\Sanctum\PersonalAccessToken::where('tokenable_id', $user->id)
->where('tokenable_type', get_class($user))
->delete();
// Simpan token di session (bisa digunakan di JavaScript)
// Buat token untuk API dengan scope dan expiration
$tokenName = config('app.name', 'Laravel') . '-' . $user->id . '-' . time();
// Token dengan scope (opsional)
$token = $user->createToken($tokenName, ['*'], now()->addDays(30))->plainTextToken;
// Simpan token di session untuk digunakan di frontend
session(['api_token' => $token]);
// Simpan timestamp login untuk validasi multi-user
session(['login_timestamp' => now()->timestamp]);
session(['user_id' => $user->id]);
return redirect()->intended(RouteServiceProvider::HOME);
// Append menu_id dynamically to HOME
$menuId = optional(\App\Models\Menu::where('name', 'Dashboard Pimpinan SIMBG')->first())->id;
$home = RouteServiceProvider::HOME . ($menuId ? ('?menu_id=' . $menuId) : '');
return redirect()->intended($home);
}
/**
* Generate API token for authenticated user
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function generateApiToken(Request $request)
{
$user = Auth::user();
if (!$user) {
return response()->json(['error' => 'Unauthorized'], 401);
}
// Delete existing tokens
\Laravel\Sanctum\PersonalAccessToken::where('tokenable_id', $user->id)
->where('tokenable_type', get_class($user))
->delete();
// Generate new token
$tokenName = config('app.name', 'Laravel') . '-' . $user->id . '-' . time();
$token = $user->createToken($tokenName, ['*'], now()->addDays(30))->plainTextToken;
return response()->json([
'token' => $token,
'token_type' => 'Bearer',
'expires_in' => 30 * 24 * 60 * 60, // 30 days in seconds
]);
}
/**
* Revoke API token for authenticated user
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function revokeApiToken(Request $request)
{
$user = Auth::user();
if (!$user) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$user->tokens()->delete();
return response()->json(['message' => 'All tokens revoked successfully']);
}
/**
@@ -54,7 +118,9 @@ class AuthenticatedSessionController extends Controller
public function destroy(Request $request)
{
if($request->user()){
$request->user()->tokens()->delete();
\Laravel\Sanctum\PersonalAccessToken::where('tokenable_id', $request->user()->id)
->where('tokenable_type', get_class($request->user()))
->delete();
}
Auth::guard('web')->logout();

View File

View File

View File

0
app/Http/Controllers/Auth/NewPasswordController.php Executable file → Normal file
View File

View File

0
app/Http/Controllers/Auth/RegisteredUserController.php Executable file → Normal file
View File

0
app/Http/Controllers/Auth/VerifyEmailController.php Executable file → Normal file
View File

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class BigdataResumesController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
return view('bigdata-resumes.index');
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Display the specified resource.
*/
public function show(string $id)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(string $id)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
{
//
}
}

View File

@@ -4,63 +4,40 @@ namespace App\Http\Controllers;
use App\Models\BusinessOrIndustry;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
class BusinessOrIndustriesController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
public function index(Request $request)
{
return view('business-industries.index');
$menuId = $request->query('menu_id') ?? $request->input('menu_id');
$permissions = $this->permissions[$menuId]?? []; // Avoid undefined index error
$creator = $permissions['allow_create'] ?? 0;
$updater = $permissions['allow_update'] ?? 0;
$destroyer = $permissions['allow_destroy'] ?? 0;
return view('business-industries.index', compact('creator', 'updater', 'destroyer','menuId'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
public function create(Request $request)
{
return view("business-industries.create");
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(string $id)
{
//
$menuId = $request->query('menu_id') ?? $request->input('menu_id');
return view("business-industries.create", compact('menuId'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(string $id)
public function edit(string $id, Request $request)
{
$menuId = $request->query('menu_id') ?? $request->input('menu_id');
$data = BusinessOrIndustry::findOrFail($id);
return view('business-industries.edit', compact('data'));
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
{
//
return view('business-industries.edit', compact('data', 'menuId'));
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Controllers\ChatbotPimpinan;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class ChatbotPimpinanController extends Controller
{
/**
* Display a listing of the resource
*/
public function index()
{
return view('chatbot-pimpinan.index');
}
}

49
app/Http/Controllers/Controller.php Executable file → Normal file
View File

@@ -2,7 +2,54 @@
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Auth;
abstract class Controller
{
//
protected array $permissions = [];
public function __construct()
{
if (!Auth::check()) {
return;
}
$this->setUserPermissions();
}
protected function setUserPermissions()
{
$user = Auth::user();
if (!$user) {
return;
}
$menus = $user->roles()
->with(['menus' => function ($query) {
$query->select('menus.id', 'menus.name')
->withPivot(['allow_show' ,'allow_create', 'allow_update', 'allow_destroy']);
}])
->get()
->pluck('menus')
->flatten()
->unique('id');
// Store permissions in an associative array
foreach ($menus as $menu) {
$this->permissions[$menu->id] = [
'allow_show' => $menu->pivot->allow_show ?? 0,
'allow_create' => $menu->pivot->allow_create ?? 0,
'allow_update' => $menu->pivot->allow_update ?? 0,
'allow_destroy' => $menu->pivot->allow_destroy ?? 0,
];
}
// Share permissions globally in views
view()->share('permissions', $this->permissions);
}
public function getPermissions()
{
return $this->permissions;
}
}

View File

@@ -4,23 +4,34 @@ namespace App\Http\Controllers;
use App\Models\Customer;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class CustomersController extends Controller
{
public function index()
public function index(Request $request)
{
return view('customers.index');
$menuId = $request->query('menu_id') ?? $request->input('menu_id');
$permissions = $this->permissions[$menuId]?? []; // Avoid undefined index error
$creator = $permissions['allow_create'] ?? 0;
$updater = $permissions['allow_update'] ?? 0;
$destroyer = $permissions['allow_destroy'] ?? 0;
return view('customers.index', compact('creator', 'updater', 'destroyer', 'menuId'));
}
public function create()
public function create(Request $request)
{
return view('customers.create');
$menuId = $request->query('menu_id');
return view('customers.create', compact('menuId'));
}
public function edit(string $id)
public function edit(Request $request, string $id)
{
$data = Customer::findOrFail($id);
return view('customers.edit', compact('data'));
$menuId = $request->query('menu_id');
return view('customers.edit', compact('data', 'menuId'));
}
public function upload(){
return view('customers.upload');
public function upload(Request $request){
$menuId = $request->query('menu_id');
return view('customers.upload', compact('menuId'));
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Dashboards;
use App\Http\Controllers\Controller;
use App\Models\ImportDatasource;
use App\Models\Menu;
use Illuminate\Http\Request;
class BigDataController extends Controller
@@ -12,11 +13,21 @@ class BigDataController extends Controller
$latest_import_datasource = ImportDatasource::latest()->first();
$latest_created = $latest_import_datasource ?
$latest_import_datasource->created_at->format("j F Y H:i:s") : null;
return view('dashboards.bigdata', compact('latest_created'));
$menus = Menu::all();
return view('dashboards.bigdata', compact('latest_created', 'menus'));
}
public function pbg()
{
return view('dashboards.pbg');
}
public function leader()
{
$latest_import_datasource = ImportDatasource::latest()->first();
$latest_created = $latest_import_datasource ?
$latest_import_datasource->created_at->format("j F Y H:i:s") : null;
$menus = Menu::all();
return view('dashboards.leader', compact('latest_created', 'menus'));
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Http\Controllers\Dashboards;
use App\Http\Controllers\Controller;
use App\Models\Menu;
use Illuminate\Http\Request;
class PotentialsController extends Controller
{
public function inside_system(){
$menus = Menu::all();
return view('dashboards.potentials.inside_system', compact('menus'));
}
public function outside_system(){
return view('dashboards.potentials.outside_system');
}
}

View File

@@ -6,15 +6,23 @@ use App\Http\Controllers\Controller;
use App\Models\Advertisement;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
class AdvertisementController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
public function index(Request $request)
{
return view('data.advertisements.index');
$menuId = $request->query('menu_id', 0);
$permissions = $this->permissions[$menuId] ?? []; // Avoid undefined index error
$creator = $permissions['allow_create'] ?? 0;
$updater = $permissions['allow_update'] ?? 0;
$destroyer = $permissions['allow_destroy'] ?? 0;
return view('data.advertisements.index', compact('creator', 'updater', 'destroyer','menuId'));
}
/**
@@ -29,8 +37,9 @@ class AdvertisementController extends Controller
/**
* Show the form for creating a new resource.
*/
public function create()
public function create(Request $request)
{
$menuId = $request->query('menu_id', 0);
$title = 'Advertisement';
$subtitle = 'Create Data';
@@ -47,14 +56,15 @@ class AdvertisementController extends Controller
// $route = 'advertisements.create';
// info("AdvertisementController@edit diakses dengan ID: $title");
return view('data.advertisements.form', compact('title', 'subtitle', 'fields', 'fieldTypes', 'apiUrl', 'dropdownOptions'));
return view('data.advertisements.form', compact('title', 'subtitle', 'fields', 'fieldTypes', 'apiUrl', 'dropdownOptions','menuId'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit($id)
public function edit(Request $request, $id)
{
$menuId = $request->query('menu_id', 0);
info("AdvertisementController@edit diakses dengan ID: $id");
$title = 'Advertisement';
$subtitle = 'Update Data';
@@ -62,7 +72,7 @@ class AdvertisementController extends Controller
// Pastikan model ditemukan
if (!$modelInstance) {
info("AdvertisementController@edit: Model tidak ditemukan.");
return redirect()->route('advertisements.index')->with('error', 'Advertisement not found');
return redirect()->route('web.advertisements.index')->with('error', 'Advertisement not found');
}
// Mengambil dan memetakan village_name dan district_name
@@ -86,7 +96,7 @@ class AdvertisementController extends Controller
// $route = 'advertisements.update'; // Menggunakan route update untuk form edit
// info("AdvertisementController@edit diakses dengan route: $route");
return view('data.advertisements.form', compact('title', 'subtitle', 'modelInstance', 'fields', 'fieldTypes', 'apiUrl', 'dropdownOptions'));
return view('data.advertisements.form', compact('title', 'subtitle', 'modelInstance', 'fields', 'fieldTypes', 'apiUrl', 'dropdownOptions', 'menuId'));
}
private function getFields()

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Http\Controllers\Data;
use App\Http\Controllers\Controller;
use App\Models\PbgTaskGoogleSheet;
use Illuminate\Http\Request;
class GoogleSheetsController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
$menu_id = $request->query('menu_id');
$user_menu_permission = $this->permissions[$menu_id];
return view('data.google-sheet.index', compact('user_menu_permission'));
}
public function create()
{
return view('data.google-sheet.create');
}
public function show(string $id)
{
$data = PbgTaskGoogleSheet::find($id);
return view('data.google-sheet.show', compact('data'));
}
public function edit(string $id)
{
return view('data.google-sheet.edit');
}
}

View File

@@ -11,24 +11,33 @@ class SpatialPlanningController extends Controller
/**
* Display a listing of the resource.
*/
public function index()
public function index(Request $request)
{
return view('data.spatialPlannings.index');
$menuId = $request->query('menu_id', 0);
$permissions = $this->permissions[$menuId] ?? []; // Avoid undefined index error
$creator = $permissions['allow_create'] ?? 0;
$updater = $permissions['allow_update'] ?? 0;
$destroyer = $permissions['allow_destroy'] ?? 0;
return view('data.spatialPlannings.index', compact('creator', 'updater', 'destroyer','menuId'));
}
/**
* show the form for creating a new resource.
*/
public function bulkCreate()
public function bulkCreate(Request $request)
{
return view('data.spatialPlannings.form-upload');
$menuId = $request->query('menu_id', 0);
return view('data.spatialPlannings.form-upload', compact('menuId'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
public function create(Request $request)
{
$menuId = $request->query('menu_id', 0);
$title = 'Rencana Tata Ruang';
$subtitle = "Create Data";
@@ -39,30 +48,15 @@ class SpatialPlanningController extends Controller
$fieldTypes = $this->getFieldTypes();
$apiUrl = url('/api/spatial-plannings');
return view('data.spatialPlannings.form', compact('title', 'subtitle', 'fields', 'fieldTypes', 'apiUrl', 'dropdownOptions'));
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(string $id)
{
//
return view('data.spatialPlannings.form', compact('title', 'subtitle', 'fields', 'fieldTypes', 'apiUrl', 'dropdownOptions','menuId'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(string $id)
public function edit(Request $request,string $id)
{
$menuId = $request->query('menu_id', 0);
$title = 'Rencana Tata Ruang';
$subtitle = 'Update Data';
@@ -78,23 +72,7 @@ class SpatialPlanningController extends Controller
$fieldTypes = $this->getFieldTypes();
$apiUrl = url('/api/spatial-plannings');
return view('data.spatialPlannings.form', compact('title', 'subtitle', 'modelInstance', 'fields', 'fieldTypes', 'apiUrl', 'dropdownOptions'));
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
{
//
return view('data.spatialPlannings.form', compact('title', 'subtitle', 'modelInstance', 'fields', 'fieldTypes', 'apiUrl', 'dropdownOptions','menuId'));
}
private function getFields()
@@ -104,9 +82,15 @@ class SpatialPlanningController extends Controller
"kbli"=> "KBLI",
"activities"=> "Kegiatan",
"area"=> "Luas (m2)",
"land_area"=> "Luas Lahan (m2)",
"location"=> "Lokasi",
"number"=> "Nomor",
"date"=> "Tanggal",
"site_bcr"=> "BCR",
"building_function"=> "Fungsi Bangunan",
"business_type_info"=> "Jenis Usaha",
"is_terbit"=> "Status Terbit",
"calculated_retribution"=> "Retribusi",
];
}
@@ -117,9 +101,15 @@ class SpatialPlanningController extends Controller
"kbli"=> "text",
"activities"=> "text",
"area"=> "text",
"land_area"=> "text",
"location"=> "text",
"number"=> "text",
"date"=> "date",
"site_bcr"=> "text",
"building_function"=> "text",
"business_type_info"=> "readonly",
"is_terbit"=> "select",
"calculated_retribution"=> "readonly",
];
}
}

View File

@@ -6,30 +6,39 @@ use App\Http\Controllers\Controller;
use App\Models\Tourism;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
class TourismController extends Controller
{
/**
* Display a listing of the resource
*/
public function index()
public function index(Request $request)
{
return view('data.tourisms.index');
$menuId = $request->query('menu_id', 0);
$permissions = $this->permissions[$menuId] ?? []; // Avoid undefined index error
$creator = $permissions['allow_create'] ?? 0;
$updater = $permissions['allow_update'] ?? 0;
$destroyer = $permissions['allow_destroy'] ?? 0;
return view('data.tourisms.index', compact('creator', 'updater', 'destroyer', 'menuId'));
}
/**
* show the form for creating a new rsource.
*/
public function bulkCreate()
public function bulkCreate(Request $request)
{
return view('data.tourisms.form-upload');
$menuId = $request->query('menu_id', 0);
return view('data.tourisms.form-upload', compact('menuId'));
}
/**
* Show th form for creating a new resource
*/
public function create()
public function create(Request $request)
{
$menuId = $request->query('menu_id', 0);
$title = 'Pariwisata';
$subtitle = 'Create Data';
@@ -44,21 +53,22 @@ class TourismController extends Controller
$apiUrl = url('/api/tourisms');
return view('data.tourisms.form', compact('title', 'subtitle', 'fields', 'fieldTypes', 'apiUrl', 'dropdownOptions'));
return view('data.tourisms.form', compact('title', 'subtitle', 'fields', 'fieldTypes', 'apiUrl', 'dropdownOptions', 'menuId'));
}
/**
* show the form for editing the specified resource.
*/
public function edit($id)
public function edit(Request $request, $id)
{
$menuId = $request->query('menu_id', 0);
$title = 'Pariwisata';
$subtitle = 'Update Data';
$modelInstance = Tourism::find($id);
// Pastikan model ditemukan
if (!$modelInstance) {
return redirect()->route('tourisms.index') ->with('error', 'Pariwisata tidak ditemukan');
return redirect()->route('web-tourisms.index') ->with('error', 'Pariwisata tidak ditemukan');
}
// Mengambil dan memetakan village_name dan district_name
@@ -78,7 +88,7 @@ class TourismController extends Controller
$apiUrl = url('/api/tourisms');
return view('data.tourisms.form', compact('title', 'subtitle', 'modelInstance', 'fields', 'fieldTypes', 'apiUrl', 'dropdownOptions'));
return view('data.tourisms.form', compact('title', 'subtitle', 'modelInstance', 'fields', 'fieldTypes', 'apiUrl', 'dropdownOptions', 'menuId'));
}
private function getFields()

View File

@@ -6,30 +6,39 @@ use App\Http\Controllers\Controller;
use App\Models\Umkm;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
class UmkmController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
public function index(Request $request)
{
return view('data.umkm.index');
$menuId = $request->query('menu_id', 0);
$permissions = $this->permissions[$menuId] ?? []; // Avoid undefined index error
$creator = $permissions['allow_create'] ?? 0;
$updater = $permissions['allow_update'] ?? 0;
$destroyer = $permissions['allow_destroy'] ?? 0;
return view('data.umkm.index', compact('creator', 'updater', 'destroyer', 'menuId'));
}
/**
* Show the form for creating a new resource.
*/
public function bulkCreate()
public function bulkCreate(Request $request)
{
return view('data.umkm.form-upload');
$menuId = $request->query('menu_id', 0);
return view('data.umkm.form-upload', compact('menuId'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
public function create(Request $request)
{
$menuId = $request->query('menu_id', 0);
$title = 'UMKM';
$subtitle = 'Create Data';
@@ -47,20 +56,21 @@ class UmkmController extends Controller
$apiUrl = url('/api/umkm');
return view('data.umkm.form', compact('title', 'subtitle', 'fields', 'fieldTypes', 'apiUrl', 'dropdownOptions'));
return view('data.umkm.form', compact('title', 'subtitle', 'fields', 'fieldTypes', 'apiUrl', 'dropdownOptions','menuId'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit($id)
public function edit(Request $request,$id)
{
$menuId = $request->query('menu_id', 0);
$title = 'UMKM';
$subtitle = 'Update Data';
$modelInstance = Umkm::find($id);
// Pastikan model ditemukan
if (!$modelInstance) {
return redirect()->route('umkm.index')->with('error', 'Umkm not found');
return redirect()->route('web-umkm.index')->with('error', 'Umkm not found');
}
// Mengambil dan memetakan village_name dan district_name
@@ -96,7 +106,7 @@ class UmkmController extends Controller
$apiUrl = url('/api/umkm');
// dd($modelInstance->business_form_id, $dropdownOptions['business_form']);
return view('data.umkm.form', compact('title', 'subtitle', 'modelInstance', 'fields', 'fieldTypes', 'apiUrl', 'dropdownOptions'));
return view('data.umkm.form', compact('title', 'subtitle', 'modelInstance', 'fields', 'fieldTypes', 'apiUrl', 'dropdownOptions','menuId'));
}
private function getFields()

View File

@@ -8,23 +8,31 @@ use Exception;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request as IndexRequest;
class DataSettingController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
public function index(IndexRequest $request)
{
return view("data-settings.index");
$menuId = $request->query('menu_id') ?? $request->input('menu_id');
$permissions = $this->permissions[$menuId]?? []; // Avoid undefined index error
$creator = $permissions['allow_create'] ?? 0;
$updater = $permissions['allow_update'] ?? 0;
$destroyer = $permissions['allow_destroy'] ?? 0;
return view("data-settings.index", compact('creator', 'updater', 'destroyer','menuId'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
public function create(IndexRequest $request)
{
return view("data-settings.create");
$menuId = $request->query('menu_id') ?? $request->input('menu_id');
return view("data-settings.create", compact('menuId'));
}
/**
@@ -57,14 +65,15 @@ class DataSettingController extends Controller
/**
* Show the form for editing the specified resource.
*/
public function edit(string $id)
public function edit(IndexRequest $request,string $id)
{
try{
$data = DataSetting::findOrFail($id);
$menuId = $request->query('menu_id') ?? $request->input('menu_id');
if(empty($data)){
return redirect()->route('data-settings.index')->with('error', 'Invalid id');
}
return view("data-settings.edit", compact("data"));
return view("data-settings.edit", compact("data", 'menuId'));
}catch(Exception $ex){
return redirect()->route("data-settings.index")->with("error", "Invalid id");
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class InvitationsController extends Controller
{
public function index(Request $request){
return view('invitations.index');
}
}

View File

@@ -13,6 +13,7 @@ use Illuminate\Support\Facades\Hash;
use App\Models\User;
use App\Traits\GlobalApiResponse;
use Illuminate\Auth\Events\Registered;
use Illuminate\Support\Facades\Auth;
class UsersController extends Controller
{
@@ -21,13 +22,20 @@ class UsersController extends Controller
$users = User::all();
return $this->resSuccess($users);
}
public function index(){
public function index(Request $request){
$menuId = $request->query('menu_id') ?? $request->input('menu_id');
$permissions = $this->permissions[$menuId]?? []; // Avoid undefined index error
$creator = $permissions['allow_create'] ?? 0;
$updater = $permissions['allow_update'] ?? 0;
$destroyer = $permissions['allow_destroy'] ?? 0;
$users = User::paginate();
return view('master.users.index', compact('users'));
return view('master.users.index', compact('users', 'creator', 'updater', 'destroyer','menuId'));
}
public function create(){
public function create(Request $request){
$menuId = $request->query('menu_id') ?? $request->input('menu_id');
$roles = Role::all();
return view('master.users.create', compact('roles'));
return view('master.users.create', compact('roles', 'menuId'));
}
public function store(UsersRequest $request){
$request->validate([
@@ -65,10 +73,11 @@ class UsersController extends Controller
$user = User::find($id);
return view('master.users.show', compact('user'));
}
public function edit($id){
public function edit(Request $request, $id){
$menuId = $request->query('menu_id') ?? $request->input('menu_id');
$user = User::find($id);
$roles = Role::all();
return view('master.users.edit', compact('user', 'roles'));
return view('master.users.edit', compact('user', 'roles', 'menuId'));
}
public function update(Request $request, $id){
$user = User::find($id);

View File

@@ -12,18 +12,35 @@ class MenusController extends Controller
/**
* Display a listing of the resource.
*/
public function index()
public function index(Request $request)
{
return view('menus.index');
$menuId = (int) $request->query('menu_id', 0);
$permissions = $this->permissions[$menuId] ?? []; // Avoid undefined index error
$creator = $permissions['allow_create'] ?? 0;
$updater = $permissions['allow_update'] ?? 0;
$destroyer = $permissions['allow_destroy'] ?? 0;
return view('menus.index', compact('creator', 'updater', 'destroyer', 'menuId'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
public function create(Request $request)
{
$parent_menus = Menu::whereNull('parent_id')->get();
return view("menus.create", compact('parent_menus'));
$menuId = $request->query('menu_id'); // Get menu_id from request
$menu = Menu::with('children')->find($menuId); // Find the menu
// Get IDs of all child menus to exclude
$excludedIds = $menu ? $this->getChildMenuIds($menu) : [$menuId];
// Fetch only menus that have children and are not in the excluded list
$parent_menus = Menu::whereHas('children')
->whereNotIn('id', $excludedIds)
->get();
return view("menus.create", compact('parent_menus', 'menuId'));
}
/**
@@ -56,11 +73,16 @@ class MenusController extends Controller
/**
* Show the form for editing the specified resource.
*/
public function edit(string $id)
public function edit(string $id, Request $request)
{
$menu = Menu::findOrFail($id);
$parent_menus = Menu::whereNull('parent_id')->where('id','!=',$id)->get();
return view("menus.edit", compact('menu','parent_menus'));
$menuId = $request->query('menu_id');
$menu = Menu::with('children')->find($id);
$excludedIds = $menu ? $this->getChildMenuIds($menu) : [$id];
$parent_menus = Menu::whereHas('children')
->whereNotIn('id', $excludedIds)
->get();
return view("menus.edit", compact('menu','parent_menus', 'menuId'));
}
/**
@@ -110,4 +132,15 @@ class MenusController extends Controller
$child->delete();
}
}
private function getChildMenuIds($menu)
{
$ids = [$menu->id]; // Start with current menu ID
foreach ($menu->children as $child) {
$ids = array_merge($ids, $this->getChildMenuIds($child)); // Recursively fetch children
}
return $ids;
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class PaymentRecapsController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
return view('payment-recaps.index');
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(string $id)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(string $id)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
{
//
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers;
use App\Models\PbgTask;
use App\Models\PbgTaskAttachment;
use Illuminate\Http\Request;
class PbgTaskAttachmentsController extends Controller
{
public function show(string $id, Request $request){
try{
$title = $request->get('type') == "berita-acara" ? "Berita Acara" : "Bukti Bayar";
$data = PbgTaskAttachment::findOrFail($id);
$pbg = PbgTask::findOrFail($data->pbg_task_id);
return view('pbg-task-attachment.show', compact('data', 'pbg', 'title'));
}catch(\Exception $e){
return view('pages.404');
}
}
}

View File

@@ -0,0 +1,178 @@
<?php
namespace App\Http\Controllers;
use App\Enums\PbgTaskApplicationTypes;
use App\Enums\PbgTaskStatus;
use App\Http\Resources\TaskAssignmentsResource;
use App\Models\PbgTask;
use App\Models\TaskAssignment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
class QuickSearchController extends Controller
{
public function index(){
return view("quick-search.index");
}
public function public_search(){
return view("public-search.index");
}
public function search_result(Request $request){
$keyword = $request->get("keyword");
return view('quick-search.result', compact('keyword'));
}
public function quick_search_datatable(Request $request)
{
try {
// Gunakan subquery untuk performa yang lebih baik dan menghindari duplikasi
$query = PbgTask::select([
'pbg_task.*',
DB::raw('(SELECT name_building FROM pbg_task_details WHERE pbg_task_details.pbg_task_uid = pbg_task.uuid LIMIT 1) as name_building'),
DB::raw('(SELECT nilai_retribusi_bangunan FROM pbg_task_retributions WHERE pbg_task_retributions.pbg_task_uid = pbg_task.uuid LIMIT 1) as nilai_retribusi_bangunan'),
DB::raw('(SELECT note FROM pbg_statuses WHERE pbg_statuses.pbg_task_uuid = pbg_task.uuid LIMIT 1) as note')
])
->orderBy('pbg_task.id', 'desc');
if ($request->filled('search')) {
$search = trim($request->get('search'));
$query->where(function ($q) use ($search) {
$q->where('pbg_task.registration_number', 'LIKE', "%$search%")
->orWhere('pbg_task.name', 'LIKE', "%$search%")
->orWhere('pbg_task.owner_name', 'LIKE', "%$search%")
->orWhere('pbg_task.address', 'LIKE', "%$search%")
->orWhereExists(function ($subQuery) use ($search) {
$subQuery->select(DB::raw(1))
->from('pbg_task_details')
->whereColumn('pbg_task_details.pbg_task_uid', 'pbg_task.uuid')
->where('pbg_task_details.name_building', 'LIKE', "%$search%");
});
});
}
return response()->json($query->paginate());
} catch (\Throwable $e) {
Log::error("Error fetching datatable data: " . $e->getMessage());
return response()->json([
'message' => 'Terjadi kesalahan saat mengambil data.',
'error' => $e->getMessage(),
], 500);
}
}
public function public_search_datatable(Request $request)
{
try {
// Hanya proses jika ada keyword search
if (!$request->filled('search') || trim($request->get('search')) === '') {
return response()->json([
'data' => [],
'total' => 0,
'current_page' => 1,
'last_page' => 1,
'per_page' => 15,
'from' => null,
'to' => null
]);
}
$search = trim($request->get('search'));
// Validasi minimal 3 karakter
if (strlen($search) < 3) {
return response()->json([
'data' => [],
'total' => 0,
'current_page' => 1,
'last_page' => 1,
'per_page' => 15,
'from' => null,
'to' => null,
'message' => 'Minimal 3 karakter untuk pencarian'
]);
}
// Gunakan subquery untuk performa yang lebih baik dan menghindari duplikasi
$query = PbgTask::select([
'pbg_task.*',
DB::raw('(SELECT name_building FROM pbg_task_details WHERE pbg_task_details.pbg_task_uid = pbg_task.uuid LIMIT 1) as name_building'),
DB::raw('(SELECT nilai_retribusi_bangunan FROM pbg_task_retributions WHERE pbg_task_retributions.pbg_task_uid = pbg_task.uuid LIMIT 1) as nilai_retribusi_bangunan'),
DB::raw('(SELECT note FROM pbg_statuses WHERE pbg_statuses.pbg_task_uuid = pbg_task.uuid LIMIT 1) as note')
])
->where(function ($q) use ($search) {
$q->where('pbg_task.registration_number', 'LIKE', "%$search%")
->orWhere('pbg_task.name', 'LIKE', "%$search%")
->orWhere('pbg_task.owner_name', 'LIKE', "%$search%")
->orWhere('pbg_task.address', 'LIKE', "%$search%")
->orWhereExists(function ($subQuery) use ($search) {
$subQuery->select(DB::raw(1))
->from('pbg_task_details')
->whereColumn('pbg_task_details.pbg_task_uid', 'pbg_task.uuid')
->where('pbg_task_details.name_building', 'LIKE', "%$search%");
});
})
->orderBy('pbg_task.id', 'desc');
$result = $query->paginate();
// Tambahkan message jika tidak ada hasil
if ($result->total() === 0) {
$result = $result->toArray();
$result['message'] = 'Tidak ada data yang ditemukan';
}
return response()->json($result);
} catch (\Throwable $e) {
Log::error("Error fetching datatable data: " . $e->getMessage());
return response()->json([
'message' => 'Terjadi kesalahan saat mengambil data.',
'error' => $e->getMessage(),
], 500);
}
}
public function show($id)
{
try {
$data = PbgTask::with([
'pbg_task_retributions',
'pbg_task_index_integrations',
'pbg_task_retributions.pbg_task_prasarana',
'pbg_status'
])->findOrFail($id);
$statusOptions = PbgTaskStatus::getStatuses();
$applicationTypes = PbgTaskApplicationTypes::labels();
return view("quick-search.detail", compact("data", 'statusOptions', 'applicationTypes'));
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
Log::warning("PbgTask with ID {$id} not found.");
return redirect()->route('quick-search.index')->with('error', 'Data tidak ditemukan.');
} catch (\Throwable $e) {
Log::error("Error in QuickSearchController@show: " . $e->getMessage());
return response()->view('pages.404', [], 500); // Optional: create `resources/views/errors/500.blade.php`
}
}
public function task_assignments(Request $request, $uuid){
try{
$query = TaskAssignment::query()
->where('pbg_task_uid', $uuid)
->orderBy('id', 'desc');
if ($request->filled('search')) {
$query->where('name', 'like', "%{$request->get('search')}%")
->orWhere('email', 'like', "%{$request->get('search')}%");
}
return TaskAssignmentsResource::collection($query->paginate(config('app.paginate_per_page', 50)));
}catch(\Exception $exception){
return response()->json(['message' => $exception->getMessage()], 500);
}
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Http\Controllers\Report;
use App\Http\Controllers\Controller;
use App\Models\Menu;
use Illuminate\Http\Request;
class GrowthReportsController extends Controller
{
public function index(){
return view('report.growth-report.index');
}
}

View File

@@ -15,7 +15,6 @@ class ReportTourismController extends Controller
public function index()
{
$tourismBasedKBLI = TourismBasedKBLI::all();
info($tourismBasedKBLI);
return view('report.tourisms.index', compact('tourismBasedKBLI'));
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ReportPaymentRecapsController extends Controller
{
public function index(Request $request){
return view('report-payment-recaps.index');
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ReportPbgPTSPController extends Controller
{
public function index(Request $request){
return view('report-pbg-ptsp.index');
}
}

View File

@@ -2,18 +2,37 @@
namespace App\Http\Controllers\RequestAssignment;
use App\Enums\PbgTaskApplicationTypes;
use App\Enums\PbgTaskFilterData;
use App\Http\Controllers\Controller;
use App\Models\PbgTask;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
use App\Enums\PbgTaskStatus;
class PbgTaskController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
public function index(Request $request)
{
return view('pbg_task.index');
$menuId = $request->query('menu_id') ?? $request->input('menu_id');
$filter = $request->query('filter');
$permissions = $this->permissions[$menuId]?? []; // Avoid undefined index error
$creator = $permissions['allow_create'] ?? 0;
$updater = $permissions['allow_update'] ?? 0;
$destroyer = $permissions['allow_destroy'] ?? 0;
return view('pbg_task.index', [
'creator' => $creator,
'updater' => $updater,
'destroyer' => $destroyer,
'filter' => $filter,
'filterOptions' => PbgTaskFilterData::getAllOptions(),
]);
}
/**
@@ -37,8 +56,24 @@ class PbgTaskController extends Controller
*/
public function show(string $id)
{
$data = PbgTask::with(['pbg_task_retributions','pbg_task_index_integrations', 'pbg_task_retributions.pbg_task_prasarana'])->findOrFail($id);
return view("pbg_task.show", compact("data"));
$data = PbgTask::with([
'pbg_task_retributions',
'pbg_task_index_integrations',
'pbg_task_retributions.pbg_task_prasarana',
'pbg_task_detail',
'pbg_status',
'dataLists' => function($query) {
$query->orderBy('data_type')->orderBy('name');
}
])->findOrFail($id);
// Group data lists by data_type for easier display
$dataListsByType = $data->dataLists->groupBy('data_type');
$statusOptions = PbgTaskStatus::getStatuses();
$applicationTypes = PbgTaskApplicationTypes::labels();
return view("pbg_task.show", compact("data", 'statusOptions', 'applicationTypes', 'dataListsByType'));
}
/**

View File

@@ -10,23 +10,31 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Auth;
class RolesController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
public function index(Request $request)
{
return view("roles.index");
$menuId = $request->query('menu_id') ?? $request->input('menu_id');
$permissions = $this->permissions[$menuId]?? []; // Avoid undefined index error
$creator = $permissions['allow_create'] ?? 0;
$updater = $permissions['allow_update'] ?? 0;
$destroyer = $permissions['allow_destroy'] ?? 0;
return view("roles.index", compact('creator', 'updater', 'destroyer', 'menuId'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
public function create(Request $request)
{
return view("roles.create");
$menuId = $request->query('menu_id');
return view("roles.create", compact('menuId'));
}
/**
@@ -59,10 +67,11 @@ class RolesController extends Controller
/**
* Show the form for editing the specified resource.
*/
public function edit(string $id)
public function edit(string $id, Request $request)
{
$menuId = $request->query('menu_id');
$role = Role::findOrFail($id);
return view("roles.edit", compact('role'));
return view("roles.edit", compact('role', 'menuId'));
}
/**
@@ -100,12 +109,13 @@ class RolesController extends Controller
}
}
public function menu_permission(string $role_id){
public function menu_permission(string $role_id, Request $request){
try{
$menuId = $request->query('menu_id');
$role = Role::findOrFail($role_id);
$menus = Menu::all();
$role_menus = RoleMenu::where('role_id', $role_id)->get() ?? collect();
return view('roles.role_menu', compact('role', 'menus', 'role_menus'));
return view('roles.role_menu', compact('role', 'menus', 'role_menus', 'menuId'));
}catch(\Exception $e){
return redirect()->back()->with("error", $e->getMessage());
}
@@ -113,8 +123,9 @@ class RolesController extends Controller
public function update_menu_permission(Request $request, string $role_id){
try{
$menuId = $request->query('menu_id');
$validateData = $request->validate([
"permissions" => "array",
"permissions" => "nullable|array",
"permissions.*.allow_show" => "nullable|boolean",
"permissions.*.allow_create" => "nullable|boolean",
"permissions.*.allow_update" => "nullable|boolean",
@@ -123,6 +134,13 @@ class RolesController extends Controller
$role = Role::find($role_id);
// Jika `permissions` tidak ada atau kosong, hapus semua permissions terkait
if (!isset($validateData['permissions']) || empty($validateData['permissions'])) {
$role->menus()->detach();
return redirect()->route("roles.index", ['menu_id' => $menuId])
->with('success', 'All menu permissions have been removed.');
}
$permissionsArray = [];
foreach ($validateData['permissions'] as $menu_id => $permission) {
$permissionsArray[$menu_id] = [
@@ -137,7 +155,7 @@ class RolesController extends Controller
// Sync will update existing records and insert new ones
$role->menus()->sync($permissionsArray);
return redirect()->route("role-menu.permission", $role_id)->with('success','Menu Permission updated successfully');
return redirect()->route("roles.index", ['menu_id' => $menuId])->with('success','Menu Permission updated successfully');
}catch(\Exception $e){
Log::error("Error updating role_menu:", ["error" => $e->getMessage()]);
return redirect()->route("role-menu.permission", $role_id)->with("error", $e->getMessage());

0
app/Http/Controllers/RoutingController.php Executable file → Normal file
View File

View File

@@ -6,6 +6,8 @@ use App\Http\Controllers\Controller;
use App\Services\ServiceSIMBG;
use Illuminate\Http\Request;
use Exception;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class SyncronizeController extends Controller
{
protected $service_simbg;
@@ -13,16 +15,36 @@ class SyncronizeController extends Controller
$this->service_simbg = $service_simbg;
}
public function index(Request $request){
return view('settings.syncronize.index');
$menuId = $request->query('menu_id');
$user = Auth::user();
$userId = $user->id;
// Ambil role_id yang dimiliki user
$roleIds = DB::table('user_role')
->where('user_id', $userId)
->pluck('role_id');
// Ambil data akses berdasarkan role_id dan menu_id
$roleAccess = DB::table('role_menu')
->whereIn('role_id', $roleIds)
->where('menu_id', $menuId)
->first();
// Pastikan roleAccess tidak null sebelum mengakses properti
$creator = $roleAccess->allow_create ?? 0;
$updater = $roleAccess->allow_update ?? 0;
$destroyer = $roleAccess->allow_destroy ?? 0;
return view('settings.syncronize.index', compact('creator', 'updater', 'destroyer'));
}
public function syncPbgTask(){
$res = $this->service_simbg->syncTaskList();
$res = $this->service_simbg->syncTaskPBG();
return $res;
}
public function syncronizeTask(Request $request){
$res = $this->service_simbg->syncTaskList();
$res = $this->service_simbg->syncTaskPBG();
return redirect()->back()->with('success', 'Processing completed successfully');
}
@@ -33,7 +55,7 @@ class SyncronizeController extends Controller
public function syncIndexIntegration(Request $request, $uuid){
$token = $request->get('token');
$res = $this->service_simbg->syncIndexIntegration($uuid, $token);
$res = $this->service_simbg->syncIndexIntegration($uuid);
return $res;
}
@@ -42,4 +64,9 @@ class SyncronizeController extends Controller
$res = $this->service_simbg->syncTaskDetailSubmit($uuid, $token);
return $res;
}
public function syncTaskAssignments($uuid){
$res = $this->service_simbg->syncTaskAssignments($uuid);
return $res;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace App\Http\Controllers;
use App\Models\Tax;
use Illuminate\Http\Request;
class TaxationController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
$menuId = $request->query('menu_id') ?? $request->input('menu_id');
$permissions = $this->permissions[$menuId]?? []; // Avoid undefined index error
$creator = $permissions['allow_create'] ?? 0;
$updater = $permissions['allow_update'] ?? 0;
$destroyer = $permissions['allow_destroy'] ?? 0;
return view('taxation.index', compact('creator', 'updater', 'destroyer', 'menuId'));
}
public function upload(Request $request)
{
$menuId = $request->query('menu_id') ?? $request->input('menu_id');
return view('taxation.upload', compact('menuId'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(string $id)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Request $request, string $id)
{
$menuId = $request->query('menu_id') ?? $request->input('menu_id');
$data = Tax::find($id);
return view('taxation.edit', compact('menuId', 'data'));
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
{
//
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class TpatptsController extends Controller
{
public function index()
{
return view('tpa-tpt.index');
}
public function create()
{
//
}
public function show(string $id)
{
//
}
public function edit(string $id)
{
//
}
}

View File

@@ -0,0 +1,167 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
use Laravel\Sanctum\PersonalAccessToken;
use Symfony\Component\HttpFoundation\Response;
class ValidateApiTokenForWeb
{
/**
* Handle an incoming request.
* Middleware ini memvalidasi token API untuk web requests
* dan melakukan auto-logout jika token tidak valid
*/
public function handle(Request $request, Closure $next): Response
{
// Skip validation untuk non-authenticated routes
if (!Auth::check()) {
return $next($request);
}
// Skip validation untuk API routes (sudah ditangani oleh auth:sanctum)
if ($request->is('api/*')) {
return $next($request);
}
$user = Auth::user();
$sessionToken = Session::get('api_token');
// Jika tidak ada token di session, generate token baru
if (!$sessionToken) {
$this->generateNewToken($user);
return $next($request);
}
// Validasi token API
if (!$this->isTokenValid($sessionToken, $user)) {
// Token invalid, check apakah ada user lain yang login
if ($this->hasOtherUserLoggedIn($user)) {
// User lain sudah login, force logout user ini
$this->forceLogout($request, 'User lain telah login. Silakan login ulang.');
return $this->redirectToLogin($request, 'User lain telah login. Silakan login ulang.');
} else {
// Generate token baru jika tidak ada user lain
$this->generateNewToken($user);
}
}
return $next($request);
}
/**
* Check apakah token API masih valid
*/
private function isTokenValid($sessionToken, $user): bool
{
if (!$sessionToken || !$user) {
return false;
}
// Extract plain token dari session token
$tokenParts = explode('|', $sessionToken);
if (count($tokenParts) !== 2) {
return false;
}
$plainToken = $tokenParts[1];
// Check token di database
$validToken = PersonalAccessToken::where('tokenable_id', $user->id)
->where('tokenable_type', get_class($user))
->where('token', hash('sha256', $plainToken))
->where(function($query) {
$query->whereNull('expires_at')
->orWhere('expires_at', '>', now());
})
->first();
return $validToken !== null;
}
/**
* Check apakah ada user lain yang login (token baru dibuat)
*/
private function hasOtherUserLoggedIn($currentUser): bool
{
$sessionUserId = Session::get('user_id');
// Jika ada user_id di session tapi tidak match dengan current user
if ($sessionUserId && $sessionUserId != $currentUser->id) {
return true;
}
// Check apakah ada token aktif lain untuk user ini
$activeTokens = PersonalAccessToken::where('tokenable_id', $currentUser->id)
->where('tokenable_type', get_class($currentUser))
->where(function($query) {
$query->whereNull('expires_at')
->orWhere('expires_at', '>', now());
})
->count();
// Jika tidak ada token aktif, kemungkinan user lain sudah login
return $activeTokens === 0;
}
/**
* Generate token baru untuk user
*/
private function generateNewToken($user): void
{
// Hapus token lama
PersonalAccessToken::where('tokenable_id', $user->id)
->where('tokenable_type', get_class($user))
->delete();
// Generate token baru
$tokenName = config('app.name', 'Laravel') . '-' . $user->id . '-' . time();
$token = $user->createToken($tokenName, ['*'], now()->addDays(30))->plainTextToken;
// Simpan token di session
Session::put('api_token', $token);
Session::put('user_id', $user->id);
Session::put('login_timestamp', now()->timestamp);
}
/**
* Force logout user dan clear semua sessions
*/
private function forceLogout(Request $request, string $reason = 'Session tidak valid'): void
{
$user = Auth::user();
if ($user) {
// Delete all tokens for this user
PersonalAccessToken::where('tokenable_id', $user->id)
->where('tokenable_type', get_class($user))
->delete();
}
// Clear session
Session::forget(['api_token', 'user_id', 'login_timestamp']);
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
}
/**
* Redirect ke login dengan pesan error
*/
private function redirectToLogin(Request $request, string $message): Response
{
if ($request->expectsJson() || $request->ajax()) {
return response()->json([
'error' => $message,
'redirect' => route('login'),
'force_logout' => true
], 401);
}
return redirect()->route('login')->with('error', $message);
}
}

0
app/Http/Requests/Auth/LoginRequest.php Executable file → Normal file
View File

View File

@@ -22,13 +22,14 @@ class SpatialPlanningRequest extends FormRequest
public function rules(): array
{
return [
'name' => 'string',
'kbli' => 'string',
'activities' => 'string',
'area' => 'string',
'location' => 'string',
'number' => 'string',
'date' => 'date_format:Y-m-d',
'name' => 'nullable|string',
'kbli' => 'nullable|string',
'activities' => 'nullable|string',
'area' => 'nullable|string',
'location' => 'nullable|string',
'number' => 'nullable|string',
'date' => 'nullable|date_format:Y-m-d',
'is_terbit' => 'nullable|boolean',
];
}

View File

@@ -1,35 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class SpatialPlanningsRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => ['required','string','max:255'],
'kbli' => ['required','string','max:255'],
'kegiatan' => ['required','string'],
'luas' => ['required','numeric','regex:/^\d{1,16}(\.\d{1,2})?$/'],
'lokasi' => ['required','string'],
'nomor' => ['required','string','max:255',Rule::unique('spatial_plannings')->ignore($this->id)],
'sp_date' => ['required','date'],
];
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class TaxationsRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'tax_no' => ['required', 'string', Rule::unique('taxs')->ignore($this->id)],
'tax_code' => ['required', 'string'],
'wp_name' => ['required', 'string'],
'business_name' => ['required', 'string'],
'address' => ['required', 'string'],
'start_validity' => ['required', 'date_format:Y-m-d'],
'end_validity' => ['required', 'date_format:Y-m-d'],
'tax_value' => ['required', 'numeric', 'regex:/^\d{1,16}(\.\d{1,2})?$/'],
'subdistrict' => ['required', 'string'],
'village' => ['required', 'string'],
];
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class BigdataResumeResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'import_datasource_id' => $this->import_datasource_id,
'potention_count' => (int) $this->potention_count,
'potention_sum' => number_format((float) $this->potention_sum, 2, ',', '.'),
'non_verified_count' => (int) $this->non_verified_count,
'non_verified_sum' => number_format((float) $this->non_verified_sum, 2, ',', '.'),
'verified_count' => (int) $this->verified_count,
'verified_sum' => number_format((float) $this->verified_sum, 2, ',', '.'),
'business_count' => (int) $this->business_count,
'business_sum' => number_format((float) $this->business_sum, 2, ',', '.'),
'non_business_count' => (int) $this->non_business_count,
'non_business_sum' => number_format((float) $this->non_business_sum, 2, ',', '.'),
'spatial_count' => (int) $this->spatial_count,
'spatial_sum' => number_format((float) $this->spatial_sum, 2, ',', '.'),
'issuance_realization_pbg_count' => (int) $this->issuance_realization_pbg_count,
'issuance_realization_pbg_sum' => number_format((float) $this->issuance_realization_pbg_sum, 2, ',', '.'),
'waiting_click_dpmptsp_count' => (int) $this->waiting_click_dpmptsp_count,
'waiting_click_dpmptsp_sum' => number_format((float) $this->waiting_click_dpmptsp_sum, 2, ',', '.'),
'process_in_technical_office_count' => (int) $this->process_in_technical_office_count,
'process_in_technical_office_sum' => number_format((float) $this->process_in_technical_office_sum, 2, ',', '.'),
'year' => $this->year,
'created_at' => $this->created_at->toDateTimeString(),
];
}
}

View File

@@ -19,8 +19,8 @@ class DataSettingResource extends JsonResource
'key' => $this->key,
'value' => $this->value,
'type' => $this->type,
'created_at' => $this->created_at->toDateTimeString(),
'updated_at' => $this->updated_at->toDateTimeString(),
'created_at' => $this->created_at ? $this->created_at->toDateTimeString() : null,
'updated_at' => $this->updated_at ? $this->updated_at->toDateTimeString() : null,
];
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Http\Resources;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
@@ -14,13 +15,21 @@ class ImportDatasourceResource extends JsonResource
*/
public function toArray(Request $request): array
{
$startTime = $this->start_time ? Carbon::parse($this->start_time) : null;
$finishTime = $this->finish_time ? Carbon::parse($this->finish_time) : null;
return [
"id"=> $this->id,
"message" => $this->message,
"response_body" => $this->response_body,
"status" => $this->status,
"start_time" => $startTime ? $startTime->toDateTimeString() : null,
"duration" => ($startTime && $finishTime)
? $finishTime->diff($startTime)->format('%H:%I:%S')
: null,
"finish_time" => $finishTime ? $finishTime->toDateTimeString() : null,
"created_at" => $this->created_at->toDateTimeString(),
"updated_at" => $this->updated_at->toDateTimeString(),
"failed_uuid" => $this->failed_uuid
];
}
}

View File

@@ -14,6 +14,16 @@ class MenuResource extends JsonResource
*/
public function toArray(Request $request): array
{
return parent::toArray($request);
// return parent::toArray($request);
return [
'id' => $this->id,
'name' => $this->name,
'icon' => $this->icon,
'url' => $this->url,
'sort_order' => $this->sort_order,
'parent' => $this->parent ? new MenuResource($this->parent) : null,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PbgTaskGoogleSheetResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return parent::toArray($request);
}
}

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