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');
......
...@@ -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
<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
...@@ -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>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
...@@ -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
...@@ -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 [];
}
}
...@@ -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,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