From 7135876ebc347df66c0649176e47efdfc7712639 Mon Sep 17 00:00:00 2001 From: arifal hidayat Date: Tue, 5 Aug 2025 01:30:37 +0700 Subject: [PATCH] create page tax with data, upload and export group by subdistrict --- app/Exports/TaxSubdistrictSheetExport.php | 59 ++++++ app/Exports/TaxationsExport.php | 23 +++ .../Controllers/Api/CustomersController.php | 3 +- .../Controllers/Api/TaxationsController.php | 63 +++++++ app/Http/Controllers/TaxationController.php | 75 ++++++++ app/Http/Resources/TaxationsResource.php | 19 ++ app/Imports/TaxationsImport.php | 78 ++++++++ app/Models/Tax.php | 23 +++ .../2025_08_04_231702_create_taxs_table.php | 38 ++++ database/seeders/MenuSeeder.php | 15 ++ database/seeders/UsersRoleMenuSeeder.php | 3 +- resources/js/taxation/index.js | 168 ++++++++++++++++++ resources/js/taxation/upload.js | 79 ++++++++ resources/views/taxation/index.blade.php | 38 ++++ resources/views/taxation/upload.blade.php | 81 +++++++++ routes/api.php | 8 +- routes/web.php | 7 + vite.config.js | 3 + 18 files changed, 780 insertions(+), 3 deletions(-) create mode 100644 app/Exports/TaxSubdistrictSheetExport.php create mode 100644 app/Exports/TaxationsExport.php create mode 100644 app/Http/Controllers/Api/TaxationsController.php create mode 100644 app/Http/Controllers/TaxationController.php create mode 100644 app/Http/Resources/TaxationsResource.php create mode 100644 app/Imports/TaxationsImport.php create mode 100644 app/Models/Tax.php create mode 100644 database/migrations/2025_08_04_231702_create_taxs_table.php create mode 100644 resources/js/taxation/index.js create mode 100644 resources/js/taxation/upload.js create mode 100644 resources/views/taxation/index.blade.php create mode 100644 resources/views/taxation/upload.blade.php diff --git a/app/Exports/TaxSubdistrictSheetExport.php b/app/Exports/TaxSubdistrictSheetExport.php new file mode 100644 index 0000000..dbe67a3 --- /dev/null +++ b/app/Exports/TaxSubdistrictSheetExport.php @@ -0,0 +1,59 @@ +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); + } +} + diff --git a/app/Exports/TaxationsExport.php b/app/Exports/TaxationsExport.php new file mode 100644 index 0000000..dbac51d --- /dev/null +++ b/app/Exports/TaxationsExport.php @@ -0,0 +1,23 @@ +distinct()->pluck('subdistrict'); + + foreach ($subdistricts as $subdistrict) { + $sheets[] = new TaxSubdistrictSheetExport($subdistrict); + } + + return $sheets; + } +} diff --git a/app/Http/Controllers/Api/CustomersController.php b/app/Http/Controllers/Api/CustomersController.php index 248ad42..06ff254 100644 --- a/app/Http/Controllers/Api/CustomersController.php +++ b/app/Http/Controllers/Api/CustomersController.php @@ -9,6 +9,7 @@ use App\Http\Resources\CustomersResource; use App\Imports\CustomersImport; use App\Models\Customer; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Log; use Maatwebsite\Excel\Facades\Excel; class CustomersController extends Controller @@ -120,7 +121,7 @@ class CustomersController extends Controller 'message' => 'File uploaded successfully', ]); }catch(\Exception $e){ - \Log::info($e->getMessage()); + Log::info($e->getMessage()); return response()->json([ 'error' => 'Failed to upload file', 'message' => $e->getMessage() diff --git a/app/Http/Controllers/Api/TaxationsController.php b/app/Http/Controllers/Api/TaxationsController.php new file mode 100644 index 0000000..ef9a193 --- /dev/null +++ b/app/Http/Controllers/Api/TaxationsController.php @@ -0,0 +1,63 @@ +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'); + } +} diff --git a/app/Http/Controllers/TaxationController.php b/app/Http/Controllers/TaxationController.php new file mode 100644 index 0000000..dfce41c --- /dev/null +++ b/app/Http/Controllers/TaxationController.php @@ -0,0 +1,75 @@ +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(string $id) + { + // + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, string $id) + { + // + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(string $id) + { + // + } +} diff --git a/app/Http/Resources/TaxationsResource.php b/app/Http/Resources/TaxationsResource.php new file mode 100644 index 0000000..026ad82 --- /dev/null +++ b/app/Http/Resources/TaxationsResource.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/app/Imports/TaxationsImport.php b/app/Imports/TaxationsImport.php new file mode 100644 index 0000000..f809cf6 --- /dev/null +++ b/app/Imports/TaxationsImport.php @@ -0,0 +1,78 @@ +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; + } +} diff --git a/app/Models/Tax.php b/app/Models/Tax.php new file mode 100644 index 0000000..485e566 --- /dev/null +++ b/app/Models/Tax.php @@ -0,0 +1,23 @@ +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'); + } +}; diff --git a/database/seeders/MenuSeeder.php b/database/seeders/MenuSeeder.php index b6c6406..b6d1dcb 100644 --- a/database/seeders/MenuSeeder.php +++ b/database/seeders/MenuSeeder.php @@ -270,6 +270,21 @@ class MenuSeeder extends Seeder ], ] ], + [ + "name" => "Pajak", + "url" => "/tax", + "icon" => "mingcute:coin-line", + "parent_id" => null, + "sort_order" => 10, + "children" => [ + [ + "name" => "Data Pajak", + "url" => "taxation", + "icon" => null, + "sort_order" => 1, + ] + ] + ] ]; foreach ($menus as $menuData) { diff --git a/database/seeders/UsersRoleMenuSeeder.php b/database/seeders/UsersRoleMenuSeeder.php index e6b8aa4..40097d0 100644 --- a/database/seeders/UsersRoleMenuSeeder.php +++ b/database/seeders/UsersRoleMenuSeeder.php @@ -25,7 +25,8 @@ class UsersRoleMenuSeeder extends Seeder 'Menu', 'Role', 'Setting Dashboard', 'PBG', 'Reklame', 'Usaha atau Industri', 'Pariwisata', 'Lap Pariwisata', 'UMKM', 'Dashboard Potensi', 'Tata Ruang', 'PDAM', 'PETA', 'Lap Pimpinan', 'Dalam Sistem', 'Luar Sistem', 'Google Sheets', 'TPA TPT', - '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', + 'Pajak', 'Data Pajak' ])->get()->keyBy('name'); // Define access levels for each role diff --git a/resources/js/taxation/index.js b/resources/js/taxation/index.js new file mode 100644 index 0000000..4a50836 --- /dev/null +++ b/resources/js/taxation/index.js @@ -0,0 +1,168 @@ +import { Grid } from "gridjs/dist/gridjs.umd.js"; +import "gridjs/dist/gridjs.umd.js"; +import GlobalConfig from "../global-config"; +import { addThousandSeparators } from "../global-config"; + +class Taxation { + constructor() { + this.toastMessage = document.getElementById("toast-message"); + this.toastElement = document.getElementById("toastNotification"); + this.toast = new bootstrap.Toast(this.toastElement); + this.table = null; + + this.initTableTaxation(); + // this.initEvents(); + this.handleExportToExcel(); + } + + handleExportToExcel() { + const button = document.getElementById("btn-export-excel"); + if (!button) { + console.error("Button not found: #btn-export-excel"); + return; + } + + const exportUrl = button.getAttribute("data-url"); + + button.addEventListener("click", function () { + button.disabled = true; + + fetch(exportUrl, { + method: "GET", + credentials: "include", + headers: { + Authorization: + "Bearer " + + document + .querySelector('meta[name="api-token"]') + .getAttribute("content"), + }, + }) + .then(function (response) { + if (!response.ok) { + throw new Error( + "Error fetching data: " + response.statusText + ); + } + return response.blob(); + }) + .then(function (blob) { + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "laporan-rekap-data-pembayaran.xlsx"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }) + .catch(function (error) { + console.error("Error fetching data:", error); + }) + .finally(function () { + button.disabled = false; + }); + }); + } + + initEvents() { + document.body.addEventListener("click", async (event) => { + const deleteButton = event.target.closest(".btn-delete-taxation"); + if (deleteButton) { + event.preventDefault(); + await this.handleDelete(deleteButton); + } + }); + } + + initTableTaxation() { + let tableContainer = document.getElementById("table-taxation"); + + if (!tableContainer) { + console.error("Table container not found!"); + return; + } + + // Clear previous table content + tableContainer.innerHTML = ""; + + this.table = new Grid({ + columns: [ + "ID", + { name: "Tax No" }, + { name: "Tax Code" }, + { name: "WP Name" }, + { name: "Business Name" }, + { name: "Address" }, + { name: "Start Validity" }, + { name: "End Validity" }, + { name: "Tax Value" }, + { name: "Subdistrict" }, + { name: "Village" }, + ], + pagination: { + limit: 50, + server: { + url: (prev, page) => { + let separator = prev.includes("?") ? "&" : "?"; + return `${prev}${separator}page=${page + 1}`; + }, + }, + }, + sort: true, + search: { + server: { + url: (prev, keyword) => { + let separator = prev.includes("?") ? "&" : "?"; + return `${prev}${separator}search=${encodeURIComponent( + keyword + )}`; + }, + }, + debounceTimeout: 1000, + }, + server: { + url: `${GlobalConfig.apiHost}/api/taxs`, + headers: { + Authorization: `Bearer ${document + .querySelector('meta[name="api-token"]') + .getAttribute("content")}`, + "Content-Type": "application/json", + }, + then: (data) => { + if (!data || !data.data) { + console.warn("⚠️ No data received from API"); + return []; + } + + return data.data.map((item) => { + return [ + item.id, + item.tax_no, + item.tax_code, + item.wp_name, + item.business_name, + item.address, + item.start_validity, + item.end_validity, + addThousandSeparators(item.tax_value), + item.subdistrict, + item.village, + ]; + }); + }, + total: (data) => { + let totalRecords = data?.meta?.total || 0; + return totalRecords; + }, + catch: (error) => { + console.error("❌ Error fetching data:", error); + }, + }, + }).render(tableContainer); + } +} + +document.addEventListener("DOMContentLoaded", function (e) { + new Taxation(); +}); diff --git a/resources/js/taxation/upload.js b/resources/js/taxation/upload.js new file mode 100644 index 0000000..ca73c7a --- /dev/null +++ b/resources/js/taxation/upload.js @@ -0,0 +1,79 @@ +import { Dropzone } from "dropzone"; +Dropzone.autoDiscover = false; + +class UploadTaxation { + constructor() { + this.spatialDropzone = null; + this.formElement = document.getElementById("formUploadTaxation"); + this.uploadButton = document.getElementById("submit-upload"); + this.spinner = document.getElementById("spinner"); + if (!this.formElement) { + console.error("Element formUploadTaxation tidak ditemukan!"); + } + } + + init() { + this.initDropzone(); + this.setupUploadButton(); + } + + initDropzone() { + const toastNotification = document.getElementById("toastNotification"); + const toast = new bootstrap.Toast(toastNotification); + let menuId = document.getElementById("menuId").value; + var previewTemplate, + dropzonePreviewNode = document.querySelector( + "#dropzone-preview-list" + ); + (dropzonePreviewNode.id = ""), + dropzonePreviewNode && + ((previewTemplate = dropzonePreviewNode.parentNode.innerHTML), + dropzonePreviewNode.parentNode.removeChild(dropzonePreviewNode), + (this.spatialDropzone = new Dropzone(".dropzone", { + url: this.formElement.action, + method: "post", + acceptedFiles: ".xls,.xlsx", + previewTemplate: previewTemplate, + previewsContainer: "#dropzone-preview", + autoProcessQueue: false, + headers: { + Authorization: `Bearer ${document + .querySelector('meta[name="api-token"]') + .getAttribute("content")}`, + }, + init: function () { + this.on("success", function (file, response) { + document.getElementById("toast-message").innerText = + response.message; + toast.show(); + setTimeout(() => { + window.location.href = `/tax?menu_id=${menuId}`; + }, 2000); + }); + this.on("error", function (file, errorMessage) { + document.getElementById("toast-message").innerText = + errorMessage.message; + toast.show(); + this.uploadButton.disabled = false; + this.spinner.classList.add("d-none"); + }); + }, + }))); + } + + setupUploadButton() { + this.uploadButton.addEventListener("click", (e) => { + if (this.spatialDropzone.files.length > 0) { + this.spatialDropzone.processQueue(); + this.uploadButton.disabled = true; + this.spinner.classList.remove("d-none"); + } else { + return; + } + }); + } +} + +document.addEventListener("DOMContentLoaded", function (e) { + new UploadTaxation().init(); +}); diff --git a/resources/views/taxation/index.blade.php b/resources/views/taxation/index.blade.php new file mode 100644 index 0000000..599ba6c --- /dev/null +++ b/resources/views/taxation/index.blade.php @@ -0,0 +1,38 @@ +@extends('layouts.vertical', ['subtitle' => 'Pajak']) +@section('css') +@vite(['node_modules/gridjs/dist/theme/mermaid.min.css']) +@endsection +@section('content') + +@include('layouts.partials/page-title', ['title' => 'Pajak', 'subtitle' => 'Data Pajak']) + + + +
+
+
+
+
+ +
+
+
+
+ Upload +
+
+
+
+
+
+
+
+ +@endsection + +@section('scripts') +@vite(['resources/js/taxation/index.js']) +@endsection \ No newline at end of file diff --git a/resources/views/taxation/upload.blade.php b/resources/views/taxation/upload.blade.php new file mode 100644 index 0000000..4b04001 --- /dev/null +++ b/resources/views/taxation/upload.blade.php @@ -0,0 +1,81 @@ +@extends('layouts.vertical', ['subtitle' => 'Pajak']) + +@section('content') + +@include('layouts.partials/page-title', ['title' => 'Pajak', 'subtitle' => 'Upload']) + + + +
+
+
+
+
Upload Data Pajak
+

+ Please upload a file with the extension .xls or .xlsx with a maximum size of 10 MB. +
+ For .xls and .xlsx files, ensure that the data is contained within a single sheet with the following columns: + kode, no, npwpd, nama_wp, nama_usaha, alamat_usaha, masa_pajak, nilai_pajak, kecamatan, desa +

+
+ +
+ +
+ +
+
+
+ +
+
+
+ +

Drop files here or click to upload.

+
+
+ +
    +
  • + +
    +
    +
    +
    + +
    +
    +
    +
    +
      +
    +

    + +
    +
    +
    + +
    +
    +
    +
  • +
+ +
+
+ +
+
+
+
+
+ +@endsection + +@section('scripts') +@vite(['resources/js/taxation/upload.js']) +@endsection \ No newline at end of file diff --git a/routes/api.php b/routes/api.php index 3c1ca88..430c21e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -28,7 +28,7 @@ use App\Http\Controllers\Api\UmkmController; use App\Http\Controllers\Api\TourismController; use App\Http\Controllers\Api\SpatialPlanningController; use App\Http\Controllers\Api\ChatbotController; - +use App\Http\Controllers\Api\TaxationsController; use Illuminate\Support\Facades\Route; Route::post('/login', [UsersController::class, 'login'])->name('api.user.login'); @@ -194,5 +194,11 @@ Route::group(['middleware' => 'auth:sanctum'], function (){ Route::get('/growth','index')->name('api.growth'); }); + Route::controller(TaxationsController::class)->group(function (){ + Route::get('/taxs', 'index')->name('api.taxs'); + Route::post('/taxs/upload', 'upload')->name('api.taxs.upload'); + Route::get('/taxs/export', 'export')->name('api.taxs.export'); + }); + // TODO: Implement new retribution calculation API endpoints using the new schema }); \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index fd59f47..8b9e8fa 100644 --- a/routes/web.php +++ b/routes/web.php @@ -31,6 +31,7 @@ use App\Http\Controllers\Data\GoogleSheetsController; use App\Http\Controllers\Report\ReportTourismController; use App\Http\Controllers\Chatbot\ChatbotController; use App\Http\Controllers\ChatbotPimpinan\ChatbotPimpinanController; +use App\Http\Controllers\TaxationController; use App\Http\Controllers\TpatptsController; use Illuminate\Support\Facades\Route; @@ -186,4 +187,10 @@ Route::group(['middleware' => 'auth'], function(){ Route::group(['prefix' => '/tools'], function (){ Route::get('/invitations', [InvitationsController::class, 'index'])->name('invitations'); }); + + // taxation + Route::group(['prefix' => '/tax'], function (){ + Route::get('/', [TaxationController::class, 'index'])->name('taxation'); + Route::get('/upload', [TaxationController::class, 'upload'])->name('taxation.upload'); + }); }); \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index fb2518c..b12f43b 100644 --- a/vite.config.js +++ b/vite.config.js @@ -148,6 +148,9 @@ export default defineConfig({ "resources/js/report-pbg-ptsp/index.js", "resources/js/tpa-tpt/index.js", "resources/js/report-payment-recaps/index.js", + // taxation + "resources/js/taxation/index.js", + "resources/js/taxation/upload.js", ], refresh: true, }),