Files
webstatement/resources/views/statements/index.blade.php
Daeng Deni Mardaeni 4616137e0c feat(webstatement): tambahkan fitur konfirmasi email dan optimasi proses download statement
- **Penambahan Fitur Konfirmasi Email:**
  - Menambahkan event listener untuk form submit:
    - Menampilkan SweetAlert jika field email telah diisi.
    - Mengonfirmasi pengiriman statement ke alamat email yang diisi pengguna.
    - Submit form hanya setelah user mengonfirmasi.

- **Optimalisasi Proses Download Statement:**
  - Menangani logic download statement dalam rentang periode (period range):
    - Mencatat log keberadaan file untuk setiap periode.
    - Membuat file ZIP yang berisi semua file statement yang tersedia dalam rentang tersebut.
    - Mengelola file sementara untuk proses kompresi dengan pembersihan otomatis.
    - Menambahkan log error dan warning untuk file yang hilang dalam rentang periode.
    - Mendukung mekanisme download file tunggal untuk periode tertentu.
  - Menyesuaikan log dengan detail proses, seperti:
    - Informasi periode yang tersedia dan tidak.
    - Notifikasi penyelesaian atau kegagalan proses download ZIP.
  - Menambahkan logging trace pada exception untuk debugging lebih rinci.

- **Perubahan Validasi Logic:**
  - Validasi baru pada `PrintStatementRequest`:
    - Menentukan `is_period_range` hanya jika `period_to` berbeda dengan `period_from`.

- **Perbaikan dan Penyesuaian Pengiriman Email:**
  - Menambahkan pengecekan field email sebelum menjalankan fungsi kirim email di `PrintStatementController`.
  - Mengintegrasikan fungsi `sendEmail` jika terdapat email pada statement.

- **Penambahan Dokumentasi Kode:**
  - Menambahkan komentar inline di beberapa bagian:
    - Logika konfirmasi email.
    - Proses pembuatan ZIP dan penanganan download.
  - Menjelaskan tiap langkah operasional untuk mempermudah pemahaman dan debugging.

Perubahan ini mengintegrasikan fitur konfirmasi email yang lebih interaktif, meningkatkan proses download statement berjenjang, serta memperbaiki validasi dan logging pada tiap langkah proses.

Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
2025-06-23 10:24:23 +07:00

418 lines
20 KiB
PHP

@extends('layouts.main')
@section('breadcrumbs')
{{ Breadcrumbs::render(request()->route()->getName()) }}
@endsection
@section('content')
<div class="grid grid-cols-8 gap-5">
<div class="col-span-2 card">
<div class="card-header">
<h3 class="card-title">Request Print Stetement</h3>
</div>
<div class="card-body">
<form
action="{{ isset($statement) ? route('statements.update', $statement->id) : route('statements.store') }}"
method="POST">
@csrf
@if (isset($statement))
@method('PUT')
@endif
<div class="grid grid-cols-1 gap-5">
@if ($multiBranch)
<div class="form-group">
<label class="form-label required" for="branch_id">Branch/Cabang</label>
<select class="input form-control tomselect @error('branch_id') is-invalid @enderror"
id="branch_id" name="branch_id" required>
<option value="">Pilih Branch/Cabang</option>
@foreach ($branches as $branchOption)
<option value="{{ $branchOption->code }}"
{{ old('branch_id', $statement->branch_id ?? ($branch->code ?? '')) == $branchOption->code ? 'selected' : '' }}>
{{ $branchOption->code }} - {{ $branchOption->name }}
</option>
@endforeach
</select>
@error('branch_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
@else
<div class="form-group">
<label class="form-label" for="branch_display">Branch/Cabang</label>
<input type="text" class="input form-control" id="branch_display"
value="{{ $branch->code ?? '' }} - {{ $branch->name ?? '' }}" readonly>
<input type="hidden" name="branch_id" value="{{ $branch->code ?? '' }}">
</div>
@endif
<div class="form-group">
<label class="form-label required" for="account_number">Account Number</label>
<input type="text" class="input form-control @error('account_number') is-invalid @enderror"
id="account_number" name="account_number"
value="{{ old('account_number', $statement->account_number ?? '') }}" required>
@error('account_number')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="form-group">
<label class="form-label" for="email">Email</label>
<input type="email" class="input form-control @error('email') is-invalid @enderror"
id="email" name="email" value="{{ old('email', $statement->email ?? '') }}"
placeholder="Optional email for send statement">
@error('email')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="form-group">
<label class="form-label required" for="start_date">Start Date</label>
<input class="input @error('period_from') border-danger bg-danger-light @enderror"
type="month" name="period_from"
value="{{ $statement->period_from ?? old('period_from') }}"
max="{{ date('Y-m', strtotime('-1 month')) }}">
@error('period_from')
<em class="text-sm alert text-danger">{{ $message }}</em>
@enderror
</div>
<div class="form-group">
<label class="form-label required" for="end_date">End Date</label>
<input class="input @error('period_to') border-danger bg-danger-light @enderror" type="month"
name="period_to" value="{{ $statement->period_to ?? old('period_to') }}"
max="{{ date('Y-m', strtotime('-1 month')) }}">
@error('period_to')
<em class="text-sm alert text-danger">{{ $message }}</em>
@enderror
</div>
</div>
<div class="mt-5 text-end">
<button type="reset" class="btn btn-light me-3">Reset</button>
<button type="submit" class="btn btn-primary">
<i class="ki-outline ki-check fs-2"></i> Submit
</button>
</div>
</form>
</div>
</div>
<div class="col-span-6">
<div class="min-w-full card card-grid" data-datatable="false" data-datatable-page-size="10"
data-datatable-state-save="false" id="statement-table" data-api-url="{{ route('statements.datatables') }}">
<div class="flex-wrap py-5 card-header">
<h3 class="card-title">
Daftar Statement Request
</h3>
<div class="flex flex-wrap gap-2 lg:gap-5">
<div class="flex">
<label class="input input-sm"> <i class="ki-filled ki-magnifier"> </i>
<input placeholder="Search Statement" id="search" type="text" value="">
</label>
</div>
</div>
</div>
<div class="card-body">
<div class="scrollable-x-auto">
<table class="table text-sm font-medium text-gray-700 align-middle table-auto table-border"
data-datatable-table="true">
<thead>
<tr>
<th class="w-14">
<input class="checkbox checkbox-sm" data-datatable-check="true" type="checkbox" />
</th>
<th class="min-w-[100px]" data-datatable-column="id">
<span class="sort"> <span class="sort-label"> ID </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="branch_name">
<span class="sort"> <span class="sort-label"> Branch </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="account_number">
<span class="sort"> <span class="sort-label"> Account Number </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="period">
<span class="sort"> <span class="sort-label"> Period </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="is_available">
<span class="sort"> <span class="sort-label"> Available </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="remarks">
<span class="sort"> <span class="sort-label"> Notes </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[180px]" data-datatable-column="created_at">
<span class="sort"> <span class="sort-label"> Created At </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[50px] text-center" data-datatable-column="actions">Action</th>
</tr>
</thead>
</table>
</div>
<div
class="flex-col gap-3 justify-center font-medium text-gray-600 card-footer md:justify-between md:flex-row text-2sm">
<div class="flex gap-2 items-center">
Show
<select class="w-16 select select-sm" data-datatable-size="true" name="perpage"> </select>
per page
</div>
<div class="flex gap-4 items-center">
<span data-datatable-info="true"> </span>
<div class="pagination" data-datatable-pagination="true">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script type="text/javascript">
/**
* Fungsi untuk menghapus data statement
* @param {number} data - ID statement yang akan dihapus
*/
function deleteData(data) {
Swal.fire({
title: 'Are you sure?',
text: "You won't be able to revert this!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, delete it!'
}).then((result) => {
if (result.isConfirmed) {
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}'
}
});
$.ajax(`statements/${data}`, {
type: 'DELETE'
}).then((response) => {
swal.fire('Deleted!', 'Statement request has been deleted.', 'success').then(() => {
window.location.reload();
});
}).catch((error) => {
console.error('Error:', error);
Swal.fire('Error!', 'An error occurred while deleting the record.', 'error');
});
}
})
}
/**
* Konfirmasi email sebelum submit form
* Menampilkan SweetAlert jika email diisi untuk konfirmasi pengiriman
*/
document.addEventListener('DOMContentLoaded', function() {
const form = document.querySelector('form');
const emailInput = document.getElementById('email');
// Log: Inisialisasi event listener untuk konfirmasi email
console.log('Email confirmation listener initialized');
form.addEventListener('submit', function(e) {
const emailValue = emailInput.value.trim();
// Jika email diisi, tampilkan konfirmasi
if (emailValue) {
e.preventDefault(); // Hentikan submit form sementara
// Log: Email terdeteksi, menampilkan konfirmasi
console.log('Email detected:', emailValue);
Swal.fire({
title: 'Konfirmasi Pengiriman Email',
text: `Apakah Anda yakin ingin mengirimkan statement ke email: ${emailValue}?`,
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Ya, Kirim Email',
cancelButtonText: 'Batal',
reverseButtons: true
}).then((result) => {
if (result.isConfirmed) {
// Log: User konfirmasi pengiriman email
console.log('User confirmed email sending');
// Submit form setelah konfirmasi
form.submit();
} else {
// Log: User membatalkan pengiriman email
console.log('User cancelled email sending');
}
});
} else {
// Log: Tidak ada email, submit form normal
console.log('No email provided, submitting form normally');
}
});
});
</script>
<script type="module">
const element = document.querySelector('#statement-table');
const searchInput = document.getElementById('search');
const apiUrl = element.getAttribute('data-api-url');
const dataTableOptions = {
apiEndpoint: apiUrl,
pageSize: 10,
columns: {
select: {
render: (item, data, context) => {
const checkbox = document.createElement('input');
checkbox.className = 'checkbox checkbox-sm';
checkbox.type = 'checkbox';
checkbox.value = data.id.toString();
checkbox.setAttribute('data-datatable-row-check', 'true');
return checkbox.outerHTML.trim();
},
},
id: {
title: 'ID',
},
branch_name: {
title: 'Branch',
},
account_number: {
title: 'Account Number',
},
period: {
title: 'Period',
render: (item, data) => {
const monthNames = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
];
const formatPeriod = (period) => {
if (!period) return '';
const year = period.substring(0, 4);
const month = parseInt(period.substring(4, 6));
return `${monthNames[month - 1]} ${year}`;
};
const fromPeriod = formatPeriod(data.period_from);
const toPeriod = data.period_to ? ` - ${formatPeriod(data.period_to)}` : '';
return fromPeriod + toPeriod;
},
},
is_available: {
title: 'Available',
render: (item, data) => {
let statusClass = data.is_available ? 'badge badge-light-success' :
'badge badge-light-danger';
let statusText = data.is_available ? 'Yes' : 'No';
return `<span class="${statusClass}">${statusText}</span>`;
},
},
remarks: {
title: 'Notes',
},
created_at: {
title: 'Created At',
render: (item, data) => {
return data.created_at ?? '';
},
},
actions: {
title: 'Actions',
render: (item, data) => {
let buttons = `<div class="flex flex-nowrap justify-center">
<a class="btn btn-sm btn-icon btn-clear btn-info" href="statements/${data.id}">
<i class="ki-outline ki-eye"></i>
</a>`;
// Show download button if statement is approved and available but not downloaded
//if (data.authorization_status === 'approved' && data.is_available && !data.is_downloaded) {
if (data.is_available) {
buttons += `<a class="btn btn-sm btn-icon btn-clear btn-success" href="statements/${data.id}/download">
<i class="ki-outline ki-cloud-download"></i>
</a>`;
}
// Show send email button if email is not empty and statement is available
if (data.is_available && data.email) {
buttons += `<a class="btn btn-sm btn-icon btn-clear btn-primary" href="statements/${data.id}/send-email" title="Send to Email">
<i class="ki-outline ki-paper-plane"></i>
</a>`;
}
// Only show delete button if status is pending
if (data.authorization_status === 'pending') {
buttons += `<a onclick="deleteData(${data.id})" class="delete btn btn-sm btn-icon btn-clear btn-danger">
<i class="ki-outline ki-trash"></i>
</a>`;
}
buttons += `</div>`;
return buttons;
},
}
},
};
let dataTable = new KTDataTable(element, dataTableOptions);
// Custom search functionality
searchInput.addEventListener('input', function() {
const searchValue = this.value.trim();
dataTable.search(searchValue, true);
});
// Handle the "select all" checkbox
const selectAllCheckbox = document.querySelector('input[data-datatable-check="true"]');
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', function() {
const isChecked = this.checked;
const rowCheckboxes = document.querySelectorAll('input[data-datatable-row-check="true"]');
rowCheckboxes.forEach(checkbox => {
checkbox.checked = isChecked;
});
});
}
</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Validate that end date is after start date
const startDateInput = document.getElementById('start_date');
const endDateInput = document.getElementById('end_date');
function validateDates() {
const startDate = new Date(startDateInput.value);
const endDate = new Date(endDateInput.value);
if (startDate > endDate) {
endDateInput.setCustomValidity('End date must be after start date');
} else {
endDateInput.setCustomValidity('');
}
}
startDateInput.addEventListener('change', validateDates);
endDateInput.addEventListener('change', validateDates);
// Set max date for date inputs to today
const today = new Date().toISOString().split('T')[0];
startDateInput.setAttribute('max', today);
endDateInput.setAttribute('max', today);
});
</script>
@endpush