Commit b0d5b251 by Hannah Zahra

Second Update

parent caf2415d
...@@ -16,12 +16,6 @@ final class Permission extends Enum ...@@ -16,12 +16,6 @@ final class Permission extends Enum
const LULUS = 'LULUS'; const LULUS = 'LULUS';
const MAKLUMAT_CUTI = 'MAKLUMAT_CUTI'; const MAKLUMAT_CUTI = 'MAKLUMAT_CUTI';
const UPDATE_ACL = 'UPDATE_ACL'; const UPDATE_ACL = 'UPDATE_ACL';
const BATAL_CUTI = 'BATAL_CUTI';
const SETTING_EMAIL = 'SETTING_EMAIL';
const SETTING_PLAN = 'SETTING_PLAN';
const APPLY_PLAN = 'APPLY_PLAN';
const APPLY_GCR = 'APPLY_GCR';
const KUOTA = 'KUOTA';
const MANAGE_USER = 'epicentrum::manage-user'; const MANAGE_USER = 'epicentrum::manage-user';
const MANAGE_ROLE = 'epicentrum::manage-role'; const MANAGE_ROLE = 'epicentrum::manage-role';
...@@ -31,6 +25,8 @@ final class Permission extends Enum ...@@ -31,6 +25,8 @@ final class Permission extends Enum
const APPLY_CLAIM = 'APPLY_CLAIM'; const APPLY_CLAIM = 'APPLY_CLAIM';
const HRVIEW = "HRVIEW"; const HRVIEW = "HRVIEW";
const TRY_PERMISSION = "TRY_PERMISSION"; const TRY_PERMISSION = "TRY_PERMISSION";
const PROJECT_MANAGER = "PROJECT_MANAGER";
const PROJECT_DIRECTOR = "PROJECT_DIRECTOR";
//should added to in table:acl_permission //should added to in table:acl_permission
} }
...@@ -14,6 +14,6 @@ public function __invoke(Request $request): RedirectResponse ...@@ -14,6 +14,6 @@ public function __invoke(Request $request): RedirectResponse
$request->session()->invalidate(); $request->session()->invalidate();
$request->session()->regenerateToken(); $request->session()->regenerateToken();
return redirect('/'); return redirect('/auth/login');
} }
} }
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
namespace App\Models; namespace App\Models;
use Portal\Mileage\Model\Project;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Laravolt\Suitable\AutoFilter; use Laravolt\Suitable\AutoFilter;
...@@ -22,4 +25,21 @@ class User extends \Laravolt\Platform\Models\User ...@@ -22,4 +25,21 @@ class User extends \Laravolt\Platform\Models\User
protected $hidden = ['password', 'remember_token']; protected $hidden = ['password', 'remember_token'];
protected $fillable = ['name', 'email', 'username', 'password', 'status', 'timezone']; protected $fillable = ['name', 'email', 'username', 'password', 'status', 'timezone'];
public function managedProjects(): BelongsToMany
{
return $this->belongsToMany(
Project::class,
'project_team', // pivot table
'pt_staff_id', // user id column
'fk_project_id' // project id column
)->withPivot('role_id')
->wherePivot('role_id', 1); // filter only project managers
}
public function aclRoles()
{
return $this->hasMany(\Portal\Mileage\Model\AclRoleUser::class, 'user_id', 'employeeCode');
}
} }
...@@ -12,6 +12,10 @@ public function up() ...@@ -12,6 +12,10 @@ public function up()
$table->id(); $table->id();
$table->date('claim_date'); $table->date('claim_date');
// User FK
$table->unsignedBigInteger('user_id')->nullable(); // nullable if some claims don't have a user yet
$table->foreign('user_id')->references('id')->on('users')->onDelete('set null');
// 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');
...@@ -37,6 +41,9 @@ public function up() ...@@ -37,6 +41,9 @@ 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');
$table->foreign('rejected_by')->references('id')->on('users')->onDelete('set null');
// Claim status (workflow) // Claim status (workflow)
$table->enum('status', [ $table->enum('status', [
'Diproses', // submitted by staff 'Diproses', // submitted by staff
......
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('project_user', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('project_id');
$table->unsignedBigInteger('user_id');
$table->unsignedInteger('project_role_id');
$table->timestamps();
$table->foreign('project_id')->references('id')->on('projects')->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->unique(['project_id', 'user_id']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('project_user');
}
};
<title>Senarai Tuntutan</title>
@extends('ui::layouts.app') @extends('ui::layouts.app')
@section('content') @section('content')
...@@ -45,7 +46,7 @@ ...@@ -45,7 +46,7 @@
<div id="loader"><div></div></div> <div id="loader"><div></div></div>
<div class="ui container"> <div class="ui container">
<h2 class="ui header">All Claims</h2> <h2 class="ui header">Senarai Tuntutan</h2>
@if(session('success')) @if(session('success'))
<div class="ui positive message"> <div class="ui positive message">
...@@ -53,6 +54,8 @@ ...@@ -53,6 +54,8 @@
</div> </div>
@endif @endif
@foreach($claimsByMonth as $month => $claims)
<h3 class="ui dividing header">{{ $month }}</h3>
<table class="ui celled table"> <table class="ui celled table">
<thead> <thead>
<tr> <tr>
...@@ -60,7 +63,8 @@ ...@@ -60,7 +63,8 @@
<th>Tarikh Tuntutan</th> <th>Tarikh Tuntutan</th>
<th>Nama Projek</th> <th>Nama Projek</th>
<th>Jenis Kenderaan</th> <th>Jenis Kenderaan</th>
<th>Jumlah Tuntutan (RM)</th> <th>Jumlah Perbatuan</th>
<th>Jumlah Tuntutan</th>
<th>Status</th> <th>Status</th>
<th>Tindakan</th> <th>Tindakan</th>
</tr> </tr>
...@@ -72,12 +76,14 @@ ...@@ -72,12 +76,14 @@
<td>{{ $claim->claim_date }}</td> <td>{{ $claim->claim_date }}</td>
<td>{{ $claim->project->p_project_description ?? '-' }}</td> <td>{{ $claim->project->p_project_description ?? '-' }}</td>
<td>{{ $claim->jenisKenderaan->name ?? '-' }}</td> <td>{{ $claim->jenisKenderaan->name ?? '-' }}</td>
<td>{{ $claim->total_mileage ?? 0 }} km</td>
<td>RM {{ number_format($claim->total_claim_amount, 2) }}</td> <td>RM {{ number_format($claim->total_claim_amount, 2) }}</td>
<td>{{ $claim->status }}</td> <td>{{ $claim->status }}</td>
<td> <td>
<a href="{{ route('mileage::applications.show', $claim->id) }}" class="ui blue button tiny"> <a href="{{ route('mileage::applications.show', $claim->id) }}" class="ui blue button tiny">
Lihat Lihat
</a> </a>
@if($claim->status === 'Diproses')
<!-- Edit button (opens modal) --> <!-- Edit button (opens modal) -->
<button class="ui yellow button tiny editBtn" style="margin-bottom: 10px" <button class="ui yellow button tiny editBtn" style="margin-bottom: 10px"
data-id="{{ $claim->id }}" data-id="{{ $claim->id }}"
...@@ -85,26 +91,30 @@ ...@@ -85,26 +91,30 @@
data-project="{{ $claim->project_id }}" data-project="{{ $claim->project_id }}"
data-vehicle="{{ $claim->jenis_kenderaan_id }}" data-vehicle="{{ $claim->jenis_kenderaan_id }}"
data-description="{{ $claim->description }}" data-description="{{ $claim->description }}"
data-distanceto="{{ $claim->distance_to }}"
data-mileage="{{ $claim->total_mileage }}" data-mileage="{{ $claim->total_mileage }}"
data-toll="{{ $claim->toll_amount }}" data-toll="{{ $claim->toll_amount }}"
data-others="{{ $claim->others_amount }}" data-others="{{ $claim->others_amount }}"
data-othersdesc="{{ $claim->others_description }}"> data-othersdesc="{{ $claim->others_description }}">
Kemaskini Kemaskini
</button> </button>
<span>
<!-- Delete button --> <!-- Delete button -->
<form action="{{ route('mileage::applications.destroy', $claim->id) }}" method="POST" style="display:inline;"> <form action="{{ route('mileage::applications.destroy', $claim->id) }}" method="POST" style="display:inline;">
@csrf @csrf
@method('DELETE') @method('DELETE')
<button type="submit" class="ui red button tiny" onclick="return confirm('Are you sure you want to delete this claim?')">Padam</button> <button type="submit" class="ui red button tiny" onclick="return confirm('Are you sure you want to delete this claim?')">Padam</button>
</form> </form>
@endif
</td> </td>
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>
</table> </table>
@endforeach
</div> </div>
<div id="loader"><div></div></div>
<!-- Edit Modal --> <!-- Edit Modal -->
<div id="editModal" class="ui modal"> <div id="editModal" class="ui modal">
<i class="close icon"></i> <i class="close icon"></i>
...@@ -145,6 +155,11 @@ ...@@ -145,6 +155,11 @@
</div> </div>
<div class="field"> <div class="field">
<label>Jarak Ke</label>
<input type="text" name="distance_to" id="editDistanceTo">
</div>
<div class="field">
<label>Jumlah Perbatuan (KM)</label> <label>Jumlah Perbatuan (KM)</label>
<input type="number" name="total_mileage" id="editDistance"> <input type="number" name="total_mileage" id="editDistance">
</div> </div>
...@@ -164,7 +179,7 @@ ...@@ -164,7 +179,7 @@
<input type="text" name="others_description" id="editOthersDescription"> <input type="text" name="others_description" id="editOthersDescription">
</div> </div>
<button type="submit" class="ui green button">Update</button> <button type="submit" class="ui green button">Kemaskini</button>
</form> </form>
</div> </div>
</div> </div>
...@@ -196,6 +211,7 @@ ...@@ -196,6 +211,7 @@
let project = $btn.data('project'); let project = $btn.data('project');
let vehicle = $btn.data('vehicle'); let vehicle = $btn.data('vehicle');
let description = $btn.data('description'); let description = $btn.data('description');
let distanceTo = $btn.data('distanceto');
let mileage = $btn.data('mileage'); let mileage = $btn.data('mileage');
let toll = $btn.data('toll'); let toll = $btn.data('toll');
let others = $btn.data('others'); let others = $btn.data('others');
...@@ -205,6 +221,7 @@ ...@@ -205,6 +221,7 @@
$('#editId').val(id); $('#editId').val(id);
$('#editDate').val(date || ''); $('#editDate').val(date || '');
$('#editDescription').val(description || ''); $('#editDescription').val(description || '');
$('#editDistanceTo').val(distanceTo || '');
$('#editDistance').val(mileage ?? ''); $('#editDistance').val(mileage ?? '');
$('#editToll').val(toll ?? ''); $('#editToll').val(toll ?? '');
$('#editOthers').val(others ?? ''); $('#editOthers').val(others ?? '');
...@@ -233,6 +250,7 @@ ...@@ -233,6 +250,7 @@
let project = $('#editProject').val(); let project = $('#editProject').val();
let vehicle = $('#editVehicle').val(); let vehicle = $('#editVehicle').val();
let description = $('#editDescription').val(); let description = $('#editDescription').val();
let distanceTo = $('#editDistanceTo').val();
let mileage = $('#editDistance').val(); let mileage = $('#editDistance').val();
let toll = $('#editToll').val(); let toll = $('#editToll').val();
let others = $('#editOthers').val(); let others = $('#editOthers').val();
......
...@@ -69,21 +69,31 @@ ...@@ -69,21 +69,31 @@
</tr> </tr>
<tr> <tr>
<td>Penalti (RM) (Jika Ada)</td> <td>Penalti (RM) (Jika Ada)</td>
@if ($claim->penalty_amount > 0) <td>
<td>RM {{ number_format($claim->penalty_amount, 2) }}</td> @if(isset($claim->display_penalty_amount))
@elseif (Str::contains($claim->penalty_reason, 'Budi Bicara')) @if($claim->display_penalty_amount > 0)
<td><span style="color: orange;">Tertakluk kepada BOD</span></td> RM {{ number_format($claim->display_penalty_amount, 2) }}
@else @else
<td>RM 0.00</td> @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 @endif
</td>
</tr> </tr>
<tr> <tr>
<td>Sebab Penalti</td> <td>Sebab Penalti</td>
<td>{{ $claim->penalty_reason ?? '-' }}</td> <td>
{{ $claim->display_penalty_reason ?? $claim->penalty_reason ?? '-' }}
</td>
</tr> </tr>
<tr> <tr>
<td><strong>Jumlah Keseluruhan Tuntutan (RM)</strong></td> <td><strong>Jumlah Keseluruhan Tuntutan (RM)</strong></td>
<td><strong>RM {{ number_format($claim->total_claim_amount ?? 0, 2) }}</strong></td> <td><strong>RM {{ number_format($claim->total_claim_amount ?? $claim->total_claim_amount ?? 0, 2) }}</strong></td>
</tr> </tr>
<tr> <tr>
<td><strong>Status</strong></td> <td><strong>Status</strong></td>
......
<title>Form Tuntutan</title>
@extends('ui::layouts.app') @extends('ui::layouts.app')
@push('style') @push('style')
...@@ -95,7 +96,7 @@ ...@@ -95,7 +96,7 @@
</div> </div>
<div class="inline field required"> <div class="inline field required">
<label class="three-wide field">Nama Projek<font color="red">*</font></label> <label class="three-wide field">Nama Projek<font color="red">*</font></label>
<select class="ui search dropdown eight wide field" name="project_id" id="project_select" required> <select class="ui search dropdown eight wide field form-control" name="project_id" id="project_select" required>
<option value="">Sila Pilih Projek</option> <option value="">Sila Pilih Projek</option>
@foreach($projects as $project) @foreach($projects as $project)
<option value="{{ $project->id }}">{{ $project->p_project_description }}</option> <option value="{{ $project->id }}">{{ $project->p_project_description }}</option>
...@@ -110,7 +111,7 @@ ...@@ -110,7 +111,7 @@
<label class="three-wide field">Jarak Dari</label> <label class="three-wide field">Jarak Dari</label>
<select class="ui search dropdown eight wide field" name="distance_from"> <select class="ui search dropdown eight wide field" name="distance_from">
<option value="">Sila Pilih Lokasi</option> <option value="">Sila Pilih Lokasi</option>
<option value="office">Pejabat</option> <option value="Pejabat">Pejabat</option>
</select> </select>
</div> </div>
<div class="inline field required"> <div class="inline field required">
...@@ -139,7 +140,7 @@ ...@@ -139,7 +140,7 @@
{{-- Rumusan Tuntutan --}} {{-- Rumusan Tuntutan --}}
<div class="ui segment"> <div class="ui segment">
<p>Rumusan Tuntutan</p> <p>Rumusan Pengiraan Tuntutan</p>
<div class="ui segment"> <div class="ui segment">
<table class="ui celled table"> <table class="ui celled table">
<tbody> <tbody>
...@@ -148,7 +149,7 @@ ...@@ -148,7 +149,7 @@
<td id="claimable_mileage">0 KM</td> <td id="claimable_mileage">0 KM</td>
</tr> </tr>
<tr> <tr>
<td>Jumlah Tuntutan Perjalanan (RM)</td> <td>Jumlah Tuntutan Perjalanan <span id="mileage_rate_label"></span></td>
<td id="mileage_claim_amount">RM 0.00</td> <td id="mileage_claim_amount">RM 0.00</td>
</tr> </tr>
<tr> <tr>
...@@ -177,11 +178,15 @@ ...@@ -177,11 +178,15 @@
</div> </div>
<!-- Hidden inputs to send calculated values to server --> <!-- Hidden inputs to send calculated values to server -->
<input type="hidden" name="user_id" id="user_id_input" value="{{ auth()->user()->id }}">
<!-- <input type="hidden" name="staff_id" id="staff_id_input" value="{{ auth()->user()->employeeCode }}"> -->
<input type="hidden" name="claimable_mileage" id="claimable_mileage_input" value="0"> <input type="hidden" name="claimable_mileage" id="claimable_mileage_input" value="0">
<input type="hidden" name="mileage_claim_amount" id="mileage_claim_amount_input" value="0.00"> <input type="hidden" name="mileage_claim_amount" id="mileage_claim_amount_input" value="0.00">
<input type="hidden" name="total_claim_amount" id="total_claim_amount_input" value="0.00"> <input type="hidden" name="total_claim_amount" id="total_claim_amount_input" value="0.00">
<input type="hidden" name="toll_amount" id="toll_amount_input" value="0.00"> <input type="hidden" name="toll_amount" id="toll_amount_input" value="0.00">
<input type="hidden" name="others_amount" id="others_amount_input" value="0.00"> <input type="hidden" name="others_amount" id="others_amount_input" value="0.00">
<input type="hidden" name="penalty_amount" id="penalty_amount_input" value="0.00">
<input type="hidden" name="penalty_reason" id="penalty_reason_input" value="-">
<button class="ui primary button blue" type="submit" id="btn_hantar">Seterusnya</button> <button class="ui primary button blue" type="submit" id="btn_hantar">Seterusnya</button>
</form> </form>
...@@ -247,12 +252,38 @@ function calculateTotalClaim() { ...@@ -247,12 +252,38 @@ function calculateTotalClaim() {
if (vehicleRates[vehicleType]) { if (vehicleRates[vehicleType]) {
claimableMileage = Math.max(0, mileage - 40); claimableMileage = Math.max(0, mileage - 40);
if (vehicleRates[vehicleType].type === 'Kereta') { if (vehicleRates[vehicleType].type === 'Kereta') {
rate = (claimableMileage <= 500) ? vehicleRates[vehicleType].baseRate : vehicleRates[vehicleType].higherMileageRate; if (claimableMileage <= 500) {
mileageClaimAmount = claimableMileage * vehicleRates[vehicleType].baseRate;
} else {
mileageClaimAmount = (500 * vehicleRates[vehicleType].baseRate) +
((claimableMileage - 500) * vehicleRates[vehicleType].higherMileageRate);
}
} else { } else {
rate = vehicleRates[vehicleType].baseRate; // Motosikal - flat rate
mileageClaimAmount = claimableMileage * vehicleRates[vehicleType].baseRate;
}
}
let rateLabel = "";
if (vehicleRates[vehicleType]) {
claimableMileage = Math.max(0, mileage - 40);
if (vehicleRates[vehicleType].type === 'Kereta') {
if (claimableMileage <= 500) {
mileageClaimAmount = claimableMileage * vehicleRates[vehicleType].baseRate;
rateLabel = `(RM ${vehicleRates[vehicleType].baseRate.toFixed(2)}/km)`;
} else {
mileageClaimAmount = (500 * vehicleRates[vehicleType].baseRate) +
((claimableMileage - 500) * vehicleRates[vehicleType].higherMileageRate);
rateLabel = `(RM ${vehicleRates[vehicleType].baseRate.toFixed(2)}/km first 500km, RM ${vehicleRates[vehicleType].higherMileageRate.toFixed(2)}/km after)`;
}
} else {
mileageClaimAmount = claimableMileage * vehicleRates[vehicleType].baseRate;
rateLabel = `(RM ${vehicleRates[vehicleType].baseRate.toFixed(2)}/km)`;
} }
mileageClaimAmount = claimableMileage * rate;
} }
// --- Penalty calculation --- // --- Penalty calculation ---
...@@ -269,20 +300,22 @@ function calculateTotalClaim() { ...@@ -269,20 +300,22 @@ function calculateTotalClaim() {
if (monthsDiff <= 1) { if (monthsDiff <= 1) {
penalty = 0; penalty = 0;
penaltyReason = "-"; penaltyReason = "-";
$('#btn_hantar').prop('disabled', false);
} else if (monthsDiff > 1 && monthsDiff <= 3) { } else if (monthsDiff > 1 && monthsDiff <= 3) {
penalty = mileageClaimAmount * 0.10; penalty = mileageClaimAmount * 0.10;
penaltyReason = "Lewat hantar lebih 1 bulan tetapi ≤ 3 bulan (10%)"; penaltyReason = "Lewat hantar lebih 1 bulan tetapi ≤ 3 bulan (10%)";
$('#btn_hantar').prop('disabled', false);
} else if (monthsDiff > 3 && monthsDiff <= 6) { } else if (monthsDiff > 3 && monthsDiff <= 6) {
penalty = mileageClaimAmount * 0.30; penalty = mileageClaimAmount * 0.30;
penaltyReason = "Lewat hantar lebih 3 bulan tetapi ≤ 6 bulan (30%)"; penaltyReason = "Lewat hantar lebih 3 bulan tetapi ≤ 6 bulan (30%)";
$('#btn_hantar').prop('disabled', false);
} else if (monthsDiff > 6) { } else if (monthsDiff > 6) {
penaltyReason = "Lewat lebih 6 bulan - Tertakluk kepada budi bicara Lembaga Pengarah"; penaltyReason = "Lewat lebih 6 bulan - Tertakluk kepada budi bicara Lembaga Pengarah";
// prevent submission for >6 months
$('#btn_hantar').prop('disabled', true); $('#btn_hantar').prop('disabled', true);
showCustomAlert("Tuntutan lebih daripada 6 bulan tidak boleh dihantar. Sila rujuk Lembaga Pengarah."); showCustomAlert("Tuntutan lebih daripada 6 bulan tidak boleh dihantar. Sila rujuk Lembaga Pengarah.");
} else {
$('#btn_hantar').prop('disabled', false);
} }
} }
const totalClaim = mileageClaimAmount + toll + others - penalty; const totalClaim = mileageClaimAmount + toll + others - penalty;
...@@ -290,6 +323,7 @@ function calculateTotalClaim() { ...@@ -290,6 +323,7 @@ function calculateTotalClaim() {
// Update visible summary // Update visible summary
$('#claimable_mileage').text(claimableMileage.toFixed(2) + ' KM'); $('#claimable_mileage').text(claimableMileage.toFixed(2) + ' KM');
$('#mileage_claim_amount').text('RM ' + mileageClaimAmount.toFixed(2)); $('#mileage_claim_amount').text('RM ' + mileageClaimAmount.toFixed(2));
$('#mileage_rate_label').text(rateLabel);
$('#toll_claim_amount').text('RM ' + toll.toFixed(2)); $('#toll_claim_amount').text('RM ' + toll.toFixed(2));
$('#others_claim_amount').text('RM ' + others.toFixed(2)); $('#others_claim_amount').text('RM ' + others.toFixed(2));
$('#penalty_claim_amount').text('RM ' + penalty.toFixed(2)); $('#penalty_claim_amount').text('RM ' + penalty.toFixed(2));
...@@ -302,6 +336,8 @@ function calculateTotalClaim() { ...@@ -302,6 +336,8 @@ function calculateTotalClaim() {
$('#toll_amount_input').val(toll.toFixed(2)); $('#toll_amount_input').val(toll.toFixed(2));
$('#others_amount_input').val(others.toFixed(2)); $('#others_amount_input').val(others.toFixed(2));
$('#total_claim_amount_input').val(totalClaim.toFixed(2)); $('#total_claim_amount_input').val(totalClaim.toFixed(2));
$('#penalty_amount_input').val(penalty.toFixed(2));
$('#penalty_reason_input').val(penaltyReason);
} }
...@@ -346,7 +382,7 @@ function calculateTotalClaim() { ...@@ -346,7 +382,7 @@ function calculateTotalClaim() {
const totalClaim = $('#total_claim_amount_input').val() || '0.00'; const totalClaim = $('#total_claim_amount_input').val() || '0.00';
const summaryHtml = ` const summaryHtml = `
<div class="ui mini modal" id="claimSummaryModal"> <div class="ui large modal" id="claimSummaryModal">
<div class="header">Ringkasan Tuntutan Anda</div> <div class="header">Ringkasan Tuntutan Anda</div>
<div class="content"> <div class="content">
<p><strong>Tarikh:</strong> ${claimDate}</p> <p><strong>Tarikh:</strong> ${claimDate}</p>
......
<title>Project Director 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);
}
@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">Project Director Dashboard</h2>
{{-- Pending Claims --}}
<h3 class="ui dividing header">Pending Claims</h3>
<table class="ui celled table">
<thead>
<tr>
<th>#</th>
<th>Staff</th>
<th>Date</th>
<th>Project</th>
<th>Mileage</th>
<th>Total</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
@php
$pendingClaims = $claims->where('status', 'Disokong');
@endphp
@forelse($pendingClaims 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>
<td>
<a href="{{ route('mileage::projectDirector.show', $claim->id) }}" class="ui blue button tiny">
Lihat
</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>
</tr>
@empty
<tr>
<td colspan="8" class="center aligned">No pending claims</td>
</tr>
@endforelse
</tbody>
</table>
{{-- Previous Claims --}}
<h3 class="ui dividing header mt-5">Previous Claims</h3>
<table class="ui celled table">
<thead>
<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
<tr>
<td colspan="7" class="center aligned">No previous claims found</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@endsection
@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);
}
</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>{{ $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>
<td>Sebab Penalti</td>
<td>
{{ $claim->display_penalty_reason ?? $claim->penalty_reason ?? '-' }}
</td>
</tr>
<tr>
<td><strong>Jumlah Keseluruhan Tuntutan (RM)</strong></td>
<td><strong>RM {{ number_format($claim->total_claim_amount ?? $claim->total_claim_amount ?? 0, 2) }}</strong></td>
</tr>
<tr>
<td><strong>Status</strong></td>
<td><strong>{{ $claim->status }}</strong></td>
</tr>
</table>
</div>
<a href="{{ route('mileage::projectDirector.index') }}" class="ui button">
Kembali
</a>
</div>
@endsection
<title>Project Manager 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);
}
@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 id="loader"><div></div></div>
<div class="ui container">
<h2 class="ui header">Project Manager Dashboard</h2>
{{-- Pending Claims --}}
<h3 class="ui dividing header">Pending Claims</h3>
<table class="ui celled table">
<thead>
<tr>
<th>#</th>
<th>Staff</th>
<th>Date</th>
<th>Project</th>
<th>Distance</th>
<th>Total</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
@forelse($claims->where('status', 'Diproses') 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>
<td>
<a href="{{ route('mileage::projectManager.show', $claim->id) }}" class="ui blue button tiny">
Lihat
</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>
</tr>
@empty
<tr>
<td colspan="7">No pending claims.</td>
</tr>
@endforelse
</tbody>
</table>
{{-- Previous Claims --}}
<h3 class="ui dividing header">Previous Claims</h3>
<table class="ui celled table">
<thead>
<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
<tr>
<td colspan="6">No previous claims.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@endsection
@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);
}
</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>{{ $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>
<td>Sebab Penalti</td>
<td>
{{ $claim->display_penalty_reason ?? $claim->penalty_reason ?? '-' }}
</td>
</tr>
<tr>
<td><strong>Jumlah Keseluruhan Tuntutan (RM)</strong></td>
<td><strong>RM {{ number_format($claim->total_claim_amount ?? $claim->total_claim_amount ?? 0, 2) }}</strong></td>
</tr>
<tr>
<td><strong>Status</strong></td>
<td><strong>{{ $claim->status }}</strong></td>
</tr>
</table>
</div>
<a href="{{ route('mileage::projectManager.index') }}" class="ui button">
Kembali
</a>
</div>
@endsection
...@@ -3,38 +3,48 @@ ...@@ -3,38 +3,48 @@
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Portal\Mileage\Http\Controllers\ApplyformController; use Portal\Mileage\Http\Controllers\ApplyformController;
use Portal\Mileage\Http\Controllers\ApplicationsController; use Portal\Mileage\Http\Controllers\ApplicationsController;
use Portal\Mileage\Http\Controllers\ProjectManagerController;
use Portal\Mileage\Http\Controllers\ProjectDirectorController;
Route::group( Route::group(
[ [
'namespace' => '\Portal\Mileage\Http\Controllers',
'prefix' => '', 'prefix' => '',
'as' => 'mileage::', 'as' => 'mileage::',
'middleware' => ['web', 'auth'], 'middleware' => ['web', 'auth'],
], ],
function () { function () {
// resource for applyform // Applyform
Route::resource('applyform', ApplyformController::class); Route::resource('applyform', ApplyformController::class);
// resource for application // Applications
Route::resource('applications', ApplicationsController::class); Route::resource('applications', ApplicationsController::class);
Route::put('applications/{id}/updateStatus', [ApplicationsController::class, 'updateStatus'])
->name('applications.updateStatus');
// Project Manager
Route::prefix('projectManager')->name('projectManager.')->group(function () {
Route::get('/', [ProjectManagerController::class, 'index'])->name('index');
Route::get('/show/{id}', [ProjectManagerController::class, 'show'])->name('show');
});
// Project Director
Route::prefix('projectDirector')->name('projectDirector.')->group(function () {
Route::get('/dashboard', [ProjectDirectorController::class, 'index'])->name('index');
Route::get('/show/{id}', [ProjectDirectorController::class, 'show'])->name('show');
Route::post('/{id}/approve', [ProjectDirectorController::class, 'approve'])->name('approve');
Route::post('/{id}/reject', [ProjectDirectorController::class, 'reject'])->name('reject');
});
} }
); );
// Guest group (currently unused)
Route::group(
Route::group(
[ [
'namespace' => '\Portal\Mileage\Http\Controllers',
'prefix' => '', 'prefix' => '',
'as' => 'mileage::', 'as' => 'mileage::',
'middleware' => ['guest'], 'middleware' => ['guest'],
], ],
function () { function () {
//
} }
); );
...@@ -20,17 +20,34 @@ public function __construct() ...@@ -20,17 +20,34 @@ public function __construct()
public function index() public function index()
{ {
// Show all claims $user = auth()->user();
$claims = Claim::with(['project', 'jenisKenderaan'])->latest()->get();
// Base query with relationships
$query = Claim::with(['project', 'jenisKenderaan'])->latest();
// Restrict for Project Manager
if ($user->managedProjects()->exists()) {
$projectIds = $user->managedProjects()->pluck('fk_project_id');
$query->whereIn('project_id', $projectIds);
}
$claims = $query->get();
// Group by month-year (example: "2025-10")
$claimsByMonth = $claims->groupBy(function($claim) {
return \Carbon\Carbon::parse($claim->claim_date)->format('F Y'); // "October 2025"
});
$projects = Project::all(); $projects = Project::all();
$jenisKenderaan = JenisKenderaan::all(); $jenisKenderaan = JenisKenderaan::all();
return view('mileage::application.index', compact('claims', 'projects', 'jenisKenderaan')); return view('mileage::application.index', compact('claimsByMonth', 'projects', 'jenisKenderaan'));
} }
public function store(Request $request) public function store(Request $request)
{ {
$validated = $request->validate([ $validated = $request->validate([
'user_id' => 'required',
'claim_date' => 'required|date|before_or_equal:today', 'claim_date' => 'required|date|before_or_equal:today',
'jenis_kenderaan_id' => 'required|exists:lkp_jenis_kenderaans,id', 'jenis_kenderaan_id' => 'required|exists:lkp_jenis_kenderaans,id',
'project_id' => 'required|exists:lkp_project,id', 'project_id' => 'required|exists:lkp_project,id',
...@@ -59,31 +76,35 @@ public function store(Request $request) ...@@ -59,31 +76,35 @@ public function store(Request $request)
$validated['calculated_amount'] = $calculatedAmount; $validated['calculated_amount'] = $calculatedAmount;
$validated['claimable_mileage'] = $claimableMileage; $validated['claimable_mileage'] = $claimableMileage;
// penalty // penalty calculation using DAYS (submission time = now)
$claimDate = Carbon::parse($validated['claim_date']); $claimDate = Carbon::parse($validated['claim_date']);
$monthsDiff = $claimDate->diffInMonths(Carbon::now()); $submissionDate = Carbon::now();
$daysDiff = $claimDate->diffInDays($submissionDate);
if ($monthsDiff <= 3) { if ($daysDiff <= 30) {
$penalty = 0; $penalty = 0;
$penaltyReason = 'Tiada penalti (≤ 3 bulan)'; $penaltyReason = 'Tiada penalti (≤ 1 bulan)';
} elseif ($monthsDiff <= 6) { } elseif ($daysDiff <= 90) { // >30 && <=90
$penalty = $calculatedAmount * 0.10; $penalty = $calculatedAmount * 0.10;
$penaltyReason = 'Lewat hantar ≤ 6 bulan (10%)'; $penaltyReason = 'Lewat hantar lebih 1 bulan tetapi ≤ 3 bulan (10%)';
} else { } elseif ($daysDiff <= 180) { // >90 && <=180
$penalty = $calculatedAmount * 0.30; $penalty = $calculatedAmount * 0.30;
$penaltyReason = 'Lewat hantar > 6 bulan (30%)'; $penaltyReason = 'Lewat hantar lebih 3 bulan tetapi ≤ 6 bulan (30%)';
} else { // >180
$penalty = 0; // no numeric penalty applied automatically
$penaltyReason = 'Lewat lebih 6 bulan - Tertakluk kepada budi bicara BOD';
} }
$validated['penalty_amount'] = $penalty; $validated['penalty_amount'] = $penalty;
$validated['penalty_reason'] = $penaltyReason; $validated['penalty_reason'] = $penaltyReason;
$validated['total_claim_amount'] = $calculatedAmount - $penalty; $validated['total_claim_amount'] = $calculatedAmount - $penalty;
$validated['status'] = 'Diproses'; // default status when first submitted // default status
$validated['status'] = 'Diproses';
// IMPORTANT: assign created model to $claim so we can return/use it // create
$claim = Claim::create($validated); $claim = Claim::create($validated);
// If front-end sent AJAX and expects JSON with claim data:
if ($request->ajax()) { if ($request->ajax()) {
return response()->json([ return response()->json([
'success' => true, 'success' => true,
...@@ -107,26 +128,26 @@ public function edit($id) ...@@ -107,26 +128,26 @@ public function edit($id)
public function update(Request $request, $id) public function update(Request $request, $id)
{ {
$claim = Claim::findOrFail($id); $claim = Claim::findOrFail($id);
// decide new status based on role (if you want automated transitions)
$newStatus = null;
if (auth()->check()) {
if (auth()->user()->hasRole('Project Manager')) { if (auth()->user()->hasRole('Project Manager')) {
$claim->status = 'Disokong'; $newStatus = 'Disokong';
} elseif (auth()->user()->hasRole('HOD')) {
$newStatus = 'Disokong';
} elseif (auth()->user()->hasRole('Project Director')) {
$newStatus = 'Disahkan';
} elseif (auth()->user()->hasRole('Finance')) {
$newStatus = 'Disemak';
} elseif (auth()->user()->hasRole('Finance Director')) {
$newStatus = 'Diluluskan';
} }
elseif (auth()->user()->hasRole('HOD')) {
$claim->status = 'Disokong';
}
elseif (auth()->user()->hasRole('Project Director')) {
$claim->status = 'Disahkan';
}
elseif (auth()->user()->hasRole('Finance')) {
$claim->status = 'Disemak';
}
elseif (auth()->user()->hasRole('Finance Director')) {
$claim->status = 'Diluluskan';
} }
$validated = $request->validate([ $validated = $request->validate([
'user_id' => 'required',
'claim_date' => 'required|date', 'claim_date' => 'required|date',
'jenis_kenderaan_id' => 'required|exists:lkp_jenis_kenderaans,id', 'jenis_kenderaan_id' => 'required|exists:lkp_jenis_kenderaans,id',
'project_id' => 'required|exists:lkp_project,id', 'project_id' => 'required|exists:lkp_project,id',
...@@ -155,31 +176,35 @@ public function update(Request $request, $id) ...@@ -155,31 +176,35 @@ public function update(Request $request, $id)
$validated['calculated_amount'] = $calculatedAmount; $validated['calculated_amount'] = $calculatedAmount;
$validated['claimable_mileage'] = $claimableMileage; $validated['claimable_mileage'] = $claimableMileage;
// penalty // penalty calculation using DAYS (submission time = now)
$claimDate = Carbon::parse($validated['claim_date']); $claimDate = Carbon::parse($validated['claim_date']);
$now = Carbon::now(); $submissionDate = Carbon::now();
$monthsDiff = $claimDate->diffInMonths($now); $daysDiff = $claimDate->diffInDays($submissionDate);
if ($monthsDiff < 1) { if ($daysDiff <= 30) {
$penalty = 0; $penalty = 0;
$penaltyReason = 'Tiada penalti (≤ 1 bulan)'; $penaltyReason = 'Tiada penalti (≤ 1 bulan)';
} elseif ($monthsDiff <= 3) { } elseif ($daysDiff <= 90) {
$penalty = $calculatedAmount * 0.10; $penalty = $calculatedAmount * 0.10;
$penaltyReason = 'Lewat hantar > 1 bulan & ≤ 3 bulan (10% Penalti)'; $penaltyReason = 'Lewat hantar >1 ≤3 bulan (10%)';
} elseif ($monthsDiff <= 6) { } elseif ($daysDiff <= 180) {
$penalty = $calculatedAmount * 0.30; $penalty = $calculatedAmount * 0.30;
$penaltyReason = 'Lewat hantar > 3 bulan & ≤ 6 bulan (30% Penalti)'; $penaltyReason = 'Lewat hantar >3 ≤6 bulan (30%)';
} else { } else {
$penalty = 0; $penalty = 0;
$penaltyReason = 'Melebihi 6 bulan (Tertakluk kepada budi bicara Lembaga Pengarah)'; $penaltyReason = 'Tertakluk kepada BOD';
// Optionally: $validated['status'] = 'Untuk Kelulusan BOD';
} }
$validated['penalty_amount'] = $penalty; $validated['penalty_amount'] = $penalty;
$validated['penalty_reason'] = $penaltyReason; $validated['penalty_reason'] = $penaltyReason;
$validated['total_claim_amount'] = $calculatedAmount - $penalty; $validated['total_claim_amount'] = $calculatedAmount - $penalty;
// update the model // apply automated status change if applicable
if ($newStatus) {
$validated['status'] = $newStatus;
}
// update
$claim->update($validated); $claim->update($validated);
if ($request->ajax()) { if ($request->ajax()) {
...@@ -203,8 +228,77 @@ public function destroy($id) ...@@ -203,8 +228,77 @@ public function destroy($id)
public function show($id) public function show($id)
{ {
// load claim
$claim = Claim::with(['project', 'jenisKenderaan'])->findOrFail($id); $claim = Claim::with(['project', 'jenisKenderaan'])->findOrFail($id);
// Recompute penalty for display based on actual submission date (created_at)
// so show page always displays correct reason even if saved earlier incorrectly.
$claimDate = Carbon::parse($claim->claim_date);
$submissionDate = $claim->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';
}
// Attach computed display-only properties (won't persist to DB)
$claim->display_penalty_amount = $displayPenalty;
$claim->display_penalty_reason = $displayReason;
$claim->display_total_claim_amount = ($calculatedAmount - $displayPenalty);
return view('mileage::application.show', compact('claim')); return view('mileage::application.show', compact('claim'));
} }
public function updateStatus(Request $request, $id)
{
$claim = Claim::findOrFail($id);
$user = auth()->user();
switch ($request->action) {
case 'verify': // PM/HOD
$claim->status = 'Disokong';
break;
case 'approve': // PD/BOD
$claim->status = 'Disahkan';
break;
case 'review': // Accounting
$claim->status = 'Disemak';
break;
case 'finance_approve': // Finance Director
$claim->status = 'Diluluskan';
break;
case 'reject': // Any approver
$claim->status = 'Ditolak';
break;
case 'amend': // Finance edits
$claim->status = 'Dibetulkan';
break;
case 'pay': // Final finance step
$claim->status = 'Dibayar';
break;
}
$claim->save();
return back()->with('success', 'Claim status updated to ' . $claim->status);
}
} }
...@@ -4,41 +4,121 @@ ...@@ -4,41 +4,121 @@
use Portal\Mileage\Model\Project; use Portal\Mileage\Model\Project;
use Portal\Mileage\Model\JenisKenderaan; use Portal\Mileage\Model\JenisKenderaan;
use Portal\Mileage\Model\ProjectTeam;
use Portal\Mileage\Model\Users;
use Illuminate\Support\Facades\Auth;
use Illuminate\Routing\Controller; use Illuminate\Routing\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use File;
use DB; use DB;
use Auth;
use Carbon\Carbon;
use Redirect;
class ApplyformController extends Controller class ApplyformController extends Controller
{ {
public function __construct() public function __construct()
{ {
//
}
/**
* Build candidate IDs for current user
*/
protected function getCandidateIds($user)
{
$candidates = [];
// 1) user id
if (!empty($user->id)) {
$candidates[] = $user->id;
}
// 2) employeeCode on User model
if (!empty($user->employeeCode)) {
$candidates[] = $user->employeeCode;
}
// 3) extra lookup from portal Users table
$portalUser = Users::where('id', $user->id)->first();
if ($portalUser) {
if (!empty($portalUser->employeeCode)) {
$candidates[] = $portalUser->employeeCode;
}
if (!empty($portalUser->pt_staff_id)) {
$candidates[] = $portalUser->pt_staff_id;
}
}
// Clean up (remove nulls, duplicates)
return array_values(array_unique(array_filter($candidates, function ($v) {
return $v !== null && $v !== '';
})));
}
/**
* Get projects for the logged in user
*/
protected function getProjectsForUser($user)
{
$candidates = $this->getCandidateIds($user);
$projectIds = [];
if (!empty($candidates)) {
$projectIds = ProjectTeam::whereIn('pt_staff_id', $candidates)
->pluck('fk_project_id')
->unique()
->toArray();
}
$isAdmin = ($user->role_id == 1);
if ($isAdmin) {
// Admin sees all
return Project::orderBy('p_project_description')->get();
} elseif (!empty($projectIds)) {
// Non-admin, but has assigned projects
return Project::whereIn('id', $projectIds)
->orderBy('p_project_description')
->get();
}
// Non-admin with no assigned projects
return collect([]);
} }
public function index() public function index()
{ {
$projects = Project::all(); $user = auth()->user();
$jenisKenderaan = JenisKenderaan::all();
return view('mileage::applyform.index', compact('projects','jenisKenderaan')); // fetch role names directly
$roles = $user->roles->pluck('name')->toArray();
$isAdmin = in_array('Administrator', $roles); // check by role name
if ($isAdmin) {
// Admin → all projects
$projects = Project::orderBy('p_project_description')->get();
} else {
// Non-admin → only their assigned projects
$projects = Project::whereHas('projectTeam', function($q) use ($user) {
$q->where('pt_staff_id', $user->employee_code);
})->get();
}
$jenisKenderaan = JenisKenderaan::all();
return view('mileage::applyform.index', compact('projects', 'jenisKenderaan'));
} }
public function create() public function create()
{ {
$projects = Project::all(); $user = Auth::user();
return view('mileage::applyform.index', compact('projects')); $projects = $this->getProjectsForUser($user);
$jenisKenderaan = JenisKenderaan::all();
return view('mileage::applyform.index', compact('projects', 'jenisKenderaan'));
} }
public function save(Request $request) public function save(Request $request)
{ {
// TODO: Implement save logic
return null; return null;
} }
}
} //end function
<?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;
class ProjectDirectorController extends Controller
{
public function index()
{
$user = auth()->user();
// Get project IDs where this user is the Project Director
$directedProjectIds = ProjectTeam::where('pt_staff_id', $user->employeeCode)
->where('role_id', 7) // 7 = Project Director
->pluck('fk_project_id')
->toArray();
// Get claims for those projects, only after PM verification
$claims = Claim::with(['user', 'project'])
->whereIn('project_id', $directedProjectIds)
->whereIn('status', ['Disahkan', 'Ditolak']) // Only show claims verified by Project Manager
->orderBy('created_at', 'desc')
->get();
return view('mileage::projectDirector.index', compact('claims'));
}
public function show($id)
{
// load claim
$claim = Claim::with(['project', 'jenisKenderaan', 'user'])->findOrFail($id);
// Recompute penalty for display based on actual submission date (created_at)
// so show page always displays correct reason even if saved earlier incorrectly.
$claimDate = Carbon::parse($claim->claim_date);
$submissionDate = $claim->submission_date ?? $claim->created_at ?? Carbon::now();
$daysDiff = $claimDate->diffInDays($submissionDate);
$calculatedAmount = $claim->calculated_amount ?? 0;
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';
}
// Attach computed display-only properties (won't persist to DB)
$claim->display_penalty_amount = $displayPenalty;
$claim->display_penalty_reason = $displayReason;
$totalClaim = ($calculatedAmount + ($claim->toll_amount ?? 0) + ($claim->others_amount ?? 0)) - $displayPenalty;
$claim->display_total_claim_amount = $totalClaim;
return view('mileage::projectDirector.show', compact('claim'));
}
public function approve($id)
{
$user = auth()->user();
$claim = Claim::findOrFail($id);
$claim->status = 'Disahkan';
$allowedProjectIds = ProjectTeam::where('pt_staff_id', $user->employeeCode)
->where('role_id', 7)
->pluck('fk_project_id')
->toArray();
if (!in_array($claim->project_id, $allowedProjectIds)) {
return back()->with('error', 'Anda tidak dibenarkan meluluskan tuntutan ini.');
}
$claim->status = 'Disahkan';
$claim->save();
return back()->with('success', 'Claim approved successfully.');
}
public function reject($id)
{
$claim = Claim::findOrFail($id);
$user = auth()->user();
$allowedProjectIds = ProjectTeam::where('pt_staff_id', $user->employeeCode)
->where('role_id', 7)
->pluck('fk_project_id')
->toArray();
if (!in_array($claim->project_id, $allowedProjectIds)) {
return back()->with('error', 'Anda tidak dibenarkan menolak tuntutan ini.');
}
$claim->status = 'Ditolak';
$claim->save();
return back()->with('error', 'Claim rejected.');
}
}
<?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 Illuminate\Routing\Controller;
use Illuminate\Http\Request;
use Carbon\Carbon;
class ProjectManagerController extends Controller
{
public function index()
{
$user = auth()->user();
// Get project IDs managed by this PM
$managedProjectIds = ProjectTeam::where('pt_staff_id', $user->employeeCode)
->where('role_id', 1) // assuming 2 = PM
->pluck('fk_project_id')
->toArray();
// Get claims only for those projects
$claims = Claim::with(['user', 'project'])
->whereIn('project_id', $managedProjectIds)
->get();
return view('mileage::projectManager.index', compact('claims'));
}
public function show($id)
{
// load claim
$claim = Claim::with(['project', 'jenisKenderaan'])->findOrFail($id);
// Recompute penalty for display based on actual submission date (created_at)
// so show page always displays correct reason even if saved earlier incorrectly.
$claimDate = Carbon::parse($claim->claim_date);
$submissionDate = $claim->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';
}
// Attach computed display-only properties (won't persist to DB)
$claim->display_penalty_amount = $displayPenalty;
$claim->display_penalty_reason = $displayReason;
$claim->display_total_claim_amount = ($calculatedAmount - $displayPenalty);
return view('mileage::projectManager.show', compact('claim'));
}
}
<?php
namespace Portal\Mileage\Model;
use Illuminate\Database\Eloquent\Model;
class AclRoleUser extends Model
{
protected $table = 'acl_role_user';
protected $fillable = ['role_id', 'user_id'];
public $timestamps = false;
public function role()
{
return $this->belongsTo(AclRole::class, 'role_id');
}
public function user()
{
return $this->belongsTo(\App\Models\User::class, 'user_id');
}
}
...@@ -4,14 +4,20 @@ ...@@ -4,14 +4,20 @@
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class Aclrole extends Model class AclRole extends Model
{ {
/** protected $table = 'acl_roles'; // ⚠️ must be your roles definition table
* The database table used by the model. protected $primaryKey = 'id';
* public $timestamps = false;
* @var string
*/
protected $table = 'acl_role_user';
public $primaryKey = 'user_id'; public function users()
{
return $this->belongsToMany(
\App\Models\User::class,
'acl_role_user',
'role_id',
'user_id'
);
}
} }
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
namespace Portal\Mileage\Model; namespace Portal\Mileage\Model;
use App\Models\User;
use Portal\Mileage\Model\Project; use Portal\Mileage\Model\Project;
use Portal\Mileage\Model\JenisKenderaan; use Portal\Mileage\Model\JenisKenderaan;
...@@ -12,13 +14,18 @@ class Claim extends Model ...@@ -12,13 +14,18 @@ class Claim extends Model
{ {
protected $fillable = [ protected $fillable = [
'claim_date', 'jenis_kenderaan_id', 'project_id', 'description', 'user_id','claim_date', 'jenis_kenderaan_id', 'project_id', 'description',
'distance_from', 'distance_to', 'distance_from', 'distance_to',
'total_mileage', 'toll_amount', 'others_amount', 'others_description', 'total_mileage', 'toll_amount', 'others_amount', 'others_description',
'calculated_amount','penalty_amount', 'penalty_reason', 'total_claim_amount', 'calculated_amount','penalty_amount', 'penalty_reason', 'total_claim_amount',
'status', 'status',
]; ];
public function user()
{
return $this->belongsTo(User::class, 'user_id', 'id');
}
public function project() public function project()
{ {
return $this->belongsTo(Project::class, 'project_id', 'id', 'p_project_description'); return $this->belongsTo(Project::class, 'project_id', 'id', 'p_project_description');
......
...@@ -49,5 +49,11 @@ public function claims() ...@@ -49,5 +49,11 @@ public function claims()
{ {
return $this->hasMany(Claim::class, 'project_id'); return $this->hasMany(Claim::class, 'project_id');
} }
public function projectTeam()
{
return $this->hasMany(\Portal\Mileage\Model\ProjectTeam::class, 'fk_project_id', 'id');
}
} }
<?php
namespace Portal\Mileage\Model;
use Portal\Mileage\Model\Project;
use Portal\Mileage\Model\JenisKenderaan;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ProjectRole extends Model
{
protected $table = 'project_role';
}
\ No newline at end of file
<?php
namespace Portal\Mileage\Model;
use Illuminate\Database\Eloquent\Model;
class ProjectTeam extends Model
{
protected $table = 'project_team';
public $timestamps = true;
protected $primaryKey = 'id';
protected $fillable = [
'fk_project_id', 'pt_project_description', 'role_id',
'pt_team', 'pt_staff_id', 'pt_project_type', 'pt_project_task_sts',
'updated_by'
];
public function project()
{
return $this->belongsTo(Project::class, 'fk_project_id', 'id');
}
// if you want, link to App user model (pt_staff_id commonly stores App user id or employee code)
public function user()
{
return $this->belongsTo(\App\Models\User::class, 'pt_staff_id', 'id');
}
public function role()
{
return $this->belongsTo(ProjectRole::class, 'role_id');
}
}
...@@ -39,5 +39,23 @@ public function staffinfo() ...@@ -39,5 +39,23 @@ public function staffinfo()
return $this->belongsTo('Portal\Mileage\Model\StaffInfo','id','fk_users'); return $this->belongsTo('Portal\Mileage\Model\StaffInfo','id','fk_users');
} }
public function projectTeams()
{
return $this->hasMany('Portal\Mileage\Model\ProjectTeam', 'pt_staff_id', 'employeeCode');
}
public function managedProjects()
{
return $this->projectTeams()->where('role_id', 1); // 1 = Project Manager
}
public function roles()
{
return $this->belongsToMany(\Portal\Mileage\Model\AclRole::class,
'acl_role_user',
'user_id',
'role_id'
);
}
} }
...@@ -84,22 +84,29 @@ protected function menu() ...@@ -84,22 +84,29 @@ protected function menu()
->data('icon', 'home') ->data('icon', 'home')
->active('home/*'); ->active('home/*');
$menu->add(__('Project Manager Dashboard'), route('mileage::projectManager.index'))
->data('icon', 'user tie')
->active('projectManager/*')
->data('permission', Permission::PROJECT_MANAGER);
$menu->add(__('Project Director Dashboard'), route('mileage::projectDirector.index'))
->data('icon', 'user tie')
->active('projectDirector/*')
->data('permission', Permission::PROJECT_DIRECTOR);
$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(__('Permohonan Mileage Claim'), route('mileage::applyform.index'))
->data('icon', 'edit outline') ->data('icon', 'edit outline')
->active('applyform/*') ->active('applyform/*');
->data('permission', Permission::APPLY_CLAIM);
$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')
->active('applications/*'); ->active('applications/*');
}); });
} }
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
<div class="detail">{{auth()->user()->roles['0']->name}}</div> <div class="detail">{{auth()->user()->roles['0']->name}}</div>
</a> </a>
<p></p> <p></p>
<a href="https://warga.ppj.gov.my" class="ui teal left ribbon label">Kembali Ke EP</a> <a href="{{ route('auth::logout') }}" class="ui teal left ribbon label">Kembali Ke EP</a>
<p></p> <p></p>
</div> </div>
</div> </div>
......
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