Commit a3cb80f0 by Hannah Zahra

Fifth Update

parent 90f032c8
...@@ -57,6 +57,10 @@ public function up() ...@@ -57,6 +57,10 @@ public function up()
$table->unsignedInteger('rejected_by')->nullable(); $table->unsignedInteger('rejected_by')->nullable();
$table->foreign('rejected_by')->references('id')->on('users')->nullOnDelete(); $table->foreign('rejected_by')->references('id')->on('users')->nullOnDelete();
// Reject reason
$table->string('reject_reason', 500)->nullable();
// Claim status (workflow) // Claim status (workflow)
$table->enum('status', [ $table->enum('status', [
'Diproses', 'Diproses',
...@@ -88,6 +92,8 @@ public function down() ...@@ -88,6 +92,8 @@ public function down()
$table->dropForeign(['rejected_by']); $table->dropForeign(['rejected_by']);
$table->dropColumn('rejected_by'); $table->dropColumn('rejected_by');
$table->dropColumn('reject_reason');
}); });
// Drop the 'claims' table entirely // Drop the 'claims' table entirely
......
...@@ -371,7 +371,19 @@ ...@@ -371,7 +371,19 @@
<td>RM {{ number_format($totalAmount, 2) }}</td> <td>RM {{ number_format($totalAmount, 2) }}</td>
<td> <td>
@foreach($projectClaims->pluck('status')->unique() as $status) @foreach($projectClaims->pluck('status')->unique() as $status)
<span class="status-label status-{{ $status }}">{{ $status }}</span> @if($status === 'Ditolak')
<span class="status-label status-Ditolak"
data-tooltip="Tekan butang 'Lihat' untuk lihat butiran tuntutan ini."
data-inverted
data-position="top center"
style="cursor: help;">
{{ $status }}
</span>
@else
<span class="status-label status-{{ $status }}">
{{ $status }}
</span>
@endif
@endforeach @endforeach
</td> </td>
<td> <td>
...@@ -409,7 +421,19 @@ ...@@ -409,7 +421,19 @@
<td>{{ number_format($claim->total_mileage, 1) }}</td> <td>{{ number_format($claim->total_mileage, 1) }}</td>
<td>RM {{ number_format($claim->total_claim_amount, 2) }}</td> <td>RM {{ number_format($claim->total_claim_amount, 2) }}</td>
<td> <td>
<span class="status-label status-{{ $claim->status }}">{{ $claim->status }}</span> @if($claim->status === 'Ditolak')
<span class="status-label status-Ditolak"
data-tooltip="{{ $claim->reject_reason ?? 'Tiada sebab diberikan.' }}"
data-inverted
data-position="top center"
style="cursor: help;">
{{ $claim->status }}
</span>
@else
<span class="status-label status-{{ $claim->status }}">
{{ $claim->status }}
</span>
@endif
</td> </td>
<td> <td>
{{-- Always show Lihat --}} {{-- Always show Lihat --}}
...@@ -525,6 +549,17 @@ ...@@ -525,6 +549,17 @@
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script> <script>
$(document).ready(function() { $(document).ready(function() {
// Initialize Semantic/Fomantic UI tooltips
$('[data-tooltip]').popup({
variation: 'very wide',
hoverable: true,
position: 'top center',
delay: {
show: 100,
hide: 200
}
});
// Toggle Summary Section // Toggle Summary Section
$('#summaryToggle').on('click', function() { $('#summaryToggle').on('click', function() {
$(this).toggleClass('active'); $(this).toggleClass('active');
......
...@@ -58,6 +58,7 @@ ...@@ -58,6 +58,7 @@
<th>Lain-lain (RM)</th> <th>Lain-lain (RM)</th>
<th>Status</th> <th>Status</th>
<th>Jumlah (RM)</th> <th>Jumlah (RM)</th>
<th>Tindakan</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
...@@ -70,7 +71,7 @@ ...@@ -70,7 +71,7 @@
@foreach($claims as $c) @foreach($claims as $c)
@php @php
$total += $c->total_claim_amount; $total += $c->total_claim;
$totalMileage += $c->total_mileage ?? 0; $totalMileage += $c->total_mileage ?? 0;
$totalToll += $c->toll_amount ?? 0; $totalToll += $c->toll_amount ?? 0;
$totalOthers += $c->others_amount ?? 0; $totalOthers += $c->others_amount ?? 0;
...@@ -85,9 +86,51 @@ ...@@ -85,9 +86,51 @@
<td>{{ number_format($c->toll_amount ?? 0, 2) }}</td> <td>{{ number_format($c->toll_amount ?? 0, 2) }}</td>
<td>{{ number_format($c->others_amount ?? 0, 2) }}</td> <td>{{ number_format($c->others_amount ?? 0, 2) }}</td>
<td> <td>
<span class="status-label status-{{ $c->status }}">{{ $c->status }}</span> @if($c->status === 'Ditolak')
<span class="status-label status-Ditolak"
data-tooltip="{{ $c->reject_reason ?? 'Tiada sebab diberikan.' }}"
data-inverted
data-position="top center"
style="cursor: help;">
{{ $c->status }}
</span>
@else
<span class="status-label status-{{ $c->status }}">
{{ $c->status }}
</span>
@endif
</td>
<td>
RM {{ number_format($c->total_claim_amount, 2) }}
</td>
<td>
{{-- actions for Diproses --}}
@if($c->status === 'Diproses')
<div style="margin-top:8px;">
<button class="ui yellow tiny button edit-btn"
data-id="{{ $c->id }}"
data-date="{{ $c->claim_date }}"
data-description="{{ $c->description }}"
data-distance-from="{{ $c->distance_from }}"
data-distance-to="{{ $c->distance_to }}"
data-mileage="{{ $c->total_mileage }}"
data-toll="{{ $c->toll_amount }}"
data-others="{{ $c->others_amount }}">
<i class="edit icon"></i> Kemaskini
</button>
<form action="{{ route('mileage::applications.destroy', $c->id) }}"
method="POST" style="display:inline;"
onsubmit="return confirm('Padam tuntutan ini?');">
@csrf
@method('DELETE')
<button type="submit" class="ui red tiny button">
<i class="trash icon"></i> Padam
</button>
</form>
</div>
@endif
</td> </td>
<td>RM {{ number_format($c->total_claim_amount, 2) }}</td>
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>
...@@ -118,5 +161,145 @@ ...@@ -118,5 +161,145 @@
<a href="{{ route('mileage::applications.index', ['month' => $month]) }}" class="ui grey button"> <a href="{{ route('mileage::applications.index', ['month' => $month]) }}" class="ui grey button">
<i class="arrow left icon"></i> Kembali <i class="arrow left icon"></i> Kembali
</a> </a>
{{-- Edit Modal --}}
<div class="ui modal" id="editModal">
<i class="close icon"></i>
<div class="header">Kemaskini Tuntutan</div>
<div class="content">
<form id="editForm" method="POST" action="">
@csrf
@method('PUT')
<div class="ui form">
<div class="field required">
<label>Tarikh</label>
<input type="date" name="claim_date" id="editDate" required>
</div>
<div class="two fields">
<div class="field required">
<label>Dari</label>
<input type="text" name="distance_from" id="editFrom" required>
</div>
<div class="field required">
<label>Ke</label>
<input type="text" name="distance_to" id="editTo" required>
</div>
</div>
<div class="field required">
<label>Butiran</label>
<input type="text" name="description" id="editDescription" required>
</div>
<div class="three fields">
<div class="field required">
<label>Jumlah KM</label>
<input type="number" step="0.1" name="total_mileage" id="editMileage" required>
</div>
<div class="field">
<label>Tol (RM)</label>
<input type="number" step="0.01" name="toll_amount" id="editToll">
</div>
<div class="field">
<label>Lain-lain (RM)</label>
<input type="number" step="0.01" name="others_amount" id="editOthers">
</div>
</div>
<div class="field" id="othersDetailsField" style="display:none;">
<label>Butiran Lain-lain</label>
<input type="text" name="others_details" id="editOthersDetails"
placeholder="Nyatakan butiran lain-lain (contoh: bayaran penghantaran, dokumen, dsb)">
</div>
</div>
</form>
</div>
<div class="actions">
<div class="ui cancel button">Batal</div>
<button type="submit" form="editForm" class="ui primary button">Simpan</button>
</div>
</div>
{{-- Edit Modal Script --}}
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script>
$(document).ready(function() {
// Initialize tooltips for Ditolak statuses
$('[data-tooltip]').popup({
variation: 'very wide',
hoverable: true,
position: 'top center',
delay: {
show: 100,
hide: 200
}
});
// Handle Edit button click
$('.edit-btn').on('click', function() {
const id = $(this).data('id');
const date = $(this).data('date');
const desc = $(this).data('description');
const from = $(this).data('distance-from');
const to = $(this).data('distance-to');
const mileage = $(this).data('mileage');
const toll = $(this).data('toll');
const others = $(this).data('others');
const updateUrl = "{{ route('mileage::applications.update', ':id') }}".replace(':id', id);
$('#editForm').attr('action', updateUrl);
$('#editDate').val(date);
$('#editDescription').val(desc);
$('#editFrom').val(from);
$('#editTo').val(to);
$('#editMileage').val(mileage);
$('#editToll').val(toll);
$('#editOthers').val(others);
// Trigger others field logic
$('#editOthers').trigger('input');
$('#editModal').modal('show');
});
// Show/Hide "Butiran Lain-lain" based on input
$('#editOthers').on('input', function() {
const value = parseFloat($(this).val()) || 0;
if (value > 0) {
$('#othersDetailsField').slideDown();
$('#editOthersDetails').attr('required', true);
} else {
$('#othersDetailsField').slideUp();
$('#editOthersDetails').removeAttr('required');
$('#editOthersDetails').val('');
}
});
// Validate before form submission
$('#editForm').on('submit', function(e) {
const others = parseFloat($('#editOthers').val()) || 0;
const othersDetails = $('#editOthersDetails').val().trim();
if (others > 0 && othersDetails === '') {
e.preventDefault();
alert('Sila isi Butiran Lain-lain kerana jumlah Lain-lain (RM) lebih daripada 0.');
$('#editOthersDetails').focus();
return;
}
let valid = true;
$('#editForm [required]').each(function() {
if ($(this).val().trim() === '') {
valid = false;
$(this).focus();
alert('Sila isi semua ruangan yang diperlukan.');
e.preventDefault();
return false;
}
});
});
});
</script>
</div> </div>
@endsection @endsection
...@@ -3,10 +3,23 @@ ...@@ -3,10 +3,23 @@
@section('content') @section('content')
<style> <style>
body { background-color: #f0f2f5; } body {
.ui.container { padding: 0 5rem 3rem 2rem; margin-top: 3rem; } background-color: #f0f2f5;
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.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 { .ui.segment {
background: white; background: white;
border-radius: 10px; border-radius: 10px;
...@@ -54,6 +67,11 @@ ...@@ -54,6 +67,11 @@
gap: 1rem; gap: 1rem;
margin-top: 1rem; margin-top: 1rem;
} }
.approval-actions .ui.button {
height: 42px;
text-align: center;
}
</style> </style>
<div class="ui container"> <div class="ui container">
...@@ -82,6 +100,7 @@ ...@@ -82,6 +100,7 @@
<th>Lain-lain (RM)</th> <th>Lain-lain (RM)</th>
<th>Status</th> <th>Status</th>
<th>Jumlah (RM)</th> <th>Jumlah (RM)</th>
<th>Tindakan</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
...@@ -112,24 +131,46 @@ ...@@ -112,24 +131,46 @@
</span> </span>
</td> </td>
<td>{{ number_format($claim->total_claim_amount, 2) }}</td> <td>{{ number_format($claim->total_claim_amount, 2) }}</td>
<td>
@if($claim->status === 'Disokong')
<form action="{{ route('mileage::projectDirector.approveClaim', $claim->id) }}" method="POST" style="display:inline;">
@csrf
<button type="submit" class="ui green tiny button">
<i class="check icon"></i> Sahkan
</button>
</form>
<button
type="button"
class="ui red tiny button reject-single-btn"
data-claim-id="{{ $claim->id }}"
data-project="{{ $project->id }}"
data-user="{{ $user->id }}"
data-month="{{ $month }}">
<i class="times icon"></i> Tolak
</button>
@else
<span style="color:#999;"></span>
@endif
</td>
</tr> </tr>
@if(!empty($claim->others_description)) @if(!empty($claim->others_description))
<tr style="background: #f9f9f9;"> <tr style="background: #f9f9f9;">
<td colspan="9"> <td colspan="10">
<strong>Butiran Lain-lain:</strong> {{ $claim->others_description }} <strong>Butiran Lain-lain:</strong> {{ $claim->others_description }}
</td> </td>
</tr> </tr>
@endif @endif
@empty @empty
<tr><td colspan="9" class="center aligned">Tiada tuntutan untuk bulan ini.</td></tr> <tr><td colspan="10" class="center aligned">Tiada tuntutan untuk bulan ini.</td></tr>
@endforelse @endforelse
</tbody> </tbody>
@if($claims->count() > 0) @if($claims->count() > 0)
<tfoot> <tfoot>
<tr> <tr>
<th colspan="8" class="right aligned">Jumlah Keseluruhan</th> <th colspan="9" class="right aligned">Jumlah Keseluruhan</th>
<th>RM {{ number_format($total, 2) }}</th> <th>RM {{ number_format($total, 2) }}</th>
</tr> </tr>
</tfoot> </tfoot>
...@@ -151,10 +192,8 @@ ...@@ -151,10 +192,8 @@
</span> </span>
</div> </div>
{{-- Only show buttons if there are still claims waiting for Director verification --}}
@if($hasPending) @if($hasPending)
<div class="approval-actions"> <div class="approval-actions">
{{-- Approve All --}}
<form action="{{ route('mileage::projectDirector.approveMonth') }}" method="POST" style="display:inline;"> <form action="{{ route('mileage::projectDirector.approveMonth') }}" method="POST" style="display:inline;">
@csrf @csrf
<input type="hidden" name="project_id" value="{{ $project->id }}"> <input type="hidden" name="project_id" value="{{ $project->id }}">
...@@ -165,77 +204,89 @@ ...@@ -165,77 +204,89 @@
</button> </button>
</form> </form>
{{-- Reject All --}} <button
<form action="{{ route('mileage::projectDirector.rejectMonth') }}" method="POST" style="display:inline;"> type="button"
@csrf class="ui red button reject-btn"
<input type="hidden" name="project_id" value="{{ $project->id }}"> data-project="{{ $project->id }}"
<input type="hidden" name="user_id" value="{{ $user->id }}"> data-user="{{ $user->id }}"
<input type="hidden" name="month" value="{{ $month }}"> data-month="{{ $month }}">
<button type="submit" class="ui red button">
<i class="times icon"></i> Tolak Semua <i class="times icon"></i> Tolak Semua
</button> </button>
</form>
</div> </div>
@endif @endif
</div> </div>
@endif @endif
</div>
{{-- Calculation Summary Section --}} {{-- Reject Modal --}}
@if($claims->count() > 0) <div class="ui small modal" id="rejectModal">
@php <i class="close icon"></i>
$totalMileage = $claims->sum('total_mileage'); <div class="header">Sebab Penolakan</div>
$totalClaimAmount = $claims->sum('total_claim_amount'); <div class="content">
$totalToll = $claims->sum('toll_amount'); <form id="rejectForm" method="POST" action="{{ route('mileage::projectDirector.rejectMonth') }}">
$totalOthers = $claims->sum('others_amount'); @csrf
$latestClaim = $claims->sortByDesc('claim_date')->first(); <input type="hidden" name="project_id" id="rejectProjectId">
$penaltyAmount = $latestClaim->penalty_amount ?? 0; <input type="hidden" name="user_id" id="rejectUserId">
$penaltyReason = $latestClaim->penalty_reason ?? '-'; <input type="hidden" name="month" id="rejectMonth">
$vehicleType = strtolower($claims->first()->jenisKenderaan->name ?? 'kereta'); <div class="ui form">
<div class="field required">
$uniqueDates = $claims->pluck('claim_date')->unique()->count(); <label>Nyatakan sebab tuntutan ini ditolak:</label>
$deduction = 40 * $uniqueDates; <textarea
$claimableDistance = max(0, $totalMileage - $deduction); name="reject_reason"
id="rejectReason"
if ($vehicleType === 'motosikal' || $vehicleType === 'motorcycle') { rows="3"
$rateNote = "Motosikal – RM0.30/km"; placeholder="Contoh: Tuntutan tidak lengkap atau tiada resit sokongan"
} elseif ($claimableDistance <= 500) { required></textarea>
$rateNote = "Kereta – RM0.55/km (≤500km)";
} else {
$secondPartKm = $claimableDistance - 500;
$rateNote = "Kereta – 500km × RM0.55 + " . number_format($secondPartKm, 1) . "km × RM0.50";
}
$finalTotal = $totalClaimAmount + $totalToll + $totalOthers - $penaltyAmount;
@endphp
<div class="ui segment">
<h4><i class="calculator icon"></i> Ringkasan Pengiraan (Mengikut Rekod Sistem)</h4>
<table class="ui definition table">
<tbody>
<tr><td><strong>Jumlah Jarak (Sebenar)</strong></td><td>{{ number_format($totalMileage, 1) }} KM</td></tr>
<tr><td><strong>Jumlah Hari Tuntutan</strong></td><td>{{ $uniqueDates }} hari</td></tr>
<tr><td><strong>Potongan 40KM/Hari</strong></td><td>40 × {{ $uniqueDates }} = {{ $deduction }} KM</td></tr>
<tr><td><strong>Jumlah Jarak Boleh Tuntut</strong></td><td>{{ number_format($claimableDistance, 1) }} KM</td></tr>
<tr><td><strong>Kadar Kiraan</strong></td><td>{{ $rateNote }}</td></tr>
<tr><td><strong>Jumlah Mileage (Dari Sistem)</strong></td><td>RM {{ number_format($totalClaimAmount, 2) }}</td></tr>
<tr><td><strong>Jumlah Tol</strong></td><td>RM {{ number_format($totalToll, 2) }}</td></tr>
<tr><td><strong>Jumlah Lain-lain</strong></td><td>RM {{ number_format($totalOthers, 2) }}</td></tr>
<tr><td><strong>Penalti</strong></td><td>RM {{ number_format($penaltyAmount, 2) }} ({{ $penaltyReason }})</td></tr>
<tr><td><strong><span style="color:#21ba45;">Jumlah Akhir</span></strong></td><td><strong>RM {{ number_format($finalTotal, 2) }}</strong></td></tr>
</tbody>
</table>
</div> </div>
@endif </div>
</form>
{{-- Calculation Rule Reminder --}} </div>
<div class="ui segment"> <div class="actions">
<h4><i class="info circle icon"></i> Seksyen Pengiraan:</h4> <div class="ui cancel button">Batal</div>
<p> <button type="submit" form="rejectForm" class="ui red button">Hantar Penolakan</button>
• Tuntutan bermula selepas 40KM pertama setiap hari.<br>
• Kadar (Kereta): RM0.55/km (&lt;500km), RM0.50/km (&gt;500km).<br>
• Kadar (Motosikal): RM0.30/km.<br>
• Penalti dikenakan sekiranya tuntutan lewat dihantar (&gt;1 bulan).
</p>
</div> </div>
</div> </div>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script>
$(document).ready(function() {
// Handle single claim rejection
$('.reject-single-btn').on('click', function() {
const claimId = $(this).data('claim-id');
const projectId = $(this).data('project');
const userId = $(this).data('user');
const month = $(this).data('month');
const action = "{{ route('mileage::projectDirector.rejectClaim', ':id') }}".replace(':id', claimId);
$('#rejectForm').attr('action', action);
$('#rejectProjectId').val(projectId);
$('#rejectUserId').val(userId);
$('#rejectMonth').val(month);
$('#rejectReason').val('');
$('#rejectModal').modal('show');
});
// Handle bulk rejection
$('.reject-btn').on('click', function() {
$('#rejectProjectId').val($(this).data('project'));
$('#rejectUserId').val($(this).data('user'));
$('#rejectMonth').val($(this).data('month'));
$('#rejectReason').val('');
$('#rejectForm').attr('action', "{{ route('mileage::projectDirector.rejectMonth') }}");
$('#rejectModal').modal('show');
});
// Validate reject form
$('#rejectForm').on('submit', function(e) {
const reason = $('#rejectReason').val().trim();
if (reason === '') {
e.preventDefault();
alert('Sila nyatakan sebab penolakan sebelum menghantar.');
$('#rejectReason').focus();
}
});
});
</script>
@endsection @endsection
...@@ -85,6 +85,12 @@ ...@@ -85,6 +85,12 @@
gap: 1rem; gap: 1rem;
margin-top: 1rem; margin-top: 1rem;
} }
.approval-actions .ui.button {
height: 42px;
text-align: center;
}
</style> </style>
<div class="ui container"> <div class="ui container">
...@@ -113,6 +119,7 @@ ...@@ -113,6 +119,7 @@
<th>Lain-lain (RM)</th> <th>Lain-lain (RM)</th>
<th>Status</th> <th>Status</th>
<th>Jumlah (RM)</th> <th>Jumlah (RM)</th>
<th>Tindakan</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
...@@ -143,6 +150,31 @@ ...@@ -143,6 +150,31 @@
</span> </span>
</td> </td>
<td>{{ number_format($claim->total_claim_amount, 2) }}</td> <td>{{ number_format($claim->total_claim_amount, 2) }}</td>
<td>
{{-- individual approve --}}
@if($claim->status === 'Diproses')
{{-- only show if status = Diproses --}}
<form action="{{ route('mileage::projectManager.approveClaim', $claim->id) }}" method="POST" style="display:inline;">
@csrf
<button type="submit" class="ui green tiny button">
<i class="check icon"></i> Sahkan
</button>
</form>
<button
type="button"
class="ui red tiny button reject-single-btn"
data-claim-id="{{ $claim->id }}"
data-project="{{ $project->id }}"
data-user="{{ $user->id }}"
data-month="{{ $month }}">
<i class="times icon"></i> Tolak
</button>
@else
{{-- show dash for processed claims --}}
<span style="color:#999;"></span>
@endif
</td>
</tr> </tr>
@if(!empty($claim->others_description)) @if(!empty($claim->others_description))
...@@ -196,16 +228,15 @@ ...@@ -196,16 +228,15 @@
</button> </button>
</form> </form>
{{-- Reject All --}} {{-- Reject All with Modal Trigger --}}
<form action="{{ route('mileage::projectManager.rejectMonth') }}" method="POST" style="display:inline;"> <button
@csrf type="button"
<input type="hidden" name="project_id" value="{{ $project->id }}"> class="ui red button reject-btn"
<input type="hidden" name="user_id" value="{{ $user->id }}"> data-project="{{ $project->id }}"
<input type="hidden" name="month" value="{{ $month }}"> data-user="{{ $user->id }}"
<button type="submit" class="ui red button"> data-month="{{ $month }}">
<i class="times icon"></i> Tolak Semua <i class="times icon"></i> Tolak Semua
</button> </button>
</form>
</div> </div>
@endif @endif
</div> </div>
...@@ -246,7 +277,7 @@ ...@@ -246,7 +277,7 @@
} }
// now match the total (no recomputation) // now match the total (no recomputation)
$finalTotal = $totalClaimAmount + $totalToll + $totalOthers - $penaltyAmount; $finalTotal = $totalClaimAmount - $penaltyAmount;
@endphp @endphp
<div class="ui segment"> <div class="ui segment">
...@@ -309,4 +340,77 @@ ...@@ -309,4 +340,77 @@
</p> </p>
</div> </div>
</div> </div>
{{-- Reject Modal --}}
<div class="ui small modal" id="rejectModal">
<i class="close icon"></i>
<div class="header">Sebab Penolakan</div>
<div class="content">
<form id="rejectForm" method="POST" action="{{ route('mileage::projectManager.rejectMonth') }}">
@csrf
<input type="hidden" name="project_id" id="rejectProjectId">
<input type="hidden" name="user_id" id="rejectUserId">
<input type="hidden" name="month" id="rejectMonth">
<div class="ui form">
<div class="field required">
<label>Nyatakan sebab tuntutan ini ditolak:</label>
<textarea
name="reject_reason"
id="rejectReason"
rows="3"
placeholder="Contoh: Tuntutan tidak lengkap atau tiada resit sokongan"
required></textarea>
</div>
</div>
</form>
</div>
<div class="actions">
<div class="ui cancel button">Batal</div>
<button type="submit" form="rejectForm" class="ui red button">Hantar Penolakan</button>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script>
$(document).ready(function() {
// Handle single claim rejection
$('.reject-single-btn').on('click', function() {
const claimId = $(this).data('claim-id');
const projectId = $(this).data('project');
const userId = $(this).data('user');
const month = $(this).data('month');
// Change form action dynamically for single reject
const action = "{{ route('mileage::projectManager.rejectClaim', ':id') }}".replace(':id', claimId);
$('#rejectForm').attr('action', action);
// Fill modal hidden fields
$('#rejectProjectId').val(projectId);
$('#rejectUserId').val(userId);
$('#rejectMonth').val(month);
$('#rejectReason').val('');
// Show modal
$('#rejectModal').modal('show');
});
// Show reject modal when button clicked
$('.reject-btn').on('click', function() {
$('#rejectProjectId').val($(this).data('project'));
$('#rejectUserId').val($(this).data('user'));
$('#rejectMonth').val($(this).data('month'));
$('#rejectReason').val(''); // clear textarea
$('#rejectModal').modal('show');
});
// Prevent submit if no reason entered
$('#rejectForm').on('submit', function(e) {
const reason = $('#rejectReason').val().trim();
if (reason === '') {
e.preventDefault();
alert('Sila nyatakan sebab penolakan sebelum menghantar.');
$('#rejectReason').focus();
}
});
});
</script>
@endsection @endsection
...@@ -39,6 +39,8 @@ function () { ...@@ -39,6 +39,8 @@ function () {
Route::get('/show/{projectId}/{userId}', [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('/approve-month', [ProjectManagerController::class, 'approveMonth'])->name('.approveMonth');
Route::post('/reject-month', [ProjectManagerController::class, 'rejectMonth'])->name('.rejectMonth'); Route::post('/reject-month', [ProjectManagerController::class, 'rejectMonth'])->name('.rejectMonth');
Route::post('/claim/{id}/approve', [ProjectManagerController::class, 'approveClaim'])->name('.approveClaim');
Route::post('/claim/{id}/reject', [ProjectManagerController::class, 'rejectClaim'])->name('.rejectClaim');
}); });
// Project Director // Project Director
...@@ -47,6 +49,8 @@ function () { ...@@ -47,6 +49,8 @@ function () {
Route::get('/show/{projectId}/{userId}', [ProjectDirectorController::class, 'show'])->name('.show'); Route::get('/show/{projectId}/{userId}', [ProjectDirectorController::class, 'show'])->name('.show');
Route::post('/approve-month', [ProjectDirectorController::class, 'approveMonth'])->name('.approveMonth'); Route::post('/approve-month', [ProjectDirectorController::class, 'approveMonth'])->name('.approveMonth');
Route::post('/reject-month', [ProjectDirectorController::class, 'rejectMonth'])->name('.rejectMonth'); Route::post('/reject-month', [ProjectDirectorController::class, 'rejectMonth'])->name('.rejectMonth');
Route::post('/claim/{id}/approve', [ProjectDirectorController::class, 'approveClaim'])->name('.approveClaim');
Route::post('/claim/{id}/reject', [ProjectDirectorController::class, 'rejectClaim'])->name('.rejectClaim');
}); });
// Finance Department // Finance Department
......
...@@ -107,43 +107,45 @@ public function update(Request $request, $id) ...@@ -107,43 +107,45 @@ public function update(Request $request, $id)
{ {
$claim = Claim::findOrFail($id); $claim = Claim::findOrFail($id);
// Check if any required DB fields are null before allowing update // Validate input
$requiredFields = [
'claim_date',
'distance_from',
'distance_to',
'description',
'total_mileage'
];
foreach ($requiredFields as $field) {
if (is_null($claim->$field) || $claim->$field === '') {
return redirect()->back()->with('error', 'Tidak boleh simpan kerana maklumat tuntutan asal tidak lengkap.');
}
}
// Validate new input before updating
$request->validate([ $request->validate([
'claim_date' => 'required|date', 'claim_date' => 'required|date',
'distance_from' => 'required|string', 'distance_from' => 'required|string|max:255',
'distance_to' => 'required|string', 'distance_to' => 'required|string|max:255',
'description' => 'required|string', 'description' => 'required|string|max:255',
'total_mileage' => 'required|numeric|min:0', 'total_mileage' => 'required|numeric|min:0',
'toll_amount' => 'nullable|numeric|min:0', 'toll_amount' => 'nullable|numeric|min:0',
'others_amount' => 'nullable|numeric|min:0', 'others_amount' => 'nullable|numeric|min:0',
'others_details' => 'required_if:others_amount,>,0|string|nullable', 'others_details' => 'nullable|string|max:255',
]); ]);
// If claim data is valid, update it // Calculate new claim amount using same logic as store()
$totalMileage = (float) $request->total_mileage;
$toll = (float) ($request->toll_amount ?? 0);
$others = (float) ($request->others_amount ?? 0);
// Deduct first 40km
$claimableMileage = $totalMileage > 40 ? $totalMileage - 40 : 0;
// Apply rate logic (same as store)
$rate = $claimableMileage > 500 ? 0.50 : 0.55;
$mileageAmount = $claimableMileage * $rate;
$totalClaim = $mileageAmount + $toll + $others;
// Update the record
$claim->update([ $claim->update([
'claim_date' => $request->claim_date, 'claim_date' => $request->claim_date,
'distance_from' => $request->distance_from, 'distance_from' => $request->distance_from,
'distance_to' => $request->distance_to, 'distance_to' => $request->distance_to,
'description' => $request->description, 'description' => $request->description,
'total_mileage' => $request->total_mileage, 'total_mileage' => $totalMileage,
'toll_amount' => $request->toll_amount, 'toll_amount' => $toll,
'others_amount' => $request->others_amount, 'others_amount' => $others,
'others_details' => $request->others_details, 'others_details' => $request->others_details,
'claimable_mileage' => $claimableMileage,
'rate' => $rate,
'mileage_amount' => $mileageAmount,
'total_claim_amount' => $totalClaim,
]); ]);
return redirect()->back()->with('success', 'Tuntutan berjaya dikemaskini.'); return redirect()->back()->with('success', 'Tuntutan berjaya dikemaskini.');
......
...@@ -167,6 +167,7 @@ public function rejectMonth(Request $request) ...@@ -167,6 +167,7 @@ public function rejectMonth(Request $request)
if (!in_array($projectId, $allowedProjectIds)) { if (!in_array($projectId, $allowedProjectIds)) {
return back()->with('error', 'Anda tidak dibenarkan melakukan tindakan ini untuk projek terpilih.'); return back()->with('error', 'Anda tidak dibenarkan melakukan tindakan ini untuk projek terpilih.');
} }
$rejectReason = $request->input('reject_reason', 'Tiada sebab diberikan.');
// update only claims that belong to this project and month and are Disokong // update only claims that belong to this project and month and are Disokong
$rejectedCount = Claim::where('project_id', $projectId) $rejectedCount = Claim::where('project_id', $projectId)
...@@ -176,13 +177,77 @@ public function rejectMonth(Request $request) ...@@ -176,13 +177,77 @@ public function rejectMonth(Request $request)
->update([ ->update([
'status' => 'Ditolak', 'status' => 'Ditolak',
'rejected_by' => $director->id, 'rejected_by' => $director->id,
'reject_reason' => $rejectReason,
'updated_at' => now(), '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 "Disokong" untuk ditolak bagi projek ini pada bulan ' . $monthLabel . '.'); return back()->with('warning', 'Tiada tuntutan "Disokong" untuk ditolak bagi projek ini pada bulan ' . $monthLabel . '.');
} }
/**
* Approve a single claim.
*/
public function approveClaim($id, Request $request)
{
$director = Auth::user();
$claim = Claim::findOrFail($id);
// Ensure this director has access to the project
$allowedProjectIds = ProjectTeam::where('pt_staff_id', $director->employeeCode)
->where('role_id', 7)
->pluck('fk_project_id')
->toArray();
if (!in_array($claim->project_id, $allowedProjectIds)) {
return back()->with('error', 'Anda tidak dibenarkan untuk meluluskan tuntutan ini.');
}
if ($claim->status !== 'Disokong') {
return back()->with('warning', 'Tuntutan ini tidak berada dalam status Disokong.');
}
$claim->update([
'status' => 'Disahkan',
'approved_by' => $director->id,
'updated_at' => now(),
]);
return back()->with('success', 'Tuntutan pada ' . $claim->claim_date->format('d/m/Y') . ' telah disahkan.');
}
/**
* Reject a single claim with reason.
*/
public function rejectClaim($id, Request $request)
{
$director = Auth::user();
$rejectReason = $request->input('reject_reason', 'Tiada sebab diberikan.');
$claim = Claim::findOrFail($id);
// Ensure this director has access to this project
$allowedProjectIds = ProjectTeam::where('pt_staff_id', $director->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.');
}
if ($claim->status !== 'Disokong') {
return back()->with('warning', 'Tuntutan ini tidak berada dalam status Disokong.');
}
$claim->update([
'status' => 'Ditolak',
'rejected_by' => $director->id,
'reject_reason' => $rejectReason,
'updated_at' => now(),
]);
return back()->with('success', 'Tuntutan telah ditolak dengan sebab: ' . $rejectReason);
}
} }
...@@ -141,49 +141,51 @@ public function approveMonth(Request $request) ...@@ -141,49 +141,51 @@ public function approveMonth(Request $request)
*/ */
public function rejectMonth(Request $request) public function rejectMonth(Request $request)
{ {
$manager = Auth::user(); $request->validate([
$monthLabel = $request->input('month'); 'project_id' => 'required|integer',
$projectId = $request->input('project_id'); 'user_id' => 'required|integer',
'month' => 'required|string',
'reject_reason' => 'required|string|max:500',
]);
if (!$projectId) { $month = Carbon::createFromFormat('Y-m', $request->month);
return back()->with('error', 'Sila pilih projek untuk ditolak.'); $claims = Claim::where('project_id', $request->project_id)
} ->where('user_id', $request->user_id)
->whereMonth('claim_date', $month->month)
->whereYear('claim_date', $month->year)
->get();
// validate month format (expects YYYY-MM) foreach ($claims as $claim) {
try { $claim->status = 'Ditolak';
if (!preg_match('/^\d{4}-\d{2}$/', $monthLabel)) { $claim->rejected_by = auth()->id();
throw new \Exception('Invalid month format'); $claim->reject_reason = $request->reject_reason;
$claim->save();
} }
$parsedMonth = Carbon::createFromFormat('Y-m', $monthLabel);
} catch (\Exception $e) { return redirect()->back()->with('success', 'Semua tuntutan telah ditolak dengan sebab: ' . $request->reject_reason);
return back()->with('error', 'Format bulan tidak sah. Sila pilih semula bulan dalam format YYYY-MM.');
} }
// ensure manager oversees this project public function approveClaim($id)
$allowedProjectIds = ProjectTeam::where('pt_staff_id', $manager->employeeCode) {
->where('role_id', 1) $claim = Claim::findOrFail($id);
->pluck('fk_project_id') $claim->status = 'Disokong';
->toArray(); $claim->verified_by = auth()->id();
$claim->save();
if (!in_array($projectId, $allowedProjectIds)) { return back()->with('success', 'Tuntutan telah disahkan.');
return back()->with('error', 'Anda tidak dibenarkan melakukan tindakan ini untuk projek terpilih.');
} }
// update only claims for this project and month with status 'Diproses' public function rejectClaim(Request $request, $id)
$rejectedCount = Claim::where('project_id', $projectId) {
->whereMonth('claim_date', $parsedMonth->month) $request->validate(['reject_reason' => 'required|string|max:500']);
->whereYear('claim_date', $parsedMonth->year)
->where('status', 'Diproses')
->update([
'status' => 'Ditolak',
'rejected_by' => $manager->id,
'updated_at' => now(),
]);
if ($rejectedCount > 0) { $claim = Claim::findOrFail($id);
return back()->with('success', $rejectedCount . ' tuntutan telah ditolak untuk projek ini bagi bulan ' . $monthLabel . '.'); $claim->status = 'Ditolak';
} $claim->rejected_by = auth()->id();
$claim->reject_reason = $request->reject_reason;
$claim->save();
return back()->with('warning', 'Tiada tuntutan "Diproses" untuk ditolak bagi projek ini pada bulan ' . $monthLabel . '.'); return back()->with('success', 'Tuntutan telah ditolak dengan sebab: ' . $request->reject_reason);
} }
} }
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