Commit 5961a3ce by Hannah Zahra

Third Update

parent b0d5b251
...@@ -22,11 +22,13 @@ final class Permission extends Enum ...@@ -22,11 +22,13 @@ final class Permission extends Enum
// asasd // asasd
// hannah punya ---------------------- // hannah punya ----------------------
const APPLY_CLAIM = 'APPLY_CLAIM';
const HRVIEW = "HRVIEW";
const TRY_PERMISSION = "TRY_PERMISSION";
const PROJECT_MANAGER = "PROJECT_MANAGER"; const PROJECT_MANAGER = "PROJECT_MANAGER";
const PROJECT_DIRECTOR = "PROJECT_DIRECTOR"; const PROJECT_DIRECTOR = "PROJECT_DIRECTOR";
const HOD = "HOD";
const BOD = "BOD";
const HR = "HR";
const FINANCE = "FINANCE";
//should added to in table:acl_permission //should added to in table:acl_permission
} }
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
namespace App\Models; namespace App\Models;
use Portal\Mileage\Model\Project; use Portal\Mileage\Model\Project;
use Portal\Mileage\Model\staffInfo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
...@@ -42,4 +43,10 @@ public function aclRoles() ...@@ -42,4 +43,10 @@ public function aclRoles()
return $this->hasMany(\Portal\Mileage\Model\AclRoleUser::class, 'user_id', 'employeeCode'); return $this->hasMany(\Portal\Mileage\Model\AclRoleUser::class, 'user_id', 'employeeCode');
} }
public function staffInfo()
{
return $this->hasOne(\Portal\Mileage\Model\StaffInfo::class, 'fk_users', 'id');
}
} }
...@@ -13,7 +13,8 @@ ...@@ -13,7 +13,8 @@
"laravel/tinker": "^2.7", "laravel/tinker": "^2.7",
"laravolt/laravolt": "^5.6.1", "laravolt/laravolt": "^5.6.1",
"maatwebsite/excel": "^3.1", "maatwebsite/excel": "^3.1",
"prettus/l5-repository": "^2.9" "prettus/l5-repository": "^2.9",
"spatie/laravel-permission": "^6.21"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.9.1", "fakerphp/faker": "^1.9.1",
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "56a540b227fd55f8f465cbb525391ce8", "content-hash": "84f4f8e8e203fe01adc76aca87743b36",
"packages": [ "packages": [
{ {
"name": "akaunting/laravel-setting", "name": "akaunting/laravel-setting",
...@@ -6955,6 +6955,89 @@ ...@@ -6955,6 +6955,89 @@
"time": "2023-03-14T16:41:21+00:00" "time": "2023-03-14T16:41:21+00:00"
}, },
{ {
"name": "spatie/laravel-permission",
"version": "6.21.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-permission.git",
"reference": "6a118e8855dfffcd90403aab77bbf35a03db51b3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-permission/zipball/6a118e8855dfffcd90403aab77bbf35a03db51b3",
"reference": "6a118e8855dfffcd90403aab77bbf35a03db51b3",
"shasum": ""
},
"require": {
"illuminate/auth": "^8.12|^9.0|^10.0|^11.0|^12.0",
"illuminate/container": "^8.12|^9.0|^10.0|^11.0|^12.0",
"illuminate/contracts": "^8.12|^9.0|^10.0|^11.0|^12.0",
"illuminate/database": "^8.12|^9.0|^10.0|^11.0|^12.0",
"php": "^8.0"
},
"require-dev": {
"laravel/passport": "^11.0|^12.0",
"laravel/pint": "^1.0",
"orchestra/testbench": "^6.23|^7.0|^8.0|^9.0|^10.0",
"phpunit/phpunit": "^9.4|^10.1|^11.5"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Spatie\\Permission\\PermissionServiceProvider"
]
},
"branch-alias": {
"dev-main": "6.x-dev",
"dev-master": "6.x-dev"
}
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"Spatie\\Permission\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"description": "Permission handling for Laravel 8.0 and up",
"homepage": "https://github.com/spatie/laravel-permission",
"keywords": [
"acl",
"laravel",
"permission",
"permissions",
"rbac",
"roles",
"security",
"spatie"
],
"support": {
"issues": "https://github.com/spatie/laravel-permission/issues",
"source": "https://github.com/spatie/laravel-permission/tree/6.21.0"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2025-07-23T16:08:05+00:00"
},
{
"name": "spatie/laravel-signal-aware-command", "name": "spatie/laravel-signal-aware-command",
"version": "1.3.0", "version": "1.3.0",
"source": { "source": {
......
...@@ -12,19 +12,18 @@ public function up() ...@@ -12,19 +12,18 @@ public function up()
$table->id(); $table->id();
$table->date('claim_date'); $table->date('claim_date');
// User FK // User FK (int unsigned)
$table->unsignedBigInteger('user_id')->nullable(); // nullable if some claims don't have a user yet $table->unsignedInteger('user_id')->nullable();
$table->foreign('user_id')->references('id')->on('users')->onDelete('set null'); $table->foreign('user_id')->references('id')->on('users')->nullOnDelete();
// Vehicle type FK // Vehicle type FK
$table->unsignedBigInteger('jenis_kenderaan_id'); $table->unsignedBigInteger('jenis_kenderaan_id');
$table->foreign('jenis_kenderaan_id')->references('id')->on('lkp_jenis_kenderaans')->onDelete('cascade'); $table->foreign('jenis_kenderaan_id')->references('id')->on('lkp_jenis_kenderaans')->onDelete('cascade');
// Project FK // Project FK
$table->unsignedBigInteger('project_id'); $table->unsignedInteger('project_id')->nullable();
$table->foreign('project_id')->references('id')->on('lkp_project'); $table->foreign('project_id')->references('id')->on('lkp_project');
// distance_from is always "Pejabat" // distance_from is always "Pejabat"
$table->string('distance_from')->default('Pejabat'); $table->string('distance_from')->default('Pejabat');
...@@ -41,19 +40,33 @@ public function up() ...@@ -41,19 +40,33 @@ public function up()
$table->string('penalty_reason')->nullable(); $table->string('penalty_reason')->nullable();
$table->decimal('total_claim_amount', 10, 2)->default(0); $table->decimal('total_claim_amount', 10, 2)->default(0);
$table->unsignedBigInteger('rejected_by')->nullable()->after('total_claim_amount'); // Add column for verified_by (optional, allows null)
$table->foreign('rejected_by')->references('id')->on('users')->onDelete('set null'); $table->unsignedInteger('verified_by')->nullable(); // Added verified_by column
$table->foreign('verified_by')->references('id')->on('users')->nullOnDelete(); // Foreign key for verified_by
// Add column for approved_by (optional, allows null)
$table->unsignedInteger('approved_by')->nullable();
$table->foreign('approved_by')->references('id')->on('users')->nullOnDelete();
// Add column for reviewed_by (optional, allows null) for finance
$table->unsignedInteger('reviewed_by')->nullable(); // Added reviewed_by column
$table->foreign('reviewed_by')->references('id')->on('users')->nullOnDelete(); // Foreign key for reviewed_by
// Rejected by (int unsigned)
$table->unsignedInteger('rejected_by')->nullable();
$table->foreign('rejected_by')->references('id')->on('users')->nullOnDelete();
// Claim status (workflow) // Claim status (workflow)
$table->enum('status', [ $table->enum('status', [
'Diproses', // submitted by staff 'Diproses',
'Disokong', // verified by PM/HOD 'Disokong',
'Disahkan', // approved by Project Director/BOD 'Disahkan',
'Disemak', // reviewed by Finance 'Disemak',
'Diluluskan', // approved by Finance Director 'Diluluskan',
'Ditolak', // rejected 'Ditolak',
'Dibetulkan', // corrected by Finance 'Dibetulkan',
'Dibayar' // final paid 'Dibayar'
])->default('Diproses'); ])->default('Diproses');
$table->timestamps(); $table->timestamps();
...@@ -62,6 +75,22 @@ public function up() ...@@ -62,6 +75,22 @@ public function up()
public function down() public function down()
{ {
Schema::table('claims', function (Blueprint $table) {
// Drop the foreign key constraints and the column
$table->dropForeign(['approved_by']);
$table->dropColumn('approved_by');
$table->dropForeign(['verified_by']);
$table->dropColumn('verified_by');
$table->dropForeign(['reviewed_by']);
$table->dropColumn('reviewed_by');
$table->dropForeign(['rejected_by']);
$table->dropColumn('rejected_by');
});
// Drop the 'claims' table entirely
Schema::dropIfExists('claims'); Schema::dropIfExists('claims');
} }
}; };
...@@ -6,21 +6,20 @@ ...@@ -6,21 +6,20 @@
return new class extends Migration return new class extends Migration
{ {
/**
* Run the migrations.
*
* @return void
*/
public function up() public function up()
{ {
Schema::create('project_user', function (Blueprint $table) { Schema::create('project_user', function (Blueprint $table) {
$table->id(); $table->id();
// Make sure data types match referenced tables
$table->unsignedBigInteger('project_id'); $table->unsignedBigInteger('project_id');
$table->unsignedBigInteger('user_id'); $table->unsignedBigInteger('user_id');
$table->unsignedInteger('project_role_id'); $table->unsignedBigInteger('project_role_id');
$table->timestamps(); $table->timestamps();
$table->foreign('project_id')->references('id')->on('projects')->onDelete('cascade'); // Foreign keys
$table->foreign('project_id')->references('id')->on('lkp_project')->onDelete('cascade');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('project_role_id')->references('id')->on('project_role')->onDelete('restrict'); $table->foreign('project_role_id')->references('id')->on('project_role')->onDelete('restrict');
...@@ -28,11 +27,6 @@ public function up() ...@@ -28,11 +27,6 @@ public function up()
}); });
} }
/**
* Reverse the migrations.
*
* @return void
*/
public function down() public function down()
{ {
Schema::dropIfExists('project_user'); Schema::dropIfExists('project_user');
......
{
"name": "mileage_claim",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@fortawesome/fontawesome-free": "^7.1.0"
},
"devDependencies": {
"axios": "^1.1.2",
"laravel-vite-plugin": "^0.7.0",
"lodash": "^4.17.19",
"postcss": "^8.1.14",
"vite": "^3.0.0"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz",
"integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz",
"integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@fortawesome/fontawesome-free": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.1.0.tgz",
"integrity": "sha512-+WxNld5ZCJHvPQCr/GnzCTVREyStrAJjisUPtUxG5ngDA8TMlPnKp6dddlTpai4+1GNmltAeuk1hJEkBohwZYA==",
"license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
"engines": {
"node": ">=6"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/axios": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz",
"integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.15.18",
"@esbuild/linux-loong64": "0.15.18",
"esbuild-android-64": "0.15.18",
"esbuild-android-arm64": "0.15.18",
"esbuild-darwin-64": "0.15.18",
"esbuild-darwin-arm64": "0.15.18",
"esbuild-freebsd-64": "0.15.18",
"esbuild-freebsd-arm64": "0.15.18",
"esbuild-linux-32": "0.15.18",
"esbuild-linux-64": "0.15.18",
"esbuild-linux-arm": "0.15.18",
"esbuild-linux-arm64": "0.15.18",
"esbuild-linux-mips64le": "0.15.18",
"esbuild-linux-ppc64le": "0.15.18",
"esbuild-linux-riscv64": "0.15.18",
"esbuild-linux-s390x": "0.15.18",
"esbuild-netbsd-64": "0.15.18",
"esbuild-openbsd-64": "0.15.18",
"esbuild-sunos-64": "0.15.18",
"esbuild-windows-32": "0.15.18",
"esbuild-windows-64": "0.15.18",
"esbuild-windows-arm64": "0.15.18"
}
},
"node_modules/esbuild-android-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz",
"integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-android-arm64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz",
"integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-darwin-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz",
"integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-darwin-arm64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz",
"integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-freebsd-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz",
"integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-freebsd-arm64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz",
"integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-32": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz",
"integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz",
"integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-arm": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz",
"integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-arm64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz",
"integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-mips64le": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz",
"integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-ppc64le": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz",
"integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-riscv64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz",
"integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-s390x": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz",
"integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-netbsd-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz",
"integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-openbsd-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz",
"integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-sunos-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz",
"integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-windows-32": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz",
"integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-windows-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz",
"integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-windows-arm64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz",
"integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/laravel-vite-plugin": {
"version": "0.7.8",
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-0.7.8.tgz",
"integrity": "sha512-HWYqpQYHR3kEQ1LsHX7gHJoNNf0bz5z5mDaHBLzS+PGLCTmYqlU5/SZyeEgObV7z7bC/cnStYcY9H1DI1D5Udg==",
"dev": true,
"license": "MIT",
"dependencies": {
"picocolors": "^1.0.0",
"vite-plugin-full-reload": "^1.0.5"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"vite": "^3.0.0 || ^4.0.0"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true,
"license": "MIT"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true,
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/rollup": {
"version": "2.79.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"dev": true,
"license": "MIT",
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=10.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/vite": {
"version": "3.2.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.11.tgz",
"integrity": "sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.15.9",
"postcss": "^8.4.18",
"resolve": "^1.22.1",
"rollup": "^2.79.1"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
},
"peerDependencies": {
"@types/node": ">= 14",
"less": "*",
"sass": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": {
"optional": true
},
"sass": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
}
}
},
"node_modules/vite-plugin-full-reload": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz",
"integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picocolors": "^1.0.0",
"picomatch": "^2.3.1"
}
}
}
}
...@@ -10,5 +10,8 @@ ...@@ -10,5 +10,8 @@
"lodash": "^4.17.19", "lodash": "^4.17.19",
"postcss": "^8.1.14", "postcss": "^8.1.14",
"vite": "^3.0.0" "vite": "^3.0.0"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^7.1.0"
} }
} }
<title>BOD Dashboard</title>
@extends('ui::layouts.app')
@section('content')
<style type="text/css">
body {
background-color: #f0f2f5;
}
.ui.container {
max-width: 100% !important;
overflow-x: hidden;
padding: 0 5rem 0 2rem;
}
.ui.segment {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
</style>
<div class="ui container mt-5">
<h2 class="ui header">Board of Director Dashboard</h2>
{{-- Pending Claims --}}
<h3 class="ui dividing header">Pending Approval</h3>
<table class="ui celled table">
<thead>
<tr>
<th style="width:40px;text-align:center;">#</th>
<th>Staff</th>
<th>Tarikh</th>
<th>Butiran</th>
<th>Jarak Layak (KM)</th>
<th>Jumlah (RM)</th>
<th>Status</th>
<th>Tindakan</th>
</tr>
</thead>
<tbody>
@php
$pendingClaims = $claims->where('status', 'Disokong');
@endphp
@forelse($pendingClaims as $claim)
<tr>
<td style="text-align:center;">{{ $loop->iteration }}</td>
<td>{{ $claim->user->name ?? '-' }}</td>
<td>{{ $claim->claim_date }}</td>
<td>{{ $claim->description ?? '-' }}</td>
<td>{{ max(0, $claim->total_mileage - 40) }} km</td>
<td>RM {{ number_format($claim->calculated_amount, 2) }}</td>
<td>{{ $claim->status }}</td>
<td style="white-space:nowrap;">
<a href="{{ route('mileage::BOD.show', $claim->id) }}" class="ui blue button tiny">
Lihat
</a>
<form action="{{ route('mileage::BOD.approve', $claim->id) }}" method="POST" style="display:inline;">
@csrf
<button class="ui green button tiny" type="submit">Sahkan</button>
</form>
<form action="{{ route('mileage::BOD.reject', $claim->id) }}" method="POST" style="display:inline;">
@csrf
<button class="ui red button tiny" type="submit">Tolak</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="8" class="center aligned">Tiada tuntutan menunggu pengesahan</td>
</tr>
@endforelse
</tbody>
</table>
{{-- Approved / Rejected Claims --}}
<h3 class="ui dividing header mt-5">Previous Decisions</h3>
<table class="ui celled table">
<thead>
<tr>
<th>#</th>
<th>Staff</th>
<th>Tarikh</th>
<th>Butiran</th>
<th>Jarak Layak (KM)</th>
<th>Jumlah (RM)</th>
<th>Status</th>
<th>Tindakan</th>
</tr>
</thead>
<tbody>
@php
$previousClaims = $claims->whereIn('status', ['Disahkan', 'Ditolak']);
@endphp
@forelse($previousClaims as $claim)
<tr>
<td>{{ $loop->iteration }}</td>
<td>{{ $claim->user->name ?? '-' }}</td>
<td>{{ $claim->claim_date }}</td>
<td>{{ $claim->description ?? '-' }}</td>
<td>{{ max(0, $claim->total_mileage - 40) }} km</td>
<td>RM {{ number_format($claim->calculated_amount, 2) }}</td>
<td>{{ $claim->status }}</td>
<td>
<a href="{{ route('mileage::BOD.show', $claim->id) }}" class="ui blue button tiny">Lihat</a>
</td>
</tr>
@empty
<tr>
<td colspan="8" class="center aligned">Tiada rekod terdahulu</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@endsection
<title>HOD Dashboard</title>
@extends('ui::layouts.app')
@section('content')
<style type="text/css">
body {
background-color: #f0f2f5;
}
.ui.container {
max-width: 100% !important;
overflow-x: hidden;
padding: 0 5rem 0 2rem;
box-sizing: border-box;
}
.ui.segments {
margin-top: 2rem;
}
.ui.segment {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* .ui.celled.table {
table-layout: fixed; /* ensures column widths respect CSS widths */
/* width: 100%;
}
.ui.celled.table th,
.ui.celled.table td {
word-wrap: break-word;
}
.ui.celled.table th:nth-last-child(1),
.ui.celled.table td:nth-last-child(1) {
width: 25%; /* make Tindakan column longest */
/* }
.ui.celled.table th:nth-last-child(2),
.ui.celled.table td:nth-last-child(2) {
width: 10%; /* for Status */
/* } */
@keyframes spin {
to { transform: rotate(360deg); }
}
#loader {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
display: none;
align-items: center; justify-content: center;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
}
#loader div {
margin-top: 20%;
margin-left:45%;
width: 60px; height: 60px;
border: 6px solid #fff;
border-top: 6px solid transparent;
border-radius: 50%;
animation: spin 1s linear infinite;
}
</style>
<div class="ui container mt-5">
<h2 class="ui header">Head of Department Dashboard</h2>
{{-- Pending Claims --}}
<h3 class="ui dividing header">Pending Verification Claims</h3>
<table class="ui celled table">
<thead>
<tr>
<th style = "text-align: center; width: 40px;">#</th>
<th style = "width: 15%;">Staff</th>
<th>Tarikh</th>
<th>Butiran Projek</th>
<th>Jarak Layak (KM)</th>
<th>Jumlah Tuntutan (RM)</th>
<th>Status</th>
<th>Tindakan</th>
</tr>
</thead>
<tbody>
@php
$pendingClaims = $claims->where('status', 'Diproses');
@endphp
@forelse($pendingClaims as $claim)
<tr>
<td style = "text-align: center;">{{ $loop->iteration }}</td>
<td>{{ $claim->user->name ?? '-' }}</td>
<td>{{ $claim->claim_date }}</td>
<td>{{ $claim->description ?? '-' }}</td>
<td>{{ max(0, $claim->total_mileage - 40) }} km</td>
<td>RM {{ number_format($claim->calculated_amount, 2) }}</td>
<td>{{ $claim->status }}</td>
<td style = "white-space: nowrap; width: 1%;">
<a href="{{ route('mileage::HOD.show', $claim->id) }}" class="ui blue button tiny">
Lihat
</a>
<form action="{{ route('mileage::HOD.verify', $claim->id) }}" method="POST" style="display:inline;">
@csrf
<button class="ui green button tiny" type="submit">Sokong</button>
</form>
<form action="{{ route('mileage::HOD.reject', $claim->id) }}" method="POST" style="display:inline;">
@csrf
<button class="ui red button tiny" type="submit">Tolak</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="8" class="center aligned">Tiada tuntutan menunggu sokongan</td>
</tr>
@endforelse
</tbody>
</table>
{{-- Verified / Rejected Claims --}}
<h3 class="ui dividing header mt-5">Previous Claims</h3>
<table class="ui celled table">
<thead>
<tr>
<th>#</th>
<th>Staff</th>
<th>Tarikh</th>
<th>Butiran Projek</th>
<th>Jarak Layak (KM)</th>
<th>Jumlah Tuntutan (RM)</th>
<th>Status</th>
<th>Tindakan</th>
</tr>
</thead>
<tbody>
@php
$previousClaims = $claims->whereIn('status', ['Disokong', 'Ditolak']);
@endphp
@forelse($previousClaims as $claim)
<tr>
<td>{{ $loop->iteration }}</td>
<td>{{ $claim->user->name ?? '-' }}</td>
<td>{{ $claim->claim_date }}</td>
<td>{{ $claim->description ?? '-' }}</td>
<td>{{ max(0, $claim->total_mileage - 40) }} km</td>
<td>RM {{ number_format($claim->calculated_amount, 2) }}</td>
<td>{{ $claim->status }}</td>
<td>
<a href="{{ route('mileage::HOD.show', $claim->id) }}" class="ui blue button tiny">
Lihat
</a>
</td>
</tr>
@empty
<tr>
<td colspan="8" class="center aligned">Tiada rekod tuntutan terdahulu</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@endsection
<title>HR Dashboard</title>
@extends('ui::layouts.app')
@section('content')
<style>
body {
background-color: #f0f2f5;
}
.ui.container {
max-width: 100% !important;
overflow-x: hidden;
padding: 0 5rem 0 2rem;
box-sizing: border-box;
margin-top: 3rem;
}
h2.ui.header {
font-size: 1.8rem;
font-weight: 700;
color: #1b1c1d;
margin-bottom: 0.5rem;
}
h3.ui.dividing.header {
font-size: 1.3rem;
font-weight: 600;
color: #444;
margin-top: 0.5rem;
margin-bottom: 1.5rem;
border-bottom: 2px solid #ddd;
padding-bottom: 0.5rem;
}
.ui.table {
margin-top: 1rem !important;
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.ui.table th {
background: #f9fafb !important;
color: #444;
font-weight: 600;
}
.ui.button {
margin-right: 0.3rem !important;
}
/* Filter bar (aligned right side of header) */
.header-with-filter {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.2rem;
}
.filter-bar {
display: flex;
align-items: center;
gap: 1rem;
background: white;
border-radius: 8px;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.08);
padding: 0.5rem 1rem;
width: fit-content;
}
.filter-bar label {
font-weight: 600;
color: #333;
margin-right: 0.5rem;
}
.filter-bar input[type="month"] {
border: 1px solid #dcdcdc;
border-radius: 6px;
padding: 0.4rem 0.6rem;
font-size: 1rem;
color: #333;
transition: all 0.2s ease;
}
.filter-bar input[type="month"]:focus {
border-color: #21ba45;
outline: none;
box-shadow: 0 0 0 2px rgba(33, 186, 69, 0.15);
}
@media (max-width: 768px) {
.ui.container {
padding: 0 2rem;
}
.header-row {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.filter-bar {
flex-direction: column;
align-items: flex-start;
width: 100%;
}
.filter-bar button {
width: 100%;
}
}
</style>
<div class="ui container mt-5" style="margin-top: 10px">
{{-- Header --}}
<br>
<div class="header-row" style="display: flex; justify-content: space-between; align-items: center;">
<h2 class="ui header">Human Resources (HR) Dashboard</h2>
{{-- Excel Export Button --}}
<button class="ui teal button" id="openExportModal">
<i class="file excel icon"></i> Muat Turun Laporan Excel
</button>
</div>
<br><br>
{{-- Header + Filter Bar in same row --}}
<div class="header-with-filter">
<h3 class="ui dividing header" style="margin-bottom: 0;">Senarai Tuntutan Mengikut Staf</h3>
<form method="GET" class="filter-bar">
<label for="month">Bulan:</label>
<input type="month" id="month" name="month" value="{{ $month ?? now()->format('Y-m') }}" required>
<button class="ui teal button">
<i class="filter icon"></i> Tapis
</button>
</form>
</div>
{{-- Staff Table --}}
<table class="ui celled table">
<thead>
<tr>
<th>Nama Staf</th>
<th>Jumlah Tuntutan</th>
<th>Jumlah Keseluruhan (RM)</th>
<th>Tindakan</th>
</tr>
</thead>
<tbody>
@forelse($users as $user)
<tr>
<td>{{ $user['name'] }}</td>
<td>{{ $user['total_claims'] }}</td>
<td>RM {{ number_format($user['total_amount'], 2) }}</td>
<td>
<a href="{{ route('mileage::HR.show', ['userId' => $user['id'], 'month' => $month]) }}"
class="ui blue button tiny">Lihat</a>
</td>
</tr>
@empty
<tr>
<td colspan="4" class="center aligned">Tiada rekod tuntutan untuk bulan ini.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{-- Filter Modal for Excel Export --}}
<div class="ui modal" id="exportModal">
<div class="header">Penapis Laporan Excel</div>
<div class="content">
<form class="ui form" action="{{ route('mileage::HR.export') }}" method="GET">
<div class="field">
<label>Nama Staf</label>
<select name="user_id" class="ui dropdown" required>
<option value="">-- Pilih Staf --</option>
@foreach($users as $user)
<option value="{{ $user['id'] }}">{{ $user['name'] }}</option>
@endforeach
</select>
</div>
<div class="field">
<label>Bulan Tuntutan</label>
<input type="month" name="month" required>
</div>
<div class="actions" style="text-align: right; margin-top: 2rem;">
<button type="button" class="ui button" id="cancelExport">Batal</button>
<button type="submit" class="ui teal button">
<i class="download icon"></i> Muat Turun
</button>
</div>
</form>
</div>
</div>
{{-- JS --}}
<script>
document.getElementById('openExportModal').addEventListener('click', function() {
$('#exportModal').modal('show');
});
document.getElementById('cancelExport').addEventListener('click', function() {
$('#exportModal').modal('hide');
});
</script>
@endsection
<title>Butiran Tuntutan Staf</title>
@extends('ui::layouts.app')
@section('content')
<style>
body { background-color: #f0f2f5; }
.ui.container { padding: 0 5rem 3rem 2rem; margin-top: 3rem; }
h2.ui.header { font-size: 1.8rem; font-weight: 700; margin-bottom: .5rem; }
.ui.table { background: white; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
.ui.segment {
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-top: 2rem;
padding: 1.5rem;
}
.ui.button { margin-top: 1rem; }
.status-label {
display: inline-block;
padding: 6px 12px;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 600;
text-transform: capitalize;
opacity: 0.9;
}
.status-Diproses { background-color: rgba(33, 133, 208, 0.3); color: #1b4f72; }
.status-Disokong { background-color: rgba(46, 204, 113, 0.3); color: #1d5d38; }
.status-Disahkan { background-color: rgba(241, 196, 15, 0.3); color: #7a6000; }
.status-Disemak { background-color: rgba(155, 89, 182, 0.3); color: #4a2c63; }
.status-Diluluskan { background-color: rgba(26, 188, 156, 0.3); color: #0b5449; }
.status-Ditolak { background-color: rgba(231, 76, 60, 0.3); color: #7e2018; }
@media (max-width: 768px) {
.ui.container { padding: 0 2rem; }
}
.totals-row td {
font-weight: 700;
background: #f7f7f8;
}
</style>
<div class="ui container">
{{-- Header --}}
<h2 class="ui header">{{ $user->name }}</h2>
<p><strong>Bulan:</strong> {{ \Carbon\Carbon::parse($month)->translatedFormat('F Y') }}</p>
{{-- Back button --}}
<a href="{{ route('mileage::HR.index', ['month' => $month]) }}" class="ui grey button">
<i class="arrow left icon"></i> Kembali
</a>
{{-- Claims Table --}}
<table class="ui celled table">
<thead>
<tr>
<th>Tarikh</th>
<th>Butiran</th>
<th>Dari</th>
<th>Ke</th>
<th>Jumlah KM</th>
<th>Tol (RM)</th>
<th>Lain-lain (RM)</th>
<th>Status</th>
<th>Jumlah (RM)</th>
<th>Tindakan</th>
</tr>
</thead>
<tbody>
@php
$total = 0;
$totalMileage = 0;
$totalToll = 0;
$totalOthers = 0;
@endphp
@forelse($claims as $claim)
@php
$total += $claim->total_claim_amount;
$totalMileage += $claim->total_mileage ?? 0;
$totalToll += $claim->toll_amount ?? 0;
$totalOthers += $claim->others_amount ?? 0;
@endphp
<tr>
<td>{{ \Carbon\Carbon::parse($claim->claim_date)->format('d/m/Y') }}</td>
<td>{{ $claim->description ?? '-' }}</td>
<td>{{ $claim->distance_from ?? '-' }}</td>
<td>{{ $claim->distance_to ?? '-' }}</td>
<td>{{ $claim->total_mileage }}</td>
<td>{{ number_format($claim->toll_amount ?? 0, 2) }}</td>
<td>{{ number_format($claim->others_amount ?? 0, 2) }}</td>
<td>
<span class="status-label status-{{ $claim->status }}">
{{ $claim->status }}
</span>
</td>
<td>{{ number_format($claim->total_claim_amount, 2) }}</td>
<td style="text-align:center;">
{{-- Lihat (View) --}}
<a href="{{ route('mileage::HR.viewEach', ['id' => $claim->id]) }}" class="ui blue button tiny">
<i class="eye icon"></i> Lihat
</a>
{{-- Tolak (Reject) --}}
@if(!in_array($claim->status, ['Ditolak', 'Diluluskan']))
<form action="{{ route('mileage::HR.reject', $claim->id) }}" method="POST" style="display:inline;">
@csrf
<button type="submit" class="ui red button tiny" onclick="return confirm('Tolak tuntutan ini?')">
<i class="times icon"></i> Tolak
</button>
</form>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="10" class="center aligned">Tiada tuntutan untuk bulan ini.</td>
</tr>
@endforelse
</tbody>
</table>
{{-- combined Calculation + Penalty Summary --}}
@if($claims->count() > 0)
@php
$uniqueDates = $claims->pluck('claim_date')->unique()->count();
$deduction = 40 * $uniqueDates;
$claimableDistance = max(0, $totalMileage - $deduction);
// Vehicle-based rate note
$vehicleType = strtolower($claims->first()->jenisKenderaan->name ?? 'kereta');
if ($vehicleType === 'motosikal' || $vehicleType === 'motorcycle') {
$rateNote = "Motosikal – RM0.30/km";
} else {
$rateNote = ($claimableDistance <= 500)
? "Kereta RM0.55/km (≤500km)"
: "Kereta 500km × RM0.55 + " . number_format($claimableDistance - 500, 1) . "km × RM0.50";
}
// Penalty data
$latestClaim = $claims->sortByDesc('claim_date')->first();
$penaltyAmount = $latestClaim->penalty_amount ?? 0;
$penaltyReason = $latestClaim->penalty_reason ?? '-';
@endphp
<div class="ui segment">
<h4><i class="calculator icon"></i> Ringkasan Pengiraan (Mengikut Rekod Sistem)</h4>
<table class="ui definition table">
<tbody>
<tr><td><strong>Jumlah Jarak (Sebenar)</strong></td><td>{{ number_format($totalMileage, 1) }} KM</td></tr>
<tr><td><strong>Jumlah Hari Tuntutan</strong></td><td>{{ $uniqueDates }} hari</td></tr>
<tr><td><strong>Potongan 40KM/Hari</strong></td><td>40 × {{ $uniqueDates }} = {{ $deduction }} KM</td></tr>
<tr><td><strong>Jumlah Jarak Layak Tuntut</strong></td><td>{{ number_format($claimableDistance, 1) }} KM</td></tr>
<tr><td><strong>Kadar Kiraan</strong></td><td>{{ $rateNote }}</td></tr>
<tr><td><strong>Jumlah Tol</strong></td><td>RM {{ number_format($totalToll, 2) }}</td></tr>
<tr><td><strong>Jumlah Lain-lain</strong></td><td>RM {{ number_format($totalOthers, 2) }}</td></tr>
<tr><td><strong>Penalti</strong></td><td>RM {{ number_format($penaltyAmount, 2) }}</td></tr>
<tr><td><strong>Sebab Penalti</strong></td><td>{{ $penaltyReason }}</td></tr>
<tr class="totals-row">
<td><strong>Jumlah Keseluruhan Akhir</strong></td>
<td><strong>RM {{ number_format($total - $penaltyAmount, 2) }}</strong></td>
</tr>
</tbody>
</table>
</div>
@endif
{{-- Info Section --}}
<div class="ui segment">
<h4><i class="info circle icon"></i> Seksyen Pengiraan:</h4>
<p>
• Tuntutan bermula selepas 40KM pertama setiap hari.<br>
• Kadar (Kereta): RM0.55/km (&lt;500km), RM0.50/km (&gt;500km).<br>
• Kadar (Motosikal): RM0.30/km.<br>
• Penalti dikenakan sekiranya tuntutan lewat dihantar (&gt;1 bulan).
</p>
</div>
</div>
@endsection
<title>Maklumat Tuntutan (HR)</title>
@extends('ui::layouts.app')
@section('content')
<style>
body { background-color: #f0f2f5; }
.ui.container { max-width: 70% !important; margin-top: 2rem; }
.ui.segment {
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
border-radius: 10px;
background-color: #fff;
padding: 2rem;
}
table.ui.definition.table td:first-child {
width: 40%;
font-weight: 600;
background-color: #f9fafb;
}
.totals-row td {
font-weight: 700;
background: #f7f7f8;
}
.button-bar { margin-top: 2rem; display: flex; justify-content: space-between; }
</style>
<div class="ui container">
<h2 class="ui header">Maklumat Tuntutan (HR)</h2>
<div class="ui segment">
<table class="ui definition table">
<tr><td>Tarikh Tuntutan</td><td>{{ \Carbon\Carbon::parse($claim->claim_date)->format('d/m/Y') }}</td></tr>
<tr><td>Jenis Kenderaan</td><td>{{ $claim->jenisKenderaan->keterangan ?? $claim->jenisKenderaan->name ?? '-' }}</td></tr>
<tr><td>Nama Projek</td><td>{{ $claim->project->p_project_description ?? 'Tanpa Projek' }}</td></tr>
<tr><td>Butiran</td><td>{{ $claim->description ?? '-' }}</td></tr>
<tr><td>Jarak Dari</td><td>{{ $claim->distance_from ?? '-' }}</td></tr>
<tr><td>Jarak Ke</td><td>{{ $claim->distance_to ?? '-' }}</td></tr>
<tr><td>Jumlah Perbatuan</td><td>{{ number_format($claim->total_mileage, 2) }} km</td></tr>
<tr><td>Toll</td><td>RM {{ number_format($claim->toll_amount ?? 0, 2) }}</td></tr>
<tr><td>Lain-lain</td><td>RM {{ number_format($claim->others_amount ?? 0, 2) }}</td></tr>
<tr><td>Butiran Lain-lain</td><td>{{ $claim->others_description ?? '-' }}</td></tr>
<tr><td>Status</td><td><strong>{{ $claim->status }}</strong></td></tr>
<tr class="totals-row"><td>Jumlah Akhir</td><td><strong>RM {{ number_format($claim->total_claim_amount, 2) }}</strong></td></tr>
</table>
</div>
<div class="button-bar">
<form action="{{ url()->previous() }}" method="GET" style="display:inline;">
<button type="submit" class="ui grey button">
<i class="arrow left icon"></i> Kembali
</button>
</form>
@if(!in_array($claim->status, ['Ditolak', 'Diluluskan']))
<form action="{{ route('mileage::HR.reject', $claim->id) }}" method="POST" style="display:inline;">
@csrf
<button type="submit" class="ui red button" onclick="return confirm('Tolak tuntutan ini?')">
<i class="times icon"></i> Tolak
</button>
</form>
@endif
</div>
</div>
@endsection
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
@extends('ui::layouts.app') @extends('ui::layouts.app')
@section('content') @section('content')
<style type="text/css"> <style>
body { body {
background-color: #f0f2f5; background-color: #f0f2f5;
} }
...@@ -12,98 +12,444 @@ ...@@ -12,98 +12,444 @@
overflow-x: hidden; overflow-x: hidden;
padding: 0 5rem 0 2rem; padding: 0 5rem 0 2rem;
box-sizing: border-box; box-sizing: border-box;
margin-top: 3rem;
} }
.ui.segments { h2.ui.header {
margin-top: 2rem; font-size: 1.8rem;
font-weight: 700;
color: #1b1c1d;
margin-bottom: 0.5rem;
} }
.ui.segment { h3.ui.dividing.header {
font-size: 1.3rem;
font-weight: 600;
color: #444;
margin-top: 1.5rem;
border-bottom: 2px solid #ddd;
padding-bottom: 0.5rem;
}
.ui.table {
margin-top: 1rem !important;
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
} }
@keyframes spin {
to { transform: rotate(360deg); } .ui.table th {
background: #f9fafb !important;
color: #444;
font-weight: 600;
}
.filter-bar {
display: flex;
align-items: center;
gap: 1rem;
background: white;
border-radius: 8px;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.08);
padding: 0.8rem 1.5rem;
width: fit-content;
margin: 1.5rem 0;
}
.filter-bar label {
font-weight: 600;
color: #333;
margin-right: 0.5rem;
}
.filter-bar input[type="month"] {
border: 1px solid #dcdcdc;
border-radius: 6px;
padding: 0.4rem 0.6rem;
font-size: 1rem;
color: #333;
transition: all 0.2s ease;
height: 38px; /* ensures same height as button */
line-height: 1.2rem;
}
.filter-bar input[type="month"]:focus {
border-color: #21ba45;
outline: none;
box-shadow: 0 0 0 2px rgba(33, 186, 69, 0.15);
}
.filter-bar button {
height: 38px;
display: flex;
align-items: center;
justify-content: center;
}
@media (max-width: 768px) {
.filter-bar {
flex-direction: column;
align-items: flex-start;
width: 100%;
}
.filter-bar button {
width: 100%;
margin-top: 0.5rem;
}
}
.summary-toggle {
cursor: pointer;
background: white;
border-radius: 10px;
padding: 1rem 1.5rem;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
justify-content: space-between;
transition: 0.2s ease;
margin-bottom: 1rem;
}
.summary-toggle:hover {
background: #f9fafb;
}
.summary-toggle i {
transition: transform 0.3s ease;
} }
#loader {
position: fixed; .summary-toggle.active i {
top: 0; left: 0; transform: rotate(180deg);
width: 100%; height: 100%; }
.summary-content {
display: none; display: none;
align-items: center; justify-content: center; background: white;
background: rgba(0, 0, 0, 0.5); border-radius: 10px;
z-index: 9999; box-shadow: 0 3px 6px rgba(0, 0, 0, 0.08);
padding: 1.2rem 1.5rem;
margin-bottom: 2rem;
} }
#loader div {
margin-top: 20%; .summary-content table {
margin-left:45%; width: 100%;
width: 60px; height: 60px; border-collapse: collapse;
border: 6px solid #fff; font-size: 0.95rem;
border-top: 6px solid transparent; }
border-radius: 50%;
animation: spin 1s linear infinite; .summary-content td {
padding: 6px 0;
}
.summary-content td:first-child {
font-weight: 600;
width: 50%;
color: #333;
}
.status-label {
display: inline-block;
padding: 6px 12px;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 600;
text-transform: capitalize;
color: #fff;
}
.status-Diproses { background-color: rgba(33,133,208,0.3); color: #1b4f72; }
.status-Disokong { background-color: rgba(46,204,113,0.3); color: #1d5d38; }
.status-Disahkan { background-color: rgba(241,196,15,0.3); color: #7a6000; }
.status-Disemak { background-color: rgba(155,89,182,0.3); color: #4a2c63; }
.status-Dibayar { background-color: rgba(26,188,156,0.3); color: #0b5449; }
.status-Ditolak { background-color: rgba(231,76,60,0.3); color: #7e2018; }
@media (max-width: 768px) {
.ui.container { padding: 0 2rem; }
.filter-bar { flex-direction: column; align-items: flex-start; width: 100%; }
} }
</style> </style>
<div id="loader"><div></div></div>
<div class="ui container"> <div class="ui container">
<h2 class="ui header">Senarai Tuntutan</h2> <h2 class="ui header">Senarai Tuntutan Bulanan</h2>
{{-- Month Filter --}}
<form method="GET" class="filter-bar">
<label for="month">Bulan:</label>
<input type="month" id="month" name="month" value="{{ $month ?? now()->format('Y-m') }}" required>
<button class="ui teal button">
<i class="filter icon"></i> Tapis
</button>
</form>
{{-- Calculation Summary Toggle --}}
<div class="summary-toggle" id="summaryToggle">
<span><i class="calculator icon"></i> Ringkasan Pengiraan Bulanan</span>
<i class="angle down icon"></i>
</div>
<div class="summary-content" id="summaryContent">
@php
// === Step 1: Base totals ===
$totalMileage = $claims->sum('total_mileage');
$totalToll = $claims->sum('toll_amount');
$totalOthers = $claims->sum('others_amount');
// === Step 2: Deduct 400 km per month (not 40 per day) ===
$claimableDistance = max(0, $totalMileage - 400);
// === Step 3: Determine vehicle type & rate ===
$vehicleType = strtolower($claims->first()->jenisKenderaan->name ?? 'kereta');
$rate1 = 0.55; // ≤500 km
$rate2 = 0.50; // >500 km
$rateMoto = 0.30;
// === Step 4: Mileage amount ===
if ($vehicleType === 'motosikal' || $vehicleType === 'motorcycle') {
$firstPart = $claimableDistance;
$secondPart = 0;
$mileageAmount = $claimableDistance * $rateMoto;
$rateNote = "Motosikal – RM0.30/km";
} else {
$firstPart = min($claimableDistance, 500);
$secondPart = max(0, $claimableDistance - 500);
$mileageAmount = ($firstPart * $rate1) + ($secondPart * $rate2);
$rateNote = "Kereta – RM0.55/km (≤500km), RM0.50/km (>500km)";
}
// === Step 5: Total before penalty ===
$totalBeforePenalty = $mileageAmount + $totalToll + $totalOthers;
// === Step 6: Calculate penalty by month ===
$latestClaim = $claims->sortByDesc('created_at')->first();
$submissionDate = \Carbon\Carbon::parse($latestClaim->created_at ?? now());
$claimDate = \Carbon\Carbon::parse($claims->min('claim_date') ?? now());
$monthsDiff = $claimDate->diffInMonths($submissionDate);
if ($monthsDiff <= 1) {
$penaltyRate = 0;
$penaltyLabel = "Tiada penalti (≤ 1 bulan)";
} elseif ($monthsDiff <= 3) {
$penaltyRate = 0.10;
$penaltyLabel = "Penalti 10% (lewat >1 bulan ≤3 bulan)";
} elseif ($monthsDiff <= 6) {
$penaltyRate = 0.30;
$penaltyLabel = "Penalti 30% (lewat >3 bulan ≤6 bulan)";
} else {
$penaltyRate = 'BOD';
$penaltyLabel = "Tertakluk kepada budi bicara Lembaga Pengarah (>6 bulan)";
}
if ($penaltyRate !== 'BOD') {
$penaltyAmount = $totalBeforePenalty * $penaltyRate;
$finalTotal = $totalBeforePenalty - $penaltyAmount;
} else {
$penaltyAmount = 0;
$finalTotal = $totalBeforePenalty;
}
// === Step 7: System total from DB ===
$totalAmount = $claims->sum('total_claim_amount');
@endphp
<table>
<tr><td>Jumlah Perbatuan (Sebenar)</td><td>{{ number_format($totalMileage, 1) }} KM</td></tr>
<tr><td>Potongan Bulanan</td><td>400 KM</td></tr>
<tr><td>Jumlah Layak Tuntut</td><td>{{ number_format($claimableDistance, 1) }} KM</td></tr>
<tr><td>Kadar Kiraan</td><td>{{ $rateNote }}</td></tr>
{{-- Breakdown of mileage --}}
<tr>
<td>Kiraan Perbatuan</td>
<td>
@if($vehicleType === 'motosikal' || $vehicleType === 'motorcycle')
{{ number_format($firstPart, 1) }} × RM{{ number_format($rateMoto, 2) }}
= <strong>RM {{ number_format($mileageAmount, 2) }}</strong>
@else
({{ number_format($firstPart, 1) }} × RM{{ number_format($rate1, 2) }})
+ ({{ number_format($secondPart, 1) }} × RM{{ number_format($rate2, 2) }})
<br><br><strong>RM {{ number_format($mileageAmount, 2) }}</strong>
@endif
</td>
</tr>
{{-- Toll and others --}}
<tr><td>Jumlah Tol</td><td>RM {{ number_format($totalToll, 2) }}</td></tr>
<tr><td>Jumlah Lain-lain</td><td>RM {{ number_format($totalOthers, 2) }}</td></tr>
{{-- Total before penalty --}}
<tr>
<td><strong>Jumlah Subtotal (Perbatuan + Tol + Lain-lain)</strong></td>
<td><strong>RM {{ number_format($totalBeforePenalty, 2) }}</strong></td>
</tr>
{{-- Penalty --}}
<tr>
<td>Penalti</td>
<td>
@if($penaltyRate === 'BOD')
<span style="color:#d35400;"><i class="exclamation triangle icon"></i> {{ $penaltyLabel }}</span>
@else
{{ $penaltyLabel }}<br>
Tolak: RM {{ number_format($penaltyAmount, 2) }}
@endif
</td>
</tr>
{{-- Final total --}}
<tr>
<td><strong>Jumlah Akhir</strong></td>
<td><strong>RM {{ number_format($finalTotal, 2) }}</strong></td>
</tr>
<tr>
<td colspan="2" style="font-style:italic; color:#888;">
*Perbezaan kecil mungkin disebabkan pembulatan atau kemaskini oleh kewangan.*
</td>
</tr>
</table>
</div>
{{-- Success message --}}
@if(session('success')) @if(session('success'))
<div class="ui positive message"> <div class="ui positive message">{{ session('success') }}</div>
{{ session('success') }} @endif
{{-- Error message --}}
@if(session('error'))
<div class="ui negative message">
<i class="close icon"></i>
<div class="header">Ralat!</div>
<p>{{ session('error') }}</p>
</div>
@endif
{{-- Group claims by project --}}
@php
$claimsByProject = $claims->groupBy(function ($claim) {
return $claim->project->p_project_description ?? 'Tanpa Projek';
});
@endphp
{{-- Existing claim tables remain unchanged --}}
@forelse($claimsByProject as $projectName => $projectClaims)
<h3 class="ui dividing header">
<i class="folder icon"></i> {{ $projectName }}
</h3>
@if($projectName !== 'Tanpa Projek')
<div style="margin-bottom: 0.5rem; font-size: 0.9rem; color: #555;">
<i class="info circle icon"></i>
Anda boleh mengemaskini tuntutan yang berstatus <strong>Diproses</strong> dengan menekan butang <strong>‘Lihat’</strong>.
</div> </div>
@endif @endif
@foreach($claimsByMonth as $month => $claims) @if($projectName !== 'Tanpa Projek')
<h3 class="ui dividing header">{{ $month }}</h3> @php
$totalMileage = $projectClaims->sum('total_mileage');
$totalAmount = $projectClaims->sum('total_claim_amount');
$firstClaim = $projectClaims->first();
@endphp
<table class="ui celled table">
<thead>
<tr>
<th>Jumlah Tuntutan</th>
<th>Jumlah Perbatuan (KM)</th>
<th>Jumlah Keseluruhan (RM)</th>
<th>Status</th>
<th>Tindakan</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ $projectClaims->count() }}</td>
<td>{{ number_format($totalMileage, 1) }}</td>
<td>RM {{ number_format($totalAmount, 2) }}</td>
<td>
@foreach($projectClaims->pluck('status')->unique() as $status)
<span class="status-label status-{{ $status }}">{{ $status }}</span>
@endforeach
</td>
<td>
<a href="{{ route('mileage::applications.show', ['application' => $firstClaim->id, 'project_id' => $firstClaim->project_id]) }}"
class="ui blue button tiny">
<i class="eye icon"></i> Lihat
</a>
</td>
</tr>
</tbody>
</table>
<br>
@else
{{-- Tanpa Projek Table --}}
<table class="ui celled table"> <table class="ui celled table">
<thead> <thead>
<tr> <tr>
<th>No.</th> <th>No.</th>
<th>Tarikh Tuntutan</th> <th>Tarikh</th>
<th>Nama Projek</th>
<th>Jenis Kenderaan</th> <th>Jenis Kenderaan</th>
<th>Jumlah Perbatuan</th> <th>Butiran</th>
<th>Jumlah Tuntutan</th> <th>Perbatuan (KM)</th>
<th>Jumlah (RM)</th>
<th>Status</th> <th>Status</th>
<th>Tindakan</th> <th>Tindakan</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach($claims as $claim) @foreach($projectClaims as $claim)
<tr> <tr>
<td>{{ $loop->iteration }}</td> <td>{{ $loop->iteration }}</td>
<td>{{ $claim->claim_date }}</td> <td>{{ \Carbon\Carbon::parse($claim->claim_date)->format('d/m/Y') }}</td>
<td>{{ $claim->project->p_project_description ?? '-' }}</td>
<td>{{ $claim->jenisKenderaan->name ?? '-' }}</td> <td>{{ $claim->jenisKenderaan->name ?? '-' }}</td>
<td>{{ $claim->total_mileage ?? 0 }} km</td> <td>{{ $claim->description }}</td>
<td>{{ number_format($claim->total_mileage, 1) }}</td>
<td>RM {{ number_format($claim->total_claim_amount, 2) }}</td> <td>RM {{ number_format($claim->total_claim_amount, 2) }}</td>
<td>{{ $claim->status }}</td>
<td> <td>
<a href="{{ route('mileage::applications.show', $claim->id) }}" class="ui blue button tiny"> <span class="status-label status-{{ $claim->status }}">{{ $claim->status }}</span>
Lihat </td>
<td>
{{-- Always show Lihat --}}
<a href="{{ route('mileage::applications.viewEach', ['id' => $claim->id]) }}"
class="ui blue button tiny">
<i class="eye icon"></i> Lihat
</a> </a>
@php
$isIncomplete = empty($claim->claim_date) || empty($claim->distance_from) || empty($claim->distance_to) || empty($claim->description) || empty($claim->total_mileage);
@endphp
@if($claim->status === 'Diproses') @if($claim->status === 'Diproses')
<!-- Edit button (opens modal) --> @if($isIncomplete)
<button class="ui yellow button tiny editBtn" style="margin-bottom: 10px" <button class="ui grey button tiny disabled" title="Maklumat tidak lengkap — tidak boleh dikemaskini.">
<i class="ban icon"></i> Tidak Lengkap
</button>
@else
<button class="ui yellow button tiny edit-btn"
data-id="{{ $claim->id }}" data-id="{{ $claim->id }}"
data-date="{{ $claim->claim_date }}" data-date="{{ $claim->claim_date }}"
data-project="{{ $claim->project_id }}"
data-vehicle="{{ $claim->jenis_kenderaan_id }}"
data-description="{{ $claim->description }}" data-description="{{ $claim->description }}"
data-distanceto="{{ $claim->distance_to }}" data-distance-from="{{ $claim->distance_from }}"
data-distance-to="{{ $claim->distance_to }}"
data-mileage="{{ $claim->total_mileage }}" data-mileage="{{ $claim->total_mileage }}"
data-toll="{{ $claim->toll_amount }}" data-toll="{{ $claim->toll_amount }}"
data-others="{{ $claim->others_amount }}" data-others="{{ $claim->others_amount }}">
data-othersdesc="{{ $claim->others_description }}"> <i class="edit icon"></i> Kemaskini
Kemaskini
</button> </button>
@endif
<!-- Delete button --> <form action="{{ route('mileage::applications.destroy', $claim->id) }}"
<form action="{{ route('mileage::applications.destroy', $claim->id) }}" method="POST" style="display:inline;"> method="POST"
style="display:inline;"
onsubmit="return confirm('Padam tuntutan ini?');">
@csrf @csrf
@method('DELETE') @method('DELETE')
<button type="submit" class="ui red button tiny" onclick="return confirm('Are you sure you want to delete this claim?')">Padam</button> <button type="submit" class="ui red button tiny">
<i class="trash icon"></i> Padam
</button>
</form> </form>
@endif @endif
</td> </td>
...@@ -111,202 +457,145 @@ ...@@ -111,202 +457,145 @@
@endforeach @endforeach
</tbody> </tbody>
</table> </table>
@endforeach @endif
</div> @empty
<div class="ui message">Tiada tuntutan dijumpai untuk bulan ini.</div>
@endforelse
<div id="loader"><div></div></div> {{-- Edit Modal --}}
<!-- Edit Modal --> <div class="ui modal" id="editModal">
<div id="editModal" class="ui modal">
<i class="close icon"></i> <i class="close icon"></i>
<div class="header">Kemaskini Tuntutan</div> <div class="header">Kemaskini Tuntutan</div>
<div class="content"> <div class="content">
<form class="ui form" id="editForm" method="POST"> <form id="editForm" method="POST" action="">
@csrf @csrf
@method('PUT') @method('PUT')
<div class="ui form">
<input type="hidden" id="editId" name="id"> <div class="field required">
<label>Tarikh</label>
<div class="field"> <input type="date" name="claim_date" id="editDate" required>
<label>Tarikh Tuntutan</label>
<input type="date" name="claim_date" id="editDate">
</div> </div>
<div class="field"> <div class="two fields">
<label>Nama Projek</label> <div class="field required">
<select class="ui dropdown" name="project_id" id="editProject"> <label>Dari</label>
@foreach($projects as $project) <input type="text" name="distance_from" id="editFrom" required>
<option value="{{ $project->id }}">{{ $project->p_project_description }}</option>
@endforeach
</select>
</div> </div>
<div class="field required">
<div class="field"> <label>Ke</label>
<label>Jenis Kenderaan</label> <input type="text" name="distance_to" id="editTo" required>
<select class="ui dropdown" name="jenis_kenderaan_id" id="editVehicle">
@foreach($jenisKenderaan as $vehicle)
<option value="{{ $vehicle->id }}">{{ $vehicle->name }}</option>
@endforeach
</select>
</div> </div>
<div class="field">
<label>Butiran Tuntutan</label>
<input type="text" name="description" id="editDescription">
</div> </div>
<div class="field"> <div class="field required">
<label>Jarak Ke</label> <label>Butiran</label>
<input type="text" name="distance_to" id="editDistanceTo"> <input type="text" name="description" id="editDescription" required>
</div> </div>
<div class="field"> <div class="three fields">
<label>Jumlah Perbatuan (KM)</label> <div class="field required">
<input type="number" name="total_mileage" id="editDistance"> <label>Jumlah KM</label>
<input type="number" step="0.1" name="total_mileage" id="editMileage" required>
</div> </div>
<div class="field"> <div class="field">
<label>Toll (RM)</label> <label>Tol (RM)</label>
<input type="number" name="toll_amount" id="editToll"> <input type="number" step="0.01" name="toll_amount" id="editToll">
</div> </div>
<div class="field"> <div class="field">
<label>Lain-lain (RM)</label> <label>Lain-lain (RM)</label>
<input type="number" name="others_amount" id="editOthers"> <input type="number" step="0.01" name="others_amount" id="editOthers">
</div>
</div> </div>
<div class="field"> <div class="field" id="othersDetailsField" style="display:none;">
<label>Butiran Lain-lain</label> <label>Butiran Lain-lain</label>
<input type="text" name="others_description" id="editOthersDescription"> <input type="text" name="others_details" id="editOthersDetails"
placeholder="Nyatakan butiran lain-lain (contoh: bayaran penghantaran, dokumen, dsb)">
</div>
</div> </div>
<button type="submit" class="ui green button">Kemaskini</button>
</form> </form>
</div> </div>
<div class="actions">
<div class="ui cancel button">Batal</div>
<button type="submit" form="editForm" class="ui primary button">Simpan</button>
</div>
</div>
</div> </div>
<!-- Script to handle modal -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.3/semantic.min.js"></script>
<script> <script>
$(document).ready(function() { $(document).ready(function() {
// Ensure ajax sends X-CSRF-TOKEN // Toggle Summary Section
$.ajaxSetup({ $('#summaryToggle').on('click', function() {
headers: { $(this).toggleClass('active');
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') $('#summaryContent').slideToggle(250);
}
}); });
// initialize Semantic UI dropdowns and modal // Handle Edit button click
$('.ui.dropdown').dropdown(); $('.edit-btn').on('click', function() {
$('#editModal').modal({ autofocus: false }); const id = $(this).data('id');
const date = $(this).data('date');
// delegate click (works if rows are dynamic) const desc = $(this).data('description');
$(document).on('click', '.editBtn', function() { const from = $(this).data('distance-from');
// quick debug: uncomment if modal doesn't open const to = $(this).data('distance-to');
// console.log('Edit clicked', $(this).data()); const mileage = $(this).data('mileage');
const toll = $(this).data('toll');
let $btn = $(this); const others = $(this).data('others');
let id = $btn.data('id');
let date = $btn.data('date'); const updateUrl = "{{ route('mileage::applications.update', ':id') }}".replace(':id', id);
let project = $btn.data('project');
let vehicle = $btn.data('vehicle'); $('#editForm').attr('action', updateUrl);
let description = $btn.data('description'); $('#editDate').val(date);
let distanceTo = $btn.data('distanceto'); $('#editDescription').val(desc);
let mileage = $btn.data('mileage'); $('#editFrom').val(from);
let toll = $btn.data('toll'); $('#editTo').val(to);
let others = $btn.data('others'); $('#editMileage').val(mileage);
let othersdesc = $btn.data('othersdesc'); $('#editToll').val(toll);
$('#editOthers').val(others);
// set simple inputs
$('#editId').val(id); // Trigger the others field logic
$('#editDate').val(date || ''); $('#editOthers').trigger('input');
$('#editDescription').val(description || '');
$('#editDistanceTo').val(distanceTo || '');
$('#editDistance').val(mileage ?? '');
$('#editToll').val(toll ?? '');
$('#editOthers').val(others ?? '');
$('#editOthersDescription').val(othersdesc ?? '');
// set semantic ui dropdown selected values
$('#editProject').dropdown('set selected', project || '');
$('#editVehicle').dropdown('set selected', vehicle || '');
if (others && others > 0) {
$('#editOthersDescription').prop('readonly', false);
} else {
$('#editOthersDescription').val('').prop('readonly', true);
}
// show modal
$('#editModal').modal('show'); $('#editModal').modal('show');
}); });
// AJAX submit update (uses route() helper safely by replacing :id) // Show/Hide "Butiran Lain-lain" based on input
$('#editForm').submit(function(e) { $('#editOthers').on('input', function() {
e.preventDefault(); const value = parseFloat($(this).val()) || 0;
if (value > 0) {
let id = $('#editId').val(); $('#othersDetailsField').slideDown();
let date = $('#editDate').val(); $('#editOthersDetails').attr('required', true);
let project = $('#editProject').val(); } else {
let vehicle = $('#editVehicle').val(); $('#othersDetailsField').slideUp();
let description = $('#editDescription').val(); $('#editOthersDetails').removeAttr('required');
let distanceTo = $('#editDistanceTo').val(); $('#editOthersDetails').val('');
let mileage = $('#editDistance').val();
let toll = $('#editToll').val();
let others = $('#editOthers').val();
let othersDesc = $('#editOthersDescription').val();
// check required fields
if (!date || !project || !vehicle || !description || !mileage) {
alert('Sila isi semua ruangan wajib.');
return;
} }
});
// check Butiran Lain-lain rule // Validate before form submission
if (others && others > 0 && (!othersDesc || othersDesc === '-')) { $('#editForm').on('submit', function(e) {
alert('Sila isi Butiran Lain-lain kerana anda mempunyai jumlah Lain-lain.'); const others = parseFloat($('#editOthers').val()) || 0;
const othersDetails = $('#editOthersDetails').val().trim();
if (others > 0 && othersDetails === '') {
e.preventDefault();
alert('Sila isi Butiran Lain-lain kerana jumlah Lain-lain (RM) lebih daripada 0.');
$('#editOthersDetails').focus();
return; return;
} }
let formData = $(this).serialize(); // contains _token // General validation for required fields
let valid = true;
$.ajax({ $('#editForm [required]').each(function() {
url: "{{ route('mileage::applications.update', ':id') }}".replace(':id', id), if ($(this).val().trim() === '') {
type: 'POST', valid = false;
data: formData + '&_method=PUT', $(this).focus();
beforeSend: function() { alert('Sila isi semua ruangan yang diperlukan.');
$('#editModal').modal('hide'); e.preventDefault();
$("#loader").show(); return false;
},
complete: function() {
$("#loader").hide();
},
success: function(response) {
if (response && response.success) {
alert(response.message || 'Claim updated successfully');
setTimeout(function() {
location.reload();
}, 300);
} else {
alert('Unexpected response from server.');
}
},
error: function(xhr) {
let msg = "Update failed.";
if (xhr.responseJSON) {
if (xhr.responseJSON.message) msg = xhr.responseJSON.message;
else if (xhr.responseJSON.errors) {
let errors = xhr.responseJSON.errors;
msg = Object.keys(errors).map(k => errors[k][0]).join("\n");
}
} else {
msg = xhr.status + ' ' + xhr.statusText;
}
alert(msg);
} }
}); });
}); });
}); });
</script> </script>
@endsection @endsection
<title>Butiran Tuntutan Bulanan</title>
@extends('ui::layouts.app') @extends('ui::layouts.app')
@section('content') @section('content')
<style> <style>
body { body { background-color: #f0f2f5; }
background-color: #f0f2f5; .ui.container { padding: 0 5rem 3rem 2rem; margin-top: 3rem; }
} h2.ui.header { font-size: 1.8rem; font-weight: 700; margin-bottom: .5rem; }
.ui.container { .ui.segment {
max-width: 70% !important; background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-top: 2rem; margin-top: 2rem;
padding: 1.5rem;
} }
.ui.segment { .status-label {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); display: inline-block;
padding: 6px 12px;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 600;
text-transform: capitalize;
opacity: 0.9;
}
.status-Diproses { background-color: rgba(33,133,208,0.3); color: #1b4f72; }
.status-Disokong { background-color: rgba(46,204,113,0.3); color: #1d5d38; }
.status-Disahkan { background-color: rgba(241,196,15,0.3); color: #7a6000; }
.status-Disemak { background-color: rgba(155,89,182,0.3); color: #4a2c63; }
.status-Diluluskan { background-color: rgba(26,188,156,0.3); color: #0b5449; }
.status-Ditolak { background-color: rgba(231,76,60,0.3); color: #7e2018; }
table.ui.table { background: white; border-radius: 10px; }
.totals-row td { font-weight: 700; background: #f7f7f8; }
.small-muted { font-size: .9rem; color: #666; }
@media (max-width: 768px) {
.ui.container { padding: 0 2rem; }
} }
</style> </style>
<div class="ui container"> <div class="ui container">
<h2 class="ui header">Maklumat Tuntutan</h2> <h2 class="ui header">Butiran Tuntutan Bulanan</h2>
<p>
<strong>Projek:</strong> {{ $projectName }} <br>
<strong>Bulan:</strong> {{ \Carbon\Carbon::parse($month)->translatedFormat('F Y') }}
</p>
{{-- Claims Table --}}
<table class="ui celled table">
<thead>
<tr>
<th>Tarikh</th>
<th>Butiran</th>
<th>Dari</th>
<th>Ke</th>
<th>Jumlah KM</th>
<th>Tol (RM)</th>
<th>Lain-lain (RM)</th>
<th>Status</th>
<th>Jumlah (RM)</th>
</tr>
</thead>
<tbody>
@php
$total = 0;
$totalMileage = 0;
$totalToll = 0;
$totalOthers = 0;
@endphp
@foreach($claims as $c)
@php
$total += $c->total_claim_amount;
$totalMileage += $c->total_mileage ?? 0;
$totalToll += $c->toll_amount ?? 0;
$totalOthers += $c->others_amount ?? 0;
@endphp
<div class="ui segment">
<table class="ui definition table">
<tr>
<td>Tarikh Tuntutan</td>
<td>{{ $claim->claim_date }}</td>
</tr>
<tr>
<td>Jenis Kenderaan</td>
<td>{{ $claim->jenisKenderaan->name ?? '-' }}</td>
</tr>
<tr>
<td>Nama Projek</td>
<td>{{ $claim->project->p_project_description ?? '-' }}</td>
</tr>
<tr>
<td>Butiran Tuntutan</td>
<td>{{ $claim->description ?? '-' }}</td>
</tr>
<tr>
<td>Jarak Dari</td>
<td>{{ $claim->distance_from ?? '-' }}</td>
</tr>
<tr>
<td>Jarak Ke</td>
<td>{{ $claim->distance_to ?? '-' }}</td>
</tr>
<tr>
<td>Jumlah Perbatuan (KM)</td>
<td>{{ $claim->total_mileage }} km</td>
</tr>
<tr>
<td>Jumlah Perbatuan Layak Tuntut</td>
<td>{{ $claim->claimable_mileage ?? 0 }} km</td>
</tr>
<tr>
<td>Jumlah Tuntutan Perjalanan (RM)</td>
<td>RM {{ number_format($claim->calculated_amount ?? 0, 2) }}</td>
</tr>
<tr>
<td>Toll (RM)</td>
<td>RM {{ number_format($claim->toll_amount ?? 0, 2) }}</td>
</tr>
<tr>
<td>Lain-lain (RM)</td>
<td>RM {{ number_format($claim->others_amount ?? 0, 2) }}</td>
</tr>
<tr>
<td>Butiran Lain-lain</td>
<td>{{ $claim->others_description ?? '-' }}</td>
</tr>
<tr>
<td>Penalti (RM) (Jika Ada)</td>
<td>
@if(isset($claim->display_penalty_amount))
@if($claim->display_penalty_amount > 0)
RM {{ number_format($claim->display_penalty_amount, 2) }}
@else
@if($claim->display_penalty_reason === 'Tertakluk kepada BOD')
<span style="color: orange;">{{ $claim->display_penalty_reason }}</span>
@else
RM 0.00
@endif
@endif
@else
RM {{ number_format($claim->penalty_amount ?? 0, 2) }}
@endif
</td>
</tr>
<tr> <tr>
<td>Sebab Penalti</td> <td>{{ \Carbon\Carbon::parse($c->claim_date)->format('d/m/Y') }}</td>
<td>{{ $c->description ?? '-' }}</td>
<td>{{ $c->distance_from ?? '-' }}</td>
<td>{{ $c->distance_to ?? '-' }}</td>
<td>{{ number_format($c->total_mileage, 1) }}</td>
<td>{{ number_format($c->toll_amount ?? 0, 2) }}</td>
<td>{{ number_format($c->others_amount ?? 0, 2) }}</td>
<td> <td>
{{ $claim->display_penalty_reason ?? $claim->penalty_reason ?? '-' }} <span class="status-label status-{{ $c->status }}">{{ $c->status }}</span>
</td> </td>
</tr> <td>RM {{ number_format($c->total_claim_amount, 2) }}</td>
<tr> </tr>
<td><strong>Jumlah Keseluruhan Tuntutan (RM)</strong></td> @endforeach
<td><strong>RM {{ number_format($claim->total_claim_amount ?? $claim->total_claim_amount ?? 0, 2) }}</strong></td> </tbody>
</tr> <tfoot>
<tr> <tr class="totals-row">
<td><strong>Status</strong></td> <td colspan="4" class="right aligned"><strong>Jumlah Keseluruhan:</strong></td>
<td><strong>{{ $claim->status }}</strong></td> <td>{{ number_format($totalMileage, 1) }} KM</td>
</tr> <td>RM {{ number_format($totalToll, 2) }}</td>
<td>RM {{ number_format($totalOthers, 2) }}</td>
<td></td>
<td><strong>RM {{ number_format($total, 2) }}</strong></td>
</tr>
</tfoot>
</table> </table>
{{-- Info Section --}}
<div class="ui segment">
<h4><i class="info circle icon"></i> Nota Pengiraan</h4>
<p>
• Potongan 40KM dikenakan bagi setiap hari perjalanan.<br>
• Kadar (Kereta): RM0.55/km bagi ≤500km, RM0.50/km bagi >500km.<br>
• Kadar (Motosikal): RM0.30/km.<br>
• Tuntutan termasuk Tol dan Lain-lain jika disertakan resit.
</p>
</div> </div>
<a href="{{ route('mileage::applications.index') }}" class="ui button"> {{-- Back button --}}
Kembali <a href="{{ route('mileage::applications.index', ['month' => $month]) }}" class="ui grey button">
<i class="arrow left icon"></i> Kembali
</a> </a>
</div> </div>
@endsection @endsection
<title>Maklumat Tuntutan</title>
@extends('ui::layouts.app')
@section('content')
<style>
body {
background-color: #f0f2f5;
}
.ui.container {
max-width: 70% !important;
margin-top: 2rem;
}
.ui.segment {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
border-radius: 10px;
background-color: #fff;
padding: 2rem;
}
table.ui.definition.table td:first-child {
width: 40%;
font-weight: 600;
background-color: #f9fafb;
}
.totals-row td {
font-weight: 700;
background: #f7f7f8;
}
.small-muted { font-size: .9rem; color: #666; }
</style>
<div class="ui container">
<h2 class="ui header">Maklumat Tuntutan</h2>
<div class="ui segment">
<table class="ui definition table">
<tr>
<td>Tarikh Tuntutan</td>
<td>{{ \Carbon\Carbon::parse($claim->claim_date)->format('d/m/Y') }}</td>
</tr>
<tr>
<td>Jenis Kenderaan</td>
<td>{{ $claim->jenisKenderaan->keterangan ?? $claim->jenisKenderaan->name ?? '-' }}</td>
</tr>
<tr>
<td>Nama Projek</td>
<td>{{ $claim->project->p_project_description ?? 'Tanpa Projek' }}</td>
</tr>
<tr>
<td>Butiran Tuntutan</td>
<td>{{ $claim->description ?? '-' }}</td>
</tr>
<tr>
<td>Jarak Dari</td>
<td>{{ $claim->distance_from ?? '-' }}</td>
</tr>
<tr>
<td>Jarak Ke</td>
<td>{{ $claim->distance_to ?? '-' }}</td>
</tr>
<tr>
<td>Jumlah Perbatuan (KM)</td>
<td>{{ number_format($claim->total_mileage ?? 0, 2) }} km</td>
</tr>
{{-- Claimable mileage (deduct 40km rule) --}}
<tr>
<td>Jumlah Perbatuan Layak Dituntut</td>
<td>
@php
$totalMileage = floatval($claim->total_mileage ?? 0);
$claimable = $claim->claimable_mileage ?? max(0, $totalMileage - 40);
@endphp
{{ number_format($claimable, 2) }} km
</td>
</tr>
{{-- Show per-km rate and mileage multiplication --}}
<tr>
<td>Kiraan (Perbatuan × Kadar)</td>
<td>
@php
// Determine rate by vehicle
$jenisId = $claim->jenis_kenderaan_id ?? ($claim->jenisKenderaan->id ?? null);
if ($jenisId == 1) {
$rate = ($claimable <= 500) ? 0.55 : 0.50; // Car
} else {
$rate = 0.30; // Motorcycle
}
$mileageAmount = round($claimable * $rate, 2);
@endphp
<span class="small-muted">{{ number_format($claimable, 2) }} km × RM {{ number_format($rate, 2) }}</span>
&nbsp;=&nbsp;
<strong>RM {{ number_format($mileageAmount, 2) }}</strong>
</td>
</tr>
{{-- Toll amount --}}
<tr>
<td>Tol (RM)</td>
<td>RM {{ number_format(floatval($claim->toll_amount ?? 0), 2) }}</td>
</tr>
{{-- Others amount --}}
<tr>
<td>Lain-lain (RM)</td>
<td>RM {{ number_format(floatval($claim->others_amount ?? 0), 2) }}</td>
</tr>
{{-- Butiran Lain-lain (always shown) --}}
<tr>
<td>Butiran Lain-lain</td>
<td>{{ $claim->others_description ?: 'Tiada Nilai Lain-lain' }}</td>
</tr>
{{-- Subtotal: mileageAmount + toll + others --}}
<tr>
<td>Jumlah Tuntutan (Perjalanan + Tol + Lain-lain)</td>
<td>
@php
$toll = floatval($claim->toll_amount ?? 0);
$others = floatval($claim->others_amount ?? 0);
$subtotal = round($mileageAmount + $toll + $others, 2);
@endphp
RM {{ number_format($subtotal, 2) }}
</td>
</tr>
<tr class="totals-row">
<td>Jumlah Akhir (RM)</td>
<td>
@php
$final = $claim->total_claim_amount ?? round($subtotal - ($displayPenalty ?? 0), 2);
@endphp
<strong>RM {{ number_format(floatval($final), 2) }}</strong>
</td>
</tr>
</table>
</div>
<a href="{{ route('mileage::applications.index') }}" class="ui button">
<i class="arrow left icon"></i> Kembali
</a>
</div>
@endsection
...@@ -2,67 +2,80 @@ ...@@ -2,67 +2,80 @@
@extends('ui::layouts.app') @extends('ui::layouts.app')
@push('style') @push('style')
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.3/semantic.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.3/semantic.min.css">
<style type="text/css"> <style type="text/css">
body { body { background-color: #f0f2f5; }
background-color: #f0f2f5;
}
.ui.container { .ui.container {
max-width: 100% !important; /* never exceed viewport */ max-width: 100% !important;
overflow-x: hidden; /* cut accidental overflow */ overflow-x: hidden;
padding: 0 5rem 0 2rem; /* breathing space from right */ padding: 0 5rem 0 2rem;
box-sizing: border-box; /* include padding in width calc */ box-sizing: border-box;
} }
.ui.segments { .ui.segments { margin-top: 2rem; }
margin-top: 2rem; .ui.segment { box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); }
}
.ui.segment {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.ui.form .inline.field { .ui.form .inline.field {
display: flex; display: flex;
flex-wrap: wrap; /* allow wrapping instead of pushing right */ flex-wrap: wrap;
gap: 0.5rem; /* spacing between label & field */ gap: 0.5rem;
width: 100%; /* force row within parent */ width: 100%;
box-sizing: border-box; box-sizing: border-box;
} }
.ui.form .inline.field > label.three-wide { .ui.form .inline.field > label.three-wide {
flex: 0 0 2rem; /* reserve space for label */ flex: 0 0 2rem;
margin-right: 50px; margin-right: 50px;
max-width: 30%; /* but never hog the full row */ max-width: 30%;
min-width: 8rem; min-width: 8rem;
white-space: normal; white-space: normal;
word-break: break-word; word-break: break-word;
} }
.ui.form .inline.field .eight.wide.field { .ui.form .inline.field .eight.wide.field {
flex: 1 1 auto; /* take remaining space */ flex: 1 1 auto;
min-width: 0; /* shrink properly */ min-width: 0;
max-width: 100%; /* don’t spill outside */ max-width: 100%;
box-sizing: border-box; box-sizing: border-box;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.ui.form .inline.field { .ui.form .inline.field { flex-direction: column !important; align-items: flex-start !important; }
flex-direction: column !important;
align-items: flex-start !important;
}
.ui.form .inline.field > label.three-wide, .ui.form .inline.field > label.three-wide,
.ui.form .inline.field .eight.wide.field { .ui.form .inline.field .eight.wide.field { width: 100% !important; max-width: 100% !important; }
width: 100% !important;
max-width: 100% !important;
} }
/* Modal styling */
#claimSummaryModal.ui.modal {
width: 500px !important;
max-width: 100vw !important;
font-size: 0.9rem;
} }
</style> #claimSummaryModal .content { max-height: 500px; overflow-y: auto; }
#claimSummaryModal .header { font-size: 1.1rem; }
#supportingDocsModal.ui.basic.modal {
left: calc(50% + 100px) !important;
transform: translate(-50%, -50%) !important;
width: 500px !important;
margin-top: 25%;
}
#supportingDocsModal .content p { font-size: 16px; line-height: 1.6; color: white; }
#supportingDocsModal .ui.icon.header { color: #b33a3a; font-size: 2rem; }
#supportingDocsModal .ui.icon.header i.icon { font-size: 5rem; }
#supportingDocsModal .actions .button {
background: linear-gradient(135deg, #21ba45 0%, #16ab39 100%);
color: white; border-radius: 10px;
padding: 10px 20px; font-weight: 600;
}
</style>
@endpush @endpush
@section('content') @section('content')
<div class="ui container"> <div class="ui container">
<div class="ui segments"> <div class="ui segments">
<div class="ui segment"> <div class="ui segment">
<h2 class="ui header" style="margin: 10px">Permohonan Tuntutan Perjalanan</h2> <h2 class="ui header" style="margin: 10px">Permohonan Tuntutan Perjalanan</h2>
...@@ -71,11 +84,13 @@ ...@@ -71,11 +84,13 @@
<div class="ui secondary segment"> <div class="ui secondary segment">
<form class="ui form" id="claimForm" action="{{ route('mileage::applications.store') }}" method="POST"> <form class="ui form" id="claimForm" action="{{ route('mileage::applications.store') }}" method="POST">
@csrf @csrf
{{-- Maklumat Tuntutan --}} {{-- Maklumat Tuntutan --}}
<div class="ui segment"> <div class="ui segment">
<p>Maklumat Tuntutan</p> <p><strong>Maklumat Tuntutan</strong></p>
<div class="ui segment"> <div class="ui segment">
<div class="ui form"> <div class="ui form">
<div class="inline field required"> <div class="inline field required">
<label class="three-wide field">Tarikh</label> <label class="three-wide field">Tarikh</label>
<div class="eight wide field ui calendar" id="claim_date"> <div class="eight wide field ui calendar" id="claim_date">
...@@ -85,6 +100,7 @@ ...@@ -85,6 +100,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="inline field required"> <div class="inline field required">
<label class="three-wide field">Jenis Kenderaan</label> <label class="three-wide field">Jenis Kenderaan</label>
<select class="ui dropdown eight wide field" name="jenis_kenderaan_id" id="jenis_kenderaan_id" required> <select class="ui dropdown eight wide field" name="jenis_kenderaan_id" id="jenis_kenderaan_id" required>
...@@ -94,19 +110,24 @@ ...@@ -94,19 +110,24 @@
@endforeach @endforeach
</select> </select>
</div> </div>
<div class="inline field required">
<label class="three-wide field">Nama Projek<font color="red">*</font></label> {{-- optional project picker --}}
<select class="ui search dropdown eight wide field form-control" name="project_id" id="project_select" required> <div class="inline field">
<option value="">Sila Pilih Projek</option> <label class="three-wide field required">Nama Projek</label>
<select class="ui search dropdown eight wide field" name="project_id" id="project_id" required>
<option value="">Pilih Projek</option>
@foreach($projects as $project) @foreach($projects as $project)
<option value="{{ $project->id }}">{{ $project->p_project_description }}</option> <option value="{{ $project->id }}">{{ $project->p_project_description }}</option>
@endforeach @endforeach
</select> </select>
</div> </div>
<div class="inline field required"> <div class="inline field required">
<label class="three-wide field">Butiran Tuntutan<font color="red">*</font></label> <label class="three-wide field">Butiran</label>
<input class="eight wide field" name="description" type="text" placeholder="Cth: Mesyuarat Projek B" required> <input class="eight wide field" name="description" type="text" placeholder="Cth: Mesyuarat Dalaman" required>
</div> </div>
<div class="inline field required"> <div class="inline field required">
<label class="three-wide field">Jarak Dari</label> <label class="three-wide field">Jarak Dari</label>
<select class="ui search dropdown eight wide field" name="distance_from"> <select class="ui search dropdown eight wide field" name="distance_from">
...@@ -114,23 +135,28 @@ ...@@ -114,23 +135,28 @@
<option value="Pejabat">Pejabat</option> <option value="Pejabat">Pejabat</option>
</select> </select>
</div> </div>
<div class="inline field required"> <div class="inline field required">
<label class="three-wide field">Jarak Ke</label> <label class="three-wide field">Jarak Ke</label>
<input class="eight wide field" name="distance_to" type="text" placeholder="Cth: Pejabat PPJ" required> <input class="eight wide field" name="distance_to" type="text" placeholder="Cth: Pejabat JKR" required>
</div> </div>
<div class="inline field required"> <div class="inline field required">
<label class="three-wide field">Jumlah Perbatuan (KM)</label> <label class="three-wide field">Jumlah Perbatuan (KM)</label>
<input class="eight wide field" name="total_mileage" id="total_mileage" type="number" step="0.01" placeholder="Cth: 120.50" required> <input class="eight wide field" name="total_mileage" id="total_mileage" type="number" step="0.01" placeholder="Cth: 120" required>
</div> </div>
<div class="inline field"> <div class="inline field">
<label class="three-wide field">Toll (RM)</label> <label class="three-wide field">Toll (RM)</label>
<input class="eight wide field" name="toll_amount" id="toll_amount" type="number" step="0.01" placeholder="Cth: 15.00"> <input class="eight wide field" name="toll_amount" id="toll_amount" type="number" step="0.01" placeholder="Cth: 10.00">
</div> </div>
<div class="inline field"> <div class="inline field">
<label class="three-wide field">Lain-lain (RM)</label> <label class="three-wide field">Lain-lain (RM)</label>
<input class="eight wide field" name="others_amount" id="others_amount" type="number" step="0.01" placeholder="Cth: 0.00"> <input class="eight wide field" name="others_amount" id="others_amount" type="number" step="0.01" placeholder="Cth: 5.00">
</div> </div>
<div class="inline field required" id="others_description_wrapper" style="display:none;">
<div class="inline field" id="others_description_wrapper" style="display:none;">
<label class="three-wide field">Butiran Lain-lain</label> <label class="three-wide field">Butiran Lain-lain</label>
<input class="eight wide field" name="others_description" id="others_description" type="text" placeholder="Nyatakan butiran lain-lain"> <input class="eight wide field" name="others_description" id="others_description" type="text" placeholder="Nyatakan butiran lain-lain">
</div> </div>
...@@ -138,290 +164,182 @@ ...@@ -138,290 +164,182 @@
</div> </div>
</div> </div>
{{-- Rumusan Tuntutan --}} {{-- Ringkasan --}}
<div class="ui segment"> <div class="ui segment">
<p>Rumusan Pengiraan Tuntutan</p> <p><strong>Ringkasan Pengiraan</strong></p>
<div class="ui segment"> <div class="ui segment">
<table class="ui celled table"> <table class="ui celled table">
<tbody> <tbody>
<tr><td>Jumlah Perbatuan Layak Tuntut</td><td id="claimable_mileage_formula">0 - 40 = 0 KM</td></tr>
<tr><td>Toll (RM)</td><td id="toll_claim">RM 0.00</td></tr>
<tr><td>Lain-lain (RM)</td><td id="others_claim">RM 0.00</td></tr>
<tr> <tr>
<td>Jumlah Perbatuan Layak Tuntut</td> <td>
<td id="claimable_mileage">0 KM</td> <strong>Jumlah Tol dan Lain-lain</strong><br>
</tr> <small>(Jumlah keseluruhan sebenar akan dikira di Senarai Tuntutan)</small>
<tr> </td>
<td>Jumlah Tuntutan Perjalanan <span id="mileage_rate_label"></span></td> <td>
<td id="mileage_claim_amount">RM 0.00</td> <strong id="total_claim">RM 0.00</strong>
</tr> </td>
<tr> </tr> </tbody>
<td>Toll (RM)</td>
<td id="toll_claim_amount">RM 0.00</td>
</tr>
<tr>
<td>Lain-lain (RM)</td>
<td id="others_claim_amount">RM 0.00</td>
</tr>
<tr>
<td>Penalti (RM) (Jika Ada)</td>
<td id="penalty_claim_amount">RM 0.00</td>
</tr>
<tr>
<td>Sebab Penalti (Jika Ada)</td>
<td id="penalty_reason">-</td>
</tr>
<tr>
<td><strong>Jumlah Keseluruhan Tuntutan (RM)</strong></td>
<td><strong id="total_claim_amount">RM 0.00</strong></td>
</tr>
</tbody>
</table> </table>
</div> </div>
</div> </div>
<!-- Hidden inputs to send calculated values to server --> <button class="ui primary button blue" type="submit">
<input type="hidden" name="user_id" id="user_id_input" value="{{ auth()->user()->id }}"> <i class="paper plane icon"></i> Hantar
<!-- <input type="hidden" name="staff_id" id="staff_id_input" value="{{ auth()->user()->employeeCode }}"> --> </button>
<input type="hidden" name="claimable_mileage" id="claimable_mileage_input" value="0"> <input type="hidden" name="claimable_mileage" id="claimable_mileage_input" value="0">
<input type="hidden" name="mileage_claim_amount" id="mileage_claim_amount_input" value="0.00">
<input type="hidden" name="total_claim_amount" id="total_claim_amount_input" value="0.00">
<input type="hidden" name="toll_amount" id="toll_amount_input" value="0.00">
<input type="hidden" name="others_amount" id="others_amount_input" value="0.00">
<input type="hidden" name="penalty_amount" id="penalty_amount_input" value="0.00">
<input type="hidden" name="penalty_reason" id="penalty_reason_input" value="-">
<button class="ui primary button blue" type="submit" id="btn_hantar">Seterusnya</button>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
@endsection @endsection
@push('script') @push('script')
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.3/semantic.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.3/semantic.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.js"></script>
<script>
$(document).ready(function() {
// --- Modal on load ---
const modalHtml = `
<div class="ui basic modal" id="supportingDocsModal">
<div class="ui icon header">
<i class="exclamation triangle icon"></i>
Peringatan Penting
</div>
<div class="content" style="text-align:center;">
<p>Dokumen sokongan seperti resit mesti dihantar kepada pihak Finance.<br>
Jika <b>tidak</b>, tuntutan akan <b>DITOLAK</b>.</p>
</div>
<div class="actions" style="text-align:center;">
<div class="ui green ok inverted button"><i class="checkmark icon"></i> Saya Faham</div>
</div>
</div>`;
$('body').append(modalHtml);
<script> $('#claimForm :input').prop('disabled', true);
$(document).ready(function() {
// Initialize dropdowns & calendar $('#supportingDocsModal').modal({
closable: false,
onApprove: function() {
$('#claimForm :input').prop('disabled', false);
$('.ui.dropdown').dropdown(); $('.ui.dropdown').dropdown();
$('#claim_date').calendar({ $('#claim_date').calendar({
type: 'date', type: 'date',
maxDate: new Date(), // Prevent selecting future dates maxDate: new Date(),
formatter: { formatter: {
date: function (date) { date: function(date) {
if (!date) return ''; if (!date) return '';
let day = date.getDate(); let d = date.getDate();
let month = date.getMonth() + 1; let m = date.getMonth() + 1;
let year = date.getFullYear(); let y = date.getFullYear();
return year + '-' + (month < 10 ? '0' : '') + month + '-' + (day < 10 ? '0' : '') + day; return `${y}-${m<10?'0':''}${m}-${d<10?'0':''}${d}`;
} }
},
onChange: function(date, text) {
$('#date').val(text);
} }
}); });
// Calculate total claim
$('#total_mileage, #toll_amount, #others_amount, #jenis_kenderaan_id').on('input change', function() {
calculateTotalClaim();
});
$('#others_amount').on('input', function() {
let val = parseFloat($(this).val()) || 0;
if (val > 0) {
$('#others_description_wrapper').slideDown();
$('#others_description').prop('required', true);
} else {
$('#others_description_wrapper').slideUp();
$('#others_description').prop('required', false).val('');
} }
}); }).modal('show');
function calculateTotalClaim() { // --- Calculation ---
const vehicleType = $('#jenis_kenderaan_id').val(); function calculateTotals() {
let mileage = parseFloat($('#total_mileage').val()) || 0; const mileage = parseFloat($('#total_mileage').val()) || 0;
const claimable = Math.max(0, mileage - 40);
const toll = parseFloat($('#toll_amount').val()) || 0; const toll = parseFloat($('#toll_amount').val()) || 0;
const others = parseFloat($('#others_amount').val()) || 0; const others = parseFloat($('#others_amount').val()) || 0;
const total = toll + others;
const vehicleRates = { $('#claimable_mileage_formula').text(`${mileage.toFixed(2)} - 40 = ${claimable.toFixed(2)} KM`);
'1': { type: 'Kereta', baseRate: 0.55, higherMileageRate: 0.50 }, $('#toll_claim').text(`RM ${toll.toFixed(2)}`);
'2': { type: 'Motosikal', baseRate: 0.30 } $('#others_claim').text(`RM ${others.toFixed(2)}`);
}; $('#total_claim').text(`RM ${total.toFixed(2)}`);
let rate = 0, claimableMileage = 0, mileageClaimAmount = 0;
if (vehicleRates[vehicleType]) { $('#claimable_mileage_input').val(claimable.toFixed(2));
claimableMileage = Math.max(0, mileage - 40);
if (vehicleRates[vehicleType].type === 'Kereta') {
if (claimableMileage <= 500) {
mileageClaimAmount = claimableMileage * vehicleRates[vehicleType].baseRate;
} else {
mileageClaimAmount = (500 * vehicleRates[vehicleType].baseRate) +
((claimableMileage - 500) * vehicleRates[vehicleType].higherMileageRate);
}
} else {
// Motosikal - flat rate
mileageClaimAmount = claimableMileage * vehicleRates[vehicleType].baseRate;
}
} }
let rateLabel = ""; $('#total_mileage, #toll_amount, #others_amount').on('input', function() {
calculateTotals();
if (vehicleRates[vehicleType]) { const othersVal = parseFloat($('#others_amount').val()) || 0;
claimableMileage = Math.max(0, mileage - 40); if (othersVal > 0) {
$('#others_description_wrapper').slideDown();
if (vehicleRates[vehicleType].type === 'Kereta') { $('#others_description').prop('required', true);
if (claimableMileage <= 500) {
mileageClaimAmount = claimableMileage * vehicleRates[vehicleType].baseRate;
rateLabel = `(RM ${vehicleRates[vehicleType].baseRate.toFixed(2)}/km)`;
} else {
mileageClaimAmount = (500 * vehicleRates[vehicleType].baseRate) +
((claimableMileage - 500) * vehicleRates[vehicleType].higherMileageRate);
rateLabel = `(RM ${vehicleRates[vehicleType].baseRate.toFixed(2)}/km first 500km, RM ${vehicleRates[vehicleType].higherMileageRate.toFixed(2)}/km after)`;
}
} else { } else {
mileageClaimAmount = claimableMileage * vehicleRates[vehicleType].baseRate; $('#others_description_wrapper').slideUp();
rateLabel = `(RM ${vehicleRates[vehicleType].baseRate.toFixed(2)}/km)`; $('#others_description').prop('required', false).val('');
}
}
// --- Penalty calculation ---
let penalty = 0, penaltyReason = "-";
const claimDate = $('#date').val();
if (claimDate) {
const claimMoment = new Date(claimDate);
const now = new Date();
// calculate month difference
const monthsDiff = (now.getFullYear() - claimMoment.getFullYear()) * 12 +
(now.getMonth() - claimMoment.getMonth());
if (monthsDiff <= 1) {
penalty = 0;
penaltyReason = "-";
$('#btn_hantar').prop('disabled', false);
} else if (monthsDiff > 1 && monthsDiff <= 3) {
penalty = mileageClaimAmount * 0.10;
penaltyReason = "Lewat hantar lebih 1 bulan tetapi ≤ 3 bulan (10%)";
$('#btn_hantar').prop('disabled', false);
} else if (monthsDiff > 3 && monthsDiff <= 6) {
penalty = mileageClaimAmount * 0.30;
penaltyReason = "Lewat hantar lebih 3 bulan tetapi ≤ 6 bulan (30%)";
$('#btn_hantar').prop('disabled', false);
} else if (monthsDiff > 6) {
penaltyReason = "Lewat lebih 6 bulan - Tertakluk kepada budi bicara Lembaga Pengarah";
$('#btn_hantar').prop('disabled', true);
showCustomAlert("Tuntutan lebih daripada 6 bulan tidak boleh dihantar. Sila rujuk Lembaga Pengarah.");
}
}
const totalClaim = mileageClaimAmount + toll + others - penalty;
// Update visible summary
$('#claimable_mileage').text(claimableMileage.toFixed(2) + ' KM');
$('#mileage_claim_amount').text('RM ' + mileageClaimAmount.toFixed(2));
$('#mileage_rate_label').text(rateLabel);
$('#toll_claim_amount').text('RM ' + toll.toFixed(2));
$('#others_claim_amount').text('RM ' + others.toFixed(2));
$('#penalty_claim_amount').text('RM ' + penalty.toFixed(2));
$('#penalty_reason').text(penaltyReason);
$('#total_claim_amount').text('RM ' + totalClaim.toFixed(2));
// Update hidden inputs
$('#claimable_mileage_input').val(claimableMileage.toFixed(2));
$('#mileage_claim_amount_input').val(mileageClaimAmount.toFixed(2));
$('#toll_amount_input').val(toll.toFixed(2));
$('#others_amount_input').val(others.toFixed(2));
$('#total_claim_amount_input').val(totalClaim.toFixed(2));
$('#penalty_amount_input').val(penalty.toFixed(2));
$('#penalty_reason_input').val(penaltyReason);
} }
});
// --- summary Modal ---
// show modal on form submit, but prevent regular submit
$('#claimForm').on('submit', function(e) { $('#claimForm').on('submit', function(e) {
e.preventDefault(); e.preventDefault();
// client-side required checks (keep yours or extend) // ✅ Project validation check
const requiredFields = [ const projectId = $('#project_id').val();
'#date', if (!projectId || projectId === "") {
'#jenis_kenderaan_id', $('body').toast({
'input[name="description"]', class: 'error',
'select[name="distance_from"]', message: 'Sila pilih projek sebelum menghantar tuntutan.',
'input[name="distance_to"]', showProgress: 'bottom'
'#total_mileage',
'#project_select'
];
let allFieldsFilled = requiredFields.every(field => {
const val = $(field).val();
return val !== null && val.toString().trim() !== '';
}); });
if (!allFieldsFilled) { $('#project_id').closest('.field').addClass('error');
showCustomAlert("Sila isi semua maklumat yang bertanda *"); return false;
return; } else {
$('#project_id').closest('.field').removeClass('error');
} }
const project = $('#project_id option:selected').text();
// build modal HTML (remove previous first)
$('#claimSummaryModal').remove();
const claimDate = $('#date').val(); const claimDate = $('#date').val();
const vehicleTypeText = $('#jenis_kenderaan_id option:selected').text(); const vehicle = $('#jenis_kenderaan_id option:selected').text();
const projectText = $('#project_select option:selected').text(); const desc = $('input[name="description"]').val();
const description = $('input[name="description"]').val(); const from = $('select[name="distance_from"] option:selected').text();
const distanceFrom = $('select[name="distance_from"] option:selected').text(); const to = $('input[name="distance_to"]').val();
const distanceTo = $('input[name="distance_to"]').val(); const mileage = $('#total_mileage').val();
const claimableMileage = $('#claimable_mileage_input').val() || '0'; const claimable = $('#claimable_mileage_input').val();
const mileageClaimAmount = $('#mileage_claim_amount_input').val() || '0.00'; const toll = $('#toll_amount').val() || 0;
const toll = $('#toll_amount_input').val() || '0.00'; const others = $('#others_amount').val() || 0;
const others = $('#others_amount_input').val() || '0.00'; const othersDesc = $('#others_description').val() || '-';
const penalty = parseFloat($('#penalty_claim_amount').text().replace('RM ', '')) || 0;
const penaltyReason = $('#penalty_reason').text() || "-"; // Remove previous modal if any
const totalClaim = $('#total_claim_amount_input').val() || '0.00'; $('#claimSummaryModal').remove();
const summaryHtml = ` const summaryModal = `
<div class="ui large modal" id="claimSummaryModal"> <div class="ui modal" id="claimSummaryModal">
<div class="header">Ringkasan Tuntutan Anda</div> <div class="header">Ringkasan Tuntutan Anda</div>
<div class="content"> <div class="content">
<p><strong>Tarikh:</strong> ${claimDate}</p> <p><strong>Tarikh:</strong> ${claimDate}</p>
<p><strong>Jenis Kenderaan:</strong> ${vehicleTypeText}</p> <p><strong>Jenis Kenderaan:</strong> ${vehicle}</p>
<p><strong>Projek:</strong> ${projectText}</p> <p><strong>Nama Projek:</strong> ${project}</p>
<p><strong>Butiran Tuntutan:</strong> ${description}</p> <p><strong>Butiran:</strong> ${desc}</p>
<p><strong>Dari:</strong> ${distanceFrom}</p> <p><strong>Dari:</strong> ${from}</p>
<p><strong>Ke:</strong> ${distanceTo}</p> <p><strong>Ke:</strong> ${to}</p>
<p><strong>Jumlah Perbatuan Layak Tuntut:</strong> ${claimableMileage} KM</p> <p><strong>Jumlah Perbatuan:</strong> ${mileage} KM</p>
<p><strong>Jumlah Tuntutan Perjalanan:</strong> RM ${parseFloat(mileageClaimAmount).toFixed(2)}</p> <p><strong>Layak Dituntut:</strong> ${mileage} - 40 = ${claimable} KM</p>
<p><strong>Toll:</strong> RM ${parseFloat(toll).toFixed(2)}</p> <p><strong>Toll:</strong> RM ${parseFloat(toll).toFixed(2)}</p>
<p><strong>Lain-lain:</strong> RM ${parseFloat(others).toFixed(2)}</p> <p><strong>Lain-lain:</strong> RM ${parseFloat(others).toFixed(2)}</p>
<p><strong>Butiran Lain-lain: </strong>${$('#others_description').val() ? `(${ $('#others_description').val() })` : '' }</p> <p><strong>Butiran Lain-lain:</strong> ${othersDesc}</p>
<p><strong>Penalti (Jika Ada):</strong> ${$('#penalty_claim_amount').text()}</p>
<p><strong>Sebab Penalti:</strong> ${$('#penalty_reason').text()}</p>
<p><strong>Jumlah Keseluruhan Tuntutan:</strong> RM ${parseFloat(totalClaim).toFixed(2)}</p>
</div> </div>
<div class="actions"> <div class="actions">
<div class="ui button" id="editClaim">Edit</div> <div class="ui grey button" id="editClaim"><i class="arrow left icon"></i> Edit</div>
<div class="ui green approve button" id="confirmClaim">Confirm</div> <div class="ui green approve button"><i class="check icon"></i> Hantar</div>
</div> </div>
</div>`; </div>`;
$('body').append(summaryHtml); $('body').append(summaryModal);
// show modal; onApprove -> do AJAX POST to store route, then redirect to show page
$('#claimSummaryModal').modal({ $('#claimSummaryModal').modal({
closable: false,
onApprove: function() { onApprove: function() {
$('#claimForm')[0].submit(); // normal form submit $('#claimForm')[0].submit();
return false; // stop modal from auto-closing twice return false;
} }
}).modal('show'); }).modal('show');
// Edit: hide modal to edit fields $(document).off('click', '#editClaim').on('click', '#editClaim', function() {
$('#editClaim').on('click', function() {
$('#claimSummaryModal').modal('hide'); $('#claimSummaryModal').modal('hide');
}); });
}); });
calculateTotalClaim(); });
}); </script>
</script>
@endpush @endpush
<title>Form Tuntutan (Tanpa Projek)</title>
@extends('ui::layouts.app')
@push('style')
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.3/semantic.min.css">
<style type="text/css">
body { background-color: #f0f2f5; }
.ui.container {
max-width: 100% !important;
overflow-x: hidden;
padding: 0 5rem 0 2rem;
box-sizing: border-box;
}
.ui.segments { margin-top: 2rem; }
.ui.segment { box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); }
.ui.form .inline.field {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
width: 100%;
box-sizing: border-box;
}
.ui.form .inline.field > label.three-wide {
flex: 0 0 2rem;
margin-right: 50px;
max-width: 30%;
min-width: 8rem;
white-space: normal;
word-break: break-word;
}
.ui.form .inline.field .eight.wide.field {
flex: 1 1 auto;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
}
@media (max-width: 768px) {
.ui.form .inline.field { flex-direction: column !important; align-items: flex-start !important; }
.ui.form .inline.field > label.three-wide,
.ui.form .inline.field .eight.wide.field { width: 100% !important; max-width: 100% !important; }
}
/* Modal styling */
#claimSummaryModal.ui.modal {
width: 500px !important;
max-width: 100vw !important;
font-size: 0.9rem;
}
#claimSummaryModal .content { max-height: 500px; overflow-y: auto; }
#claimSummaryModal .header { font-size: 1.1rem; }
#supportingDocsModal.ui.basic.modal {
left: calc(50% + 100px) !important;
transform: translate(-50%, -50%) !important;
width: 500px !important;
margin-top: 25%;
}
#supportingDocsModal .content p { font-size: 16px; line-height: 1.6; color: white; }
#supportingDocsModal .ui.icon.header { color: #b33a3a; font-size: 2rem; }
#supportingDocsModal .ui.icon.header i.icon { font-size: 5rem; }
#supportingDocsModal .actions .button {
background: linear-gradient(135deg, #21ba45 0%, #16ab39 100%);
color: white; border-radius: 10px;
padding: 10px 20px; font-weight: 600;
}
</style>
@endpush
@section('content')
<div class="ui container">
<div class="ui segments">
<div class="ui segment">
<h2 class="ui header" style="margin: 10px">Permohonan Tuntutan Perjalanan (Tanpa Projek)</h2>
</div>
<div class="ui secondary segment">
<form class="ui form" id="claimForm" action="{{ route('mileage::applications.store') }}" method="POST">
@csrf
{{-- Maklumat Tuntutan --}}
<div class="ui segment">
<p><strong>Maklumat Tuntutan</strong></p>
<div class="ui segment">
<div class="ui form">
<div class="inline field required">
<label class="three-wide field">Tarikh</label>
<div class="eight wide field ui calendar" id="claim_date">
<div class="ui input left icon">
<i class="calendar icon"></i>
<input type="text" name="claim_date" id="date" placeholder="Tarikh Tuntutan" required readonly>
</div>
</div>
</div>
<div class="inline field required">
<label class="three-wide field">Jenis Kenderaan</label>
<select class="ui dropdown eight wide field" name="jenis_kenderaan_id" id="jenis_kenderaan_id" required>
<option value="">Sila Pilih</option>
@foreach($jenisKenderaan as $jk)
<option value="{{ $jk->id }}">{{ $jk->name }}</option>
@endforeach
</select>
</div>
<div class="inline field required">
<label class="three-wide field">Butiran Tuntutan</label>
<input class="eight wide field" name="description" type="text" placeholder="Cth: Mesyuarat Dalaman" required>
</div>
<div class="inline field required">
<label class="three-wide field">Jarak Dari</label>
<select class="ui search dropdown eight wide field" name="distance_from">
<option value="">Sila Pilih Lokasi</option>
<option value="Pejabat">Pejabat</option>
</select>
</div>
<div class="inline field required">
<label class="three-wide field">Jarak Ke</label>
<input class="eight wide field" name="distance_to" type="text" placeholder="Cth: Pejabat JKR" required>
</div>
<div class="inline field required">
<label class="three-wide field">Jumlah Perbatuan (KM)</label>
<input class="eight wide field" name="total_mileage" id="total_mileage" type="number" step="0.01" placeholder="Cth: 120" required>
</div>
<div class="inline field">
<label class="three-wide field">Toll (RM)</label>
<input class="eight wide field" name="toll_amount" id="toll_amount" type="number" step="0.01" placeholder="Cth: 10.00">
</div>
<div class="inline field">
<label class="three-wide field">Lain-lain (RM)</label>
<input class="eight wide field" name="others_amount" id="others_amount" type="number" step="0.01" placeholder="Cth: 5.00">
</div>
<div class="inline field" id="others_description_wrapper" style="display:none;">
<label class="three-wide field">Butiran Lain-lain</label>
<input class="eight wide field" name="others_description" id="others_description" type="text" placeholder="Nyatakan butiran lain-lain">
</div>
</div>
</div>
</div>
{{-- Ringkasan --}}
<div class="ui segment">
<p><strong>Ringkasan Pengiraan</strong></p>
<div class="ui segment">
<table class="ui celled table">
<tbody>
<tr><td>Jumlah Perbatuan Layak Tuntut</td><td id="claimable_mileage_formula">0 - 40 = 0 KM</td></tr>
<tr><td>Toll (RM)</td><td id="toll_claim">RM 0.00</td></tr>
<tr><td>Lain-lain (RM)</td><td id="others_claim">RM 0.00</td></tr>
<tr>
<td>
<strong>Jumlah Tol dan Lain-lain</strong><br>
<small>(Jumlah keseluruhan sebenar akan dikira di Senarai Tuntutan)</small>
</td>
<td>
<strong id="total_claim">RM 0.00</strong>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<button class="ui primary button blue" type="submit">
<i class="paper plane icon"></i> Hantar
</button>
<input type="hidden" name="claimable_mileage" id="claimable_mileage_input" value="0">
</form>
</div>
</div>
</div>
@endsection
@push('script')
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.3/semantic.min.js"></script>
<script>
$(document).ready(function() {
// --- Warning Modal on load ---
const modalHtml = `
<div class="ui basic modal" id="supportingDocsModal">
<div class="ui icon header">
<i class="exclamation triangle icon"></i>
Peringatan Penting
</div>
<div class="content" style="text-align:center;">
<p style="font-size:16px; line-height:1.5;">
Dokumen sokongan seperti resit mesti dihantar kepada pihak Finance sebagai bukti.<br>
Jika <b>tidak</b>, tuntutan akan <b>DITOLAK</b>.
</p>
</div>
<div class="actions" style="text-align:center;">
<div class="ui green ok inverted button">
<i class="checkmark icon"></i> Saya Faham
</div>
</div>
</div>`;
$('body').append(modalHtml);
// Disable inputs until user acknowledges
$('#claimForm :input').prop('disabled', true);
$('#supportingDocsModal').modal({
closable: false,
onApprove: function() {
$('#claimForm :input').prop('disabled', false);
$('.ui.dropdown').dropdown();
$('#claim_date').calendar({
type: 'date',
maxDate: new Date(),
formatter: {
date: function(date) {
if (!date) return '';
let d = date.getDate();
let m = date.getMonth() + 1;
let y = date.getFullYear();
return `${y}-${m<10?'0':''}${m}-${d<10?'0':''}${d}`;
}
}
});
}
}).modal('show');
// --- Calculation ---
function calculateTotals() {
const mileage = parseFloat($('#total_mileage').val()) || 0;
const claimable = Math.max(0, mileage - 40);
const toll = parseFloat($('#toll_amount').val()) || 0;
const others = parseFloat($('#others_amount').val()) || 0;
const total = toll + others;
$('#claimable_mileage_formula').text(`${mileage.toFixed(2)} - 40 = ${claimable.toFixed(2)} KM`);
$('#toll_claim').text(`RM ${toll.toFixed(2)}`);
$('#others_claim').text(`RM ${others.toFixed(2)}`);
$('#total_claim').text(`RM ${total.toFixed(2)}`);
$('#claimable_mileage_input').val(claimable.toFixed(2));
}
$('#total_mileage, #toll_amount, #others_amount').on('input', function() {
calculateTotals();
const othersVal = parseFloat($('#others_amount').val()) || 0;
if (othersVal > 0) {
$('#others_description_wrapper').slideDown();
$('#others_description').prop('required', true);
} else {
$('#others_description_wrapper').slideUp();
$('#others_description').prop('required', false).val('');
}
});
// --- Summary Modal ---
$('#claimForm').on('submit', function(e) {
e.preventDefault();
const claimDate = $('#date').val();
const vehicle = $('#jenis_kenderaan_id option:selected').text();
const desc = $('input[name="description"]').val();
const from = $('select[name="distance_from"] option:selected').text();
const to = $('input[name="distance_to"]').val();
const mileage = $('#total_mileage').val();
const claimable = $('#claimable_mileage_input').val();
const toll = $('#toll_amount').val() || 0;
const others = $('#others_amount').val() || 0;
const othersDesc = $('#others_description').val() || '-';
const subtotal = $('#total_claim').text() || '-';
$('#claimSummaryModal').remove();
const summaryModal = `
<div class="ui modal" id="claimSummaryModal">
<div class="header">Ringkasan Tuntutan Anda</div>
<div class="content">
<p><strong>Tarikh:</strong> ${claimDate}</p>
<p><strong>Jenis Kenderaan:</strong> ${vehicle}</p>
<p><strong>Butiran:</strong> ${desc}</p>
<p><strong>Dari:</strong> ${from}</p>
<p><strong>Ke:</strong> ${to}</p>
<p><strong>Jumlah Perbatuan:</strong> ${mileage} KM</p>
<p><strong>Layak Dituntut:</strong> ${mileage} - 40 = ${claimable} KM</p>
<p><strong>Toll:</strong> RM ${parseFloat(toll).toFixed(2)}</p>
<p><strong>Lain-lain:</strong> RM ${parseFloat(others).toFixed(2)}</p>
<p><strong>Butiran Lain-lain:</strong> ${othersDesc}</p>
<p><strong>Jumlah Tol dan Lain-lain (Jumlah Keseluruhan akan Dikira di Senarai Tuntutan):</strong> ${subtotal}</p>
</div>
<div class="actions">
<div class="ui grey button" id="editClaim"><i class="arrow left icon"></i> Edit</div>
<div class="ui green approve button"><i class="check icon"></i> Hantar</div>
</div>
</div>`;
$('body').append(summaryModal);
$('#claimSummaryModal').modal({
closable: false,
onApprove: function() {
$('#claimForm')[0].submit();
return false;
}
}).modal('show');
$(document).off('click', '#editClaim').on('click', '#editClaim', function() {
$('#claimSummaryModal').modal('hide');
});
});
});
</script>
@endpush
...@@ -90,18 +90,27 @@ ...@@ -90,18 +90,27 @@
</div> </div>
</h4> </h4>
</td> </td>
<td>{{ data_get($staff, 'staffinfo.lkpdepartment.description') }}</td> <td>{{ data_get($staff, 'staffinfo.department.description') }}</td>
</tr>
<tr>
<td width="50%">
<h4 class="ui image header">
<i class="user tie icon" class="ui mini rounded image"></i>
<div class="content" style="font-style: unset">
<span
style="font-size: 14px !important;font-weight: normal !important;padding-left:10px">Peranan</span>
</div>
</h4>
</td>
<td>
@php
$roles = $staff->roles->pluck('name')->toArray();
@endphp
{{ !empty($roles) ? implode(', ', $roles) : '-' }}
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@if (auth()->user()->can('HRVIEWS'))
<div>HR je leh nampak</div>
@endif
@if (auth()->user()->can('TRY_PERMISSION'))
<div>try try try</div>
@endif
</div> </div>
</div> </div>
</div> </div>
......
<title>Finance Dashboard</title>
@extends('ui::layouts.app')
@section('content')
<style>
body {
background-color: #f0f2f5;
}
.ui.container {
max-width: 100% !important;
overflow-x: hidden;
padding: 0 5rem 0 2rem;
box-sizing: border-box;
margin-top: 3rem;
}
h2.ui.header {
font-size: 1.8rem;
font-weight: 700;
color: #1b1c1d;
margin-bottom: 0.5rem;
}
h3.ui.dividing.header {
font-size: 1.3rem;
font-weight: 600;
color: #444;
margin-top: 0.5rem;
margin-bottom: 1.5rem;
border-bottom: 2px solid #ddd;
padding-bottom: 0.5rem;
}
.ui.statistics {
display: flex !important;
justify-content: space-between;
align-items: stretch;
flex-wrap: nowrap;
gap: 1.5rem;
margin-top: 2rem;
margin-bottom: 2rem;
}
.ui.statistic {
flex: 1;
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
text-align: center;
transition: transform 0.2s ease-in-out;
}
.ui.statistic:hover {
transform: translateY(-4px);
}
.ui.statistic .value {
font-size: 2rem;
font-weight: bold;
color: #2185d0;
}
.ui.statistic .label {
font-size: 1rem;
color: #555;
margin-top: 0.3rem;
}
/* Filter bar (aligned right side of header) */
.header-with-filter {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.2rem;
}
.filter-bar {
display: flex;
align-items: center;
gap: 1rem;
background: white;
border-radius: 8px;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.08);
padding: 0.5rem 1rem;
width: fit-content;
}
.filter-bar label {
font-weight: 600;
color: #333;
margin-right: 0.5rem;
}
.filter-bar input[type="month"] {
border: 1px solid #dcdcdc;
border-radius: 6px;
padding: 0.4rem 0.6rem;
font-size: 1rem;
color: #333;
transition: all 0.2s ease;
}
.filter-bar input[type="month"]:focus {
border-color: #21ba45;
outline: none;
box-shadow: 0 0 0 2px rgba(33, 186, 69, 0.15);
}
.ui.table {
margin-top: 1rem !important;
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.ui.table th {
background: #f9fafb !important;
color: #444;
font-weight: 600;
}
.status-label {
display: inline-block;
padding: 6px 12px;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 600;
color: #fff;
text-transform: capitalize;
border: none;
opacity: 0.9;
}
.status-Diproses { background-color: rgba(33, 133, 208, 0.3); color: #1b4f72; }
.status-Disokong { background-color: rgba(46, 204, 113, 0.3); color: #1d5d38; }
.status-Disahkan { background-color: rgba(241, 196, 15, 0.3); color: #7a6000; }
.status-Disemak { background-color: rgba(155, 89, 182, 0.3); color: #4a2c63; }
.status-Dibayar { background-color: rgba(26, 188, 156, 0.3); color: #0b5449; }
.status-Ditolak { background-color: rgba(231, 76, 60, 0.3); color: #7e2018; }
@media (max-width: 768px) {
.ui.container { padding: 0 2rem; }
.header-row, .header-with-filter { flex-direction: column; align-items: flex-start; gap: 1rem; }
.filter-bar { flex-direction: column; align-items: flex-start; width: 100%; }
.filter-bar button { width: 100%; }
.ui.statistics { flex-wrap: wrap; }
.ui.statistic { flex: 0 0 48%; }
}
@media (max-width: 600px) {
.ui.statistic { flex: 0 0 100%; }
}
</style>
<div class="ui container mt-5" style="margin-top: 10px">
{{-- Header --}}
<br>
<div class="header-row" style="display: flex; justify-content: space-between; align-items: center;">
<h2 class="ui header">Finance Dashboard</h2>
{{-- Excel Export Button --}}
<button class="ui teal button" id="openExportModal">
<i class="file excel icon"></i> Muat Turun Laporan Excel
</button>
</div>
{{-- Statistic Section --}}
<div class="ui statistics">
<div class="statistic"><div class="value">{{ $summary['processed'] }}</div><div class="label">Diproses</div></div>
<div class="statistic"><div class="value">{{ $summary['verified'] }}</div><div class="label">Disokong</div></div>
<div class="statistic"><div class="value">{{ $summary['approved'] }}</div><div class="label">Disahkan</div></div>
<div class="statistic"><div class="value">{{ $summary['reviewed'] }}</div><div class="label">Disemak</div></div>
<div class="statistic"><div class="value">{{ $summary['paid'] }}</div><div class="label">Dibayar</div></div>
<div class="statistic"><div class="value">{{ $summary['rejected'] }}</div><div class="label">Ditolak</div></div>
</div>
{{-- Header + Filter Bar in same row --}}
<div class="header-with-filter">
<h3 class="ui dividing header" style="margin-bottom: 0;">Senarai Tuntutan Mengikut Staf</h3>
<form method="GET" class="filter-bar">
<label for="month">Bulan:</label>
<input type="month" id="month" name="month" value="{{ $month ?? now()->format('Y-m') }}" required>
<button class="ui teal button">
<i class="filter icon"></i> Tapis
</button>
</form>
</div>
{{-- Staff Summary Table --}}
<table class="ui celled table">
<thead>
<tr>
<th>Nama Staf</th>
<th>Jumlah Tuntutan</th>
<th>Jumlah Keseluruhan (RM)</th>
<th>Tindakan</th>
</tr>
</thead>
<tbody>
@forelse($users as $user)
<tr>
<td>{{ $user['name'] }}</td>
<td>{{ $user['total_claims'] }}</td>
<td>RM {{ number_format($user['total_amount'], 2) }}</td>
<td>
<a href="{{ route('mileage::finance.show', ['userId' => $user['id'], 'month' => $month]) }}"
class="ui blue button tiny">Lihat</a>
</td>
</tr>
@empty
<tr>
<td colspan="4" class="center aligned">Tiada tuntutan untuk bulan ini.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{-- Filter Modal for Excel Export --}}
<div class="ui modal" id="exportModal">
<div class="header">Penapis Laporan Excel</div>
<div class="content">
<form class="ui form" action="{{ route('mileage::finance.export') }}" method="GET">
<div class="field">
<label>Nama Staf</label>
<select name="user_id" class="ui dropdown" required>
<option value="">-- Pilih Staf --</option>
@foreach($users as $user)
<option value="{{ $user['id'] }}">{{ $user['name'] }}</option>
@endforeach
</select>
</div>
<div class="field">
<label>Bulan Tuntutan</label>
<input type="month" name="month" value="{{ $month ?? now()->format('Y-m') }}" required>
</div>
<div class="actions" style="text-align: right; margin-top: 2rem;">
<button type="button" class="ui button" id="cancelExport">Batal</button>
<button type="submit" class="ui teal button">
<i class="download icon"></i> Muat Turun
</button>
</div>
</form>
</div>
</div>
{{-- JS --}}
<script>
document.getElementById('openExportModal').addEventListener('click', function() {
$('#exportModal').modal('show');
});
document.getElementById('cancelExport').addEventListener('click', function() {
$('#exportModal').modal('hide');
});
</script>
@endsection
<title>Butiran Tuntutan Staf</title>
@extends('ui::layouts.app')
@section('content')
<style>
body { background-color: #f0f2f5; }
.ui.container { padding: 0 5rem 3rem 2rem; margin-top: 3rem; }
h2.ui.header { font-size: 1.8rem; font-weight: 700; margin-bottom: .5rem; }
.ui.table { background: white; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
.ui.segment {
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-top: 2rem;
padding: 1.5rem;
}
.ui.button {
margin-top: 1rem;
}
/* --- Larger, more comfortable action buttons --- */
.actions-group .ui.button.tiny {
padding: 9px 14px !important; /* more padding for height & width */
font-size: 0.9rem !important; /* slightly larger font */
line-height: 1.2rem !important;
min-width: 100px; /* more space for labels */
text-align: center;
border-radius: 8px !important; /* smoother rounded edges */
font-weight: 600 !important;
transition: all 0.2s ease-in-out;
}
/* Hover effect for better feedback */
.actions-group .ui.button.tiny:hover {
transform: translateY(-2px);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
}
/* Keep consistent gap between buttons */
.actions-group {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
}
.inline-form {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 0;
}
.actions-group .ui.red.button.tiny,
.actions-group .ui.green.button.tiny,
.actions-group .ui.blue.button.tiny {
border-radius: 6px !important;
font-weight: 500 !important;
}
/* Responsive alignment */
@media (max-width: 768px) {
.actions-group {
flex-direction: column;
gap: 4px;
}
}
.status-label {
display: inline-block;
padding: 6px 12px;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 600;
text-transform: capitalize;
opacity: 0.9;
}
.status-Diproses { background-color: rgba(33,133,208,0.3); color:#1b4f72; }
.status-Disokong { background-color: rgba(46,204,113,0.3); color:#1d5d38; }
.status-Disahkan { background-color: rgba(241,196,15,0.3); color:#7a6000; }
.status-Disemak { background-color: rgba(155,89,182,0.3); color:#4a2c63; }
.status-Dibayar { background-color: rgba(26,188,156,0.3); color:#0b5449; }
.status-Ditolak { background-color: rgba(231,76,60,0.3); color:#7e2018; }
@media (max-width: 768px) {
.ui.container { padding: 0 2rem; }
}
.totals-row td { font-weight: 700; background: #f7f7f8; }
td form {
display: inline-block;
margin-left: 4px;
}
</style>
<div class="ui container">
{{-- Header --}}
<h2 class="ui header">{{ $user->name }}</h2>
<p><strong>Bulan:</strong> {{ \Carbon\Carbon::parse($month)->translatedFormat('F Y') }}</p>
{{-- Back button --}}
<a href="{{ route('mileage::finance.index', ['month' => $month]) }}" class="ui grey button">
<i class="arrow left icon"></i> Kembali
</a>
{{-- Claims Table --}}
<table class="ui celled table">
<thead>
<tr>
<th>#</th> {{-- new column for numbering --}}
<th>Tarikh</th>
<th>Butiran</th>
<th>Jenis Kenderaan</th>
<th>Dari</th>
<th>Ke</th>
<th>KM</th>
<th>Tol (RM)</th>
<th>Lain-lain (RM)</th>
<th>Jumlah (RM)</th>
<th>Status</th>
<th>Tindakan</th>
</tr>
</thead>
<tbody>
@php
use Carbon\Carbon;
// Sort claims by ascending date
$claims = $claims->sortBy(fn($c) => Carbon::parse($c->claim_date));
// Step 1: Non-rejected claims
$validClaims = $claims->where('status', '!=', 'Ditolak');
// Step 2: Restrict to claims within the displayed month
$monthStart = Carbon::parse($month)->startOfMonth();
$monthEnd = Carbon::parse($month)->endOfMonth();
$validInMonth = $validClaims->filter(function($c) use ($monthStart, $monthEnd) {
if (empty($c->claim_date)) return false;
try {
$d = Carbon::parse($c->claim_date);
} catch (\Exception $e) {
return false;
}
return $d->between($monthStart, $monthEnd);
});
// Step 3: Count unique calendar days (YYYY-MM-DD)
$uniqueDays = $validInMonth
->pluck('claim_date')
->map(fn($d) => Carbon::parse($d)->toDateString())
->unique()
->count();
// Step 4: Compute base totals
$total = 0;
$totalMileage = $validInMonth->sum('total_mileage');
$totalToll = $validInMonth->sum('toll_amount');
$totalOthers = $validInMonth->sum('others_amount');
// Step 5: Deduction
$deduction = 40 * $uniqueDays;
// === Calculate mileageAmount BEFORE the table ===
$rateCar1 = 0.55; $rateCar2 = 0.50; $rateMoto = 0.30;
$mileageAmount = 0;
$vehicleGroups = [];
$vehicleGroupCount = max(1, $validInMonth->groupBy(fn($c) => strtolower($c->jenisKenderaan->name ?? 'kereta'))->count());
foreach ($validInMonth->groupBy(fn($c) => strtolower($c->jenisKenderaan->name ?? 'kereta')) as $type => $group) {
$typeMileage = $group->sum('total_mileage');
$typeClaimable = max(0, $typeMileage - ($deduction / $vehicleGroupCount));
if (in_array($type, ['motosikal', 'motorcycle'])) {
$typeAmount = $typeClaimable * $rateMoto;
} else {
$firstPart = min($typeClaimable, 500);
$secondPart = max(0, $typeClaimable - 500);
$typeAmount = ($firstPart * $rateCar1) + ($secondPart * $rateCar2);
}
$mileageAmount += $typeAmount;
}
$subtotal = $mileageAmount + $totalToll + $totalOthers;
@endphp
@php
// Group all claims by project (null = Tiada Projek)
$groupedClaims = $claims->groupBy(function ($c) {
return $c->project->p_project_description ?? 'Tiada Projek';
});
// Move "Tiada Projek" to the bottom
$groupedClaims = $groupedClaims->sortBy(function ($claims, $projectName) {
return $projectName === 'Tiada Projek' ? 1 : 0;
});
@endphp
@forelse($groupedClaims as $projectName => $projectClaims)
{{-- Project header --}}
<tr style="background:#f8f9fa;">
<td colspan="11" style="font-weight:bold;">
<i class="file alternate outline icon"></i> {{ $projectName }}
</td>
</tr>
@foreach($projectClaims as $index => $claim)
@php
$rowTotal = 0;
$claimableKM = 0;
$vehicle = strtolower($claim->jenisKenderaan->name ?? 'kereta');
if ($claim->status !== 'Ditolak') {
$claimableKM = $claim->total_mileage ?? 0;
if (in_array($vehicle, ['motosikal', 'motorcycle'])) {
$rowTotal = $claimableKM * 0.30;
} else {
$firstPart = min($claimableKM, 500);
$secondPart = max(0, $claimableKM - 500);
$rowTotal = ($firstPart * 0.55) + ($secondPart * 0.50);
}
}
@endphp
<tr>
<td>{{ $loop->iteration }}</td>
<td>{{ \Carbon\Carbon::parse($claim->claim_date)->format('d/m/Y') }}</td>
<td>{{ $claim->description ?? '-' }}</td>
<td>{{ ucfirst($vehicle) }}</td>
<td>{{ $claim->distance_from ?? '-' }}</td>
<td>{{ $claim->distance_to ?? '-' }}</td>
<td>{{ number_format($claim->total_mileage, 1) }}</td>
<td>{{ number_format($claim->toll_amount ?? 0, 2) }}</td>
<td>{{ number_format($claim->others_amount ?? 0, 2) }}</td>
<td>
{{ $claim->status !== 'Ditolak'
? 'RM ' . number_format($rowTotal, 2)
: '- (Tuntutan Ditolak)' }}
</td>
<td><span class="status-label status-{{ $claim->status }}">{{ $claim->status }}</span></td>
<td>
<div class="actions-group">
{{-- Lihat (View) button - always visible --}}
<a href="{{ route('mileage::finance.viewEach', $claim->id) }}"
class="ui blue button tiny"
title="Lihat Butiran">
<i class="eye icon"></i> Lihat
</a>
{{-- Kemaskini button - show only for claims that can be edited --}}
@if(in_array($claim->status, ['Diproses', 'Disokong', 'Disahkan']))
<button class="ui yellow button tiny edit-btn"
data-id="{{ $claim->id }}"
data-date="{{ $claim->claim_date }}"
data-description="{{ $claim->description }}"
data-distance-from="{{ $claim->distance_from }}"
data-distance-to="{{ $claim->distance_to }}"
data-mileage="{{ $claim->total_mileage }}"
data-toll="{{ $claim->toll_amount }}"
data-others="{{ $claim->others_amount }}"
data-others-details="{{ $claim->others_details }}">
<i class="edit icon"></i> Kemaskini
</button>
@endif
{{-- Show Semak/Tolak buttons only if claim is not final --}}
@if(!in_array($claim->status, ['Disemak', 'Ditolak', 'Dibayar']))
<form action="{{ route('mileage::finance.updateStatus', $claim->id) }}" method="POST" class="inline-form">
@csrf
@method('PUT')
@if(in_array($claim->status, ['Disahkan', 'Disokong', 'Diproses']))
<button type="submit" name="action" value="Disemak" class="ui green button tiny">
<i class="check icon"></i> Semak
</button>
@endif
<button type="submit" name="action" value="Ditolak" class="ui red button tiny">
<i class="times icon"></i> Tolak
</button>
</form>
@endif
</div>
</td>
</tr>
@endforeach
@php
$projMileage = $projectClaims->sum('total_mileage');
$projToll = $projectClaims->sum('toll_amount');
$projOthers = $projectClaims->sum('others_amount');
@endphp
<tr class="totals-row">
<td colspan="6" style="text-align:right;"><strong>Jumlah Projek Ini</strong></td>
<td><strong>{{ number_format($projMileage, 1) }} KM</strong></td>
<td><strong>RM {{ number_format($projToll, 2) }}</strong></td>
<td><strong>RM {{ number_format($projOthers, 2) }}</strong></td>
<td colspan="2"></td>
</tr>
@empty
<tr>
<td colspan="11" class="center aligned">Tiada tuntutan untuk bulan ini.</td>
</tr>
@endforelse
@if($validInMonth->count() > 0)
<tr class="totals-row">
<td colspan="6" style="text-align:right;"><strong>Jumlah Keseluruhan</strong></td>
<td><strong>{{ number_format($totalMileage, 1) }} KM</strong></td>
<td><strong>RM {{ number_format($totalToll, 2) }}</strong></td>
<td><strong>RM {{ number_format($totalOthers, 2) }}</strong></td>
<td colspan="2"><strong>RM {{ number_format($mileageAmount, 2) }}</strong></td>
</tr>
@endif
</tbody>
</table>
{{-- Detailed Calculation Summary --}}
@if($claims->count() > 0)
@php
// Use already filtered valid claims & computed uniqueDays
$claimableDistance = max(0, $totalMileage - $deduction);
// === Mileage by vehicle type ===
$rateCar1 = 0.55; $rateCar2 = 0.50; $rateMoto = 0.30;
$mileageAmount = 0;
$vehicleGroups = [];
foreach ($validInMonth->groupBy(fn($c) => strtolower($c->jenisKenderaan->name ?? 'kereta')) as $type => $group) {
$typeMileage = $group->sum('total_mileage');
$typeClaimable = max(0, $typeMileage - ($deduction / max($validInMonth->groupBy('jenisKenderaan.name')->count(), 1)));
if (in_array($type, ['motosikal', 'motorcycle'])) {
$typeAmount = $typeClaimable * $rateMoto;
$vehicleGroups[] = [
'type' => ucfirst($type),
'details' => "{$typeClaimable} km × RM{$rateMoto}",
'amount' => $typeAmount
];
} else {
$firstPart = min($typeClaimable, 500);
$secondPart = max(0, $typeClaimable - 500);
$typeAmount = ($firstPart * $rateCar1) + ($secondPart * $rateCar2);
$vehicleGroups[] = [
'type' => ucfirst($type),
'details' => "{$firstPart} km × RM{$rateCar1}" .
($secondPart > 0 ? " + {$secondPart} km × RM{$rateCar2}" : ""),
'amount' => $typeAmount
];
}
$mileageAmount += $typeAmount;
}
// === Subtotal ===
$subtotal = $mileageAmount + $totalToll + $totalOthers;
// === Penalty ===
$claimMonthEnd = Carbon::parse($month)->endOfMonth();
$latestClaim = $validInMonth->sortByDesc('created_at')->first();
$submissionDate = $latestClaim ? Carbon::parse($latestClaim->created_at) : now();
$monthsDiff = $claimMonthEnd->diffInMonths($submissionDate, false);
if ($monthsDiff <= 1) {
$penaltyRate = 0; $penaltyLabel = "Tiada penalti (≤ 1 bulan)";
} elseif ($monthsDiff <= 3) {
$penaltyRate = 0.10; $penaltyLabel = "Penalti 10% (lewat ≤ 3 bulan)";
} elseif ($monthsDiff <= 6) {
$penaltyRate = 0.30; $penaltyLabel = "Penalti 30% (lewat ≤ 6 bulan)";
} else {
$penaltyRate = 'BOD'; $penaltyLabel = "Tertakluk kepada BOD (> 6 bulan)";
}
$penaltyAmount = $penaltyRate === 'BOD' ? 0 : $subtotal * $penaltyRate;
$finalTotal = $subtotal - $penaltyAmount;
@endphp
<div class="ui segment">
<h4><i class="calculator icon"></i> Ringkasan Pengiraan Bulanan</h4>
<table class="ui definition table">
<tr><td>Jumlah Perbatuan (Sebenar)</td><td>{{ number_format($totalMileage,1) }} KM</td></tr>
<tr><td>Hari Tuntutan</td><td>{{ $uniqueDays }} hari</td></tr>
<tr><td>Potongan 40 KM/Hari</td><td>{{ $uniqueDays }} × 40 = {{ $deduction }} KM</td></tr>
<tr>
<td>Jumlah Layak Tuntut</td>
<td>
{{ number_format($totalMileage,1) }} KM − {{ number_format($deduction,1) }} KM
= <strong>{{ number_format($claimableDistance,1) }} KM</strong>
</td>
</tr>
<tr>
<td>Kadar Kiraan</td>
<td>
@foreach($vehicleGroups as $v)
• {{ ucfirst($v['type']) }}: {!! $v['details'] !!} = <strong>RM {{ number_format($v['amount'],2) }}</strong><br>
@endforeach
</td>
</tr>
<tr><td>Jumlah Perbatuan (RM)</td><td>RM {{ number_format($mileageAmount,2) }}</td></tr>
<tr><td>Jumlah Tol</td><td>RM {{ number_format($totalToll,2) }}</td></tr>
<tr><td>Jumlah Lain-lain</td><td>RM {{ number_format($totalOthers,2) }}</td></tr>
<tr><td><strong>Subtotal (Sebelum Penalti)</strong></td><td><strong>RM {{ number_format($subtotal,2) }}</strong></td></tr>
<tr>
<td>Penalti</td>
<td>
@if($penaltyRate==='BOD')
<span style="color:#d35400;"><i class="exclamation triangle icon"></i> {{ $penaltyLabel }}</span>
@else
{{ $penaltyLabel }}<br>Tolak: RM {{ number_format($penaltyAmount,2) }}
@endif
</td>
</tr>
<tr class="totals-row">
<td><strong>Jumlah Akhir (Selepas Penalti)</strong></td>
<td><strong>RM {{ number_format($finalTotal,2) }}</strong></td>
</tr>
</table>
</div>
@endif
{{-- Edit Modal --}}
<div class="ui modal" id="editModal">
<i class="close icon"></i>
<div class="header">Kemaskini Tuntutan</div>
<div class="content">
<form id="editForm" method="POST" action="">
@csrf
@method('PUT')
<div class="ui form">
<div class="field required">
<label>Tarikh</label>
<input type="date" name="claim_date" id="editDate" required>
</div>
<div class="two fields">
<div class="field required">
<label>Dari</label>
<input type="text" name="distance_from" id="editFrom" required>
</div>
<div class="field required">
<label>Ke</label>
<input type="text" name="distance_to" id="editTo" required>
</div>
</div>
<div class="field required">
<label>Butiran</label>
<input type="text" name="description" id="editDescription" required>
</div>
<div class="three fields">
<div class="field required">
<label>Jumlah KM</label>
<input type="number" step="0.1" name="total_mileage" id="editMileage" required>
</div>
<div class="field">
<label>Tol (RM)</label>
<input type="number" step="0.01" name="toll_amount" id="editToll">
</div>
<div class="field">
<label>Lain-lain (RM)</label>
<input type="number" step="0.01" name="others_amount" id="editOthers">
</div>
</div>
<div class="field" id="othersDetailsField" style="display:none;">
<label>Butiran Lain-lain</label>
<input type="text" name="others_details" id="editOthersDetails"
placeholder="Nyatakan butiran lain-lain (contoh: bayaran penghantaran, dokumen, dsb)">
</div>
</div>
</form>
</div>
<div class="actions">
<div class="ui cancel button">Batal</div>
<button type="submit" form="editForm" class="ui primary button">Simpan</button>
</div>
</div>
</div>
@endsection
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script>
$(document).ready(function() {
// === Handle Edit button click ===
$('.edit-btn').on('click', function() {
const id = $(this).data('id');
const date = $(this).data('date');
const desc = $(this).data('description');
const from = $(this).data('distance-from');
const to = $(this).data('distance-to');
const mileage = $(this).data('mileage');
const toll = $(this).data('toll');
const others = $(this).data('others');
const othersDetails = $(this).data('others-details');
const updateUrl = "{{ route('mileage::finance.update', ':id') }}".replace(':id', id);
$('#editForm').attr('action', updateUrl);
$('#editDate').val(date);
$('#editDescription').val(desc);
$('#editFrom').val(from);
$('#editTo').val(to);
$('#editMileage').val(mileage);
$('#editToll').val(toll);
$('#editOthers').val(others);
$('#editOthersDetails').val(othersDetails || '');
// Trigger the others field logic
$('#editOthers').trigger('input');
$('#editModal').modal('show');
});
// === Show/Hide "Butiran Lain-lain" based on input ===
$('#editOthers').on('input', function() {
const value = parseFloat($(this).val()) || 0;
if (value > 0) {
$('#othersDetailsField').slideDown();
$('#editOthersDetails').attr('required', true);
} else {
$('#othersDetailsField').slideUp();
$('#editOthersDetails').removeAttr('required').val('');
}
});
// === Validate before form submission ===
$('#editForm').on('submit', function(e) {
const others = parseFloat($('#editOthers').val()) || 0;
const othersDetails = $('#editOthersDetails').val().trim();
if (others > 0 && othersDetails === '') {
e.preventDefault();
alert('Sila isi Butiran Lain-lain kerana jumlah Lain-lain (RM) lebih daripada 0.');
$('#editOthersDetails').focus();
return;
}
let valid = true;
$('#editForm [required]').each(function() {
if ($(this).val().trim() === '') {
valid = false;
alert('Sila isi semua ruangan yang diperlukan.');
$(this).focus();
e.preventDefault();
return false;
}
});
});
});
</script>
<title>Maklumat Tuntutan (Finance)</title>
@extends('ui::layouts.app')
@section('content')
<style>
body { background-color: #f0f2f5; }
.ui.container { max-width: 70% !important; margin-top: 2rem; }
.ui.segment {
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
border-radius: 10px;
background-color: #fff;
padding: 2rem;
}
table.ui.definition.table td:first-child {
width: 40%;
font-weight: 600;
background-color: #f9fafb;
}
.totals-row td {
font-weight: 700;
background: #f7f7f8;
}
.button-bar { margin-top: 2rem; display: flex; justify-content: space-between; }
</style>
<div class="ui container">
<h2 class="ui header">Maklumat Tuntutan (Finance)</h2>
<div class="ui segment">
<table class="ui definition table">
<tr><td>Tarikh Tuntutan</td><td>{{ \Carbon\Carbon::parse($claim->claim_date)->format('d/m/Y') }}</td></tr>
<tr><td>Jenis Kenderaan</td><td>{{ $claim->jenisKenderaan->keterangan ?? $claim->jenisKenderaan->name ?? '-' }}</td></tr>
<tr><td>Nama Projek</td><td>{{ $claim->project->p_project_description ?? 'Tanpa Projek' }}</td></tr>
<tr><td>Butiran</td><td>{{ $claim->description ?? '-' }}</td></tr>
<tr><td>Jarak Dari</td><td>{{ $claim->distance_from ?? '-' }}</td></tr>
<tr><td>Jarak Ke</td><td>{{ $claim->distance_to ?? '-' }}</td></tr>
<tr><td>Jumlah Perbatuan</td><td>{{ number_format($claim->total_mileage, 2) }} km</td></tr>
<tr><td>Toll</td><td>RM {{ number_format($claim->toll_amount ?? 0, 2) }}</td></tr>
<tr><td>Lain-lain</td><td>RM {{ number_format($claim->others_amount ?? 0, 2) }}</td></tr>
<tr><td>Butiran Lain-lain</td><td>{{ $claim->others_description ?? '-' }}</td></tr>
<tr><td>Status</td><td><strong>{{ $claim->status }}</strong></td></tr>
<tr class="totals-row"><td>Jumlah Akhir</td><td><strong>RM {{ number_format($claim->total_claim_amount, 2) }}</strong></td></tr>
</table>
</div>
<div class="button-bar">
<form action="{{ url()->previous() }}" method="GET" style="display:inline;">
<button type="submit" class="ui grey button">
<i class="arrow left icon"></i> Kembali
</button>
</form>
</div>
</div>
@endsection
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
@extends('ui::layouts.app') @extends('ui::layouts.app')
@section('content') @section('content')
<style type="text/css"> <style>
body { body {
background-color: #f0f2f5; background-color: #f0f2f5;
} }
...@@ -10,129 +10,148 @@ ...@@ -10,129 +10,148 @@
.ui.container { .ui.container {
max-width: 100% !important; max-width: 100% !important;
overflow-x: hidden; overflow-x: hidden;
padding: 0 5rem 0 2rem; padding: 0 5rem 3rem 2rem;
box-sizing: border-box; margin-top: 3rem;
} }
.ui.segments { h2.ui.header {
margin-top: 2rem; font-size: 1.8rem;
font-weight: 700;
color: #1b1c1d;
margin-bottom: 0.5rem;
} }
.ui.segment { h3.ui.dividing.header {
font-size: 1.3rem;
font-weight: 600;
color: #444;
border-bottom: 2px solid #ddd;
padding-bottom: 0.5rem;
}
.ui.table {
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
} }
@keyframes spin {
to { transform: rotate(360deg); } .ui.table th {
background: #f9fafb !important;
color: #444;
font-weight: 600;
} }
#loader {
position: fixed; /* Filter bar styling */
top: 0; left: 0; .filter-bar {
width: 100%; height: 100%; display: flex;
display: none; align-items: center;
align-items: center; justify-content: center; gap: 1rem;
background: rgba(0, 0, 0, 0.5); background: white;
z-index: 9999; border-radius: 8px;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.08);
padding: 0.8rem 1.5rem;
width: fit-content;
margin: 1.5rem 0;
} }
#loader div {
margin-top: 20%; .filter-bar label {
margin-left:45%; font-weight: 600;
width: 60px; height: 60px; color: #333;
border: 6px solid #fff; margin-right: 0.5rem;
border-top: 6px solid transparent; }
border-radius: 50%;
animation: spin 1s linear infinite; .filter-bar input[type="month"] {
border: 1px solid #dcdcdc;
border-radius: 6px;
padding: 0.4rem 0.6rem;
font-size: 1rem;
color: #333;
transition: all 0.2s ease;
}
.filter-bar input[type="month"]:focus {
border-color: #21ba45;
outline: none;
box-shadow: 0 0 0 2px rgba(33, 186, 69, 0.15);
} }
</style> </style>
<div class="ui container mt-5"> <div class="ui container mt-5">
{{-- Header --}}
<div class="header-row" style="display:flex;justify-content:space-between;align-items:center;">
<h2 class="ui header">Project Director Dashboard</h2> <h2 class="ui header">Project Director Dashboard</h2>
</div>
{{-- Pending Claims --}} {{-- Month Filter --}}
<h3 class="ui dividing header">Pending Claims</h3> <form method="GET" class="filter-bar">
<label for="month">Bulan:</label>
<input type="month" id="month" name="month" value="{{ $month }}" required>
<button class="ui teal button">
<i class="filter icon"></i> Tapis
</button>
</form>
{{-- Projects Section --}}
@forelse($projects as $project)
<div class="project-section">
<h3 class="ui dividing header">
{{ $project->p_project_description ?? $project->project_name ?? 'Tanpa Projek' }}
</h3>
@php
$claimsForProject = $claims->where('project_id', $project->id);
$staffGrouped = $claimsForProject->groupBy('user_id');
@endphp
@if($staffGrouped->count() > 0)
{{-- Claims Table --}}
<table class="ui celled table"> <table class="ui celled table">
<thead> <thead>
<tr> <tr>
<th>#</th> <th style="text-align:center; width:40px;">#</th>
<th>Staff</th> <th>Nama Staf</th>
<th>Date</th> <th>Jumlah Tuntutan</th>
<th>Project</th> <th>Jumlah Keseluruhan (RM)</th>
<th>Mileage</th>
<th>Total</th>
<th>Status</th> <th>Status</th>
<th>Action</th> <th>Tindakan</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach($staffGrouped as $userId => $staffClaims)
@php @php
$pendingClaims = $claims->where('status', 'Disokong'); $user = $staffClaims->first()->user;
$totalAmount = $staffClaims->sum('total_claim_amount');
$status = $staffClaims->unique('status')->pluck('status')->implode(', ');
@endphp @endphp
@forelse($pendingClaims as $claim)
<tr> <tr>
<td>{{ $loop->iteration }}</td> <td style="text-align:center;">{{ $loop->iteration }}</td>
<td>{{ $claim->user->name ?? '-' }}</td> <td>{{ $user->name ?? '-' }}</td>
<td>{{ $claim->claim_date }}</td> <td>{{ $staffClaims->count() }}</td>
<td>{{ $claim->project->p_project_description ?? '-' }}</td> <td>RM {{ number_format($totalAmount, 2) }}</td>
<td>{{ $claim->total_mileage }} km</td> <td>{{ $status }}</td>
<td>RM {{ number_format($claim->total_claim_amount, 2) }}</td>
<td>{{ $claim->status }}</td>
<td> <td>
<a href="{{ route('mileage::projectDirector.show', $claim->id) }}" class="ui blue button tiny"> <a href="{{ route('mileage::projectDirector.show', [
'projectId' => $project->id,
'userId' => $user->id,
'month' => $month
]) }}"
class="ui blue button tiny">
Lihat Lihat
</a> </a>
<form action="{{ route('mileage::projectDirector.approve', $claim->id) }}" method="POST" style="display:inline;">
@csrf
<button class="ui green button tiny" type="submit">Approve</button>
</form>
<form action="{{ route('mileage::projectDirector.reject', $claim->id) }}" method="POST" style="display:inline;">
@csrf
<button class="ui red button tiny" type="submit">Reject</button>
</form>
</td> </td>
</tr> </tr>
@empty @endforeach
<tr>
<td colspan="8" class="center aligned">No pending claims</td>
</tr>
@endforelse
</tbody> </tbody>
</table> </table>
<br>
{{-- Previous Claims --}} @else
<h3 class="ui dividing header mt-5">Previous Claims</h3> <div class="ui message">Tiada tuntutan untuk projek ini bulan ini.</div>
<table class="ui celled table"> @endif
<thead> </div>
<tr>
<th>#</th>
<th>Staff</th>
<th>Date</th>
<th>Project</th>
<th>Mileage</th>
<th>Total</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@php
$previousClaims = $claims->whereIn('status', ['Disahkan', 'Ditolak', 'Diluluskan']);
@endphp
@forelse($previousClaims as $claim)
<tr>
<td>{{ $loop->iteration }}</td>
<td>{{ $claim->user->name ?? '-' }}</td>
<td>{{ $claim->claim_date }}</td>
<td>{{ $claim->project->p_project_description ?? '-' }}</td>
<td>{{ $claim->total_mileage }} km</td>
<td>RM {{ number_format($claim->total_claim_amount, 2) }}</td>
<td>{{ $claim->status }}</td>
</tr>
@empty @empty
<tr> <div class="ui message">Tiada projek di bawah anda.</div>
<td colspan="7" class="center aligned">No previous claims found</td>
</tr>
@endforelse @endforelse
</tbody>
</table>
</div> </div>
@endsection @endsection
<title>Butiran Tuntutan Staf</title>
@extends('ui::layouts.app') @extends('ui::layouts.app')
@section('content') @section('content')
<style> <style>
body { body { background-color: #f0f2f5; }
background-color: #f0f2f5; .ui.container { padding: 0 5rem 3rem 2rem; margin-top: 3rem; }
h2.ui.header { font-size: 1.8rem; font-weight: 700; margin-bottom: .5rem; }
.ui.table { background: white; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
.ui.segment {
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-top: 2rem;
padding: 1.5rem;
} }
.ui.container { .ui.button { margin-top: 1rem; }
max-width: 70% !important;
.summary-box {
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
border-radius: 8px;
padding: 1rem 1.5rem;
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.1);
margin-top: 2rem; margin-top: 2rem;
} }
.ui.segment {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); .status-label {
display: inline-block;
padding: 6px 12px;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 600;
text-transform: capitalize;
opacity: 0.9;
}
.status-Diproses { background-color: rgba(33, 133, 208, 0.3); color: #1b4f72; }
.status-Disokong { background-color: rgba(46, 204, 113, 0.3); color: #1d5d38; }
.status-Disahkan { background-color: rgba(241, 196, 15, 0.3); color: #7a6000; }
.status-Disemak { background-color: rgba(155, 89, 182, 0.3); color: #4a2c63; }
.status-Diluluskan { background-color: rgba(26, 188, 156, 0.3); color: #0b5449; }
.status-Ditolak { background-color: rgba(231, 76, 60, 0.3); color: #7e2018; }
@media (max-width: 768px) {
.ui.container { padding: 0 2rem; }
.summary-box { flex-direction: column; gap: 0.5rem; }
}
.approval-actions {
display: flex;
justify-content: flex-end;
gap: 1rem;
margin-top: 1rem;
} }
</style> </style>
<div class="ui container"> <div class="ui container">
<h2 class="ui header">Maklumat Tuntutan</h2> {{-- Header --}}
<h2 class="ui header">{{ $user->name }}</h2>
<p>
<strong>Bulan:</strong> {{ \Carbon\Carbon::parse($month)->translatedFormat('F Y') }}<br>
<strong>Projek:</strong> {{ $project->p_project_description ?? $project->project_name ?? 'Tanpa Projek' }}
</p>
<div class="ui segment"> {{-- Back button --}}
<table class="ui definition table"> <a href="{{ route('mileage::projectDirector.index', ['month' => $month]) }}" class="ui grey button">
<tr> <i class="arrow left icon"></i> Kembali
<td>Tarikh Tuntutan</td> </a>
<td>{{ $claim->claim_date }}</td>
</tr> {{-- Claims Table --}}
<tr> <table class="ui celled table">
<td>Jenis Kenderaan</td> <thead>
<td>{{ $claim->jenisKenderaan->name ?? '-' }}</td> <tr>
</tr> <th>Tarikh</th>
<tr> <th>Butiran</th>
<td>Nama Projek</td> <th>Dari</th>
<td>{{ $claim->project->p_project_description ?? '-' }}</td> <th>Ke</th>
</tr> <th>Jumlah KM</th>
<tr> <th>Tol (RM)</th>
<td>Butiran Tuntutan</td> <th>Lain-lain (RM)</th>
<th>Status</th>
<th>Jumlah (RM)</th>
</tr>
</thead>
<tbody>
@php
$total = 0;
$totalMileage = 0;
$totalToll = 0;
$totalOthers = 0;
@endphp
@forelse($claims as $claim)
@php
$total += $claim->total_claim_amount;
$totalMileage += $claim->total_mileage;
$totalToll += $claim->toll_amount ?? 0;
$totalOthers += $claim->others_amount ?? 0;
@endphp
<tr>
<td>{{ \Carbon\Carbon::parse($claim->claim_date)->format('d/m/Y') }}</td>
<td>{{ $claim->description ?? '-' }}</td> <td>{{ $claim->description ?? '-' }}</td>
</tr>
<tr>
<td>Jarak Dari</td>
<td>{{ $claim->distance_from ?? '-' }}</td> <td>{{ $claim->distance_from ?? '-' }}</td>
</tr>
<tr>
<td>Jarak Ke</td>
<td>{{ $claim->distance_to ?? '-' }}</td> <td>{{ $claim->distance_to ?? '-' }}</td>
</tr> <td>{{ $claim->total_mileage }}</td>
<tr> <td>{{ number_format($claim->toll_amount ?? 0, 2) }}</td>
<td>Jumlah Perbatuan (KM)</td> <td>{{ number_format($claim->others_amount ?? 0, 2) }}</td>
<td>{{ $claim->total_mileage }} km</td>
</tr>
<tr>
<td>Jumlah Perbatuan Layak Tuntut</td>
<td>{{ $claim->claimable_mileage ?? 0 }} km</td>
</tr>
<tr>
<td>Jumlah Tuntutan Perjalanan (RM)</td>
<td>RM {{ number_format($claim->calculated_amount ?? 0, 2) }}</td>
</tr>
<tr>
<td>Toll (RM)</td>
<td>RM {{ number_format($claim->toll_amount ?? 0, 2) }}</td>
</tr>
<tr>
<td>Lain-lain (RM)</td>
<td>RM {{ number_format($claim->others_amount ?? 0, 2) }}</td>
</tr>
<tr>
<td>Butiran Lain-lain</td>
<td>{{ $claim->others_description ?? '-' }}</td>
</tr>
<tr>
<td>Penalti (RM) (Jika Ada)</td>
<td> <td>
@if(isset($claim->display_penalty_amount)) <span class="status-label status-{{ $claim->status }}">
@if($claim->display_penalty_amount > 0) {{ $claim->status }}
RM {{ number_format($claim->display_penalty_amount, 2) }} </span>
@else
@if($claim->display_penalty_reason === 'Tertakluk kepada BOD')
<span style="color: orange;">{{ $claim->display_penalty_reason }}</span>
@else
RM 0.00
@endif
@endif
@else
RM {{ number_format($claim->penalty_amount ?? 0, 2) }}
@endif
</td> </td>
<td>{{ number_format($claim->total_claim_amount, 2) }}</td>
</tr> </tr>
<tr>
<td>Sebab Penalti</td> @if(!empty($claim->others_description))
<td> <tr style="background: #f9f9f9;">
{{ $claim->display_penalty_reason ?? $claim->penalty_reason ?? '-' }} <td colspan="9">
<strong>Butiran Lain-lain:</strong> {{ $claim->others_description }}
</td> </td>
</tr> </tr>
@endif
@empty
<tr><td colspan="9" class="center aligned">Tiada tuntutan untuk bulan ini.</td></tr>
@endforelse
</tbody>
@if($claims->count() > 0)
<tfoot>
<tr> <tr>
<td><strong>Jumlah Keseluruhan Tuntutan (RM)</strong></td> <th colspan="8" class="right aligned">Jumlah Keseluruhan</th>
<td><strong>RM {{ number_format($claim->total_claim_amount ?? $claim->total_claim_amount ?? 0, 2) }}</strong></td> <th>RM {{ number_format($total, 2) }}</th>
</tr>
<tr>
<td><strong>Status</strong></td>
<td><strong>{{ $claim->status }}</strong></td>
</tr> </tr>
</tfoot>
@endif
</table> </table>
{{-- Status & Bulk Approval Section --}}
@if($claims->count() > 0)
@php
$uniqueStatuses = $claims->pluck('status')->unique();
$hasPending = $claims->where('status', 'Disokong')->count() > 0;
@endphp
<div class="summary-box">
<div>
<strong>Status Semasa: </strong>
<span class="status-label status-{{ $claims->first()->status }}">
{{ $uniqueStatuses->implode(', ') }}
</span>
</div> </div>
<a href="{{ route('mileage::projectDirector.index') }}" class="ui button"> {{-- Only show buttons if there are still claims waiting for Director verification --}}
Kembali @if($hasPending)
</a> <div class="approval-actions">
{{-- Approve All --}}
<form action="{{ route('mileage::projectDirector.approveMonth') }}" method="POST" style="display:inline;">
@csrf
<input type="hidden" name="project_id" value="{{ $project->id }}">
<input type="hidden" name="user_id" value="{{ $user->id }}">
<input type="hidden" name="month" value="{{ $month }}">
<button type="submit" class="ui green button">
<i class="check icon"></i> Sahkan Semua
</button>
</form>
{{-- Reject All --}}
<form action="{{ route('mileage::projectDirector.rejectMonth') }}" method="POST" style="display:inline;">
@csrf
<input type="hidden" name="project_id" value="{{ $project->id }}">
<input type="hidden" name="user_id" value="{{ $user->id }}">
<input type="hidden" name="month" value="{{ $month }}">
<button type="submit" class="ui red button">
<i class="times icon"></i> Tolak Semua
</button>
</form>
</div>
@endif
</div>
@endif
{{-- Calculation Summary Section --}}
@if($claims->count() > 0)
@php
$totalMileage = $claims->sum('total_mileage');
$totalClaimAmount = $claims->sum('total_claim_amount');
$totalToll = $claims->sum('toll_amount');
$totalOthers = $claims->sum('others_amount');
$latestClaim = $claims->sortByDesc('claim_date')->first();
$penaltyAmount = $latestClaim->penalty_amount ?? 0;
$penaltyReason = $latestClaim->penalty_reason ?? '-';
$vehicleType = strtolower($claims->first()->jenisKenderaan->name ?? 'kereta');
$uniqueDates = $claims->pluck('claim_date')->unique()->count();
$deduction = 40 * $uniqueDates;
$claimableDistance = max(0, $totalMileage - $deduction);
if ($vehicleType === 'motosikal' || $vehicleType === 'motorcycle') {
$rateNote = "Motosikal – RM0.30/km";
} elseif ($claimableDistance <= 500) {
$rateNote = "Kereta – RM0.55/km (≤500km)";
} else {
$secondPartKm = $claimableDistance - 500;
$rateNote = "Kereta – 500km × RM0.55 + " . number_format($secondPartKm, 1) . "km × RM0.50";
}
$finalTotal = $totalClaimAmount + $totalToll + $totalOthers - $penaltyAmount;
@endphp
<div class="ui segment">
<h4><i class="calculator icon"></i> Ringkasan Pengiraan (Mengikut Rekod Sistem)</h4>
<table class="ui definition table">
<tbody>
<tr><td><strong>Jumlah Jarak (Sebenar)</strong></td><td>{{ number_format($totalMileage, 1) }} KM</td></tr>
<tr><td><strong>Jumlah Hari Tuntutan</strong></td><td>{{ $uniqueDates }} hari</td></tr>
<tr><td><strong>Potongan 40KM/Hari</strong></td><td>40 × {{ $uniqueDates }} = {{ $deduction }} KM</td></tr>
<tr><td><strong>Jumlah Jarak Boleh Tuntut</strong></td><td>{{ number_format($claimableDistance, 1) }} KM</td></tr>
<tr><td><strong>Kadar Kiraan</strong></td><td>{{ $rateNote }}</td></tr>
<tr><td><strong>Jumlah Mileage (Dari Sistem)</strong></td><td>RM {{ number_format($totalClaimAmount, 2) }}</td></tr>
<tr><td><strong>Jumlah Tol</strong></td><td>RM {{ number_format($totalToll, 2) }}</td></tr>
<tr><td><strong>Jumlah Lain-lain</strong></td><td>RM {{ number_format($totalOthers, 2) }}</td></tr>
<tr><td><strong>Penalti</strong></td><td>RM {{ number_format($penaltyAmount, 2) }} ({{ $penaltyReason }})</td></tr>
<tr><td><strong><span style="color:#21ba45;">Jumlah Akhir</span></strong></td><td><strong>RM {{ number_format($finalTotal, 2) }}</strong></td></tr>
</tbody>
</table>
</div>
@endif
{{-- Calculation Rule Reminder --}}
<div class="ui segment">
<h4><i class="info circle icon"></i> Seksyen Pengiraan:</h4>
<p>
• Tuntutan bermula selepas 40KM pertama setiap hari.<br>
• Kadar (Kereta): RM0.55/km (&lt;500km), RM0.50/km (&gt;500km).<br>
• Kadar (Motosikal): RM0.30/km.<br>
• Penalti dikenakan sekiranya tuntutan lewat dihantar (&gt;1 bulan).
</p>
</div>
</div> </div>
@endsection @endsection
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
@extends('ui::layouts.app') @extends('ui::layouts.app')
@section('content') @section('content')
<style type="text/css"> <style>
body { body {
background-color: #f0f2f5; background-color: #f0f2f5;
} }
...@@ -10,125 +10,138 @@ ...@@ -10,125 +10,138 @@
.ui.container { .ui.container {
max-width: 100% !important; max-width: 100% !important;
overflow-x: hidden; overflow-x: hidden;
padding: 0 5rem 0 2rem; padding: 0 5rem 3rem 2rem;
box-sizing: border-box; margin-top: 3rem;
} }
.ui.segments { h2.ui.header {
margin-top: 2rem; font-size: 1.8rem;
font-weight: 700;
color: #1b1c1d;
margin-bottom: 0.5rem;
} }
.ui.segment { h3.ui.dividing.header {
font-size: 1.3rem;
font-weight: 600;
color: #444;
border-bottom: 2px solid #ddd;
padding-bottom: 0.5rem;
}
.ui.table {
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
} }
@keyframes spin {
to { transform: rotate(360deg); } .ui.table th {
background: #f9fafb !important;
color: #444;
font-weight: 600;
} }
#loader {
position: fixed; /* Filter bar styling */
top: 0; left: 0; .filter-bar {
width: 100%; height: 100%; display: flex;
display: none; align-items: center;
align-items: center; justify-content: center; gap: 1rem;
background: rgba(0, 0, 0, 0.5); background: white;
z-index: 9999; border-radius: 8px;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.08);
padding: 0.8rem 1.5rem;
width: fit-content;
margin: 1.5rem 0;
} }
#loader div {
margin-top: 20%; .filter-bar label {
margin-left:45%; font-weight: 600;
width: 60px; height: 60px; color: #333;
border: 6px solid #fff; margin-right: 0.5rem;
border-top: 6px solid transparent; }
border-radius: 50%;
animation: spin 1s linear infinite; .filter-bar input[type="month"] {
border: 1px solid #dcdcdc;
border-radius: 6px;
padding: 0.4rem 0.6rem;
font-size: 1rem;
color: #333;
transition: all 0.2s ease;
}
.filter-bar input[type="month"]:focus {
border-color: #21ba45;
outline: none;
box-shadow: 0 0 0 2px rgba(33, 186, 69, 0.15);
} }
</style> </style>
<div id="loader"><div></div></div> <div class="ui container mt-5">
<div class="ui container"> {{-- Header --}}
<div class="header-row" style="display:flex;justify-content:space-between;align-items:center;">
<h2 class="ui header">Project Manager Dashboard</h2> <h2 class="ui header">Project Manager Dashboard</h2>
</div>
{{-- Pending Claims --}} {{-- Month Filter --}}
<h3 class="ui dividing header">Pending Claims</h3> <form method="GET" class="filter-bar">
<label for="month">Bulan:</label>
<input type="month" id="month" name="month" value="{{ $month }}" required>
<button class="ui teal button">
<i class="filter icon"></i> Tapis
</button>
</form>
{{-- Projects Section --}}
@forelse($projects as $project)
<div class="project-section">
<h3 class="ui dividing header">{{ $project->p_project_description ?? $project->project_name ?? 'Tanpa Projek' }}</h3>
@php
// Match claims by the project ID
$claimsForProject = $claims->where('project_id', $project->id);
$staffGrouped = $claimsForProject->groupBy('user_id');
@endphp
@if($staffGrouped->count() > 0)
<table class="ui celled table"> <table class="ui celled table">
<thead> <thead>
<tr> <tr>
<th>#</th> <th style="text-align:center; width:40px;">#</th>
<th>Staff</th> <th>Nama Staf</th>
<th>Date</th> <th>Jumlah Tuntutan</th>
<th>Project</th> <th>Jumlah Keseluruhan (RM)</th>
<th>Distance</th> <th>Tindakan</th>
<th>Total</th>
<th>Status</th>
<th>Action</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@forelse($claims->where('status', 'Diproses') as $claim) @foreach($staffGrouped as $userId => $staffClaims)
@php
$user = $staffClaims->first()->user;
$totalAmount = $staffClaims->sum('total_claim_amount');
@endphp
<tr> <tr>
<td>{{ $loop->iteration }}</td> <td style="text-align:center;">{{ $loop->iteration }}</td>
<td>{{ $claim->user->name ?? '-' }}</td> <td>{{ $user->name ?? '-' }}</td>
<td>{{ $claim->claim_date }}</td> <td>{{ $staffClaims->count() }}</td>
<td>{{ $claim->project->p_project_description ?? '-' }}</td> <td>RM {{ number_format($totalAmount, 2) }}</td>
<td>{{ $claim->total_mileage }} km</td>
<td>RM {{ number_format($claim->total_claim_amount, 2) }}</td>
<td>{{ $claim->status }}</td>
<td> <td>
<a href="{{ route('mileage::projectManager.show', $claim->id) }}" class="ui blue button tiny"> <a href="{{ route('mileage::projectManager.show', ['projectId' => $project->id, 'userId' => $user->id, 'month' => $month]) }}"
class="ui blue button tiny">
Lihat Lihat
</a> </a>
<form action="{{ route('mileage::applications.updateStatus', $claim->id) }}" method="POST" style="display:inline;">
@csrf
@method('PUT')
<input type="hidden" name="action" value="verify">
<button class="ui green button tiny" type="submit">Verify</button>
</form>
<form action="{{ route('mileage::applications.updateStatus', $claim->id) }}" method="POST" style="display:inline;">
@csrf
@method('PUT')
<input type="hidden" name="action" value="reject">
<button class="ui red button tiny" type="submit">Reject</button>
</form>
</td> </td>
</tr> </tr>
@empty @endforeach
<tr>
<td colspan="7">No pending claims.</td>
</tr>
@endforelse
</tbody> </tbody>
</table> </table>
<br>
{{-- Previous Claims --}} @else
<h3 class="ui dividing header">Previous Claims</h3> <div class="ui message">Tiada tuntutan untuk projek ini bulan ini.</div>
<table class="ui celled table"> @endif
<thead> </div>
<tr>
<th>#</th>
<th>Staff</th>
<th>Project</th>
<th>Distance</th>
<th>Total</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@forelse($claims->where('status', '!=', 'Diproses') as $claim)
<tr>
<td>{{ $loop->iteration }}</td>
<td>{{ $claim->user->name ?? '-' }}</td>
<td>{{ $claim->project->p_project_description ?? '-' }}</td>
<td>{{ $claim->total_mileage }} km</td>
<td>RM {{ number_format($claim->total_claim_amount, 2) }}</td>
<td>{{ $claim->status }}</td>
</tr>
@empty @empty
<tr> <div class="ui message">Tiada projek di bawah anda.</div>
<td colspan="6">No previous claims.</td>
</tr>
@endforelse @endforelse
</tbody>
</table>
</div> </div>
@endsection @endsection
<title>Butiran Tuntutan Staf</title>
@extends('ui::layouts.app') @extends('ui::layouts.app')
@section('content') @section('content')
...@@ -6,104 +7,306 @@ ...@@ -6,104 +7,306 @@
background-color: #f0f2f5; background-color: #f0f2f5;
} }
.ui.container { .ui.container {
max-width: 70% !important; padding: 0 5rem 3rem 2rem;
margin-top: 2rem; margin-top: 3rem;
}
h2.ui.header {
font-size: 1.8rem;
font-weight: 700;
margin-bottom: .5rem;
}
.ui.table {
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
} }
.ui.segment { .ui.segment {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-top: 2rem;
padding: 1.5rem;
}
.ui.button { margin-top: 1rem; }
.summary-box {
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
border-radius: 8px;
padding: 1rem 1.5rem;
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.1);
margin-top: 2rem;
}
.status-label {
display: inline-block;
padding: 6px 12px;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 600;
text-transform: capitalize;
opacity: 0.9;
}
.status-Diproses {
background-color: rgba(33, 133, 208, 0.3);
color: #1b4f72;
}
.status-Disokong {
background-color: rgba(46, 204, 113, 0.3);
color: #1d5d38;
}
.status-Disahkan {
background-color: rgba(241, 196, 15, 0.3);
color: #7a6000;
}
.status-Disemak {
background-color: rgba(155, 89, 182, 0.3);
color: #4a2c63;
}
.status-Diluluskan {
background-color: rgba(26, 188, 156, 0.3);
color: #0b5449;
}
.status-Ditolak {
background-color: rgba(231, 76, 60, 0.3);
color: #7e2018;
}
@media (max-width: 768px) {
.ui.container { padding: 0 2rem; }
.summary-box { flex-direction: column; gap: 0.5rem; }
}
.approval-actions {
display: flex;
justify-content: flex-end;
gap: 1rem;
margin-top: 1rem;
} }
</style> </style>
<div class="ui container"> <div class="ui container">
<h2 class="ui header">Maklumat Tuntutan</h2> {{-- Header --}}
<h2 class="ui header">{{ $user->name }}</h2>
<p>
<strong>Bulan:</strong> {{ \Carbon\Carbon::parse($month)->translatedFormat('F Y') }}<br>
<strong>Projek:</strong> {{ $project->p_project_description ?? $project->project_name ?? 'Tanpa Projek' }}
</p>
<div class="ui segment"> {{-- Back button --}}
<table class="ui definition table"> <a href="{{ route('mileage::projectManager.index', ['month' => $month]) }}" class="ui grey button">
<tr> <i class="arrow left icon"></i> Kembali
<td>Tarikh Tuntutan</td> </a>
<td>{{ $claim->claim_date }}</td>
</tr> {{-- Claims Table --}}
<tr> <table class="ui celled table">
<td>Jenis Kenderaan</td> <thead>
<td>{{ $claim->jenisKenderaan->name ?? '-' }}</td>
</tr>
<tr> <tr>
<td>Nama Projek</td> <th>Tarikh</th>
<td>{{ $claim->project->p_project_description ?? '-' }}</td> <th>Butiran</th>
<th>Dari</th>
<th>Ke</th>
<th>Jumlah KM</th>
<th>Tol (RM)</th>
<th>Lain-lain (RM)</th>
<th>Status</th>
<th>Jumlah (RM)</th>
</tr> </tr>
</thead>
<tbody>
@php
$total = 0;
$totalMileage = 0;
$totalToll = 0;
$totalOthers = 0;
@endphp
@forelse($claims as $claim)
@php
$total += $claim->total_claim_amount;
$totalMileage += $claim->total_mileage;
$totalToll += $claim->toll_amount ?? 0;
$totalOthers += $claim->others_amount ?? 0;
@endphp
<tr> <tr>
<td>Butiran Tuntutan</td> <td>{{ \Carbon\Carbon::parse($claim->claim_date)->format('d/m/Y') }}</td>
<td>{{ $claim->description ?? '-' }}</td> <td>{{ $claim->description ?? '-' }}</td>
</tr>
<tr>
<td>Jarak Dari</td>
<td>{{ $claim->distance_from ?? '-' }}</td> <td>{{ $claim->distance_from ?? '-' }}</td>
<td>{{ $claim->distance_to ?? '-' }}</td>
<td>{{ $claim->total_mileage }}</td>
<td>{{ number_format($claim->toll_amount ?? 0, 2) }}</td>
<td>{{ number_format($claim->others_amount ?? 0, 2) }}</td>
<td>
<span class="status-label status-{{ $claim->status }}">
{{ $claim->status }}
</span>
</td>
<td>{{ number_format($claim->total_claim_amount, 2) }}</td>
</tr>
@if(!empty($claim->others_description))
<tr style="background: #f9f9f9;">
<td colspan="9">
<strong>Butiran Lain-lain:</strong> {{ $claim->others_description }}
</td>
</tr> </tr>
@endif
@empty
<tr><td colspan="9" class="center aligned">Tiada tuntutan untuk bulan ini.</td></tr>
@endforelse
</tbody>
@if($claims->count() > 0)
<tfoot>
<tr> <tr>
<td>Jarak Ke</td> <th colspan="8" class="right aligned">Jumlah Keseluruhan</th>
<td>{{ $claim->distance_to ?? '-' }}</td> <th>RM {{ number_format($total, 2) }}</th>
</tr> </tr>
</tfoot>
@endif
</table>
{{-- Status & Bulk Approval Section --}}
@if($claims->count() > 0)
@php
$uniqueStatuses = $claims->pluck('status')->unique();
$hasPending = $claims->where('status', 'Diproses')->count() > 0;
@endphp
<div class="summary-box">
<div>
<strong>Status Semasa: </strong>
<span class="status-label status-{{ $claims->first()->status }}">
{{ $uniqueStatuses->implode(', ') }}
</span>
</div>
{{-- Only show buttons if there are still pending claims --}}
@if($hasPending)
<div class="approval-actions">
{{-- Approve All --}}
<form action="{{ route('mileage::projectManager.approveMonth') }}" method="POST" style="display:inline;">
@csrf
<input type="hidden" name="project_id" value="{{ $project->id }}">
<input type="hidden" name="user_id" value="{{ $user->id }}">
<input type="hidden" name="month" value="{{ $month }}">
<button type="submit" class="ui green button">
<i class="check icon"></i> Sokong Semua
</button>
</form>
{{-- Reject All --}}
<form action="{{ route('mileage::projectManager.rejectMonth') }}" method="POST" style="display:inline;">
@csrf
<input type="hidden" name="project_id" value="{{ $project->id }}">
<input type="hidden" name="user_id" value="{{ $user->id }}">
<input type="hidden" name="month" value="{{ $month }}">
<button type="submit" class="ui red button">
<i class="times icon"></i> Tolak Semua
</button>
</form>
</div>
@endif
</div>
@endif
{{-- Calculation Summary Section --}}
@if($claims->count() > 0)
@php
// Pull data already saved by backend (trusted totals)
$totalMileage = $claims->sum('total_mileage');
$totalClaimAmount = $claims->sum('total_claim_amount');
$totalToll = $claims->sum('toll_amount');
$totalOthers = $claims->sum('others_amount');
$latestClaim = $claims->sortByDesc('claim_date')->first();
$penaltyAmount = $latestClaim->penalty_amount ?? 0;
$penaltyReason = $latestClaim->penalty_reason ?? '-';
$vehicleType = strtolower($claims->first()->jenisKenderaan->name ?? 'kereta');
// derive claimable distance from DB total if possible
$uniqueDates = $claims->pluck('claim_date')->unique()->count();
$deduction = 40 * $uniqueDates;
$claimableDistance = max(0, $totalMileage - $deduction);
// determine rate breakdown (for display only)
$rateNote = '';
$firstPartKm = 0;
$secondPartKm = 0;
if ($vehicleType === 'motosikal' || $vehicleType === 'motorcycle') {
$rateNote = "Motosikal – RM0.30/km";
} else {
if ($claimableDistance <= 500) {
$rateNote = "Kereta – RM0.55/km (≤500km)";
} else {
$firstPartKm = 500;
$secondPartKm = $claimableDistance - 500;
$rateNote = "Kereta – 500km × RM0.55 + " . number_format($secondPartKm, 1) . "km × RM0.50";
}
}
// now match the total (no recomputation)
$finalTotal = $totalClaimAmount + $totalToll + $totalOthers - $penaltyAmount;
@endphp
<div class="ui segment">
<h4><i class="calculator icon"></i> Ringkasan Pengiraan (Mengikut Rekod Sistem)</h4>
<table class="ui definition table">
<tbody>
<tr> <tr>
<td>Jumlah Perbatuan (KM)</td> <td><strong>Jumlah Jarak (Sebenar)</strong></td>
<td>{{ $claim->total_mileage }} km</td> <td>{{ number_format($totalMileage, 1) }} KM</td>
</tr> </tr>
<tr> <tr>
<td>Jumlah Perbatuan Layak Tuntut</td> <td><strong>Jumlah Hari Tuntutan</strong></td>
<td>{{ $claim->claimable_mileage ?? 0 }} km</td> <td>{{ $uniqueDates }} hari</td>
</tr> </tr>
<tr> <tr>
<td>Jumlah Tuntutan Perjalanan (RM)</td> <td><strong>Potongan 40KM/Hari</strong></td>
<td>RM {{ number_format($claim->calculated_amount ?? 0, 2) }}</td> <td>40 × {{ $uniqueDates }} = {{ $deduction }} KM</td>
</tr> </tr>
<tr> <tr>
<td>Toll (RM)</td> <td><strong>Jumlah Jarak Boleh Tuntut</strong></td>
<td>RM {{ number_format($claim->toll_amount ?? 0, 2) }}</td> <td>{{ number_format($claimableDistance, 1) }} KM</td>
</tr> </tr>
<tr> <tr>
<td>Lain-lain (RM)</td> <td><strong>Kadar Kiraan</strong></td>
<td>RM {{ number_format($claim->others_amount ?? 0, 2) }}</td> <td>{{ $rateNote }}</td>
</tr> </tr>
<tr> <tr>
<td>Butiran Lain-lain</td> <td><strong>Jumlah Mileage (Dari Sistem)</strong></td>
<td>{{ $claim->others_description ?? '-' }}</td> <td>RM {{ number_format($totalClaimAmount, 2) }}</td>
</tr> </tr>
<tr> <tr>
<td>Penalti (RM) (Jika Ada)</td> <td><strong>Jumlah Tol</strong></td>
<td> <td>RM {{ number_format($totalToll, 2) }}</td>
@if(isset($claim->display_penalty_amount))
@if($claim->display_penalty_amount > 0)
RM {{ number_format($claim->display_penalty_amount, 2) }}
@else
@if($claim->display_penalty_reason === 'Tertakluk kepada BOD')
<span style="color: orange;">{{ $claim->display_penalty_reason }}</span>
@else
RM 0.00
@endif
@endif
@else
RM {{ number_format($claim->penalty_amount ?? 0, 2) }}
@endif
</td>
</tr> </tr>
<tr> <tr>
<td>Sebab Penalti</td> <td><strong>Jumlah Lain-lain</strong></td>
<td> <td>RM {{ number_format($totalOthers, 2) }}</td>
{{ $claim->display_penalty_reason ?? $claim->penalty_reason ?? '-' }}
</td>
</tr> </tr>
<tr> <tr>
<td><strong>Jumlah Keseluruhan Tuntutan (RM)</strong></td> <td><strong>Penalti</strong></td>
<td><strong>RM {{ number_format($claim->total_claim_amount ?? $claim->total_claim_amount ?? 0, 2) }}</strong></td> <td>RM {{ number_format($penaltyAmount, 2) }} ({{ $penaltyReason }})</td>
</tr> </tr>
<tr> <tr>
<td><strong>Status</strong></td> <td><strong><span style="color:#21ba45;">Jumlah Akhir </span></strong></td>
<td><strong>{{ $claim->status }}</strong></td> <td><strong>RM {{ number_format($finalTotal, 2) }}</strong></td>
</tr> </tr>
</tbody>
</table> </table>
</div> </div>
@endif
<a href="{{ route('mileage::projectManager.index') }}" class="ui button"> {{-- Calculation Rule Reminder --}}
Kembali <div class="ui segment">
</a> <h4><i class="info circle icon"></i> Seksyen Pengiraan:</h4>
<p>
• Tuntutan bermula selepas 40KM pertama setiap hari.<br>
• Kadar (Kereta): RM0.55/km (&lt;500km), RM0.50/km (&gt;500km).<br>
• Kadar (Motosikal): RM0.30/km.<br>
• Penalti dikenakan sekiranya tuntutan lewat dihantar (&gt;1 bulan).
</p>
</div>
</div> </div>
@endsection @endsection
...@@ -5,6 +5,10 @@ ...@@ -5,6 +5,10 @@
use Portal\Mileage\Http\Controllers\ApplicationsController; use Portal\Mileage\Http\Controllers\ApplicationsController;
use Portal\Mileage\Http\Controllers\ProjectManagerController; use Portal\Mileage\Http\Controllers\ProjectManagerController;
use Portal\Mileage\Http\Controllers\ProjectDirectorController; use Portal\Mileage\Http\Controllers\ProjectDirectorController;
use Portal\Mileage\Http\Controllers\HODController;
use Portal\Mileage\Http\Controllers\BODController;
use Portal\Mileage\Http\Controllers\HRController;
use Portal\Mileage\Http\Controllers\FinanceController;
Route::group( Route::group(
[ [
...@@ -14,25 +18,75 @@ ...@@ -14,25 +18,75 @@
], ],
function () { function () {
// Applyform // Applyform
Route::resource('applyform', ApplyformController::class); Route::prefix('applyform')->name('applyform')->group(function () {
Route::get('/', [ApplyformController::class, 'index'])->name('.index');
Route::get('/noproject', [ApplyformController::class, 'noproject'])->name('.noproject');
});
// Route::resource('applyform', ApplyformController::class);
// Applications // Applications
Route::resource('applications', ApplicationsController::class); Route::resource('applications', ApplicationsController::class);
Route::put('applications/{id}/updateStatus', [ApplicationsController::class, 'updateStatus']) Route::put('applications/{id}/updateStatus', [ApplicationsController::class, 'updateStatus'])
->name('applications.updateStatus'); ->name('applications.updateStatus');
Route::get('applications/view/claim/{id}', [ApplicationsController::class, 'viewEach'])->name('applications.viewEach');
Route::put('applications/{id}', [ApplicationsController::class, 'update'])
->name('applications.update');
Route::get('/applications/dates', [\Portal\Mileage\Http\Controllers\ApplicationsController::class, 'getClaimDates'])
->name('mileage::applications.getDates');
// Project Manager // Project Manager
Route::prefix('projectManager')->name('projectManager.')->group(function () { Route::prefix('projectManager')->name('projectManager')->group(function () {
Route::get('/', [ProjectManagerController::class, 'index'])->name('index'); Route::get('/', [ProjectManagerController::class, 'index'])->name('.index');
Route::get('/show/{id}', [ProjectManagerController::class, 'show'])->name('show'); Route::get('/show/{projectId}/{userId}', [ProjectManagerController::class, 'show'])->name('.show');
Route::post('/approve-month', [ProjectManagerController::class, 'approveMonth'])->name('.approveMonth');
Route::post('/reject-month', [ProjectManagerController::class, 'rejectMonth'])->name('.rejectMonth');
}); });
// Project Director // Project Director
Route::prefix('projectDirector')->name('projectDirector.')->group(function () { Route::prefix('projectDirector')->name('projectDirector')->group(function () {
Route::get('/dashboard', [ProjectDirectorController::class, 'index'])->name('index'); Route::get('/dashboard', [ProjectDirectorController::class, 'index'])->name('.index');
Route::get('/show/{id}', [ProjectDirectorController::class, 'show'])->name('show'); Route::get('/show/{projectId}/{userId}', [ProjectDirectorController::class, 'show'])->name('.show');
Route::post('/{id}/approve', [ProjectDirectorController::class, 'approve'])->name('approve'); Route::post('/approve-month', [ProjectDirectorController::class, 'approveMonth'])->name('.approveMonth');
Route::post('/{id}/reject', [ProjectDirectorController::class, 'reject'])->name('reject'); Route::post('/reject-month', [ProjectDirectorController::class, 'rejectMonth'])->name('.rejectMonth');
});
// Finance Department
Route::prefix('finance')->name('finance')->group(function () {
Route::get('/dashboard', [FinanceController::class, 'index'])->name('.index');
Route::get('/show/{userId}', [FinanceController::class, 'show'])->name('.show');
Route::put('/finance/update/{id}', [FinanceController::class, 'update'])->name('.update');
Route::get('/view/claim/{id}', [FinanceController::class, 'viewEach'])->name('.viewEach');
Route::put('/finance/claims/{id}/update-status', [FinanceController::class, 'updateStatus'])->name('.updateStatus');
Route::post('/{id}/review', [FinanceController::class, 'review'])->name('.review');
Route::post('/{id}/reject', [FinanceController::class, 'reject'])->name('.reject');
Route::get('/export', [FinanceController::class, 'export'])->name('.export');
});
// HOD
Route::prefix('HOD')->name('HOD')->group(function () {
Route::get('/dashboard', [HODController::class, 'index'])->name('.index');
Route::get('/show/{id}', [HODController::class, 'show'])->name('.show');
Route::post('/{id}/verify', [HODController::class, 'verify'])->name('.verify');
Route::post('/{id}/reject', [HODController::class, 'reject'])->name('.reject');
});
// BOD
Route::prefix('BOD')->name('BOD')->group(function () {
Route::get('/dashboard', [BODController::class, 'index'])->name('.index');
Route::get('/show/{id}', [BODController::class, 'show'])->name('.show');
Route::post('/{id}/review', [BODController::class, 'approve'])->name('.approve');
Route::post('/{id}/reject', [BODController::class, 'reject'])->name('.reject');
});
// HR
Route::prefix('HR')->name('HR')->group(function () {
Route::get('/dashboard', [HRController::class, 'index'])->name('.index');
Route::get('/show/{userId}', [HRController::class, 'show'])->name('.show');
Route::get('/view/claim/{id}', [HRController::class, 'viewEach'])->name('.viewEach');
Route::post('/{id}/reject', [HRController::class, 'reject'])->name('.reject');
Route::get('/export', [HRController::class, 'export'])->name('.export');
}); });
} }
); );
......
<?php
namespace Portal\Mileage\Exports;
use Portal\Mileage\Model\Claim;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
use Maatwebsite\Excel\Concerns\WithStyles;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class ClaimsExport implements FromCollection, WithHeadings, WithMapping, ShouldAutoSize, WithStyles
{
private const DATA_START_ROW = 7;
protected $user;
protected $claims;
public function __construct($user = null, $claims = null)
{
$this->user = $user;
$this->claims = $claims;
}
public function collection(): Collection
{
// Only Disemak claims should be exported
return $this->claims->where('status', 'Disemak') ?? collect();
}
public function headings(): array
{
$roleName = $this->user?->roles?->first()?->name ?? '-';
return [
['BORANG TUNTUTAN PERJALANAN'],
[''],
[
'Nama Staf:', $this->user->name ?? '-',
'Peranan:', $roleName,
],
[
'Syarikat:', '3F RESOURCES SDN BHD',
'Tarikh:', Carbon::now()->format('d/m/Y'),
],
[''],
[
'Tarikh',
'Butiran Tuntutan',
'Dari',
'Ke',
'Jumlah KM (Hari)',
'Tol (RM)',
'Lain-lain (RM)',
],
];
}
public function map($claim): array
{
return [
Carbon::parse($claim->claim_date)->format('d/m/Y'),
$claim->description ?? '-',
$claim->distance_from ?? '-',
$claim->distance_to ?? '-',
number_format($claim->total_mileage ?? 0, 2),
number_format($claim->toll_amount ?? 0, 2),
number_format($claim->others_amount ?? 0, 2),
];
}
public function styles(Worksheet $sheet): array
{
$lastDataRow = $sheet->getHighestRow();
$totalRow = $lastDataRow + 2;
$signRow = $totalRow + 3;
// Header Formatting
$sheet->mergeCells('A1:G1');
$sheet->getStyle('A1')->applyFromArray([
'font' => ['bold' => true, 'size' => 14],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER],
]);
// Table Header Formatting
$sheet->getStyle('A6:G6')->applyFromArray([
'font' => ['bold' => true],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER],
'fill' => [
'fillType' => Fill::FILL_SOLID,
'startColor' => ['argb' => 'FFEFEFEF'],
],
]);
// Align numeric columns to the right
$dataRange = 'E' . self::DATA_START_ROW . ':G' . $lastDataRow;
$sheet->getStyle($dataRange)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT);
// Total Row Formatting
$sheet->setCellValue("A{$totalRow}", 'Jumlah Keseluruhan:');
$sheet->mergeCells("A{$totalRow}:D{$totalRow}");
$sheet->setCellValue("E{$totalRow}", "=SUM(E" . self::DATA_START_ROW . ":E{$lastDataRow})");
$sheet->setCellValue("F{$totalRow}", "=SUM(F" . self::DATA_START_ROW . ":F{$lastDataRow})");
$sheet->setCellValue("G{$totalRow}", "=SUM(G" . self::DATA_START_ROW . ":G{$lastDataRow})");
$sheet->getStyle("A{$totalRow}:G{$totalRow}")->getFont()->setBold(true);
// Notes Section Formatting
$sheet->setCellValue("A{$signRow}", 'Seksyen Pengiraan:');
$sheet->setCellValue("A" . ($signRow + 1), '• Tuntutan bermula selepas 40KM pertama setiap hari.');
$sheet->setCellValue("A" . ($signRow + 2), '• Kadar (Kereta): RM0.55/km (<500km), RM0.50/km (>500km).');
$sheet->setCellValue("A" . ($signRow + 3), '• Kadar (Motosikal): RM0.30/km.');
$sheet->getStyle("A{$signRow}")->getFont()->setBold(true);
// Signature Section Formatting
$signStart = $signRow + 6;
// Disahkan oleh (Project Manager / HOD) with Project name or description
$approvedList = $this->claims
->whereNotNull('approved_by')
->map(function ($claim) {
$user = $claim->approvedBy?->name ?? '-';
$role = $claim->approvedBy?->roles?->first()?->name ?? 'Penyemak';
// If project exists, use project name, else fallback to description
$project = $claim->project ? $claim->project->p_project_description : "(Butiran Projek: " . ($claim->description ?? '-') . ")";
return "{$user} - {$role} ({$project})"; // Project name in parentheses
})
->unique()
->implode("\n\n");
// Diluluskan oleh (Project Director / BOD)
$reviewedList = $this->claims
->whereNotNull('reviewed_by')
->map(function ($claim) {
$user = $claim->reviewedBy?->name ?? '-';
$role = $claim->reviewedBy?->roles?->first()?->name ?? 'Pelulus';
$project = $claim->project ? $claim->project->name : "(Butiran Projek: " . ($claim->description ?? '-') . ")";
return "{$user} - {$role} {$project}";
})
->unique()
->implode("\n\n");
// Disediakan oleh (Staff) with Role
$preparedBy = $this->user->name ?? '-';
$preparedRole = $this->user->roles->first()->name ?? 'Staf';
// Write to Excel Cells
$sheet->setCellValue("A{$signStart}", 'Disediakan oleh:');
$sheet->setCellValue("A" . ($signStart + 1), "{$preparedBy}\n({$preparedRole})");
$sheet->getStyle("A" . ($signStart + 1))->getAlignment()->setWrapText(true);
$sheet->setCellValue("C{$signStart}", 'Disahkan oleh:');
$sheet->setCellValue("C" . ($signStart + 1), $approvedList ?: '-');
$sheet->getStyle("C" . ($signStart + 1))->getAlignment()->setWrapText(true);
$sheet->setCellValue("E{$signStart}", 'Diluluskan oleh:');
$sheet->setCellValue("E" . ($signStart + 1), $reviewedList ?: '-');
$sheet->getStyle("E" . ($signStart + 1))->getAlignment()->setWrapText(true);
$sheet->getStyle("A{$signStart}:E{$signStart}")->getFont()->setBold(true);
return [];
}
}
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
use Portal\Mileage\Model\Claim; use Portal\Mileage\Model\Claim;
use Illuminate\Routing\Controller; use Illuminate\Routing\Controller;
use Illuminate\Support\Collection;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Carbon\Carbon; use Carbon\Carbon;
...@@ -18,14 +19,32 @@ public function __construct() ...@@ -18,14 +19,32 @@ public function __construct()
// //
} }
public function index() public function index(Request $request)
{ {
$user = auth()->user(); $user = auth()->user();
$filter = $request->get('filter'); // ?filter=with or without
$month = $request->get('month', Carbon::now()->format('Y-m')); // e.g., 2025-10
// Parse month & year
$parsedMonth = Carbon::createFromFormat('Y-m', $month);
$monthNum = $parsedMonth->month;
$yearNum = $parsedMonth->year;
// Base query: user's claims within selected month
$query = Claim::with(['project', 'jenisKenderaan', 'user'])
->where('user_id', $user->id)
->whereMonth('claim_date', $monthNum)
->whereYear('claim_date', $yearNum)
->latest();
// Apply filter (with / without project)
if ($filter === 'with') {
$query->whereNotNull('project_id');
} elseif ($filter === 'without') {
$query->whereNull('project_id');
}
// Base query with relationships // Restrict for Project Manager (see only their project claims)
$query = Claim::with(['project', 'jenisKenderaan'])->latest();
// Restrict for Project Manager
if ($user->managedProjects()->exists()) { if ($user->managedProjects()->exists()) {
$projectIds = $user->managedProjects()->pluck('fk_project_id'); $projectIds = $user->managedProjects()->pluck('fk_project_id');
$query->whereIn('project_id', $projectIds); $query->whereIn('project_id', $projectIds);
...@@ -33,88 +52,69 @@ public function index() ...@@ -33,88 +52,69 @@ public function index()
$claims = $query->get(); $claims = $query->get();
// Group by month-year (example: "2025-10") // Load supporting dropdown data
$claimsByMonth = $claims->groupBy(function($claim) {
return \Carbon\Carbon::parse($claim->claim_date)->format('F Y'); // "October 2025"
});
$projects = Project::all(); $projects = Project::all();
$jenisKenderaan = JenisKenderaan::all(); $jenisKenderaan = JenisKenderaan::all();
return view('mileage::application.index', compact('claimsByMonth', 'projects', 'jenisKenderaan')); // Pass all to view
return view('mileage::application.index', [
'claims' => $claims,
'projects' => $projects,
'jenisKenderaan' => $jenisKenderaan,
'month' => $month,
'filter' => $filter,
]);
} }
public function store(Request $request) public function store(Request $request)
{ {
// Validate request data
$validated = $request->validate([ $validated = $request->validate([
'user_id' => 'required', 'claim_date' => 'required|date',
'claim_date' => 'required|date|before_or_equal:today',
'jenis_kenderaan_id' => 'required|exists:lkp_jenis_kenderaans,id', 'jenis_kenderaan_id' => 'required|exists:lkp_jenis_kenderaans,id',
'project_id' => 'required|exists:lkp_project,id', 'project_id' => 'nullable|exists:lkp_project,id',
'description' => 'required', 'description' => 'required',
'distance_from' => 'required', 'distance_from' => 'nullable',
'distance_to' => 'required', 'distance_to' => 'nullable',
'total_mileage' => 'required|numeric', 'total_mileage' => 'required|numeric',
'toll_amount' => 'nullable|numeric', 'toll_amount' => 'nullable|numeric',
'others_amount' => 'nullable|numeric', 'others_amount' => 'nullable|numeric',
'others_description' => 'nullable|string|max:255', 'others_description' => 'nullable|string|max:255',
]); ]);
// calculation logic $validated['user_id'] = auth()->id(); // Get the current authenticated user
$claimableMileage = max(0, $validated['total_mileage'] - 40);
if ($validated['jenis_kenderaan_id'] == 1) { // Car // Calculate the total claim amount and mileage
$rate = $claimableMileage <= 500 ? 0.55 : 0.50; $calculatedAmount = $this->calculateClaimAmount($validated);
} else { $validated['calculated_amount'] = $calculatedAmount;
$rate = 0.30;
}
$calculatedAmount = $claimableMileage * $rate; // Handle the status based on actions (e.g., project manager, director, finance)
$calculatedAmount += floatval($validated['toll_amount'] ?? 0); $validated['status'] = 'Diproses'; // Initial status
$calculatedAmount += floatval($validated['others_amount'] ?? 0);
$validated['calculated_amount'] = $calculatedAmount; // Store the claim in the database
$validated['claimable_mileage'] = $claimableMileage; $claim = Claim::create($validated);
// penalty calculation using DAYS (submission time = now)
$claimDate = Carbon::parse($validated['claim_date']);
$submissionDate = Carbon::now();
$daysDiff = $claimDate->diffInDays($submissionDate);
if ($daysDiff <= 30) {
$penalty = 0;
$penaltyReason = 'Tiada penalti (≤ 1 bulan)';
} elseif ($daysDiff <= 90) { // >30 && <=90
$penalty = $calculatedAmount * 0.10;
$penaltyReason = 'Lewat hantar lebih 1 bulan tetapi ≤ 3 bulan (10%)';
} elseif ($daysDiff <= 180) { // >90 && <=180
$penalty = $calculatedAmount * 0.30;
$penaltyReason = 'Lewat hantar lebih 3 bulan tetapi ≤ 6 bulan (30%)';
} else { // >180
$penalty = 0; // no numeric penalty applied automatically
$penaltyReason = 'Lewat lebih 6 bulan - Tertakluk kepada budi bicara BOD';
}
$validated['penalty_amount'] = $penalty; // Now, we need to manage the role and verify the project
$validated['penalty_reason'] = $penaltyReason;
$validated['total_claim_amount'] = $calculatedAmount - $penalty;
// default status // Example: If project_id is set, link the claim with the project and handle roles
$validated['status'] = 'Diproses'; if ($claim->project) {
$project = $claim->project;
// create // Here, you can check if the claim should be verified by a Project Manager (HOD)
$claim = Claim::create($validated); // For now, we’re assuming you use the role directly to set verified_by
if ($project->project_manager_id) {
$claim->verified_by = $project->project_manager_id;
}
if ($request->ajax()) { // Similarly, you can handle who approves and reviews (e.g., Project Director or BOD)
return response()->json([ if ($project->project_director_id) {
'success' => true, $claim->approved_by = $project->project_director_id;
'message' => 'Claim submitted successfully!', }
'claim' => $claim->load(['project', 'jenisKenderaan']),
]); $claim->save();
} }
return redirect()->route('mileage::applications.index') return redirect()->route('mileage::applications.index')->with('success', 'Claim submitted successfully!');
->with('success', 'Claim submitted successfully!');
} }
public function edit($id) public function edit($id)
...@@ -130,92 +130,46 @@ public function update(Request $request, $id) ...@@ -130,92 +130,46 @@ public function update(Request $request, $id)
{ {
$claim = Claim::findOrFail($id); $claim = Claim::findOrFail($id);
// decide new status based on role (if you want automated transitions) // Check if any required DB fields are null before allowing update
$newStatus = null; $requiredFields = [
if (auth()->check()) { 'claim_date',
if (auth()->user()->hasRole('Project Manager')) { 'distance_from',
$newStatus = 'Disokong'; 'distance_to',
} elseif (auth()->user()->hasRole('HOD')) { 'description',
$newStatus = 'Disokong'; 'total_mileage'
} elseif (auth()->user()->hasRole('Project Director')) { ];
$newStatus = 'Disahkan';
} elseif (auth()->user()->hasRole('Finance')) { foreach ($requiredFields as $field) {
$newStatus = 'Disemak'; if (is_null($claim->$field) || $claim->$field === '') {
} elseif (auth()->user()->hasRole('Finance Director')) { return redirect()->back()->with('error', 'Tidak boleh simpan kerana maklumat tuntutan asal tidak lengkap.');
$newStatus = 'Diluluskan';
} }
} }
$validated = $request->validate([ // Validate new input before updating
'user_id' => 'required', $request->validate([
'claim_date' => 'required|date', 'claim_date' => 'required|date',
'jenis_kenderaan_id' => 'required|exists:lkp_jenis_kenderaans,id', 'distance_from' => 'required|string',
'project_id' => 'required|exists:lkp_project,id', 'distance_to' => 'required|string',
'description' => 'required', 'description' => 'required|string',
'distance_from' => 'nullable', 'total_mileage' => 'required|numeric|min:0',
'distance_to' => 'nullable', 'toll_amount' => 'nullable|numeric|min:0',
'total_mileage' => 'required|numeric', 'others_amount' => 'nullable|numeric|min:0',
'toll_amount' => 'nullable|numeric', 'others_details' => 'required_if:others_amount,>,0|string|nullable',
'others_amount' => 'nullable|numeric',
'others_description' => 'nullable|string|max:255',
]); ]);
// calculation logic (same as store) // If claim data is valid, update it
$claimableMileage = max(0, $validated['total_mileage'] - 40); $claim->update([
'claim_date' => $request->claim_date,
if ($validated['jenis_kenderaan_id'] == 1) { 'distance_from' => $request->distance_from,
$rate = $claimableMileage <= 500 ? 0.55 : 0.50; 'distance_to' => $request->distance_to,
} else { 'description' => $request->description,
$rate = 0.30; 'total_mileage' => $request->total_mileage,
} 'toll_amount' => $request->toll_amount,
'others_amount' => $request->others_amount,
$calculatedAmount = $claimableMileage * $rate; 'others_details' => $request->others_details,
$calculatedAmount += floatval($validated['toll_amount'] ?? 0);
$calculatedAmount += floatval($validated['others_amount'] ?? 0);
$validated['calculated_amount'] = $calculatedAmount;
$validated['claimable_mileage'] = $claimableMileage;
// penalty calculation using DAYS (submission time = now)
$claimDate = Carbon::parse($validated['claim_date']);
$submissionDate = Carbon::now();
$daysDiff = $claimDate->diffInDays($submissionDate);
if ($daysDiff <= 30) {
$penalty = 0;
$penaltyReason = 'Tiada penalti (≤ 1 bulan)';
} elseif ($daysDiff <= 90) {
$penalty = $calculatedAmount * 0.10;
$penaltyReason = 'Lewat hantar >1 ≤3 bulan (10%)';
} elseif ($daysDiff <= 180) {
$penalty = $calculatedAmount * 0.30;
$penaltyReason = 'Lewat hantar >3 ≤6 bulan (30%)';
} else {
$penalty = 0;
$penaltyReason = 'Tertakluk kepada BOD';
}
$validated['penalty_amount'] = $penalty;
$validated['penalty_reason'] = $penaltyReason;
$validated['total_claim_amount'] = $calculatedAmount - $penalty;
// apply automated status change if applicable
if ($newStatus) {
$validated['status'] = $newStatus;
}
// update
$claim->update($validated);
if ($request->ajax()) {
return response()->json([
'success' => true,
'message' => 'Claim updated successfully!',
'claim' => $claim->load(['project', 'jenisKenderaan']),
]); ]);
}
return redirect()->route('mileage::applications.index')->with('success', 'Claim updated successfully!'); return redirect()->back()->with('success', 'Tuntutan berjaya dikemaskini.');
} }
public function destroy($id) public function destroy($id)
...@@ -226,39 +180,44 @@ public function destroy($id) ...@@ -226,39 +180,44 @@ public function destroy($id)
return redirect()->route('mileage::applications.index')->with('success', 'Claim deleted successfully!'); return redirect()->route('mileage::applications.index')->with('success', 'Claim deleted successfully!');
} }
public function show($id) public function show(Request $request, $id)
{ {
// load claim $claim = Claim::with(['project', 'jenisKenderaan', 'user'])->findOrFail($id);
$claim = Claim::with(['project', 'jenisKenderaan'])->findOrFail($id); $month = Carbon::parse($claim->claim_date)->format('Y-m');
$userId = $claim->user_id;
// Recompute penalty for display based on actual submission date (created_at) $projectId = $request->get('project_id', $claim->project_id);
// so show page always displays correct reason even if saved earlier incorrectly.
$claimDate = Carbon::parse($claim->claim_date); $query = Claim::with(['project', 'jenisKenderaan'])
$submissionDate = $claim->created_at ?? Carbon::now(); ->where('user_id', $userId)
$daysDiff = $claimDate->diffInDays($submissionDate); ->whereYear('claim_date', Carbon::parse($claim->claim_date)->year)
->whereMonth('claim_date', Carbon::parse($claim->claim_date)->month);
$calculatedAmount = $claim->calculated_amount ?? 0;
if ($projectId) {
if ($daysDiff <= 30) { $query->where('project_id', $projectId);
$displayPenalty = 0;
$displayReason = 'Tiada penalti (≤ 1 bulan)';
} elseif ($daysDiff <= 90) {
$displayPenalty = $calculatedAmount * 0.10;
$displayReason = 'Lewat hantar >1 ≤3 bulan (10%)';
} elseif ($daysDiff <= 180) {
$displayPenalty = $calculatedAmount * 0.30;
$displayReason = 'Lewat hantar >3 ≤6 bulan (30%)';
} else { } else {
$displayPenalty = 0; $query->whereNull('project_id');
$displayReason = 'Tertakluk kepada BOD';
} }
// Attach computed display-only properties (won't persist to DB) $claims = $query->orderBy('claim_date', 'asc')->get();
$claim->display_penalty_amount = $displayPenalty;
$claim->display_penalty_reason = $displayReason;
$claim->display_total_claim_amount = ($calculatedAmount - $displayPenalty);
return view('mileage::application.show', compact('claim')); $totalMileage = $claims->sum('total_mileage');
$totalToll = $claims->sum('toll_amount');
$totalOthers = $claims->sum('others_amount');
$totalAmount = $claims->sum('total_claim_amount');
$projectName = $claims->first()->project->p_project_description ?? 'Tanpa Projek';
return view('mileage::application.show', compact(
'claims', 'claim', 'projectName', 'totalMileage', 'totalToll', 'totalOthers', 'totalAmount', 'month'
));
}
/**
* Show a single claim (HR detailed view)
*/
public function viewEach($id)
{
$claim = Claim::with(['project', 'jenisKenderaan'])->findOrFail($id);
return view('mileage.applications.viewEach', compact('claim'));
} }
public function updateStatus(Request $request, $id) public function updateStatus(Request $request, $id)
...@@ -269,22 +228,27 @@ public function updateStatus(Request $request, $id) ...@@ -269,22 +228,27 @@ public function updateStatus(Request $request, $id)
switch ($request->action) { switch ($request->action) {
case 'verify': // PM/HOD case 'verify': // PM/HOD
$claim->status = 'Disokong'; $claim->status = 'Disokong';
$claim->approved_by = $user->id;
break; break;
case 'approve': // PD/BOD case 'approve': // PD/BOD
$claim->status = 'Disahkan'; $claim->status = 'Disahkan';
$claim->approved_by = $user->id;
break; break;
case 'review': // Accounting case 'review': // Finance
$claim->status = 'Disemak'; $claim->status = 'Disemak';
$claim->approved_by = $user->id;
break; break;
case 'finance_approve': // Finance Director case 'finance_approve': // Finance Director
$claim->status = 'Diluluskan'; $claim->status = 'Diluluskan';
$claim->approved_by = $user->id;
break; break;
case 'reject': // Any approver case 'reject': // Any approver
$claim->status = 'Ditolak'; $claim->status = 'Ditolak';
$claim->rejected_by = $user->id;
break; break;
case 'amend': // Finance edits case 'amend': // Finance edits
...@@ -301,4 +265,5 @@ public function updateStatus(Request $request, $id) ...@@ -301,4 +265,5 @@ public function updateStatus(Request $request, $id)
return back()->with('success', 'Claim status updated to ' . $claim->status); return back()->with('success', 'Claim status updated to ' . $claim->status);
} }
} }
...@@ -121,4 +121,13 @@ public function save(Request $request) ...@@ -121,4 +121,13 @@ public function save(Request $request)
// TODO: Implement save logic // TODO: Implement save logic
return null; return null;
} }
public function noproject()
{
$user = Auth::user();
$jenisKenderaan = JenisKenderaan::all();
return view('mileage::applyform.noproject', compact('jenisKenderaan'));
}
} }
<?php
namespace Portal\Mileage\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use Portal\Mileage\Model\Users;
use Portal\Mileage\Model\Claim;
class BODController extends Controller
{
public function index()
{
$bod = Auth::user();
// Get all claims with no project, approved by HOD or previously verified
$claims = Claim::with(['user', 'project'])
->whereNull('project_id')
->where(function ($query) use ($bod) {
$query->whereIn('status', ['Disokong', 'Disahkan'])
->orWhere(function ($sub) use ($bod) {
// Only show claims rejected by this specific BOD
$sub->where('status', 'Ditolak')
->where('rejected_by', $bod->id);
});
})
->orderBy('claim_date', 'desc')
->get();
return view('mileage::bod.index', compact('claims'));
}
public function show($id)
{
$claim = Claim::with(['user', 'project', 'jenisKenderaan'])->findOrFail($id);
// Calculate claimable mileage
$claim->claimable_mileage = max(0, $claim->total_mileage - 40);
// Calculate penalties
$claimDate = Carbon::parse($claim->claim_date);
$submissionDate = $claim->created_at ?? Carbon::now();
$daysDiff = $claimDate->diffInDays($submissionDate);
$calculatedAmount = $claim->calculated_amount ?? 0;
if ($daysDiff <= 30) {
$claim->display_penalty_amount = 0;
$claim->display_penalty_reason = 'Tiada penalti (≤ 1 bulan)';
} elseif ($daysDiff <= 90) {
$claim->display_penalty_amount = $calculatedAmount * 0.10;
$claim->display_penalty_reason = 'Lewat hantar >1 ≤3 bulan (10%)';
} elseif ($daysDiff <= 180) {
$claim->display_penalty_amount = $calculatedAmount * 0.30;
$claim->display_penalty_reason = 'Lewat hantar >3 ≤6 bulan (30%)';
} else {
$claim->display_penalty_amount = 0;
$claim->display_penalty_reason = 'Tertakluk kepada BOD';
}
$claim->display_total_claim_amount =
($calculatedAmount + ($claim->toll_amount ?? 0) + ($claim->others_amount ?? 0))
- $claim->display_penalty_amount;
return view('mileage::bod.show', compact('claim'));
}
public function approve($id)
{
$bod = Auth::user();
$claim = Claim::findOrFail($id);
if ($claim->status !== 'Disokong') {
return back()->with('error', 'Tuntutan ini belum disokong oleh HOD.');
}
$claim->status = 'Disahkan';
$claim->approved_by = $bod->id;
$claim->save();
return back()->with('success', 'Tuntutan berjaya disahkan oleh BOD.');
}
public function reject($id)
{
$bod = Auth::user();
$claim = Claim::findOrFail($id);
$claim->status = 'Ditolak';
$claim->rejected_by = $bod->id;
$claim->save();
return back()->with('error', 'Tuntutan telah ditolak oleh BOD.');
}
}
...@@ -22,8 +22,8 @@ public function index() ...@@ -22,8 +22,8 @@ public function index()
} }
$staff = Users::where('employeeCode',auth()->user()->employeeCode) $staff = Users::where('employeeCode', auth()->user()->employeeCode)
->with('staffinfo') ->with('staffinfo.department', 'roles') // added nested relationship
->first(); ->first();
return view('mileage::dashboard.pegawai',compact('staff')); return view('mileage::dashboard.pegawai',compact('staff'));
......
<?php
namespace Portal\Mileage\Http\Controllers;
use Portal\Mileage\Model\Users;
use Portal\Mileage\Model\Claim;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Carbon\Carbon;
use Maatwebsite\Excel\Facades\Excel;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Portal\Mileage\Exports\ClaimsExport;
class FinanceController extends Controller
{
public function index(Request $request)
{
$month = $request->get('month', Carbon::now()->format('Y-m'));
$parsedMonth = Carbon::parse($month);
$claims = Claim::with(['user', 'project'])
->whereIn('status', ['Disahkan', 'Disokong', 'Disemak', 'Diproses', 'Ditolak', 'Dibayar'])
->whereMonth('claim_date', $parsedMonth->month)
->whereYear('claim_date', $parsedMonth->year)
->get();
$summary = [
'processed' => $claims->where('status', 'Diproses')->count(),
'verified' => $claims->where('status', 'Disokong')->count(),
'approved' => $claims->where('status', 'Disahkan')->count(),
'reviewed' => $claims->where('status', 'Disemak')->count(),
'paid' => $claims->where('status', 'Dibayar')->count(),
'rejected' => $claims->where('status', 'Ditolak')->count(),
];
$users = $claims
->groupBy('user_id')
->map(function ($userClaims) {
$user = $userClaims->first()->user;
return [
'id' => $user->id ?? null,
'name' => $user->name ?? '-',
'total_claims' => $userClaims->count(),
'total_amount' => $userClaims->sum('calculated_amount'),
];
})
->sortBy('name')
->values();
return view('mileage::finance.index', compact('users', 'summary', 'month'));
}
public function show($userId, Request $request)
{
$month = $request->get('month', Carbon::now()->format('Y-m'));
$parsedMonth = Carbon::parse($month);
$user = Users::findOrFail($userId);
$claims = Claim::with(['project', 'jenisKenderaan'])
->where('user_id', $userId)
->whereMonth('claim_date', $parsedMonth->month)
->whereYear('claim_date', $parsedMonth->year)
->get();
return view('mileage::finance.show', compact('user', 'claims', 'month'));
}
public function viewEach($id)
{
$claim = Claim::with(['user', 'project', 'jenisKenderaan'])->findOrFail($id);
return view('mileage::finance.view', compact('claim'));
}
public function update(Request $request, $id)
{
$claim = Claim::findOrFail($id);
$validated = $request->validate([
'claim_date' => 'required|date',
'distance_from' => 'required|string|max:255',
'distance_to' => 'required|string|max:255',
'description' => 'required|string|max:255',
'total_mileage' => 'required|numeric|min:0',
'toll_amount' => 'nullable|numeric|min:0',
'others_amount' => 'nullable|numeric|min:0',
'others_details' => 'nullable|string|max:255',
]);
if (($request->others_amount ?? 0) > 0 && empty($request->others_details)) {
return back()->with('error', 'Butiran lain-lain perlu diisi apabila jumlah lain-lain melebihi 0.');
}
$claim->update([
'claim_date' => $validated['claim_date'],
'distance_from' => $validated['distance_from'],
'distance_to' => $validated['distance_to'],
'description' => $validated['description'],
'total_mileage' => $validated['total_mileage'],
'toll_amount' => $validated['toll_amount'] ?? 0,
'others_amount' => $validated['others_amount'] ?? 0,
'others_details' => $validated['others_details'] ?? null,
'updated_at' => now(),
]);
$vehicleType = strtolower($claim->jenisKenderaan->name ?? 'kereta');
$mileage = $claim->total_mileage ?? 0;
$rateCar1 = 0.55;
$rateCar2 = 0.50;
$rateMoto = 0.30;
if (in_array($vehicleType, ['motosikal', 'motorcycle'])) {
$mileageAmount = $mileage * $rateMoto;
} else {
$firstPart = min($mileage, 500);
$secondPart = max(0, $mileage - 500);
$mileageAmount = ($firstPart * $rateCar1) + ($secondPart * $rateCar2);
}
$claim->calculated_amount = $mileageAmount + ($claim->toll_amount ?? 0) + ($claim->others_amount ?? 0);
$claim->save();
return back()->with('success', 'Maklumat tuntutan telah berjaya dikemaskini.');
}
public function updateStatus(Request $request, $id)
{
$claim = Claim::findOrFail($id);
$action = $request->input('action');
if (!in_array($action, ['Disemak', 'Ditolak'])) {
return back()->with('error', 'Tindakan tidak sah.');
}
if (in_array($claim->status, ['Disemak', 'Ditolak', 'Dibayar'])) {
return back()->with('error', 'Tuntutan ini telah disemak atau dimuktamadkan.');
}
// update status and reviewer
$claim->update([
'status' => $action,
'reviewed_by' => auth()->id(), // ✅ log who reviewed the claim
]);
return back()->with('success', "Tuntutan telah dikemaskini kepada status {$action}.");
}
public function export(Request $request)
{
$month = $request->input('month');
$userId = $request->input('user_id');
$parsedMonth = $month ? Carbon::parse($month) : Carbon::now();
$query = Claim::with(['user', 'project', 'approvedBy', 'reviewedBy'])
->where('status', 'Disemak')
->whereMonth('claim_date', $parsedMonth->month)
->whereYear('claim_date', $parsedMonth->year)
->orderBy('claim_date', 'asc');
if (!empty($userId)) {
$query->where('user_id', $userId);
}
$claims = Claim::with(['user', 'project', 'approvedBy', 'reviewedBy'])
->whereMonth('claim_date', $parsedMonth->month)
->whereYear('claim_date', $parsedMonth->year)
->where('status', 'Disemak')
->get();
$user = $claims->first()->user ?? null;
if ($claims->isEmpty()) {
return back()->with('error', 'Tiada tuntutan dijumpai untuk bulan ini.');
}
$fileName = !empty($userId)
? 'Laporan_Tuntutan_' . str_replace(' ', '_', $user->name) . '_' . $parsedMonth->format('m-Y') . '.xlsx'
: 'Laporan_Tuntutan_Semua_Staf_' . $parsedMonth->format('m-Y') . '.xlsx';
$excelData = Excel::raw(new ClaimsExport($user, $claims), \Maatwebsite\Excel\Excel::XLSX);
return new StreamedResponse(
function () use ($excelData) {
echo $excelData;
},
200,
[
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'Content-Disposition' => 'inline; filename="' . $fileName . '"',
]
);
}
}
<?php
namespace Portal\Mileage\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use Portal\Mileage\Model\Users;
use Portal\Mileage\Model\Claim;
class HODController extends Controller
{
public function index()
{
$hod = Auth::user();
// check if HOD has staff info
if (!$hod || !$hod->staffInfo) {
return back()->with('error', 'Maklumat jabatan HOD tidak dijumpai.');
}
$departmentId = $hod->staffInfo->fk_lkp_department;
// get all staff IDs under the same department
$staffIds = Users::whereHas('staffInfo', function ($query) use ($departmentId) {
$query->where('fk_lkp_department', $departmentId);
})->pluck('id');
// fetch claims under the HOD’s department
$claims = Claim::with(['user', 'project'])
->whereIn('user_id', $staffIds)
->whereNull('project_id')
->where(function ($q) use ($hod) {
$q->whereIn('status', ['Diproses', 'Disokong']) // pending for review
->orWhere(function ($sub) use ($hod) {
// Only show rejected claims that were rejected by THIS HOD
$sub->where('status', 'Ditolak')
->where('rejected_by', $hod->id);
});
})
->orderBy('claim_date', 'desc')
->get();
return view('mileage::hod.index', compact('claims'));
}
public function show($id)
{
$claim = Claim::with(['project', 'jenisKenderaan', 'user'])->findOrFail($id);
// Deduct first 40 km
$totalMileage = $claim->total_mileage ?? 0;
$claim->claimable_mileage = max(0, $totalMileage - 40);
// Penalty logic
$claimDate = Carbon::parse($claim->claim_date);
$submissionDate = $claim->created_at ?? Carbon::now();
$daysDiff = $claimDate->diffInDays($submissionDate);
$calculatedAmount = $claim->calculated_amount ?? 0;
if ($daysDiff <= 30) {
$displayPenalty = 0;
$displayReason = 'Tiada penalti (≤ 1 bulan)';
} elseif ($daysDiff <= 90) {
$displayPenalty = $calculatedAmount * 0.10;
$displayReason = 'Lewat hantar >1 ≤3 bulan (10%)';
} elseif ($daysDiff <= 180) {
$displayPenalty = $calculatedAmount * 0.30;
$displayReason = 'Lewat hantar >3 ≤6 bulan (30%)';
} else {
$displayPenalty = 0;
$displayReason = 'Tertakluk kepada BOD';
}
$claim->display_penalty_amount = $displayPenalty;
$claim->display_penalty_reason = $displayReason;
$claim->display_total_claim_amount =
($calculatedAmount + ($claim->toll_amount ?? 0) + ($claim->others_amount ?? 0)) - $displayPenalty;
return view('mileage::hod.show', compact('claim'));
}
public function verify($id)
{
$hod = Auth::user();
$claim = Claim::with('user.staffInfo')->findOrFail($id);
// Check if claim is from HOD’s department
$hodDept = $hod->staffInfo->fk_lkp_department ?? null;
$claimDept = $claim->user->staffInfo->fk_lkp_department ?? null;
if ($hodDept !== $claimDept) {
return back()->with('error', 'Anda tidak dibenarkan menyokong tuntutan ini.');
}
$claim->status = 'Disokong';
$claim->approved_by = $hod->id;
$claim->save();
return back()->with('success', 'Tuntutan berjaya disokong.');
}
public function reject($id)
{
$hod = Auth::user();
$claim = Claim::with('user.staffInfo')->findOrFail($id);
$hodDept = $hod->staffInfo->fk_lkp_department ?? null;
$claimDept = $claim->user->staffInfo->fk_lkp_department ?? null;
if ($hodDept !== $claimDept) {
return back()->with('error', 'Anda tidak dibenarkan menolak tuntutan ini.');
}
$claim->status = 'Ditolak';
$claim->rejected_by = $hod->id;
$claim->save();
return back()->with('error', 'Tuntutan telah ditolak.');
}
}
<?php
namespace Portal\Mileage\Http\Controllers;
use Portal\Mileage\Model\Users;
use Portal\Mileage\Model\Project;
use Portal\Mileage\Model\JenisKenderaan;
use Portal\Mileage\Model\Claim;
use Portal\Mileage\Model\ProjectTeam;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
use Maatwebsite\Excel\Facades\Excel;
use Portal\Mileage\Exports\ClaimsExport;
use DB;
class HRController extends Controller
{
public function index(Request $request)
{
$month = $request->input('month', Carbon::now()->format('Y-m'));
$parsedMonth = Carbon::parse($month);
// Group claims by staff for the selected month
$claims = Claim::with('user')
->whereMonth('claim_date', $parsedMonth->month)
->whereYear('claim_date', $parsedMonth->year)
->get()
->groupBy('user_id');
$users = $claims->map(function ($claimsForUser) {
$user = $claimsForUser->first()->user;
return [
'id' => $user->id,
'name' => $user->name ?? '-',
'total_claims' => $claimsForUser->count(),
'total_amount' => $claimsForUser->sum('total_claim_amount'),
];
});
return view('mileage::hr.index', compact('users', 'month'));
}
/**
* Show individual claim details
*/
public function show($userId, Request $request)
{
$month = $request->input('month', Carbon::now()->format('Y-m'));
$parsedMonth = Carbon::parse($month);
$user = Users::findOrFail($userId);
$claims = Claim::where('user_id', $userId)
->whereMonth('claim_date', $parsedMonth->month)
->whereYear('claim_date', $parsedMonth->year)
->orderBy('claim_date', 'asc')
->get();
return view('mileage::hr.show', compact('user', 'claims', 'month'));
}
/**
* Show a single claim (HR detailed view)
*/
public function viewEach($id)
{
$claim = Claim::with(['user', 'project', 'jenisKenderaan'])->findOrFail($id);
return view('mileage::HR.view', compact('claim'));
}
/**
* Reject claim (HR decision)
*/
public function reject($id)
{
$hr = Auth::user();
$claim = Claim::findOrFail($id);
$claim->status = 'Ditolak';
$claim->rejected_by = $hr->id; // record who rejected it
$claim->save();
return back()->with('error', 'Tuntutan telah ditolak oleh HR.');
}
/**
* Export all claims to Excel
*/
public function export(Request $request)
{
$userId = $request->input('user_id');
$month = $request->input('month');
// parse month safely
$parsedMonth = $month ? Carbon::parse($month) : null;
// fetch user
$user = $userId ? Users::find($userId) : null;
// query claims by filter
$claims = Claim::with(['user', 'project'])
->when($userId, fn($q) => $q->where('user_id', $userId))
->when($parsedMonth, fn($q) =>
$q->whereMonth('claim_date', $parsedMonth->month)
->whereYear('claim_date', $parsedMonth->year)
)
->orderBy('claim_date', 'asc')
->get();
// if no data found, return with warning
if ($claims->isEmpty()) {
return back()->with('error', 'Tiada tuntutan dijumpai untuk penapis yang dipilih.');
}
// build filename dynamically
$staffName = $user ? str_replace(' ', '_', strtoupper($user->name)) : 'SEMUA_STAF';
$monthName = $parsedMonth ? $parsedMonth->format('m-Y') : Carbon::now()->format('m-Y');
$fileName = "Tuntutan_{$staffName}_{$monthName}.xlsx";
// generate Excel using filtered data
return Excel::download(new ClaimsExport($user, $claims), $fileName);
}
}
...@@ -3,115 +3,186 @@ ...@@ -3,115 +3,186 @@
namespace Portal\Mileage\Http\Controllers; namespace Portal\Mileage\Http\Controllers;
use Portal\Mileage\Model\Users; use Portal\Mileage\Model\Users;
use Portal\Mileage\Model\Project;
use Portal\Mileage\Model\JenisKenderaan;
use Portal\Mileage\Model\Claim; use Portal\Mileage\Model\Claim;
use Portal\Mileage\Model\Project;
use Portal\Mileage\Model\ProjectTeam; use Portal\Mileage\Model\ProjectTeam;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
class ProjectDirectorController extends Controller class ProjectDirectorController extends Controller
{ {
public function index() /**
* Display the Project Director Dashboard (Monthly Filter View)
*/
public function index(Request $request)
{ {
$user = auth()->user(); $director = Auth::user();
$month = $request->input('month', Carbon::now()->format('Y-m'));
// Get project IDs where this user is the Project Director // Validate month format
$directedProjectIds = ProjectTeam::where('pt_staff_id', $user->employeeCode) try {
->where('role_id', 7) // 7 = Project Director if (!preg_match('/^\d{4}-\d{2}$/', $month)) {
->pluck('fk_project_id') $month = now()->format('Y-m');
->toArray(); }
$parsedMonth = Carbon::createFromFormat('Y-m', $month);
} catch (\Exception $e) {
$month = now()->format('Y-m');
$parsedMonth = Carbon::createFromFormat('Y-m', $month);
}
// Get project IDs under this Project Director
$projectIds = ProjectTeam::where('pt_staff_id', $director->employeeCode)
->where('role_id', 7) // Project Director role
->pluck('fk_project_id');
// Get projects under the PD
$projects = Project::whereIn('id', $projectIds)->get();
// Get claims for those projects, only after PM verification // Get claims for those projects in the selected month
$claims = Claim::with(['user', 'project']) $claims = Claim::with(['user', 'project'])
->whereIn('project_id', $directedProjectIds) ->whereIn('project_id', $projectIds)
->whereIn('status', ['Disahkan', 'Ditolak']) // Only show claims verified by Project Manager ->whereMonth('claim_date', $parsedMonth->month)
->orderBy('created_at', 'desc') ->whereYear('claim_date', $parsedMonth->year)
->get(); ->get();
return view('mileage::projectDirector.index', compact('claims')); return view('mileage::projectDirector.index', compact('projects', 'claims', 'month'));
} }
public function show($id)
{
// load claim
$claim = Claim::with(['project', 'jenisKenderaan', 'user'])->findOrFail($id);
// Recompute penalty for display based on actual submission date (created_at)
// so show page always displays correct reason even if saved earlier incorrectly.
$claimDate = Carbon::parse($claim->claim_date);
$submissionDate = $claim->submission_date ?? $claim->created_at ?? Carbon::now();
$daysDiff = $claimDate->diffInDays($submissionDate);
$calculatedAmount = $claim->calculated_amount ?? 0; /**
* Show all claims for one staff within a project for the selected month.
*/
public function show($projectId, $userId, Request $request)
{
$month = $request->input('month', Carbon::now()->format('Y-m'));
if ($daysDiff <= 30) { try {
$displayPenalty = 0; if (!preg_match('/^\d{4}-\d{2}$/', $month)) {
$displayReason = 'Tiada penalti (≤ 1 bulan)'; $month = now()->format('Y-m');
} elseif ($daysDiff <= 90) {
$displayPenalty = $calculatedAmount * 0.10;
$displayReason = 'Lewat hantar >1 ≤3 bulan (10%)';
} elseif ($daysDiff <= 180) {
$displayPenalty = $calculatedAmount * 0.30;
$displayReason = 'Lewat hantar >3 ≤6 bulan (30%)';
} else {
$displayPenalty = 0;
$displayReason = 'Tertakluk kepada BOD';
} }
$parsedMonth = Carbon::createFromFormat('Y-m', $month);
} catch (\Exception $e) {
$month = now()->format('Y-m');
$parsedMonth = Carbon::createFromFormat('Y-m', $month);
}
$project = Project::findOrFail($projectId);
$user = Users::findOrFail($userId);
// Attach computed display-only properties (won't persist to DB) $claims = Claim::with(['project', 'jenisKenderaan'])
$claim->display_penalty_amount = $displayPenalty; ->where('project_id', $projectId)
$claim->display_penalty_reason = $displayReason; ->where('user_id', $userId)
$totalClaim = ($calculatedAmount + ($claim->toll_amount ?? 0) + ($claim->others_amount ?? 0)) - $displayPenalty; ->whereMonth('claim_date', $parsedMonth->month)
$claim->display_total_claim_amount = $totalClaim; ->whereYear('claim_date', $parsedMonth->year)
->orderBy('claim_date', 'asc')
->get();
return view('mileage::projectDirector.show', compact('claim')); return view('mileage::projectDirector.show', compact('project', 'user', 'claims', 'month'));
} }
public function approve($id) /**
* Approve all claims for a specific project in the selected month.
*/
public function approveMonth(Request $request)
{ {
$user = auth()->user(); $director = Auth::user();
$monthLabel = $request->input('month');
$projectId = $request->input('project_id');
$claim = Claim::findOrFail($id); // validate project_id presence
$claim->status = 'Disahkan'; if (!$projectId) {
return back()->with('error', 'Sila pilih projek untuk disahkan.');
}
// validate month format
try {
if (!preg_match('/^\d{4}-\d{2}$/', $monthLabel)) {
throw new \Exception('Invalid month format');
}
$parsedMonth = Carbon::createFromFormat('Y-m', $monthLabel);
} catch (\Exception $e) {
return back()->with('error', 'Format bulan tidak sah. Sila pilih semula bulan dalam format YYYY-MM.');
}
$allowedProjectIds = ProjectTeam::where('pt_staff_id', $user->employeeCode) // ensure director actually oversees this project
$allowedProjectIds = ProjectTeam::where('pt_staff_id', $director->employeeCode)
->where('role_id', 7) ->where('role_id', 7)
->pluck('fk_project_id') ->pluck('fk_project_id')
->toArray(); ->toArray();
if (!in_array($claim->project_id, $allowedProjectIds)) { if (!in_array($projectId, $allowedProjectIds)) {
return back()->with('error', 'Anda tidak dibenarkan meluluskan tuntutan ini.'); return back()->with('error', 'Anda tidak dibenarkan melakukan tindakan ini untuk projek terpilih.');
} }
$claim->status = 'Disahkan'; // update only claims that belong to this project and month and are Disokong
$claim->save(); $approvedCount = Claim::where('project_id', $projectId)
->whereMonth('claim_date', $parsedMonth->month)
->whereYear('claim_date', $parsedMonth->year)
->where('status', 'Disokong')
->update([
'status' => 'Disahkan',
'approved_by' => $director->id,
'updated_at' => now(),
]);
if ($approvedCount > 0) {
return back()->with('success', $approvedCount . ' tuntutan telah disahkan untuk projek ini bagi bulan ' . $monthLabel . '.');
}
return back()->with('success', 'Claim approved successfully.'); return back()->with('warning', 'Tiada tuntutan "Disokong" untuk disahkan bagi projek ini pada bulan ' . $monthLabel . '.');
} }
public function reject($id) /**
* Reject all claims for a specific project in the selected month.
*/
public function rejectMonth(Request $request)
{ {
$claim = Claim::findOrFail($id); $director = Auth::user();
$user = auth()->user(); $monthLabel = $request->input('month');
$projectId = $request->input('project_id');
if (!$projectId) {
return back()->with('error', 'Sila pilih projek untuk ditolak.');
}
$allowedProjectIds = ProjectTeam::where('pt_staff_id', $user->employeeCode) // validate month format
try {
if (!preg_match('/^\d{4}-\d{2}$/', $monthLabel)) {
throw new \Exception('Invalid month format');
}
$parsedMonth = Carbon::createFromFormat('Y-m', $monthLabel);
} catch (\Exception $e) {
return back()->with('error', 'Format bulan tidak sah. Sila pilih semula bulan dalam format YYYY-MM.');
}
// ensure director actually oversees this project
$allowedProjectIds = ProjectTeam::where('pt_staff_id', $director->employeeCode)
->where('role_id', 7) ->where('role_id', 7)
->pluck('fk_project_id') ->pluck('fk_project_id')
->toArray(); ->toArray();
if (!in_array($claim->project_id, $allowedProjectIds)) { if (!in_array($projectId, $allowedProjectIds)) {
return back()->with('error', 'Anda tidak dibenarkan menolak tuntutan ini.'); return back()->with('error', 'Anda tidak dibenarkan melakukan tindakan ini untuk projek terpilih.');
} }
$claim->status = 'Ditolak'; // update only claims that belong to this project and month and are Disokong
$claim->save(); $rejectedCount = Claim::where('project_id', $projectId)
->whereMonth('claim_date', $parsedMonth->month)
->whereYear('claim_date', $parsedMonth->year)
->where('status', 'Disokong')
->update([
'status' => 'Ditolak',
'rejected_by' => $director->id,
'updated_at' => now(),
]);
if ($rejectedCount > 0) {
return back()->with('success', $rejectedCount . ' tuntutan telah ditolak untuk projek ini bagi bulan ' . $monthLabel . '.');
}
return back()->with('error', 'Claim rejected.'); return back()->with('warning', 'Tiada tuntutan "Disokong" untuk ditolak bagi projek ini pada bulan ' . $monthLabel . '.');
} }
} }
...@@ -3,67 +3,187 @@ ...@@ -3,67 +3,187 @@
namespace Portal\Mileage\Http\Controllers; namespace Portal\Mileage\Http\Controllers;
use Portal\Mileage\Model\Users; use Portal\Mileage\Model\Users;
use Portal\Mileage\Model\Project;
use Portal\Mileage\Model\JenisKenderaan;
use Portal\Mileage\Model\Claim; use Portal\Mileage\Model\Claim;
use Portal\Mileage\Model\Project;
use Portal\Mileage\Model\ProjectTeam; use Portal\Mileage\Model\ProjectTeam;
use App\Http\Controllers\Controller;
use Illuminate\Routing\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon; use Carbon\Carbon;
class ProjectManagerController extends Controller class ProjectManagerController extends Controller
{ {
public function index() /**
* Display the project manager dashboard (monthly view only).
*/
public function index(Request $request)
{
$manager = Auth::user();
$month = $request->input('month', Carbon::now()->format('Y-m'));
try {
// validate the format (YYYY-MM)
if (!preg_match('/^\d{4}-\d{2}$/', $month)) {
$month = now()->format('Y-m');
}
$parsedMonth = Carbon::createFromFormat('Y-m', $month);
} catch (\Exception $e) {
$month = now()->format('Y-m');
$parsedMonth = Carbon::createFromFormat('Y-m', $month);
}
// Get project IDs assigned to this Project Manager
$projectIds = \Portal\Mileage\Model\ProjectTeam::where('pt_staff_id', $manager->employeeCode)
->where('role_id', 1) // "Project Manager" role
->pluck('fk_project_id');
// Get projects under the PM
$projects = \Portal\Mileage\Model\Project::whereIn('id', $projectIds)->get();
// Get all claims for those projects in the selected month
$claims = \Portal\Mileage\Model\Claim::with(['user', 'project'])
->whereIn('project_id', $projectIds)
->whereMonth('claim_date', $parsedMonth->month)
->whereYear('claim_date', $parsedMonth->year)
->get();
return view('mileage::projectManager.index', compact('projects', 'claims', 'month'));
}
/**
* Show all claims for one staff for the selected month.
*/
public function show($projectId, $userId, Request $request)
{ {
$user = auth()->user(); $month = $request->input('month', Carbon::now()->format('Y-m'));
try {
if (!preg_match('/^\d{4}-\d{2}$/', $month)) {
$month = now()->format('Y-m');
}
$parsedMonth = Carbon::createFromFormat('Y-m', $month);
} catch (\Exception $e) {
$month = now()->format('Y-m');
$parsedMonth = Carbon::createFromFormat('Y-m', $month);
}
// Get project IDs managed by this PM $project = \Portal\Mileage\Model\Project::findOrFail($projectId);
$managedProjectIds = ProjectTeam::where('pt_staff_id', $user->employeeCode) $user = \Portal\Mileage\Model\Users::findOrFail($userId);
->where('role_id', 1) // assuming 2 = PM
$claims = \Portal\Mileage\Model\Claim::with(['project'])
->where('project_id', $projectId)
->where('user_id', $userId)
->whereMonth('claim_date', $parsedMonth->month)
->whereYear('claim_date', $parsedMonth->year)
->orderBy('claim_date', 'asc')
->get();
return view('mileage::projectManager.show', compact('project', 'user', 'claims', 'month'));
}
/**
* Approve all pending claims for a specific project in the selected month.
*/
public function approveMonth(Request $request)
{
$manager = Auth::user();
$monthLabel = $request->input('month');
$projectId = $request->input('project_id');
// require project_id
if (!$projectId) {
return back()->with('error', 'Sila pilih projek untuk disokong.');
}
// validate month format (expects YYYY-MM)
try {
if (!preg_match('/^\d{4}-\d{2}$/', $monthLabel)) {
throw new \Exception('Invalid month format');
}
$parsedMonth = Carbon::createFromFormat('Y-m', $monthLabel);
} catch (\Exception $e) {
return back()->with('error', 'Format bulan tidak sah. Sila pilih semula bulan dalam format YYYY-MM.');
}
// ensure manager oversees this project
$allowedProjectIds = ProjectTeam::where('pt_staff_id', $manager->employeeCode)
->where('role_id', 1) // project manager role
->pluck('fk_project_id') ->pluck('fk_project_id')
->toArray(); ->toArray();
// Get claims only for those projects if (!in_array($projectId, $allowedProjectIds)) {
$claims = Claim::with(['user', 'project']) return back()->with('error', 'Anda tidak dibenarkan melakukan tindakan ini untuk projek terpilih.');
->whereIn('project_id', $managedProjectIds) }
->get();
// update only claims for this project and month with status 'Diproses'
$approvedCount = Claim::where('project_id', $projectId)
->whereMonth('claim_date', $parsedMonth->month)
->whereYear('claim_date', $parsedMonth->year)
->where('status', 'Diproses')
->update([
'status' => 'Disokong',
'approved_by' => $manager->id,
'updated_at' => now(),
]);
if ($approvedCount > 0) {
return back()->with('success', $approvedCount . ' tuntutan telah disokong untuk projek ini bagi bulan ' . $monthLabel . '.');
}
return view('mileage::projectManager.index', compact('claims')); return back()->with('warning', 'Tiada tuntutan "Diproses" untuk disokong bagi projek ini pada bulan ' . $monthLabel . '.');
} }
public function show($id) /**
* Reject all pending claims for a specific project in the selected month.
*/
public function rejectMonth(Request $request)
{ {
// load claim $manager = Auth::user();
$claim = Claim::with(['project', 'jenisKenderaan'])->findOrFail($id); $monthLabel = $request->input('month');
$projectId = $request->input('project_id');
// Recompute penalty for display based on actual submission date (created_at)
// so show page always displays correct reason even if saved earlier incorrectly. if (!$projectId) {
$claimDate = Carbon::parse($claim->claim_date); return back()->with('error', 'Sila pilih projek untuk ditolak.');
$submissionDate = $claim->created_at ?? Carbon::now(); }
$daysDiff = $claimDate->diffInDays($submissionDate);
// validate month format (expects YYYY-MM)
$calculatedAmount = $claim->calculated_amount ?? 0; try {
if (!preg_match('/^\d{4}-\d{2}$/', $monthLabel)) {
if ($daysDiff <= 30) { throw new \Exception('Invalid month format');
$displayPenalty = 0; }
$displayReason = 'Tiada penalti (≤ 1 bulan)'; $parsedMonth = Carbon::createFromFormat('Y-m', $monthLabel);
} elseif ($daysDiff <= 90) { } catch (\Exception $e) {
$displayPenalty = $calculatedAmount * 0.10; return back()->with('error', 'Format bulan tidak sah. Sila pilih semula bulan dalam format YYYY-MM.');
$displayReason = 'Lewat hantar >1 ≤3 bulan (10%)'; }
} elseif ($daysDiff <= 180) {
$displayPenalty = $calculatedAmount * 0.30; // ensure manager oversees this project
$displayReason = 'Lewat hantar >3 ≤6 bulan (30%)'; $allowedProjectIds = ProjectTeam::where('pt_staff_id', $manager->employeeCode)
} else { ->where('role_id', 1)
$displayPenalty = 0; ->pluck('fk_project_id')
$displayReason = 'Tertakluk kepada BOD'; ->toArray();
}
if (!in_array($projectId, $allowedProjectIds)) {
// Attach computed display-only properties (won't persist to DB) return back()->with('error', 'Anda tidak dibenarkan melakukan tindakan ini untuk projek terpilih.');
$claim->display_penalty_amount = $displayPenalty; }
$claim->display_penalty_reason = $displayReason;
$claim->display_total_claim_amount = ($calculatedAmount - $displayPenalty); // update only claims for this project and month with status 'Diproses'
$rejectedCount = Claim::where('project_id', $projectId)
return view('mileage::projectManager.show', compact('claim')); ->whereMonth('claim_date', $parsedMonth->month)
->whereYear('claim_date', $parsedMonth->year)
->where('status', 'Diproses')
->update([
'status' => 'Ditolak',
'rejected_by' => $manager->id,
'updated_at' => now(),
]);
if ($rejectedCount > 0) {
return back()->with('success', $rejectedCount . ' tuntutan telah ditolak untuk projek ini bagi bulan ' . $monthLabel . '.');
}
return back()->with('warning', 'Tiada tuntutan "Diproses" untuk ditolak bagi projek ini pada bulan ' . $monthLabel . '.');
} }
} }
...@@ -35,5 +35,21 @@ public function jenisKenderaan() ...@@ -35,5 +35,21 @@ public function jenisKenderaan()
{ {
return $this->belongsTo(JenisKenderaan::class, 'jenis_kenderaan_id', 'id'); return $this->belongsTo(JenisKenderaan::class, 'jenis_kenderaan_id', 'id');
} }
public function verifiedBy()
{
return $this->belongsTo(Users::class, 'verified_by');
}
public function approvedBy()
{
return $this->belongsTo(Users::class, 'approved_by');
}
public function reviewedBy()
{
return $this->belongsTo(User::class, 'reviewed_by');
}
} }
<?php
namespace Portal\Mileage\Model;
use Illuminate\Database\Eloquent\Model;
class LkpDepartment extends Model
{
/**
* The database table used by the model.
*
* @var string
*/
protected $table = 'lkp_department';
protected $primaryKey = 'id';
public $timestamps = false;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'code',
'description'
];
/**
* Relationship to StaffInfo (one department has many staff)
*/
public function staff()
{
return $this->hasMany(StaffInfo::class, 'fk_lkp_department', 'id');
}
}
...@@ -55,5 +55,17 @@ public function projectTeam() ...@@ -55,5 +55,17 @@ public function projectTeam()
return $this->hasMany(\Portal\Mileage\Model\ProjectTeam::class, 'fk_project_id', 'id'); return $this->hasMany(\Portal\Mileage\Model\ProjectTeam::class, 'fk_project_id', 'id');
} }
// Define relationship to the user as the project manager
public function projectManager()
{
return $this->belongsTo(User::class, 'project_manager_id');
}
// Define relationship to the user as the project director
public function projectDirector()
{
return $this->belongsTo(User::class, 'project_director_id');
}
} }
...@@ -15,6 +15,16 @@ class StaffInfo extends Model ...@@ -15,6 +15,16 @@ class StaffInfo extends Model
* @var string * @var string
*/ */
protected $table = 'staff_info'; protected $table = 'staff_info';
protected $primaryKey = 'staff_id';
public $timestamps = true;
protected $fillable = [
'staff_id',
'fk_users',
'fk_lkp_department',
'status',
];
/** /**
* undocumented function * undocumented function
...@@ -49,6 +59,15 @@ public function lpelulus() ...@@ -49,6 +59,15 @@ public function lpelulus()
return $this->belongsTo('Portal\Mileage\Model\Users','pelulus'); return $this->belongsTo('Portal\Mileage\Model\Users','pelulus');
} }
public function department()
{
return $this->belongsTo(\Portal\Mileage\Model\LkpDepartment::class, 'fk_lkp_department', 'id');
}
public function user()
{
return $this->belongsTo(\Portal\Mileage\Model\Users::class, 'fk_users', 'id');
}
......
...@@ -3,9 +3,11 @@ ...@@ -3,9 +3,11 @@
namespace Portal\Mileage\Model; namespace Portal\Mileage\Model;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
class Users extends Model class Users extends Authenticatable
{ {
use Notifiable; use Notifiable;
...@@ -15,6 +17,16 @@ class Users extends Model ...@@ -15,6 +17,16 @@ class Users extends Model
* @var string * @var string
*/ */
protected $table = 'users'; protected $table = 'users';
protected $primaryKey = 'id';
public $timestamps = true;
protected $fillable = [
'name',
'email',
'password',
'staff_id',
'status',
];
/** /**
...@@ -34,11 +46,12 @@ public function info() ...@@ -34,11 +46,12 @@ public function info()
* @return void * @return void
* @author * @author
**/ **/
public function staffinfo() public function staffInfo()
{ {
return $this->belongsTo('Portal\Mileage\Model\StaffInfo','id','fk_users'); return $this->hasOne(\Portal\Mileage\Model\StaffInfo::class, 'fk_users', 'id');
} }
public function projectTeams() public function projectTeams()
{ {
return $this->hasMany('Portal\Mileage\Model\ProjectTeam', 'pt_staff_id', 'employeeCode'); return $this->hasMany('Portal\Mileage\Model\ProjectTeam', 'pt_staff_id', 'employeeCode');
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
use DB; use DB;
use Carbon\Carbon; use Carbon\Carbon;
/** /**
* Class PackageServiceProvider * Class PackageServiceProvider
* *
...@@ -85,23 +86,47 @@ protected function menu() ...@@ -85,23 +86,47 @@ protected function menu()
->active('home/*'); ->active('home/*');
$menu->add(__('Project Manager Dashboard'), route('mileage::projectManager.index')) $menu->add(__('Project Manager Dashboard'), route('mileage::projectManager.index'))
->data('icon', 'user tie') ->data('icon', 'briefcase')
->active('projectManager/*') ->active('projectManager/*')
->data('permission', Permission::PROJECT_MANAGER); ->data('permission', Permission::PROJECT_MANAGER);
$menu->add(__('Project Director Dashboard'), route('mileage::projectDirector.index')) $menu->add(__('Project Director Dashboard'), route('mileage::projectDirector.index'))
->data('icon', 'user tie') ->data('icon', 'compass')
->active('projectDirector/*') ->active('projectDirector/*')
->data('permission', Permission::PROJECT_DIRECTOR); ->data('permission', Permission::PROJECT_DIRECTOR);
$menu->add(__('HOD Dashboard'), route('mileage::HOD.index'))
->data('icon', 'black tie')
->active('HOD/*')
->data('permission', Permission::HOD);
$menu->add(__('BOD Dashboard'), route('mileage::BOD.index'))
->data('icon', 'users cog')
->active('BOD/*')
->data('permission', Permission::BOD);
$menu->add(__('HR Dashboard'), route('mileage::HR.index'))
->data('icon', 'id badge')
->active('HR/*')
->data('permission', Permission::HR);
$menu->add(__('Finance Dashboard'), route('mileage::finance.index'))
->data('icon', 'calculator')
->active('finance/*')
->data('permission', Permission::FINANCE);
$menu = $menus->add(__('Borang Permohonan Mileage Claim'))->data('icon', 'align justify'); $menu = $menus->add(__('Borang Permohonan Mileage Claim'))->data('icon', 'align justify');
$now = Carbon::today()->format('Y-m-d'); $now = Carbon::today()->format('Y-m-d');
$menu->add(__('Permohonan Mileage Claim'), route('mileage::applyform.index')) $menu->add(__('Bawah Projek'), route('mileage::applyform.index'))
->data('icon', 'edit outline') ->data('icon', 'check')
->active('applyform/*'); ->active('applyform/*');
$menu->add(__('Tiada Projek'), route('mileage::applyform.noproject'))
->data('icon', 'close')
->active('noproject/*');
$menu = $menus->add(__('Senarai Permohonan'))->data('icon', 'align justify'); $menu = $menus->add(__('Senarai Permohonan'))->data('icon', 'align justify');
$menu->add(__('Senarai Permohonan Mileage Claim'), route('mileage::applications.index')) $menu->add(__('Senarai Permohonan Mileage Claim'), route('mileage::applications.index'))
->data('icon', 'bars') ->data('icon', 'bars')
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
<title>@yield('site.title', "Welcome Home") | {{ config('app.name') }}</title> <title>@yield('site.title', "Welcome Home") | {{ config('app.name') }}</title>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<meta name="csrf-token" content="{{ csrf_token() }}">
<meta http-equiv="x-ua-compatible" content="IE=edge, chrome=1"/> <meta http-equiv="x-ua-compatible" content="IE=edge, chrome=1"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
@stack('meta') @stack('meta')
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment