feat(holidaycalendar): implement full feature set for holiday calendar management

- Mengganti namespace model `HolidayCalendar` dari `Entities` ke `Models`.
- Menambahkan validasi izin untuk semua aksi CRUD dan ekspor pada `HolidayCalendarController`.
- Mengintegrasikan fitur izin pada tombol aksi (create, update, delete, export) di view `index.blade.php`.
- Mengupdate logika form view `create.blade.php` untuk mendukung pengelolaan izin dan action dinamis.
- Menambahkan class test `HolidayCalendarControllerTest` dengan pengujian lengkap mencakup:
  - Hak akses untuk membaca, membuat, memperbarui, menghapus, dan mengekspor data.
  - Validasi data saat penyimpanan/pembaruan.
  - Validasi respon HTTP untuk setiap aksi berdasarkan izin.
- Memastikan user tanpa izin akan menerima pesan atau pembatasan akses yang relevan (HTTP 403).
- Fitur ekspor CSV hanya dapat diakses oleh user dengan izin `basic-data.export`.
- Memperbaiki rendering tindakan pada data tabel di `index.blade.php` agar responsif terhadap izin user.

Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
This commit is contained in:
Daeng Deni Mardaeni
2025-05-17 11:34:12 +07:00
parent 52b48263a2
commit 4c6a6d8cea
5 changed files with 439 additions and 74 deletions

View File

@@ -6,7 +6,7 @@ use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithColumnFormatting;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Modules\Basicdata\Entities\HolidayCalendar;
use Modules\Basicdata\Models\HolidayCalendar;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
class HolidayCalendarExport implements WithColumnFormatting, WithHeadings, FromCollection, WithMapping

View File

@@ -12,15 +12,35 @@
class HolidayCalendarController extends Controller
{
public $user;
/**
* Get the authenticated user.
*
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
protected function getUser()
{
return \Illuminate\Support\Facades\Auth::guard('web')->user();
}
public function index()
{
// Check if the authenticated user has the required permission to view holiday calendars
$user = $this->getUser();
if (is_null($user) || !$user->can('basic-data.read')) {
abort(403, 'Sorry! You are not allowed to view holiday calendars.');
}
return view('basicdata::holidaycalendar.index');
}
public function store(HolidayCalendarRequest $request)
{
// Check if the authenticated user has the required permission to create holiday calendars
$user = $this->getUser();
if (is_null($user) || !$user->can('basic-data.create')) {
abort(403, 'Sorry! You are not allowed to create holiday calendars.');
}
$validate = $request->validated();
if ($validate) {
@@ -40,17 +60,35 @@
public function create()
{
// Check if the authenticated user has the required permission to create holiday calendars
$user = $this->getUser();
if (is_null($user) || !$user->can('basic-data.create')) {
abort(403, 'Sorry! You are not allowed to create holiday calendars.');
}
return view('basicdata::holidaycalendar.create');
}
public function edit($id)
{
// Check if the authenticated user has the required permission to update holiday calendars
$user = $this->getUser();
if (is_null($user) || !$user->can('basic-data.update')) {
abort(403, 'Sorry! You are not allowed to update holiday calendars.');
}
$holiday = HolidayCalendar::find($id);
return view('basicdata::holidaycalendar.create', compact('holiday'));
}
public function update(HolidayCalendarRequest $request, $id)
{
// Check if the authenticated user has the required permission to update holiday calendars
$user = $this->getUser();
if (is_null($user) || !$user->can('basic-data.update')) {
abort(403, 'Sorry! You are not allowed to update holiday calendars.');
}
$validate = $request->validated();
if ($validate) {
@@ -74,6 +112,12 @@
public function destroy($id)
{
// Check if the authenticated user has the required permission to delete holiday calendars
$user = $this->getUser();
if (is_null($user) || !$user->can('basic-data.delete')) {
abort(403, 'Sorry! You are not allowed to delete holiday calendars.');
}
try {
$holiday = HolidayCalendar::find($id);
$holiday->delete();
@@ -90,15 +134,23 @@
public function deleteMultiple(Request $request)
{
// Check if the authenticated user has the required permission to delete holiday calendars
$user = $this->getUser();
if (is_null($user) || !$user->can('basic-data.delete')) {
return response()->json(['success' => false, 'message' => 'Sorry! You are not allowed to delete holiday calendars.'], 403);
}
$ids = $request->input('ids');
HolidayCalendar::whereIn('id', $ids)->delete();
return response()->json(['message' => 'Holidays deleted successfully']);
return response()->json(['success' => true, 'message' => 'Holidays deleted successfully']);
}
public function dataForDatatables(Request $request)
{
if (is_null($this->user) || !$this->user->can('currency.view')) {
//abort(403, 'Sorry! You are not allowed to view users.');
// Check if the authenticated user has the required permission to view holiday calendars
$user = $this->getUser();
if (is_null($user) || !$user->can('basic-data.read')) {
return response()->json(['success' => false, 'message' => 'Sorry! You are not allowed to view holiday calendars.'], 403);
}
// Retrieve data from the database
@@ -159,6 +211,12 @@
public function export()
{
// Check if the authenticated user has the required permission to export holiday calendars
$user = $this->getUser();
if (is_null($user) || !$user->can('basic-data.export')) {
abort(403, 'Sorry! You are not allowed to export holiday calendars.');
}
return Excel::download(new HolidayCalendarExport, 'holiday_calendar.xlsx');
}
}

View File

@@ -6,71 +6,79 @@
@section('content')
<div class="w-full grid gap-5 lg:gap-7.5 mx-auto">
@if(isset($holiday->id))
<form action="{{ route('basicdata.holidaycalendar.update', $holiday->id) }}" method="POST">
<form method="POST" action="{{ isset($holiday->id) ? route('basicdata.holidaycalendar.update', $holiday->id) : route('basicdata.holidaycalendar.store') }}">
@csrf
@if(isset($holiday->id))
<input type="hidden" name="id" value="{{ $holiday->id }}">
@method('PUT')
@else
<form method="POST" action="{{ route('basicdata.holidaycalendar.store') }}">
@endif
@csrf
<div class="card pb-2.5">
<div class="card-header" id="basic_settings">
<h3 class="card-title">
{{ isset($holiday->id) ? 'Edit' : 'Tambah' }} Hari Libur
</h3>
<div class="flex items-center gap-2">
<a href="{{ route('basicdata.holidaycalendar.index') }}" class="btn btn-xs btn-info"><i class="ki-filled ki-exit-left"></i> Back</a>
</div>
</div>
<div class="card-body grid gap-5">
<div class="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
<label class="form-label max-w-56">
Tanggal
</label>
<div class="flex flex-wrap items-baseline w-full">
<input class="input @error('date') border-danger bg-danger-light @enderror" type="date" name="date" value="{{ old('date', isset($holiday) ? $holiday->date->format('Y-m-d') : '') }}">
@error('date')
<em class="alert text-danger text-sm">{{ $message }}</em>
@enderror
</div>
</div>
<div class="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
<label class="form-label max-w-56">
Deskripsi
</label>
<div class="flex flex-wrap items-baseline w-full">
<input class="input @error('description') border-danger bg-danger-light @enderror"
type="text"
name="description"
value="{{ old('description', $holiday->description ?? '') }}">
@error('description')
<em class="alert text-danger text-sm">{{ $message }}</em>
@enderror
</div>
</div>
<div class="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
<label class="form-label max-w-56">
Tipe
</label>
<div class="flex flex-wrap items-baseline w-full">
<select class="select @error('type') border-danger bg-danger-light @enderror" name="type">
<option value="">Pilih Tipe</option>
<option value="national_holiday" {{ old('type', $holiday->type ?? '') == 'national_holiday' ? 'selected' : '' }}>Nasional</option>
<option value="collective_leave" {{ old('type', $holiday->type ?? '') == 'collective_leave' ? 'selected' : '' }}>Cuti Bersama</option>
</select>
@error('type')
<em class="alert text-danger text-sm">{{ $message }}</em>
@enderror
</div>
</div>
<div class="flex justify-end">
<button type="submit" class="btn btn-primary">
Save
</button>
</div>
</div>
@endif
<div class="card pb-2.5">
<div class="card-header" id="basic_settings">
<h3 class="card-title">
{{ isset($holiday->id) ? 'Edit' : 'Tambah' }} Hari Libur
</h3>
<div class="flex items-center gap-2">
<a href="{{ route('basicdata.holidaycalendar.index') }}" class="btn btn-xs btn-info"><i class="ki-filled ki-exit-left"></i> Back</a>
</div>
</div>
<div class="card-body grid gap-5">
<div class="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
<label class="form-label max-w-56">
Tanggal
</label>
<div class="flex flex-wrap items-baseline w-full">
<input class="input @error('date') border-danger bg-danger-light @enderror" type="date" name="date" value="{{ old('date', isset($holiday) ? $holiday->date->format('Y-m-d') : '') }}">
@error('date')
<em class="alert text-danger text-sm">{{ $message }}</em>
@enderror
</div>
</form>
</div>
<div class="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
<label class="form-label max-w-56">
Deskripsi
</label>
<div class="flex flex-wrap items-baseline w-full">
<input class="input @error('description') border-danger bg-danger-light @enderror"
type="text"
name="description"
value="{{ old('description', $holiday->description ?? '') }}">
@error('description')
<em class="alert text-danger text-sm">{{ $message }}</em>
@enderror
</div>
</div>
<div class="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
<label class="form-label max-w-56">
Tipe
</label>
<div class="flex flex-wrap items-baseline w-full">
<select class="select @error('type') border-danger bg-danger-light @enderror" name="type">
<option value="">Pilih Tipe</option>
<option value="national_holiday" {{ old('type', $holiday->type ?? '') == 'national_holiday' ? 'selected' : '' }}>Nasional</option>
<option value="collective_leave" {{ old('type', $holiday->type ?? '') == 'collective_leave' ? 'selected' : '' }}>Cuti Bersama</option>
</select>
@error('type')
<em class="alert text-danger text-sm">{{ $message }}</em>
@enderror
</div>
</div>
<div class="flex justify-end">
@if(isset($holiday->id))
@can('basic-data.update')
<button type="submit" class="btn btn-primary">
Save
</button>
@endcan
@else
@can('basic-data.create')
<button type="submit" class="btn btn-primary">
Save
</button>
@endcan
@endif
</div>
</div>
</div>
</form>
</div>
@endsection

View File

@@ -19,9 +19,15 @@
</div>
<div class="flex flex-wrap gap-2.5">
<div class="h-[24px] border border-r-gray-200"></div>
@can('basic-data.export')
<a class="btn btn-sm btn-light" href="{{ route('basicdata.holidaycalendar.export') }}"> Export to Excel </a>
@endcan
@can('basic-data.create')
<a class="btn btn-sm btn-primary" href="{{ route('basicdata.holidaycalendar.create') }}"> Tambah Hari Libur </a>
@endcan
@can('basic-data.delete')
<button class="btn btn-sm btn-danger hidden" id="deleteSelected" onclick="deleteSelectedRows()">Delete Selected</button>
@endcan
</div>
</div>
</div>
@@ -177,14 +183,22 @@
actions: {
title: 'Action',
render: (item, data) => {
return `<div class="flex flex-nowrap justify-center">
<a class="btn btn-sm btn-icon btn-clear btn-info" href="basic-data/holidaycalendar/${data.id}/edit">
let html = `<div class="flex flex-nowrap justify-center">`;
@can('basic-data.update')
html += `<a class="btn btn-sm btn-icon btn-clear btn-info" href="basic-data/holidaycalendar/${data.id}/edit">
<i class="ki-outline ki-notepad-edit"></i>
</a>
<a onclick="deleteData(${data.id})" class="delete btn btn-sm btn-icon btn-clear btn-danger">
</a>`;
@endcan
@can('basic-data.delete')
html += `<a onclick="deleteData(${data.id})" class="delete btn btn-sm btn-icon btn-clear btn-danger">
<i class="ki-outline ki-trash"></i>
</a>
</div>`;
</a>`;
@endcan
html += `</div>`;
return html;
},
}
},

View File

@@ -0,0 +1,285 @@
<?php
namespace Modules\Basicdata\Tests\Feature;
use Tests\TestCase;
use Modules\Basicdata\Models\HolidayCalendar;
use Modules\Usermanagement\Models\User;
use Modules\Usermanagement\Models\Role;
use Modules\Usermanagement\Models\Permission;
use Modules\Usermanagement\Models\PermissionGroup;
use Illuminate\Foundation\Testing\RefreshDatabase;
use PHPUnit\Framework\Attributes\Test;
class HolidayCalendarControllerTest extends TestCase
{
use RefreshDatabase;
protected $user;
protected $adminRole;
protected $holiday;
protected function setUp(): void
{
parent::setUp();
// Create permission group first
$permissionGroup = PermissionGroup::create([
'name' => 'basic-data',
'slug' => 'basic-data'
]);
// Create permissions with permission_group_id
Permission::create([
'name' => 'basic-data.create',
'guard_name' => 'web',
'permission_group_id' => $permissionGroup->id
]);
Permission::create([
'name' => 'basic-data.read',
'guard_name' => 'web',
'permission_group_id' => $permissionGroup->id
]);
Permission::create([
'name' => 'basic-data.update',
'guard_name' => 'web',
'permission_group_id' => $permissionGroup->id
]);
Permission::create([
'name' => 'basic-data.delete',
'guard_name' => 'web',
'permission_group_id' => $permissionGroup->id
]);
Permission::create([
'name' => 'basic-data.export',
'guard_name' => 'web',
'permission_group_id' => $permissionGroup->id
]);
// Create admin role with all permissions
$this->adminRole = Role::create(['name' => 'admin', 'guard_name' => 'web']);
$this->adminRole->givePermissionTo(Permission::all());
// Create a user with admin role
$this->user = User::factory()->create();
$this->user->assignRole($this->adminRole);
// Create a holiday calendar for testing
$this->holiday = HolidayCalendar::create([
'date' => '2023-01-01',
'description' => 'New Year',
'type' => 'national_holiday'
]);
}
#[Test]
public function user_with_permission_can_view_holidays_index()
{
$response = $this->actingAs($this->user)
->get(route('basicdata.holidaycalendar.index'));
$response->assertStatus(200);
}
#[Test]
public function user_without_permission_cannot_view_holidays_index()
{
// Create a role without permissions
$role = Role::create(['name' => 'viewer', 'guard_name' => 'web']);
// Create a user with the viewer role
$user = User::factory()->create();
$user->assignRole($role);
$response = $this->actingAs($user)
->get(route('basicdata.holidaycalendar.index'));
$response->assertStatus(403);
}
#[Test]
public function user_with_permission_can_create_holiday()
{
$response = $this->actingAs($this->user)
->get(route('basicdata.holidaycalendar.create'));
$response->assertStatus(200);
}
#[Test]
public function user_without_permission_cannot_create_holiday()
{
// Create a role with only read permission
$role = Role::create(['name' => 'reader', 'guard_name' => 'web']);
$role->givePermissionTo('basic-data.read');
// Create a user with the reader role
$user = User::factory()->create();
$user->assignRole($role);
$response = $this->actingAs($user)
->get(route('basicdata.holidaycalendar.create'));
$response->assertStatus(403);
}
#[Test]
public function user_with_permission_can_store_holiday()
{
$holidayData = [
'date' => '2023-12-25',
'description' => 'Christmas',
'type' => 'national_holiday'
];
$response = $this->actingAs($this->user)
->post(route('basicdata.holidaycalendar.store'), $holidayData);
$response->assertRedirect(route('basicdata.holidaycalendar.index'));
$this->assertDatabaseHas('holiday_calendars', $holidayData);
}
#[Test]
public function user_without_permission_cannot_store_holiday()
{
// Create a role with only read permission
$role = Role::create(['name' => 'reader', 'guard_name' => 'web']);
$role->givePermissionTo('basic-data.read');
// Create a user with the reader role
$user = User::factory()->create();
$user->assignRole($role);
$holidayData = [
'date' => '2023-12-25',
'description' => 'Christmas',
'type' => 'national_holiday'
];
$response = $this->actingAs($user)
->post(route('basicdata.holidaycalendar.store'), $holidayData);
$response->assertStatus(403);
$this->assertDatabaseMissing('holiday_calendars', $holidayData);
}
#[Test]
public function user_with_permission_can_edit_holiday()
{
$response = $this->actingAs($this->user)
->get(route('basicdata.holidaycalendar.edit', $this->holiday->id));
$response->assertStatus(200);
}
#[Test]
public function user_without_permission_cannot_edit_holiday()
{
// Create a role with only read permission
$role = Role::create(['name' => 'reader', 'guard_name' => 'web']);
$role->givePermissionTo('basic-data.read');
// Create a user with the reader role
$user = User::factory()->create();
$user->assignRole($role);
$response = $this->actingAs($user)
->get(route('basicdata.holidaycalendar.edit', $this->holiday->id));
$response->assertStatus(403);
}
#[Test]
public function user_with_permission_can_update_holiday()
{
$updatedData = [
'date' => '2023-01-01',
'description' => 'New Year Updated',
'type' => 'collective_leave'
];
$response = $this->actingAs($this->user)
->put(route('basicdata.holidaycalendar.update', $this->holiday->id), $updatedData);
$response->assertRedirect(route('basicdata.holidaycalendar.index'));
$this->assertDatabaseHas('holiday_calendars', $updatedData);
}
#[Test]
public function user_without_permission_cannot_update_holiday()
{
// Create a role with only read permission
$role = Role::create(['name' => 'reader', 'guard_name' => 'web']);
$role->givePermissionTo('basic-data.read');
// Create a user with the reader role
$user = User::factory()->create();
$user->assignRole($role);
$updatedData = [
'date' => '2023-01-01',
'description' => 'New Year Updated',
'type' => 'collective_leave'
];
$response = $this->actingAs($user)
->put(route('basicdata.holidaycalendar.update', $this->holiday->id), $updatedData);
$response->assertStatus(403);
$this->assertDatabaseMissing('holiday_calendars', $updatedData);
}
#[Test]
public function user_with_permission_can_delete_holiday()
{
$response = $this->actingAs($this->user)
->delete(route('basicdata.holidaycalendar.destroy', $this->holiday->id));
$response->assertRedirect(route('basicdata.holidaycalendar.index'));
$this->assertSoftDeleted($this->holiday);
}
#[Test]
public function user_without_permission_cannot_delete_holiday()
{
// Create a role with only read permission
$role = Role::create(['name' => 'reader', 'guard_name' => 'web']);
$role->givePermissionTo('basic-data.read');
// Create a user with the reader role
$user = User::factory()->create();
$user->assignRole($role);
$response = $this->actingAs($user)
->delete(route('basicdata.holidaycalendar.destroy', $this->holiday->id));
$response->assertStatus(403);
$this->assertDatabaseHas('holiday_calendars', ['id' => $this->holiday->id, 'deleted_at' => null]);
}
#[Test]
public function user_with_permission_can_export_holidays()
{
$response = $this->actingAs($this->user)
->get(route('basicdata.holidaycalendar.export'));
$response->assertStatus(200);
}
#[Test]
public function user_without_permission_cannot_export_holidays()
{
// Create a role with only read permission
$role = Role::create(['name' => 'reader', 'guard_name' => 'web']);
$role->givePermissionTo('basic-data.read');
// Create a user with the reader role
$user = User::factory()->create();
$user->assignRole($role);
$response = $this->actingAs($user)
->get(route('basicdata.holidaycalendar.export'));
$response->assertStatus(403);
}
}