feat: enhance leave request validation and fix timezone issues

This commit is contained in:
Yoga Pangestu 2026-08-07 15:36:44 +07:00
parent 14140c39cc
commit 4b70da6715
3 changed files with 73 additions and 13 deletions

View File

@ -2,6 +2,7 @@
namespace App\Http\Requests\Admin\HR; namespace App\Http\Requests\Admin\HR;
use App\Models\LeaveRequest;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
class LeaveRequestRequest extends FormRequest class LeaveRequestRequest extends FormRequest
@ -13,8 +14,33 @@ public function authorize(): bool
public function rules(): array public function rules(): array
{ {
$leaveRequest = $this->route('leaveRequest');
return [ return [
'start_date' => ['required', 'date', 'after_or_equal:today'], 'start_date' => [
'required',
'date',
'after:today',
function ($attribute, $value, $fail) use ($leaveRequest) {
$employee = $this->user()->employee;
if (! $employee) {
return;
}
$query = LeaveRequest::where('employee_id', $employee->id)
->where('start_date', $value)
->where('status', '!=', 'cancelled');
if ($leaveRequest) {
$query->where('id', '!=', $leaveRequest->id);
}
if ($query->exists()) {
$fail('Anda sudah mengajukan cuti pada tanggal ini.');
}
},
],
'end_date' => ['required', 'date', 'after_or_equal:start_date'], 'end_date' => ['required', 'date', 'after_or_equal:start_date'],
]; ];
} }

View File

@ -0,0 +1,35 @@
# Fix: Validasi Pengajuan Cuti & Timezone
## Tujuan
Memperbaiki 3 masalah pada modul pengajuan cuti:
1. Timezone bug — tanggal yang dipilih mundur 1 hari (user pilih 8, ke-save 7)
2. Tidak bisa mengajukan cuti untuk hari ini
3. Tidak bisa mengajukan cuti di tanggal yang sama lebih dari 1x
## File yang Dibaca
- `resources/js/pages/admin/hr/leave-request/index.tsx` → Frontend form pengajuan cuti
- `app/Http/Requests/Admin/HR/LeaveRequestRequest.php` → Validasi backend
- `app/Services/Admin/HR/LeaveRequestService.php` → Service logic
- `app/Models/LeaveRequest.php` → Model dengan cast `date:Y-m-d`
## Pola yang Ditemukan
- Frontend menggunakan `toISOString().split('T')[0]` untuk format tanggal → **PROBLEM**: `toISOString()` konversi ke UTC, sehingga waktu WIB (UTC+7) mundur 1 hari
- Validasi backend hanya `after_or_equal:today` → bisa submit untuk hari ini
- Tidak ada pengecekan duplikat tanggal cuti per employee
## Perubahan
### 1. Frontend — Fix Timezone (`index.tsx`)
- Tambah helper function `toLocalDateString()` yang menggunakan `getFullYear()`, `getMonth()`, `getDate()` (local time) alih-alih `toISOString()` (UTC)
- Ganti semua `toISOString().split('T')[0]``toLocalDateString(...)`
### 2. Backend — Validasi Stricter (`LeaveRequestRequest.php`)
- `start_date`: `after_or_equal:today``after:today` (tidak bisa hari ini)
- Tambah custom closure rule: cek apakah employee sudah punya leave request dengan `start_date` yang sama (exclude status `cancelled` dan record sendiri saat update)
- Error message: "Anda sudah mengajukan cuti pada tanggal ini."
## Cara Kerja
1. User pilih tanggal 8 Agustus → DatePicker simpan sebagai `Date` object (local time)
2. `toLocalDateString()` format sebagai `"2026-08-08"` tanpa konversi UTC
3. Backend validasi: pastikan tanggal > hari ini + belum ada cuti di tanggal yang sama
4. Service simpan ke DB dengan cast `date:Y-m-d` → tetap `"2026-08-08"`

View File

@ -52,6 +52,13 @@ type Props = {
}; };
}; };
function toLocalDateString(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
export default function LeaveRequestIndex({ leaveRequests, filters, filterOptions }: Props) { export default function LeaveRequestIndex({ leaveRequests, filters, filterOptions }: Props) {
const { can } = useCan(); const { can } = useCan();
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
@ -225,9 +232,7 @@ export default function LeaveRequestIndex({ leaveRequests, filters, filterOption
name="start_date" name="start_date"
value={ value={
startDate startDate
? startDate ? toLocalDateString(startDate)
.toISOString()
.split('T')[0]
: '' : ''
} }
/> />
@ -248,9 +253,7 @@ export default function LeaveRequestIndex({ leaveRequests, filters, filterOption
name="end_date" name="end_date"
value={ value={
endDate endDate
? endDate ? toLocalDateString(endDate)
.toISOString()
.split('T')[0]
: '' : ''
} }
/> />
@ -312,9 +315,7 @@ export default function LeaveRequestIndex({ leaveRequests, filters, filterOption
name="start_date" name="start_date"
value={ value={
editingStartDate editingStartDate
? editingStartDate ? toLocalDateString(editingStartDate)
.toISOString()
.split('T')[0]
: '' : ''
} }
/> />
@ -337,9 +338,7 @@ export default function LeaveRequestIndex({ leaveRequests, filters, filterOption
name="end_date" name="end_date"
value={ value={
editingEndDate editingEndDate
? editingEndDate ? toLocalDateString(editingEndDate)
.toISOString()
.split('T')[0]
: '' : ''
} }
/> />