Compare commits
83 Commits
feat/restr
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91085e8796 | ||
|
|
61e6eb9803 | ||
|
|
148dfebb4a | ||
|
|
aa34fff979 | ||
|
|
1a24b18719 | ||
|
|
e265e2ec35 | ||
|
|
e577da737b | ||
|
|
05ca927c38 | ||
|
|
fc4b419878 | ||
|
|
53d12d6798 | ||
|
|
809eb85255 | ||
|
|
8a513460bb | ||
|
|
fc74875cce | ||
|
|
beb7d935c9 | ||
|
|
5c4cebd2b3 | ||
|
|
cbe3d00c96 | ||
|
|
65d9247b46 | ||
|
|
63310f2748 | ||
|
|
c6257b79bf | ||
|
|
38493063c4 | ||
|
|
954b2d8716 | ||
|
|
41cfce589b | ||
|
|
8de1b51fea | ||
|
|
fef6ae7522 | ||
|
|
6f1cb4195a | ||
|
|
6a060f5dac | ||
|
|
844fbdfa89 | ||
|
|
e18c0cb3b6 | ||
|
|
0a9d9071e4 | ||
|
|
4b28bebcc2 | ||
|
|
1bcd2023da | ||
|
|
1b084ed485 | ||
|
|
71ca8dc553 | ||
|
|
3cddd271c8 | ||
|
|
e9a70a827c | ||
|
|
2cbc4172da | ||
|
|
d7e9f44b20 | ||
|
|
7c7aa0e2a5 | ||
|
|
0111ab14e1 | ||
|
|
68e9d5eebf | ||
|
|
209ef07f9c | ||
|
|
6896fd62a3 | ||
|
|
b73183becf | ||
|
|
9f9c3758ed | ||
|
|
48a340d684 | ||
|
|
3ff3dc8f17 | ||
|
|
7936eb1dbf | ||
|
|
ec047821a1 | ||
|
|
e0ed007a39 | ||
|
|
bb63ea8084 | ||
|
|
7a19d9f39d | ||
|
|
93af7ab2a1 | ||
|
|
6158903260 | ||
|
|
09e7d41ddc | ||
|
|
2f4ef6cb56 | ||
|
|
4f94e9d8f7 | ||
|
|
fa6a0079dc | ||
|
|
f7497cbec8 | ||
|
|
b5f7bf39b2 | ||
|
|
ef3c9d6fc3 | ||
|
|
1288ab509d | ||
|
|
588e3ad5e2 | ||
|
|
3902a486f7 | ||
|
|
dd1cd72450 | ||
|
|
af05a39a82 | ||
|
|
0abf278aa3 | ||
|
|
c2cb1b99f2 | ||
|
|
7135876ebc | ||
|
|
456eec83dc | ||
|
|
6a22b55a1c | ||
|
|
5aab6fa3d1 | ||
|
|
a1e302a56d | ||
|
|
a7f578ca3d | ||
|
|
c33193d5f0 | ||
|
|
2c7c99bcf1 | ||
|
|
a01b6f5611 | ||
|
|
2f43ebe97e | ||
|
|
e5baf5318f | ||
|
|
b895f61701 | ||
|
|
5dd92aa323 | ||
|
|
7eb5a850c2 | ||
|
|
200b398868 | ||
|
|
ccff82bd22 |
@@ -2,7 +2,7 @@ APP_NAME=Laravel
|
|||||||
APP_ENV=local
|
APP_ENV=local
|
||||||
APP_KEY=
|
APP_KEY=
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
APP_TIMEZONE=UTC
|
APP_TIMEZONE=Asia/Jakarta
|
||||||
APP_URL=http://localhost
|
APP_URL=http://localhost
|
||||||
API_URL=http://localhost:8000
|
API_URL=http://localhost:8000
|
||||||
|
|
||||||
@@ -65,6 +65,7 @@ AWS_BUCKET=
|
|||||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
VITE_APP_HOST=localhost
|
||||||
|
|
||||||
API_KEY_GOOGLE="xxxxx"
|
API_KEY_GOOGLE="xxxxx"
|
||||||
SPREAD_SHEET_ID="xxxxx"
|
SPREAD_SHEET_ID="xxxxx"
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
APP_NAME=SIBEDAS-PBG
|
|
||||||
APP_ENV=local
|
|
||||||
APP_KEY=base64:xqCpwixWKqgu1Ca22gFizoOt44p7h+cgTOKuhS/P0Jw=
|
|
||||||
APP_DEBUG=true
|
|
||||||
APP_TIMEZONE=Asia/Jakarta
|
|
||||||
APP_URL=http://localhost:8000
|
|
||||||
|
|
||||||
API_URL=http://localhost:8000
|
|
||||||
|
|
||||||
APP_LOCALE=en
|
|
||||||
APP_FALLBACK_LOCALE=en
|
|
||||||
APP_FAKER_LOCALE=en_US
|
|
||||||
|
|
||||||
APP_MAINTENANCE_DRIVER=file
|
|
||||||
# APP_MAINTENANCE_STORE=database
|
|
||||||
|
|
||||||
PHP_CLI_SERVER_WORKERS=4
|
|
||||||
|
|
||||||
BCRYPT_ROUNDS=12
|
|
||||||
|
|
||||||
LOG_CHANNEL=stack
|
|
||||||
LOG_STACK=single
|
|
||||||
LOG_DEPRECATIONS_CHANNEL=null
|
|
||||||
LOG_LEVEL=debug
|
|
||||||
|
|
||||||
DB_CONNECTION=mariadb
|
|
||||||
DB_HOST=db
|
|
||||||
DB_PORT=3306
|
|
||||||
DB_DATABASE=sibedas_db
|
|
||||||
DB_USERNAME=root
|
|
||||||
DB_PASSWORD=root
|
|
||||||
|
|
||||||
SESSION_DRIVER=database
|
|
||||||
SESSION_LIFETIME=120
|
|
||||||
SESSION_ENCRYPT=false
|
|
||||||
SESSION_PATH=/
|
|
||||||
SESSION_DOMAIN=null
|
|
||||||
|
|
||||||
BROADCAST_CONNECTION=log
|
|
||||||
FILESYSTEM_DISK=local
|
|
||||||
QUEUE_CONNECTION=database
|
|
||||||
|
|
||||||
CACHE_STORE=database
|
|
||||||
CACHE_PREFIX=
|
|
||||||
|
|
||||||
MEMCACHED_HOST=127.0.0.1
|
|
||||||
|
|
||||||
REDIS_CLIENT=phpredis
|
|
||||||
REDIS_HOST=127.0.0.1
|
|
||||||
REDIS_PASSWORD=null
|
|
||||||
REDIS_PORT=6379
|
|
||||||
|
|
||||||
MAIL_MAILER=log
|
|
||||||
MAIL_HOST=127.0.0.1
|
|
||||||
MAIL_PORT=2525
|
|
||||||
MAIL_USERNAME=null
|
|
||||||
MAIL_PASSWORD=null
|
|
||||||
MAIL_ENCRYPTION=null
|
|
||||||
MAIL_FROM_ADDRESS="hello@example.com"
|
|
||||||
MAIL_FROM_NAME="${APP_NAME}"
|
|
||||||
|
|
||||||
AWS_ACCESS_KEY_ID=
|
|
||||||
AWS_SECRET_ACCESS_KEY=
|
|
||||||
AWS_DEFAULT_REGION=us-east-1
|
|
||||||
AWS_BUCKET=
|
|
||||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
|
||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
|
||||||
|
|
||||||
SIMBG_HOST="https://simbg.pu.go.id/"
|
|
||||||
SIMBG_EMAIL="dputr@bandungkab.go.id"
|
|
||||||
SIMBG_PASSWORD="Simbg123"
|
|
||||||
|
|
||||||
API_KEY_GOOGLE="AIzaSyBxfEShFkKmykkc7RJR3lVzkQ_xGHK3qr0"
|
|
||||||
SPREAD_SHEET_ID="1QoXzuLdEX3MK70Yrfigz0Qj5rAt4T819jX85vubBNdY"
|
|
||||||
OPENAI_API_KEY="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"
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -21,3 +21,6 @@ yarn-error.log
|
|||||||
/.nova
|
/.nova
|
||||||
/.vscode
|
/.vscode
|
||||||
/.zed
|
/.zed
|
||||||
|
/.composer
|
||||||
|
/.config
|
||||||
|
/.npm
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
no,pemohon,alamat,activities,luas_lahan,bcr_kawasan,area,no_tapak,no_skkl,no_ukl,fungsi_bangunan,sub_fungsi_bangunan,jumlah_lantai
|
|
||||||
01/222,PT CIPTA INDAH PERTIWI No Telpon : 082217633434,Jl Salam No 51 Desa/Kel. Cihapit Kec. Bandung wetan Kota Bandung,PERUMAHAN LA LA TOWN,16629,0.2449,4072.4421,648.11./222 - SP /TR 07 Januari 2025;644.2./207 - SP /TR 02 Januari 2025,500.10.29.15/631-REKTEK/TR Tanggal 14 Februari 2023;500.10.29.15/2837-REKTEK/Bid. Taru Tanggal 25 Juli 2024,14022310313204030 Tanggal 14 Februari 2023;500.10.29.7/Kep.618-DLH/2024 Tanggal 13 Desember 2024;161023087545 Tanggal 16 Oktober2023,Fungsi Hunian,Fungsi Hunian,1
|
|
||||||
03/481,MUHAMAD GUNTUR JULIADI No Telpon : 081223334486,Cangkuang Residence RT 004/003 Ds Cangkuang Wetan Kec. Dayeuhkolot Kab. Bandung,PERUMAHAN CANGKUANG RESIDENCE 2,2360,0.5924,1398.064,648.11./481 - SP /TR 15 Januari 2025;647./226 - SP /TR 07 Januari 2025;647/225 - SP /TR 07 Januari 2025;644/221 - SP /TR 07 Januari 2025;645.4/557 - SP /TR 20 Januari 2025,500.10.29.15/2894-REKTEK/Bid. Taru Tanggal 08 Agustus 2024;400.7.22/4927-KRK/04/Bid.Taru Tanggal 06 Desember 2024;500.10.29.15/2619-REKTEK/Bid. Taru Tanggal 10 Juli 2024;500.10.29.15/4155-REKTEK/Bid. Taru Tanggal 18 Nopember 2024;503/123/III-DPMPTSP/2020 Tanggal 24 MAret 2020,167240099471 Tanggal 16 Juli 2024;9120111161314 Tanggal 11 Nopember 2019;8120312190151 Tanggal 04 Desember 2023;500.10.29.7/Kep.614-DLH/2024 Tanggal 20 Nopember 2024;2910240290118 Tanggal 29 Oktober 2024;8120001992981 Tanggal 12 Juli 2024;SK.294/Menlhk/Setjen/PLA.4/5/2021 Tanggal 31 Mei 2021,Fungsi Hunian,Fungsi Hunian,1
|
|
||||||
08/480,PT SURYA KHARISMA PARAHYANGAN No Telpon : 081394461006 ,Jalan Cibaligo No 48 Desa /Kel. Cigugur tengah Kec. Cimahi tengah kota Cimahi ,PERUMAHAN PARAHYANGAN GARDEN CITY,116360,0.5164,60088.304,648.11./480 - SP /TR 15 Januari 2025,500.10.29.15/603-REKTEK/TR Tanggal 05 Desember 2023,1810220245722 Tanggal 18 Oktober 2022;500.10.29.7/Kep.632-DLH/2024 Tanggal 24 Desember 2024,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
09/223,PT CLARICHEM INDONESIA No Telpon : 081315399694,Kawasan pergudangan taman tekno sektor XI blok L2 No 30 Ds/Kel. Setu Kec. Setu kota Tangerag Banten ,PERGUDANGAN DAN PENYIMPANAN,1586,0.7017,1112.8962,644./223 - SP /TR 07 Januari 2025,400.7.22/4427-KRK/03/Bid.Taru Tanggal 06 Desember 2024,9120008832767 Tanggal 26 Nopember 2024,Usaha Besar (Non-Mikro),Fungsi Usaha,1
|
|
||||||
10/657,PT HALKA GITA No Telpon : 0816734374,Jl Raya Majalaya KM 2 RT 02/09 Desa Hegarmanah Kec. Cikancung Kab. Bandung,REVISI 1 KOMPLEK PERGUDANGAN,19100,0.699,13350.9,644./657 - SP /TR 23 Januari 2025,500.10.29.15/6750-REKTEK/TR Tanggal 21 Desember 2023,9120300510839 Tanggal 16 Desember 2023;500.10.29.7/Kep.609-DLH/2024 Tanggal 12 Nopember 2024,Usaha Besar (Non-Mikro),Fungsi Usaha,2
|
|
||||||
11/658,PT SURYA TIRTA KENCANA No Telpon : 08986155493,Kutawaringin Industrial Park Kav. 276-277 Desa Jelegong Kec. Kutawaringin Kab. Bandung,PERGUDANGAN DAN PENYIMPANAN BAHAN KIMIA,3932,0.6995,2750.434,644./658 - SP /TR 23 Januari 2025;640/540 - SP /TR 20 Januari 2025,10112410113204010 Tanggal 10 Nopember 2024;591.4/031/IX-DPMPTSP/2020 Tanggal 03 Sseptember 2020,8120017120594 Tanggal 31 Agustus 2018;600.4.5/056/6838-Ktr/TL Tanggal 24 Desember 2024;0220305221438 Tanggal 20 Nopember 2021;500.10.29.7/4295/TL Tanggal 21 September 2024,Usaha Besar (Non-Mikro),Fungsi Usaha,1
|
|
||||||
13 / 550,PT BERKAH SEPUH JAYA No Telpon : 082118781624,Ruko komplek dewaadru blok R3 RT 005 ds/kec. Bojongsoang kab. Bandung,REVISI 1 PERUMAHAN VILLA CEMARA ASRI,17379,0.5476,9516.7404,648.11/550 - SP/TR 20 Januari 2025,150223101113204049 Tanggal 15 Februari 2023,1233000451657 Tanggal 5 Mei 2023;500.10.29.7/kep.392-DLH/2025 Tgl. 2 Januari 2025,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
14 / 655,PT RISKI ANUGERAH SEDJAHTERA No Telpon : 082118781624,Griya permata asri blok C2 No 18 Ds Lengkong Kec. Bojongsoang Kab. Bandung,REVISI 1 PERUMAHAN TAMAN KATAPANG INDAH,28425,0.6532,18567.21,648.11 /655 - SP /TR 23 Januari 2025,500.10.29.15/2333-REKTEK/TR Tanggal 22 Mei 2024;500.10.29.15/3811-REKTEK/Bid. Taru Tanggal 10 Oktober 2024,1205000302875 Tanggal 02 April 2024;500.10.29.7/Kep.408-DLH/2025 Tanggal 10 Januari 2025,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
15 / 551,PT BUMI SUWARNA SEJAHTERA No Telpon : 082118781624,Jl Ciwastra no 73 Kel. Mekarmulya kec. Rancasari kota bandung,REVISI 1 PERUMAHAN BUMI KARA RESIDENCE,29711,0.5424,16115.2464,648.11/551-SP/TR 20 Januari 2025,500.10.29.15/6097-REKTEK/TR Tanggal 27 Nopember 2023,1311230078306 Tgl 13 Nopember 2023;500.10.29.7/Kep.493-DLH/2024 Tanggal 20 Maret 2024,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
16 / 656,PT AMBER HASYA No Telpon : 081312411661,Jl Sarijadi raya No 111 Kel. Sarijadi Kec. Sukasarii kota bandung,REVISI 1 PERUMAHAN GREEN HARMONI RESIDENCE ,39520,0.2726,10773.152,648.11/656-SP/TR 23 Januari 2025,500.10.29.15/2732-REKTEK/Bid. Taru Tanggal 19 Juli 2024,9120004882226 Tgl 22 Agustus 2019;500.10.29.7/Kep.617-DLH/2024 Tanggal 4 Desember 2024,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
17 / 224,PT KARYA UTAMA SEJATI JAYA No Telpon : 081320772222,Kp. Andir DS/Kel. Manggahang Kec. Baleendah Kab. Bandung,PERUMAHAN GRAND CIPARAY RESIDENCE,12045,0.5945,7160.7525,648.11/224-SP/TR 07 Januari 2025;644/822-SP/TR 13 Februari 2025;640/524-SP/TR 17 Januari 2025;642/763-SP/TR 11 Februari 2025,500/5300-PERTEKTR/2022/TR Tanggal 04 Agustus 2022;400.7.22/4872-KRK/25/Bid.TR Tanggal 24 Desember 2024;Pu.650/7580-REKTEK/TR Tanggal 20 Desember 2022;01112410113204120 Tanggal 1 November 2024,1801220061544 Tgl 05 September 2022;600.4.5/052/1722-Ktr/TL Tanggal 20 Mei 2024;9120401971173 Tgl 11 Desember 2024;1406220076826 Tgl 15 November 2022;8120103802893 Tgl 29 Agustus 2018,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
21 / 688,PT PRAKARSA FAJAR PROPERTINDO No Telpon : 082315094712,Jl. Otto Iskandardinata No 429 Kel. Pungkur Kec. Regol Kota Bandung,PERUMAHAN ALAM BUMI CIPARAY,17417,0.6793,11831.3681,648.11/688-SP/TR 03 Februari 2025,500.10.29.15/1075-REKTEK/TR Tanggal 04 Maret 2024,9120007841259 Tgl 15 Agustus 2019;500.10.29.7/Kep.405-DLH/2025 Tanggal 06 Januari 2025,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
22 / 767,PT CITRA SENTOSA JAYATAMA No Telpon : 087825393706,Jl. Nyengseret No 29 RT 01 Kel. Pelindung hewaqn Kota Bandung,PERUMAHAN CITRA ASRI RESIDENCE,28900,0.692,19998.8,648.11/767-SP/TR 12 Februari 2025,500.10.29.15/2617-REKTEK/Bid. Taru Tanggal 10 Juli 2024,9120107971097 Tgl 19 September 2019;500.10.29.7/Kep.612-DLH/2024 Tanggal 18 November 2024,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
23/ 641,PT PESONA MITRA KEMBAR MAS No Telpon : 081220763283,Jl Kembar mas IV No 2A RT 05/09 Kel. Pasirluyu Kec. Regol Kota Bandung,REVISI 5 PERUMAHAN PODOMORO PARK,1160011,0.6691,776163.3601,648.11/641- SP/TR 22 Januari 2025,591/057-DPMPTSP/2017 Tanggal 28 Desember 2017,650/Kep.628-DLH/2018 Tanggal 30 Nopember 2018,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
24 / 659,PT PAVITRA PARA ARTHA No Telpon : 081809063866,Jl . Kubang Beureum No 39 RT 007 / 011 Kelurahan Sekejati Kecamatan Buahbatu,REVISI 4 PERUMAHAN PRIVATE VILLAGE,88245,0.5819,51349.7655,648.11/659 - SP/TR 23 Januari 2025;645/932 - SP/TR 21 Februari 2025;645/552-SP/TR 20 Januari 2025;647/640-SP/TR 22 Januari 2025;647/930-SP/TR 21 Februari 2025,591.4/001-BPMP/2014 Tanggal 06 Januari 2014;591.4/031-DPMPTSP/2019 Tanggal 10 Juli 2019;503/104/XII-DPMPTSP/2019- Tgl 20 Desember 2019;400.1.22/4521-KRK/15/Bid.Taru Tgl 16 Desember 2024;650/2597-KRTR/2021/TR Tgl 9 Desember 2021;500.10.29.15/2839-REKTEK/Bid.TAru Tgl 2 Agustus 2024,500.10.29.6/4898/TL Tanggal 08 Nopember 2023;667/2995/TL Tanggal 11 Agustus 2020;9120000431996 Tanggal 20 Oktober 2021;500.10.29.7/Kep.629-DLH/2024;9120605793306 Tanggal 9 Mei 2023;1601240112040181 Tanggal 16 Januari 2024;8120215192631 Tanggal 3 Januari 2022;34/LH.01.06.05/DLH Tanggal 3 Januari 2025;9120101841825 Tanggal 18 Oktober 2023,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
29 / 931,PT KARYA HAFANA INTAN MANDIRI No Telpon : 082120002925 ,Kp./Desa Ciluluk Kecamatan Cikancung Kabupaten Bandung,PERUMAHAN GRIYA CIHANYIR PERMAI,24432.33,0.5945,14525.02019,648.11/931 - SP/TR 21 Februari 2025,400.7.22/4840-KRK/21/Bid.Taru Tgl. 18 Desember 2024,2908210007625 Tanggal 06 Mei 2024;500.10.29.7/Kep.414-DLH/2025 Tanggal 30 Januari 2025,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
30 / 933,PT ARUM JAYA PROPERTI No Telpon : 085100942672,Perumahan Banyu Arum Residence Blok A Kp. Tegal Tengah RT 003/013 Desa Cangkuang Kec. Rancaekek,REVISI 1 PERUMAHAN BUMI ARUM REGENCY,58899,0.4838,28495.3362,648.11/933 - SP/PR 21 FEBRUARI 2025,591.4/026-DPMPTSP/2018 Tgl 24 September 2018,667/7210/TL Tanggal 16 Nopember 2018;500.10.29.6/Kep.420-DLH/2025 Tanmggal 3 Februari 2025,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
31 / 762,SANDY SALMAN No Telpon : 082118781624,Jl Kembar VIII No 26 RT 005/010 Kel Cigereleng Kec. Regol Kota Bandung 40253,REVISI 1 PERUMAHAN D HEUVEL WIWAHA PADASUKA,3095,0.4295,1329.3025,648.11/ 762 - SP / PR 11 Februari 2025;648/523-SP/TR 17 Januari 2025;647/1140-SP/TR 27 Februari 2025;647/934-SP/TR 21 Februari 2025;647/1233-SP/TR 06 Maret 2025,"650/5689-PETEKTR/2022/TR Tanggal 30 Agustus 2022;650/2748-KRTR/2021/TR Tgl 21 Desember 2021;593.SK.93-BKPMD/90 Tanggal 11 April 1990;593/SK.225-BKPMD/1991 Tanggal 28 Juni 1991;591,4/009-BPMP/2014 Tanggal 27 Maret 2014;500.10.29.15/711-REKTEK/TR Tanggal 15 Februari 2024;500.10.29.15/2877-REKTEK/Bid.Taru Tanggal 19 Agustus 2024 ",LH.01.04/5866-SPPL/TL Tanggal 27 Desember 2022;1402220064386 Tanggal 04 Februari 2022;500.10.29.7/Kep.593-DLH/2024 Tanggal 27 September 2024;667/3257/BPLH Tanggal 06 Oktober 2014;2711230388253 Tanggal 27 Nopember 2023;500.10.29.7/Kep.630-DLH/2024 Tanggal 20 Desember 2024;9120312050674 Tanggal 07 Oktober 2019;500.10.29.7/Kep.426-DLH/2025 Tanggal 14 Februari 2025,Fungsi Hunian,Fungsi Hunian,1
|
|
||||||
36/ 1141,HANDRIAWAN No Telpon : 081313350382,Jalan Taman Holis Indah E-5 No 4-5 RT 03/06 Ds/Kel. Cigondewah Kec. Bandung kulon. Kota Bandung,TOKO DAN GUDANG,2025,0.7848,1589.22,647/1141-SP/TR 27 Februari 2025;647/1211-SP/TR 04 Maret 2025;648.2/1091-SP/TR 25 Februari 2025,400.7.22/559-KRK/14/Bid.Taru Tanggal 17 Januari 2025;400.7.22/859-KRK/38/Bid.Taru Tanggal 17 Februari 2025;500.10.29.15/4064 - REKTEK/Bid.Taru Tanggal 25 Nopember 2024,1401250067962 Tanggal 14 Januari 2025;1812240057832 Tanggal 18 Desember 2024;1611210013009 Tanggal 16 Nopember 2021,Usaha Besar (Non-Mikro),Fungsi Usaha,1
|
|
||||||
39/ 1232,JOHANSJAH SUGIANTO No Telpon : 081314974495,Jalan Kepodang VI/6 blok k2 Rengas Kec. Ciputat timur kota Tangerang selatan,TOKO,2490,21.9,54531,644.2/1232-SP/TR 06 Maret 2025;644.2/1234-SP/TR 06 Maret 2025,400.7.22/1089 - KRK/45//Bid.Taru Tanggal 24 Februari 2025;503/0017-PKKPRNB/DPMPTSP/II/2025 Tanggal 12 Februari 2025,3101250002573 Tanggal 31 Januari 2025;2107230112343 Tanggal 21 Juli 2023,Tidak Diketahui,Tidak Diketahui,3
|
|
||||||
41 / 1090,TIM AD HOC PERUMAHAN SUKAWANGI RESIDENCE No Telpon : 08112255770,Kp. Cihalimun RT 02/04 Ds. Cibeureum Kec. Kertasari Kab. Bandung,PERUMAHAN SUKAWANGI RESIDENCE,,1,,648.11/ 1090 - SP / PR 25 Februari 2025,,,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
42 / 1298,PT TIGA REKAN INDONESIA No Telpon : 08122444717,Jl Gradiul No 40 RT 04/07 Kel. Rancaekek Kencana Kec. Rancaekek Kab. Bandung,PERUMAHAN GREEN HILL VILLAGE,16548,0.5917,9791.4516,648.11/1298 - SP/TR 17 Maret 2025,50.10.29.15/2742-REKTEK/Bid. Taru Tanggal 24 Juli 2024,2505240130006 Tanggal 25 Mei 2024;600.4.5/061/832-Ktr/Bid. TL/2025 Tgl. 19 Februari 2025,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
43 / 1322,PT UNILOA ARDIYANTO INVESTAMA No Telpon : 081220180480,Jl Raya Ebah 103 Desa Sukamantri Kecamatan Paseh Kab. Bandung,PERUMAHAN MARISON CIPAKU,19209,0.6146,11805.8514,648.11/1322 - SP/TR 18 Maret 2025,24042410313204051 Tanggal 24 JApril 2024,0238010110358 Tanggal 11 Januari 2024;500.10.29.7/Kep. 451-DLH/2025 Tgl. 13 Maret 2025,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
44 / 1323,PT RADINAKA KRAMAT ABADI No Telpon : 082118781624,Jl. Sukamukti RT 01/06 Desa Sukamukti Kec. Katapang Kab. Bandung,PERUMAHAN GAHARU PALEDANG RESIDENCE,14665,0.68003,9972.63995,648.11/1323 - SP/TR 18 Maret 2025,02032510213204087 Tanggal 02 Maret 2025,0238010110358 Tanggal 11 Januari 2024;600.4.5/064/1165-Ktr/Bid. TL/2025 Tgl. 12 Maret 2025,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
45 / 1361,PT SANGKURIANG KRAMAT ABADI No Telpon : 082118781624,Kampung rancakasiat Desa Rancamulya Kec. Pameungpeuk Kab. Bandung,PERUMAHAN PONDOK ASRI SUKAMUKTI,20451,0.6806,13918.9506,648.11/1361 - SP/TR 24 Maret 2025,500.10.29.15/3451-REKTEK/Bid.Taru Tanggal 26 September 2024,0811230037204 Tanggal 08 Nopember 2023;500.10.29.7/Kep.460-DLH/2025 Tgl. 21 Maret 2025,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
46 / 1360,PT AUF ABDURACCHMAN JAYA No Telpon : 082118781624,Jl. Rancabungur rancakasiat Desa Malakasari Kec. Baleendah Kab. Bandung,PERUMAHAN BUMI SHANGRILA RESIDENCE,12846,0.6412,8236.8552,648.11/1360 - SP/TR 24 Maret 2025,400.7.22/093/1390-KRK/Bid.Taru Tanggal 17 Maret 2025,0603250028082 Tanggal 06 Maret 2025;600.4.5/072/1270-Ktr/Bid.TL/2025 Tgl. 21 Maret 2025,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
47 / 1142,ANTO DWI HARTANTO No Telpon : 082126215611,Jl. Elang V No 8 RT 08/01 Kel. Garuda Kec. Andir Kota Bandung,PERGUDANGAN DAN PENYIMPANAN,1307,0.5616,734.0112,647/1142 - SP/TR 27 Februari 2025;648.11/823 - SP/TR 13 Februari 2025;644.2/1389 - SP/TR 26 Maret 2025;644.2/1388 - SP/TR 26 Maret 2025,500.10.29.15/3827-REKTEK/Bid.Taru Tanggal 17 Oktober 2024;500.10.29.15/3944-REKTEK/Bid.Taru Tanggal 28 Oktober 2024;503/0072-DPMPTSP/XI/2023 Tanggal 29 November 2023;400.7.22/1046-KRK/Bid.Taru Tanggal 21 Februari 2025,2009240075774 Tanggal 20 September 2024 2025;2009240075774 Tanggal 20 September 2025;500.10.29.7/Hep-461-DLH/2025 Tanggal 21 Maret 2025;0298000921888 Tanggal 24 Januari 2022,Usaha Besar (Non-Mikro),Fungsi Usaha,1
|
|
||||||
51 / 1161,PT INGRIA PRATAMA CAPITALINDO Tbk No Telpon : 087722361043,Ruko Pondok Cabe Mutiara Jl Pondok cabe No 27 Kel. Pamulang Kec. Pamulang Kota Tangerang Selatan,REVISI 2 PERUMAHAN BUKIT ESMA CICALENGKA 2 ,74300,0.6292,46749.56,648.11/1161 - SP/TR 28 Februari 2025,50.10.29.15/2927-REKTEK/Bid. Taru Tanggal 14 Agustus 2024,591.4/005/IX-DPMPTSP/2019 Tanggal 09 September 2019;500.10.29.7/Kep.613-DLH/2024 Tgl. 18 November 2025,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
52 / 1441 ,PT ABADI MUKTI KIRANA No Telpon : 087821848944,Jl Terusan jamika No 88 Kel. Jamika Kec. Bojongloa kaler Kota Bandung, PERUMAHAN KOTA BARU ARJASARI,222816,1,222816,648.11/1441 - SP/TR 11 April 2025,400.7.28/1710/TR Tanggal 23 April 2024,9120108201236 Tanggal 13 Februari 2019;500.10.29.7/Kep.419-DLH/2025 Tgl. 03 Februari 2025,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
53 / 1566,PT MAKMUR INDAH DAMAI No Telpon : 081394722234,Jl. Podomoro Boulevard utara I Desa Lengkong Kec. Bojongsoang Kab. Bandung,PERUMAHAN BAROS INDAH RESIDENCE,17513,1,17513,647/1566 - SP/TR 25 April 2025,50.10.29.15/2507-REKTEK/Bid. Taru Tanggal 25 Juni 2024,2106240023693 Tanggal 21 Juni 2024;500.10.29.7/Kep.447-DLH/2024 Tgl. 10 Maret 2025,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
54 / 1343,PT KARYA BUMI BESTARI No Telpon : 08156080428,Kembar Mas IV No 2A RT 05/09 Kel. Pasirluyu Kec. Regol Kota Bandung,PERUMAHAN BESTARI MAS,26134,1,26134,648.11/1343 - SP/TR 20 Maret 2025;647/1565 - SP/TR 25 April 2025;648.1/1543 - SP/TR 17 April 2025,12042310213204004 Tanggal 12 April 2023;23122410113204057 Tanggal 23 Desember 2024;400.7.22/1047-KRK/42/Bid.Taru Tanggal 21 Februari 2025,1204230043635 Tanggal 12 April 2023;500.10.29.7/964/Bid. TL/2025 Tgl. 19 Februari 2025;9120304390956 Tanggal 05 Desember 2023;0307230050445 Tanggal 03 Juli 2023,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
57 / 1372,TENNI DIANA No Telpon : 082116106640,Cikaahuripan RT 002/005 Kec. Neglasari Kota Tangerang,PERADAGANGAN KHUSUS KARPET PERMADANI DAN PENUTUP DINDING DN LANTAI DI TOKO,1290,1,1290,644/1372 - SP/TR 26 Maret 2025;647/1557 - SP/TR 22 April 2025;640/1559 - SP/TR 22 April 2025;644/1371 - SP/TR 26 Maret 2025;644/1375 - SP/TR 26 Maret 2025;644/1373 - SP/TR 26 Maret 2025,500.10.29.15/4355-REKTEK/TR Tanggal 25 Agustus 2023;500.10.29.15/1073-REKTEK/TR Tanggal 04 Maret 2024;400.7.22/759-KRK/34/Bid.Taru Tanggal 11 Februari 2025;500.10.29.15/4654-REKTEK/Bid. Taru Tanggal 08 September 2023;500.10.29.15/4160-REKTEK/Bid. Taru Tanggal 15 Nopember 2024;500.10.29.15/2500-REKTEK/Bid. Taru Tanggal 19 Juni 2024,9120306652149 Tanggal 26 Juni 2019;0220107462528 Tanggal 08 September 2022;1283000240318 Tanggal 01 Februari 2021;600.4.5/071-KTR/Bid.TL/2025 Tgl. 05 Mei 2025;0220301251224 Tanggal 11 Agustus 2023;1263000210596 Tanggal 09 Februari 2021;1405240295325 5 Mei 2024,Tidak Diketahui,Tidak Diketahui,1
|
|
||||||
63 / 1567,Ir. BERSIH TARIGANT No Telpon : 081322777581,Jl Multatuli No 3 rt 001/001 Ds/Kel. Lebakgede Kec. Coblong Kota Bandung,PERUMAHAN TUSCANY HILL,23796,0.2428,5777.6688,648.11/1567 - SP/TR 25 April 2025,50.10.29.15/3829-REKTEK/Bid. Taru Tanggal 15 Oktober 2024,2007220014068 Tanggal 20 Juli 2025;500.10.29.7/Kep.429-DLH/2025 Tgl. 10 Maret 2025,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
64 / 1682,PT CIPTA BERKAT PROPERTI No Telpon : 081910661980,Ruko Matahari Cigado No 25 Jalan Anggadireja Kel. Baleendah Kec. Baleendah Kab. Bandung,PERUMAHAN GARDEN VIEW CICALENGKA,45666,1,45666,648.11/1682 - SP/TR 06 Mei 2025,50.10.29.15/3156-REKTEK/Bid. Taru Tanggal 05 September 2024,2310210017936 Tanggal 23 Oktober 2021;500.10.29.7/Kep.477-DLH/2025 Tgl. 17 April 2025,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
64 / 1682,PT CIPTA BERKAT PROPERTI No Telpon : 081910661980,Ruko Matahari Cigado No 25 Jalan Anggadireja Kel. Baleendah Kec. Baleendah Kab. Bandung,C. KWT PERUMAHAN,0,1,0,,,,Fungsi Hunian,Fungsi Hunian,1
|
|
||||||
65 / 1644,PT ARGUNA JAYA PROPERTY No Telpon : 081214211164,Jl Pidada IV/4 Denpasar BR/Link Sedana metra Kel. Ubung Kec. Denpasar Utara Kota Denpasar,REVISI 1 PERUMAHAN ARGUNA SINDANGPANON,8553,0.53,4533.09,648.11/1644 - SP/TR 29 April 2025;644/1558 - SP/TR 22 April 2025,503/009/II-DPMPTSP/2020 Tanggal 25 Februari 2020;50.10.29.15/4063-REKTEK/Bid. Taru Tanggal 25 Nopember 2025,9120004890734 Tanggal 03 Agustus 2019;667/2081/TL Tgl. 08 Juni 2020;0609240042720001 Tanggal 06 September 2024,Fungsi Hunian,Fungsi Hunian,1
|
|
||||||
67/ 1370,NENENG FATIMAH No Telpon : 08977980040,Jalan Batu Indah I No 26 RT 002/003 Kel. Batununggal Kec. Bandung Kidul Kota Bandung,PERGUDANGAN DAN PENYIMPANAN,2034,1,2034,644/1370 - SP/TR 26 Maret 2025,50.10.29.15/3340-REKTEK/Bid. Taru Tanggal 18 September 2025,1009240038601 Tanggal 10 September 2024,Usaha Besar (Non-Mikro),Fungsi Usaha,1
|
|
||||||
68/ 1374,LILI JOJON No Telpon : 082219855556,Kp. Cibisoro RT 004/008 Desa Nanjung Kec. Mrgaasih Kab. Bandung,PERGUDANGAN DAN PENYIMPANAN,1654,1,1654,644/1374 - SP/TR 26 Maret 2025;644.2/1683 - SP/TR 06 Mei 2025;645/1730 - SP/TR 07 Mei 2025,50.10.29.15/3210-REKTEK/Bid. Taru Tanggal 10 September 2024;400.7.22/1254-KRK/58/Bid. Taru Tanggal 10 Maret 2025;50.10.29.15/3443-REKTEK/Bid. Taru Tanggal 26 September 2024,1220000361065 Tanggal 16 Maret 2021;1712240056863 Tanggal 17 Desember 2024;0103230081544 Tanggal 01 Maret 2023;600.4.5/071/1503-Ktr/Bid. TL/2025 Tanggal 17 April 2025,Usaha Besar (Non-Mikro),Fungsi Usaha,1
|
|
||||||
71/ 1845,PT SANGKURIANG KRAMAT ABADI No Telpon : 082116602145,Kp. Rancakasiat Desa Rancamulya Kec. Pameungpeuk Kab. Bandung,PERUMAHAN CLUSTER GAHARU EMERALD,19889,1,19889,648.11 /1845 - SP/TR 19 Mei 2025,400.7.22/1526-KRK/96/Bid. Taru Tanggal 22 April 2025,0811230037204 Tanggal 08 Nopember 2023;500.10.29.7/Kep. 489-DLH/2025 Tanggal 15 Mei 2025,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
72/ 1731,PT MARGA TIRTA KENCANA No Telpon : 082130000146,Jlan BKR Lingkar selatan No 140 Kota Bandung,PERUMAHAN TAMAN CIBADUYUT INDAH 2 DAN 3,205344,1,205344,648.11 /1731 - SP/TR 07 Mei 2025;648.11 /1946 - SP/TR 22 Mei 2025;643 /1881 - SP/TR 20 Mei 2025;644 /1684 - SP/TR 06 Mei 2025;648.12 /1825 - SP/TR 16 Mei 2025,591.4/002-BPMP/2010 Tanggal 10 Januari 2010;500.10.29.15/3108-REKTEK/Bid.taru Tanggal 29 Agustus 2024;500.10.29.15/3319-REKTEK/TR Tanggal 14 Juli 2023;12092410313204101 Tanggal 12 September 2024;400.7.22/1231-KRK/Bid.Taru Tanggal 06 Maret 2025,667/3803/DLH Tanggal 22 Desember 2016;9120204202309 Tanggal 20 Februari 2019;2002230028317 Tanggal 20 Februari 2023;600.4.5/073/1562-Ktr/Bid.TL/2025 Tanggal 23 April 2025;8120006931402 Tanggal 28 September 2022;12710006324580004 Tanggal 05 Maret 2025,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
77/ 1879,PT PUTRA RAHMAN PRADANA No Telpon : 081312214962,Perum Cemerlang Permai Blok C No 16 Kel. Sukakarya Kec. Warudoyongf Kota Sukabumi,REVISI 1 PERUMAHAN BUKIT PINUS BANIARAN,90711,1,90711,648.11 /1879 - SP/TR 20 Mei 2025,22122310313204009 Tanggal 22 Desember 2023,500.10.29.7/Kep.482-DLH/2025 Tanggal 24 April 2025,Fungsi Hunian,Fungsi Hunian,2
|
|
||||||
|
61
Dockerfile
61
Dockerfile
@@ -9,6 +9,54 @@ COPY . .
|
|||||||
EXPOSE 5173
|
EXPOSE 5173
|
||||||
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
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
|
# Production stage
|
||||||
FROM php:8.2-fpm AS production
|
FROM php:8.2-fpm AS production
|
||||||
WORKDIR /var/www
|
WORKDIR /var/www
|
||||||
@@ -16,8 +64,12 @@ WORKDIR /var/www
|
|||||||
# Install PHP extensions
|
# Install PHP extensions
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
git curl zip unzip libpng-dev libonig-dev libxml2-dev libzip-dev \
|
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
|
&& 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
|
# Install Node.js
|
||||||
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
||||||
&& apt-get install -y nodejs
|
&& apt-get install -y nodejs
|
||||||
@@ -49,8 +101,15 @@ RUN php artisan config:clear \
|
|||||||
|
|
||||||
RUN php artisan storage:link
|
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
|
# Permissions
|
||||||
RUN chown -R www-data:www-data /var/www && chmod -R 755 /var/www/storage /var/www/public
|
RUN chown -R www-data:www-data /var/www && chmod -R 755 /var/www/storage /var/www/public
|
||||||
|
|
||||||
EXPOSE 9000
|
EXPOSE 9000
|
||||||
CMD ["php-fpm"]
|
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||||
|
|||||||
@@ -1,210 +0,0 @@
|
|||||||
# Struktur Tabel Retribusi PBG yang Dioptimalkan
|
|
||||||
|
|
||||||
## Ringkasan Optimasi
|
|
||||||
|
|
||||||
Struktur tabel baru ini **lebih sederhana**, **fokus pada perhitungan**, dan **menghilangkan redundansi** dari struktur sebelumnya.
|
|
||||||
|
|
||||||
## Perbandingan Struktur
|
|
||||||
|
|
||||||
### SEBELUM (Kompleks)
|
|
||||||
|
|
||||||
- `building_functions` - 8 kolom + relationship kompleks
|
|
||||||
- `building_function_parameters` - 12 kolom dengan mismatch model/migration
|
|
||||||
- `retribution_formulas` - Menyimpan formula sebagai string
|
|
||||||
- `retribution_proposals` - 15+ kolom dengan banyak redundansi
|
|
||||||
- `floor_height_indices` - OK, tidak berubah
|
|
||||||
|
|
||||||
### SESUDAH (Sederhana)
|
|
||||||
|
|
||||||
- `building_types` - **7 kolom**, hierarki sederhana
|
|
||||||
- `retribution_indices` - **6 kolom**, parameter calculation saja
|
|
||||||
- `height_indices` - **3 kolom**, sama seperti sebelumnya
|
|
||||||
- `retribution_configs` - **5 kolom**, konfigurasi global
|
|
||||||
- `retribution_calculations` - **8 kolom**, hasil perhitungan saja
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Detail Struktur Tabel Baru
|
|
||||||
|
|
||||||
### 1. `building_types`
|
|
||||||
|
|
||||||
**Fungsi:** Menyimpan jenis fungsi bangunan dengan hierarki sederhana
|
|
||||||
|
|
||||||
| Kolom | Tipe | Keterangan |
|
|
||||||
| ------------- | ------------ | ---------------------------------- |
|
|
||||||
| `id` | bigint | Primary key |
|
|
||||||
| `code` | varchar(10) | Kode unik (UMKM, KEAGAMAAN, dll) |
|
|
||||||
| `name` | varchar(100) | Nama fungsi bangunan |
|
|
||||||
| `parent_id` | bigint | ID parent (untuk hierarki) |
|
|
||||||
| `level` | tinyint | Level hierarki (1=parent, 2=child) |
|
|
||||||
| `coefficient` | decimal(8,4) | **Koefisien untuk perhitungan** |
|
|
||||||
| `is_free` | boolean | **Apakah gratis (keagamaan, MBR)** |
|
|
||||||
|
|
||||||
### 2. `retribution_indices`
|
|
||||||
|
|
||||||
**Fungsi:** Menyimpan parameter indeks untuk perhitungan (1:1 dengan building_types)
|
|
||||||
|
|
||||||
| Kolom | Tipe | Keterangan |
|
|
||||||
| ----------------------- | ------------ | ---------------------------------- |
|
|
||||||
| `id` | bigint | Primary key |
|
|
||||||
| `building_type_id` | bigint | FK ke building_types |
|
|
||||||
| `ip_permanent` | decimal(8,4) | **Indeks Permanensi** |
|
|
||||||
| `ip_complexity` | decimal(8,4) | **Indeks Kompleksitas** |
|
|
||||||
| `locality_index` | decimal(8,4) | **Indeks Lokalitas** |
|
|
||||||
| `infrastructure_factor` | decimal(8,4) | **Faktor prasarana (default 50%)** |
|
|
||||||
|
|
||||||
### 3. `height_indices`
|
|
||||||
|
|
||||||
**Fungsi:** Indeks ketinggian per lantai (sama seperti sebelumnya)
|
|
||||||
|
|
||||||
| Kolom | Tipe | Keterangan |
|
|
||||||
| -------------- | ------------ | ---------------------------------------- |
|
|
||||||
| `id` | bigint | Primary key |
|
|
||||||
| `floor_number` | tinyint | Nomor lantai (1,2,3,4,5,6) |
|
|
||||||
| `height_index` | decimal(8,6) | **IP Ketinggian (1.0, 1.09, 1.12, dst)** |
|
|
||||||
|
|
||||||
### 4. `retribution_configs`
|
|
||||||
|
|
||||||
**Fungsi:** Konfigurasi global untuk perhitungan (menggantikan hard-coded values)
|
|
||||||
|
|
||||||
| Kolom | Tipe | Keterangan |
|
|
||||||
| ------------- | ------------- | --------------------- |
|
|
||||||
| `id` | bigint | Primary key |
|
|
||||||
| `key` | varchar(50) | Kunci konfigurasi |
|
|
||||||
| `value` | decimal(15,2) | **Nilai konfigurasi** |
|
|
||||||
| `description` | varchar(200) | Deskripsi |
|
|
||||||
|
|
||||||
**Data yang disimpan:**
|
|
||||||
|
|
||||||
- `BASE_VALUE` = 70350 (nilai dasar)
|
|
||||||
- `INFRASTRUCTURE_MULTIPLIER` = 0.5 (50% prasarana)
|
|
||||||
- `HEIGHT_MULTIPLIER` = 0.5 (pengali indeks ketinggian)
|
|
||||||
|
|
||||||
### 5. `retribution_calculations`
|
|
||||||
|
|
||||||
**Fungsi:** Hasil perhitungan retribusi (history)
|
|
||||||
|
|
||||||
| Kolom | Tipe | Keterangan |
|
|
||||||
| -------------------- | ------------- | -------------------------------- |
|
|
||||||
| `id` | bigint | Primary key |
|
|
||||||
| `calculation_id` | varchar(20) | ID unik perhitungan |
|
|
||||||
| `building_type_id` | bigint | FK ke building_types |
|
|
||||||
| `floor_number` | tinyint | Lantai yang dipilih |
|
|
||||||
| `building_area` | decimal(12,2) | **Luas bangunan input** |
|
|
||||||
| `retribution_amount` | decimal(15,2) | **Hasil perhitungan** |
|
|
||||||
| `calculation_detail` | json | **Detail breakdown perhitungan** |
|
|
||||||
| `calculated_at` | timestamp | Waktu perhitungan |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Formula Perhitungan
|
|
||||||
|
|
||||||
### Formula Excel yang Diimplementasikan:
|
|
||||||
|
|
||||||
```
|
|
||||||
H13 = coefficient * (ip_permanent + ip_complexity + (0.5 * height_index))
|
|
||||||
|
|
||||||
Main Calculation = building_area * (locality_index * BASE_VALUE * H13)
|
|
||||||
|
|
||||||
Infrastructure = INFRASTRUCTURE_MULTIPLIER * Main Calculation
|
|
||||||
|
|
||||||
Total Retribution = Main Calculation + Infrastructure
|
|
||||||
```
|
|
||||||
|
|
||||||
### Implementasi dalam Service:
|
|
||||||
|
|
||||||
```php
|
|
||||||
// Step 1: Calculate H13 coefficient
|
|
||||||
$h13 = $buildingType->coefficient * (
|
|
||||||
$indices->ip_permanent +
|
|
||||||
$indices->ip_complexity +
|
|
||||||
(0.5 * $heightIndex)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Step 2: Main calculation
|
|
||||||
$mainCalculation = $buildingArea * ($indices->locality_index * $baseValue * $h13);
|
|
||||||
|
|
||||||
// Step 3: Infrastructure (50% additional)
|
|
||||||
$infrastructureCalculation = 0.5 * $mainCalculation;
|
|
||||||
|
|
||||||
// Step 4: Total
|
|
||||||
$totalRetribution = $mainCalculation + $infrastructureCalculation;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Keuntungan Struktur Baru
|
|
||||||
|
|
||||||
### ✅ **Simplicity**
|
|
||||||
|
|
||||||
- **5 tabel** vs 8+ tabel sebelumnya
|
|
||||||
- **Kolom minimal** hanya yang diperlukan untuk perhitungan
|
|
||||||
- **No redundant data** seperti ip_ketinggian di proposals
|
|
||||||
|
|
||||||
### ✅ **Performance**
|
|
||||||
|
|
||||||
- **Proper indexes** untuk query yang sering digunakan
|
|
||||||
- **Normalized structure** mengurangi storage
|
|
||||||
- **Cached configs** untuk values yang jarang berubah
|
|
||||||
|
|
||||||
### ✅ **Maintainability**
|
|
||||||
|
|
||||||
- **Clear separation** antara master data dan calculation results
|
|
||||||
- **Configurable values** tidak hard-coded lagi
|
|
||||||
- **Single responsibility** setiap tabel punya tujuan jelas
|
|
||||||
|
|
||||||
### ✅ **Flexibility**
|
|
||||||
|
|
||||||
- **Easy to extend** untuk fungsi bangunan baru
|
|
||||||
- **Configurable formulas** lewat RetributionConfig
|
|
||||||
- **Audit trail** lewat calculation history
|
|
||||||
|
|
||||||
### ✅ **Data Integrity**
|
|
||||||
|
|
||||||
- **Proper constraints** untuk validasi data
|
|
||||||
- **Foreign key relationships** yang benar
|
|
||||||
- **No model-migration mismatch**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Migration Guide
|
|
||||||
|
|
||||||
### Langkah Implementasi:
|
|
||||||
|
|
||||||
1. **Run Migration:** `php artisan migrate` untuk tabel baru
|
|
||||||
2. **Seed Data:** Data master berdasarkan Excel akan otomatis ter-seed
|
|
||||||
3. **Update Code:** Ganti penggunaan model lama dengan model baru
|
|
||||||
4. **Test Calculation:** Verifikasi hasil perhitungan sama dengan Excel
|
|
||||||
5. **Deploy:** Struktur siap production
|
|
||||||
|
|
||||||
### Data Migration (Optional):
|
|
||||||
|
|
||||||
Jika ada data existing di tabel lama yang perlu dipindahkan, buat script migration untuk transfer data dari struktur lama ke struktur baru.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Usage Example
|
|
||||||
|
|
||||||
```php
|
|
||||||
// Initialize service
|
|
||||||
$calculator = new RetributionCalculatorService();
|
|
||||||
|
|
||||||
// Calculate retribution
|
|
||||||
$result = $calculator->calculate(
|
|
||||||
buildingTypeId: 8, // UMKM
|
|
||||||
floorNumber: 2, // 2 lantai
|
|
||||||
buildingArea: 100.50, // 100.5 m2
|
|
||||||
saveResult: true // Simpan ke database
|
|
||||||
);
|
|
||||||
|
|
||||||
// Result structure
|
|
||||||
[
|
|
||||||
'building_type' => [...],
|
|
||||||
'total_retribution' => 31658.25,
|
|
||||||
'formatted_amount' => 'Rp 31,658.25',
|
|
||||||
'calculation_steps' => [...],
|
|
||||||
'calculation_id' => 'RTB-20250130140530-123'
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Struktur ini **jauh lebih clean**, **mudah dipahami**, dan **optimal untuk perhitungan retribusi PBG**!
|
|
||||||
191
README.md
191
README.md
@@ -1,112 +1,167 @@
|
|||||||
# Usage icon
|
# Sibedas PBG Web
|
||||||
|
|
||||||
search or pick icon in <a href="https://icon-sets.iconify.design/mingcute/?keyword=mingcute">here</a>
|
Aplikasi web untuk manajemen data PBG (Pendidikan Berkelanjutan Guru) dengan fitur integrasi Google Sheets.
|
||||||
|
|
||||||
# Set up queue for running automatically
|
## 🚀 Quick Start
|
||||||
|
|
||||||
- Install Supervisor
|
### Prerequisites
|
||||||
|
|
||||||
```
|
- Docker & Docker Compose
|
||||||
sudo apt update && sudo apt install supervisor -y
|
- Domain name (untuk production)
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd sibedas-pbg-web
|
||||||
|
./scripts/setup-local.sh
|
||||||
|
# Access: http://localhost:8000
|
||||||
```
|
```
|
||||||
|
|
||||||
- Create Supervisor Config
|
### Production Deployment
|
||||||
|
|
||||||
```
|
```bash
|
||||||
sudo nano /etc/supervisor/conf.d/laravel-worker.conf
|
# 1. Setup environment
|
||||||
|
cp env.production.example .env
|
||||||
|
nano .env
|
||||||
|
|
||||||
[program:laravel-worker]
|
# 2. Deploy with SSL (Recommended)
|
||||||
process_name=%(program_name)s_%(process_num)02d
|
./scripts/setup-reverse-proxy.sh setup
|
||||||
command=php /home/arifal/development/sibedas-pbg-web/artisan queue:work --queue=default --timeout=82800 --tries=1
|
|
||||||
autostart=true
|
# 3. Check status
|
||||||
autorestart=true
|
./scripts/setup-reverse-proxy.sh status
|
||||||
numprocs=1
|
# Access: https://yourdomain.com
|
||||||
redirect_stderr=true
|
|
||||||
stdout_logfile=/home/arifal/development/sibedas-pbg-web/storage/logs/worker.log
|
|
||||||
stopasgroup=true
|
|
||||||
killasgroup=true
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- Reload Supervisor
|
## 🏗️ Architecture
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo supervisorctl reread
|
Browser → Port 8000 → Nginx → PHP-FPM → MariaDB
|
||||||
sudo supervisorctl update
|
|
||||||
sudo supervisorctl start laravel-worker
|
|
||||||
sudo supervisorctl restart laravel-worker
|
|
||||||
sudo supervisorctl status
|
|
||||||
```
|
```
|
||||||
|
|
||||||
# How to running
|
### Production dengan Reverse Proxy
|
||||||
|
|
||||||
- Install composer package
|
|
||||||
|
|
||||||
```
|
```
|
||||||
composer install
|
Internet → Reverse Proxy (80/443) → Internal Nginx → PHP-FPM → MariaDB
|
||||||
```
|
```
|
||||||
|
|
||||||
- Install npm package
|
## 🔧 Configuration
|
||||||
|
|
||||||
```
|
### Environment Variables
|
||||||
npm install && npm run build
|
|
||||||
|
```bash
|
||||||
|
# Domain & SSL
|
||||||
|
DOMAIN=sibedas.yourdomain.com
|
||||||
|
EMAIL=admin@yourdomain.com
|
||||||
|
SSL_TYPE=self-signed # atau letsencrypt
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DB_PASSWORD=your_secure_password
|
||||||
|
MYSQL_ROOT_PASSWORD=your_root_password
|
||||||
|
|
||||||
|
# Laravel
|
||||||
|
APP_KEY=base64:your_app_key_here
|
||||||
|
APP_URL=https://sibedas.yourdomain.com
|
||||||
```
|
```
|
||||||
|
|
||||||
- Create symlinks storage
|
## 🚀 Production Deployment Steps
|
||||||
|
|
||||||
```
|
### 1. Server Preparation
|
||||||
php artisan storage:link
|
|
||||||
|
```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
|
||||||
```
|
```
|
||||||
|
|
||||||
- Running migration
|
### 2. Clone & Setup
|
||||||
|
|
||||||
```
|
```bash
|
||||||
php artisan migrate
|
git clone <repository-url>
|
||||||
|
cd sibedas-pbg-web
|
||||||
|
chmod +x scripts/*.sh
|
||||||
|
cp env.production.example .env
|
||||||
|
nano .env
|
||||||
```
|
```
|
||||||
|
|
||||||
- Running seeder
|
### 3. Deploy
|
||||||
|
|
||||||
```
|
```bash
|
||||||
php artisan db:seed
|
# Full deployment with SSL
|
||||||
|
./scripts/setup-reverse-proxy.sh setup
|
||||||
|
|
||||||
|
# Or step by step
|
||||||
|
./scripts/deploy-production.sh deploy
|
||||||
|
./scripts/setup-ssl.sh letsencrypt
|
||||||
```
|
```
|
||||||
|
|
||||||
- Create view table
|
### 4. Verify
|
||||||
- excute all sql queries on folder database/view_query
|
|
||||||
|
|
||||||
# Add ENV variable
|
```bash
|
||||||
|
docker-compose ps
|
||||||
- API_KEY_GOOGLE
|
./scripts/setup-reverse-proxy.sh status
|
||||||
|
curl -f http://localhost/health-check
|
||||||
```
|
|
||||||
Get api key from google developer console for and turn on spreadsheet api or feaature for google sheet
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- SPREAD_SHEET_ID
|
## 📊 Monitoring
|
||||||
|
|
||||||
```
|
```bash
|
||||||
Get spreadsheet id from google sheet link
|
# Check status
|
||||||
|
./scripts/setup-reverse-proxy.sh status
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose logs [service]
|
||||||
|
|
||||||
|
# Check SSL certificate
|
||||||
|
./scripts/setup-ssl.sh check
|
||||||
```
|
```
|
||||||
|
|
||||||
- OPENAI_API_KEY
|
## 🛠️ Common Commands
|
||||||
|
|
||||||
```
|
```bash
|
||||||
Get OpenAI API key from chatgpt subscription
|
# 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
|
||||||
```
|
```
|
||||||
|
|
||||||
- ENV
|
## 📁 Scripts
|
||||||
|
|
||||||
```
|
### Essential Scripts
|
||||||
|
|
||||||
API_KEY_GOOGLE="xxxxx"
|
- `scripts/setup-reverse-proxy.sh` - Setup lengkap reverse proxy dan SSL
|
||||||
SPREAD_SHEET_ID="xxxxx"
|
- `scripts/deploy-production.sh` - Deployment production
|
||||||
OPENAI_API_KEY="xxxxx"
|
- `scripts/setup-ssl.sh` - Setup SSL certificates
|
||||||
|
|
||||||
```
|
### Optional Scripts
|
||||||
|
|
||||||
# Technology version
|
- `scripts/setup-local.sh` - Setup local development
|
||||||
|
- `scripts/import-sibedas-database.sh` - Manual database import
|
||||||
|
|
||||||
- php 8.3
|
## 📚 Documentation
|
||||||
- Laravel 11
|
|
||||||
- node v22.13.0
|
Untuk dokumentasi lengkap, lihat [docs/README.md](docs/README.md)
|
||||||
- npm 10.9.2
|
|
||||||
- mariadb Ver 15.1 Distrib 10.6.18-MariaDB, for debian-linux-gnu (x86_64) using EditLine wrapper
|
## 🆘 Support
|
||||||
- Ubuntu 24.04
|
|
||||||
|
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/`
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class AssignSpatialPlanningsToCalculation extends Command
|
|||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $description = 'Assign retribution calculations to spatial plannings (supports recalculate for existing calculations)';
|
protected $description = 'Assign retribution calculations to spatial plannings (recalculate mode recalculates with current values)';
|
||||||
|
|
||||||
protected $calculatorService;
|
protected $calculatorService;
|
||||||
|
|
||||||
@@ -57,6 +57,7 @@ class AssignSpatialPlanningsToCalculation extends Command
|
|||||||
$q->where('is_active', true);
|
$q->where('is_active', true);
|
||||||
});
|
});
|
||||||
$this->info('🔄 Recalculate mode: Processing spatial plannings with existing calculations');
|
$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) {
|
} elseif (!$force) {
|
||||||
// Normal mode: only process those without active calculations
|
// Normal mode: only process those without active calculations
|
||||||
$query->whereDoesntHave('retributionCalculations', function ($q) {
|
$query->whereDoesntHave('retributionCalculations', function ($q) {
|
||||||
@@ -140,6 +141,7 @@ class AssignSpatialPlanningsToCalculation extends Command
|
|||||||
['Errors', $errors],
|
['Errors', $errors],
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
$this->info('📊 Recalculate mode recalculated all existing calculations with current values');
|
||||||
} else {
|
} else {
|
||||||
$this->table(
|
$this->table(
|
||||||
['Metric', 'Count'],
|
['Metric', 'Count'],
|
||||||
@@ -189,7 +191,7 @@ class AssignSpatialPlanningsToCalculation extends Command
|
|||||||
|
|
||||||
if ($recalculate) {
|
if ($recalculate) {
|
||||||
// Recalculate mode: Always create new calculation
|
// Recalculate mode: Always create new calculation
|
||||||
$calculationResult = $this->performCalculation($spatialPlanning, $buildingType);
|
$calculationResult = $this->performCalculation($spatialPlanning, $buildingType, true);
|
||||||
|
|
||||||
// Check if spatial planning has existing active calculation
|
// Check if spatial planning has existing active calculation
|
||||||
$currentActiveCalculation = $spatialPlanning->activeRetributionCalculation;
|
$currentActiveCalculation = $spatialPlanning->activeRetributionCalculation;
|
||||||
@@ -213,7 +215,7 @@ class AssignSpatialPlanningsToCalculation extends Command
|
|||||||
// Assign new calculation
|
// Assign new calculation
|
||||||
$spatialPlanning->assignRetributionCalculation(
|
$spatialPlanning->assignRetributionCalculation(
|
||||||
$calculation,
|
$calculation,
|
||||||
"Recalculated: Area {$oldArea}→{$buildingArea}, Amount {$oldAmount}→{$newAmount}"
|
"Recalculated: Original area {$oldArea}m² → New area {$buildingArea}m², Amount {$oldAmount}→{$newAmount}"
|
||||||
);
|
);
|
||||||
|
|
||||||
$isRecalculated = true;
|
$isRecalculated = true;
|
||||||
@@ -234,7 +236,7 @@ class AssignSpatialPlanningsToCalculation extends Command
|
|||||||
|
|
||||||
$spatialPlanning->assignRetributionCalculation(
|
$spatialPlanning->assignRetributionCalculation(
|
||||||
$calculation,
|
$calculation,
|
||||||
'Recalculated (new calculation)'
|
'Recalculated (new calculation with current values)'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -255,7 +257,7 @@ class AssignSpatialPlanningsToCalculation extends Command
|
|||||||
$reused = true;
|
$reused = true;
|
||||||
} else {
|
} else {
|
||||||
// Create new calculation
|
// Create new calculation
|
||||||
$calculationResult = $this->performCalculation($spatialPlanning, $buildingType);
|
$calculationResult = $this->performCalculation($spatialPlanning, $buildingType, false);
|
||||||
|
|
||||||
$calculation = RetributionCalculation::create([
|
$calculation = RetributionCalculation::create([
|
||||||
'building_type_id' => $buildingType->id,
|
'building_type_id' => $buildingType->id,
|
||||||
@@ -401,10 +403,16 @@ class AssignSpatialPlanningsToCalculation extends Command
|
|||||||
/**
|
/**
|
||||||
* Perform calculation using RetributionCalculatorService
|
* Perform calculation using RetributionCalculatorService
|
||||||
*/
|
*/
|
||||||
private function performCalculation(SpatialPlanning $spatialPlanning, BuildingType $buildingType): array
|
private function performCalculation(SpatialPlanning $spatialPlanning, BuildingType $buildingType, bool $recalculate = false): array
|
||||||
{
|
{
|
||||||
// Round area to 2 decimal places to match database storage format
|
// Round area to 2 decimal places to match database storage format
|
||||||
$buildingArea = round($spatialPlanning->getCalculationArea(), 2);
|
$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}m²");
|
||||||
|
}
|
||||||
|
|
||||||
$floorNumber = $spatialPlanning->number_of_floors ?: 1;
|
$floorNumber = $spatialPlanning->number_of_floors ?: 1;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -436,6 +444,7 @@ class AssignSpatialPlanningsToCalculation extends Command
|
|||||||
'is_free' => $buildingType->is_free,
|
'is_free' => $buildingType->is_free,
|
||||||
'calculation_date' => now()->toDateTimeString(),
|
'calculation_date' => now()->toDateTimeString(),
|
||||||
'total' => $result['total_retribution'],
|
'total' => $result['total_retribution'],
|
||||||
|
'is_recalculated' => $recalculate,
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -446,6 +455,11 @@ class AssignSpatialPlanningsToCalculation extends Command
|
|||||||
// Basic fallback calculation
|
// Basic fallback calculation
|
||||||
$totalAmount = $buildingType->is_free ? 0 : ($buildingArea * 50000);
|
$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 [
|
return [
|
||||||
'amount' => $totalAmount,
|
'amount' => $totalAmount,
|
||||||
'detail' => [
|
'detail' => [
|
||||||
@@ -460,6 +474,7 @@ class AssignSpatialPlanningsToCalculation extends Command
|
|||||||
'is_free' => $buildingType->is_free,
|
'is_free' => $buildingType->is_free,
|
||||||
'calculation_date' => now()->toDateTimeString(),
|
'calculation_date' => now()->toDateTimeString(),
|
||||||
'total' => $totalAmount,
|
'total' => $totalAmount,
|
||||||
|
'is_recalculated' => $recalculate,
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use App\Jobs\SyncronizeSIMBG;
|
|
||||||
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(){
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
public function handle()
|
|
||||||
{
|
|
||||||
SyncronizeSIMBG::dispatch()->onQueue('default');
|
|
||||||
Log::info("running scheduler daily scraping");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,317 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use App\Models\SpatialPlanning;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Maatwebsite\Excel\Facades\Excel;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
class InitSpatialPlanningDatas extends Command
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'spatial:init {file? : Path to the CSV/Excel file} {--truncate : Clear existing data before import} {--safe-truncate : Clear only spatial plannings without retribution proposals} {--force-truncate : Force truncate by disabling foreign key checks}';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'Import spatial planning data from CSV/Excel file for retribution';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle()
|
|
||||||
{
|
|
||||||
$filePath = $this->argument('file') ?? 'public/templates/Data_2025___Estimasi_Jumlah_Lantai.csv';
|
|
||||||
$fullPath = storage_path('app/' . $filePath);
|
|
||||||
|
|
||||||
// Check if file exists
|
|
||||||
if (!file_exists($fullPath)) {
|
|
||||||
$this->error("File not found: {$fullPath}");
|
|
||||||
$this->info("Available files in templates:");
|
|
||||||
$this->listAvailableFiles();
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle truncate options
|
|
||||||
if (($this->option('truncate') && $this->option('safe-truncate')) ||
|
|
||||||
($this->option('truncate') && $this->option('force-truncate')) ||
|
|
||||||
($this->option('safe-truncate') && $this->option('force-truncate'))) {
|
|
||||||
$this->error('Cannot use multiple truncate options together. Choose only one.');
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm truncate if requested
|
|
||||||
if ($this->option('truncate')) {
|
|
||||||
if ($this->confirm('This will delete all existing spatial planning data and related retribution proposals. Continue?')) {
|
|
||||||
$this->info('Truncating tables...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// First delete retribution proposals that reference spatial plannings
|
|
||||||
$deletedProposals = DB::table('retribution_proposals')
|
|
||||||
->whereNotNull('spatial_planning_id')
|
|
||||||
->count();
|
|
||||||
|
|
||||||
if ($deletedProposals > 0) {
|
|
||||||
DB::table('retribution_proposals')
|
|
||||||
->whereNotNull('spatial_planning_id')
|
|
||||||
->delete();
|
|
||||||
$this->info("Deleted {$deletedProposals} retribution proposals linked to spatial plannings.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method 1: Try truncate with disabled foreign key checks
|
|
||||||
try {
|
|
||||||
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
|
|
||||||
DB::table('spatial_plannings')->truncate();
|
|
||||||
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
|
|
||||||
$this->info('Spatial plannings table truncated successfully.');
|
|
||||||
} catch (\Exception $truncateError) {
|
|
||||||
// Method 2: Fallback to delete if truncate fails
|
|
||||||
$this->warn('Truncate failed, using delete method...');
|
|
||||||
$deletedSpatial = DB::table('spatial_plannings')->delete();
|
|
||||||
$this->info("Deleted {$deletedSpatial} spatial planning records.");
|
|
||||||
|
|
||||||
// Reset auto increment
|
|
||||||
DB::statement('ALTER TABLE spatial_plannings AUTO_INCREMENT = 1');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->error('Failed to truncate tables: ' . $e->getMessage());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$this->info('Operation cancelled.');
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force truncate - disable foreign key checks and truncate everything
|
|
||||||
if ($this->option('force-truncate')) {
|
|
||||||
if ($this->confirm('This will FORCE truncate ALL spatial planning data by disabling foreign key checks. This is risky! Continue?')) {
|
|
||||||
$this->info('Force truncating with disabled foreign key checks...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
DB::beginTransaction();
|
|
||||||
|
|
||||||
// Disable foreign key checks
|
|
||||||
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
|
|
||||||
|
|
||||||
// Truncate both tables
|
|
||||||
DB::table('retribution_proposals')->truncate();
|
|
||||||
DB::table('spatial_plannings')->truncate();
|
|
||||||
|
|
||||||
// Re-enable foreign key checks
|
|
||||||
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
|
|
||||||
|
|
||||||
$this->info('Force truncate completed successfully.');
|
|
||||||
|
|
||||||
DB::commit();
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
DB::rollBack();
|
|
||||||
// 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 force truncate: ' . $e->getMessage());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$this->info('Operation cancelled.');
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Safe truncate - only delete spatial plannings without retribution proposals
|
|
||||||
if ($this->option('safe-truncate')) {
|
|
||||||
if ($this->confirm('This will delete only spatial planning data that have no retribution proposals. Continue?')) {
|
|
||||||
$this->info('Safe truncating spatial plannings...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
DB::beginTransaction();
|
|
||||||
|
|
||||||
// Count spatial plannings with retribution proposals
|
|
||||||
$withProposals = DB::table('spatial_plannings')
|
|
||||||
->whereExists(function ($query) {
|
|
||||||
$query->select(DB::raw(1))
|
|
||||||
->from('retribution_proposals')
|
|
||||||
->whereColumn('retribution_proposals.spatial_planning_id', 'spatial_plannings.id');
|
|
||||||
})
|
|
||||||
->count();
|
|
||||||
|
|
||||||
// Delete spatial plannings without retribution proposals
|
|
||||||
$deletedCount = DB::table('spatial_plannings')
|
|
||||||
->whereNotExists(function ($query) {
|
|
||||||
$query->select(DB::raw(1))
|
|
||||||
->from('retribution_proposals')
|
|
||||||
->whereColumn('retribution_proposals.spatial_planning_id', 'spatial_plannings.id');
|
|
||||||
})
|
|
||||||
->delete();
|
|
||||||
|
|
||||||
$this->info("Deleted {$deletedCount} spatial plannings without retribution proposals.");
|
|
||||||
$this->info("Kept {$withProposals} spatial plannings that have retribution proposals.");
|
|
||||||
|
|
||||||
DB::commit();
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
DB::rollBack();
|
|
||||||
$this->error('Failed to safe truncate: ' . $e->getMessage());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$this->info('Operation cancelled.');
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->info("Starting import from: {$filePath}");
|
|
||||||
$this->info("Full path: {$fullPath}");
|
|
||||||
|
|
||||||
try {
|
|
||||||
DB::beginTransaction();
|
|
||||||
|
|
||||||
$data = Excel::toArray([], $fullPath);
|
|
||||||
|
|
||||||
if (empty($data) || empty($data[0])) {
|
|
||||||
$this->error('No data found in the file.');
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
$rows = $data[0]; // Get first sheet
|
|
||||||
$headers = array_shift($rows); // Remove header row
|
|
||||||
|
|
||||||
$this->info("Found " . count($rows) . " data rows to import.");
|
|
||||||
$this->info("Headers: " . implode(', ', $headers));
|
|
||||||
|
|
||||||
$progressBar = $this->output->createProgressBar(count($rows));
|
|
||||||
$progressBar->start();
|
|
||||||
|
|
||||||
$imported = 0;
|
|
||||||
$skipped = 0;
|
|
||||||
|
|
||||||
foreach ($rows as $index => $row) {
|
|
||||||
try {
|
|
||||||
// Skip empty rows
|
|
||||||
if (empty(array_filter($row))) {
|
|
||||||
$skipped++;
|
|
||||||
$progressBar->advance();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map CSV columns to model attributes
|
|
||||||
$spatialData = [
|
|
||||||
'name' => $this->cleanString($row[1] ?? ''), // pemohon
|
|
||||||
'location' => $this->cleanString($row[2] ?? ''), // alamat
|
|
||||||
'activities' => $this->cleanString($row[3] ?? ''), // activities
|
|
||||||
'land_area' => $this->cleanNumber($row[4] ?? 0), // luas_lahan
|
|
||||||
'site_bcr' => $this->cleanNumber($row[5] ?? 0), // bcr_kawasan
|
|
||||||
'area' => $this->cleanNumber($row[6] ?? 0), // area
|
|
||||||
'no_tapak' => $this->cleanString($row[7] ?? ''), // no_tapak
|
|
||||||
'no_skkl' => $this->cleanString($row[8] ?? ''), // no_skkl
|
|
||||||
'no_ukl' => $this->cleanString($row[9] ?? ''), // no_ukl
|
|
||||||
'building_function' => $this->cleanString($row[10] ?? ''), // fungsi_bangunan
|
|
||||||
'sub_building_function' => $this->cleanString($row[11] ?? ''), // sub_fungsi_bangunan
|
|
||||||
'number_of_floors' => $this->cleanNumber($row[12] ?? 1), // jumlah_lantai
|
|
||||||
'number' => $this->cleanString($row[0] ?? ''), // no
|
|
||||||
'date' => now(), // Set current date
|
|
||||||
'kbli' => null, // Not in CSV, set as null
|
|
||||||
];
|
|
||||||
|
|
||||||
// Validate required fields
|
|
||||||
if (empty($spatialData['name']) && empty($spatialData['activities'])) {
|
|
||||||
$skipped++;
|
|
||||||
$progressBar->advance();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
SpatialPlanning::create($spatialData);
|
|
||||||
$imported++;
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$this->newLine();
|
|
||||||
$this->error("Error importing row " . ($index + 2) . ": " . $e->getMessage());
|
|
||||||
$skipped++;
|
|
||||||
}
|
|
||||||
|
|
||||||
$progressBar->advance();
|
|
||||||
}
|
|
||||||
|
|
||||||
$progressBar->finish();
|
|
||||||
$this->newLine(2);
|
|
||||||
|
|
||||||
DB::commit();
|
|
||||||
|
|
||||||
$this->info("Import completed successfully!");
|
|
||||||
$this->info("Imported: {$imported} records");
|
|
||||||
$this->info("Skipped: {$skipped} records");
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
DB::rollBack();
|
|
||||||
$this->error("Import failed: " . $e->getMessage());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean string data
|
|
||||||
*/
|
|
||||||
private function cleanString($value)
|
|
||||||
{
|
|
||||||
if (is_null($value)) return null;
|
|
||||||
return trim(str_replace(["\n", "\r", "\t"], ' ', $value));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean numeric data
|
|
||||||
*/
|
|
||||||
private function cleanNumber($value)
|
|
||||||
{
|
|
||||||
if (is_null($value) || $value === '') return 0;
|
|
||||||
|
|
||||||
// Remove non-numeric characters except decimal point
|
|
||||||
$cleaned = preg_replace('/[^0-9.]/', '', $value);
|
|
||||||
|
|
||||||
return is_numeric($cleaned) ? (float) $cleaned : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List available template files
|
|
||||||
*/
|
|
||||||
private function listAvailableFiles()
|
|
||||||
{
|
|
||||||
$templatesPath = storage_path('app/public/templates');
|
|
||||||
if (is_dir($templatesPath)) {
|
|
||||||
$this->info("Files in storage/app/public/templates:");
|
|
||||||
$extensions = ['csv', 'xlsx', 'xls'];
|
|
||||||
foreach ($extensions as $ext) {
|
|
||||||
$files = glob($templatesPath . '/*.' . $ext);
|
|
||||||
foreach ($files as $file) {
|
|
||||||
$this->line(' - ' . basename($file));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$publicTemplatesPath = public_path('templates');
|
|
||||||
if (is_dir($publicTemplatesPath)) {
|
|
||||||
$this->info("Files in public/templates:");
|
|
||||||
$extensions = ['csv', 'xlsx', 'xls'];
|
|
||||||
foreach ($extensions as $ext) {
|
|
||||||
$files = glob($publicTemplatesPath . '/*.' . $ext);
|
|
||||||
foreach ($files as $file) {
|
|
||||||
$this->line(' - ' . basename($file));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
790
app/Console/Commands/InjectSpatialPlanningsData.php
Normal file
790
app/Console/Commands/InjectSpatialPlanningsData.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use App\Jobs\ScrapingDataJob;
|
|
||||||
use App\Models\ImportDatasource;
|
|
||||||
use App\Services\ServiceGoogleSheet;
|
|
||||||
use App\Services\ServicePbgTask;
|
|
||||||
use App\Services\ServiceTabPbgTask;
|
|
||||||
use App\Services\ServiceTokenSIMBG;
|
|
||||||
use GuzzleHttp\Client; // Import Guzzle Client
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
|
|
||||||
class ScrapingData extends Command
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'app:scraping-data';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'Command description';
|
|
||||||
/**
|
|
||||||
* Inject dependencies.
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
) {
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handle()
|
|
||||||
{
|
|
||||||
dispatch(new ScrapingDataJob());
|
|
||||||
|
|
||||||
$this->info("Scraping job dispatched successfully");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<?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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
80
app/Console/Commands/StartScrapingData.php
Normal file
80
app/Console/Commands/StartScrapingData.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
62
app/Console/Commands/SyncDashboardPbg.php
Normal file
62
app/Console/Commands/SyncDashboardPbg.php
Normal 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()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
98
app/Console/Commands/SyncGoogleSheetData.php
Normal file
98
app/Console/Commands/SyncGoogleSheetData.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
65
app/Console/Commands/SyncPbgTaskPayments.php
Normal file
65
app/Console/Commands/SyncPbgTaskPayments.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/Console/Commands/TruncatePBGTable.php
Normal file
42
app/Console/Commands/TruncatePBGTable.php
Normal 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;');
|
||||||
|
}
|
||||||
|
}
|
||||||
266
app/Console/Commands/TruncateSpatialPlanningData.php
Normal file
266
app/Console/Commands/TruncateSpatialPlanningData.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,14 +9,32 @@ enum PbgTaskFilterData : string
|
|||||||
case verified = 'verified';
|
case verified = 'verified';
|
||||||
case non_verified = 'non-verified';
|
case non_verified = 'non-verified';
|
||||||
case all = 'all';
|
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 {
|
public static function getAllOptions() : array {
|
||||||
return [
|
return [
|
||||||
self::all->value => 'Potensi Berkas',
|
self::all->value => 'Semua Berkas',
|
||||||
self::business->value => 'Usaha',
|
self::business->value => 'Usaha',
|
||||||
self::non_business->value => 'Bukan Usaha',
|
self::non_business->value => 'Bukan Usaha',
|
||||||
self::verified->value => 'Terverifikasi',
|
self::verified->value => 'Terverifikasi',
|
||||||
self::non_verified->value => 'Belum 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',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,4 +56,90 @@ enum PbgTaskStatus: int
|
|||||||
{
|
{
|
||||||
return self::getStatuses()[$status] ?? null;
|
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
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
118
app/Exports/PbgTaskExport.php
Normal file
118
app/Exports/PbgTaskExport.php
Normal 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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
59
app/Exports/TaxSubdistrictSheetExport.php
Normal file
59
app/Exports/TaxSubdistrictSheetExport.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
23
app/Exports/TaxationsExport.php
Normal file
23
app/Exports/TaxationsExport.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ use App\Http\Controllers\Controller;
|
|||||||
use App\Http\Resources\BigdataResumeResource;
|
use App\Http\Resources\BigdataResumeResource;
|
||||||
use App\Models\BigdataResume;
|
use App\Models\BigdataResume;
|
||||||
use App\Models\DataSetting;
|
use App\Models\DataSetting;
|
||||||
|
use App\Models\SpatialPlanning;
|
||||||
|
use App\Models\PbgTaskPayment;
|
||||||
use Barryvdh\DomPDF\Facade\Pdf;
|
use Barryvdh\DomPDF\Facade\Pdf;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -23,15 +25,16 @@ class BigDataResumeController extends Controller
|
|||||||
{
|
{
|
||||||
try{
|
try{
|
||||||
$filterDate = $request->get("filterByDate");
|
$filterDate = $request->get("filterByDate");
|
||||||
|
$type = trim($request->get("type"));
|
||||||
|
|
||||||
if (!$filterDate || $filterDate === "latest") {
|
if (!$filterDate || $filterDate === "latest") {
|
||||||
$big_data_resume = BigdataResume::where('year', 'leader')->latest()->first();
|
$big_data_resume = BigdataResume::where('resume_type', $type)->latest()->first();
|
||||||
if (!$big_data_resume) {
|
if (!$big_data_resume) {
|
||||||
return $this->response_empty_resume();
|
return $this->response_empty_resume();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$big_data_resume = BigdataResume::where('year', 'leader')
|
$big_data_resume = BigdataResume::whereDate('created_at', $filterDate)
|
||||||
->whereDate('created_at', $filterDate)
|
->where('resume_type', $type)
|
||||||
->orderBy('id', 'desc')
|
->orderBy('id', 'desc')
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
@@ -41,23 +44,28 @@ class BigDataResumeController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$data_settings = DataSetting::all();
|
$data_settings = DataSetting::all();
|
||||||
if($data_settings->isEmpty()){
|
$target_pad = 0;
|
||||||
return response()->json(['message' => 'No data setting found']);
|
if($data_settings->where('key', 'TARGET_PAD')->first()){
|
||||||
|
$target_pad = floatval($data_settings->where('key', 'TARGET_PAD')->first()->value ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanNumber($value) {
|
$realisasi_terbit_pbg_sum = $big_data_resume->issuance_realization_pbg_sum;
|
||||||
return floatval(str_replace('.', '', $value));
|
$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;
|
||||||
|
|
||||||
$target_pad = floatval(optional($data_settings->where('key', 'TARGET_PAD')->first())->value);
|
// Get real-time spatial planning data using new calculation formula
|
||||||
$realisasi_terbit_pbg_sum = cleanNumber(optional($data_settings->where('key', 'REALISASI_TERBIT_PBG_SUM')->first())->value);
|
$spatialData = $this->getSpatialPlanningData();
|
||||||
$realisasi_terbit_pbg_count = cleanNumber(optional($data_settings->where('key', 'REALISASI_TERBIT_PBG_COUNT')->first())->value);
|
$tata_ruang = $spatialData['sum'];
|
||||||
$menunggu_klik_dpmptsp_sum = cleanNumber(optional($data_settings->where('key', 'MENUNGGU_KLIK_DPMPTSP_SUM')->first())->value);
|
$tata_ruang_count = $spatialData['count'];
|
||||||
$menunggu_klik_dpmptsp_count = cleanNumber(optional($data_settings->where('key', 'MENUNGGU_KLIK_DPMPTSP_COUNT')->first())->value);
|
|
||||||
$proses_dinas_teknis_sum = cleanNumber(optional($data_settings->where('key', 'PROSES_DINAS_TEKNIS_SUM')->first())->value);
|
// Get real-time PBG Task Payments data
|
||||||
$proses_dinas_teknis_count = cleanNumber(optional($data_settings->where('key', 'PROSES_DINAS_TEKNIS_COUNT')->first())->value);
|
$pbgPaymentsData = $this->getPbgTaskPaymentsData();
|
||||||
|
$pbg_task_payments_sum = $pbgPaymentsData['sum'];
|
||||||
|
$pbg_task_payments_count = $pbgPaymentsData['count'];
|
||||||
|
|
||||||
$tata_ruang = $big_data_resume->spatial_sum;
|
|
||||||
$kekurangan_potensi = $target_pad - $big_data_resume->potention_sum;
|
$kekurangan_potensi = $target_pad - $big_data_resume->potention_sum;
|
||||||
|
|
||||||
// percentage kekurangan potensi
|
// percentage kekurangan potensi
|
||||||
@@ -68,46 +76,62 @@ class BigDataResumeController extends Controller
|
|||||||
$total_potensi_percentage = $big_data_resume->potention_sum > 0 && $target_pad > 0
|
$total_potensi_percentage = $big_data_resume->potention_sum > 0 && $target_pad > 0
|
||||||
? round(($big_data_resume->potention_sum / $target_pad) * 100, 2) : 0;
|
? round(($big_data_resume->potention_sum / $target_pad) * 100, 2) : 0;
|
||||||
|
|
||||||
// percentage verified document
|
// // percentage verified document (verified_sum / potention_sum) - by value/amount
|
||||||
$verified_percentage = $big_data_resume->verified_sum > 0 && $big_data_resume->potention_sum > 0
|
// $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;
|
// ? round(($big_data_resume->verified_sum / $big_data_resume->potention_sum) * 100, 2) : 0;
|
||||||
|
|
||||||
// percentage non-verified document
|
// // percentage non-verified document (non_verified_sum / potention_sum) - by value/amount
|
||||||
$non_verified_percentage = $big_data_resume->non_verified_sum > 0 && $big_data_resume->potention_sum > 0
|
// $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;
|
// ? round(($big_data_resume->non_verified_sum / $big_data_resume->potention_sum) * 100, 2) : 0;
|
||||||
|
|
||||||
// percentage business document
|
// Alternative: percentage by count (if needed)
|
||||||
$business_percentage = $big_data_resume->business_sum > 0 && $big_data_resume->non_verified_sum > 0
|
$verified_count_percentage = $big_data_resume->potention_count > 0 && $big_data_resume->verified_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 && $big_data_resume->non_verified_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;
|
? round(($big_data_resume->business_sum / $big_data_resume->non_verified_sum) * 100, 2) : 0;
|
||||||
|
|
||||||
// percentage non-business document
|
// percentage non-business document (non_business / non_verified)
|
||||||
$non_business_percentage = $big_data_resume->non_business_sum > 0 && $big_data_resume->potention_sum > 0
|
$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->potention_sum) * 100, 2) : 0;
|
? round(($big_data_resume->non_business_sum / $big_data_resume->non_verified_sum) * 100, 2) : 0;
|
||||||
|
|
||||||
// percentage tata ruang
|
// percentage tata ruang (spatial / potention)
|
||||||
$tata_ruang_percentage = $tata_ruang > 0 && $big_data_resume->potention_sum > 0
|
$tata_ruang_percentage = $big_data_resume->potention_sum > 0 && $tata_ruang >= 0
|
||||||
? round(($tata_ruang / $big_data_resume->potention_sum) * 100, 2) : 0;
|
? round(($tata_ruang / $big_data_resume->potention_sum) * 100, 2) : 0;
|
||||||
|
|
||||||
// percentage realisasi terbit pbg
|
// percentage realisasi terbit pbg (issuance / verified)
|
||||||
$realisasi_terbit_percentage = $big_data_resume->verified_sum > 0 && $realisasi_terbit_pbg_sum > 0
|
$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;
|
? round(($realisasi_terbit_pbg_sum / $big_data_resume->verified_sum) * 100, 2) : 0;
|
||||||
|
|
||||||
// percentage menunggu klik dpmptsp
|
// percentage menunggu klik dpmptsp (waiting / verified)
|
||||||
$menunggu_klik_dpmptsp_percentage = $big_data_resume->verified_sum > 0 && $menunggu_klik_dpmptsp_sum > 0
|
$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;
|
? round(($menunggu_klik_dpmptsp_sum / $big_data_resume->verified_sum) * 100, 2) : 0;
|
||||||
|
|
||||||
// percentage proses_dinas_teknis
|
// percentage proses_dinas_teknis (process / verified)
|
||||||
$proses_dinas_teknis_percentage = $big_data_resume->verified_sum > 0 && $proses_dinas_teknis_sum > 0
|
$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;
|
? 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 = [
|
$result = [
|
||||||
'target_pad' => [
|
'target_pad' => [
|
||||||
'sum' => $target_pad,
|
'sum' => $target_pad,
|
||||||
'percentage' => 100,
|
'percentage' => 100,
|
||||||
],
|
],
|
||||||
'tata_ruang' => [
|
'tata_ruang' => [
|
||||||
'sum' => $big_data_resume->spatial_sum,
|
'sum' => $tata_ruang,
|
||||||
'count' => $big_data_resume->spatial_count,
|
'count' => $tata_ruang_count,
|
||||||
'percentage' => $tata_ruang_percentage,
|
'percentage' => $tata_ruang_percentage,
|
||||||
],
|
],
|
||||||
'kekurangan_potensi' => [
|
'kekurangan_potensi' => [
|
||||||
@@ -122,12 +146,12 @@ class BigDataResumeController extends Controller
|
|||||||
'verified_document' => [
|
'verified_document' => [
|
||||||
'sum' => (float) $big_data_resume->verified_sum,
|
'sum' => (float) $big_data_resume->verified_sum,
|
||||||
'count' => $big_data_resume->verified_count,
|
'count' => $big_data_resume->verified_count,
|
||||||
'percentage' => $verified_percentage
|
'percentage' => $verified_count_percentage
|
||||||
],
|
],
|
||||||
'non_verified_document' => [
|
'non_verified_document' => [
|
||||||
'sum' => (float) $big_data_resume->non_verified_sum,
|
'sum' => (float) $big_data_resume->non_verified_sum,
|
||||||
'count' => $big_data_resume->non_verified_count,
|
'count' => $big_data_resume->non_verified_count,
|
||||||
'percentage' => $non_verified_percentage
|
'percentage' => $non_verified_count_percentage
|
||||||
],
|
],
|
||||||
'business_document' => [
|
'business_document' => [
|
||||||
'sum' => (float) $big_data_resume->business_sum,
|
'sum' => (float) $big_data_resume->business_sum,
|
||||||
@@ -153,6 +177,16 @@ class BigDataResumeController extends Controller
|
|||||||
'sum' => $proses_dinas_teknis_sum,
|
'sum' => $proses_dinas_teknis_sum,
|
||||||
'count' => $proses_dinas_teknis_count,
|
'count' => $proses_dinas_teknis_count,
|
||||||
'percentage' => $proses_dinas_teknis_percentage
|
'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);
|
return response()->json($result);
|
||||||
@@ -322,9 +356,15 @@ class BigDataResumeController extends Controller
|
|||||||
return $pdf->download('laporan-pimpinan.pdf');
|
return $pdf->download('laporan-pimpinan.pdf');
|
||||||
}
|
}
|
||||||
private function response_empty_resume(){
|
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 = [
|
$result = [
|
||||||
'target_pad' => [
|
'target_pad' => [
|
||||||
'sum' => 0,
|
'sum' => $target_pad,
|
||||||
'percentage' => 100,
|
'percentage' => 100,
|
||||||
],
|
],
|
||||||
'tata_ruang' => [
|
'tata_ruang' => [
|
||||||
@@ -374,9 +414,97 @@ class BigDataResumeController extends Controller
|
|||||||
'sum' => 0,
|
'sum' => 0,
|
||||||
'count' => 0,
|
'count' => 0,
|
||||||
'percentage' => 0
|
'percentage' => 0
|
||||||
|
],
|
||||||
|
'pbg_task_payments' => [
|
||||||
|
'sum' => 0,
|
||||||
|
'count' => 0,
|
||||||
|
'percentage' => 0
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
return response()->json($result);
|
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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Http\Resources\CustomersResource;
|
|||||||
use App\Imports\CustomersImport;
|
use App\Imports\CustomersImport;
|
||||||
use App\Models\Customer;
|
use App\Models\Customer;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
use Maatwebsite\Excel\Facades\Excel;
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
|
||||||
class CustomersController extends Controller
|
class CustomersController extends Controller
|
||||||
@@ -120,7 +121,7 @@ class CustomersController extends Controller
|
|||||||
'message' => 'File uploaded successfully',
|
'message' => 'File uploaded successfully',
|
||||||
]);
|
]);
|
||||||
}catch(\Exception $e){
|
}catch(\Exception $e){
|
||||||
\Log::info($e->getMessage());
|
Log::info($e->getMessage());
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'error' => 'Failed to upload file',
|
'error' => 'Failed to upload file',
|
||||||
'message' => $e->getMessage()
|
'message' => $e->getMessage()
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ use App\Models\Customer;
|
|||||||
use App\Models\SpatialPlanning;
|
use App\Models\SpatialPlanning;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Models\TourismBasedKBLI;
|
use App\Models\TourismBasedKBLI;
|
||||||
|
use App\Models\Tax;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class LackOfPotentialController extends Controller
|
class LackOfPotentialController extends Controller
|
||||||
{
|
{
|
||||||
@@ -18,8 +20,13 @@ class LackOfPotentialController extends Controller
|
|||||||
$total_pdam = Customer::count();
|
$total_pdam = Customer::count();
|
||||||
$total_tata_ruang = SpatialPlanning::count();
|
$total_tata_ruang = SpatialPlanning::count();
|
||||||
$total_tata_ruang_usaha = SpatialPlanning::where('building_function','like', '%usaha%')->count();
|
$total_tata_ruang_usaha = SpatialPlanning::where('building_function','like', '%usaha%')->count();
|
||||||
$total_tata_ruang_non_usaha = SpatialPlanning::where('building_function','like', '%hunian%')->count();
|
$total_tata_ruang_non_usaha = SpatialPlanning::where('building_function','not like', '%usaha%')->count();
|
||||||
$data_report_tourism = TourismBasedKBLI::all();
|
$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([
|
return response()->json([
|
||||||
'total_reklame' => $total_reklame,
|
'total_reklame' => $total_reklame,
|
||||||
@@ -28,6 +35,12 @@ class LackOfPotentialController extends Controller
|
|||||||
'total_tata_ruang_usaha' => $total_tata_ruang_usaha,
|
'total_tata_ruang_usaha' => $total_tata_ruang_usaha,
|
||||||
'total_tata_ruang_non_usaha' => $total_tata_ruang_non_usaha,
|
'total_tata_ruang_non_usaha' => $total_tata_ruang_non_usaha,
|
||||||
'data_report' => $data_report_tourism,
|
'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);
|
], 200);
|
||||||
}catch(\Exception $e){
|
}catch(\Exception $e){
|
||||||
return response()->json([
|
return response()->json([
|
||||||
@@ -35,4 +48,63 @@ class LackOfPotentialController extends Controller
|
|||||||
], 500);
|
], 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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ use App\Models\DataSetting;
|
|||||||
use App\Models\ImportDatasource;
|
use App\Models\ImportDatasource;
|
||||||
use App\Models\PbgTask;
|
use App\Models\PbgTask;
|
||||||
use App\Models\PbgTaskGoogleSheet;
|
use App\Models\PbgTaskGoogleSheet;
|
||||||
use App\Services\GoogleSheetService;
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
@@ -20,10 +19,6 @@ use Illuminate\Validation\Rules\Enum;
|
|||||||
|
|
||||||
class PbgTaskController extends Controller
|
class PbgTaskController extends Controller
|
||||||
{
|
{
|
||||||
protected $googleSheetService;
|
|
||||||
public function __construct(GoogleSheetService $googleSheetService){
|
|
||||||
$this->googleSheetService = $googleSheetService;
|
|
||||||
}
|
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
info($request);
|
info($request);
|
||||||
@@ -132,39 +127,48 @@ class PbgTaskController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'nullable|string|max:255',
|
||||||
'owner_name' => 'required|string|max:255',
|
'owner_name' => 'nullable|string|max:255',
|
||||||
'application_type' => ['nullable', new Enum(PbgTaskApplicationTypes::class)],
|
'application_type' => ['nullable', new Enum(PbgTaskApplicationTypes::class)],
|
||||||
'condition' => 'required|string|max:255',
|
'condition' => 'nullable|string|max:255',
|
||||||
'registration_number' => 'required|string|max:255',
|
'registration_number' => 'nullable|string|max:255',
|
||||||
'document_number' => 'required|string|max:255',
|
'document_number' => 'nullable|string|max:255',
|
||||||
'status' => ['nullable', new Enum(PbgTaskStatus::class)],
|
'status' => ['nullable', new Enum(PbgTaskStatus::class)],
|
||||||
'address' => 'required|string|max:255',
|
'address' => 'nullable|string|max:255',
|
||||||
'slf_status_name' => 'nullable|string|max:255',
|
'slf_status_name' => 'nullable|string|max:255',
|
||||||
'function_type' => 'required|string|max:255',
|
'function_type' => 'nullable|string|max:255',
|
||||||
'consultation_type' => 'required|string|max:255',
|
'consultation_type' => 'nullable|string|max:255',
|
||||||
'due_date' => 'nullable|date|after_or_equal:today',
|
'due_date' => 'nullable|date',
|
||||||
|
'is_valid' => 'nullable|boolean',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$statusLabel = $validated['status'] !== null ? PbgTaskStatus::getLabel($validated['status']) : null;
|
$statusLabel = $validated['status'] !== null ? PbgTaskStatus::getLabel($validated['status']) : null;
|
||||||
$applicationLabel = $validated['application_type'] !== null ? PbgTaskApplicationTypes::getLabel($validated['application_type']) : null;
|
$applicationLabel = $validated['application_type'] !== null ? PbgTaskApplicationTypes::getLabel($validated['application_type']) : null;
|
||||||
|
|
||||||
$pbg_task->update([
|
// Prepare update data - only include fields that are actually provided
|
||||||
'name' => $validated['name'],
|
$updateData = [];
|
||||||
'owner_name' => $validated['owner_name'],
|
|
||||||
'application_type' => $validated['application_type'],
|
foreach ($validated as $key => $value) {
|
||||||
'application_type_name' => $applicationLabel, // Automatically set application_type_name
|
if ($value !== null || $request->has($key)) {
|
||||||
'condition' => $validated['condition'],
|
$updateData[$key] = $value;
|
||||||
'registration_number' => $validated['registration_number'],
|
}
|
||||||
'document_number' => $validated['document_number'],
|
}
|
||||||
'status' => $validated['status'],
|
|
||||||
'status_name' => $statusLabel, // Automatically set status_name
|
// Handle special cases for labels
|
||||||
'address' => $validated['address'],
|
if (isset($updateData['status'])) {
|
||||||
'slf_status_name' => $validated['slf_status_name'],
|
$updateData['status_name'] = $statusLabel;
|
||||||
'function_type' => $validated['function_type'],
|
}
|
||||||
'consultation_type' => $validated['consultation_type'],
|
|
||||||
'due_date' => $validated['due_date'],
|
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([
|
return response()->json([
|
||||||
"success"=> true,
|
"success"=> true,
|
||||||
"message"=> "Data berhasil diubah",
|
"message"=> "Data berhasil diubah",
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ use Barryvdh\DomPDF\Facade\Pdf;
|
|||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Maatwebsite\Excel\Facades\Excel;
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
use App\Enums\PbgTaskStatus;
|
||||||
|
|
||||||
class RequestAssignmentController extends Controller
|
class RequestAssignmentController extends Controller
|
||||||
{
|
{
|
||||||
@@ -21,52 +22,365 @@ class RequestAssignmentController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$query = PbgTask::with([
|
// 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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
'attachments' => function ($q) {
|
||||||
$q->whereIn('pbg_type', ['berita_acara', 'bukti_bayar']);
|
$q->whereIn('pbg_type', ['berita_acara', 'bukti_bayar']);
|
||||||
},
|
},
|
||||||
'googleSheet'
|
'pbg_task_retributions',
|
||||||
|
'pbg_task_detail',
|
||||||
|
'pbg_status'
|
||||||
])->orderBy('id', 'desc');
|
])->orderBy('id', 'desc');
|
||||||
|
|
||||||
if ($request->has('filter') && !empty($request->get('filter'))) {
|
// Log final query count for debugging
|
||||||
$filter = strtolower($request->get('filter'));
|
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) {
|
switch ($filter) {
|
||||||
|
case 'all':
|
||||||
|
// No additional filters, just return all valid records
|
||||||
|
break;
|
||||||
case 'non-business':
|
case 'non-business':
|
||||||
$query->whereRaw("LOWER(function_type) != ?", ['sebagai tempat usaha'])->orWhereNull('function_type');
|
// 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;
|
break;
|
||||||
|
|
||||||
case 'business':
|
case 'business':
|
||||||
$query->whereRaw("LOWER(function_type) = ?", ['sebagai tempat usaha']);
|
// 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;
|
break;
|
||||||
|
|
||||||
case 'verified':
|
case 'verified':
|
||||||
$query->whereHas('googleSheet', function ($q) {
|
// Match BigdataResume verified logic exactly
|
||||||
$q->whereRaw("LOWER(status_verifikasi) = ?", ['selesai verifikasi']);
|
$query->whereIn("status", PbgTaskStatus::getVerified());
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'non-verified':
|
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) {
|
$query->where(function ($q) {
|
||||||
$q->whereDoesntHave('googleSheet')
|
$q->where(function ($q2) {
|
||||||
->orWhereHas('googleSheet', function ($q2) {
|
$q2->where(function ($q3) {
|
||||||
$q2->whereRaw("LOWER(status_verifikasi) != ?", ['selesai verifikasi'])->orWhereNull('status_verifikasi');
|
$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_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', 5)
|
||||||
|
->where('pbg_task_detail_data_lists.status', '!=', 1);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Log unrecognized filter for debugging
|
||||||
|
Log::warning('Unrecognized filter value', ['filter' => $filter]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->has('search') && !empty($request->get("search"))) {
|
/**
|
||||||
$search = $request->get('search');
|
* Apply search logic to the query
|
||||||
|
*/
|
||||||
|
private function applySearch($query, string $search)
|
||||||
|
{
|
||||||
|
// Search in pbg_task columns
|
||||||
$query->where(function ($q) use ($search) {
|
$query->where(function ($q) use ($search) {
|
||||||
$q->where('name', 'LIKE', "%$search%")
|
$q->where('name', 'LIKE', "%$search%")
|
||||||
->orWhere('registration_number', 'LIKE', "%$search%")
|
->orWhere('registration_number', 'LIKE', "%$search%")
|
||||||
->orWhere('document_number', 'LIKE', "%$search%");
|
->orWhere('owner_name', 'LIKE', "%$search%")
|
||||||
|
->orWhere('address', 'LIKE', "%$search%");
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return RequestAssignmentResouce::collection($query->paginate());
|
// 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)
|
public function report_payment_recaps(Request $request)
|
||||||
@@ -158,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_detail', 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_detail', function ($q4) {
|
||||||
|
$q4->where('unit', '>', 1);
|
||||||
|
})
|
||||||
|
->orWhereDoesntHave('pbg_task_detail');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->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
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ class SpatialPlanningController extends Controller
|
|||||||
$search = $request->input('search', '');
|
$search = $request->input('search', '');
|
||||||
|
|
||||||
$query = SpatialPlanning::query();
|
$query = SpatialPlanning::query();
|
||||||
|
|
||||||
|
// Only include spatial plannings that are not yet issued (is_terbit = false)
|
||||||
|
$query->where('is_terbit', false);
|
||||||
|
|
||||||
if ($search) {
|
if ($search) {
|
||||||
$query->where(function ($q) use ($search) {
|
$query->where(function ($q) use ($search) {
|
||||||
$q->where('name', 'like', "%$search%")
|
$q->where('name', 'like', "%$search%")
|
||||||
@@ -42,9 +46,11 @@ class SpatialPlanningController extends Controller
|
|||||||
// Menambhakan nomor urut (No)
|
// Menambhakan nomor urut (No)
|
||||||
$start = ($spatialPlannings->currentPage()-1) * $perPage + 1;
|
$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) {
|
$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);
|
info($data);
|
||||||
@@ -104,9 +110,10 @@ class SpatialPlanningController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display the specified resource.
|
* 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
98
app/Http/Controllers/Api/TaxationsController.php
Normal file
98
app/Http/Controllers/Api/TaxationsController.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,7 +36,9 @@ class UsersController extends Controller
|
|||||||
return UserResource::collection($query->paginate(config('app.paginate_per_page', 50)));
|
return UserResource::collection($query->paginate(config('app.paginate_per_page', 50)));
|
||||||
}
|
}
|
||||||
public function logout(Request $request){
|
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']);
|
return response()->json(['message' => 'logged out successfully']);
|
||||||
}
|
}
|
||||||
public function store(UsersRequest $request){
|
public function store(UsersRequest $request){
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ class AuthenticatedSessionController extends Controller
|
|||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
// Hapus token lama jika ada
|
// Hapus token lama jika ada
|
||||||
$user->tokens()->delete();
|
\Laravel\Sanctum\PersonalAccessToken::where('tokenable_id', $user->id)
|
||||||
|
->where('tokenable_type', get_class($user))
|
||||||
|
->delete();
|
||||||
|
|
||||||
// Buat token untuk API dengan scope dan expiration
|
// Buat token untuk API dengan scope dan expiration
|
||||||
$tokenName = config('app.name', 'Laravel') . '-' . $user->id . '-' . time();
|
$tokenName = config('app.name', 'Laravel') . '-' . $user->id . '-' . time();
|
||||||
@@ -48,7 +50,14 @@ class AuthenticatedSessionController extends Controller
|
|||||||
// Simpan token di session untuk digunakan di frontend
|
// Simpan token di session untuk digunakan di frontend
|
||||||
session(['api_token' => $token]);
|
session(['api_token' => $token]);
|
||||||
|
|
||||||
return redirect()->intended(RouteServiceProvider::HOME);
|
// Simpan timestamp login untuk validasi multi-user
|
||||||
|
session(['login_timestamp' => now()->timestamp]);
|
||||||
|
session(['user_id' => $user->id]);
|
||||||
|
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,7 +75,9 @@ class AuthenticatedSessionController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete existing tokens
|
// Delete existing tokens
|
||||||
$user->tokens()->delete();
|
\Laravel\Sanctum\PersonalAccessToken::where('tokenable_id', $user->id)
|
||||||
|
->where('tokenable_type', get_class($user))
|
||||||
|
->delete();
|
||||||
|
|
||||||
// Generate new token
|
// Generate new token
|
||||||
$tokenName = config('app.name', 'Laravel') . '-' . $user->id . '-' . time();
|
$tokenName = config('app.name', 'Laravel') . '-' . $user->id . '-' . time();
|
||||||
@@ -107,7 +118,9 @@ class AuthenticatedSessionController extends Controller
|
|||||||
public function destroy(Request $request)
|
public function destroy(Request $request)
|
||||||
{
|
{
|
||||||
if($request->user()){
|
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();
|
Auth::guard('web')->logout();
|
||||||
|
|||||||
@@ -82,9 +82,15 @@ class SpatialPlanningController extends Controller
|
|||||||
"kbli"=> "KBLI",
|
"kbli"=> "KBLI",
|
||||||
"activities"=> "Kegiatan",
|
"activities"=> "Kegiatan",
|
||||||
"area"=> "Luas (m2)",
|
"area"=> "Luas (m2)",
|
||||||
|
"land_area"=> "Luas Lahan (m2)",
|
||||||
"location"=> "Lokasi",
|
"location"=> "Lokasi",
|
||||||
"number"=> "Nomor",
|
"number"=> "Nomor",
|
||||||
"date"=> "Tanggal",
|
"date"=> "Tanggal",
|
||||||
|
"site_bcr"=> "BCR",
|
||||||
|
"building_function"=> "Fungsi Bangunan",
|
||||||
|
"business_type_info"=> "Jenis Usaha",
|
||||||
|
"is_terbit"=> "Status Terbit",
|
||||||
|
"calculated_retribution"=> "Retribusi",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,9 +101,15 @@ class SpatialPlanningController extends Controller
|
|||||||
"kbli"=> "text",
|
"kbli"=> "text",
|
||||||
"activities"=> "text",
|
"activities"=> "text",
|
||||||
"area"=> "text",
|
"area"=> "text",
|
||||||
|
"land_area"=> "text",
|
||||||
"location"=> "text",
|
"location"=> "text",
|
||||||
"number"=> "text",
|
"number"=> "text",
|
||||||
"date"=> "date",
|
"date"=> "date",
|
||||||
|
"site_bcr"=> "text",
|
||||||
|
"building_function"=> "text",
|
||||||
|
"business_type_info"=> "readonly",
|
||||||
|
"is_terbit"=> "select",
|
||||||
|
"calculated_retribution"=> "readonly",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ use App\Http\Resources\TaskAssignmentsResource;
|
|||||||
use App\Models\PbgTask;
|
use App\Models\PbgTask;
|
||||||
use App\Models\TaskAssignment;
|
use App\Models\TaskAssignment;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class QuickSearchController extends Controller
|
class QuickSearchController extends Controller
|
||||||
{
|
{
|
||||||
@@ -15,6 +17,10 @@ class QuickSearchController extends Controller
|
|||||||
return view("quick-search.index");
|
return view("quick-search.index");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function public_search(){
|
||||||
|
return view("public-search.index");
|
||||||
|
}
|
||||||
|
|
||||||
public function search_result(Request $request){
|
public function search_result(Request $request){
|
||||||
$keyword = $request->get("keyword");
|
$keyword = $request->get("keyword");
|
||||||
|
|
||||||
@@ -24,21 +30,105 @@ class QuickSearchController extends Controller
|
|||||||
public function quick_search_datatable(Request $request)
|
public function quick_search_datatable(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$query = PbgTask::orderBy('id', 'desc');
|
// 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')) {
|
if ($request->filled('search')) {
|
||||||
$search = $request->get('search');
|
$search = trim($request->get('search'));
|
||||||
$query->where(function ($q) use ($search) {
|
$query->where(function ($q) use ($search) {
|
||||||
$q->where('name', 'LIKE', "%$search%")
|
$q->where('pbg_task.registration_number', 'LIKE', "%$search%")
|
||||||
->orWhere('registration_number', 'LIKE', "%$search%")
|
->orWhere('pbg_task.name', 'LIKE', "%$search%")
|
||||||
->orWhere('address', 'LIKE', "%$search%")
|
->orWhere('pbg_task.owner_name', 'LIKE', "%$search%")
|
||||||
->orWhere('document_number', '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());
|
return response()->json($query->paginate());
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
\Log::error("Error fetching datatable data: " . $e->getMessage());
|
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([
|
return response()->json([
|
||||||
'message' => 'Terjadi kesalahan saat mengambil data.',
|
'message' => 'Terjadi kesalahan saat mengambil data.',
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
@@ -52,7 +142,8 @@ class QuickSearchController extends Controller
|
|||||||
$data = PbgTask::with([
|
$data = PbgTask::with([
|
||||||
'pbg_task_retributions',
|
'pbg_task_retributions',
|
||||||
'pbg_task_index_integrations',
|
'pbg_task_index_integrations',
|
||||||
'pbg_task_retributions.pbg_task_prasarana'
|
'pbg_task_retributions.pbg_task_prasarana',
|
||||||
|
'pbg_status'
|
||||||
])->findOrFail($id);
|
])->findOrFail($id);
|
||||||
|
|
||||||
$statusOptions = PbgTaskStatus::getStatuses();
|
$statusOptions = PbgTaskStatus::getStatuses();
|
||||||
@@ -60,10 +151,10 @@ class QuickSearchController extends Controller
|
|||||||
|
|
||||||
return view("quick-search.detail", compact("data", 'statusOptions', 'applicationTypes'));
|
return view("quick-search.detail", compact("data", 'statusOptions', 'applicationTypes'));
|
||||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||||
\Log::warning("PbgTask with ID {$id} not found.");
|
Log::warning("PbgTask with ID {$id} not found.");
|
||||||
return redirect()->route('quick-search.index')->with('error', 'Data tidak ditemukan.');
|
return redirect()->route('quick-search.index')->with('error', 'Data tidak ditemukan.');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
\Log::error("Error in QuickSearchController@show: " . $e->getMessage());
|
Log::error("Error in QuickSearchController@show: " . $e->getMessage());
|
||||||
return response()->view('pages.404', [], 500); // Optional: create `resources/views/errors/500.blade.php`
|
return response()->view('pages.404', [], 500); // Optional: create `resources/views/errors/500.blade.php`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,10 +56,24 @@ class PbgTaskController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function show(string $id)
|
public function show(string $id)
|
||||||
{
|
{
|
||||||
$data = PbgTask::with(['pbg_task_retributions','pbg_task_index_integrations', 'pbg_task_retributions.pbg_task_prasarana'])->findOrFail($id);
|
$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();
|
$statusOptions = PbgTaskStatus::getStatuses();
|
||||||
$applicationTypes = PbgTaskApplicationTypes::labels();
|
$applicationTypes = PbgTaskApplicationTypes::labels();
|
||||||
return view("pbg_task.show", compact("data", 'statusOptions', 'applicationTypes'));
|
|
||||||
|
return view("pbg_task.show", compact("data", 'statusOptions', 'applicationTypes', 'dataListsByType'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,17 +3,12 @@
|
|||||||
namespace App\Http\Controllers\Settings;
|
namespace App\Http\Controllers\Settings;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Services\ServiceSIMBG;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
class SyncronizeController extends Controller
|
class SyncronizeController extends Controller
|
||||||
{
|
{
|
||||||
protected $service_simbg;
|
|
||||||
public function __construct(ServiceSIMBG $service_simbg){
|
|
||||||
$this->service_simbg = $service_simbg;
|
|
||||||
}
|
|
||||||
public function index(Request $request){
|
public function index(Request $request){
|
||||||
$menuId = $request->query('menu_id');
|
$menuId = $request->query('menu_id');
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
@@ -37,36 +32,4 @@ class SyncronizeController extends Controller
|
|||||||
|
|
||||||
return view('settings.syncronize.index', compact('creator', 'updater', 'destroyer'));
|
return view('settings.syncronize.index', compact('creator', 'updater', 'destroyer'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function syncPbgTask(){
|
|
||||||
$res = $this->service_simbg->syncTaskPBG();
|
|
||||||
return $res;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function syncronizeTask(Request $request){
|
|
||||||
$res = $this->service_simbg->syncTaskPBG();
|
|
||||||
return redirect()->back()->with('success', 'Processing completed successfully');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getUserToken(){
|
|
||||||
$res = $this->service_simbg->getToken();
|
|
||||||
return $res;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function syncIndexIntegration(Request $request, $uuid){
|
|
||||||
$token = $request->get('token');
|
|
||||||
$res = $this->service_simbg->syncIndexIntegration($uuid);
|
|
||||||
return $res;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function syncTaskDetailSubmit(Request $request, $uuid){
|
|
||||||
$token = $request->get('token');
|
|
||||||
$res = $this->service_simbg->syncTaskDetailSubmit($uuid, $token);
|
|
||||||
return $res;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function syncTaskAssignments($uuid){
|
|
||||||
$res = $this->service_simbg->syncTaskAssignments($uuid);
|
|
||||||
return $res;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
78
app/Http/Controllers/TaxationController.php
Normal file
78
app/Http/Controllers/TaxationController.php
Normal 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)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
167
app/Http/Middleware/ValidateApiTokenForWeb.php
Normal file
167
app/Http/Middleware/ValidateApiTokenForWeb.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,13 +22,14 @@ class SpatialPlanningRequest extends FormRequest
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name' => 'string',
|
'name' => 'nullable|string',
|
||||||
'kbli' => 'string',
|
'kbli' => 'nullable|string',
|
||||||
'activities' => 'string',
|
'activities' => 'nullable|string',
|
||||||
'area' => 'string',
|
'area' => 'nullable|string',
|
||||||
'location' => 'string',
|
'location' => 'nullable|string',
|
||||||
'number' => 'string',
|
'number' => 'nullable|string',
|
||||||
'date' => 'date_format:Y-m-d',
|
'date' => 'nullable|date_format:Y-m-d',
|
||||||
|
'is_terbit' => 'nullable|boolean',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
38
app/Http/Requests/TaxationsRequest.php
Normal file
38
app/Http/Requests/TaxationsRequest.php
Normal 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'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,11 @@ class MenuResource extends JsonResource
|
|||||||
'url' => $this->url,
|
'url' => $this->url,
|
||||||
'sort_order' => $this->sort_order,
|
'sort_order' => $this->sort_order,
|
||||||
'parent' => $this->parent ? new MenuResource($this->parent) : null,
|
'parent' => $this->parent ? new MenuResource($this->parent) : null,
|
||||||
|
'children' => $this->when($this->relationLoaded('children'), function () {
|
||||||
|
return $this->children->sortBy('sort_order')->map(function ($child) {
|
||||||
|
return new MenuResource($child);
|
||||||
|
});
|
||||||
|
}),
|
||||||
'created_at' => $this->created_at,
|
'created_at' => $this->created_at,
|
||||||
'updated_at' => $this->updated_at
|
'updated_at' => $this->updated_at
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ class RequestAssignmentResouce extends JsonResource
|
|||||||
->where('pbg_type', 'bukti_bayar')
|
->where('pbg_type', 'bukti_bayar')
|
||||||
->sortByDesc('created_at')
|
->sortByDesc('created_at')
|
||||||
->first(),
|
->first(),
|
||||||
|
'pbg_task_retributions' => $this->pbg_task_retributions,
|
||||||
|
'pbg_task_detail' => $this->pbg_task_detail,
|
||||||
|
'pbg_status' => $this->pbg_status,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace App\Http\Resources;
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
class SpatialPlanningsResource extends JsonResource
|
class TaxationsResource extends JsonResource
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Transform the resource into an array.
|
* Transform the resource into an array.
|
||||||
78
app/Imports/TaxationsImport.php
Normal file
78
app/Imports/TaxationsImport.php
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Imports;
|
||||||
|
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Maatwebsite\Excel\Concerns\ToCollection;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithBatchInserts;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithChunkReading;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use App\Models\Tax;
|
||||||
|
|
||||||
|
class TaxationsImport implements ToCollection, WithMultipleSheets, WithChunkReading, WithBatchInserts, ShouldQueue, WithHeadingRow
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param Collection $collection
|
||||||
|
*/
|
||||||
|
public function collection(Collection $collection)
|
||||||
|
{
|
||||||
|
$batchData = [];
|
||||||
|
$batchSize = 1000;
|
||||||
|
|
||||||
|
foreach ($collection as $row) {
|
||||||
|
|
||||||
|
$masaPajak = trim($row['masa_pajak']) ?? '';
|
||||||
|
|
||||||
|
$masaParts = explode('-', $masaPajak);
|
||||||
|
|
||||||
|
$startValidity = null;
|
||||||
|
$endValidity = null;
|
||||||
|
|
||||||
|
if (count($masaParts) === 2) {
|
||||||
|
$startValidity = \Carbon\Carbon::createFromFormat('d/m/Y', trim($masaParts[0]))->format('Y-m-d');
|
||||||
|
$endValidity = \Carbon\Carbon::createFromFormat('d/m/Y', trim($masaParts[1]))->format('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
$batchData[] = [
|
||||||
|
'tax_code' => trim($row['kode']) ?? '',
|
||||||
|
'tax_no' => trim($row['no']) ?? '',
|
||||||
|
'npwpd' => trim($row['npwpd']) ?? '',
|
||||||
|
'wp_name' => trim($row['nama_wp']) ?? '',
|
||||||
|
'business_name' => trim($row['nama_usaha']) ?? '',
|
||||||
|
'address' => trim($row['alamat_usaha']) ?? '',
|
||||||
|
'start_validity' => $startValidity,
|
||||||
|
'end_validity' => $endValidity,
|
||||||
|
'tax_value' => (float) str_replace(',', '', trim($row['nilai_pajak']) ?? '0'),
|
||||||
|
'subdistrict' => trim($row['kecamatan']) ?? '',
|
||||||
|
'village' => trim($row['desa']) ?? '',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (count($batchData) >= $batchSize) {
|
||||||
|
Tax::upsert($batchData, ['tax_no'], ['tax_code', 'tax_no', 'npwpd', 'wp_name', 'business_name', 'address', 'start_validity', 'end_validity', 'tax_value', 'subdistrict', 'village']);
|
||||||
|
$batchData = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($batchData)) {
|
||||||
|
Tax::upsert($batchData, ['tax_no'], ['tax_code', 'tax_no', 'npwpd', 'wp_name', 'business_name', 'address', 'start_validity', 'end_validity', 'tax_value', 'subdistrict', 'village']);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
public function sheets(): array {
|
||||||
|
return [
|
||||||
|
0 => $this
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function chunkSize(): int
|
||||||
|
{
|
||||||
|
return 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function batchSize(): int
|
||||||
|
{
|
||||||
|
return 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,10 +8,11 @@ use App\Models\ImportDatasource;
|
|||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Queue\Queueable;
|
use Illuminate\Foundation\Queue\Queueable;
|
||||||
use App\Services\ServiceTabPbgTask;
|
use App\Services\ServiceTabPbgTask;
|
||||||
use App\Services\ServiceGoogleSheet;
|
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class RetrySyncronizeJob implements ShouldQueue
|
class RetrySyncronizeJob implements ShouldQueue
|
||||||
{
|
{
|
||||||
use Queueable, Dispatchable, InteractsWithQueue, SerializesModels;
|
use Queueable, Dispatchable, InteractsWithQueue, SerializesModels;
|
||||||
@@ -28,7 +29,6 @@ class RetrySyncronizeJob implements ShouldQueue
|
|||||||
{
|
{
|
||||||
try{
|
try{
|
||||||
$service_tab_pbg_task = app(ServiceTabPbgTask::class);
|
$service_tab_pbg_task = app(ServiceTabPbgTask::class);
|
||||||
$service_google_sheet = app(ServiceGoogleSheet::class);
|
|
||||||
|
|
||||||
$failed_import = ImportDatasource::find($this->import_datasource_id);
|
$failed_import = ImportDatasource::find($this->import_datasource_id);
|
||||||
|
|
||||||
@@ -46,10 +46,7 @@ class RetrySyncronizeJob implements ShouldQueue
|
|||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
|
|
||||||
$data_setting_result = $service_google_sheet->get_big_resume_data();
|
BigdataResume::generateResumeData($failed_import->id, date('Y'), "simbg");
|
||||||
|
|
||||||
BigdataResume::generateResumeData($failed_import->id, "all", $data_setting_result);
|
|
||||||
BigdataResume::generateResumeData($failed_import->id, now()->year, $data_setting_result);
|
|
||||||
|
|
||||||
$failed_import->update([
|
$failed_import->update([
|
||||||
'status' => ImportDatasourceStatus::Success->value,
|
'status' => ImportDatasourceStatus::Success->value,
|
||||||
@@ -58,7 +55,7 @@ class RetrySyncronizeJob implements ShouldQueue
|
|||||||
'failed_uuid' => null
|
'failed_uuid' => null
|
||||||
]);
|
]);
|
||||||
}catch(\Exception $e){
|
}catch(\Exception $e){
|
||||||
\Log::error("RetrySyncronizeJob Failed: ". $e->getMessage(), [
|
Log::error("RetrySyncronizeJob Failed: ". $e->getMessage(), [
|
||||||
'exception' => $e,
|
'exception' => $e,
|
||||||
]);
|
]);
|
||||||
if(isset($failed_import)){
|
if(isset($failed_import)){
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Jobs;
|
|||||||
|
|
||||||
use App\Models\BigdataResume;
|
use App\Models\BigdataResume;
|
||||||
use App\Models\ImportDatasource;
|
use App\Models\ImportDatasource;
|
||||||
|
use App\Models\PbgTask;
|
||||||
use App\Services\ServiceGoogleSheet;
|
use App\Services\ServiceGoogleSheet;
|
||||||
use App\Services\ServicePbgTask;
|
use App\Services\ServicePbgTask;
|
||||||
use App\Services\ServiceTabPbgTask;
|
use App\Services\ServiceTabPbgTask;
|
||||||
@@ -21,72 +22,189 @@ class ScrapingDataJob implements ShouldQueue
|
|||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inject dependencies instead of creating them inside.
|
* Create a new job instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct()
|
||||||
) {
|
{
|
||||||
|
// Use dedicated scraping queue
|
||||||
|
$this->queue = 'scraping';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the job.
|
* Execute the job with optimized schema:
|
||||||
|
* 1. Scrape Google Sheet first
|
||||||
|
* 2. Scrape PBG Task to get parent data
|
||||||
|
* 3. Loop through parent tasks to scrape details via ServiceTabPbgTask
|
||||||
*/
|
*/
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
try {
|
$import_datasource = null;
|
||||||
|
$failed_uuid = null;
|
||||||
|
$processedTasks = 0;
|
||||||
|
$totalTasks = 0;
|
||||||
|
|
||||||
$client = app(Client::class);
|
try {
|
||||||
|
Log::info("=== SCRAPING DATA JOB STARTED ===");
|
||||||
|
|
||||||
|
// Initialize services
|
||||||
|
$service_google_sheet = app(ServiceGoogleSheet::class);
|
||||||
$service_pbg_task = app(ServicePbgTask::class);
|
$service_pbg_task = app(ServicePbgTask::class);
|
||||||
$service_tab_pbg_task = app(ServiceTabPbgTask::class);
|
$service_tab_pbg_task = app(ServiceTabPbgTask::class);
|
||||||
$service_google_sheet = app(ServiceGoogleSheet::class);
|
|
||||||
$service_token = app(ServiceTokenSIMBG::class);
|
// Create ImportDatasource record
|
||||||
// Create a record with "processing" status
|
|
||||||
$import_datasource = ImportDatasource::create([
|
$import_datasource = ImportDatasource::create([
|
||||||
'message' => 'Initiating scraping...',
|
'message' => 'Starting optimized scraping process...',
|
||||||
'response_body' => null,
|
'response_body' => null,
|
||||||
'status' => 'processing',
|
'status' => 'processing',
|
||||||
'start_time' => now(),
|
'start_time' => now(),
|
||||||
'failed_uuid' => null
|
'failed_uuid' => null
|
||||||
]);
|
]);
|
||||||
|
$import_datasource->update(['message' => 'Scraping PBG Task parent data...']);
|
||||||
|
|
||||||
$failed_uuid = null;
|
|
||||||
|
|
||||||
// Run the scraping services
|
|
||||||
$service_google_sheet->run_service();
|
|
||||||
$service_pbg_task->run_service();
|
$service_pbg_task->run_service();
|
||||||
try{
|
|
||||||
$service_tab_pbg_task->run_service();
|
// STEP 3: Get all PBG tasks for detail scraping
|
||||||
}catch(\Exception $e){
|
$totalTasks = PbgTask::count();
|
||||||
$failed_uuid = $service_tab_pbg_task->getFailedUUID();
|
|
||||||
throw $e;
|
$import_datasource->update([
|
||||||
|
'message' => "Scraping details for {$totalTasks} PBG tasks..."
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Process tasks in chunks for memory efficiency
|
||||||
|
$chunkSize = 100;
|
||||||
|
$processedTasks = 0;
|
||||||
|
|
||||||
|
PbgTask::orderBy('id')->chunk($chunkSize, function ($pbg_tasks) use (
|
||||||
|
$service_tab_pbg_task,
|
||||||
|
&$processedTasks,
|
||||||
|
$totalTasks,
|
||||||
|
$import_datasource,
|
||||||
|
&$failed_uuid
|
||||||
|
) {
|
||||||
|
foreach ($pbg_tasks as $pbg_task) {
|
||||||
|
try {
|
||||||
|
// Scrape all details for this task
|
||||||
|
$this->processTaskDetails($service_tab_pbg_task, $pbg_task->uuid);
|
||||||
|
|
||||||
|
$processedTasks++;
|
||||||
|
|
||||||
|
// Update progress every 10 tasks
|
||||||
|
if ($processedTasks % 10 === 0) {
|
||||||
|
$progress = round(($processedTasks / $totalTasks) * 100, 2);
|
||||||
|
Log::info("Progress update", [
|
||||||
|
'processed' => $processedTasks,
|
||||||
|
'total' => $totalTasks,
|
||||||
|
'progress' => "{$progress}%"
|
||||||
|
]);
|
||||||
|
|
||||||
|
$import_datasource->update([
|
||||||
|
'message' => "Processing details: {$processedTasks}/{$totalTasks} ({$progress}%)"
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// $data_setting_result = $service_google_sheet->get_big_resume_data();
|
} catch (\Exception $e) {
|
||||||
|
Log::warning("Failed to process task details", [
|
||||||
|
'uuid' => $pbg_task->uuid,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
|
||||||
// BigdataResume::generateResumeData($import_datasource->id, "all", $data_setting_result);
|
// Store failed UUID but continue processing
|
||||||
// BigdataResume::generateResumeData($import_datasource->id, now()->year, $data_setting_result);
|
$failed_uuid = $pbg_task->uuid;
|
||||||
|
|
||||||
// Update status to success
|
// Only stop if it's a critical error
|
||||||
|
if ($this->isCriticalError($e)) {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$import_datasource->update(['message' => 'Scraping Google Sheet data...']);
|
||||||
|
|
||||||
|
$service_google_sheet->run_service();
|
||||||
|
|
||||||
|
$import_datasource->update(['message' => 'Generating BigData resume...']);
|
||||||
|
|
||||||
|
BigdataResume::generateResumeData($import_datasource->id, date('Y'), "simbg");
|
||||||
|
|
||||||
|
Log::info("BigData resume generated successfully");
|
||||||
|
|
||||||
|
// Update final status
|
||||||
$import_datasource->update([
|
$import_datasource->update([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'message' => 'Scraping completed successfully.',
|
'message' => "Scraping completed successfully. Processed {$processedTasks}/{$totalTasks} tasks.",
|
||||||
'finish_time' => now()
|
'finish_time' => now(),
|
||||||
|
'failed_uuid' => $failed_uuid // Store last failed UUID if any
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::info("=== SCRAPING DATA JOB COMPLETED SUCCESSFULLY ===", [
|
||||||
|
'import_datasource_id' => $import_datasource->id,
|
||||||
|
'processed_tasks' => $processedTasks,
|
||||||
|
'total_tasks' => $totalTasks,
|
||||||
|
'has_failures' => !is_null($failed_uuid)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error('Scraping failed: ' . $e->getMessage(), ['trace' => $e->getTraceAsString()]);
|
Log::error('=== SCRAPING DATA JOB FAILED ===', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'file' => $e->getFile(),
|
||||||
|
'line' => $e->getLine(),
|
||||||
|
'processed_tasks' => $processedTasks,
|
||||||
|
'total_tasks' => $totalTasks,
|
||||||
|
'failed_uuid' => $failed_uuid,
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
// Update status to failed
|
// Update ImportDatasource with failure info
|
||||||
if (isset($import_datasource)) {
|
if ($import_datasource) {
|
||||||
$import_datasource->update([
|
$import_datasource->update([
|
||||||
'status' => 'failed',
|
'status' => 'failed',
|
||||||
'response_body' => 'Terjadi kesalahan, Syncronize tidak selesai',
|
'message' => "Scraping failed: {$e->getMessage()}. Processed {$processedTasks}/{$totalTasks} tasks.",
|
||||||
|
'response_body' => 'Scraping process interrupted due to error',
|
||||||
'finish_time' => now(),
|
'finish_time' => now(),
|
||||||
'failed_uuid' => $failed_uuid,
|
'failed_uuid' => $failed_uuid,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark the job as failed
|
// Don't retry this job
|
||||||
$this->fail($e);
|
$this->fail($e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process all detail endpoints for a single PBG task
|
||||||
|
*/
|
||||||
|
private function processTaskDetails(ServiceTabPbgTask $service, string $uuid): void
|
||||||
|
{
|
||||||
|
// Call all detail scraping methods for this task
|
||||||
|
$service->scraping_task_details($uuid);
|
||||||
|
$service->scraping_pbg_data_list($uuid);
|
||||||
|
$service->scraping_task_retributions($uuid);
|
||||||
|
$service->scraping_task_integrations($uuid);
|
||||||
|
$service->scraping_task_detail_status($uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if an error is critical enough to stop the entire process
|
||||||
|
*/
|
||||||
|
private function isCriticalError(\Exception $e): bool
|
||||||
|
{
|
||||||
|
$criticalMessages = [
|
||||||
|
'authentication failed',
|
||||||
|
'token expired',
|
||||||
|
'database connection',
|
||||||
|
'memory exhausted',
|
||||||
|
'maximum execution time'
|
||||||
|
];
|
||||||
|
|
||||||
|
$errorMessage = strtolower($e->getMessage());
|
||||||
|
|
||||||
|
foreach ($criticalMessages as $critical) {
|
||||||
|
if (strpos($errorMessage, $critical) !== false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Jobs;
|
|
||||||
|
|
||||||
use App\Models\ImportDatasource;
|
|
||||||
use App\Services\GoogleSheetService;
|
|
||||||
use App\Services\ServiceGoogleSheet;
|
|
||||||
use App\Services\ServicePbgTask;
|
|
||||||
use App\Services\ServiceTabPbgTask;
|
|
||||||
use GuzzleHttp\Client;
|
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class SyncronizeSIMBG implements ShouldQueue
|
|
||||||
{
|
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
||||||
|
|
||||||
public $tries = 1;
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
// Avoid injecting non-serializable dependencies here
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handle(): void
|
|
||||||
{
|
|
||||||
$import_datasource = ImportDatasource::where('status', 'processing')->first();
|
|
||||||
|
|
||||||
if (!$import_datasource) {
|
|
||||||
$import_datasource = ImportDatasource::create([
|
|
||||||
'message' => 'Initiating scraping...',
|
|
||||||
'response_body' => null,
|
|
||||||
'status' => 'processing'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// Create an instance of GuzzleHttp\Client inside handle()
|
|
||||||
$client = new Client();
|
|
||||||
|
|
||||||
// Create instances of services inside handle()
|
|
||||||
$service_pbg_task = app(ServicePbgTask::class);
|
|
||||||
$service_tab_pbg_task = app(ServiceTabPbgTask::class);
|
|
||||||
|
|
||||||
// Create a record with "processing" status
|
|
||||||
|
|
||||||
|
|
||||||
// Run the service
|
|
||||||
$service_google_sheet = new ServiceGoogleSheet();
|
|
||||||
\Log::info('Starting Google Sheet service');
|
|
||||||
$service_google_sheet->run_service();
|
|
||||||
\Log::info('Google Sheet service completed');
|
|
||||||
|
|
||||||
\Log::info('Starting PBG Task service');
|
|
||||||
$service_pbg_task->run_service();
|
|
||||||
\Log::info('PBG Task service completed');
|
|
||||||
|
|
||||||
\Log::info('Starting Tab PBG Task service');
|
|
||||||
$service_tab_pbg_task->run_service();
|
|
||||||
\Log::info('Tab PBG Task service completed');
|
|
||||||
|
|
||||||
// Update the record status to "success" after completion
|
|
||||||
$import_datasource->update([
|
|
||||||
'status' => 'success',
|
|
||||||
'message' => 'Scraping completed successfully.'
|
|
||||||
]);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
\Log::error("SyncronizeSIMBG Job Failed: " . $e->getMessage(), [
|
|
||||||
'exception' => $e,
|
|
||||||
]);
|
|
||||||
$import_datasource->update([
|
|
||||||
'status' => 'failed',
|
|
||||||
'message' => 'Failed job'
|
|
||||||
]);
|
|
||||||
$this->fail($e); // Mark the job as failed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,8 @@ namespace App\Models;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Enums\PbgTaskStatus;
|
||||||
|
use App\Services\ServiceGoogleSheet;
|
||||||
|
|
||||||
class BigdataResume extends Model
|
class BigdataResume extends Model
|
||||||
{
|
{
|
||||||
@@ -30,6 +32,12 @@ class BigdataResume extends Model
|
|||||||
'issuance_realization_pbg_sum',
|
'issuance_realization_pbg_sum',
|
||||||
'process_in_technical_office_count',
|
'process_in_technical_office_count',
|
||||||
'process_in_technical_office_sum',
|
'process_in_technical_office_sum',
|
||||||
|
'business_rab_count',
|
||||||
|
'business_krk_count',
|
||||||
|
'business_dlh_count',
|
||||||
|
'non_business_rab_count',
|
||||||
|
'non_business_krk_count',
|
||||||
|
'resume_type',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function importDatasource()
|
public function importDatasource()
|
||||||
@@ -37,101 +45,336 @@ class BigdataResume extends Model
|
|||||||
return $this->belongsTo(ImportDatasource::class, 'import_datasource_id');
|
return $this->belongsTo(ImportDatasource::class, 'import_datasource_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function generateResumeData($import_datasource_id, $year, $data_setting){
|
public static function generateResumeData($import_datasource_id, $year, $resume_type){
|
||||||
$stats = PbgTask::with(['googleSheet', 'pbg_task_retributions'])
|
// Get accurate counts without joins to avoid duplicates from multiple retributions
|
||||||
->leftJoin('pbg_task_retributions as ptr', 'pbg_task.uuid', '=', 'ptr.pbg_task_uid')
|
// Filter only valid data (is_valid = true)
|
||||||
->leftJoin('pbg_task_google_sheet as ptgs', 'pbg_task.registration_number', '=', 'ptgs.no_registrasi')
|
$verified_count = PbgTask::whereIn('status', PbgTaskStatus::getVerified())
|
||||||
->when($year !== 'all', function ($query) use ($year) {
|
->where('is_valid', true)
|
||||||
$query->whereYear('pbg_task.task_created_at', (int) $year);
|
->where('pbg_task.due_date', '>=', $year.'-02-01')
|
||||||
|
->count();
|
||||||
|
$non_verified_count = PbgTask::whereIn('status', PbgTaskStatus::getNonVerified())
|
||||||
|
->where('is_valid', true)
|
||||||
|
->where('pbg_task.due_date', '>=', $year.'-02-01')
|
||||||
|
->count();
|
||||||
|
$waiting_click_dpmptsp_count = PbgTask::whereIn('status', PbgTaskStatus::getWaitingClickDpmptsp())
|
||||||
|
->where('is_valid', true)
|
||||||
|
->where('pbg_task.due_date', '>=', $year.'-02-01')
|
||||||
|
->count();
|
||||||
|
$issuance_realization_pbg_count = PbgTask::whereIn('status', PbgTaskStatus::getIssuanceRealizationPbg())
|
||||||
|
->where('is_valid', true)
|
||||||
|
->where('pbg_task.due_date', '>=', $year.'-02-01')
|
||||||
|
->count();
|
||||||
|
$process_in_technical_office_count = PbgTask::whereIn('status', PbgTaskStatus::getProcessInTechnicalOffice())
|
||||||
|
->where('is_valid', true)
|
||||||
|
->where('pbg_task.due_date', '>=', $year.'-02-01')
|
||||||
|
->count();
|
||||||
|
$potention_count = PbgTask::whereIn('status', PbgTaskStatus::getPotention())
|
||||||
|
->where('is_valid', true)
|
||||||
|
->where('pbg_task.due_date', '>=', $year.'-02-01')
|
||||||
|
->count();
|
||||||
|
|
||||||
|
// Business count: function_type LIKE usaha OR (non-business with unit > 1)
|
||||||
|
$business_count = 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_detail', function ($q4) {
|
||||||
|
$q4->where('unit', '>', 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->whereIn("status", PbgTaskStatus::getNonVerified());
|
||||||
|
})
|
||||||
|
->where('is_valid', true)
|
||||||
|
->where('due_date', '>=', $year.'-02-01')
|
||||||
|
->count();
|
||||||
|
|
||||||
|
// Non-business count: function_type NOT LIKE usaha AND (unit IS NULL OR unit <= 1)
|
||||||
|
$non_business_count = 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_detail', function ($q4) {
|
||||||
|
$q4->where('unit', '>', 1);
|
||||||
|
})
|
||||||
|
->orWhereDoesntHave('pbg_task_detail');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->where('is_valid', true)
|
||||||
|
->where('due_date', '>=', $year.'-02-01')
|
||||||
|
->count();
|
||||||
|
|
||||||
|
// Business RAB count - for each business task with data_type=3:
|
||||||
|
// if any status != 1 then return 1, if all status = 1 then return 0, then sum all
|
||||||
|
$business_rab_count = DB::table('pbg_task')
|
||||||
|
->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)
|
||||||
|
->where('due_date', '>=', $year.'-02-01')
|
||||||
|
->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);
|
||||||
|
})
|
||||||
|
->selectRaw('
|
||||||
|
SUM(
|
||||||
|
CASE
|
||||||
|
WHEN EXISTS (
|
||||||
|
SELECT 1 FROM pbg_task_detail_data_lists ptddl
|
||||||
|
WHERE ptddl.pbg_task_uuid = pbg_task.uuid
|
||||||
|
AND ptddl.data_type = 3
|
||||||
|
AND ptddl.status != 1
|
||||||
|
) THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
) as total_count
|
||||||
|
')
|
||||||
|
->value('total_count') ?? 0;
|
||||||
|
|
||||||
|
// Business KRK count - for each business task with data_type=2:
|
||||||
|
// if any status != 1 then return 1, if all status = 1 then return 0, then sum all
|
||||||
|
$business_krk_count = DB::table('pbg_task')
|
||||||
|
->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)
|
||||||
|
->where('due_date', '>=', $year.'-02-01')
|
||||||
|
->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);
|
||||||
|
})
|
||||||
|
->selectRaw('
|
||||||
|
SUM(
|
||||||
|
CASE
|
||||||
|
WHEN EXISTS (
|
||||||
|
SELECT 1 FROM pbg_task_detail_data_lists ptddl
|
||||||
|
WHERE ptddl.pbg_task_uuid = pbg_task.uuid
|
||||||
|
AND ptddl.data_type = 2
|
||||||
|
AND ptddl.status != 1
|
||||||
|
) THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
) as total_count
|
||||||
|
')
|
||||||
|
->value('total_count') ?? 0;
|
||||||
|
|
||||||
|
// Business DLH count - for each business task with data_type=5:
|
||||||
|
// if any status != 1 then return 1, if all status = 1 then return 0, then sum all
|
||||||
|
$business_dlh_count = DB::table('pbg_task')
|
||||||
|
->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)
|
||||||
|
->where('due_date', '>=', $year.'-02-01')
|
||||||
|
->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);
|
||||||
|
})
|
||||||
|
->selectRaw('
|
||||||
|
SUM(
|
||||||
|
CASE
|
||||||
|
WHEN EXISTS (
|
||||||
|
SELECT 1 FROM pbg_task_detail_data_lists ptddl
|
||||||
|
WHERE ptddl.pbg_task_uuid = pbg_task.uuid
|
||||||
|
AND ptddl.data_type = 5
|
||||||
|
AND ptddl.status != 1
|
||||||
|
) THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
) as total_count
|
||||||
|
')
|
||||||
|
->value('total_count') ?? 0;
|
||||||
|
|
||||||
|
// Non-Business RAB count - for each non-business task with data_type=3:
|
||||||
|
// if any status != 1 then return 1, if all status = 1 then return 0, then sum all
|
||||||
|
$non_business_rab_count = DB::table('pbg_task')
|
||||||
|
->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)
|
||||||
|
->where('due_date', '>=', $year.'-02-01')
|
||||||
|
->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);
|
||||||
|
})
|
||||||
|
->selectRaw('
|
||||||
|
SUM(
|
||||||
|
CASE
|
||||||
|
WHEN EXISTS (
|
||||||
|
SELECT 1 FROM pbg_task_detail_data_lists ptddl
|
||||||
|
WHERE ptddl.pbg_task_uuid = pbg_task.uuid
|
||||||
|
AND ptddl.data_type = 3
|
||||||
|
AND ptddl.status != 1
|
||||||
|
) THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
) as total_count
|
||||||
|
')
|
||||||
|
->value('total_count') ?? 0;
|
||||||
|
|
||||||
|
// Non-Business KRK count - for each non-business task with data_type=2:
|
||||||
|
// if any status != 1 then return 1, if all status = 1 then return 0, then sum all
|
||||||
|
$non_business_krk_count = DB::table('pbg_task')
|
||||||
|
->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)
|
||||||
|
->where('due_date', '>=', $year.'-02-01')
|
||||||
|
->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);
|
||||||
|
})
|
||||||
|
->selectRaw('
|
||||||
|
SUM(
|
||||||
|
CASE
|
||||||
|
WHEN EXISTS (
|
||||||
|
SELECT 1 FROM pbg_task_detail_data_lists ptddl
|
||||||
|
WHERE ptddl.pbg_task_uuid = pbg_task.uuid
|
||||||
|
AND ptddl.data_type = 2
|
||||||
|
AND ptddl.status != 1
|
||||||
|
) THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
) as total_count
|
||||||
|
')
|
||||||
|
->value('total_count') ?? 0;
|
||||||
|
|
||||||
|
// Debug: Check if there are non-verified tasks and their retribution data
|
||||||
|
$debug_non_verified = PbgTask::whereIn('status', PbgTaskStatus::getNonVerified())
|
||||||
|
->where('is_valid', true)
|
||||||
|
->where('due_date', '>=', $year.'-02-01')
|
||||||
|
->with('pbg_task_retributions')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
\Log::info('Non-verified tasks debug', [
|
||||||
|
'year' => $year,
|
||||||
|
'non_verified_statuses' => PbgTaskStatus::getNonVerified(),
|
||||||
|
'tasks_count' => $debug_non_verified->count(),
|
||||||
|
'tasks_with_retribution' => $debug_non_verified->filter(fn($task) => $task->pbg_task_retributions)->count(),
|
||||||
|
'sample_retribution_values' => $debug_non_verified->take(3)->map(fn($task) => [
|
||||||
|
'uuid' => $task->uuid,
|
||||||
|
'status' => $task->status,
|
||||||
|
'has_retribution' => !is_null($task->pbg_task_retributions),
|
||||||
|
'retribution_value' => $task->pbg_task_retributions?->nilai_retribusi_bangunan ?? 'NULL'
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Calculate totals using count-based formula
|
||||||
|
// Business: $business_count * 200 * 44300
|
||||||
|
// Non-Business: $non_business_count * 72 * 16000
|
||||||
|
$business_total = $business_count * 200 * 44300;
|
||||||
|
$non_business_total = $non_business_count * 72 * 16000;
|
||||||
|
$non_verified_total = $business_total + $non_business_total;
|
||||||
|
|
||||||
|
// Get other sum values using proper aggregation to handle multiple retributions
|
||||||
|
$stats = PbgTask::leftJoin('pbg_task_retributions as ptr', 'pbg_task.uuid', '=', 'ptr.pbg_task_uid')
|
||||||
|
->where('pbg_task.is_valid', true)
|
||||||
|
->where('pbg_task.due_date', '>=', $year.'-02-01')
|
||||||
->selectRaw("
|
->selectRaw("
|
||||||
COUNT(CASE WHEN LOWER(TRIM(ptgs.status_verifikasi)) = 'selesai verifikasi' THEN 1 END) AS verified_count,
|
SUM(CASE WHEN pbg_task.status in (".implode(',', PbgTaskStatus::getVerified()).") THEN COALESCE(ptr.nilai_retribusi_bangunan, 0) ELSE 0 END) AS verified_total,
|
||||||
SUM(CASE WHEN LOWER(TRIM(ptgs.status_verifikasi)) = 'selesai verifikasi' THEN ptr.nilai_retribusi_bangunan ELSE 0 END) AS verified_total,
|
SUM(CASE WHEN pbg_task.status in (".implode(',', PbgTaskStatus::getWaitingClickDpmptsp()).") THEN COALESCE(ptr.nilai_retribusi_bangunan, 0) ELSE 0 END) AS waiting_click_dpmptsp_total,
|
||||||
|
SUM(CASE WHEN pbg_task.status in (".implode(',', PbgTaskStatus::getIssuanceRealizationPbg()).") THEN COALESCE(ptr.nilai_retribusi_bangunan, 0) ELSE 0 END) AS issuance_realization_pbg_total,
|
||||||
COUNT(CASE WHEN LOWER(TRIM(ptgs.status_verifikasi)) != 'selesai verifikasi' OR ptgs.status_verifikasi IS NULL THEN 1 END) AS non_verified_count,
|
SUM(CASE WHEN pbg_task.status in (".implode(',', PbgTaskStatus::getProcessInTechnicalOffice()).") THEN COALESCE(ptr.nilai_retribusi_bangunan, 0) ELSE 0 END) AS process_in_technical_office_total,
|
||||||
SUM(CASE WHEN LOWER(TRIM(ptgs.status_verifikasi)) != 'selesai verifikasi' OR ptgs.status_verifikasi IS NULL THEN ptr.nilai_retribusi_bangunan ELSE 0 END) AS non_verified_total,
|
SUM(CASE WHEN pbg_task.status in (".implode(',', PbgTaskStatus::getPotention()).") THEN COALESCE(ptr.nilai_retribusi_bangunan, 0) ELSE 0 END) AS potention_total,
|
||||||
|
COUNT(CASE WHEN pbg_task.status in (".implode(',', PbgTaskStatus::getNonVerified()).") THEN 1 END) AS non_verified_tasks_count,
|
||||||
COUNT(CASE WHEN (LOWER(TRIM(ptgs.status_verifikasi)) != 'selesai verifikasi' OR ptgs.status_verifikasi IS NULL)
|
COUNT(CASE WHEN pbg_task.status in (".implode(',', PbgTaskStatus::getNonVerified()).") AND ptr.nilai_retribusi_bangunan IS NOT NULL THEN 1 END) AS non_verified_with_retribution_count
|
||||||
AND LOWER(TRIM(pbg_task.function_type)) = 'sebagai tempat usaha' THEN 1 END) AS business_count,
|
|
||||||
SUM(CASE WHEN (LOWER(TRIM(ptgs.status_verifikasi)) != 'selesai verifikasi' OR ptgs.status_verifikasi IS NULL)
|
|
||||||
AND LOWER(TRIM(pbg_task.function_type)) = 'sebagai tempat usaha' THEN ptr.nilai_retribusi_bangunan ELSE 0 END) AS business_total,
|
|
||||||
|
|
||||||
COUNT(CASE WHEN (LOWER(TRIM(ptgs.status_verifikasi)) != 'selesai verifikasi' OR ptgs.status_verifikasi IS NULL)
|
|
||||||
AND (LOWER(TRIM(pbg_task.function_type)) != 'sebagai tempat usaha' OR pbg_task.function_type IS NULL) THEN 1 END) AS non_business_count,
|
|
||||||
SUM(CASE WHEN (LOWER(TRIM(ptgs.status_verifikasi)) != 'selesai verifikasi' OR ptgs.status_verifikasi IS NULL)
|
|
||||||
AND (LOWER(TRIM(pbg_task.function_type)) != 'sebagai tempat usaha' OR pbg_task.function_type IS NULL) THEN ptr.nilai_retribusi_bangunan ELSE 0 END) AS non_business_total
|
|
||||||
")
|
")
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
// Assign Results
|
\Log::info('Stats calculation result', [
|
||||||
$verified_count = $stats->verified_count ?? 0;
|
'business_count' => $business_count,
|
||||||
$verified_total = $stats->verified_total ?? 0;
|
'non_business_count' => $non_business_count,
|
||||||
$non_verified_count = $stats->non_verified_count ?? 0;
|
'business_total' => $business_total,
|
||||||
$non_verified_total = $stats->non_verified_total ?? 0;
|
'non_business_total' => $non_business_total,
|
||||||
$business_count = $stats->business_count ?? 0;
|
'non_verified_total' => $non_verified_total,
|
||||||
$business_total = $stats->business_total ?? 0;
|
'non_verified_tasks_count' => $stats->non_verified_tasks_count ?? 'NULL',
|
||||||
$non_business_count = $stats->non_business_count ?? 0;
|
'non_verified_with_retribution_count' => $stats->non_verified_with_retribution_count ?? 'NULL'
|
||||||
$non_business_total = $stats->non_business_total ?? 0;
|
]);
|
||||||
|
|
||||||
$query_potention = once(function () use ($year) {
|
$service_google_sheet = app(ServiceGoogleSheet::class);
|
||||||
$query = PbgTask::leftJoin('pbg_task_retributions as ptr', 'pbg_task.uuid', '=', 'ptr.pbg_task_uid')
|
|
||||||
->selectRaw('COUNT(DISTINCT pbg_task.id) as task_count, SUM(ptr.nilai_retribusi_bangunan) as total_retribution');
|
|
||||||
|
|
||||||
if ($year !== 'all') {
|
|
||||||
$query->whereYear('pbg_task.task_created_at', (int) $year);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $query->first();
|
|
||||||
});
|
|
||||||
|
|
||||||
$potention_count = $query_potention->task_count ?? 0;
|
|
||||||
$potention_total = $query_potention->total_retribution ?? 0;
|
|
||||||
|
|
||||||
$query_spatial_plannings = once(function () use ($year) {
|
|
||||||
$query = PbgTask::leftJoin('spatial_plannings as sp', 'pbg_task.document_number', '=', 'sp.number')
|
|
||||||
->leftJoin('pbg_task_retributions as ptr', 'ptr.pbg_task_uid', '=', 'pbg_task.uuid')
|
|
||||||
->selectRaw('
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(DISTINCT sp.id) > 0 THEN COUNT(DISTINCT sp.id)
|
|
||||||
ELSE (SELECT COUNT(*) FROM spatial_plannings)
|
|
||||||
END as task_count,
|
|
||||||
SUM(CASE WHEN sp.id IS NOT NULL AND ptr.id IS NOT NULL THEN ptr.nilai_retribusi_bangunan ELSE 0 END) as total_retribution
|
|
||||||
');
|
|
||||||
|
|
||||||
if ($year !== 'all') {
|
|
||||||
$query->whereYear('pbg_task.task_created_at', (int) $year);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $query->first();
|
|
||||||
});
|
|
||||||
|
|
||||||
$spatial_planning_count = $query_spatial_plannings->task_count ?? 0;
|
|
||||||
$spatial_planning_total = $query_spatial_plannings->total_retribution;
|
|
||||||
|
|
||||||
$potention_count -= $spatial_planning_count;
|
|
||||||
$potention_total -= $spatial_planning_total;
|
|
||||||
|
|
||||||
return self::create([
|
return self::create([
|
||||||
'import_datasource_id' => $import_datasource_id,
|
'import_datasource_id' => $import_datasource_id,
|
||||||
'spatial_count' => $spatial_planning_count,
|
'spatial_count' => $service_google_sheet->getSpatialPlanningWithCalculationCount() ?? 0,
|
||||||
'spatial_sum' => $spatial_planning_total ?? 0.00,
|
'spatial_sum' => $service_google_sheet->getSpatialPlanningCalculationSum() ?? 0.00,
|
||||||
'potention_count' => $potention_count ?? 0,
|
'potention_count' => $potention_count,
|
||||||
'potention_sum' => $potention_total ?? 0.00,
|
'potention_sum' => ($stats->potention_total ?? 0),
|
||||||
'non_verified_count' => $non_verified_count ?? 0,
|
'non_verified_count' => $non_verified_count,
|
||||||
'non_verified_sum' => $non_verified_total ?? 0.00,
|
'non_verified_sum' => $non_verified_total,
|
||||||
'verified_count' => $verified_count ?? 0,
|
'verified_count' => $verified_count,
|
||||||
'verified_sum' => $verified_total ?? 0.00,
|
'verified_sum' => $stats->verified_total ?? 0.00,
|
||||||
'business_count' => $business_count ?? 0,
|
'business_count' => $business_count,
|
||||||
'business_sum' => $business_total ?? 0.00,
|
'business_sum' => $business_total,
|
||||||
'non_business_count' => $non_business_count ?? 0,
|
'non_business_count' => $non_business_count,
|
||||||
'non_business_sum' => $non_business_total ?? 0.00,
|
'non_business_sum' => $non_business_total,
|
||||||
'year' => $year,
|
'year' => $year,
|
||||||
'waiting_click_dpmptsp_count' => $data_setting['MENUNGGU_KLIK_DPMPTSP_COUNT'] ?? 0,
|
'waiting_click_dpmptsp_count' => $waiting_click_dpmptsp_count,
|
||||||
'waiting_click_dpmptsp_sum' => $data_setting['MENUNGGU_KLIK_DPMPTSP_SUM'] ?? 0.00,
|
'waiting_click_dpmptsp_sum' => $stats->waiting_click_dpmptsp_total ?? 0.00,
|
||||||
'issuance_realization_pbg_count' => $data_setting['REALISASI_TERBIT_PBG_COUNT'] ?? 0,
|
'issuance_realization_pbg_count' => $issuance_realization_pbg_count,
|
||||||
'issuance_realization_pbg_sum' => $data_setting['REALISASI_TERBIT_PBG_SUM'] ?? 0.00,
|
'issuance_realization_pbg_sum' => $stats->issuance_realization_pbg_total ?? 0.00,
|
||||||
'process_in_technical_office_count' => $data_setting['PROSES_DINAS_TEKNIS_COUNT'] ?? 0,
|
'process_in_technical_office_count' => $process_in_technical_office_count,
|
||||||
'process_in_technical_office_sum' => $data_setting['PROSES_DINAS_TEKNIS_SUM'] ??0.00,
|
'process_in_technical_office_sum' => $stats->process_in_technical_office_total ?? 0.00,
|
||||||
|
'business_rab_count' => $business_rab_count,
|
||||||
|
'business_krk_count' => $business_krk_count,
|
||||||
|
'business_dlh_count' => $business_dlh_count,
|
||||||
|
'non_business_rab_count' => $non_business_rab_count,
|
||||||
|
'non_business_krk_count' => $non_business_krk_count,
|
||||||
|
'resume_type' => $resume_type,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
57
app/Models/PbgStatus.php
Normal file
57
app/Models/PbgStatus.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
class PbgStatus extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'pbg_statuses';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'pbg_task_uuid',
|
||||||
|
'status',
|
||||||
|
'status_name',
|
||||||
|
'slf_status',
|
||||||
|
'slf_status_name',
|
||||||
|
'due_date',
|
||||||
|
'uid',
|
||||||
|
'note',
|
||||||
|
'file',
|
||||||
|
'data_due_date',
|
||||||
|
'data_created_at',
|
||||||
|
'slf_data',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function pbgTask()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(PbgTask::class, 'pbg_task_uuid', 'uuid');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function createOrUpdateFromApi(array $apiResponse, string $pbgTaskUuid)
|
||||||
|
{
|
||||||
|
$data = $apiResponse['data'] ?? [];
|
||||||
|
|
||||||
|
return self::updateOrCreate(
|
||||||
|
[
|
||||||
|
'pbg_task_uuid' => $pbgTaskUuid,
|
||||||
|
'status' => $apiResponse['status'], // key pencarian unik
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'status_name' => $apiResponse['status_name'] ?? null,
|
||||||
|
'slf_status' => $apiResponse['slf_status'] ?? null,
|
||||||
|
'slf_status_name' => $apiResponse['slf_status_name'] ?? null,
|
||||||
|
'due_date' => $apiResponse['due_date'] ?? null,
|
||||||
|
|
||||||
|
// nested data
|
||||||
|
'uid' => $data['uid'] ?? null,
|
||||||
|
'note' => $data['note'] ?? null,
|
||||||
|
'file' => $data['file'] ?? null,
|
||||||
|
'data_due_date' => $data['due_date'] ?? null,
|
||||||
|
'data_created_at' => isset($data['created_at']) ? Carbon::parse($data['created_at'])->format('Y-m-d H:i:s') : null,
|
||||||
|
|
||||||
|
'slf_data' => $apiResponse['slf_data'] ?? null,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,8 @@ class PbgTask extends Model
|
|||||||
'consultation_type',
|
'consultation_type',
|
||||||
'due_date',
|
'due_date',
|
||||||
'land_certificate_phase',
|
'land_certificate_phase',
|
||||||
'task_created_at'
|
'task_created_at',
|
||||||
|
'is_valid'
|
||||||
];
|
];
|
||||||
|
|
||||||
public function pbg_task_retributions(){
|
public function pbg_task_retributions(){
|
||||||
@@ -43,7 +44,7 @@ class PbgTask extends Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function googleSheet(){
|
public function googleSheet(){
|
||||||
return $this->hasOne(PbgTaskGoogleSheet::class, 'no_registrasi', 'registration_number');
|
return $this->hasOne(PbgTaskGoogleSheet::class, 'formatted_registration_number', 'registration_number');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function taskAssignments()
|
public function taskAssignments()
|
||||||
@@ -54,4 +55,91 @@ class PbgTask extends Model
|
|||||||
public function attachments(){
|
public function attachments(){
|
||||||
return $this->hasMany(PbgTaskAttachment::class, 'pbg_task_id', 'id');
|
return $this->hasMany(PbgTaskAttachment::class, 'pbg_task_id', 'id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the data lists associated with this PBG task (One to Many)
|
||||||
|
* One pbg_task can have many data lists
|
||||||
|
*/
|
||||||
|
public function dataLists()
|
||||||
|
{
|
||||||
|
return $this->hasMany(PbgTaskDetailDataList::class, 'pbg_task_uuid', 'uuid');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pbg_status()
|
||||||
|
{
|
||||||
|
return $this->hasOne(PbgStatus::class, 'pbg_task_uuid', 'uuid');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get only data lists with files
|
||||||
|
*/
|
||||||
|
public function dataListsWithFiles()
|
||||||
|
{
|
||||||
|
return $this->hasMany(PbgTaskDetailDataList::class, 'pbg_task_uuid', 'uuid')
|
||||||
|
->whereNotNull('file')
|
||||||
|
->where('file', '!=', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data lists by status
|
||||||
|
*/
|
||||||
|
public function dataListsByStatus($status)
|
||||||
|
{
|
||||||
|
return $this->hasMany(PbgTaskDetailDataList::class, 'pbg_task_uuid', 'uuid')
|
||||||
|
->where('status', $status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data lists by data type
|
||||||
|
*/
|
||||||
|
public function dataListsByType($dataType)
|
||||||
|
{
|
||||||
|
return $this->hasMany(PbgTaskDetailDataList::class, 'pbg_task_uuid', 'uuid')
|
||||||
|
->where('data_type', $dataType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or update data lists from API response
|
||||||
|
*/
|
||||||
|
public function syncDataLists(array $dataLists): void
|
||||||
|
{
|
||||||
|
foreach ($dataLists as $listData) {
|
||||||
|
PbgTaskDetailDataList::updateOrCreate(
|
||||||
|
['uid' => $listData['uid']],
|
||||||
|
[
|
||||||
|
'name' => $listData['name'] ?? null,
|
||||||
|
'description' => $listData['description'] ?? null,
|
||||||
|
'status' => $listData['status'] ?? null,
|
||||||
|
'status_name' => $listData['status_name'] ?? null,
|
||||||
|
'data_type' => $listData['data_type'] ?? null,
|
||||||
|
'data_type_name' => $listData['data_type_name'] ?? null,
|
||||||
|
'file' => $listData['file'] ?? null,
|
||||||
|
'note' => $listData['note'] ?? null,
|
||||||
|
'pbg_task_uuid' => $this->uuid,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data lists count by status
|
||||||
|
*/
|
||||||
|
public function getDataListsCountByStatusAttribute()
|
||||||
|
{
|
||||||
|
return $this->dataLists()
|
||||||
|
->selectRaw('status, COUNT(*) as count')
|
||||||
|
->groupBy('status')
|
||||||
|
->pluck('count', 'status');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data lists count by data type
|
||||||
|
*/
|
||||||
|
public function getDataListsCountByTypeAttribute()
|
||||||
|
{
|
||||||
|
return $this->dataLists()
|
||||||
|
->selectRaw('data_type, COUNT(*) as count')
|
||||||
|
->groupBy('data_type')
|
||||||
|
->pluck('count', 'data_type');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,54 +141,210 @@ class PbgTaskDetail extends Model
|
|||||||
return $this->belongsTo(PbgTask::class, 'pbg_task_uid', 'uuid');
|
return $this->belongsTo(PbgTask::class, 'pbg_task_uid', 'uuid');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to clean and convert latitude/longitude values
|
||||||
|
*/
|
||||||
|
private static function cleanCoordinate($value): ?float
|
||||||
|
{
|
||||||
|
if ($value === null || $value === '' || $value === '?' || $value === '-') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to string and trim whitespace
|
||||||
|
$stringValue = trim((string) $value);
|
||||||
|
|
||||||
|
// Check for common invalid values
|
||||||
|
if (in_array($stringValue, ['', '?', '-', 'null', 'NULL', 'N/A', '0,'], true)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove degree symbol and other non-numeric characters except minus and decimal point
|
||||||
|
$cleaned = preg_replace('/[^\d.-]/', '', $stringValue);
|
||||||
|
|
||||||
|
// Check if cleaned value is empty or just a hyphen
|
||||||
|
if ($cleaned === '' || $cleaned === '-' || $cleaned === '.') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate if it's a valid number and within reasonable coordinate bounds
|
||||||
|
if (is_numeric($cleaned)) {
|
||||||
|
$coordinate = (float) $cleaned;
|
||||||
|
|
||||||
|
// Basic validation for reasonable coordinate ranges
|
||||||
|
// Latitude: -90 to 90, Longitude: -180 to 180
|
||||||
|
if ($coordinate >= -180 && $coordinate <= 180) {
|
||||||
|
return $coordinate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to clean and convert integer values
|
||||||
|
*/
|
||||||
|
private static function cleanIntegerValue($value): int
|
||||||
|
{
|
||||||
|
if ($value === null || $value === '' || $value === '?') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to string and trim whitespace
|
||||||
|
$stringValue = trim((string) $value);
|
||||||
|
|
||||||
|
// Check for common invalid values
|
||||||
|
if (in_array($stringValue, ['', '?', '-', 'null', 'NULL', 'N/A'], true)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any non-numeric characters except minus
|
||||||
|
$cleaned = preg_replace('/[^\d-]/', '', $stringValue);
|
||||||
|
|
||||||
|
// Check if cleaned value is empty or just invalid characters
|
||||||
|
if ($cleaned === '' || $cleaned === '-') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate if it's a valid number
|
||||||
|
if (is_numeric($cleaned)) {
|
||||||
|
return (int) $cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to clean and convert numeric values
|
||||||
|
*/
|
||||||
|
private static function cleanNumericValue($value, bool $nullable = false): ?float
|
||||||
|
{
|
||||||
|
if ($value === null || $value === '' || $value === '?') {
|
||||||
|
return $nullable ? null : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to string and trim whitespace
|
||||||
|
$stringValue = trim((string) $value);
|
||||||
|
|
||||||
|
// Check for common invalid values
|
||||||
|
if (in_array($stringValue, ['', '?', '-', 'null', 'NULL', 'N/A'], true)) {
|
||||||
|
return $nullable ? null : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any non-numeric characters except minus and decimal point
|
||||||
|
$cleaned = preg_replace('/[^\d.-]/', '', $stringValue);
|
||||||
|
|
||||||
|
// Check if cleaned value is empty or just invalid characters
|
||||||
|
if ($cleaned === '' || $cleaned === '-' || $cleaned === '.') {
|
||||||
|
return $nullable ? null : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate if it's a valid number
|
||||||
|
if (is_numeric($cleaned)) {
|
||||||
|
return (float) $cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $nullable ? null : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to handle date parsing with fallback
|
||||||
|
*/
|
||||||
|
private static function parseDate($date): ?string
|
||||||
|
{
|
||||||
|
if (!$date || $date === '?' || $date === 'null') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Carbon::parse($date)->format('Y-m-d');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to handle datetime parsing with fallback
|
||||||
|
*/
|
||||||
|
private static function parseDateTime($datetime): ?string
|
||||||
|
{
|
||||||
|
if (!$datetime || $datetime === '?' || $datetime === 'null') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Carbon::parse($datetime)->format('Y-m-d H:i:s');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create or update PbgTaskDetail from API response
|
* Create or update PbgTaskDetail from API response
|
||||||
*/
|
*/
|
||||||
public static function createFromApiResponse(array $data, string $pbgTaskUuid): self
|
public static function createFromApiResponse(array $data, string $pbgTaskUuid): self
|
||||||
{
|
{
|
||||||
$detailData = [
|
$detailData = [
|
||||||
|
// Foreign key relationship - string, required
|
||||||
'pbg_task_uid' => $pbgTaskUuid,
|
'pbg_task_uid' => $pbgTaskUuid,
|
||||||
'uid' => $data['uid'] ?? null,
|
|
||||||
'nik' => $data['nik'] ?? null,
|
// Basic information
|
||||||
'type_card' => $data['type_card'] ?? null,
|
'uid' => $data['uid'] ?? "N/A", // string, unique, required
|
||||||
'ownership' => $data['ownership'] ?? null,
|
'nik' => isset($data['nik']) && $data['nik'] !== '' && $data['nik'] !== '?' ? $data['nik'] : null, // string, nullable
|
||||||
'owner_name' => $data['owner_name'] ?? null,
|
'type_card' => isset($data['type_card']) && $data['type_card'] !== '' && $data['type_card'] !== '?' ? $data['type_card'] : null, // string, nullable
|
||||||
'ward_id' => $data['ward_id'] ?? null,
|
'ownership' => $data['ownership'] ?? null, // string, nullable
|
||||||
'ward_name' => $data['ward_name'] ?? null,
|
'owner_name' => $data['owner_name'] ?? "N/A", // string, required
|
||||||
'district_id' => $data['district_id'] ?? null,
|
|
||||||
'district_name' => $data['district_name'] ?? null,
|
// Owner location information - all required
|
||||||
'regency_id' => $data['regency_id'] ?? null,
|
'ward_id' => self::cleanIntegerValue($data['ward_id'] ?? 0), // bigInteger, required
|
||||||
'regency_name' => $data['regency_name'] ?? null,
|
'ward_name' => $data['ward_name'] ?? "N/A", // string, required
|
||||||
'province_id' => $data['province_id'] ?? null,
|
'district_id' => self::cleanIntegerValue($data['district_id'] ?? 0), // integer, required
|
||||||
'province_name' => $data['province_name'] ?? null,
|
'district_name' => $data['district_name'] ?? "N/A", // string, required
|
||||||
'address' => $data['address'] ?? null,
|
'regency_id' => self::cleanIntegerValue($data['regency_id'] ?? 0), // integer, required
|
||||||
'owner_email' => $data['owner_email'] ?? null,
|
'regency_name' => $data['regency_name'] ?? "N/A", // string, required
|
||||||
'owner_phone' => $data['owner_phone'] ?? null,
|
'province_id' => self::cleanIntegerValue($data['province_id'] ?? 0), // integer, required
|
||||||
'user' => $data['user'] ?? null,
|
'province_name' => $data['province_name'] ?? "N/A", // string, required
|
||||||
'name' => $data['name'] ?? null,
|
'address' => $data['address'] ?? "N/A", // text, required
|
||||||
'email' => $data['email'] ?? null,
|
|
||||||
'phone' => $data['phone'] ?? null,
|
// Owner contact information - required
|
||||||
'user_nik' => $data['user_nik'] ?? null,
|
'owner_email' => $data['owner_email'] ?? "N/A", // string, required
|
||||||
'user_province_id' => $data['user_province_id'] ?? null,
|
'owner_phone' => $data['owner_phone'] ?? "N/A", // string, required
|
||||||
'user_province_name' => $data['user_province_name'] ?? null,
|
|
||||||
'user_regency_id' => $data['user_regency_id'] ?? null,
|
// User information - all required
|
||||||
'user_regency_name' => $data['user_regency_name'] ?? null,
|
'user' => self::cleanIntegerValue($data['user'] ?? 0), // integer, required
|
||||||
'user_district_id' => $data['user_district_id'] ?? null,
|
'name' => $data['name'] ?? "N/A", // string, required
|
||||||
'user_district_name' => $data['user_district_name'] ?? null,
|
'email' => $data['email'] ?? "N/A", // string, required
|
||||||
'user_address' => $data['user_address'] ?? null,
|
'phone' => $data['phone'] ?? "N/A", // string, required
|
||||||
'status' => $data['status'] ?? null,
|
'user_nik' => $data['user_nik'] ?? "N/A", // string, required
|
||||||
'status_name' => $data['status_name'] ?? null,
|
|
||||||
'slf_status' => $data['slf_status'] ?? null,
|
// User location information - all required
|
||||||
'slf_status_name' => $data['slf_status_name'] ?? null,
|
'user_province_id' => self::cleanIntegerValue($data['user_province_id'] ?? 0), // integer, required
|
||||||
'sppst_status' => $data['sppst_status'] ?? null,
|
'user_province_name' => $data['user_province_name'] ?? "N/A", // string, required
|
||||||
'sppst_file' => $data['sppst_file'] ?? null,
|
'user_regency_id' => self::cleanIntegerValue($data['user_regency_id'] ?? 0), // integer, required
|
||||||
'sppst_status_name' => $data['sppst_status_name'] ?? null,
|
'user_regency_name' => $data['user_regency_name'] ?? "N/A", // string, required
|
||||||
'file_pbg' => $data['file_pbg'] ?? null,
|
'user_district_id' => self::cleanIntegerValue($data['user_district_id'] ?? 0), // integer, required
|
||||||
'file_pbg_date' => isset($data['file_pbg_date']) ? Carbon::parse($data['file_pbg_date'])->format('Y-m-d') : null,
|
'user_district_name' => $data['user_district_name'] ?? "N/A", // string, required
|
||||||
'due_date' => isset($data['due_date']) ? Carbon::parse($data['due_date'])->format('Y-m-d') : null,
|
'user_address' => $data['user_address'] ?? "N/A", // text, required
|
||||||
'start_date' => isset($data['start_date']) ? Carbon::parse($data['start_date'])->format('Y-m-d') : null,
|
|
||||||
'document_number' => $data['document_number'] ?? null,
|
// Status information
|
||||||
'registration_number' => $data['registration_number'] ?? null,
|
'status' => self::cleanIntegerValue($data['status'] ?? 0), // integer, required
|
||||||
|
'status_name' => $data['status_name'] ?? "N/A", // string, required
|
||||||
|
'slf_status' => isset($data['slf_status']) && is_numeric($data['slf_status']) ? (int) $data['slf_status'] : null, // integer, nullable
|
||||||
|
'slf_status_name' => $data['slf_status_name'] ?? null, // string, nullable
|
||||||
|
'sppst_status' => self::cleanIntegerValue($data['sppst_status'] ?? 0), // integer, required
|
||||||
|
'sppst_file' => $data['sppst_file'] ?? null, // string, nullable
|
||||||
|
'sppst_status_name' => $data['sppst_status_name'] ?? "N/A", // string, required
|
||||||
|
|
||||||
|
// Files and documents
|
||||||
|
'file_pbg' => $data['file_pbg'] ?? null, // string, nullable
|
||||||
|
'file_pbg_date' => self::parseDate($data['file_pbg_date'] ?? null), // date, nullable
|
||||||
|
'due_date' => self::parseDate($data['due_date'] ?? null), // date, nullable
|
||||||
|
'start_date' => self::parseDate($data['start_date'] ?? null) ?? now()->format('Y-m-d'), // date, required
|
||||||
|
'document_number' => $data['document_number'] ?? null, // string, nullable
|
||||||
|
'registration_number' => $data['registration_number'] ?? "N/A", // string, required
|
||||||
|
|
||||||
|
// Application information - all nullable
|
||||||
'function_type' => $data['function_type'] ?? null,
|
'function_type' => $data['function_type'] ?? null,
|
||||||
'application_type' => $data['application_type'] ?? null,
|
'application_type' => $data['application_type'] ?? null,
|
||||||
'application_type_name' => $data['application_type_name'] ?? null,
|
'application_type_name' => $data['application_type_name'] ?? null,
|
||||||
@@ -196,52 +352,74 @@ class PbgTaskDetail extends Model
|
|||||||
'condition' => $data['condition'] ?? null,
|
'condition' => $data['condition'] ?? null,
|
||||||
'prototype' => $data['prototype'] ?? null,
|
'prototype' => $data['prototype'] ?? null,
|
||||||
'permanency' => $data['permanency'] ?? null,
|
'permanency' => $data['permanency'] ?? null,
|
||||||
'building_type' => $data['building_type'] ?? null,
|
|
||||||
|
// Building information - all nullable
|
||||||
|
'building_type' => isset($data['building_type']) && is_numeric($data['building_type']) ? (int) $data['building_type'] : null, // integer, nullable
|
||||||
'building_type_name' => $data['building_type_name'] ?? null,
|
'building_type_name' => $data['building_type_name'] ?? null,
|
||||||
'building_purpose' => $data['building_purpose'] ?? null,
|
'building_purpose' => $data['building_purpose'] ?? null,
|
||||||
'building_use' => $data['building_use'] ?? null,
|
'building_use' => $data['building_use'] ?? null,
|
||||||
'occupancy' => $data['occupancy'] ?? null,
|
'occupancy' => $data['occupancy'] ?? null,
|
||||||
'name_building' => $data['name_building'] ?? null,
|
'name_building' => $data['name_building'] ?? null,
|
||||||
'total_area' => $data['total_area'] ?? null,
|
|
||||||
'area' => $data['area'] ?? null,
|
// Building dimensions and specifications
|
||||||
'area_type' => $data['area_type'] ?? null,
|
'total_area' => self::cleanNumericValue($data['total_area'] ?? 0), // decimal(10,2), required
|
||||||
'height' => $data['height'] ?? null,
|
'area' => self::cleanNumericValue($data['area'] ?? null, true), // decimal(10,2), nullable
|
||||||
'floor' => $data['floor'] ?? null,
|
'area_type' => $data['area_type'] ?? null, // string, nullable
|
||||||
'floor_area' => $data['floor_area'] ?? null,
|
'height' => self::cleanNumericValue($data['height'] ?? 0), // decimal(8,2), required
|
||||||
'basement' => $data['basement'] ?? null,
|
'floor' => self::cleanIntegerValue($data['floor'] ?? 0), // integer, required
|
||||||
'basement_height' => $data['basement_height'] ?? null,
|
'floor_area' => self::cleanNumericValue($data['floor_area'] ?? null, true), // decimal(10,2), nullable
|
||||||
'basement_area' => $data['basement_area'] ?? null,
|
'basement' => isset($data['basement']) && $data['basement'] !== '' && $data['basement'] !== '?' ? $data['basement'] : null, // string, nullable
|
||||||
'unit' => $data['unit'] ?? null,
|
'basement_height' => self::cleanNumericValue($data['basement_height'] ?? null, true), // decimal(8,2), nullable
|
||||||
'prev_retribution' => $data['prev_retribution'] ?? null,
|
'basement_area' => self::cleanNumericValue($data['basement_area'] ?? 0), // decimal(10,2), required
|
||||||
'prev_pbg' => $data['prev_pbg'] ?? null,
|
'unit' => isset($data['unit']) && is_numeric($data['unit']) ? (int) $data['unit'] : null, // integer, nullable
|
||||||
'prev_total_area' => $data['prev_total_area'] ?? null,
|
|
||||||
'koefisien_dasar_bangunan' => $data['koefisien_dasar_bangunan'] ?? null,
|
// Previous information
|
||||||
'koefisien_lantai_bangunan' => $data['koefisien_lantai_bangunan'] ?? null,
|
'prev_retribution' => self::cleanNumericValue($data['prev_retribution'] ?? null, true), // decimal(15,2), nullable
|
||||||
'koefisien_lantai_hijau' => $data['koefisien_lantai_hijau'] ?? null,
|
'prev_pbg' => $data['prev_pbg'] ?? null, // string, nullable
|
||||||
'koefisien_tapak_basement' => $data['koefisien_tapak_basement'] ?? null,
|
'prev_total_area' => self::cleanNumericValue($data['prev_total_area'] ?? null, true), // decimal(10,2), nullable
|
||||||
'ketinggian_bangunan' => $data['ketinggian_bangunan'] ?? null,
|
|
||||||
|
// Coefficients - all nullable, decimal(8,4)
|
||||||
|
'koefisien_dasar_bangunan' => self::cleanNumericValue($data['koefisien_dasar_bangunan'] ?? null, true),
|
||||||
|
'koefisien_lantai_bangunan' => self::cleanNumericValue($data['koefisien_lantai_bangunan'] ?? null, true),
|
||||||
|
'koefisien_lantai_hijau' => self::cleanNumericValue($data['koefisien_lantai_hijau'] ?? null, true),
|
||||||
|
'koefisien_tapak_basement' => self::cleanNumericValue($data['koefisien_tapak_basement'] ?? null, true),
|
||||||
|
'ketinggian_bangunan' => self::cleanNumericValue($data['ketinggian_bangunan'] ?? null, true), // decimal(8,2), nullable
|
||||||
|
|
||||||
|
// Road information - all nullable
|
||||||
'jalan_arteri' => $data['jalan_arteri'] ?? null,
|
'jalan_arteri' => $data['jalan_arteri'] ?? null,
|
||||||
'jalan_kolektor' => $data['jalan_kolektor'] ?? null,
|
'jalan_kolektor' => $data['jalan_kolektor'] ?? null,
|
||||||
'jalan_bangunan' => $data['jalan_bangunan'] ?? null,
|
'jalan_bangunan' => $data['jalan_bangunan'] ?? null,
|
||||||
'gsb' => $data['gsb'] ?? null,
|
'gsb' => self::cleanNumericValue($data['gsb'] ?? null, true), // decimal(8,2), nullable
|
||||||
'kkr_number' => $data['kkr_number'] ?? null,
|
'kkr_number' => $data['kkr_number'] ?? null, // string, nullable
|
||||||
|
|
||||||
|
// Unit data as JSON - nullable
|
||||||
'unit_data' => $data['unit_data'] ?? null,
|
'unit_data' => $data['unit_data'] ?? null,
|
||||||
'is_mbr' => $data['is_mbr'] ?? false,
|
|
||||||
'code' => $data['code'] ?? null,
|
// Additional flags
|
||||||
'building_ward_id' => $data['building_ward_id'] ?? null,
|
'is_mbr' => (bool) ($data['is_mbr'] ?? false), // boolean, default false
|
||||||
'building_ward_name' => $data['building_ward_name'] ?? null,
|
'code' => $data['code'] ?? "N/A", // string, required
|
||||||
'building_district_id' => $data['building_district_id'] ?? null,
|
|
||||||
'building_district_name' => $data['building_district_name'] ?? null,
|
// Building location information - all required
|
||||||
'building_regency_id' => $data['building_regency_id'] ?? null,
|
'building_ward_id' => self::cleanIntegerValue($data['building_ward_id'] ?? 0), // bigInteger, required
|
||||||
'building_regency_name' => $data['building_regency_name'] ?? null,
|
'building_ward_name' => $data['building_ward_name'] ?? "N/A", // string, required
|
||||||
'building_province_id' => $data['building_province_id'] ?? null,
|
'building_district_id' => self::cleanIntegerValue($data['building_district_id'] ?? 0), // integer, required
|
||||||
'building_province_name' => $data['building_province_name'] ?? null,
|
'building_district_name' => $data['building_district_name'] ?? "N/A", // string, required
|
||||||
'building_address' => $data['building_address'] ?? null,
|
'building_regency_id' => self::cleanIntegerValue($data['building_regency_id'] ?? 0), // integer, required
|
||||||
'latitude' => $data['latitude'] ?? null,
|
'building_regency_name' => $data['building_regency_name'] ?? "N/A", // string, required
|
||||||
'longitude' => $data['longitude'] ?? null,
|
'building_province_id' => self::cleanIntegerValue($data['building_province_id'] ?? 0), // integer, required
|
||||||
|
'building_province_name' => $data['building_province_name'] ?? "N/A", // string, required
|
||||||
|
'building_address' => $data['building_address'] ?? "N/A", // text, required
|
||||||
|
|
||||||
|
// Coordinates - decimal(15,8), nullable
|
||||||
|
'latitude' => self::cleanCoordinate($data['latitude'] ?? null),
|
||||||
|
'longitude' => self::cleanCoordinate($data['longitude'] ?? null),
|
||||||
|
|
||||||
|
// Additional files - nullable
|
||||||
'building_photo' => $data['building_photo'] ?? null,
|
'building_photo' => $data['building_photo'] ?? null,
|
||||||
'pbg_parent' => $data['pbg_parent'] ?? null,
|
'pbg_parent' => $data['pbg_parent'] ?? null,
|
||||||
'api_created_at' => isset($data['created_at']) ? Carbon::parse($data['created_at'])->format('Y-m-d H:i:s') : null,
|
|
||||||
|
// Original created_at from API - nullable
|
||||||
|
'api_created_at' => self::parseDateTime($data['created_at'] ?? null),
|
||||||
];
|
];
|
||||||
|
|
||||||
return static::updateOrCreate(
|
return static::updateOrCreate(
|
||||||
@@ -249,4 +427,6 @@ class PbgTaskDetail extends Model
|
|||||||
$detailData
|
$detailData
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
161
app/Models/PbgTaskDetailDataList.php
Normal file
161
app/Models/PbgTaskDetailDataList.php
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class PbgTaskDetailDataList extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'pbg_task_detail_data_lists';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'uid',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'status',
|
||||||
|
'status_name',
|
||||||
|
'data_type',
|
||||||
|
'data_type_name',
|
||||||
|
'file',
|
||||||
|
'note',
|
||||||
|
'pbg_task_uuid',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'status' => 'integer',
|
||||||
|
'data_type' => 'integer',
|
||||||
|
'created_at' => 'datetime',
|
||||||
|
'updated_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relationship to PbgTask (Many to One)
|
||||||
|
* Many data lists belong to one pbg_task
|
||||||
|
*/
|
||||||
|
public function pbgTask()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(PbgTask::class, 'pbg_task_uuid', 'uuid');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the full file path
|
||||||
|
*/
|
||||||
|
public function getFilePathAttribute()
|
||||||
|
{
|
||||||
|
return $this->file ? storage_path('app/public/' . $this->file) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the file URL
|
||||||
|
*/
|
||||||
|
public function getFileUrlAttribute()
|
||||||
|
{
|
||||||
|
return $this->file ? asset('storage/' . $this->file) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if file exists
|
||||||
|
*/
|
||||||
|
public function hasFile()
|
||||||
|
{
|
||||||
|
return !empty($this->file) && file_exists($this->getFilePathAttribute());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status badge color based on status
|
||||||
|
*/
|
||||||
|
public function getStatusBadgeAttribute()
|
||||||
|
{
|
||||||
|
return match($this->status) {
|
||||||
|
1 => 'success', // Sesuai
|
||||||
|
0 => 'danger', // Tidak Sesuai
|
||||||
|
default => 'secondary'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope: Filter by status
|
||||||
|
*/
|
||||||
|
public function scopeByStatus($query, $status)
|
||||||
|
{
|
||||||
|
return $query->where('status', $status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope: Filter by data type
|
||||||
|
*/
|
||||||
|
public function scopeByDataType($query, $dataType)
|
||||||
|
{
|
||||||
|
return $query->where('data_type', $dataType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope: With files only
|
||||||
|
*/
|
||||||
|
public function scopeWithFiles($query)
|
||||||
|
{
|
||||||
|
return $query->whereNotNull('file')->where('file', '!=', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope: Search by name or description
|
||||||
|
*/
|
||||||
|
public function scopeSearch($query, $search)
|
||||||
|
{
|
||||||
|
return $query->where(function ($q) use ($search) {
|
||||||
|
$q->where('name', 'LIKE', "%{$search}%")
|
||||||
|
->orWhere('description', 'LIKE', "%{$search}%")
|
||||||
|
->orWhere('status_name', 'LIKE', "%{$search}%")
|
||||||
|
->orWhere('data_type_name', 'LIKE', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file extension from file path
|
||||||
|
*/
|
||||||
|
public function getFileExtensionAttribute()
|
||||||
|
{
|
||||||
|
if (!$this->file) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return strtoupper(pathinfo($this->file, PATHINFO_EXTENSION));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get filename from file path
|
||||||
|
*/
|
||||||
|
public function getFileNameAttribute()
|
||||||
|
{
|
||||||
|
if (!$this->file) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return basename($this->file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get formatted created date
|
||||||
|
*/
|
||||||
|
public function getFormattedCreatedAtAttribute()
|
||||||
|
{
|
||||||
|
return $this->created_at ? $this->created_at->format('d M Y, H:i') : '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get truncated description
|
||||||
|
*/
|
||||||
|
public function getTruncatedDescriptionAttribute()
|
||||||
|
{
|
||||||
|
return $this->description ? \Str::limit($this->description, 80) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get truncated note
|
||||||
|
*/
|
||||||
|
public function getTruncatedNoteAttribute()
|
||||||
|
{
|
||||||
|
return $this->note ? \Str::limit($this->note, 100) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
136
app/Models/PbgTaskPayment.php
Normal file
136
app/Models/PbgTaskPayment.php
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class PbgTaskPayment extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'pbg_task_id',
|
||||||
|
'pbg_task_uid',
|
||||||
|
// mapped fields
|
||||||
|
'row_no',
|
||||||
|
'consultation_type',
|
||||||
|
'source_registration_number',
|
||||||
|
'owner_name',
|
||||||
|
'building_location',
|
||||||
|
'building_function',
|
||||||
|
'building_name',
|
||||||
|
'application_date_raw',
|
||||||
|
'verification_status',
|
||||||
|
'application_status',
|
||||||
|
'owner_address',
|
||||||
|
'owner_phone',
|
||||||
|
'owner_email',
|
||||||
|
'note_date_raw',
|
||||||
|
'document_shortage_note',
|
||||||
|
'image_url',
|
||||||
|
'krk_kkpr',
|
||||||
|
'krk_number',
|
||||||
|
'lh',
|
||||||
|
'ska',
|
||||||
|
'remarks',
|
||||||
|
'helpdesk',
|
||||||
|
'person_in_charge',
|
||||||
|
'pbg_operator',
|
||||||
|
'ownership',
|
||||||
|
'taru_potential',
|
||||||
|
'agency_validation',
|
||||||
|
'retribution_category',
|
||||||
|
'ba_tpt_number',
|
||||||
|
'ba_tpt_date_raw',
|
||||||
|
'ba_tpa_number',
|
||||||
|
'ba_tpa_date_raw',
|
||||||
|
'skrd_number',
|
||||||
|
'skrd_date_raw',
|
||||||
|
'ptsp_status',
|
||||||
|
'issued_status',
|
||||||
|
'payment_date_raw',
|
||||||
|
'sts_format',
|
||||||
|
'issuance_year',
|
||||||
|
'current_year',
|
||||||
|
'village',
|
||||||
|
'district',
|
||||||
|
'building_area',
|
||||||
|
'building_height',
|
||||||
|
'floor_count',
|
||||||
|
'unit_count',
|
||||||
|
'proposed_retribution',
|
||||||
|
'retribution_total_simbg',
|
||||||
|
'retribution_total_pad',
|
||||||
|
'penalty_amount',
|
||||||
|
'business_category',
|
||||||
|
'created_at',
|
||||||
|
'updated_at'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'application_date_raw' => 'date',
|
||||||
|
'note_date_raw' => 'date',
|
||||||
|
'ba_tpt_date_raw' => 'date',
|
||||||
|
'ba_tpa_date_raw' => 'date',
|
||||||
|
'skrd_date_raw' => 'date',
|
||||||
|
'payment_date_raw' => 'date',
|
||||||
|
'issuance_year' => 'integer',
|
||||||
|
'current_year' => 'integer',
|
||||||
|
'floor_count' => 'integer',
|
||||||
|
'unit_count' => 'integer',
|
||||||
|
'building_area' => 'decimal:2',
|
||||||
|
'building_height' => 'decimal:2',
|
||||||
|
'proposed_retribution' => 'decimal:2',
|
||||||
|
'retribution_total_simbg' => 'decimal:2',
|
||||||
|
'retribution_total_pad' => 'decimal:2',
|
||||||
|
'penalty_amount' => 'decimal:2'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the PBG task that owns this payment
|
||||||
|
*/
|
||||||
|
public function pbgTask(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(PbgTask::class, 'pbg_task_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean and convert registration number for matching
|
||||||
|
*/
|
||||||
|
public static function cleanRegistrationNumber(string $registrationNumber): string
|
||||||
|
{
|
||||||
|
return trim($registrationNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert pad amount string to decimal
|
||||||
|
*/
|
||||||
|
public static function convertPadAmount(?string $padAmount): float
|
||||||
|
{
|
||||||
|
if (empty($padAmount)) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove dots (thousands separator) and convert to float
|
||||||
|
$cleaned = str_replace('.', '', $padAmount);
|
||||||
|
$cleaned = str_replace(',', '.', $cleaned); // Handle comma as decimal separator if present
|
||||||
|
|
||||||
|
return is_numeric($cleaned) ? (float) $cleaned : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert date string to proper format
|
||||||
|
*/
|
||||||
|
public static function convertPaymentDate(?string $dateString): ?string
|
||||||
|
{
|
||||||
|
if (empty($dateString)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Carbon::parse($dateString)->format('Y-m-d');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,7 +56,38 @@ class RetributionCalculation extends Model
|
|||||||
*/
|
*/
|
||||||
public static function generateCalculationId(): string
|
public static function generateCalculationId(): string
|
||||||
{
|
{
|
||||||
return 'CALC-' . date('Ymd') . '-' . str_pad(mt_rand(1, 9999), 4, '0', STR_PAD_LEFT);
|
$maxAttempts = 10;
|
||||||
|
$attempt = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Use microseconds for better uniqueness but keep within 20 char limit
|
||||||
|
// Format: CALC-YYYYMMDD-XXXXX (20 chars exactly)
|
||||||
|
$microseconds = (int) (microtime(true) * 1000) % 100000; // 5 digits max
|
||||||
|
$id = 'CALC-' . date('Ymd') . '-' . str_pad($microseconds, 5, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
|
// Check if ID already exists
|
||||||
|
if (!self::where('calculation_id', $id)->exists()) {
|
||||||
|
return $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attempt++;
|
||||||
|
// Add small delay to ensure different microsecond values
|
||||||
|
usleep(1000); // 1ms delay
|
||||||
|
|
||||||
|
} while ($attempt < $maxAttempts);
|
||||||
|
|
||||||
|
// Fallback to random 5-digit number if all attempts fail
|
||||||
|
for ($i = 0; $i < 100; $i++) {
|
||||||
|
$random = mt_rand(10000, 99999);
|
||||||
|
$id = 'CALC-' . date('Ymd') . '-' . $random;
|
||||||
|
|
||||||
|
if (!self::where('calculation_id', $id)->exists()) {
|
||||||
|
return $id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final fallback - use current timestamp seconds
|
||||||
|
return 'CALC-' . date('Ymd') . '-' . str_pad(time() % 100000, 5, '0', STR_PAD_LEFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -34,14 +34,24 @@ class SpatialPlanning extends Model
|
|||||||
*
|
*
|
||||||
* @var array<int, string>
|
* @var array<int, string>
|
||||||
*/
|
*/
|
||||||
protected $fillable = ['name', 'kbli', 'activities', 'area', 'location', 'number', 'date', 'no_tapak', 'no_skkl', 'no_ukl', 'building_function', 'sub_building_function', 'number_of_floors', 'land_area', 'site_bcr'];
|
protected $fillable = ['name', 'kbli', 'activities', 'area', 'location', 'number', 'date', 'no_tapak', 'no_skkl', 'no_ukl', 'building_function', 'sub_building_function', 'number_of_floors', 'land_area', 'site_bcr', 'is_terbit'];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'area' => 'decimal:6',
|
'area' => 'decimal:6',
|
||||||
'land_area' => 'decimal:6',
|
'land_area' => 'decimal:6',
|
||||||
'site_bcr' => 'decimal:6',
|
'site_bcr' => 'decimal:6',
|
||||||
'number_of_floors' => 'integer',
|
'number_of_floors' => 'integer',
|
||||||
'date' => 'date'
|
'date' => 'date',
|
||||||
|
'is_terbit' => 'boolean'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $appends = [
|
||||||
|
'calculated_retribution',
|
||||||
|
'formatted_retribution',
|
||||||
|
'is_business_type',
|
||||||
|
'calculation_details',
|
||||||
|
'old_calculation_amount',
|
||||||
|
'calculation_source'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
@@ -62,7 +72,171 @@ class SpatialPlanning extends Model
|
|||||||
return (float) ($this->area ?? $this->land_area ?? 0);
|
return (float) ($this->area ?? $this->land_area ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get calculated retribution amount
|
||||||
|
* Priority: Manual calculation (new formula) > Active calculation (old system)
|
||||||
|
*/
|
||||||
|
public function getCalculatedRetributionAttribute(): float
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// PRIORITY 1: Use new manual formula (LUAS LAHAN × BCR × HARGA SATUAN)
|
||||||
|
$manualCalculation = $this->calculateRetributionManually();
|
||||||
|
|
||||||
|
// If manual calculation is valid (> 0), use it
|
||||||
|
if ($manualCalculation > 0) {
|
||||||
|
return $manualCalculation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PRIORITY 2: Fallback to active retribution calculation if exists
|
||||||
|
$activeCalculation = $this->activeRetributionCalculation;
|
||||||
|
|
||||||
|
if ($activeCalculation && $activeCalculation->retributionCalculation) {
|
||||||
|
return (float) $activeCalculation->retributionCalculation->retribution_amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PRIORITY 3: Return 0 if nothing works
|
||||||
|
return 0.0;
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
\Log::warning('Failed to calculate retribution for SpatialPlanning ID: ' . $this->id, [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'spatial_planning' => $this->toArray()
|
||||||
|
]);
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manual calculation based on area and building function
|
||||||
|
* Formula: LUAS LAHAN × BCR × HARGA SATUAN
|
||||||
|
* NON USAHA: 16,000 per m2
|
||||||
|
* USAHA: 44,300 per m2
|
||||||
|
*/
|
||||||
|
private function calculateRetributionManually(): float
|
||||||
|
{
|
||||||
|
// Get land area (luas lahan)
|
||||||
|
$landArea = (float) ($this->land_area ?? 0);
|
||||||
|
|
||||||
|
// Get BCR (Building Coverage Ratio) - convert from percentage to decimal
|
||||||
|
$bcrPercentage = (float) ($this->site_bcr ?? 0);
|
||||||
|
$bcr = $bcrPercentage / 100; // Convert percentage to decimal (24.49% -> 0.2449)
|
||||||
|
|
||||||
|
if ($landArea <= 0 || $bcr <= 0) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if this is business (USAHA) or non-business (NON USAHA)
|
||||||
|
$isBusiness = $this->isBusinessType();
|
||||||
|
|
||||||
|
// Set unit price based on business type
|
||||||
|
$unitPrice = $isBusiness ? 44300 : 16000;
|
||||||
|
|
||||||
|
// Calculate: LUAS LAHAN × BCR (as decimal) × HARGA SATUAN
|
||||||
|
$calculatedAmount = $landArea * $bcr * $unitPrice;
|
||||||
|
|
||||||
|
return $calculatedAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if this spatial planning is for business purposes
|
||||||
|
*/
|
||||||
|
private function isBusinessType(): bool
|
||||||
|
{
|
||||||
|
$buildingFunction = strtolower($this->building_function ?? $this->activities ?? '');
|
||||||
|
|
||||||
|
// Business keywords
|
||||||
|
$businessKeywords = [
|
||||||
|
'usaha', 'dagang', 'perdagangan', 'komersial', 'commercial', 'bisnis', 'business',
|
||||||
|
'toko', 'warung', 'pasar', 'kios', 'mall', 'plaza', 'supermarket', 'department',
|
||||||
|
'hotel', 'resort', 'restoran', 'restaurant', 'cafe', 'kantor', 'perkantoran', 'office',
|
||||||
|
'industri', 'pabrik', 'gudang', 'warehouse', 'manufacturing', 'produksi',
|
||||||
|
'bengkel', 'workshop', 'showroom', 'dealer', 'apotek', 'pharmacy', 'klinik swasta',
|
||||||
|
'rumah sakit swasta', 'bank', 'atm', 'money changer', 'asuransi', 'leasing',
|
||||||
|
'rental', 'sewa', 'jasa', 'service', 'salon', 'spa', 'fitness', 'gym',
|
||||||
|
'tempat usaha', 'fungsi usaha', 'kegiatan usaha', 'bangunan usaha'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if any business keyword is found
|
||||||
|
foreach ($businessKeywords as $keyword) {
|
||||||
|
if (str_contains($buildingFunction, $keyword)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-business (default)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get formatted retribution amount for display
|
||||||
|
*/
|
||||||
|
public function getFormattedRetributionAttribute(): string
|
||||||
|
{
|
||||||
|
$amount = $this->calculated_retribution;
|
||||||
|
return number_format($amount, 0, ',', '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this is business type
|
||||||
|
*/
|
||||||
|
public function getIsBusinessTypeAttribute(): bool
|
||||||
|
{
|
||||||
|
return $this->isBusinessType();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get calculation details for transparency
|
||||||
|
*/
|
||||||
|
public function getCalculationDetailsAttribute(): array
|
||||||
|
{
|
||||||
|
$landArea = (float) ($this->land_area ?? 0);
|
||||||
|
$bcrPercentage = (float) ($this->site_bcr ?? 0);
|
||||||
|
$bcr = $bcrPercentage / 100; // Convert to decimal
|
||||||
|
$isBusiness = $this->isBusinessType();
|
||||||
|
$unitPrice = $isBusiness ? 44300 : 16000;
|
||||||
|
$calculatedAmount = $landArea * $bcr * $unitPrice;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'formula' => 'LUAS LAHAN × BCR (decimal) × HARGA SATUAN',
|
||||||
|
'land_area' => $landArea,
|
||||||
|
'bcr_percentage' => $bcrPercentage,
|
||||||
|
'bcr_decimal' => $bcr,
|
||||||
|
'business_type' => $isBusiness ? 'USAHA' : 'NON USAHA',
|
||||||
|
'unit_price' => $unitPrice,
|
||||||
|
'calculation' => "{$landArea} × {$bcr} × {$unitPrice}",
|
||||||
|
'result' => $calculatedAmount,
|
||||||
|
'building_function' => $this->building_function ?? $this->activities ?? 'N/A'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get old calculation amount from database
|
||||||
|
*/
|
||||||
|
public function getOldCalculationAmountAttribute(): float
|
||||||
|
{
|
||||||
|
$activeCalculation = $this->activeRetributionCalculation;
|
||||||
|
|
||||||
|
if ($activeCalculation && $activeCalculation->retributionCalculation) {
|
||||||
|
return (float) $activeCalculation->retributionCalculation->retribution_amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get calculation source info
|
||||||
|
*/
|
||||||
|
public function getCalculationSourceAttribute(): string
|
||||||
|
{
|
||||||
|
$manualCalculation = $this->calculateRetributionManually();
|
||||||
|
$hasActiveCalculation = $this->hasActiveRetributionCalculation();
|
||||||
|
|
||||||
|
if ($manualCalculation > 0) {
|
||||||
|
return $hasActiveCalculation ? 'NEW_FORMULA' : 'NEW_FORMULA_ONLY';
|
||||||
|
} elseif ($hasActiveCalculation) {
|
||||||
|
return 'OLD_DATABASE';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'NONE';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
app/Models/Tax.php
Normal file
23
app/Models/Tax.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Tax extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'taxs';
|
||||||
|
protected $fillable = [
|
||||||
|
'tax_code',
|
||||||
|
'tax_no',
|
||||||
|
'npwpd',
|
||||||
|
'wp_name',
|
||||||
|
'business_name',
|
||||||
|
'address',
|
||||||
|
'start_validity',
|
||||||
|
'end_validity',
|
||||||
|
'tax_value',
|
||||||
|
'subdistrict',
|
||||||
|
'village',
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -14,8 +14,6 @@ use Illuminate\Support\Facades\Blade;
|
|||||||
use Illuminate\Support\Facades\View;
|
use Illuminate\Support\Facades\View;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use App\Services\ServiceSIMBG;
|
|
||||||
use App\Services\GoogleSheetService;
|
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@@ -64,7 +62,8 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
$query->whereHas('roles', function ($subQuery) use ($user) {
|
$query->whereHas('roles', function ($subQuery) use ($user) {
|
||||||
$subQuery->whereIn('roles.id', $user->roles->pluck('id'))
|
$subQuery->whereIn('roles.id', $user->roles->pluck('id'))
|
||||||
->where('role_menu.allow_show', 1);
|
->where('role_menu.allow_show', 1);
|
||||||
});
|
})
|
||||||
|
->orderBy('sort_order', 'asc');
|
||||||
}])
|
}])
|
||||||
->whereNull('parent_id') // Ambil hanya menu utama
|
->whereNull('parent_id') // Ambil hanya menu utama
|
||||||
->orderBy('sort_order', 'asc')
|
->orderBy('sort_order', 'asc')
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvi
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\RateLimiter;
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use App\Models\Menu;
|
||||||
|
|
||||||
class RouteServiceProvider extends ServiceProvider
|
class RouteServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@@ -17,7 +18,7 @@ class RouteServiceProvider extends ServiceProvider
|
|||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
public const HOME = '/home';
|
public const HOME = '/dashboards/bigdata';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define your route model bindings, pattern filters, and other route configuration.
|
* Define your route model bindings, pattern filters, and other route configuration.
|
||||||
|
|||||||
@@ -1,153 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services;
|
|
||||||
|
|
||||||
use Google_Client;
|
|
||||||
use Google_Service_Sheets;
|
|
||||||
|
|
||||||
class GoogleSheetService
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Create a new class instance.
|
|
||||||
*/
|
|
||||||
protected $client;
|
|
||||||
protected $service;
|
|
||||||
protected $spreadsheetID;
|
|
||||||
protected $service_sheets;
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->client = new Google_Client();
|
|
||||||
$this->client->setApplicationName("Sibedas Google Sheets API");
|
|
||||||
$this->client->setScopes([Google_Service_Sheets::SPREADSHEETS_READONLY]);
|
|
||||||
$this->client->setAuthConfig(storage_path("app/teak-banner-450003-s8-ea05661d9db0.json"));
|
|
||||||
$this->client->setAccessType("offline");
|
|
||||||
|
|
||||||
$this->service = new Google_Service_Sheets($this->client);
|
|
||||||
$this->spreadsheetID = env("SPREAD_SHEET_ID");
|
|
||||||
|
|
||||||
$this->service_sheets = new Google_Service_Sheets($this->client);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getSheetData($range){
|
|
||||||
$response = $this->service->spreadsheets_values->get($this->spreadsheetID, $range);
|
|
||||||
return $response->getValues();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getLastRowByColumn($column = "A")
|
|
||||||
{
|
|
||||||
try{
|
|
||||||
// Ambil spreadsheet
|
|
||||||
$spreadsheet = $this->service->spreadsheets->get($this->spreadsheetID);
|
|
||||||
$sheets = $spreadsheet->getSheets();
|
|
||||||
|
|
||||||
if (!empty($sheets)) {
|
|
||||||
// Ambil nama sheet pertama dengan benar
|
|
||||||
$firstSheetTitle = $sheets[0]->getProperties()->getTitle();
|
|
||||||
|
|
||||||
// ✅ Format range harus benar!
|
|
||||||
$range = "{$firstSheetTitle}!{$column}:{$column}";
|
|
||||||
|
|
||||||
// Ambil data dari kolom yang diminta
|
|
||||||
$response = $this->service->spreadsheets_values->get($this->spreadsheetID, $range);
|
|
||||||
$values = $response->getValues();
|
|
||||||
|
|
||||||
// Cek nilai terakhir yang tidak kosong
|
|
||||||
$lastRow = 0;
|
|
||||||
if (!empty($values)) {
|
|
||||||
foreach ($values as $index => $row) {
|
|
||||||
if (!empty($row[0])) { // Jika ada data, update lastRow
|
|
||||||
$lastRow = $index + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $lastRow;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}catch(\Exception $e){
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public function getHeader()
|
|
||||||
{
|
|
||||||
try{
|
|
||||||
$spreadsheet = $this->service->spreadsheets->get($this->spreadsheetID);
|
|
||||||
$sheets = $spreadsheet->getSheets();
|
|
||||||
|
|
||||||
// Ambil nama sheet pertama
|
|
||||||
$firstSheetTitle = $sheets[0]->getProperties()->getTitle();
|
|
||||||
|
|
||||||
// Ambil data dari baris pertama (header)
|
|
||||||
$range = "{$firstSheetTitle}!1:1";
|
|
||||||
$response = $this->service->spreadsheets_values->get($this->spreadsheetID, $range);
|
|
||||||
$values = $response->getValues();
|
|
||||||
|
|
||||||
// Kembalikan header (baris pertama)
|
|
||||||
return !empty($values) ? $values[0] : [];
|
|
||||||
}catch(\Exception $e){
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getLastColumn()
|
|
||||||
{
|
|
||||||
$spreadsheet = $this->service->spreadsheets->get($this->spreadsheetID);
|
|
||||||
$sheets = $spreadsheet->getSheets();
|
|
||||||
|
|
||||||
// Ambil nama sheet pertama
|
|
||||||
$firstSheetTitle = $sheets[0]->getProperties()->getTitle();
|
|
||||||
|
|
||||||
// Ambil baris pertama untuk mendapatkan jumlah kolom yang terisi
|
|
||||||
$range = "{$firstSheetTitle}!1:1";
|
|
||||||
$response = $this->service->spreadsheets_values->get($this->spreadsheetID, $range);
|
|
||||||
$values = $response->getValues();
|
|
||||||
|
|
||||||
// Hitung jumlah kolom yang memiliki nilai
|
|
||||||
return !empty($values) ? count(array_filter($values[0], fn($value) => $value !== "")) : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getSheetDataCollection($totalRow = 10){
|
|
||||||
try{
|
|
||||||
$spreadsheet = $this->service->spreadsheets->get($this->spreadsheetID);
|
|
||||||
$sheets = $spreadsheet->getSheets();
|
|
||||||
$firstSheetTitle = $sheets[0]->getProperties()->getTitle();
|
|
||||||
|
|
||||||
$header = $this->getHeader();
|
|
||||||
$header = array_map(function($columnHeader) {
|
|
||||||
// Trim spaces first, then replace non-alphanumeric characters with underscores
|
|
||||||
$columnHeader = trim($columnHeader);
|
|
||||||
return strtolower(preg_replace('/[^A-Za-z0-9_]/', '_', $columnHeader));
|
|
||||||
}, $header);
|
|
||||||
$range = "{$firstSheetTitle}!2:{$totalRow}";
|
|
||||||
|
|
||||||
$response = $this->service->spreadsheets_values->get($this->spreadsheetID, $range);
|
|
||||||
$values = $response->getValues();
|
|
||||||
|
|
||||||
$mappedData = [];
|
|
||||||
if (!empty($values)) {
|
|
||||||
foreach ($values as $row) {
|
|
||||||
$rowData = [];
|
|
||||||
foreach ($header as $index => $columnHeader) {
|
|
||||||
// Map header to the corresponding value from the row
|
|
||||||
$rowData[$columnHeader] = isset($row[$index]) ? $row[$index] : null;
|
|
||||||
}
|
|
||||||
$mappedData[] = $rowData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $mappedData;
|
|
||||||
}catch(\Exception $e){
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public function get_data_by_sheet($no_sheet = 1){
|
|
||||||
$spreadsheet = $this->service->spreadsheets->get($this->spreadsheetID);
|
|
||||||
$sheets = $spreadsheet->getSheets();
|
|
||||||
$sheetTitle = $sheets[$no_sheet]->getProperties()->getTitle();
|
|
||||||
$range = "{$sheetTitle}";
|
|
||||||
$response = $this->service->spreadsheets_values->get($this->spreadsheetID, $range);
|
|
||||||
$values = $response->getValues();
|
|
||||||
return!empty($values)? $values : [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services;
|
|
||||||
use App\Traits\GlobalApiResponse;
|
|
||||||
use GuzzleHttp\Client;
|
|
||||||
use Exception;
|
|
||||||
class ServiceClient
|
|
||||||
{
|
|
||||||
use GlobalApiResponse;
|
|
||||||
private $client;
|
|
||||||
private $baseUrl;
|
|
||||||
private $headers;
|
|
||||||
/**
|
|
||||||
* Create a new class instance.
|
|
||||||
*/
|
|
||||||
public function __construct($baseUrl = '', $headers = [])
|
|
||||||
{
|
|
||||||
$this->client = new Client();
|
|
||||||
$this->baseUrl = $baseUrl;
|
|
||||||
$this->headers = array_merge(
|
|
||||||
[
|
|
||||||
'Accept' => 'application/json',
|
|
||||||
'Content-Type' => 'application/json'
|
|
||||||
],
|
|
||||||
$headers
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function makeRequest($url, $method = 'GET', $body = null, $headers = [], $timeout = 14400){
|
|
||||||
try {
|
|
||||||
|
|
||||||
$headers = array_merge($this->headers, $headers);
|
|
||||||
|
|
||||||
$options = [
|
|
||||||
'headers' => $headers,
|
|
||||||
'timeout' => $timeout,
|
|
||||||
'connect_timeout' => 60
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($body) {
|
|
||||||
$options['json'] = $body; // Guzzle akan mengonversi array ke JSON
|
|
||||||
}
|
|
||||||
|
|
||||||
$response = $this->client->request($method, $this->baseUrl . $url, $options);
|
|
||||||
$responseBody = (string) $response->getBody();
|
|
||||||
|
|
||||||
if (!str_contains($response->getHeaderLine('Content-Type'), 'application/json')) {
|
|
||||||
\Log::error('Unexpected response format: ' . $responseBody);
|
|
||||||
return $this->resError('API response is not JSON');
|
|
||||||
}
|
|
||||||
|
|
||||||
$resultResponse = json_decode($responseBody, true, 512, JSON_THROW_ON_ERROR);
|
|
||||||
return $this->resSuccess($resultResponse);
|
|
||||||
} catch (\GuzzleHttp\Exception\ClientException $e) {
|
|
||||||
// Handle 4xx errors (e.g., 401 Unauthorized)
|
|
||||||
$responseBody = (string) $e->getResponse()->getBody();
|
|
||||||
$errorResponse = json_decode($responseBody, true);
|
|
||||||
|
|
||||||
if (isset($errorResponse['code']) && $errorResponse['code'] === 'token_not_valid') {
|
|
||||||
return $this->resError('Invalid token, please refresh your token.', $errorResponse, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->resError('Client error from API', $errorResponse, $e->getResponse()->getStatusCode());
|
|
||||||
} catch (\GuzzleHttp\Exception\ServerException $e) {
|
|
||||||
// Handle 5xx errors (e.g., Internal Server Error)
|
|
||||||
return $this->resError('Server error from API', (string) $e->getResponse()->getBody(), 500);
|
|
||||||
} catch (\GuzzleHttp\Exception\RequestException $e) {
|
|
||||||
// Handle network errors (e.g., timeout, connection issues)
|
|
||||||
return $this->resError('Network error: ' . $e->getMessage(), null, 503);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
// Handle unexpected errors
|
|
||||||
return $this->resError('Unexpected error: ' . $e->getMessage(), null, 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fungsi untuk melakukan permintaan GET
|
|
||||||
public function get($url, $headers = [])
|
|
||||||
{
|
|
||||||
return $this->makeRequest($url, 'GET', null, $headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fungsi untuk melakukan permintaan POST
|
|
||||||
public function post($url, $body, $headers = [])
|
|
||||||
{
|
|
||||||
return $this->makeRequest($url, 'POST', $body, $headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fungsi untuk melakukan permintaan PUT
|
|
||||||
public function put($url, $body, $headers = [])
|
|
||||||
{
|
|
||||||
return $this->makeRequest($url, 'PUT', $body, $headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fungsi untuk melakukan permintaan DELETE
|
|
||||||
public function delete($url, $headers = [])
|
|
||||||
{
|
|
||||||
return $this->makeRequest($url, 'DELETE', null, $headers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,6 +13,9 @@ use Exception;
|
|||||||
use Google\Client as Google_Client;
|
use Google\Client as Google_Client;
|
||||||
use Google\Service\Sheets as Google_Service_Sheets;
|
use Google\Service\Sheets as Google_Service_Sheets;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use App\Models\PbgTask;
|
||||||
class ServiceGoogleSheet
|
class ServiceGoogleSheet
|
||||||
{
|
{
|
||||||
protected $client;
|
protected $client;
|
||||||
@@ -36,8 +39,8 @@ class ServiceGoogleSheet
|
|||||||
|
|
||||||
public function run_service(){
|
public function run_service(){
|
||||||
try{
|
try{
|
||||||
$this->sync_big_data();
|
|
||||||
$this->sync_google_sheet_data();
|
$this->sync_google_sheet_data();
|
||||||
|
$this->sync_pbg_task_payments();
|
||||||
}catch(Exception $e){
|
}catch(Exception $e){
|
||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
@@ -135,7 +138,16 @@ class ServiceGoogleSheet
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Count occurrences of each no_registrasi
|
// Count occurrences of each no_registrasi
|
||||||
$registrasiCounts = array_count_values(array_column($mapUpsert, 'no_registrasi'));
|
// Filter out null values before counting to avoid array_count_values error
|
||||||
|
$registrationNumbers = array_filter(array_column($mapUpsert, 'no_registrasi'), function($value) {
|
||||||
|
// Ensure only string and integer values are counted
|
||||||
|
return $value !== null && $value !== '' && (is_string($value) || is_int($value));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Additional safety check: convert all values to strings
|
||||||
|
$registrationNumbers = array_map('strval', $registrationNumbers);
|
||||||
|
|
||||||
|
$registrasiCounts = array_count_values($registrationNumbers);
|
||||||
|
|
||||||
// Filter duplicates (those appearing more than once)
|
// Filter duplicates (those appearing more than once)
|
||||||
$duplicates = array_filter($registrasiCounts, function ($count) {
|
$duplicates = array_filter($registrasiCounts, function ($count) {
|
||||||
@@ -146,8 +158,14 @@ class ServiceGoogleSheet
|
|||||||
Log::warning("Duplicate no_registrasi found", ['duplicates' => array_keys($duplicates)]);
|
Log::warning("Duplicate no_registrasi found", ['duplicates' => array_keys($duplicates)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove duplicates before upsert
|
// Remove duplicates before upsert - filter out entries with null no_registrasi
|
||||||
$mapUpsert = collect($mapUpsert)->unique('no_registrasi')->values()->all();
|
$mapUpsert = collect($mapUpsert)
|
||||||
|
->filter(function($item) {
|
||||||
|
return !empty($item['no_registrasi']);
|
||||||
|
})
|
||||||
|
->unique('no_registrasi')
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
$batchSize = 1000;
|
$batchSize = 1000;
|
||||||
$chunks = array_chunk($mapUpsert, $batchSize);
|
$chunks = array_chunk($mapUpsert, $batchSize);
|
||||||
@@ -200,9 +218,19 @@ class ServiceGoogleSheet
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ($data_setting_result as $key => $value) {
|
foreach ($data_setting_result as $key => $value) {
|
||||||
DataSetting::updateOrInsert(
|
// Ensure value is not null before saving to database
|
||||||
|
$processedValue = 0; // Default to 0 instead of null
|
||||||
|
if ($value !== null && $value !== '') {
|
||||||
|
if (strpos($key, '_COUNT') !== false) {
|
||||||
|
$processedValue = $this->convertToInteger($value) ?? 0;
|
||||||
|
} else {
|
||||||
|
$processedValue = $this->convertToDecimal($value) ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DataSetting::updateOrCreate(
|
||||||
["key" => $key], // Find by key
|
["key" => $key], // Find by key
|
||||||
["value" => $value] // Update or insert value
|
["value" => $processedValue] // Update or insert value
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +251,6 @@ class ServiceGoogleSheet
|
|||||||
]);
|
]);
|
||||||
try {
|
try {
|
||||||
$sections = [
|
$sections = [
|
||||||
'TARGET_PAD' => "TARGET PAD 2024",
|
|
||||||
'KEKURANGAN_POTENSI' => "DEVIASI TARGET DENGAN POTENSI TOTAL BERKAS",
|
'KEKURANGAN_POTENSI' => "DEVIASI TARGET DENGAN POTENSI TOTAL BERKAS",
|
||||||
'TOTAL_POTENSI_BERKAS' => "•TOTAL BERKAS 2025",
|
'TOTAL_POTENSI_BERKAS' => "•TOTAL BERKAS 2025",
|
||||||
'BELUM_TERVERIFIKASI' => "•BERKAS AKTUAL BELUM TERVERIFIKASI (POTENSI):",
|
'BELUM_TERVERIFIKASI' => "•BERKAS AKTUAL BELUM TERVERIFIKASI (POTENSI):",
|
||||||
@@ -238,7 +265,7 @@ class ServiceGoogleSheet
|
|||||||
$result = [];
|
$result = [];
|
||||||
|
|
||||||
foreach ($sections as $key => $identifier) {
|
foreach ($sections as $key => $identifier) {
|
||||||
$values = $this->get_values_from_section($identifier, [10, 11], 2);
|
$values = $this->get_values_from_section($identifier, [10, 11], 9);
|
||||||
|
|
||||||
if (!empty($values)) {
|
if (!empty($values)) {
|
||||||
$result[$key] = [
|
$result[$key] = [
|
||||||
@@ -251,7 +278,8 @@ class ServiceGoogleSheet
|
|||||||
|
|
||||||
BigdataResume::create([
|
BigdataResume::create([
|
||||||
'import_datasource_id' => $import_datasource->id,
|
'import_datasource_id' => $import_datasource->id,
|
||||||
'year' => 'leader',
|
'year' => date('Y'),
|
||||||
|
'resume_type' => 'leader',
|
||||||
// USAHA
|
// USAHA
|
||||||
'business_count' => $this->convertToInteger($result['USAHA']['total'] ?? null) ?? 0,
|
'business_count' => $this->convertToInteger($result['USAHA']['total'] ?? null) ?? 0,
|
||||||
'business_sum' => $this->convertToDecimal($result['USAHA']['nominal'] ?? null) ?? 0,
|
'business_sum' => $this->convertToDecimal($result['USAHA']['nominal'] ?? null) ?? 0,
|
||||||
@@ -278,12 +306,16 @@ class ServiceGoogleSheet
|
|||||||
'process_in_technical_office_sum' => $this->convertToDecimal($result['PROSES_DINAS_TEKNIS']['nominal'] ?? null) ?? 0,
|
'process_in_technical_office_sum' => $this->convertToDecimal($result['PROSES_DINAS_TEKNIS']['nominal'] ?? null) ?? 0,
|
||||||
// TATA RUANG
|
// TATA RUANG
|
||||||
'spatial_count' => $this->getSpatialPlanningWithCalculationCount(),
|
'spatial_count' => $this->getSpatialPlanningWithCalculationCount(),
|
||||||
'spatial_sum' => $this->getSpatialPlanningCalculationSum()
|
'spatial_sum' => $this->getSpatialPlanningCalculationSum(),
|
||||||
|
'business_rab_count' => 0,
|
||||||
|
'business_krk_count' => 0,
|
||||||
|
'non_business_rab_count' => 0,
|
||||||
|
'non_business_krk_count' => 0,
|
||||||
|
'non_business_dlh_count' => 0,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Save data settings
|
// Save data settings
|
||||||
$dataSettings = [
|
$dataSettings = [
|
||||||
'TARGET_PAD' => $result['TARGET_PAD']['nominal'] ?? null,
|
|
||||||
'KEKURANGAN_POTENSI' => $result['KEKURANGAN_POTENSI']['nominal'] ?? null,
|
'KEKURANGAN_POTENSI' => $result['KEKURANGAN_POTENSI']['nominal'] ?? null,
|
||||||
'REALISASI_TERBIT_PBG_COUNT' => $result['REALISASI_TERBIT_PBG']['total'] ?? null,
|
'REALISASI_TERBIT_PBG_COUNT' => $result['REALISASI_TERBIT_PBG']['total'] ?? null,
|
||||||
'REALISASI_TERBIT_PBG_SUM' => $result['REALISASI_TERBIT_PBG']['nominal'] ?? null,
|
'REALISASI_TERBIT_PBG_SUM' => $result['REALISASI_TERBIT_PBG']['nominal'] ?? null,
|
||||||
@@ -294,9 +326,20 @@ class ServiceGoogleSheet
|
|||||||
];
|
];
|
||||||
|
|
||||||
foreach ($dataSettings as $key => $value) {
|
foreach ($dataSettings as $key => $value) {
|
||||||
DataSetting::updateOrInsert(
|
// Ensure value is not null before saving to database
|
||||||
|
$processedValue = 0; // Default to 0 instead of null
|
||||||
|
if ($value !== null && $value !== '') {
|
||||||
|
// Try to convert to appropriate type based on key name
|
||||||
|
if (strpos($key, '_COUNT') !== false) {
|
||||||
|
$processedValue = $this->convertToInteger($value) ?? 0;
|
||||||
|
} else {
|
||||||
|
$processedValue = $this->convertToDecimal($value) ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DataSetting::updateOrCreate(
|
||||||
['key' => $key],
|
['key' => $key],
|
||||||
['value' => $this->convertToInteger($value) ?? 0]
|
['value' => $processedValue]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,46 +363,498 @@ class ServiceGoogleSheet
|
|||||||
|
|
||||||
public function get_big_resume_data(){
|
public function get_big_resume_data(){
|
||||||
try {
|
try {
|
||||||
$sheet_big_data = $this->get_data_by_sheet();
|
$sections = [
|
||||||
$data_setting_result = []; // Initialize result storage
|
'KEKURANGAN_POTENSI' => "DEVIASI TARGET DENGAN POTENSI TOTAL BERKAS",
|
||||||
|
'TOTAL_POTENSI_BERKAS' => "•TOTAL BERKAS 2025",
|
||||||
|
'BELUM_TERVERIFIKASI' => "•BERKAS AKTUAL BELUM TERVERIFIKASI (POTENSI):",
|
||||||
|
'TERVERIFIKASI' => "•BERKAS AKTUAL TERVERIFIKASI DINAS TEKNIS 2025:",
|
||||||
|
'NON_USAHA' => "•NON USAHA: HUNIAN, SOSBUD, KEAGAMAAN",
|
||||||
|
'USAHA' => "•USAHA: USAHA, CAMPURAN, KOLEKTIF, PRASARANA",
|
||||||
|
'PROSES_DINAS_TEKNIS' => "•TERPROSES DI DPUTR: belum selesai rekomtek'",
|
||||||
|
'WAITING_KLIK_DPMPTSP' => "•TERPROSES DI PTSP: Pengiriman SKRD/ Validasi di PTSP",
|
||||||
|
'REALISASI_TERBIT_PBG' => "•BERKAS YANG TERBIT PBG 2025:"
|
||||||
|
];
|
||||||
|
|
||||||
$found_section = null; // Track which section is found
|
$result = [];
|
||||||
|
|
||||||
foreach ($sheet_big_data as $row) {
|
foreach ($sections as $key => $identifier) {
|
||||||
// Check for section headers
|
$values = $this->get_values_from_section($identifier, [10, 11], 9);
|
||||||
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 (!empty($values)) {
|
||||||
if ($found_section && isset($row[0]) && trim($row[0]) === "Grand Total") {
|
$result[$key] = [
|
||||||
if ($found_section === "MENUNGGU_KLIK_DPMPTSP") {
|
'identifier' => $identifier,
|
||||||
$data_setting_result["MENUNGGU_KLIK_DPMPTSP_COUNT"] = $this->convertToInteger($row[2]) ?? null;
|
'total' => $values[0] ?? null, // index 0 untuk total/jumlah
|
||||||
$data_setting_result["MENUNGGU_KLIK_DPMPTSP_SUM"] = $this->convertToDecimal($row[3]) ?? null;
|
'nominal' => $values[1] ?? null // index 1 untuk nominal
|
||||||
} elseif ($found_section === "REALISASI_TERBIT_PBG") {
|
];
|
||||||
$data_setting_result["REALISASI_TERBIT_PBG_COUNT"] = $this->convertToInteger($row[2]) ?? null;
|
|
||||||
$data_setting_result["REALISASI_TERBIT_PBG_SUM"] = $this->convertToDecimal($row[4]) ?? null;
|
|
||||||
} elseif ($found_section === "PROSES_DINAS_TEKNIS") {
|
|
||||||
$data_setting_result["PROSES_DINAS_TEKNIS_COUNT"] = $this->convertToInteger($row[2]) ?? null;
|
|
||||||
$data_setting_result["PROSES_DINAS_TEKNIS_SUM"] = $this->convertToDecimal($row[3]) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset section tracking after capturing "Grand Total"
|
|
||||||
$found_section = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $data_setting_result;
|
|
||||||
|
// Save data settings
|
||||||
|
$dataSettings = [
|
||||||
|
'KEKURANGAN_POTENSI' => $this->convertToDecimal($result['KEKURANGAN_POTENSI']['nominal']) ?? 0,
|
||||||
|
'REALISASI_TERBIT_PBG_COUNT' => $this->convertToInteger($result['REALISASI_TERBIT_PBG']['total']) ?? 0,
|
||||||
|
'REALISASI_TERBIT_PBG_SUM' => $this->convertToDecimal($result['REALISASI_TERBIT_PBG']['nominal']) ?? 0,
|
||||||
|
'MENUNGGU_KLIK_DPMPTSP_COUNT' => $this->convertToInteger($result['WAITING_KLIK_DPMPTSP']['total']) ?? 0,
|
||||||
|
'MENUNGGU_KLIK_DPMPTSP_SUM' => $this->convertToDecimal($result['WAITING_KLIK_DPMPTSP']['nominal']) ?? 0,
|
||||||
|
'PROSES_DINAS_TEKNIS_COUNT' => $this->convertToInteger($result['PROSES_DINAS_TEKNIS']['total']) ?? 0,
|
||||||
|
'PROSES_DINAS_TEKNIS_SUM' => $this->convertToDecimal($result['PROSES_DINAS_TEKNIS']['nominal']) ?? 0,
|
||||||
|
'SPATIAL_PLANNING_COUNT' => $this->getSpatialPlanningWithCalculationCount(),
|
||||||
|
'SPATIAL_PLANNING_SUM' => $this->getSpatialPlanningCalculationSum()
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($dataSettings as $key => $value) {
|
||||||
|
// Ensure value is not null before saving to database
|
||||||
|
$processedValue = 0; // Default to 0 instead of null
|
||||||
|
if ($value !== null && $value !== '') {
|
||||||
|
// Try to convert to appropriate type based on key name
|
||||||
|
if (strpos($key, '_COUNT') !== false) {
|
||||||
|
$processedValue = $this->convertToInteger($value) ?? 0;
|
||||||
|
} else {
|
||||||
|
$processedValue = $this->convertToDecimal($value) ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DataSetting::updateOrCreate(
|
||||||
|
['key' => $key],
|
||||||
|
['value' => $processedValue]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $dataSettings;
|
||||||
}catch(Exception $exception){
|
}catch(Exception $exception){
|
||||||
Log::error("Error getting big resume data", ['error' => $exception->getMessage()]);
|
Log::error("Error getting big resume data", ['error' => $exception->getMessage()]);
|
||||||
throw $exception;
|
throw $exception;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function get_data_by_sheet($no_sheet = 1){
|
/**
|
||||||
|
* Get sheet data where the first row is treated as headers, and subsequent rows
|
||||||
|
* are returned as associative arrays keyed by header names. Supports selecting
|
||||||
|
* a contiguous column range plus additional specific columns.
|
||||||
|
*
|
||||||
|
* Example: get_sheet_data_with_headers_range('Data', 'A', 'AX', ['BX'])
|
||||||
|
*
|
||||||
|
* @param string $sheet_name
|
||||||
|
* @param string $start_column_letter Inclusive start column letter (e.g., 'A')
|
||||||
|
* @param string $end_column_letter Inclusive end column letter (e.g., 'AX')
|
||||||
|
* @param array $extra_column_letters Additional discrete column letters (e.g., ['BX'])
|
||||||
|
* @return array{headers: array<int,string>, data: array<int,array<string,?string>>, selected_columns: array<int,int>}
|
||||||
|
*/
|
||||||
|
public function get_sheet_data_with_headers_range(string $sheet_name, string $start_column_letter, string $end_column_letter, array $extra_column_letters = [])
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$sheet_data = $this->get_data_by_sheet_name($sheet_name);
|
||||||
|
|
||||||
|
if (empty($sheet_data)) {
|
||||||
|
Log::warning("No data found in sheet", ['sheet_name' => $sheet_name]);
|
||||||
|
return [
|
||||||
|
'headers' => [],
|
||||||
|
'data' => [],
|
||||||
|
'selected_columns' => []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build selected column indices: range A..AX and extras like BX
|
||||||
|
$selected_indices = $this->expandColumnRangeToIndices($start_column_letter, $end_column_letter);
|
||||||
|
foreach ($extra_column_letters as $letter) {
|
||||||
|
$selected_indices[] = $this->columnLetterToIndex($letter);
|
||||||
|
}
|
||||||
|
// Ensure unique and sorted
|
||||||
|
$selected_indices = array_values(array_unique($selected_indices));
|
||||||
|
sort($selected_indices);
|
||||||
|
|
||||||
|
$result = [
|
||||||
|
'headers' => [],
|
||||||
|
'data' => [],
|
||||||
|
'selected_columns' => $selected_indices
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($sheet_data as $row_index => $row) {
|
||||||
|
if (!is_array($row)) continue;
|
||||||
|
|
||||||
|
if ($row_index === 0) {
|
||||||
|
// First row contains headers (by selected columns)
|
||||||
|
foreach ($selected_indices as $col_index) {
|
||||||
|
$raw = isset($row[$col_index]) ? trim((string) $row[$col_index]) : '';
|
||||||
|
// Fallback to column letter if empty
|
||||||
|
$header = $raw !== '' ? $raw : $this->indexToColumnLetter($col_index);
|
||||||
|
$result['headers'][$col_index] = $this->normalizeHeader($header);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$row_assoc = [];
|
||||||
|
$has_data = false;
|
||||||
|
foreach ($selected_indices as $col_index) {
|
||||||
|
$header = $result['headers'][$col_index] ?? $this->normalizeHeader($this->indexToColumnLetter($col_index));
|
||||||
|
$value = isset($row[$col_index]) ? trim((string) $row[$col_index]) : '';
|
||||||
|
$row_assoc[$header] = ($value === '') ? null : $value;
|
||||||
|
if ($value !== '') {
|
||||||
|
$has_data = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($has_data) {
|
||||||
|
$result['data'][] = $row_assoc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error("Error getting sheet data with headers", [
|
||||||
|
'sheet_name' => $sheet_name,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a column letter (e.g., 'A', 'Z', 'AA', 'AX', 'BX') to a zero-based index (A=0)
|
||||||
|
*/
|
||||||
|
private function columnLetterToIndex(string $letter): int
|
||||||
|
{
|
||||||
|
$letter = strtoupper(trim($letter));
|
||||||
|
$length = strlen($letter);
|
||||||
|
$index = 0;
|
||||||
|
for ($i = 0; $i < $length; $i++) {
|
||||||
|
$index = $index * 26 + (ord($letter[$i]) - ord('A') + 1);
|
||||||
|
}
|
||||||
|
return $index - 1; // zero-based
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert zero-based column index to column letter (0='A')
|
||||||
|
*/
|
||||||
|
private function indexToColumnLetter(int $index): string
|
||||||
|
{
|
||||||
|
$index += 1; // make 1-based for calculation
|
||||||
|
$letters = '';
|
||||||
|
while ($index > 0) {
|
||||||
|
$mod = ($index - 1) % 26;
|
||||||
|
$letters = chr($mod + ord('A')) . $letters;
|
||||||
|
$index = intdiv($index - 1, 26);
|
||||||
|
}
|
||||||
|
return $letters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand a column range like 'A'..'AX' to zero-based indices array
|
||||||
|
*/
|
||||||
|
private function expandColumnRangeToIndices(string $start_letter, string $end_letter): array
|
||||||
|
{
|
||||||
|
$start = $this->columnLetterToIndex($start_letter);
|
||||||
|
$end = $this->columnLetterToIndex($end_letter);
|
||||||
|
if ($start > $end) {
|
||||||
|
[$start, $end] = [$end, $start];
|
||||||
|
}
|
||||||
|
return range($start, $end);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize header: trim, lowercase, replace spaces with underscore, remove non-alnum/underscore
|
||||||
|
*/
|
||||||
|
private function normalizeHeader(string $header): string
|
||||||
|
{
|
||||||
|
$header = trim($header);
|
||||||
|
$header = strtolower($header);
|
||||||
|
$header = preg_replace('/\s+/', '_', $header);
|
||||||
|
$header = preg_replace('/[^a-z0-9_]/', '', $header);
|
||||||
|
return $header;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sync_pbg_task_payments(){
|
||||||
|
try {
|
||||||
|
$sheetName = 'Data';
|
||||||
|
$startLetter = 'A';
|
||||||
|
$endLetter = 'AX';
|
||||||
|
$extraLetters = ['BF'];
|
||||||
|
|
||||||
|
// Fetch header row only (row 1) across A..BF and build header/selection
|
||||||
|
$headerRange = sprintf('%s!%s1:%s1', $sheetName, $startLetter, 'BF');
|
||||||
|
$headerResponse = $this->service->spreadsheets_values->get($this->spreadsheetID, $headerRange);
|
||||||
|
$headerRow = $headerResponse->getValues()[0] ?? [];
|
||||||
|
if (empty($headerRow)) {
|
||||||
|
Log::warning("No header row found in sheet", ['sheet' => $sheetName]);
|
||||||
|
return ['success' => false, 'message' => 'No header row found'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selected indices: A..AX plus BF
|
||||||
|
$selected_indices = $this->expandColumnRangeToIndices($startLetter, $endLetter);
|
||||||
|
foreach ($extraLetters as $letter) {
|
||||||
|
$selected_indices[] = $this->columnLetterToIndex($letter);
|
||||||
|
}
|
||||||
|
$selected_indices = array_values(array_unique($selected_indices));
|
||||||
|
sort($selected_indices);
|
||||||
|
|
||||||
|
// Build normalized headers map (index -> header)
|
||||||
|
$headers = [];
|
||||||
|
foreach ($selected_indices as $colIdx) {
|
||||||
|
$raw = isset($headerRow[$colIdx]) ? trim((string) $headerRow[$colIdx]) : '';
|
||||||
|
$header = $raw !== '' ? $raw : $this->indexToColumnLetter($colIdx);
|
||||||
|
$headers[$colIdx] = $this->normalizeHeader($header);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log environment and header diagnostics
|
||||||
|
Log::info('sync_pbg_task_payments: diagnostics', [
|
||||||
|
'spreadsheet_id' => $this->spreadsheetID,
|
||||||
|
'sheet' => $sheetName,
|
||||||
|
'selected_indices_count' => count($selected_indices)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Validate that expected headers exist after normalization before truncating table
|
||||||
|
$expectedHeaders = [
|
||||||
|
'no','jenis_konsultasi','no_registrasi','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','krkkkpr','no_krk','lh','ska','keterangan',
|
||||||
|
'helpdesk','pj','operator_pbg','kepemilikan','potensi_taru','validasi_dinas','kategori_retribusi',
|
||||||
|
'no_urut_ba_tpt_20250001','tanggal_ba_tpt','no_urut_ba_tpa','tanggal_ba_tpa','no_urut_skrd_20250001',
|
||||||
|
'tanggal_skrd','ptsp','selesai_terbit','tanggal_pembayaran_yyyymmdd','format_sts','tahun_terbit',
|
||||||
|
'tahun_berjalan','kelurahan','kecamatan','lb','tb','jlb','unit','usulan_retribusi',
|
||||||
|
'nilai_retribusi_keseluruhan_simbg','nilai_retribusi_keseluruhan_pad','denda','usaha__non_usaha'
|
||||||
|
];
|
||||||
|
|
||||||
|
$normalizedHeaderValues = array_values($headers);
|
||||||
|
$overlap = array_intersect($expectedHeaders, $normalizedHeaderValues);
|
||||||
|
|
||||||
|
if (count($overlap) < 10) { // too few matching headers, likely wrong sheet or headers changed
|
||||||
|
Log::error('sync_pbg_task_payments: header mismatch detected', [
|
||||||
|
'expected_sample' => array_slice($expectedHeaders, 0, 15),
|
||||||
|
'found_sample' => array_slice($normalizedHeaderValues, 0, 30),
|
||||||
|
'match_count' => count($overlap)
|
||||||
|
]);
|
||||||
|
return ['success' => false, 'message' => 'Header mismatch - aborting to prevent null inserts'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate table and restart identity (only after header validation)
|
||||||
|
Schema::disableForeignKeyConstraints();
|
||||||
|
DB::table('pbg_task_payments')->truncate();
|
||||||
|
Schema::enableForeignKeyConstraints();
|
||||||
|
|
||||||
|
// Map header -> db column
|
||||||
|
$map = [
|
||||||
|
'no' => 'row_no',
|
||||||
|
'jenis_konsultasi' => 'consultation_type',
|
||||||
|
'no_registrasi' => 'source_registration_number',
|
||||||
|
'nama_pemilik' => 'owner_name',
|
||||||
|
'lokasi_bg' => 'building_location',
|
||||||
|
'fungsi_bg' => 'building_function',
|
||||||
|
'nama_bangunan' => 'building_name',
|
||||||
|
'tgl_permohonan' => 'application_date_raw',
|
||||||
|
'status_verifikasi' => 'verification_status',
|
||||||
|
'status_permohonan' => 'application_status',
|
||||||
|
'alamat_pemilik' => 'owner_address',
|
||||||
|
'no_hp' => 'owner_phone',
|
||||||
|
'email' => 'owner_email',
|
||||||
|
'tanggal_catatan' => 'note_date_raw',
|
||||||
|
'catatan_kekurangan_dokumen' => 'document_shortage_note',
|
||||||
|
'gambar' => 'image_url',
|
||||||
|
'krkkkpr' => 'krk_kkpr',
|
||||||
|
'no_krk' => 'krk_number',
|
||||||
|
'lh' => 'lh',
|
||||||
|
'ska' => 'ska',
|
||||||
|
'keterangan' => 'remarks',
|
||||||
|
'helpdesk' => 'helpdesk',
|
||||||
|
'pj' => 'person_in_charge',
|
||||||
|
'operator_pbg' => 'pbg_operator',
|
||||||
|
'kepemilikan' => 'ownership',
|
||||||
|
'potensi_taru' => 'taru_potential',
|
||||||
|
'validasi_dinas' => 'agency_validation',
|
||||||
|
'kategori_retribusi' => 'retribution_category',
|
||||||
|
'no_urut_ba_tpt_20250001' => 'ba_tpt_number',
|
||||||
|
'tanggal_ba_tpt' => 'ba_tpt_date_raw',
|
||||||
|
'no_urut_ba_tpa' => 'ba_tpa_number',
|
||||||
|
'tanggal_ba_tpa' => 'ba_tpa_date_raw',
|
||||||
|
'no_urut_skrd_20250001' => 'skrd_number',
|
||||||
|
'tanggal_skrd' => 'skrd_date_raw',
|
||||||
|
'ptsp' => 'ptsp_status',
|
||||||
|
'selesai_terbit' => 'issued_status',
|
||||||
|
'tanggal_pembayaran_yyyymmdd' => 'payment_date_raw',
|
||||||
|
'format_sts' => 'sts_format',
|
||||||
|
'tahun_terbit' => 'issuance_year',
|
||||||
|
'tahun_berjalan' => 'current_year',
|
||||||
|
'kelurahan' => 'village',
|
||||||
|
'kecamatan' => 'district',
|
||||||
|
'lb' => 'building_area',
|
||||||
|
'tb' => 'building_height',
|
||||||
|
'jlb' => 'floor_count',
|
||||||
|
'unit' => 'unit_count',
|
||||||
|
'usulan_retribusi' => 'proposed_retribution',
|
||||||
|
'nilai_retribusi_keseluruhan_simbg' => 'retribution_total_simbg',
|
||||||
|
'nilai_retribusi_keseluruhan_pad' => 'retribution_total_pad',
|
||||||
|
'denda' => 'penalty_amount',
|
||||||
|
'usaha__non_usaha' => 'business_category',
|
||||||
|
];
|
||||||
|
|
||||||
|
// We'll build registration map lazily per chunk to limit memory
|
||||||
|
$regToTask = [];
|
||||||
|
|
||||||
|
// Build and insert in small batches to avoid high memory usage
|
||||||
|
$batch = [];
|
||||||
|
$inserted = 0;
|
||||||
|
// Stream rows in chunks from API to avoid loading full sheet
|
||||||
|
$rowStart = 2; // data starts from row 2
|
||||||
|
$chunkRowSize = 1000; // number of rows per chunk
|
||||||
|
$inserted = 0;
|
||||||
|
while (true) {
|
||||||
|
$rowEnd = $rowStart + $chunkRowSize - 1;
|
||||||
|
$range = sprintf('%s!%s%d:%s%d', $sheetName, $startLetter, $rowStart, 'BF', $rowEnd);
|
||||||
|
$resp = $this->service->spreadsheets_values->get($this->spreadsheetID, $range);
|
||||||
|
$values = $resp->getValues() ?? [];
|
||||||
|
|
||||||
|
|
||||||
|
if (empty($values)) {
|
||||||
|
break; // no more rows
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Chunk fetched', [
|
||||||
|
'rowStart' => $rowStart,
|
||||||
|
'rowEnd' => $rowEnd,
|
||||||
|
'count' => count($values)
|
||||||
|
]);
|
||||||
|
// Preload registration map for this chunk
|
||||||
|
$chunkRegs = [];
|
||||||
|
foreach ($values as $row) {
|
||||||
|
foreach ($selected_indices as $colIdx) {
|
||||||
|
// find normalized header for this index
|
||||||
|
$h = $headers[$colIdx] ?? null;
|
||||||
|
if ($h === 'no_registrasi') {
|
||||||
|
$val = isset($row[$colIdx]) ? trim((string) $row[$colIdx]) : '';
|
||||||
|
if ($val !== '') { $chunkRegs[$val] = true; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($chunkRegs)) {
|
||||||
|
$keys = array_keys($chunkRegs);
|
||||||
|
$tasks = PbgTask::whereIn('registration_number', $keys)->get(['id','uuid','registration_number']);
|
||||||
|
foreach ($tasks as $task) {
|
||||||
|
$regToTask[trim($task->registration_number)] = ['id' => $task->id, 'uuid' => $task->uuid];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build and insert this chunk
|
||||||
|
$batch = [];
|
||||||
|
foreach ($values as $rowIndex => $row) {
|
||||||
|
$record = [
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Map row values by headers
|
||||||
|
$rowByHeader = [];
|
||||||
|
foreach ($selected_indices as $colIdx) {
|
||||||
|
$h = $headers[$colIdx] ?? null;
|
||||||
|
if ($h === null) continue;
|
||||||
|
$rowByHeader[$h] = isset($row[$colIdx]) ? trim((string) $row[$colIdx]) : null;
|
||||||
|
if ($rowByHeader[$h] === '') $rowByHeader[$h] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log first non-empty row mapping for diagnostics
|
||||||
|
if ($rowIndex === 0) {
|
||||||
|
$nonEmptySample = [];
|
||||||
|
foreach ($rowByHeader as $k => $v) {
|
||||||
|
if ($v !== null && count($nonEmptySample) < 10) { $nonEmptySample[$k] = $v; }
|
||||||
|
}
|
||||||
|
Log::info('sync_pbg_task_payments: first row sample after normalization', [
|
||||||
|
'sample' => $nonEmptySample
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if this row looks like a header row
|
||||||
|
$headerCheckKeys = ['no','jenis_konsultasi','no_registrasi'];
|
||||||
|
$headerMatches = 0;
|
||||||
|
foreach ($headerCheckKeys as $hk) {
|
||||||
|
if (!array_key_exists($hk, $rowByHeader)) { continue; }
|
||||||
|
$val = $rowByHeader[$hk];
|
||||||
|
if ($val === null) { continue; }
|
||||||
|
if ($this->normalizeHeader($val) === $hk) {
|
||||||
|
$headerMatches++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($headerMatches >= 2) {
|
||||||
|
continue; // looks like a repeated header row, skip
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if the entire row is empty (no values)
|
||||||
|
$hasAnyData = false;
|
||||||
|
foreach ($rowByHeader as $v) {
|
||||||
|
if ($v !== null && $v !== '') { $hasAnyData = true; break; }
|
||||||
|
}
|
||||||
|
if (!$hasAnyData) { continue; }
|
||||||
|
|
||||||
|
foreach ($map as $header => $column) {
|
||||||
|
$value = $rowByHeader[$header] ?? null;
|
||||||
|
|
||||||
|
switch ($column) {
|
||||||
|
case 'row_no':
|
||||||
|
case 'floor_count':
|
||||||
|
case 'unit_count':
|
||||||
|
case 'issuance_year':
|
||||||
|
case 'current_year':
|
||||||
|
$record[$column] = ($value === null || $value === '') ? null : (int) $value;
|
||||||
|
break;
|
||||||
|
case 'application_date_raw':
|
||||||
|
case 'note_date_raw':
|
||||||
|
case 'ba_tpt_date_raw':
|
||||||
|
case 'ba_tpa_date_raw':
|
||||||
|
case 'skrd_date_raw':
|
||||||
|
case 'payment_date_raw':
|
||||||
|
$record[$column] = $this->convertToDate($value);
|
||||||
|
break;
|
||||||
|
case 'building_area':
|
||||||
|
case 'building_height':
|
||||||
|
case 'proposed_retribution':
|
||||||
|
case 'retribution_total_simbg':
|
||||||
|
case 'retribution_total_pad':
|
||||||
|
case 'penalty_amount':
|
||||||
|
$record[$column] = $this->convertToDecimal($value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (is_string($value)) { $value = trim($value); }
|
||||||
|
$record[$column] = ($value === '' ? null : $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final trim pass
|
||||||
|
foreach ($record as $k => $v) {
|
||||||
|
if (is_string($v)) {
|
||||||
|
$t = trim($v);
|
||||||
|
$record[$k] = ($t === '') ? null : $t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve relation
|
||||||
|
$sourceReg = $rowByHeader['no_registrasi'] ?? null;
|
||||||
|
if (is_string($sourceReg)) { $sourceReg = trim($sourceReg); }
|
||||||
|
if (!empty($sourceReg) && isset($regToTask[$sourceReg])) {
|
||||||
|
$record['pbg_task_id'] = $regToTask[$sourceReg]['id'];
|
||||||
|
$record['pbg_task_uid'] = $regToTask[$sourceReg]['uuid'];
|
||||||
|
} else {
|
||||||
|
$record['pbg_task_id'] = null;
|
||||||
|
$record['pbg_task_uid'] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$batch[] = $record;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($batch)) {
|
||||||
|
\App\Models\PbgTaskPayment::insert($batch);
|
||||||
|
$inserted += count($batch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// next chunk
|
||||||
|
$rowStart = $rowEnd + 1;
|
||||||
|
if (function_exists('gc_collect_cycles')) { gc_collect_cycles(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('PBG Task Payments reloaded from sheet', ['inserted' => $inserted]);
|
||||||
|
|
||||||
|
return ['success' => true, 'inserted' => $inserted];
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error("Error syncing PBG task payments", ['error' => $e->getMessage()]);
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function get_data_by_sheet($no_sheet = 8){
|
||||||
$spreadsheet = $this->service->spreadsheets->get($this->spreadsheetID);
|
$spreadsheet = $this->service->spreadsheets->get($this->spreadsheetID);
|
||||||
$sheets = $spreadsheet->getSheets();
|
$sheets = $spreadsheet->getSheets();
|
||||||
$sheetTitle = $sheets[$no_sheet]->getProperties()->getTitle();
|
$sheetTitle = $sheets[$no_sheet]->getProperties()->getTitle();
|
||||||
@@ -369,6 +864,45 @@ class ServiceGoogleSheet
|
|||||||
return!empty($values)? $values : [];
|
return!empty($values)? $values : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function get_data_by_sheet_name($sheet_name){
|
||||||
|
try {
|
||||||
|
$spreadsheet = $this->service->spreadsheets->get($this->spreadsheetID);
|
||||||
|
$sheets = $spreadsheet->getSheets();
|
||||||
|
|
||||||
|
// Find sheet by name
|
||||||
|
$targetSheet = null;
|
||||||
|
foreach ($sheets as $sheet) {
|
||||||
|
if ($sheet->getProperties()->getTitle() === $sheet_name) {
|
||||||
|
$targetSheet = $sheet;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$targetSheet) {
|
||||||
|
Log::warning("Sheet not found", ['sheet_name' => $sheet_name]);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$range = "{$sheet_name}";
|
||||||
|
$response = $this->service->spreadsheets_values->get($this->spreadsheetID, $range);
|
||||||
|
$values = $response->getValues();
|
||||||
|
|
||||||
|
Log::info("Sheet data retrieved", [
|
||||||
|
'sheet_name' => $sheet_name,
|
||||||
|
'total_rows' => count($values ?? [])
|
||||||
|
]);
|
||||||
|
|
||||||
|
return !empty($values) ? $values : [];
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error("Error getting data by sheet name", [
|
||||||
|
'sheet_name' => $sheet_name,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get specific values from a row that contains a specific text/section identifier
|
* Get specific values from a row that contains a specific text/section identifier
|
||||||
* @param string $section_identifier Text to search for in the row
|
* @param string $section_identifier Text to search for in the row
|
||||||
@@ -376,7 +910,7 @@ class ServiceGoogleSheet
|
|||||||
* @param int $no_sheet Sheet number (0-based)
|
* @param int $no_sheet Sheet number (0-based)
|
||||||
* @return array Array of values from specified columns, or empty array if section not found
|
* @return array Array of values from specified columns, or empty array if section not found
|
||||||
*/
|
*/
|
||||||
private function get_values_from_section(string $section_identifier, array $column_indices = [], int $no_sheet = 1) {
|
private function get_values_from_section(string $section_identifier, array $column_indices = [], int $no_sheet = 8) {
|
||||||
try {
|
try {
|
||||||
$sheet_data = $this->get_data_by_sheet($no_sheet);
|
$sheet_data = $this->get_data_by_sheet($no_sheet);
|
||||||
|
|
||||||
@@ -471,14 +1005,16 @@ class ServiceGoogleSheet
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get count of spatial plannings that have active retribution calculations
|
* Get count of spatial plannings that can be calculated with new formula
|
||||||
*/
|
*/
|
||||||
public function getSpatialPlanningWithCalculationCount(): int
|
public function getSpatialPlanningWithCalculationCount(): int
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
return SpatialPlanning::whereHas('retributionCalculations', function ($query) {
|
// Count spatial plannings that have valid data and are not yet issued (is_terbit = false)
|
||||||
$query->where('is_active', true);
|
return SpatialPlanning::where('land_area', '>', 0)
|
||||||
})->count();
|
->where('site_bcr', '>', 0)
|
||||||
|
->where('is_terbit', false)
|
||||||
|
->count();
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error("Error getting spatial planning with calculation count", ['error' => $e->getMessage()]);
|
Log::error("Error getting spatial planning with calculation count", ['error' => $e->getMessage()]);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -486,25 +1022,29 @@ class ServiceGoogleSheet
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get total sum of retribution amounts for spatial plannings with active calculations
|
* Get total sum of retribution amounts using new calculation formula
|
||||||
*/
|
*/
|
||||||
public function getSpatialPlanningCalculationSum(): float
|
public function getSpatialPlanningCalculationSum(): float
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
// Get all spatial plannings that have active calculations
|
// Get spatial plannings that are not yet issued (is_terbit = false) and have valid data
|
||||||
$spatialPlannings = SpatialPlanning::whereHas('retributionCalculations', function ($query) {
|
$spatialPlannings = SpatialPlanning::where('land_area', '>', 0)
|
||||||
$query->where('is_active', true);
|
->where('site_bcr', '>', 0)
|
||||||
})->with(['retributionCalculations.retributionCalculation'])
|
->where('is_terbit', false)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$totalSum = 0;
|
$totalSum = 0;
|
||||||
foreach ($spatialPlannings as $spatialPlanning) {
|
foreach ($spatialPlannings as $spatialPlanning) {
|
||||||
$activeCalculation = $spatialPlanning->activeRetributionCalculation;
|
// Use new calculation formula: LUAS LAHAN × BCR × HARGA SATUAN
|
||||||
if ($activeCalculation && $activeCalculation->retributionCalculation) {
|
$totalSum += $spatialPlanning->calculated_retribution;
|
||||||
$totalSum += $activeCalculation->retributionCalculation->retribution_amount;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log::info("Spatial Planning Calculation Sum (is_terbit = false only)", [
|
||||||
|
'total_records' => $spatialPlannings->count(),
|
||||||
|
'total_sum' => $totalSum,
|
||||||
|
'filtered_by' => 'is_terbit = false'
|
||||||
|
]);
|
||||||
|
|
||||||
return (float) $totalSum;
|
return (float) $totalSum;
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error("Error getting spatial planning calculation sum", ['error' => $e->getMessage()]);
|
Log::error("Error getting spatial planning calculation sum", ['error' => $e->getMessage()]);
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ class ServicePbgTask
|
|||||||
};
|
};
|
||||||
|
|
||||||
do {
|
do {
|
||||||
$url = "{$this->simbg_host}/api/pbg/v1/list/?page={$currentPage}&size={$this->fetch_per_page}&sort=ASC";
|
$url = "{$this->simbg_host}/api/pbg/v1/list/?page={$currentPage}&size={$this->fetch_per_page}&sort=ASC&date&search&status&slf_status&type=task&sort_by=created_at&application_type=1&start_date&end_date";
|
||||||
|
|
||||||
$fetch_data = $fetchData($url);
|
$fetch_data = $fetchData($url);
|
||||||
if (!$fetch_data) {
|
if (!$fetch_data) {
|
||||||
@@ -123,6 +123,8 @@ class ServicePbgTask
|
|||||||
$data = $response['data'];
|
$data = $response['data'];
|
||||||
$totalPage = isset($response['total_page']) ? (int) $response['total_page'] : 1;
|
$totalPage = isset($response['total_page']) ? (int) $response['total_page'] : 1;
|
||||||
|
|
||||||
|
Log::info("Total data scraping {$totalPage}");
|
||||||
|
|
||||||
$saved_data = [];
|
$saved_data = [];
|
||||||
foreach ($data as $item) {
|
foreach ($data as $item) {
|
||||||
$saved_data[] = [
|
$saved_data[] = [
|
||||||
|
|||||||
@@ -1,663 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services;
|
|
||||||
|
|
||||||
use App\Enums\ImportDatasourceStatus;
|
|
||||||
use App\Models\BigdataResume;
|
|
||||||
use App\Models\GlobalSetting;
|
|
||||||
use App\Models\ImportDatasource;
|
|
||||||
use App\Models\PbgTaskIndexIntegrations;
|
|
||||||
use App\Models\PbgTaskPrasarana;
|
|
||||||
use App\Models\PbgTaskRetributions;
|
|
||||||
use App\Models\TaskAssignment;
|
|
||||||
use Exception;
|
|
||||||
use App\Models\PbgTask;
|
|
||||||
use App\Traits\GlobalApiResponse;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use App\Services\ServiceClient;
|
|
||||||
use App\Services\GoogleSheetService;
|
|
||||||
use App\Models\DataSetting;
|
|
||||||
use App\Models\PbgTaskGoogleSheet;
|
|
||||||
|
|
||||||
class ServiceSIMBG
|
|
||||||
{
|
|
||||||
use GlobalApiResponse;
|
|
||||||
private $email;
|
|
||||||
private $password;
|
|
||||||
private $simbg_host;
|
|
||||||
private $fetch_per_page;
|
|
||||||
private $service_client;
|
|
||||||
private $googleSheetService;
|
|
||||||
/**
|
|
||||||
* Create a new class instance.
|
|
||||||
*/
|
|
||||||
public function __construct(GoogleSheetService $googleSheetService)
|
|
||||||
{
|
|
||||||
$settings = GlobalSetting::whereIn('key', [
|
|
||||||
'SIMBG_EMAIL', 'SIMBG_PASSWORD', 'SIMBG_HOST', 'FETCH_PER_PAGE'
|
|
||||||
])->pluck('value', 'key');
|
|
||||||
|
|
||||||
$this->email = trim((string) ($settings['SIMBG_EMAIL'] ?? ""));
|
|
||||||
$this->password = trim((string) ($settings['SIMBG_PASSWORD'] ?? ""));
|
|
||||||
$this->simbg_host = trim((string) ($settings['SIMBG_HOST'] ?? ""));
|
|
||||||
$this->fetch_per_page = trim((string) ($settings['FETCH_PER_PAGE'] ?? ""));
|
|
||||||
|
|
||||||
$this->service_client = new ServiceClient($this->simbg_host);
|
|
||||||
$this->googleSheetService = $googleSheetService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getToken(){
|
|
||||||
try{
|
|
||||||
$url = "/api/user/v1/auth/login/";
|
|
||||||
$body = [
|
|
||||||
'email' => $this->email,
|
|
||||||
'password' => $this->password,
|
|
||||||
];
|
|
||||||
|
|
||||||
$res = $this->service_client->post($url, $body);
|
|
||||||
if(!$res->original['success']){
|
|
||||||
Log::error("Token not retrieved ", ['response' => $res]);
|
|
||||||
throw new Exception("Token not retrieved.");
|
|
||||||
}
|
|
||||||
return $res;
|
|
||||||
}catch(Exception $e){
|
|
||||||
Log::error("Error on method get token ", ['response' => $e->getMessage()]);
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function syncIndexIntegration($uuids)
|
|
||||||
{
|
|
||||||
try{
|
|
||||||
if(empty($uuids)){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$initResToken = $this->getToken();
|
|
||||||
if (empty($initResToken->original['data']['token']['access'])) {
|
|
||||||
Log::error("API response indicates failure", ['token' => 'Failed to retrieve token']);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$token = $initResToken->original['data']['token']['access'];
|
|
||||||
|
|
||||||
$integrations = [];
|
|
||||||
foreach($uuids as $uuid){
|
|
||||||
$url = "/api/pbg/v1/detail/" . $uuid . "/retribution/indeks-terintegrasi/";
|
|
||||||
|
|
||||||
$headers = [
|
|
||||||
'Authorization' => "Bearer " . $token,
|
|
||||||
];
|
|
||||||
|
|
||||||
$res = $this->service_client->get($url, $headers);
|
|
||||||
|
|
||||||
if (empty($res->original['success']) || !$res->original['success']) {
|
|
||||||
// Log error
|
|
||||||
Log::error("API response indicates failure", ['url' => $url, 'uuid' => $uuid]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = $res->original['data']['data'] ?? null;
|
|
||||||
if (!$data) {
|
|
||||||
Log::error("No valid data returned from API", ['url' => $url, 'uuid' => $uuid]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$integrations[] = [
|
|
||||||
'pbg_task_uid' => $uuid,
|
|
||||||
'indeks_fungsi_bangunan' => $data['indeks_fungsi_bangunan'] ?? null,
|
|
||||||
'indeks_parameter_kompleksitas' => $data['indeks_parameter_kompleksitas'] ?? null,
|
|
||||||
'indeks_parameter_permanensi' => $data['indeks_parameter_permanensi'] ?? null,
|
|
||||||
'indeks_parameter_ketinggian' => $data['indeks_parameter_ketinggian'] ?? null,
|
|
||||||
'faktor_kepemilikan' => $data['faktor_kepemilikan'] ?? null,
|
|
||||||
'indeks_terintegrasi' => $data['indeks_terintegrasi'] ?? null,
|
|
||||||
'total' => $data['total'] ?? null,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
PbgTaskIndexIntegrations::upsert($integrations, ['pbg_task_uid'], ['indeks_fungsi_bangunan',
|
|
||||||
'indeks_parameter_kompleksitas', 'indeks_parameter_permanensi', 'indeks_parameter_ketinggian', 'faktor_kepemilikan', 'indeks_terintegrasi', 'total']);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}catch (Exception $e){
|
|
||||||
Log::error('error when sync index integration ', ['index integration'=> $e->getMessage()]);
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function syncTaskPBG()
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
Log::info("Processing google sheet sync");
|
|
||||||
$importDatasource = ImportDatasource::create([
|
|
||||||
'status' => ImportDatasourceStatus::Processing->value,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// sync google sheet first
|
|
||||||
$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"] = $this->convertToInteger($row[2]) ?? null;
|
|
||||||
$data_setting_result["MENUNGGU_KLIK_DPMPTSP_SUM"] = $this->convertToDecimal($row[3]) ?? null;
|
|
||||||
} elseif ($found_section === "REALISASI_TERBIT_PBG") {
|
|
||||||
$data_setting_result["REALISASI_TERBIT_PBG_COUNT"] = $this->convertToInteger($row[2]) ?? null;
|
|
||||||
$data_setting_result["REALISASI_TERBIT_PBG_SUM"] = $this->convertToDecimal($row[4]) ?? null;
|
|
||||||
} elseif ($found_section === "PROSES_DINAS_TEKNIS") {
|
|
||||||
$data_setting_result["PROSES_DINAS_TEKNIS_COUNT"] = $this->convertToInteger($row[2]) ?? null;
|
|
||||||
$data_setting_result["PROSES_DINAS_TEKNIS_SUM"] = $this->convertToDecimal($row[3]) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset section tracking after capturing "Grand Total"
|
|
||||||
$found_section = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Log::info("data setting result", ['result' => $data_setting_result]);
|
|
||||||
|
|
||||||
foreach ($data_setting_result as $key => $value) {
|
|
||||||
DataSetting::updateOrInsert(
|
|
||||||
["key" => $key], // Find by key
|
|
||||||
["value" => $value] // Update or insert value
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$mapToUpsert = [];
|
|
||||||
|
|
||||||
foreach ($sheetData as $data) {
|
|
||||||
$mapToUpsert[] = [
|
|
||||||
'no_registrasi' => $this->cleanString($data['no__registrasi'] ?? null),
|
|
||||||
'jenis_konsultasi' => $this->cleanString($data['jenis_konsultasi'] ?? null),
|
|
||||||
'fungsi_bg' => $this->cleanString($data['fungsi_bg'] ?? null),
|
|
||||||
'tgl_permohonan' => $this->convertToDate($this->cleanString($data['tgl_permohonan'] ?? null)),
|
|
||||||
'status_verifikasi' => $this->cleanString($data['status_verifikasi'] ?? null),
|
|
||||||
'status_permohonan' => $this->convertToDate($this->cleanString($data['status_permohonan'] ?? null)),
|
|
||||||
'alamat_pemilik' => $this->cleanString($data['alamat_pemilik'] ?? null),
|
|
||||||
'no_hp' => $this->cleanString($data['no__hp'] ?? null),
|
|
||||||
'email' => $this->cleanString($data['e_mail'] ?? null),
|
|
||||||
'tanggal_catatan' => $this->convertToDate($this->cleanString($data['tanggal_catatan'] ?? null)),
|
|
||||||
'catatan_kekurangan_dokumen' => $this->cleanString($data['catatan_kekurangan_dokumen'] ?? null),
|
|
||||||
'gambar' => $this->cleanString($data['gambar'] ?? null),
|
|
||||||
'krk_kkpr' => $this->cleanString($data['krk_kkpr'] ?? null),
|
|
||||||
'no_krk' => $this->cleanString($data['no__krk'] ?? null),
|
|
||||||
'lh' => $this->cleanString($data['lh'] ?? null),
|
|
||||||
'ska' => $this->cleanString($data['ska'] ?? null),
|
|
||||||
'keterangan' => $this->cleanString($data['keterangan'] ?? null),
|
|
||||||
'helpdesk' => $this->cleanString($data['helpdesk'] ?? null),
|
|
||||||
'pj' => $this->cleanString($data['pj'] ?? null),
|
|
||||||
'kepemilikan' => $this->cleanString($data['kepemilikan'] ?? null),
|
|
||||||
'potensi_taru' => $this->cleanString($data['potensi_taru'] ?? null),
|
|
||||||
'validasi_dinas' => $this->cleanString($data['validasi_dinas'] ?? null),
|
|
||||||
'kategori_retribusi' => $this->cleanString($data['kategori_retribusi'] ?? null),
|
|
||||||
'no_urut_ba_tpt' => $this->cleanString($data['no__urut_ba_tpt__2024_0001_'] ?? null),
|
|
||||||
'tanggal_ba_tpt' => $this->convertToDate($this->cleanString($data['tanggal_ba_tpt'] ?? null)),
|
|
||||||
'no_urut_ba_tpa' => $this->cleanString($data['no__urut_ba_tpa'] ?? null),
|
|
||||||
'tanggal_ba_tpa' => $this->convertToDate($this->cleanString($data['tanggal_ba_tpa'] ?? null)),
|
|
||||||
'no_urut_skrd' => $this->cleanString($data['no__urut_skrd__2024_0001_'] ?? null),
|
|
||||||
'tanggal_skrd' => $this->convertToDate($this->cleanString($data['tanggal_skrd'] ?? null)),
|
|
||||||
'ptsp' => $this->cleanString($data['ptsp'] ?? null),
|
|
||||||
'selesai_terbit' => $this->cleanString($data['selesai_terbit'] ?? null),
|
|
||||||
'tanggal_pembayaran' => $this->convertToDate($this->cleanString($data['tanggal_pembayaran__yyyy_mm_dd_'] ?? null)),
|
|
||||||
'format_sts' => $this->cleanString($data['format_sts'] ?? null),
|
|
||||||
'tahun_terbit' => (int) ($data['tahun_terbit'] ?? null),
|
|
||||||
'tahun_berjalan' => (int) ($data['tahun_berjalan'] ?? null),
|
|
||||||
'kelurahan' => $this->cleanString($data['kelurahan'] ?? null),
|
|
||||||
'kecamatan' => $this->cleanString($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' => $this->cleanString($data['latitude'] ?? null),
|
|
||||||
'longitude' => $this->cleanString($data['longitude'] ?? null),
|
|
||||||
'nik_nib' => $this->cleanString($data['nik_nib'] ?? null),
|
|
||||||
'dok_tanah' => $this->cleanString($data['dok__tanah'] ?? null),
|
|
||||||
'temuan' => $this->cleanString($data['temuan'] ?? null),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$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',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$initResToken = $this->getToken();
|
|
||||||
if (empty($initResToken->original['data']['token']['access'])) {
|
|
||||||
$importDatasource->update([
|
|
||||||
'status' => ImportDatasourceStatus::Failed->value,
|
|
||||||
'response_body' => 'Failed to retrieve token'
|
|
||||||
]);
|
|
||||||
return $this->resError("Failed to retrieve token");
|
|
||||||
}
|
|
||||||
$apiToken = $initResToken->original['data']['token']['access'];
|
|
||||||
$headers = ['Authorization' => "Bearer " . $apiToken];
|
|
||||||
|
|
||||||
$url = "/api/pbg/v1/list/?page=1&size={$this->fetch_per_page}&sort=ASC";
|
|
||||||
$initialResponse = $this->service_client->get($url, $headers);
|
|
||||||
|
|
||||||
$totalPage = $initialResponse->original['data']['total_page'] ?? 0;
|
|
||||||
if ($totalPage == 0) {
|
|
||||||
$importDatasource->update([
|
|
||||||
'status' => ImportDatasourceStatus::Failed->value,
|
|
||||||
'response_body' => 'Invalid response: no total_page'
|
|
||||||
]);
|
|
||||||
return $this->resError("Invalid response from API");
|
|
||||||
}
|
|
||||||
|
|
||||||
$savedCount = $failedCount = 0;
|
|
||||||
|
|
||||||
Log::info("Fetching tasks", ['total page' => $totalPage]);
|
|
||||||
|
|
||||||
for ($currentPage = 1; $currentPage <= $totalPage; $currentPage++) {
|
|
||||||
try {
|
|
||||||
$pageUrl = "/api/pbg/v1/list/?page={$currentPage}&size={$this->fetch_per_page}&sort=ASC";
|
|
||||||
|
|
||||||
Log::info("Fetching tasks", ['currentPage' => $currentPage]);
|
|
||||||
$headers = [
|
|
||||||
'Authorization' => "Bearer " . $apiToken, // Update headers
|
|
||||||
];
|
|
||||||
|
|
||||||
for ($attempt = 0; $attempt < 2; $attempt++) { // Try twice (original + retry)
|
|
||||||
|
|
||||||
$response = $this->service_client->get($pageUrl, $headers);
|
|
||||||
|
|
||||||
if ($response instanceof \Illuminate\Http\JsonResponse) {
|
|
||||||
$decodedResponse = json_decode($response->getContent(), true);
|
|
||||||
|
|
||||||
if (isset($decodedResponse['errors']['code']) && $decodedResponse['errors']['code'] === 'token_not_valid') {
|
|
||||||
$initResToken = $this->getToken();
|
|
||||||
|
|
||||||
if (!empty($initResToken->original['data']['token']['access'])) {
|
|
||||||
$new_token = $initResToken->original['data']['token']['access'];
|
|
||||||
$headers['Authorization'] = "Bearer " . $new_token;
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
Log::error("Failed to refresh token");
|
|
||||||
return $this->resError("Failed to refresh token");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success case, break loop
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
$tasks = $response->original['data']['data'] ?? [];
|
|
||||||
|
|
||||||
if (empty($tasks)) {
|
|
||||||
Log::warning("No data found on page", ['page' => $currentPage]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$tasksCollective = [];
|
|
||||||
foreach ($tasks as $item) {
|
|
||||||
try {
|
|
||||||
$tasksCollective[] = [
|
|
||||||
'uuid' => $item['uid'],
|
|
||||||
'name' => $item['name'],
|
|
||||||
'owner_name' => $item['owner_name'],
|
|
||||||
'application_type' => $item['application_type'],
|
|
||||||
'application_type_name' => $item['application_type_name'],
|
|
||||||
'condition' => $item['condition'],
|
|
||||||
'registration_number' => $item['registration_number'],
|
|
||||||
'document_number' => $item['document_number'],
|
|
||||||
'address' => $item['address'],
|
|
||||||
'status' => $item['status'],
|
|
||||||
'status_name' => $item['status_name'],
|
|
||||||
'slf_status' => $item['slf_status'] ?? null,
|
|
||||||
'slf_status_name' => $item['slf_status_name'] ?? null,
|
|
||||||
'function_type' => $item['function_type'],
|
|
||||||
'consultation_type' => $item['consultation_type'],
|
|
||||||
'due_date' => $item['due_date'],
|
|
||||||
'land_certificate_phase' => $item['land_certificate_phase'],
|
|
||||||
'task_created_at' => isset($item['created_at']) ? Carbon::parse($item['created_at'])->format('Y-m-d H:i:s') : null,
|
|
||||||
'updated_at' => now(),
|
|
||||||
'created_at' => now(),
|
|
||||||
];
|
|
||||||
|
|
||||||
$this->syncTaskDetailSubmit($item['uid'], $apiToken);
|
|
||||||
$this->syncTaskAssignments($item['uid']);
|
|
||||||
$savedCount++;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$failedCount++;
|
|
||||||
Log::error("Failed to process task", [
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
'task' => $item,
|
|
||||||
]);
|
|
||||||
continue; // Skip failed task, continue processing the rest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($tasksCollective)) {
|
|
||||||
PbgTask::upsert($tasksCollective, ['uuid'], [
|
|
||||||
'name', 'owner_name', 'application_type', 'application_type_name', 'condition',
|
|
||||||
'registration_number', 'document_number', 'address', 'status', 'status_name',
|
|
||||||
'slf_status', 'slf_status_name', 'function_type', 'consultation_type', 'due_date',
|
|
||||||
'land_certificate_phase', 'task_created_at', 'updated_at'
|
|
||||||
]);
|
|
||||||
|
|
||||||
$uuids = array_column($tasksCollective, 'uuid');
|
|
||||||
$this->syncIndexIntegration($uuids);
|
|
||||||
}
|
|
||||||
} catch (Exception $e) {
|
|
||||||
Log::error("Failed to process page", [
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
'page' => $currentPage,
|
|
||||||
]);
|
|
||||||
continue; // Skip the failed page and move to the next
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BigdataResume::generateResumeData($importDatasource->id, "all", $data_setting_result);
|
|
||||||
BigdataResume::generateResumeData($importDatasource->id, now()->year, $data_setting_result);
|
|
||||||
|
|
||||||
// Final update after processing all pages
|
|
||||||
$importDatasource->update([
|
|
||||||
'status' => ImportDatasourceStatus::Success->value,
|
|
||||||
'message' => "Successfully processed: $savedCount, Failed: $failedCount"
|
|
||||||
]);
|
|
||||||
|
|
||||||
Log::info("syncTaskList completed", ['savedCount' => $savedCount, 'failedCount' => $failedCount]);
|
|
||||||
|
|
||||||
return $this->resSuccess(['savedCount' => $savedCount, 'failedCount' => $failedCount]);
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
Log::error("syncTaskList failed", ['error' => $e->getMessage()]);
|
|
||||||
if (isset($importDatasource)) {
|
|
||||||
$importDatasource->update([
|
|
||||||
'status' => ImportDatasourceStatus::Failed->value,
|
|
||||||
'response_body' => 'Critical failure: ' . $e->getMessage()
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
return $this->resError("Critical failure occurred: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function syncTaskDetailSubmit($uuid, $token)
|
|
||||||
{
|
|
||||||
try{
|
|
||||||
$url = "/api/pbg/v1/detail/" . $uuid . "/retribution/submit/";
|
|
||||||
|
|
||||||
$headers = [
|
|
||||||
'Authorization' => "Bearer " . $token,
|
|
||||||
];
|
|
||||||
|
|
||||||
for ($attempt = 0; $attempt < 2; $attempt++) {
|
|
||||||
$res = $this->service_client->get($url, $headers);
|
|
||||||
|
|
||||||
// Check if response is JsonResponse and decode it
|
|
||||||
if ($res instanceof \Illuminate\Http\JsonResponse) {
|
|
||||||
$decodedResponse = json_decode($res->getContent(), true);
|
|
||||||
|
|
||||||
if (isset($decodedResponse['errors']['code']) && $decodedResponse['errors']['code'] === 'token_not_valid') {
|
|
||||||
$initResToken = $this->getToken();
|
|
||||||
|
|
||||||
if (!empty($initResToken->original['data']['token']['access'])) {
|
|
||||||
$new_token = $initResToken->original['data']['token']['access'];
|
|
||||||
|
|
||||||
$headers['Authorization'] = "Bearer " . $new_token;
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
Log::error("Failed to refresh token");
|
|
||||||
return $this->resError("Failed to refresh token");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure response is valid before accessing properties
|
|
||||||
$responseData = $res->original ?? [];
|
|
||||||
$data = $responseData['data']['data'] ?? [];
|
|
||||||
if (empty($data)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$detailCreatedAt = isset($data['created_at'])
|
|
||||||
? Carbon::parse($data['created_at'])->format('Y-m-d H:i:s')
|
|
||||||
: null;
|
|
||||||
|
|
||||||
$detailUpdatedAt = isset($data['updated_at'])
|
|
||||||
? Carbon::parse($data['updated_at'])->format('Y-m-d H:i:s')
|
|
||||||
: null;
|
|
||||||
|
|
||||||
$pbg_task_retributions = PbgTaskRetributions::updateOrCreate(
|
|
||||||
['detail_id' => $data['id']],
|
|
||||||
[
|
|
||||||
'detail_uid' => $data['uid'] ?? null,
|
|
||||||
'detail_created_at' => $detailCreatedAt ?? null,
|
|
||||||
'detail_updated_at' => $detailUpdatedAt ?? null,
|
|
||||||
'luas_bangunan' => $data['luas_bangunan'] ?? null,
|
|
||||||
'indeks_lokalitas' => $data['indeks_lokalitas'] ?? null,
|
|
||||||
'wilayah_shst' => $data['wilayah_shst'] ?? null,
|
|
||||||
'kegiatan_id' => $data['kegiatan']['id'] ?? null,
|
|
||||||
'kegiatan_name' => $data['kegiatan']['name'] ?? null,
|
|
||||||
'nilai_shst' => $data['nilai_shst'] ?? null,
|
|
||||||
'indeks_terintegrasi' => $data['indeks_terintegrasi'] ?? null,
|
|
||||||
'indeks_bg_terbangun' => $data['indeks_bg_terbangun'] ?? null,
|
|
||||||
'nilai_retribusi_bangunan' => $data['nilai_retribusi_bangunan'] ?? null,
|
|
||||||
'nilai_prasarana' => $data['nilai_prasarana'] ?? null,
|
|
||||||
'created_by' => $data['created_by'] ?? null,
|
|
||||||
'pbg_document' => $data['pbg_document'] ?? null,
|
|
||||||
'underpayment' => $data['underpayment'] ?? null,
|
|
||||||
'skrd_amount' => $data['skrd_amount'] ?? null,
|
|
||||||
'pbg_task_uid' => $uuid,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
$pbg_task_retribution_id = $pbg_task_retributions->id;
|
|
||||||
|
|
||||||
$prasaranaData = $data['prasarana'] ?? [];
|
|
||||||
if (!empty($prasaranaData)) {
|
|
||||||
$insertData = array_map(fn($item) => [
|
|
||||||
'pbg_task_uid' => $uuid,
|
|
||||||
'pbg_task_retribution_id' => $pbg_task_retribution_id,
|
|
||||||
'prasarana_id' => $item['id'] ?? null,
|
|
||||||
'prasarana_type' => $item['prasarana_type'] ?? null,
|
|
||||||
'building_type' => $item['building_type'] ?? null,
|
|
||||||
'total' => $item['total'] ?? null,
|
|
||||||
'quantity' => $item['quantity'] ?? null,
|
|
||||||
'unit' => $item['unit'] ?? null,
|
|
||||||
'index_prasarana' => $item['index_prasarana'] ?? null,
|
|
||||||
], $prasaranaData);
|
|
||||||
|
|
||||||
// Use bulk insert or upsert for faster database operation
|
|
||||||
PbgTaskPrasarana::upsert($insertData, ['prasarana_id']);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
|
|
||||||
}catch(Exception $e){
|
|
||||||
Log::error("Failed to sync task detail submit", ['error' => $e->getMessage(), 'uuid' => $uuid]);
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function syncTaskAssignments($uuid){
|
|
||||||
try{
|
|
||||||
$init_token = $this->getToken();
|
|
||||||
$token = $init_token->original['data']['token']['access'];
|
|
||||||
$url = "/api/pbg/v1/list-tim-penilai/". $uuid . "/?page=1&size=10";
|
|
||||||
$headers = [
|
|
||||||
'Authorization' => "Bearer " . $token,
|
|
||||||
];
|
|
||||||
|
|
||||||
$response = $this->service_client->get($url, $headers);
|
|
||||||
$datas = $response->original['data']['data'] ?? [];
|
|
||||||
if(empty($datas)){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$task_assignments = [];
|
|
||||||
|
|
||||||
foreach ($datas as $data) {
|
|
||||||
$task_assignments[] = [
|
|
||||||
'pbg_task_uid' => $uuid,
|
|
||||||
'user_id' => $data['user_id'],
|
|
||||||
'name' => $data['name'],
|
|
||||||
'username' => $data['username'],
|
|
||||||
'email' => $data['email'],
|
|
||||||
'phone_number' => $data['phone_number'],
|
|
||||||
'role' => $data['role'],
|
|
||||||
'role_name' => $data['role_name'],
|
|
||||||
'is_active' => $data['is_active'],
|
|
||||||
'file' => !empty($data['file']) ? json_encode($data['file']) : null,
|
|
||||||
'expertise' => !empty($data['expertise']) ? json_encode($data['expertise']) : null,
|
|
||||||
'experience' => !empty($data['experience']) ? json_encode($data['experience']) : null,
|
|
||||||
'is_verif' => $data['is_verif'],
|
|
||||||
'uid' => $data['uid'],
|
|
||||||
'status' => $data['status'],
|
|
||||||
'status_name' => $data['status_name'],
|
|
||||||
'note' => $data['note'],
|
|
||||||
'ta_id' => $data['id'],
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
TaskAssignment::upsert(
|
|
||||||
$task_assignments,
|
|
||||||
['uid'],
|
|
||||||
['ta_id','name', 'username', 'email', 'phone_number', 'role', 'role_name', 'is_active', 'file', 'expertise', 'experience', 'is_verif', 'status', 'status_name', 'note', 'updated_at']
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}catch(Exception $e){
|
|
||||||
Log::error("Failed to sync task assignments", ['error' => $e->getMessage()]);
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
$cleaned = str_replace('.','', $value);
|
|
||||||
|
|
||||||
// Otherwise, cast to integer
|
|
||||||
return (int) $cleaned;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function cleanString($value)
|
|
||||||
{
|
|
||||||
return isset($value) ? trim(strip_tags($value)) : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,10 @@
|
|||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
use App\Models\GlobalSetting;
|
use App\Models\GlobalSetting;
|
||||||
|
use App\Models\PbgStatus;
|
||||||
use App\Models\PbgTask;
|
use App\Models\PbgTask;
|
||||||
use App\Models\PbgTaskDetail;
|
use App\Models\PbgTaskDetail;
|
||||||
|
use App\Models\PbgTaskDetailDataList;
|
||||||
use App\Models\PbgTaskIndexIntegrations;
|
use App\Models\PbgTaskIndexIntegrations;
|
||||||
use App\Models\PbgTaskPrasarana;
|
use App\Models\PbgTaskPrasarana;
|
||||||
use App\Models\PbgTaskRetributions;
|
use App\Models\PbgTaskRetributions;
|
||||||
@@ -36,44 +38,119 @@ class ServiceTabPbgTask
|
|||||||
$this->user_refresh_token = $auth_data['refresh'];
|
$this->user_refresh_token = $auth_data['refresh'];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function run_service($retry_uuid = null)
|
public function run_service($retry_uuid = null, $chunk_size = 50)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$pbg_tasks = PbgTask::orderBy('id')->get();
|
$query = PbgTask::orderBy('id');
|
||||||
$start = false;
|
|
||||||
|
|
||||||
foreach ($pbg_tasks as $pbg_task) {
|
// If retry_uuid is provided, start from that UUID
|
||||||
if($retry_uuid){
|
if ($retry_uuid) {
|
||||||
if($pbg_task->uuid === $retry_uuid){
|
$retryTask = PbgTask::where('uuid', $retry_uuid)->first();
|
||||||
$start = true;
|
if ($retryTask) {
|
||||||
|
$query->where('id', '>=', $retryTask->id);
|
||||||
|
Log::info("Resuming sync from UUID: {$retry_uuid} (ID: {$retryTask->id})");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!$start){
|
$totalTasks = $query->count();
|
||||||
|
$processedCount = 0;
|
||||||
|
|
||||||
|
Log::info("Starting sync for {$totalTasks} PBG Tasks with chunk size: {$chunk_size}");
|
||||||
|
|
||||||
|
// Process in chunks to reduce memory usage
|
||||||
|
$query->chunk($chunk_size, function ($pbg_tasks) use (&$processedCount, $totalTasks) {
|
||||||
|
$chunkStartTime = now();
|
||||||
|
|
||||||
|
foreach ($pbg_tasks as $pbg_task) {
|
||||||
|
try {
|
||||||
|
$this->current_uuid = $pbg_task->uuid;
|
||||||
|
$taskStartTime = now();
|
||||||
|
|
||||||
|
// Process all endpoints for this task
|
||||||
|
$this->processTaskEndpoints($pbg_task->uuid);
|
||||||
|
|
||||||
|
$processedCount++;
|
||||||
|
$taskTime = now()->diffInSeconds($taskStartTime);
|
||||||
|
|
||||||
|
// Log progress every 10 tasks
|
||||||
|
if ($processedCount % 10 === 0) {
|
||||||
|
$progress = round(($processedCount / $totalTasks) * 100, 2);
|
||||||
|
Log::info("Progress: {$processedCount}/{$totalTasks} ({$progress}%) - Last task took {$taskTime}s");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error("Failed on UUID: {$this->current_uuid}, Error: " . $e->getMessage());
|
||||||
|
|
||||||
|
// Check if this is a critical error that should stop the process
|
||||||
|
if ($this->isCriticalError($e)) {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-critical errors, log and continue
|
||||||
|
Log::warning("Skipping UUID {$this->current_uuid} due to non-critical error");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try{
|
|
||||||
$this->current_uuid = $pbg_task->uuid;
|
$chunkTime = now()->diffInSeconds($chunkStartTime);
|
||||||
$this->scraping_task_details($pbg_task->uuid);
|
Log::info("Processed chunk of {$pbg_tasks->count()} tasks in {$chunkTime} seconds");
|
||||||
$this->scraping_task_assignments($pbg_task->uuid);
|
|
||||||
$this->scraping_task_retributions($pbg_task->uuid);
|
// Small delay between chunks to prevent API rate limiting
|
||||||
$this->scraping_task_integrations($pbg_task->uuid);
|
if ($pbg_tasks->count() === $chunk_size) {
|
||||||
}catch(\Exception $e){
|
sleep(1);
|
||||||
Log::error("Failed on UUID: {$this->current_uuid}, Error: " . $e->getMessage());
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Log::info("Successfully completed sync for {$processedCount} PBG Tasks");
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error("Failed to syncronize: " . $e->getMessage());
|
Log::error("Failed to synchronize: " . $e->getMessage());
|
||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process all endpoints for a single task
|
||||||
|
*/
|
||||||
|
private function processTaskEndpoints(string $uuid): void
|
||||||
|
{
|
||||||
|
$this->scraping_task_details($uuid);
|
||||||
|
$this->scraping_pbg_data_list($uuid);
|
||||||
|
// $this->scraping_task_assignments($uuid);
|
||||||
|
$this->scraping_task_retributions($uuid);
|
||||||
|
$this->scraping_task_integrations($uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if an error is critical and should stop the process
|
||||||
|
*/
|
||||||
|
private function isCriticalError(\Exception $e): bool
|
||||||
|
{
|
||||||
|
$message = $e->getMessage();
|
||||||
|
|
||||||
|
// Critical authentication errors
|
||||||
|
if (strpos($message, 'Token refresh and login failed') !== false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Critical system errors
|
||||||
|
if (strpos($message, 'Connection refused') !== false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database connection errors
|
||||||
|
if (strpos($message, 'database') !== false && strpos($message, 'connection') !== false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public function getFailedUUID(){
|
public function getFailedUUID(){
|
||||||
return $this->current_uuid;
|
return $this->current_uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function scraping_task_details($uuid)
|
public function scraping_task_details($uuid)
|
||||||
{
|
{
|
||||||
$url = "{$this->simbg_host}/api/pbg/v1/detail/{$uuid}/";
|
$url = "{$this->simbg_host}/api/pbg/v1/detail/{$uuid}/";
|
||||||
$options = [
|
$options = [
|
||||||
@@ -98,6 +175,8 @@ class ServiceTabPbgTask
|
|||||||
|
|
||||||
$data = $responseData['data'];
|
$data = $responseData['data'];
|
||||||
|
|
||||||
|
Log::info("Executed uid : {$uuid}");
|
||||||
|
|
||||||
// Use the static method from PbgTaskDetail model to create/update
|
// Use the static method from PbgTaskDetail model to create/update
|
||||||
PbgTaskDetail::createFromApiResponse($data, $uuid);
|
PbgTaskDetail::createFromApiResponse($data, $uuid);
|
||||||
|
|
||||||
@@ -142,7 +221,76 @@ class ServiceTabPbgTask
|
|||||||
throw new \Exception("Failed to fetch task details for UUID {$uuid} after retries.");
|
throw new \Exception("Failed to fetch task details for UUID {$uuid} after retries.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private function scraping_task_assignments($uuid)
|
public function scraping_task_detail_status($uuid)
|
||||||
|
{
|
||||||
|
$url = "{$this->simbg_host}/api/pbg/v1/detail/{$uuid}/status/";
|
||||||
|
$options = [
|
||||||
|
'headers' => [
|
||||||
|
'Authorization' => "Bearer {$this->user_token}",
|
||||||
|
'Content-Type' => 'application/json'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$maxRetries = 3;
|
||||||
|
$initialDelay = 1;
|
||||||
|
$retriedAfter401 = false;
|
||||||
|
|
||||||
|
for ($retryCount = 0; $retryCount < $maxRetries; $retryCount++) {
|
||||||
|
try {
|
||||||
|
$response = $this->client->get($url, $options);
|
||||||
|
$responseData = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
|
if (empty($responseData['data']) || !is_array($responseData['data'])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $responseData['data'];
|
||||||
|
|
||||||
|
// Use the static method from PbgTaskDetail model to create/update
|
||||||
|
PbgStatus::createOrUpdateFromApi($data, $uuid);
|
||||||
|
|
||||||
|
return $responseData;
|
||||||
|
} catch (\GuzzleHttp\Exception\ClientException $e) {
|
||||||
|
if ($e->getCode() === 401 && !$retriedAfter401) {
|
||||||
|
Log::warning("401 Unauthorized - Refreshing token and retrying...");
|
||||||
|
try{
|
||||||
|
$this->refreshToken();
|
||||||
|
$options['headers']['Authorization'] = "Bearer {$this->user_token}";
|
||||||
|
$retriedAfter401 = true;
|
||||||
|
continue;
|
||||||
|
}catch(\Exception $refreshError){
|
||||||
|
Log::error("Token refresh and login failed: " . $refreshError->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (\GuzzleHttp\Exception\ServerException | \GuzzleHttp\Exception\ConnectException $e) {
|
||||||
|
if ($e->getCode() === 502) {
|
||||||
|
Log::warning("502 Bad Gateway - Retrying in {$initialDelay} seconds...");
|
||||||
|
} else {
|
||||||
|
Log::error("Network error ({$e->getCode()}) - Retrying in {$initialDelay} seconds...");
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep($initialDelay);
|
||||||
|
$initialDelay *= 2;
|
||||||
|
} catch (\GuzzleHttp\Exception\RequestException $e) {
|
||||||
|
Log::error("Request error ({$e->getCode()}): " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
} catch (\JsonException $e) {
|
||||||
|
Log::error("JSON decoding error: " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::critical("Unhandled error: " . $e->getMessage(), ['trace' => $e->getTraceAsString()]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::error("Failed to fetch task detail status for UUID {$uuid} after {$maxRetries} retries.");
|
||||||
|
throw new \Exception("Failed to fetch task details for UUID {$uuid} after retries.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scraping_task_assignments($uuid)
|
||||||
{
|
{
|
||||||
$url = "{$this->simbg_host}/api/pbg/v1/list-tim-penilai/{$uuid}/?page=1&size=10";
|
$url = "{$this->simbg_host}/api/pbg/v1/list-tim-penilai/{$uuid}/?page=1&size=10";
|
||||||
$options = [
|
$options = [
|
||||||
@@ -235,7 +383,173 @@ class ServiceTabPbgTask
|
|||||||
throw new \Exception("Failed to fetch task assignments for UUID {$uuid} after retries.");
|
throw new \Exception("Failed to fetch task assignments for UUID {$uuid} after retries.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private function scraping_task_retributions($uuid)
|
public function scraping_pbg_data_list($uuid){
|
||||||
|
$url = "{$this->simbg_host}/api/pbg/v1/detail/{$uuid}/list-data/?sort=DESC";
|
||||||
|
$options = [
|
||||||
|
'headers' => [
|
||||||
|
'Authorization' => "Bearer {$this->user_token}",
|
||||||
|
'Content-Type' => 'application/json'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$maxRetries = 3;
|
||||||
|
$initialDelay = 1;
|
||||||
|
$retriedAfter401 = false;
|
||||||
|
|
||||||
|
for ($retryCount = 0; $retryCount < $maxRetries; $retryCount++) {
|
||||||
|
try{
|
||||||
|
$response = $this->client->get($url, $options);
|
||||||
|
$responseData = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
|
if (empty($responseData['data']) || !is_array($responseData['data'])) {
|
||||||
|
Log::info("No data list found for UUID: {$uuid}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $responseData['data'];
|
||||||
|
|
||||||
|
Log::info("Processing data list for UUID: {$uuid}, found " . count($data) . " items");
|
||||||
|
|
||||||
|
// Process each data list item and save to database
|
||||||
|
$this->processDataListItems($data, $uuid);
|
||||||
|
|
||||||
|
return $responseData;
|
||||||
|
} catch (\GuzzleHttp\Exception\ClientException $e) {
|
||||||
|
if ($e->getCode() === 401 && !$retriedAfter401) {
|
||||||
|
Log::warning("401 Unauthorized - Refreshing token and retrying...");
|
||||||
|
try{
|
||||||
|
$this->refreshToken();
|
||||||
|
$options['headers']['Authorization'] = "Bearer {$this->user_token}";
|
||||||
|
$retriedAfter401 = true;
|
||||||
|
continue;
|
||||||
|
}catch(\Exception $refreshError){
|
||||||
|
Log::error("Token refresh and login failed: " . $refreshError->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (\GuzzleHttp\Exception\ServerException | \GuzzleHttp\Exception\ConnectException $e) {
|
||||||
|
if ($e->getCode() === 502) {
|
||||||
|
Log::warning("502 Bad Gateway - Retrying in {$initialDelay} seconds...");
|
||||||
|
} else {
|
||||||
|
Log::error("Network error ({$e->getCode()}) - Retrying in {$initialDelay} seconds...");
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep($initialDelay);
|
||||||
|
$initialDelay *= 2;
|
||||||
|
} catch (\GuzzleHttp\Exception\RequestException $e) {
|
||||||
|
Log::error("Request error ({$e->getCode()}): " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
} catch (\JsonException $e) {
|
||||||
|
Log::error("JSON decoding error: " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::critical("Unhandled error: " . $e->getMessage(), ['trace' => $e->getTraceAsString()]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::error("Failed to fetch task data list for UUID {$uuid} after {$maxRetries} retries.");
|
||||||
|
throw new \Exception("Failed to fetch task data list for UUID {$uuid} after retries.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process and save data list items to database (Optimized with bulk operations)
|
||||||
|
*/
|
||||||
|
private function processDataListItems(array $dataListItems, string $pbgTaskUuid): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (empty($dataListItems)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$batchData = [];
|
||||||
|
$validItems = 0;
|
||||||
|
|
||||||
|
foreach ($dataListItems as $item) {
|
||||||
|
// Validate required fields
|
||||||
|
if (empty($item['uid'])) {
|
||||||
|
Log::warning("Skipping data list item with missing UID for PBG Task: {$pbgTaskUuid}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse created_at if exists
|
||||||
|
$createdAt = null;
|
||||||
|
if (!empty($item['created_at'])) {
|
||||||
|
try {
|
||||||
|
$createdAt = Carbon::parse($item['created_at'])->format('Y-m-d H:i:s');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::warning("Invalid created_at format for data list UID: {$item['uid']}, Error: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$batchData[] = [
|
||||||
|
'uid' => $item['uid'],
|
||||||
|
'name' => $item['name'] ?? null,
|
||||||
|
'description' => $item['description'] ?? null,
|
||||||
|
'status' => $item['status'] ?? null,
|
||||||
|
'status_name' => $item['status_name'] ?? null,
|
||||||
|
'data_type' => $item['data_type'] ?? null,
|
||||||
|
'data_type_name' => $item['data_type_name'] ?? null,
|
||||||
|
'file' => $item['file'] ?? null,
|
||||||
|
'note' => $item['note'] ?? null,
|
||||||
|
'pbg_task_uuid' => $pbgTaskUuid,
|
||||||
|
'created_at' => $createdAt ?: now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$validItems++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($batchData)) {
|
||||||
|
// Use upsert for bulk insert/update operations
|
||||||
|
PbgTaskDetailDataList::upsert(
|
||||||
|
$batchData,
|
||||||
|
['uid'], // Unique columns
|
||||||
|
[
|
||||||
|
'name', 'description', 'status', 'status_name',
|
||||||
|
'data_type', 'data_type_name', 'file', 'note',
|
||||||
|
'pbg_task_uuid', 'updated_at'
|
||||||
|
] // Columns to update
|
||||||
|
);
|
||||||
|
|
||||||
|
Log::info("Successfully bulk processed {$validItems} data list items for PBG Task: {$pbgTaskUuid}");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error("Error bulk processing data list items for PBG Task {$pbgTaskUuid}: " . $e->getMessage());
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternative method using PbgTask model's syncDataLists for cleaner code
|
||||||
|
*/
|
||||||
|
private function processDataListItemsWithModel(array $dataListItems, string $pbgTaskUuid): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Find the PbgTask
|
||||||
|
$pbgTask = PbgTask::where('uuid', $pbgTaskUuid)->first();
|
||||||
|
|
||||||
|
if (!$pbgTask) {
|
||||||
|
Log::error("PBG Task not found with UUID: {$pbgTaskUuid}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the model's syncDataLists method
|
||||||
|
$pbgTask->syncDataLists($dataListItems);
|
||||||
|
|
||||||
|
$processedCount = count($dataListItems);
|
||||||
|
Log::info("Successfully synced {$processedCount} data list items for PBG Task: {$pbgTaskUuid} using model method");
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error("Error syncing data list items for PBG Task {$pbgTaskUuid}: " . $e->getMessage());
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scraping_task_retributions($uuid)
|
||||||
{
|
{
|
||||||
$url = "{$this->simbg_host}/api/pbg/v1/detail/" . $uuid . "/retribution/submit/";
|
$url = "{$this->simbg_host}/api/pbg/v1/detail/" . $uuid . "/retribution/submit/";
|
||||||
$options = [
|
$options = [
|
||||||
@@ -352,7 +666,7 @@ class ServiceTabPbgTask
|
|||||||
throw new \Exception("Failed to fetch task retributions for UUID {$uuid} after retries.");
|
throw new \Exception("Failed to fetch task retributions for UUID {$uuid} after retries.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private function scraping_task_integrations($uuid){
|
public function scraping_task_integrations($uuid){
|
||||||
$url = "{$this->simbg_host}/api/pbg/v1/detail/" . $uuid . "/retribution/indeks-terintegrasi/";
|
$url = "{$this->simbg_host}/api/pbg/v1/detail/" . $uuid . "/retribution/indeks-terintegrasi/";
|
||||||
$options = [
|
$options = [
|
||||||
'headers' => [
|
'headers' => [
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware) {
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
|
$middleware->alias([
|
||||||
|
'validate.api.token.web' => \App\Http\Middleware\ValidateApiTokenForWeb::class,
|
||||||
|
]);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions) {
|
->withExceptions(function (Exceptions $exceptions) {
|
||||||
$exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $th){
|
$exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $th){
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* Drop unused retribution-related tables that have been replaced with new schema
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Disable foreign key checks to avoid constraint errors
|
||||||
|
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Drop tables - order doesn't matter now since FK checks are disabled
|
||||||
|
|
||||||
|
// 1. Drop retribution_proposals
|
||||||
|
if (Schema::hasTable('retribution_proposals')) {
|
||||||
|
Schema::dropIfExists('retribution_proposals');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Drop building_function_parameters
|
||||||
|
if (Schema::hasTable('building_function_parameters')) {
|
||||||
|
Schema::dropIfExists('building_function_parameters');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Drop retribution_formulas
|
||||||
|
if (Schema::hasTable('retribution_formulas')) {
|
||||||
|
Schema::dropIfExists('retribution_formulas');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Drop building_functions
|
||||||
|
if (Schema::hasTable('building_functions')) {
|
||||||
|
Schema::dropIfExists('building_functions');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Drop floor_height_indices
|
||||||
|
if (Schema::hasTable('floor_height_indices')) {
|
||||||
|
Schema::dropIfExists('floor_height_indices');
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
// Re-enable foreign key checks
|
||||||
|
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* Note: This rollback recreates the tables, but they will be empty.
|
||||||
|
* Use this only if you need to rollback immediately after running the migration.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// Recreate building_functions table (parent first)
|
||||||
|
Schema::create('building_functions', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('code')->unique()->comment('Kode unik fungsi bangunan');
|
||||||
|
$table->string('name', 255)->comment('Nama fungsi bangunan');
|
||||||
|
$table->text('description')->nullable()->comment('Deskripsi detail fungsi bangunan');
|
||||||
|
$table->unsignedBigInteger('parent_id')->nullable()->comment('ID parent untuk hierarki');
|
||||||
|
$table->foreign('parent_id')->references('id')->on('building_functions')->onDelete('cascade');
|
||||||
|
$table->integer('level')->default(0)->comment('Level hierarki (0=root, 1=child, dst)');
|
||||||
|
$table->integer('sort_order')->default(0)->comment('Urutan tampilan');
|
||||||
|
$table->decimal('base_tariff', 15, 2)->nullable()->comment('Tarif dasar per m2');
|
||||||
|
|
||||||
|
// Indexes untuk performa
|
||||||
|
$table->index(['parent_id', 'level']);
|
||||||
|
$table->index(['level', 'sort_order']);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recreate building_function_parameters table
|
||||||
|
Schema::create('building_function_parameters', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('building_function_id')->constrained('building_functions')->onDelete('cascade');
|
||||||
|
$table->decimal('fungsi_bangunan', 10, 6)->nullable()->comment('Parameter fungsi bangunan');
|
||||||
|
$table->decimal('ip_permanen', 10, 6)->nullable()->comment('Parameter IP permanen');
|
||||||
|
$table->decimal('ip_kompleksitas', 10, 6)->nullable()->comment('Parameter IP kompleksitas');
|
||||||
|
$table->decimal('indeks_lokalitas', 10, 6)->nullable()->comment('Parameter indeks lokalitas');
|
||||||
|
$table->decimal('asumsi_prasarana', 8, 6)->nullable()->comment('Parameter asumsi prasarana untuk perhitungan retribusi');
|
||||||
|
$table->decimal('koefisien_dasar', 15, 6)->nullable()->comment('Koefisien dasar perhitungan');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Unique constraint untuk 1:1 relationship
|
||||||
|
$table->unique('building_function_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recreate retribution_formulas table
|
||||||
|
Schema::create('retribution_formulas', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('building_function_id')->constrained('building_functions')->onDelete('cascade');
|
||||||
|
$table->string('name', 255)->comment('Nama formula');
|
||||||
|
$table->integer('floor_number')->comment('Nomor lantai (1, 2, 3, dst, 0=semua lantai)');
|
||||||
|
$table->text('formula_expression')->comment('Rumus matematika untuk perhitungan');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Indexes untuk performa
|
||||||
|
$table->index(['building_function_id', 'floor_number'], 'idx_building_floor');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recreate retribution_proposals table
|
||||||
|
Schema::create('retribution_proposals', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('spatial_planning_id')->nullable()->constrained('spatial_plannings')->onDelete('cascade');
|
||||||
|
$table->foreignId('building_function_id')->constrained('building_functions')->onDelete('cascade');
|
||||||
|
$table->foreignId('retribution_formula_id')->constrained('retribution_formulas')->onDelete('cascade');
|
||||||
|
$table->string('proposal_number')->unique()->comment('Nomor usulan retribusi');
|
||||||
|
$table->integer('floor_number')->comment('Nomor lantai (1, 2, 3, dst)');
|
||||||
|
$table->decimal('floor_area', 15, 6)->comment('Luas lantai ini (m2)');
|
||||||
|
$table->decimal('total_building_area', 15, 6)->comment('Total luas bangunan (m2)');
|
||||||
|
$table->decimal('ip_ketinggian', 10, 6)->comment('IP ketinggian untuk lantai ini');
|
||||||
|
$table->decimal('floor_retribution_amount', 15, 2)->comment('Jumlah retribusi untuk lantai ini');
|
||||||
|
$table->decimal('total_retribution_amount', 15, 2)->comment('Total retribusi keseluruhan');
|
||||||
|
$table->json('calculation_parameters')->nullable()->comment('Parameter yang digunakan dalam perhitungan');
|
||||||
|
$table->json('calculation_breakdown')->nullable()->comment('Breakdown detail perhitungan');
|
||||||
|
$table->text('notes')->nullable()->comment('Catatan tambahan');
|
||||||
|
$table->datetime('calculated_at')->comment('Waktu perhitungan dilakukan');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Indexes untuk performa
|
||||||
|
$table->index(['spatial_planning_id', 'floor_number'], 'idx_spatial_floor');
|
||||||
|
$table->index(['building_function_id'], 'idx_function');
|
||||||
|
$table->index('calculated_at');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recreate floor_height_indices table
|
||||||
|
Schema::create('floor_height_indices', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->integer('floor_number')->comment('Nomor lantai');
|
||||||
|
$table->decimal('ip_ketinggian', 10, 6)->comment('Indeks ketinggian per lantai');
|
||||||
|
$table->text('description')->nullable()->comment('Deskripsi indeks ketinggian');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Unique constraint untuk floor_number
|
||||||
|
$table->unique('floor_number');
|
||||||
|
$table->index('floor_number');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
38
database/migrations/2025_08_04_231702_create_taxs_table.php
Normal file
38
database/migrations/2025_08_04_231702_create_taxs_table.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('taxs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('tax_code');
|
||||||
|
$table->string('tax_no')->unique();
|
||||||
|
$table->string('npwpd');
|
||||||
|
$table->string('wp_name');
|
||||||
|
$table->string('business_name');
|
||||||
|
$table->text('address');
|
||||||
|
$table->date('start_validity');
|
||||||
|
$table->date('end_validity');
|
||||||
|
$table->decimal('tax_value', 12, 2);
|
||||||
|
$table->string('subdistrict');
|
||||||
|
$table->string('village');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('taxs');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('pbg_task_details', function (Blueprint $table) {
|
||||||
|
$table->string("nik")->nullable()->change();
|
||||||
|
$table->string("type_card")->nullable()->change();
|
||||||
|
$table->string("basement")->nullable()->change();
|
||||||
|
$table->decimal('latitude', 15, 8)->nullable()->change();
|
||||||
|
$table->decimal('longitude', 15, 8)->nullable()->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('pbg_task_details', function (Blueprint $table) {
|
||||||
|
$table->string("nik")->nullable()->change();
|
||||||
|
$table->string("type_card")->nullable()->change();
|
||||||
|
$table->string("basement")->nullable()->change();
|
||||||
|
$table->decimal('latitude', 15, 8)->nullable()->change();
|
||||||
|
$table->decimal('longitude', 15, 8)->nullable()->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('pbg_task_detail_data_lists', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('uid')->unique(); // UID from response
|
||||||
|
$table->string('name'); // Nama data
|
||||||
|
$table->text('description')->nullable(); // Deskripsi (bisa null)
|
||||||
|
$table->integer('status')->nullable(); // Status (1 = Sesuai, etc)
|
||||||
|
$table->string('status_name')->nullable(); // Nama status
|
||||||
|
$table->integer('data_type')->nullable(); // Tipe data (1 = Data Teknis Tanah, etc)
|
||||||
|
$table->string('data_type_name')->nullable(); // Nama tipe data
|
||||||
|
$table->text('file')->nullable(); // Path file
|
||||||
|
$table->text('note')->nullable(); // Catatan
|
||||||
|
|
||||||
|
// Foreign key ke pbg_task (1 to many relationship)
|
||||||
|
$table->string('pbg_task_uuid')->nullable();
|
||||||
|
$table->foreign('pbg_task_uuid')->references('uuid')->on('pbg_task')->onDelete('cascade');
|
||||||
|
|
||||||
|
// Indexes for better performance
|
||||||
|
$table->index('uid');
|
||||||
|
$table->index('pbg_task_uuid');
|
||||||
|
$table->index('status');
|
||||||
|
$table->index('data_type');
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('pbg_task_detail_data_lists');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('bigdata_resumes', function (Blueprint $table) {
|
||||||
|
$table->string('resume_type')->nullable();
|
||||||
|
$table->integer('business_rab_count')->default(0);
|
||||||
|
$table->integer('business_krk_count')->default(0);
|
||||||
|
$table->integer('non_business_rab_count')->default(0);
|
||||||
|
$table->integer('non_business_krk_count')->default(0);
|
||||||
|
$table->integer('non_business_dlh_count')->default(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('bigdata_resumes', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('resume_type');
|
||||||
|
$table->dropColumn('business_rab_count');
|
||||||
|
$table->dropColumn('business_krk_count');
|
||||||
|
$table->dropColumn('non_business_rab_count');
|
||||||
|
$table->dropColumn('non_business_krk_count');
|
||||||
|
$table->dropColumn('non_business_dlh_count');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('pbg_task', function (Blueprint $table) {
|
||||||
|
$table->boolean('is_valid')->default(true)->after('status');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('pbg_task', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('is_valid');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('bigdata_resumes', function (Blueprint $table) {
|
||||||
|
$table->renameColumn('non_business_dlh_count', 'business_dlh_count');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('bigdata_resumes', function (Blueprint $table) {
|
||||||
|
$table->renameColumn('business_dlh_count', 'non_business_dlh_count');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('spatial_plannings', function (Blueprint $table) {
|
||||||
|
$table->boolean('is_terbit')->default(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('spatial_plannings', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('is_terbit');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('pbg_task_payments', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('pbg_task_id')->constrained('pbg_task', 'id')->onDelete('cascade');
|
||||||
|
$table->string('pbg_task_uid');
|
||||||
|
$table->string('registration_number');
|
||||||
|
$table->string('sts_form_number')->nullable();
|
||||||
|
$table->date('payment_date')->nullable();
|
||||||
|
$table->decimal('pad_amount', 12, 2)->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Add index for better performance
|
||||||
|
$table->index('pbg_task_id');
|
||||||
|
$table->index('pbg_task_uid');
|
||||||
|
$table->index('registration_number');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('pbg_task_payments');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('pbg_statuses', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('pbg_task_uuid');
|
||||||
|
$table->integer('status');
|
||||||
|
$table->string('status_name');
|
||||||
|
$table->integer('slf_status')->nullable();
|
||||||
|
$table->string('slf_status_name')->nullable();
|
||||||
|
$table->date('due_date')->nullable();
|
||||||
|
|
||||||
|
// nested "data"
|
||||||
|
$table->string('uid')->nullable();
|
||||||
|
$table->text('note')->nullable();
|
||||||
|
$table->string('file')->nullable();
|
||||||
|
$table->date('data_due_date')->nullable();
|
||||||
|
$table->timestamp('data_created_at')->nullable();
|
||||||
|
|
||||||
|
$table->json('slf_data')->nullable(); // kalau nanti slf_data ada struktur JSON
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('pbg_statuses');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('pbg_task_payments', function (Blueprint $table) {
|
||||||
|
// Drop existing foreign key if present
|
||||||
|
if (Schema::hasColumn('pbg_task_payments', 'pbg_task_id')) {
|
||||||
|
$table->dropForeign(['pbg_task_id']);
|
||||||
|
// Make column nullable
|
||||||
|
$table->unsignedBigInteger('pbg_task_id')->nullable()->change();
|
||||||
|
// Recreate foreign key
|
||||||
|
$table->foreign('pbg_task_id')->references('id')->on('pbg_task')->cascadeOnDelete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop legacy columns if no longer needed
|
||||||
|
if (Schema::hasColumn('pbg_task_payments', 'registration_number')) {
|
||||||
|
$table->dropIndex(['registration_number']);
|
||||||
|
$table->dropColumn('registration_number');
|
||||||
|
}
|
||||||
|
if (Schema::hasColumn('pbg_task_payments', 'sts_form_number')) {
|
||||||
|
$table->dropColumn('sts_form_number');
|
||||||
|
}
|
||||||
|
if (Schema::hasColumn('pbg_task_payments', 'payment_date')) {
|
||||||
|
$table->dropColumn('payment_date');
|
||||||
|
}
|
||||||
|
if (Schema::hasColumn('pbg_task_payments', 'pad_amount')) {
|
||||||
|
$table->dropColumn('pad_amount');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make pbg_task_uid nullable
|
||||||
|
if (Schema::hasColumn('pbg_task_payments', 'pbg_task_uid')) {
|
||||||
|
$table->string('pbg_task_uid')->nullable()->change();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new columns (renamed for table conventions)
|
||||||
|
$table->integer('row_no')->nullable();
|
||||||
|
$table->string('consultation_type')->nullable();
|
||||||
|
$table->string('source_registration_number')->nullable();
|
||||||
|
$table->string('owner_name')->nullable();
|
||||||
|
$table->text('building_location')->nullable();
|
||||||
|
$table->string('building_function')->nullable();
|
||||||
|
$table->string('building_name')->nullable();
|
||||||
|
$table->date('application_date_raw')->nullable();
|
||||||
|
$table->string('verification_status')->nullable();
|
||||||
|
$table->string('application_status')->nullable();
|
||||||
|
$table->text('owner_address')->nullable();
|
||||||
|
$table->string('owner_phone')->nullable();
|
||||||
|
$table->string('owner_email')->nullable();
|
||||||
|
$table->date('note_date_raw')->nullable();
|
||||||
|
$table->text('document_shortage_note')->nullable();
|
||||||
|
$table->string('image_url')->nullable();
|
||||||
|
$table->string('krk_kkpr')->nullable();
|
||||||
|
$table->string('krk_number')->nullable();
|
||||||
|
$table->string('lh')->nullable();
|
||||||
|
$table->string('ska')->nullable();
|
||||||
|
$table->string('remarks')->nullable();
|
||||||
|
$table->string('helpdesk')->nullable();
|
||||||
|
$table->string('person_in_charge')->nullable();
|
||||||
|
$table->string('pbg_operator')->nullable();
|
||||||
|
$table->string('ownership')->nullable();
|
||||||
|
$table->string('taru_potential')->nullable();
|
||||||
|
$table->string('agency_validation')->nullable();
|
||||||
|
$table->string('retribution_category')->nullable();
|
||||||
|
$table->string('ba_tpt_number')->nullable();
|
||||||
|
$table->date('ba_tpt_date_raw')->nullable();
|
||||||
|
$table->string('ba_tpa_number')->nullable();
|
||||||
|
$table->date('ba_tpa_date_raw')->nullable();
|
||||||
|
$table->string('skrd_number')->nullable();
|
||||||
|
$table->date('skrd_date_raw')->nullable();
|
||||||
|
$table->string('ptsp_status')->nullable();
|
||||||
|
$table->string('issued_status')->nullable();
|
||||||
|
$table->date('payment_date_raw')->nullable();
|
||||||
|
$table->string('sts_format')->nullable();
|
||||||
|
$table->integer('issuance_year')->nullable();
|
||||||
|
$table->integer('current_year')->nullable();
|
||||||
|
$table->string('village')->nullable();
|
||||||
|
$table->string('district')->nullable();
|
||||||
|
$table->decimal('building_area', 18, 2)->nullable()->default(0);
|
||||||
|
$table->decimal('building_height', 18, 2)->nullable()->default(0);
|
||||||
|
$table->integer('floor_count')->nullable();
|
||||||
|
$table->integer('unit_count')->nullable();
|
||||||
|
$table->decimal('proposed_retribution', 18, 2)->nullable()->default(0);
|
||||||
|
$table->decimal('retribution_total_simbg', 18, 2)->nullable()->default(0);
|
||||||
|
$table->decimal('retribution_total_pad', 18, 2)->nullable()->default(0);
|
||||||
|
$table->decimal('penalty_amount', 18, 2)->nullable()->default(0);
|
||||||
|
$table->string('business_category')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('pbg_task_payments', function (Blueprint $table) {
|
||||||
|
if (Schema::hasColumn('pbg_task_payments', 'pbg_task_id')) {
|
||||||
|
// Drop FK, revert to not nullable, recreate FK
|
||||||
|
$table->dropForeign(['pbg_task_id']);
|
||||||
|
$table->unsignedBigInteger('pbg_task_id')->nullable(false)->change();
|
||||||
|
$table->foreign('pbg_task_id')->references('id')->on('pbg_task')->cascadeOnDelete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revert pbg_task_uid to not nullable
|
||||||
|
if (Schema::hasColumn('pbg_task_payments', 'pbg_task_uid')) {
|
||||||
|
$table->string('pbg_task_uid')->nullable(false)->change();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop the added columns
|
||||||
|
$columns = [
|
||||||
|
'row_no','consultation_type','source_registration_number','owner_name','building_location','building_function','building_name','application_date_raw',
|
||||||
|
'verification_status','application_status','owner_address','owner_phone','owner_email','note_date_raw','document_shortage_note',
|
||||||
|
'image_url','krk_kkpr','krk_number','lh','ska','remarks','helpdesk','person_in_charge','pbg_operator','ownership','taru_potential',
|
||||||
|
'agency_validation','retribution_category','ba_tpt_number','ba_tpt_date_raw','ba_tpa_number','ba_tpa_date_raw',
|
||||||
|
'skrd_number','skrd_date_raw','ptsp_status','issued_status','payment_date_raw','sts_format','issuance_year',
|
||||||
|
'current_year','village','district','building_area','building_height','floor_count','unit_count','proposed_retribution','retribution_total_simbg',
|
||||||
|
'retribution_total_pad','penalty_amount','business_category'
|
||||||
|
];
|
||||||
|
foreach ($columns as $col) {
|
||||||
|
if (Schema::hasColumn('pbg_task_payments', $col)) {
|
||||||
|
$table->dropColumn($col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore legacy columns
|
||||||
|
if (!Schema::hasColumn('pbg_task_payments', 'registration_number')) {
|
||||||
|
$table->string('registration_number');
|
||||||
|
$table->index('registration_number');
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('pbg_task_payments', 'sts_form_number')) {
|
||||||
|
$table->string('sts_form_number')->nullable();
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('pbg_task_payments', 'payment_date')) {
|
||||||
|
$table->date('payment_date')->nullable();
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('pbg_task_payments', 'pad_amount')) {
|
||||||
|
$table->decimal('pad_amount', 18, 2)->default(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -25,7 +25,7 @@ class GlobalSettingSeeder extends Seeder
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
"key" => "SIMBG_PASSWORD",
|
"key" => "SIMBG_PASSWORD",
|
||||||
"value" => "Simbg123",
|
"value" => "LogitechG29",
|
||||||
"type" => "string",
|
"type" => "string",
|
||||||
"description" => "Password SIMBG",
|
"description" => "Password SIMBG",
|
||||||
"created_at" => Carbon::now()->format("Y-m-d H:i:s"),
|
"created_at" => Carbon::now()->format("Y-m-d H:i:s"),
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ class MenuSeeder extends Seeder
|
|||||||
*/
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
|
Menu::whereIn('name', ['Data Pajak','Pajak'])->delete();
|
||||||
|
|
||||||
$menus = [
|
$menus = [
|
||||||
[
|
[
|
||||||
"name" => "Neng Bedas",
|
"name" => "Neng Bedas",
|
||||||
@@ -30,7 +32,7 @@ class MenuSeeder extends Seeder
|
|||||||
"sort_order" => 2,
|
"sort_order" => 2,
|
||||||
"children" => [
|
"children" => [
|
||||||
[
|
[
|
||||||
"name" => "Dashboard Pimpinan SIMBG",
|
"name" => "Dashboard Pimpinan (SIMBG)",
|
||||||
"url" => "dashboard.home",
|
"url" => "dashboard.home",
|
||||||
"icon" => null,
|
"icon" => null,
|
||||||
"sort_order" => 1,
|
"sort_order" => 1,
|
||||||
@@ -67,12 +69,6 @@ class MenuSeeder extends Seeder
|
|||||||
"icon" => null,
|
"icon" => null,
|
||||||
"sort_order" => 4,
|
"sort_order" => 4,
|
||||||
],
|
],
|
||||||
[
|
|
||||||
"name" => "Dashboard Pimpinan",
|
|
||||||
"url" => "dashboard.leader",
|
|
||||||
"icon" => null,
|
|
||||||
"sort_order" => 5,
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -193,6 +189,12 @@ class MenuSeeder extends Seeder
|
|||||||
"icon" => null,
|
"icon" => null,
|
||||||
"sort_order" => 9,
|
"sort_order" => 9,
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"name" => "Pajak",
|
||||||
|
"url" => "taxation",
|
||||||
|
"icon" => null,
|
||||||
|
"sort_order" => 10,
|
||||||
|
]
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -280,7 +282,7 @@ class MenuSeeder extends Seeder
|
|||||||
private function createOrUpdateMenu($menuData, $parentId = null){
|
private function createOrUpdateMenu($menuData, $parentId = null){
|
||||||
$menuData['parent_id'] = $parentId;
|
$menuData['parent_id'] = $parentId;
|
||||||
|
|
||||||
$menu = Menu::updateOrCreate(['name' => $menuData['name']], Arr::except($menuData, ['children']));
|
$menu = Menu::updateOrCreate(['url' => $menuData['url']], Arr::except($menuData, ['children']));
|
||||||
|
|
||||||
if(!empty($menuData['children'])){
|
if(!empty($menuData['children'])){
|
||||||
foreach($menuData['children'] as $child){
|
foreach($menuData['children'] as $child){
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class RetributionDataSeeder extends Seeder
|
|||||||
['id' => 9, 'code' => 'USH_BESAR', 'name' => 'Usaha Besar (Non-Mikro)', 'parent_id' => 4, 'level' => 2, 'is_free' => false, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()],
|
['id' => 9, 'code' => 'USH_BESAR', 'name' => 'Usaha Besar (Non-Mikro)', 'parent_id' => 4, 'level' => 2, 'is_free' => false, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()],
|
||||||
['id' => 10, 'code' => 'HUN_SEDH', 'name' => 'Hunian Sederhana <100', 'parent_id' => 5, 'level' => 2, 'is_free' => false, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()],
|
['id' => 10, 'code' => 'HUN_SEDH', 'name' => 'Hunian Sederhana <100', 'parent_id' => 5, 'level' => 2, 'is_free' => false, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()],
|
||||||
['id' => 11, 'code' => 'HUN_TSEDH', 'name' => 'Hunian Tidak Sederhana >100', 'parent_id' => 5, 'level' => 2, 'is_free' => false, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()],
|
['id' => 11, 'code' => 'HUN_TSEDH', 'name' => 'Hunian Tidak Sederhana >100', 'parent_id' => 5, 'level' => 2, 'is_free' => false, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()],
|
||||||
['id' => 12, 'code' => 'MBR', 'name' => 'Rumah Tinggal MBR', 'parent_id' => 5, 'level' => 2, 'is_free' => true, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()],
|
['id' => 12, 'code' => 'MBR', 'name' => 'Rumah Tinggal MBR', 'parent_id' => 5, 'level' => 2, 'is_free' => false, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Seed Retribution Indices berdasarkan Excel (with coefficient moved here)
|
// Seed Retribution Indices berdasarkan Excel (with coefficient moved here)
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ class UsersRoleMenuSeeder extends Seeder
|
|||||||
// Fetch all menus in a single query and index by name
|
// Fetch all menus in a single query and index by name
|
||||||
$menus = Menu::whereIn('name', [
|
$menus = Menu::whereIn('name', [
|
||||||
'Dashboard', 'Master', 'Settings', 'Data Settings', 'Data', 'Laporan', 'Neng Bedas',
|
'Dashboard', 'Master', 'Settings', 'Data Settings', 'Data', 'Laporan', 'Neng Bedas',
|
||||||
'Approval', 'Tools', 'Dashboard Pimpinan', 'Dashboard PBG', 'Users', 'Syncronize',
|
'Approval', 'Tools', 'Users', 'Syncronize', 'Dashboard Pimpinan (SIMBG)',
|
||||||
'Menu', 'Role', 'Setting Dashboard', 'PBG', 'Reklame', 'Usaha atau Industri', 'Pariwisata',
|
'Menu', 'Role', 'Setting Dashboard', 'PBG', 'Reklame', 'Usaha atau Industri', 'Pariwisata',
|
||||||
'Lap Pariwisata', 'UMKM', 'Dashboard Potensi', 'Tata Ruang', 'PDAM', 'PETA',
|
'Lap Pariwisata', 'UMKM', 'Dashboard Potensi', 'Tata Ruang', 'PDAM', 'PETA',
|
||||||
'Lap Pimpinan', 'Dalam Sistem', 'Luar Sistem', 'Google Sheets', 'TPA TPT',
|
'Lap Pimpinan', 'Dalam Sistem', 'Luar Sistem', 'Google Sheets', 'TPA TPT', 'Pajak',
|
||||||
'Approval Pejabat', 'Undangan', 'Rekap Pembayaran', 'Lap Rekap Data Pembayaran', 'Lap PBG (PTSP)', 'Lap Pertumbuhan'
|
'Approval Pejabat', 'Undangan', 'Rekap Pembayaran', 'Lap Rekap Data Pembayaran', 'Lap PBG (PTSP)', 'Lap Pertumbuhan'
|
||||||
])->get()->keyBy('name');
|
])->get()->keyBy('name');
|
||||||
|
|
||||||
@@ -32,14 +32,14 @@ class UsersRoleMenuSeeder extends Seeder
|
|||||||
$permissions = [
|
$permissions = [
|
||||||
'superadmin' => [
|
'superadmin' => [
|
||||||
'Dashboard', 'Master', 'Settings', 'Data Settings', 'Data', 'Laporan', 'Neng Bedas',
|
'Dashboard', 'Master', 'Settings', 'Data Settings', 'Data', 'Laporan', 'Neng Bedas',
|
||||||
'Approval', 'Tools', 'Dashboard Pimpinan', 'Dashboard PBG', 'Users', 'Syncronize',
|
'Approval', 'Tools', 'Users', 'Syncronize', 'Dashboard Pimpinan (SIMBG)',
|
||||||
'Menu', 'Role', 'Setting Dashboard', 'PBG', 'Reklame', 'Usaha atau Industri', 'Pariwisata',
|
'Menu', 'Role', 'Setting Dashboard', 'PBG', 'Reklame', 'Usaha atau Industri', 'Pariwisata',
|
||||||
'Lap Pariwisata', 'UMKM', 'Dashboard Potensi', 'Tata Ruang', 'PDAM', 'Dalam Sistem',
|
'Lap Pariwisata', 'UMKM', 'Dashboard Potensi', 'Tata Ruang', 'PDAM', 'Dalam Sistem',
|
||||||
'Luar Sistem', 'Lap Pimpinan', 'Google Sheets', 'TPA TPT', 'Approval Pejabat',
|
'Luar Sistem', 'Lap Pimpinan', 'Google Sheets', 'TPA TPT', 'Approval Pejabat',
|
||||||
'Undangan', 'Rekap Pembayaran', 'Lap Rekap Data Pembayaran', 'Lap PBG (PTSP)', 'Lap Pertumbuhan'
|
'Undangan', 'Rekap Pembayaran', 'Lap Rekap Data Pembayaran', 'Lap PBG (PTSP)', 'Lap Pertumbuhan', 'Pajak'
|
||||||
],
|
],
|
||||||
'user' => ['Dashboard', 'Data', 'Laporan', 'Neng Bedas',
|
'user' => ['Dashboard', 'Data', 'Laporan', 'Neng Bedas',
|
||||||
'Approval', 'Tools', 'Dashboard Pimpinan', 'Dashboard PBG', 'Users', 'Syncronize',
|
'Approval', 'Tools', 'Users', 'Syncronize', 'Dashboard Pimpinan (SIMBG)',
|
||||||
'Menu', 'Role', 'Setting Dashboard', 'PBG', 'Reklame', 'Usaha atau Industri', 'Pariwisata',
|
'Menu', 'Role', 'Setting Dashboard', 'PBG', 'Reklame', 'Usaha atau Industri', 'Pariwisata',
|
||||||
'Lap Pariwisata', 'UMKM', 'Dashboard Potensi', 'Tata Ruang', 'PDAM', 'Dalam Sistem',
|
'Lap Pariwisata', 'UMKM', 'Dashboard Potensi', 'Tata Ruang', 'PDAM', 'Dalam Sistem',
|
||||||
'Luar Sistem', 'Lap Pimpinan', 'Google Sheets', 'TPA TPT', 'Approval Pejabat',
|
'Luar Sistem', 'Lap Pimpinan', 'Google Sheets', 'TPA TPT', 'Approval Pejabat',
|
||||||
@@ -67,10 +67,10 @@ class UsersRoleMenuSeeder extends Seeder
|
|||||||
|
|
||||||
// Attach User to role super admin
|
// Attach User to role super admin
|
||||||
$accountSuperadmin = User::where('email', 'superadmin@sibedas.com')->first();
|
$accountSuperadmin = User::where('email', 'superadmin@sibedas.com')->first();
|
||||||
|
$accountDevelopment = User::where('email', 'development@sibedas.com')->first();
|
||||||
$accountUser = User::where('email', 'user@sibedas.com')->first();
|
$accountUser = User::where('email', 'user@sibedas.com')->first();
|
||||||
// $accountDefault = User::where('email','user@demo.com')->first();
|
|
||||||
$accountSuperadmin->roles()->sync([$roles['superadmin']->id]);
|
$accountSuperadmin->roles()->sync([$roles['superadmin']->id]);
|
||||||
|
$accountDevelopment->roles()->sync([$roles['superadmin']->id]);
|
||||||
$accountUser->roles()->sync([$roles['user']->id]);
|
$accountUser->roles()->sync([$roles['user']->id]);
|
||||||
// $accountDefault->roles()->sync([$roles['user']->id]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
82
docker-compose.local.yml
Normal file
82
docker-compose.local.yml
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
target: local
|
||||||
|
container_name: sibedas_app_local
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
APP_ENV: local
|
||||||
|
APP_DEBUG: true
|
||||||
|
APP_URL: http://localhost:8000
|
||||||
|
VITE_APP_URL: http://localhost:8000
|
||||||
|
DB_CONNECTION: mariadb
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 3306
|
||||||
|
DB_DATABASE: siebdas_20250815
|
||||||
|
DB_USERNAME: root
|
||||||
|
DB_PASSWORD: root
|
||||||
|
volumes:
|
||||||
|
- .:/var/www
|
||||||
|
# Exclude node_modules to prevent conflicts
|
||||||
|
- /var/www/node_modules
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
networks:
|
||||||
|
- sibedas_network_local
|
||||||
|
# Add user mapping for permission compatibility
|
||||||
|
user: "1000:1000"
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: sibedas_nginx_local
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8000:80"
|
||||||
|
volumes:
|
||||||
|
- .:/var/www
|
||||||
|
- ./docker/nginx/conf.d/app.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
depends_on:
|
||||||
|
- app
|
||||||
|
networks:
|
||||||
|
- sibedas_network_local
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: mariadb:10.6
|
||||||
|
container_name: sibedas_db_local
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: root
|
||||||
|
MYSQL_DATABASE: siebdas_20250815
|
||||||
|
ports:
|
||||||
|
- "3306:3306"
|
||||||
|
volumes:
|
||||||
|
- sibedas_dbdata_local:/var/lib/mysql
|
||||||
|
- ./sibedas.sql:/docker-entrypoint-initdb.d/sibedas.sql
|
||||||
|
networks:
|
||||||
|
- sibedas_network_local
|
||||||
|
|
||||||
|
vite:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
target: development
|
||||||
|
container_name: sibedas_vite_local
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
VITE_APP_URL: http://localhost:8000
|
||||||
|
volumes:
|
||||||
|
- .:/var/www
|
||||||
|
- /var/www/node_modules
|
||||||
|
ports:
|
||||||
|
- "5173:5173"
|
||||||
|
networks:
|
||||||
|
- sibedas_network_local
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
sibedas_dbdata_local:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
sibedas_network_local:
|
||||||
|
driver: bridge
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
services:
|
services:
|
||||||
|
# Sibedas Application Container (Internal)
|
||||||
app:
|
app:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
@@ -7,74 +8,214 @@ services:
|
|||||||
container_name: sibedas_app
|
container_name: sibedas_app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
APP_ENV: local
|
APP_ENV: ${APP_ENV:-production}
|
||||||
APP_DEBUG: true
|
APP_DEBUG: ${APP_DEBUG:-false}
|
||||||
APP_URL: http://localhost
|
APP_KEY: ${APP_KEY}
|
||||||
VITE_APP_URL: http://localhost
|
APP_URL: ${APP_URL:-https://sibedas.yourdomain.com}
|
||||||
DB_CONNECTION: mariadb
|
VITE_APP_URL: ${VITE_APP_URL:-https://sibedas.yourdomain.com}
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DB_CONNECTION: ${DB_CONNECTION:-mariadb}
|
||||||
DB_HOST: db
|
DB_HOST: db
|
||||||
DB_PORT: 3306
|
DB_PORT: 3306
|
||||||
DB_DATABASE: sibedas_db
|
DB_DATABASE: ${DB_DATABASE:-sibedas}
|
||||||
DB_USERNAME: root
|
DB_USERNAME: ${DB_USERNAME:-sibedas_user}
|
||||||
DB_PASSWORD: root
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
volumes:
|
|
||||||
- .:/var/www
|
|
||||||
depends_on:
|
|
||||||
- db
|
|
||||||
networks:
|
|
||||||
- sibedas_net
|
|
||||||
|
|
||||||
nginx:
|
# Cache Configuration (using database)
|
||||||
|
CACHE_DRIVER: ${CACHE_DRIVER:-database}
|
||||||
|
|
||||||
|
# Session Configuration (using database)
|
||||||
|
SESSION_DRIVER: ${SESSION_DRIVER:-database}
|
||||||
|
SESSION_LIFETIME: ${SESSION_LIFETIME:-120}
|
||||||
|
|
||||||
|
# Queue Configuration (using database)
|
||||||
|
QUEUE_CONNECTION: ${QUEUE_CONNECTION:-database}
|
||||||
|
|
||||||
|
# Mail Configuration
|
||||||
|
MAIL_MAILER: ${MAIL_MAILER:-smtp}
|
||||||
|
MAIL_HOST: ${MAIL_HOST}
|
||||||
|
MAIL_PORT: ${MAIL_PORT:-587}
|
||||||
|
MAIL_USERNAME: ${MAIL_USERNAME}
|
||||||
|
MAIL_PASSWORD: ${MAIL_PASSWORD}
|
||||||
|
MAIL_ENCRYPTION: ${MAIL_ENCRYPTION:-tls}
|
||||||
|
MAIL_FROM_ADDRESS: ${MAIL_FROM_ADDRESS}
|
||||||
|
MAIL_FROM_NAME: ${MAIL_FROM_NAME:-"Sibedas"}
|
||||||
|
|
||||||
|
# Google Sheets API
|
||||||
|
SPREAD_SHEET_ID: ${SPREAD_SHEET_ID}
|
||||||
|
volumes:
|
||||||
|
# Only mount specific directories for production security
|
||||||
|
- sibedas_app_storage:/var/www/storage
|
||||||
|
- sibedas_app_bootstrap_cache:/var/www/bootstrap/cache
|
||||||
|
- ./public:/var/www/public:ro
|
||||||
|
- ./docker/supervisor:/etc/supervisor/conf.d:ro
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- sibedas_network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "php", "-v"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 60s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 1G
|
||||||
|
cpus: "1.0"
|
||||||
|
reservations:
|
||||||
|
memory: 512M
|
||||||
|
cpus: "0.5"
|
||||||
|
# Use Supervisor for queue and scheduler
|
||||||
|
command:
|
||||||
|
["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||||
|
|
||||||
|
# Internal Nginx for Sibedas App
|
||||||
|
nginx-internal:
|
||||||
image: nginx:alpine
|
image: nginx:alpine
|
||||||
container_name: sibedas_nginx
|
container_name: sibedas_nginx_internal
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./public:/var/www/public:ro
|
||||||
|
- ./docker/nginx/conf.d/sibedas-internal.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
- sibedas_nginx_internal_logs:/var/log/nginx
|
||||||
|
depends_on:
|
||||||
|
app:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- sibedas_network
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"wget",
|
||||||
|
"--quiet",
|
||||||
|
"--tries=1",
|
||||||
|
"--spider",
|
||||||
|
"http://localhost/health-check",
|
||||||
|
]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 128M
|
||||||
|
cpus: "0.25"
|
||||||
|
|
||||||
|
# Reverse Proxy Nginx (Main Entry Point)
|
||||||
|
nginx-proxy:
|
||||||
|
build:
|
||||||
|
context: ./docker/nginx
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: sibedas_nginx_proxy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "8000:80"
|
- "${NGINX_HTTP_PORT:-80}:80"
|
||||||
|
- "${NGINX_HTTPS_PORT:-443}:443"
|
||||||
|
environment:
|
||||||
|
DOMAIN: ${DOMAIN:-sibedas.yourdomain.com}
|
||||||
|
EMAIL: ${EMAIL:-admin@yourdomain.com}
|
||||||
|
SSL_TYPE: ${SSL_TYPE:-self-signed}
|
||||||
volumes:
|
volumes:
|
||||||
- .:/var/www
|
- sibedas_nginx_proxy_logs:/var/log/nginx
|
||||||
- ./docker/nginx/conf.d/app.conf:/etc/nginx/conf.d/default.conf
|
- sibedas_ssl_certs:/etc/nginx/ssl
|
||||||
|
- sibedas_letsencrypt:/etc/letsencrypt
|
||||||
depends_on:
|
depends_on:
|
||||||
- app
|
nginx-internal:
|
||||||
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
- sibedas_net
|
- sibedas_network
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"wget",
|
||||||
|
"--quiet",
|
||||||
|
"--tries=1",
|
||||||
|
"--spider",
|
||||||
|
"http://localhost/health-check",
|
||||||
|
]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 256M
|
||||||
|
cpus: "0.5"
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: mariadb:10.6
|
image: mariadb:10.6
|
||||||
container_name: sibedas_db
|
container_name: sibedas_db
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: root
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
||||||
MYSQL_DATABASE: sibedas_db
|
MYSQL_DATABASE: ${DB_DATABASE:-sibedas}
|
||||||
MYSQL_USER: root
|
MYSQL_USER: ${DB_USERNAME:-sibedas_user}
|
||||||
MYSQL_PASSWORD: root
|
MYSQL_PASSWORD: ${DB_PASSWORD}
|
||||||
|
MYSQL_INNODB_BUFFER_POOL_SIZE: ${MYSQL_INNODB_BUFFER_POOL_SIZE:-1G}
|
||||||
ports:
|
ports:
|
||||||
- "3306:3306"
|
# Only expose if needed for external access
|
||||||
|
- "${DB_EXTERNAL_PORT:-3306}:3306"
|
||||||
volumes:
|
volumes:
|
||||||
- dbdata:/var/lib/mysql
|
- sibedas_dbdata:/var/lib/mysql
|
||||||
- ./sibedas.sql:/docker-entrypoint-initdb.d/sibedas.sql
|
- ./sibedas.sql:/docker-entrypoint-initdb.d/sibedas.sql:ro
|
||||||
|
- ./docker/mysql/conf.d:/etc/mysql/conf.d:ro
|
||||||
|
- sibedas_db_logs:/var/log/mysql
|
||||||
networks:
|
networks:
|
||||||
- sibedas_net
|
- sibedas_network
|
||||||
|
healthcheck:
|
||||||
vite:
|
test:
|
||||||
build:
|
[
|
||||||
context: .
|
"CMD",
|
||||||
dockerfile: Dockerfile
|
"mysqladmin",
|
||||||
target: development
|
"ping",
|
||||||
container_name: sibedas_vite
|
"-h",
|
||||||
restart: unless-stopped
|
"localhost",
|
||||||
environment:
|
"-u",
|
||||||
VITE_APP_URL: http://localhost
|
"${DB_USERNAME:-sibedas_user}",
|
||||||
volumes:
|
"-p${DB_PASSWORD}",
|
||||||
- .:/var/www
|
]
|
||||||
- /var/www/node_modules
|
interval: 30s
|
||||||
ports:
|
timeout: 10s
|
||||||
- "5173:5173"
|
retries: 3
|
||||||
networks:
|
start_period: 60s
|
||||||
- sibedas_net
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 2G
|
||||||
|
cpus: "1.0"
|
||||||
|
reservations:
|
||||||
|
memory: 1G
|
||||||
|
cpus: "0.5"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
dbdata:
|
sibedas_dbdata:
|
||||||
|
driver: local
|
||||||
|
sibedas_app_storage:
|
||||||
|
driver: local
|
||||||
|
sibedas_app_bootstrap_cache:
|
||||||
|
driver: local
|
||||||
|
sibedas_nginx_internal_logs:
|
||||||
|
driver: local
|
||||||
|
sibedas_nginx_proxy_logs:
|
||||||
|
driver: local
|
||||||
|
sibedas_db_logs:
|
||||||
|
driver: local
|
||||||
|
sibedas_ssl_certs:
|
||||||
|
driver: local
|
||||||
|
sibedas_letsencrypt:
|
||||||
|
driver: local
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
sibedas_net:
|
sibedas_network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.20.0.0/16
|
||||||
|
|||||||
55
docker/mysql/conf.d/production.cnf
Normal file
55
docker/mysql/conf.d/production.cnf
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
[mysqld]
|
||||||
|
# Basic Settings
|
||||||
|
default-storage-engine = innodb
|
||||||
|
sql-mode = "STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"
|
||||||
|
max_connections = 200
|
||||||
|
max_user_connections = 180
|
||||||
|
|
||||||
|
# Character Set
|
||||||
|
character-set-server = utf8mb4
|
||||||
|
collation-server = utf8mb4_unicode_ci
|
||||||
|
|
||||||
|
# InnoDB Settings
|
||||||
|
innodb_buffer_pool_size = 1G
|
||||||
|
innodb_buffer_pool_instances = 4
|
||||||
|
innodb_log_file_size = 256M
|
||||||
|
innodb_log_buffer_size = 64M
|
||||||
|
innodb_flush_log_at_trx_commit = 2
|
||||||
|
innodb_flush_method = O_DIRECT
|
||||||
|
innodb_file_per_table = 1
|
||||||
|
innodb_open_files = 400
|
||||||
|
|
||||||
|
# Query Cache (disabled in MySQL 8.0+, but kept for compatibility)
|
||||||
|
query_cache_type = OFF
|
||||||
|
query_cache_size = 0
|
||||||
|
|
||||||
|
# Temp Tables
|
||||||
|
tmp_table_size = 64M
|
||||||
|
max_heap_table_size = 64M
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
slow_query_log = 1
|
||||||
|
slow_query_log_file = /var/log/mysql/slow.log
|
||||||
|
long_query_time = 2
|
||||||
|
log_queries_not_using_indexes = 1
|
||||||
|
|
||||||
|
# Binary Logging
|
||||||
|
log-bin = mysql-bin
|
||||||
|
binlog_format = ROW
|
||||||
|
expire_logs_days = 7
|
||||||
|
max_binlog_size = 100M
|
||||||
|
|
||||||
|
# Safety
|
||||||
|
max_allowed_packet = 64M
|
||||||
|
bind-address = 0.0.0.0
|
||||||
|
|
||||||
|
# Performance Schema
|
||||||
|
performance_schema = ON
|
||||||
|
performance_schema_max_table_instances = 400
|
||||||
|
performance_schema_max_table_handles = 4000
|
||||||
|
|
||||||
|
[mysql]
|
||||||
|
default-character-set = utf8mb4
|
||||||
|
|
||||||
|
[client]
|
||||||
|
default-character-set = utf8mb4
|
||||||
32
docker/nginx/Dockerfile
Normal file
32
docker/nginx/Dockerfile
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Install required packages
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
openssl \
|
||||||
|
certbot \
|
||||||
|
certbot-nginx \
|
||||||
|
bash
|
||||||
|
|
||||||
|
# Create SSL directory
|
||||||
|
RUN mkdir -p /etc/nginx/ssl
|
||||||
|
|
||||||
|
# Copy SSL certificates (if they exist)
|
||||||
|
COPY ssl/ /etc/nginx/ssl/
|
||||||
|
|
||||||
|
# Copy Nginx configuration
|
||||||
|
COPY conf.d/ /etc/nginx/conf.d/
|
||||||
|
|
||||||
|
# Create log directories
|
||||||
|
RUN mkdir -p /var/log/nginx
|
||||||
|
|
||||||
|
# Copy SSL setup script
|
||||||
|
COPY ssl-setup.sh /usr/local/bin/ssl-setup.sh
|
||||||
|
RUN chmod +x /usr/local/bin/ssl-setup.sh
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||||
|
CMD wget --quiet --tries=1 --spider http://localhost/health-check || exit 1
|
||||||
|
|
||||||
|
EXPOSE 80 443
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
113
docker/nginx/conf.d/production.conf
Normal file
113
docker/nginx/conf.d/production.conf
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name yourdomain.com www.yourdomain.com;
|
||||||
|
|
||||||
|
# Redirect HTTP to HTTPS
|
||||||
|
return 301 https://$server_name$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name yourdomain.com www.yourdomain.com;
|
||||||
|
|
||||||
|
root /var/www/public;
|
||||||
|
index index.php index.html index.htm;
|
||||||
|
|
||||||
|
# SSL Configuration
|
||||||
|
ssl_certificate /etc/nginx/ssl/certificate.crt;
|
||||||
|
ssl_certificate_key /etc/nginx/ssl/private.key;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
|
||||||
|
ssl_prefer_server_ciphers off;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_timeout 10m;
|
||||||
|
|
||||||
|
# Security Headers
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||||
|
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
|
||||||
|
# Gzip Compression
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
|
gzip_types
|
||||||
|
text/plain
|
||||||
|
text/css
|
||||||
|
text/xml
|
||||||
|
text/javascript
|
||||||
|
application/x-javascript
|
||||||
|
application/xml+rss
|
||||||
|
application/javascript
|
||||||
|
application/json
|
||||||
|
application/xml
|
||||||
|
image/svg+xml;
|
||||||
|
|
||||||
|
# Client Max Body Size
|
||||||
|
client_max_body_size 100M;
|
||||||
|
|
||||||
|
# Health check endpoint
|
||||||
|
location /health-check {
|
||||||
|
access_log off;
|
||||||
|
return 200 "healthy\n";
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle static files
|
||||||
|
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
access_log off;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deny access to sensitive files
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
access_log off;
|
||||||
|
log_not_found off;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/(\.env|\.git|composer\.(json|lock)|package\.(json|lock)|webpack\.(mix\.js|config\.js)|.*\.md)$ {
|
||||||
|
deny all;
|
||||||
|
access_log off;
|
||||||
|
log_not_found off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Laravel-specific rules
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
fastcgi_pass app:9000;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
||||||
|
include fastcgi_params;
|
||||||
|
|
||||||
|
# Production optimizations
|
||||||
|
fastcgi_buffer_size 128k;
|
||||||
|
fastcgi_buffers 4 256k;
|
||||||
|
fastcgi_busy_buffers_size 256k;
|
||||||
|
fastcgi_temp_file_write_size 256k;
|
||||||
|
fastcgi_connect_timeout 60s;
|
||||||
|
fastcgi_send_timeout 60s;
|
||||||
|
fastcgi_read_timeout 60s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Error pages
|
||||||
|
error_page 404 /index.php;
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
access_log /var/log/nginx/access.log;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
}
|
||||||
137
docker/nginx/conf.d/reverse-proxy.conf
Normal file
137
docker/nginx/conf.d/reverse-proxy.conf
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# Reverse Proxy Configuration for Sibedas PBG Web
|
||||||
|
# This configuration handles SSL termination and routes traffic to the appropriate containers
|
||||||
|
|
||||||
|
# Upstream for Sibedas application
|
||||||
|
upstream sibedas_backend {
|
||||||
|
server sibedas_app:9000;
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Rate limiting
|
||||||
|
limit_req_zone $binary_remote_addr zone=sibedas_limit:10m rate=10r/s;
|
||||||
|
|
||||||
|
# HTTP to HTTPS redirect
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
# Redirect all HTTP traffic to HTTPS
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTPS Server for Sibedas
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name sibedas.yourdomain.com; # Change this to your domain
|
||||||
|
|
||||||
|
# SSL Configuration
|
||||||
|
ssl_certificate /etc/nginx/ssl/sibedas.crt;
|
||||||
|
ssl_certificate_key /etc/nginx/ssl/sibedas.key;
|
||||||
|
|
||||||
|
# SSL Security Settings
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
|
||||||
|
ssl_prefer_server_ciphers off;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_timeout 10m;
|
||||||
|
|
||||||
|
# Security Headers
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
add_header X-Frame-Options DENY always;
|
||||||
|
add_header X-Content-Type-Options nosniff always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none';" always;
|
||||||
|
|
||||||
|
# Rate limiting
|
||||||
|
limit_req zone=sibedas_limit burst=20 nodelay;
|
||||||
|
|
||||||
|
# Client max body size
|
||||||
|
client_max_body_size 100M;
|
||||||
|
|
||||||
|
# Gzip compression
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_types
|
||||||
|
text/plain
|
||||||
|
text/css
|
||||||
|
text/xml
|
||||||
|
text/javascript
|
||||||
|
application/json
|
||||||
|
application/javascript
|
||||||
|
application/xml+rss
|
||||||
|
application/atom+xml
|
||||||
|
image/svg+xml;
|
||||||
|
|
||||||
|
# Root directory
|
||||||
|
root /var/www/public;
|
||||||
|
index index.php index.html index.htm;
|
||||||
|
|
||||||
|
# Handle Laravel routes
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
# PHP-FPM configuration
|
||||||
|
location ~ \.php$ {
|
||||||
|
fastcgi_pass sibedas_backend;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
||||||
|
include fastcgi_params;
|
||||||
|
|
||||||
|
# FastCGI optimizations
|
||||||
|
fastcgi_buffers 16 16k;
|
||||||
|
fastcgi_buffer_size 32k;
|
||||||
|
fastcgi_connect_timeout 300;
|
||||||
|
fastcgi_send_timeout 300;
|
||||||
|
fastcgi_read_timeout 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Static files caching
|
||||||
|
location ~* \.(jpg|jpeg|png|gif|ico|css|js|pdf|txt)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deny access to sensitive files
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /\.ht {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check endpoint
|
||||||
|
location /health-check {
|
||||||
|
access_log off;
|
||||||
|
return 200 "healthy\n";
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
access_log /var/log/nginx/sibedas_access.log;
|
||||||
|
error_log /var/log/nginx/sibedas_error.log;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Additional server blocks for other applications can be added here
|
||||||
|
# Example:
|
||||||
|
# server {
|
||||||
|
# listen 443 ssl http2;
|
||||||
|
# server_name other-app.yourdomain.com;
|
||||||
|
#
|
||||||
|
# ssl_certificate /etc/nginx/ssl/other-app.crt;
|
||||||
|
# ssl_certificate_key /etc/nginx/ssl/other-app.key;
|
||||||
|
#
|
||||||
|
# location / {
|
||||||
|
# proxy_pass http://other_app_container:port;
|
||||||
|
# proxy_set_header Host $host;
|
||||||
|
# proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
# }
|
||||||
|
# }
|
||||||
58
docker/nginx/conf.d/sibedas-internal.conf
Normal file
58
docker/nginx/conf.d/sibedas-internal.conf
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Internal Nginx Configuration for Sibedas PBG Web
|
||||||
|
# This configuration is for the internal container, accessed by reverse proxy
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# Root directory
|
||||||
|
root /var/www/public;
|
||||||
|
index index.php index.html index.htm;
|
||||||
|
|
||||||
|
# Handle Laravel routes
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
# PHP-FPM configuration
|
||||||
|
location ~ \.php$ {
|
||||||
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
||||||
|
include fastcgi_params;
|
||||||
|
|
||||||
|
# FastCGI optimizations
|
||||||
|
fastcgi_buffers 16 16k;
|
||||||
|
fastcgi_buffer_size 32k;
|
||||||
|
fastcgi_connect_timeout 300;
|
||||||
|
fastcgi_send_timeout 300;
|
||||||
|
fastcgi_read_timeout 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Static files caching
|
||||||
|
location ~* \.(jpg|jpeg|png|gif|ico|css|js|pdf|txt)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deny access to sensitive files
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /\.ht {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check endpoint
|
||||||
|
location /health-check {
|
||||||
|
access_log off;
|
||||||
|
return 200 "healthy\n";
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
access_log /var/log/nginx/sibedas_internal_access.log;
|
||||||
|
error_log /var/log/nginx/sibedas_internal_error.log;
|
||||||
|
}
|
||||||
123
docker/nginx/ssl-setup.sh
Normal file
123
docker/nginx/ssl-setup.sh
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# SSL Setup Script for Sibedas PBG Web
|
||||||
|
# This script handles SSL certificate generation and renewal
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DOMAIN="${DOMAIN:-sibedas.yourdomain.com}"
|
||||||
|
EMAIL="${EMAIL:-admin@yourdomain.com}"
|
||||||
|
SSL_DIR="/etc/nginx/ssl"
|
||||||
|
CERT_FILE="$SSL_DIR/sibedas.crt"
|
||||||
|
KEY_FILE="$SSL_DIR/sibedas.key"
|
||||||
|
|
||||||
|
# Function to generate self-signed certificate
|
||||||
|
generate_self_signed() {
|
||||||
|
echo "Generating self-signed SSL certificate for $DOMAIN..."
|
||||||
|
|
||||||
|
# Create SSL directory if it doesn't exist
|
||||||
|
mkdir -p "$SSL_DIR"
|
||||||
|
|
||||||
|
# Generate self-signed certificate
|
||||||
|
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||||
|
-keyout "$KEY_FILE" \
|
||||||
|
-out "$CERT_FILE" \
|
||||||
|
-subj "/C=ID/ST=Jakarta/L=Jakarta/O=Sibedas/OU=IT/CN=$DOMAIN/emailAddress=$EMAIL"
|
||||||
|
|
||||||
|
echo "Self-signed certificate generated successfully!"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to setup Let's Encrypt certificate
|
||||||
|
setup_letsencrypt() {
|
||||||
|
echo "Setting up Let's Encrypt certificate for $DOMAIN..."
|
||||||
|
|
||||||
|
# Check if certbot is available
|
||||||
|
if ! command -v certbot &> /dev/null; then
|
||||||
|
echo "Certbot not found. Installing..."
|
||||||
|
apk add --no-cache certbot certbot-nginx
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stop nginx temporarily
|
||||||
|
nginx -s stop || true
|
||||||
|
|
||||||
|
# Get certificate
|
||||||
|
certbot certonly --standalone \
|
||||||
|
--email "$EMAIL" \
|
||||||
|
--agree-tos \
|
||||||
|
--no-eff-email \
|
||||||
|
-d "$DOMAIN"
|
||||||
|
|
||||||
|
# Copy certificates to nginx ssl directory
|
||||||
|
cp /etc/letsencrypt/live/$DOMAIN/fullchain.pem "$CERT_FILE"
|
||||||
|
cp /etc/letsencrypt/live/$DOMAIN/privkey.pem "$KEY_FILE"
|
||||||
|
|
||||||
|
# Set proper permissions
|
||||||
|
chmod 644 "$CERT_FILE"
|
||||||
|
chmod 600 "$KEY_FILE"
|
||||||
|
|
||||||
|
# Start nginx
|
||||||
|
nginx
|
||||||
|
|
||||||
|
echo "Let's Encrypt certificate setup completed!"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to renew Let's Encrypt certificate
|
||||||
|
renew_certificate() {
|
||||||
|
echo "Renewing Let's Encrypt certificate..."
|
||||||
|
|
||||||
|
certbot renew --quiet
|
||||||
|
|
||||||
|
# Copy renewed certificates
|
||||||
|
cp /etc/letsencrypt/live/$DOMAIN/fullchain.pem "$CERT_FILE"
|
||||||
|
cp /etc/letsencrypt/live/$DOMAIN/privkey.pem "$KEY_FILE"
|
||||||
|
|
||||||
|
# Reload nginx
|
||||||
|
nginx -s reload
|
||||||
|
|
||||||
|
echo "Certificate renewal completed!"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check certificate validity
|
||||||
|
check_certificate() {
|
||||||
|
if [ -f "$CERT_FILE" ] && [ -f "$KEY_FILE" ]; then
|
||||||
|
echo "Certificate files exist."
|
||||||
|
echo "Certificate details:"
|
||||||
|
openssl x509 -in "$CERT_FILE" -text -noout | grep -E "(Subject:|Not Before|Not After)"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "Certificate files not found."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main script logic
|
||||||
|
case "${1:-setup}" in
|
||||||
|
"setup")
|
||||||
|
if [ "$SSL_TYPE" = "letsencrypt" ]; then
|
||||||
|
setup_letsencrypt
|
||||||
|
else
|
||||||
|
generate_self_signed
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
"renew")
|
||||||
|
renew_certificate
|
||||||
|
;;
|
||||||
|
"check")
|
||||||
|
check_certificate
|
||||||
|
;;
|
||||||
|
"self-signed")
|
||||||
|
generate_self_signed
|
||||||
|
;;
|
||||||
|
"letsencrypt")
|
||||||
|
setup_letsencrypt
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 {setup|renew|check|self-signed|letsencrypt}"
|
||||||
|
echo ""
|
||||||
|
echo "Environment variables:"
|
||||||
|
echo " DOMAIN: Domain name (default: sibedas.yourdomain.com)"
|
||||||
|
echo " EMAIL: Email address for Let's Encrypt (default: admin@yourdomain.com)"
|
||||||
|
echo " SSL_TYPE: Type of SSL (letsencrypt or self-signed, default: self-signed)"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
14
docker/nginx/ssl/.gitignore
vendored
Normal file
14
docker/nginx/ssl/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# SSL Certificates - Do not commit these files
|
||||||
|
*.crt
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
*.p12
|
||||||
|
*.pfx
|
||||||
|
|
||||||
|
# Let's Encrypt
|
||||||
|
letsencrypt/
|
||||||
|
live/
|
||||||
|
archive/
|
||||||
|
|
||||||
|
# Keep this directory in git but ignore certificate files
|
||||||
|
!.gitignore
|
||||||
1
docker/php/memory-limit.ini
Normal file
1
docker/php/memory-limit.ini
Normal file
@@ -0,0 +1 @@
|
|||||||
|
memory_limit=512M
|
||||||
@@ -6,13 +6,13 @@ pidfile=/var/run/supervisord.pid
|
|||||||
|
|
||||||
[program:laravel-queue-worker]
|
[program:laravel-queue-worker]
|
||||||
process_name=%(program_name)s_%(process_num)02d
|
process_name=%(program_name)s_%(process_num)02d
|
||||||
command=php /var/www/pupr/artisan queue:work --queue=default --timeout=82800 --tries=3 --memory=512 --sleep=3
|
command=php /var/www/artisan queue:work --queue=default --timeout=82800 --tries=3 --memory=512 --sleep=3
|
||||||
directory=/var/www/pupr
|
directory=/var/www
|
||||||
autostart=true
|
autostart=true
|
||||||
autorestart=true
|
autorestart=true
|
||||||
numprocs=2
|
numprocs=2
|
||||||
redirect_stderr=true
|
redirect_stderr=true
|
||||||
stdout_logfile=/var/www/pupr/storage/logs/queue-worker.log
|
stdout_logfile=/var/www/storage/logs/queue-worker.log
|
||||||
stdout_logfile_maxbytes=10MB
|
stdout_logfile_maxbytes=10MB
|
||||||
stdout_logfile_backups=5
|
stdout_logfile_backups=5
|
||||||
stopasgroup=true
|
stopasgroup=true
|
||||||
@@ -22,13 +22,13 @@ priority=10
|
|||||||
|
|
||||||
[program:laravel-scheduler]
|
[program:laravel-scheduler]
|
||||||
process_name=%(program_name)s_%(process_num)02d
|
process_name=%(program_name)s_%(process_num)02d
|
||||||
command=php /var/www/pupr/artisan schedule:work
|
command=php /var/www/artisan schedule:work
|
||||||
directory=/var/www/pupr
|
directory=/var/www
|
||||||
autostart=true
|
autostart=true
|
||||||
autorestart=true
|
autorestart=true
|
||||||
numprocs=1
|
numprocs=1
|
||||||
redirect_stderr=true
|
redirect_stderr=true
|
||||||
stdout_logfile=/var/www/pupr/storage/logs/scheduler.log
|
stdout_logfile=/var/www/storage/logs/scheduler.log
|
||||||
stdout_logfile_maxbytes=10MB
|
stdout_logfile_maxbytes=10MB
|
||||||
stdout_logfile_backups=5
|
stdout_logfile_backups=5
|
||||||
stopasgroup=true
|
stopasgroup=true
|
||||||
|
|||||||
449
docs/README.md
Normal file
449
docs/README.md
Normal file
@@ -0,0 +1,449 @@
|
|||||||
|
# Sibedas PBG Web - Documentation
|
||||||
|
|
||||||
|
Dokumentasi lengkap untuk setup dan penggunaan aplikasi Sibedas PBG Web.
|
||||||
|
|
||||||
|
## 📋 Table of Contents
|
||||||
|
|
||||||
|
1. [Quick Start](#-quick-start)
|
||||||
|
2. [Architecture](#-architecture)
|
||||||
|
3. [Environment Setup](#-environment-setup)
|
||||||
|
4. [Production Deployment](#-production-deployment)
|
||||||
|
5. [SSL Configuration](#-ssl-configuration)
|
||||||
|
6. [Monitoring](#-monitoring)
|
||||||
|
7. [Troubleshooting](#-troubleshooting)
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Docker & Docker Compose
|
||||||
|
- Domain name (untuk production)
|
||||||
|
- Port 80 dan 443 terbuka (untuk Let's Encrypt)
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone repository
|
||||||
|
git clone <repository-url>
|
||||||
|
cd sibedas-pbg-web
|
||||||
|
|
||||||
|
# Setup local environment
|
||||||
|
./scripts/setup-local.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy environment file
|
||||||
|
cp env.production.example .env
|
||||||
|
|
||||||
|
# Edit environment variables
|
||||||
|
nano .env
|
||||||
|
|
||||||
|
# Deploy dengan reverse proxy dan SSL
|
||||||
|
./scripts/setup-reverse-proxy.sh setup
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏗️ Architecture
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
```
|
||||||
|
Browser → Port 8000 → Nginx → PHP-FPM → MariaDB
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production dengan Reverse Proxy
|
||||||
|
|
||||||
|
```
|
||||||
|
Internet → Reverse Proxy (80/443) → Internal Nginx → PHP-FPM → MariaDB
|
||||||
|
```
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
- **Reverse Proxy Nginx**: Entry point, SSL termination, routing
|
||||||
|
- **Internal Nginx**: Serves Sibedas application
|
||||||
|
- **Application Container**: PHP-FPM with Supervisor (queue & scheduler)
|
||||||
|
- **Database Container**: MariaDB with backup import
|
||||||
|
|
||||||
|
## ⚙️ Environment Setup
|
||||||
|
|
||||||
|
### Required Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Domain & SSL
|
||||||
|
DOMAIN=sibedas.yourdomain.com
|
||||||
|
EMAIL=admin@yourdomain.com
|
||||||
|
SSL_TYPE=self-signed # atau letsencrypt
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DB_PASSWORD=your_secure_password
|
||||||
|
MYSQL_ROOT_PASSWORD=your_root_password
|
||||||
|
|
||||||
|
# Laravel
|
||||||
|
APP_KEY=base64:your_app_key_here
|
||||||
|
APP_URL=https://sibedas.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generate App Key
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec app php artisan key:generate
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Production Deployment
|
||||||
|
|
||||||
|
### Step-by-Step Production Deployment
|
||||||
|
|
||||||
|
#### 1. **Server Preparation**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update system
|
||||||
|
sudo apt update && sudo apt upgrade -y
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Logout and login again for group changes
|
||||||
|
exit
|
||||||
|
# SSH back to server
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. **Clone Repository**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone project
|
||||||
|
git clone <repository-url>
|
||||||
|
cd sibedas-pbg-web
|
||||||
|
|
||||||
|
# Set proper permissions
|
||||||
|
chmod +x scripts/*.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. **Environment Configuration**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy environment template
|
||||||
|
cp env.production.example .env
|
||||||
|
|
||||||
|
# Edit environment variables
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
**Required Environment Variables:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Domain & SSL
|
||||||
|
DOMAIN=sibedas.yourdomain.com
|
||||||
|
EMAIL=admin@yourdomain.com
|
||||||
|
SSL_TYPE=letsencrypt # atau self-signed untuk testing
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DB_DATABASE=sibedas
|
||||||
|
DB_USERNAME=sibedas_user
|
||||||
|
DB_PASSWORD=your_secure_database_password
|
||||||
|
MYSQL_ROOT_PASSWORD=your_secure_root_password
|
||||||
|
|
||||||
|
# Laravel
|
||||||
|
APP_NAME="Sibedas PBG Web"
|
||||||
|
APP_ENV=production
|
||||||
|
APP_DEBUG=false
|
||||||
|
APP_KEY=base64:your_app_key_here
|
||||||
|
APP_URL=https://sibedas.yourdomain.com
|
||||||
|
VITE_APP_URL=https://sibedas.yourdomain.com
|
||||||
|
|
||||||
|
# Mail Configuration
|
||||||
|
MAIL_MAILER=smtp
|
||||||
|
MAIL_HOST=smtp.gmail.com
|
||||||
|
MAIL_PORT=587
|
||||||
|
MAIL_USERNAME=your_email@gmail.com
|
||||||
|
MAIL_PASSWORD=your_app_password
|
||||||
|
MAIL_ENCRYPTION=tls
|
||||||
|
MAIL_FROM_ADDRESS=your_email@gmail.com
|
||||||
|
MAIL_FROM_NAME="Sibedas PBG Web"
|
||||||
|
|
||||||
|
# Google Sheets API
|
||||||
|
SPREAD_SHEET_ID=your_google_sheets_id_here
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. **Generate Application Key**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate Laravel app key
|
||||||
|
php artisan key:generate --show
|
||||||
|
# Copy the generated key to .env file
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. **Deploy Application**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Option A: Full deployment with SSL (Recommended)
|
||||||
|
./scripts/setup-reverse-proxy.sh setup
|
||||||
|
|
||||||
|
# Option B: Deploy without SSL first
|
||||||
|
./scripts/deploy-production.sh deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. **SSL Setup (if not done in step 5)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For Let's Encrypt (Production)
|
||||||
|
DOMAIN=yourdomain.com EMAIL=admin@yourdomain.com ./scripts/setup-ssl.sh letsencrypt
|
||||||
|
|
||||||
|
# For Self-Signed (Testing)
|
||||||
|
./scripts/setup-ssl.sh self-signed
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 7. **Verify Deployment**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check container status
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Check application health
|
||||||
|
curl -f http://localhost/health-check
|
||||||
|
|
||||||
|
# Check SSL certificate
|
||||||
|
./scripts/setup-ssl.sh check
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose logs nginx-proxy
|
||||||
|
docker-compose logs app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scripts yang Diperlukan
|
||||||
|
|
||||||
|
#### **Essential Scripts (Wajib)**
|
||||||
|
|
||||||
|
- `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 (otomatis via docker-compose)
|
||||||
|
|
||||||
|
#### **Scripts yang Tidak Diperlukan**
|
||||||
|
|
||||||
|
- `scripts/build-and-zip.sh` - Tidak diperlukan karena menggunakan Docker build
|
||||||
|
|
||||||
|
### Deployment Commands Summary
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Setup environment
|
||||||
|
cp env.production.example .env
|
||||||
|
nano .env
|
||||||
|
|
||||||
|
# 2. Deploy with SSL (Recommended)
|
||||||
|
./scripts/setup-reverse-proxy.sh setup
|
||||||
|
|
||||||
|
# 3. Or deploy step by step
|
||||||
|
./scripts/deploy-production.sh deploy
|
||||||
|
./scripts/setup-ssl.sh letsencrypt
|
||||||
|
|
||||||
|
# 4. Check status
|
||||||
|
./scripts/setup-reverse-proxy.sh status
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 SSL Configuration
|
||||||
|
|
||||||
|
### Self-Signed Certificate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SSL_TYPE=self-signed ./scripts/setup-reverse-proxy.sh setup
|
||||||
|
```
|
||||||
|
|
||||||
|
### Let's Encrypt Certificate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DOMAIN=myapp.com EMAIL=admin@myapp.com SSL_TYPE=letsencrypt ./scripts/setup-reverse-proxy.sh setup
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSL Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check certificate
|
||||||
|
./scripts/setup-ssl.sh check
|
||||||
|
|
||||||
|
# Renew certificate
|
||||||
|
./scripts/setup-ssl.sh renew
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Monitoring
|
||||||
|
|
||||||
|
### Container Status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check all containers
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Check specific service
|
||||||
|
docker-compose ps app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Application logs
|
||||||
|
docker-compose logs app
|
||||||
|
|
||||||
|
# Reverse proxy logs
|
||||||
|
docker-compose logs nginx-proxy
|
||||||
|
|
||||||
|
# Database logs
|
||||||
|
docker-compose logs db
|
||||||
|
|
||||||
|
# Follow logs
|
||||||
|
docker-compose logs -f nginx-proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Health Checks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Application health
|
||||||
|
curl -f http://localhost/health-check
|
||||||
|
|
||||||
|
# SSL certificate
|
||||||
|
./scripts/setup-ssl.sh check
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Troubleshooting
|
||||||
|
|
||||||
|
### SSL Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check certificate files
|
||||||
|
docker exec sibedas_nginx_proxy ls -la /etc/nginx/ssl/
|
||||||
|
|
||||||
|
# Test SSL connection
|
||||||
|
openssl s_client -connect yourdomain.com:443
|
||||||
|
|
||||||
|
# Check nginx config
|
||||||
|
docker exec sibedas_nginx_proxy nginx -t
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Restart services
|
||||||
|
docker-compose restart
|
||||||
|
|
||||||
|
# Check network
|
||||||
|
docker network ls
|
||||||
|
|
||||||
|
# Check volumes
|
||||||
|
docker volume ls
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Import database manually
|
||||||
|
./scripts/import-sibedas-database.sh
|
||||||
|
|
||||||
|
# Check database connection
|
||||||
|
docker exec sibedas_app php artisan db:monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check resource usage
|
||||||
|
docker stats
|
||||||
|
|
||||||
|
# Check nginx access logs
|
||||||
|
docker exec sibedas_nginx_proxy tail -f /var/log/nginx/sibedas_access.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Maintenance
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Database backup
|
||||||
|
docker exec sibedas_db mysqldump -u root -p sibedas > backup.sql
|
||||||
|
|
||||||
|
# Volume backup
|
||||||
|
docker run --rm -v sibedas_app_storage:/data -v $(pwd):/backup alpine tar czf /backup/storage.tar.gz -C /data .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pull latest changes
|
||||||
|
git pull
|
||||||
|
|
||||||
|
# Rebuild and restart
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSL Certificate Renewal
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Manual renewal
|
||||||
|
./scripts/setup-ssl.sh renew
|
||||||
|
|
||||||
|
# Automatic renewal (cron)
|
||||||
|
0 12 * * * /path/to/sibedas-pbg-web/scripts/setup-ssl.sh renew
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
sibedas-pbg-web/
|
||||||
|
├── docker/ # Docker configurations
|
||||||
|
│ ├── nginx/ # Nginx configs
|
||||||
|
│ ├── mysql/ # MySQL configs
|
||||||
|
│ └── supervisor/ # Supervisor configs
|
||||||
|
├── scripts/ # Deployment scripts
|
||||||
|
│ ├── setup-local.sh # Local development
|
||||||
|
│ ├── setup-reverse-proxy.sh # Reverse proxy setup
|
||||||
|
│ ├── deploy-production.sh # Production deployment
|
||||||
|
│ ├── setup-ssl.sh # SSL setup
|
||||||
|
│ └── import-sibedas-database.sh # Database import
|
||||||
|
├── docs/ # Documentation
|
||||||
|
├── docker-compose.yml # Production compose
|
||||||
|
├── docker-compose.local.yml # Local development compose
|
||||||
|
└── README.md # Main README
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🆘 Support
|
||||||
|
|
||||||
|
### Common Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start services
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Stop services
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose logs [service]
|
||||||
|
|
||||||
|
# Execute commands
|
||||||
|
docker-compose exec app php artisan [command]
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
./scripts/setup-reverse-proxy.sh status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Getting Help
|
||||||
|
|
||||||
|
1. Check logs: `docker-compose logs [service]`
|
||||||
|
2. Check status: `./scripts/setup-reverse-proxy.sh status`
|
||||||
|
3. Restart services: `docker-compose restart`
|
||||||
|
4. Review this documentation
|
||||||
|
|
||||||
|
## 📚 Additional Resources
|
||||||
|
|
||||||
|
- [Docker Documentation](https://docs.docker.com/)
|
||||||
|
- [Nginx Documentation](https://nginx.org/en/docs/)
|
||||||
|
- [Let's Encrypt Documentation](https://letsencrypt.org/docs/)
|
||||||
|
- [Laravel Documentation](https://laravel.com/docs/)
|
||||||
@@ -3,10 +3,6 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:prod": "NODE_OPTIONS='--max-old-space-size=2048 --max-semi-space-size=1024' vite build --config vite.config.production.js",
|
|
||||||
"build:chunked": "./build-chunked.sh",
|
|
||||||
"build:local": "vite build && echo 'Build completed! Now run: ./deploy.sh to upload to server'",
|
|
||||||
"build:zip": "./build-and-zip.sh",
|
|
||||||
"dev": "vite"
|
"dev": "vite"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
233
public/js/utils/simple-session-validator.js
Normal file
233
public/js/utils/simple-session-validator.js
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
/**
|
||||||
|
* Simple Session Validator
|
||||||
|
* Menangani validasi session tanpa periodic checking
|
||||||
|
* Hanya respond pada 401 errors dari API requests
|
||||||
|
*/
|
||||||
|
class SimpleSessionValidator {
|
||||||
|
constructor() {
|
||||||
|
this.isLoggingOut = false;
|
||||||
|
this.consecutiveErrors = 0;
|
||||||
|
this.maxConsecutiveErrors = 2;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
console.log("Simple Session Validator initialized");
|
||||||
|
|
||||||
|
// Intercept all AJAX requests untuk detect 401
|
||||||
|
this.interceptAjaxRequests();
|
||||||
|
|
||||||
|
// Listen untuk page visibility changes
|
||||||
|
document.addEventListener("visibilitychange", () => {
|
||||||
|
if (!document.hidden && this.consecutiveErrors > 0) {
|
||||||
|
// Reset errors ketika user kembali ke tab
|
||||||
|
this.consecutiveErrors = 0;
|
||||||
|
console.log("Page visible, reset error counter");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interceptAjaxRequests() {
|
||||||
|
const validator = this;
|
||||||
|
|
||||||
|
// Intercept fetch requests
|
||||||
|
const originalFetch = window.fetch;
|
||||||
|
window.fetch = async function (...args) {
|
||||||
|
try {
|
||||||
|
const response = await originalFetch(...args);
|
||||||
|
|
||||||
|
// Check if response is 401 dan URL mengandung /api/
|
||||||
|
if (response.status === 401) {
|
||||||
|
const url = args[0];
|
||||||
|
if (typeof url === "string" && url.includes("/api/")) {
|
||||||
|
console.log("401 detected in API fetch request:", url);
|
||||||
|
validator.handleApiError401(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fetch request failed:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Intercept XMLHttpRequest
|
||||||
|
const originalXHRSend = XMLHttpRequest.prototype.send;
|
||||||
|
const originalXHROpen = XMLHttpRequest.prototype.open;
|
||||||
|
|
||||||
|
XMLHttpRequest.prototype.open = function (...args) {
|
||||||
|
this._url = args[1];
|
||||||
|
return originalXHROpen.apply(this, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
XMLHttpRequest.prototype.send = function (...args) {
|
||||||
|
const xhr = this;
|
||||||
|
const originalOnReadyStateChange = xhr.onreadystatechange;
|
||||||
|
|
||||||
|
xhr.onreadystatechange = function () {
|
||||||
|
if (xhr.readyState === 4 && xhr.status === 401) {
|
||||||
|
if (xhr._url && xhr._url.includes("/api/")) {
|
||||||
|
console.log(
|
||||||
|
"401 detected in API XHR request:",
|
||||||
|
xhr._url
|
||||||
|
);
|
||||||
|
validator.handleApiError401(xhr._url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalOnReadyStateChange) {
|
||||||
|
originalOnReadyStateChange.apply(xhr, arguments);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return originalXHRSend.apply(this, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Intercept jQuery AJAX jika tersedia
|
||||||
|
if (typeof $ !== "undefined" && $.ajaxSetup) {
|
||||||
|
$(document).ajaxError(function (event, xhr, settings, thrownError) {
|
||||||
|
if (
|
||||||
|
xhr.status === 401 &&
|
||||||
|
settings.url &&
|
||||||
|
settings.url.includes("/api/")
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
"401 detected in jQuery AJAX request:",
|
||||||
|
settings.url
|
||||||
|
);
|
||||||
|
validator.handleApiError401(settings.url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleApiError401(url) {
|
||||||
|
if (this.isLoggingOut) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`API 401 Error detected on ${url}`);
|
||||||
|
|
||||||
|
// Increment consecutive errors
|
||||||
|
this.consecutiveErrors++;
|
||||||
|
|
||||||
|
// Jika sudah 2x error berturut-turut, logout
|
||||||
|
if (this.consecutiveErrors >= this.maxConsecutiveErrors) {
|
||||||
|
this.handleSessionInvalid(
|
||||||
|
"Token API tidak valid. User lain telah login."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSessionInvalid(message) {
|
||||||
|
if (this.isLoggingOut) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoggingOut = true;
|
||||||
|
console.log("Handling session invalid:", message);
|
||||||
|
|
||||||
|
// Show notification
|
||||||
|
this.showNotification(message, "warning");
|
||||||
|
|
||||||
|
// Redirect to login after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
this.forceLogout();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification(message, type = "info") {
|
||||||
|
// Try different notification libraries
|
||||||
|
if (typeof toastr !== "undefined") {
|
||||||
|
toastr[type](message);
|
||||||
|
} else if (typeof Swal !== "undefined") {
|
||||||
|
Swal.fire({
|
||||||
|
title: "Peringatan Session",
|
||||||
|
text: message,
|
||||||
|
icon: type,
|
||||||
|
confirmButtonText: "OK",
|
||||||
|
allowOutsideClick: false,
|
||||||
|
timer: 5000,
|
||||||
|
timerProgressBar: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Create custom notification
|
||||||
|
this.createCustomNotification(message, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createCustomNotification(message, type) {
|
||||||
|
const notification = document.createElement("div");
|
||||||
|
notification.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: ${type === "warning" ? "#ffc107" : "#007bff"};
|
||||||
|
color: white;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
z-index: 9999;
|
||||||
|
max-width: 400px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
`;
|
||||||
|
notification.innerHTML = `
|
||||||
|
<strong>Peringatan!</strong><br>
|
||||||
|
${message}<br>
|
||||||
|
<small>Anda akan diarahkan ke halaman login...</small>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// Remove after 8 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notification.parentNode) {
|
||||||
|
notification.parentNode.removeChild(notification);
|
||||||
|
}
|
||||||
|
}, 8000);
|
||||||
|
}
|
||||||
|
|
||||||
|
forceLogout() {
|
||||||
|
console.log("Forcing logout...");
|
||||||
|
|
||||||
|
// Try to logout via API first
|
||||||
|
fetch("/logout", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-CSRF-TOKEN":
|
||||||
|
document
|
||||||
|
.querySelector('meta[name="csrf-token"]')
|
||||||
|
?.getAttribute("content") || "",
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
window.location.href = "/login";
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Force redirect even if logout fails
|
||||||
|
window.location.href = "/login";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method untuk manual reset
|
||||||
|
reset() {
|
||||||
|
this.consecutiveErrors = 0;
|
||||||
|
this.isLoggingOut = false;
|
||||||
|
console.log("Session validator reset");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize when DOM is ready
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
window.simpleSessionValidator = new SimpleSessionValidator();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export for module usage
|
||||||
|
if (typeof module !== "undefined" && module.exports) {
|
||||||
|
module.exports = SimpleSessionValidator;
|
||||||
|
}
|
||||||
@@ -81,25 +81,29 @@ class BigdataResume {
|
|||||||
return data.data.map((item) => [
|
return data.data.map((item) => [
|
||||||
item.id,
|
item.id,
|
||||||
item.year,
|
item.year,
|
||||||
item.potention_count,
|
addThousandSeparators(item.potention_count),
|
||||||
addThousandSeparators(item.potention_sum),
|
addThousandSeparators(item.potention_sum),
|
||||||
item.non_verified_count,
|
addThousandSeparators(item.non_verified_count),
|
||||||
addThousandSeparators(item.non_verified_sum),
|
addThousandSeparators(item.non_verified_sum),
|
||||||
item.verified_count,
|
addThousandSeparators(item.verified_count),
|
||||||
addThousandSeparators(item.verified_sum),
|
addThousandSeparators(item.verified_sum),
|
||||||
item.business_count,
|
addThousandSeparators(item.business_count),
|
||||||
addThousandSeparators(item.business_sum),
|
addThousandSeparators(item.business_sum),
|
||||||
item.non_business_count,
|
addThousandSeparators(item.non_business_count),
|
||||||
addThousandSeparators(item.non_business_sum),
|
addThousandSeparators(item.non_business_sum),
|
||||||
item.spatial_count,
|
addThousandSeparators(item.spatial_count),
|
||||||
addThousandSeparators(item.spatial_sum),
|
addThousandSeparators(item.spatial_sum),
|
||||||
item.waiting_click_dpmptsp_count,
|
addThousandSeparators(item.waiting_click_dpmptsp_count),
|
||||||
addThousandSeparators(item.waiting_click_dpmptsp_sum),
|
addThousandSeparators(item.waiting_click_dpmptsp_sum),
|
||||||
item.issuance_realization_pbg_count,
|
addThousandSeparators(
|
||||||
|
item.issuance_realization_pbg_count
|
||||||
|
),
|
||||||
addThousandSeparators(
|
addThousandSeparators(
|
||||||
item.issuance_realization_pbg_sum
|
item.issuance_realization_pbg_sum
|
||||||
),
|
),
|
||||||
item.process_in_technical_office_count,
|
addThousandSeparators(
|
||||||
|
item.process_in_technical_office_count
|
||||||
|
),
|
||||||
addThousandSeparators(
|
addThousandSeparators(
|
||||||
item.process_in_technical_office_sum
|
item.process_in_technical_office_sum
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Grid } from "gridjs/dist/gridjs.umd.js";
|
import { Grid } from "gridjs/dist/gridjs.umd.js";
|
||||||
import gridjs from "gridjs/dist/gridjs.umd.js";
|
import gridjs from "gridjs/dist/gridjs.umd.js";
|
||||||
import "gridjs/dist/gridjs.umd.js";
|
import "gridjs/dist/gridjs.umd.js";
|
||||||
import GlobalConfig from "../global-config.js";
|
import GlobalConfig, { addThousandSeparators } from "../global-config.js";
|
||||||
import Swal from "sweetalert2";
|
import Swal from "sweetalert2";
|
||||||
|
|
||||||
class BusinessIndustries {
|
class BusinessIndustries {
|
||||||
@@ -106,7 +106,8 @@ class BusinessIndustries {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
then: (data) =>
|
then: (data) =>
|
||||||
data.data.map((item) => [
|
data.data.map((item) => {
|
||||||
|
return [
|
||||||
item.id,
|
item.id,
|
||||||
item.nama_kecamatan,
|
item.nama_kecamatan,
|
||||||
item.nama_kelurahan,
|
item.nama_kelurahan,
|
||||||
@@ -114,15 +115,16 @@ class BusinessIndustries {
|
|||||||
item.nama_wajib_pajak,
|
item.nama_wajib_pajak,
|
||||||
item.alamat_wajib_pajak,
|
item.alamat_wajib_pajak,
|
||||||
item.alamat_objek_pajak,
|
item.alamat_objek_pajak,
|
||||||
item.luas_bumi,
|
addThousandSeparators(item.luas_bumi),
|
||||||
item.luas_bangunan,
|
addThousandSeparators(item.luas_bangunan),
|
||||||
item.njop_bumi,
|
addThousandSeparators(item.njop_bumi),
|
||||||
item.njop_bangunan,
|
addThousandSeparators(item.njop_bangunan),
|
||||||
item.ketetapan,
|
addThousandSeparators(item.ketetapan),
|
||||||
item.tahun_pajak,
|
item.tahun_pajak,
|
||||||
item.created_at,
|
item.created_at,
|
||||||
item.id, // ID for Actions column
|
item.id, // ID for Actions column
|
||||||
]),
|
];
|
||||||
|
}),
|
||||||
total: (data) => data.total,
|
total: (data) => data.total,
|
||||||
},
|
},
|
||||||
}).render(tableContainer);
|
}).render(tableContainer);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user