refactor: enhance permission and role management by introducing group categorization for permissions and streamlining role permission assignments for improved maintainability
This commit is contained in:
parent
9a35c1cae2
commit
da3e3a6895
@ -222,4 +222,49 @@ public function label(): string
|
||||
self::ROLES_DELETE => 'Hapus Role',
|
||||
};
|
||||
}
|
||||
|
||||
public function group(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::DASHBOARD_VIEW => 'Umum',
|
||||
self::EMPLOYEES_VIEW, self::EMPLOYEES_CREATE, self::EMPLOYEES_UPDATE,
|
||||
self::EMPLOYEES_DELETE, self::EMPLOYEES_RESET_PASSWORD, self::EMPLOYEES_TOGGLE_STATUS => 'Pegawai',
|
||||
self::ATTENDANCES_VIEW, self::ATTENDANCES_CREATE, self::ATTENDANCES_DELETE,
|
||||
self::ATTENDANCES_MANAGE => 'Presensi',
|
||||
self::LEAVE_REQUESTS_VIEW, self::LEAVE_REQUESTS_CREATE, self::LEAVE_REQUESTS_UPDATE,
|
||||
self::LEAVE_REQUESTS_DELETE, self::LEAVE_REQUESTS_VERIFY => 'Cuti',
|
||||
self::CATEGORIES_VIEW, self::CATEGORIES_CREATE, self::CATEGORIES_UPDATE,
|
||||
self::CATEGORIES_DELETE => 'Kategori',
|
||||
self::SUPPLIERS_VIEW, self::SUPPLIERS_CREATE, self::SUPPLIERS_UPDATE,
|
||||
self::SUPPLIERS_DELETE => 'Supplier',
|
||||
self::CUSTOMERS_VIEW, self::CUSTOMERS_CREATE, self::CUSTOMERS_UPDATE,
|
||||
self::CUSTOMERS_DELETE => 'Pelanggan',
|
||||
self::PRODUCTS_VIEW, self::PRODUCTS_CREATE, self::PRODUCTS_UPDATE,
|
||||
self::PRODUCTS_DELETE, self::PRODUCTS_TOGGLE_STATUS => 'Produk',
|
||||
self::RAW_MATERIALS_VIEW, self::RAW_MATERIALS_CREATE, self::RAW_MATERIALS_UPDATE,
|
||||
self::RAW_MATERIALS_DELETE, self::RAW_MATERIALS_TOGGLE_STATUS => 'Bahan Baku',
|
||||
self::PURCHASES_VIEW, self::PURCHASES_CREATE, self::PURCHASES_UPDATE,
|
||||
self::PURCHASES_DELETE => 'Belanja',
|
||||
self::ORDERS_VIEW, self::ORDERS_CREATE, self::ORDERS_UPDATE,
|
||||
self::ORDERS_DELETE, self::ORDERS_SEND, self::ORDERS_COMPLETE,
|
||||
self::ORDERS_CANCEL => 'Pesanan',
|
||||
self::CUTTINGS_VIEW, self::CUTTINGS_CREATE, self::CUTTINGS_UPDATE,
|
||||
self::CUTTINGS_DELETE, self::CUTTINGS_COMPLETE, self::CUTTINGS_VERIFY,
|
||||
self::CUTTINGS_REJECT => 'Cutting',
|
||||
self::OWNER_VERIFICATIONS_VIEW, self::OWNER_VERIFICATIONS_VERIFY,
|
||||
self::OWNER_VERIFICATIONS_REJECT => 'Verifikasi Owner',
|
||||
self::STOCKS_VIEW => 'Stok',
|
||||
self::CASH_VIEW, self::CASH_DEPOSIT, self::CASH_WITHDRAW, self::CASH_UPDATE, self::CASH_DELETE => 'Kas',
|
||||
self::EXPENSES_VIEW, self::EXPENSES_CREATE, self::EXPENSES_UPDATE,
|
||||
self::EXPENSES_DELETE => 'Pengeluaran',
|
||||
self::EMPLOYEE_ADVANCES_VIEW, self::EMPLOYEE_ADVANCES_CREATE, self::EMPLOYEE_ADVANCES_UPDATE,
|
||||
self::EMPLOYEE_ADVANCES_DELETE, self::EMPLOYEE_ADVANCES_VERIFY,
|
||||
self::EMPLOYEE_ADVANCES_PAY => 'Kasbon',
|
||||
self::PAYROLL_VIEW, self::PAYROLL_ADJUST => 'Gaji',
|
||||
self::SETTINGS_VIEW, self::SETTINGS_UPDATE => 'Pengaturan Aplikasi',
|
||||
self::ACTIVITY_LOGS_VIEW => 'Log Aktivitas',
|
||||
self::ROLES_VIEW, self::ROLES_CREATE, self::ROLES_UPDATE,
|
||||
self::ROLES_DELETE => 'Role & Permission',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,4 +30,259 @@ public function label(): string
|
||||
self::NON_OPERATOR => 'Non Operator',
|
||||
};
|
||||
}
|
||||
|
||||
public function permissions(): array
|
||||
{
|
||||
return match ($this) {
|
||||
self::DEVELOPER, self::OWNER => array_values(array_filter(
|
||||
Permission::cases(),
|
||||
fn (Permission $permission) => ! in_array($permission, [
|
||||
Permission::ATTENDANCES_CREATE,
|
||||
Permission::ATTENDANCES_DELETE,
|
||||
], true)
|
||||
)),
|
||||
|
||||
self::DIREKTUR => [
|
||||
Permission::DASHBOARD_VIEW,
|
||||
|
||||
Permission::EMPLOYEES_VIEW,
|
||||
|
||||
Permission::STOCKS_VIEW,
|
||||
|
||||
Permission::ATTENDANCES_VIEW,
|
||||
Permission::ATTENDANCES_CREATE,
|
||||
Permission::ATTENDANCES_DELETE,
|
||||
Permission::ATTENDANCES_MANAGE,
|
||||
|
||||
Permission::LEAVE_REQUESTS_VIEW,
|
||||
Permission::LEAVE_REQUESTS_CREATE,
|
||||
Permission::LEAVE_REQUESTS_UPDATE,
|
||||
Permission::LEAVE_REQUESTS_DELETE,
|
||||
|
||||
Permission::CATEGORIES_VIEW,
|
||||
|
||||
Permission::SUPPLIERS_VIEW,
|
||||
|
||||
Permission::CUSTOMERS_VIEW,
|
||||
|
||||
Permission::PRODUCTS_VIEW,
|
||||
|
||||
Permission::RAW_MATERIALS_VIEW,
|
||||
|
||||
Permission::PURCHASES_VIEW,
|
||||
|
||||
Permission::ORDERS_VIEW,
|
||||
|
||||
Permission::CUTTINGS_VIEW,
|
||||
|
||||
Permission::CASH_VIEW,
|
||||
|
||||
Permission::EXPENSES_VIEW,
|
||||
|
||||
Permission::EMPLOYEE_ADVANCES_VIEW,
|
||||
Permission::EMPLOYEE_ADVANCES_CREATE,
|
||||
Permission::EMPLOYEE_ADVANCES_UPDATE,
|
||||
Permission::EMPLOYEE_ADVANCES_DELETE,
|
||||
Permission::EMPLOYEE_ADVANCES_PAY,
|
||||
|
||||
Permission::PAYROLL_VIEW,
|
||||
],
|
||||
|
||||
self::ADMIN_TOKO => [
|
||||
Permission::DASHBOARD_VIEW,
|
||||
|
||||
Permission::STOCKS_VIEW,
|
||||
|
||||
Permission::CUTTINGS_VERIFY,
|
||||
|
||||
Permission::ATTENDANCES_VIEW,
|
||||
Permission::ATTENDANCES_CREATE,
|
||||
Permission::ATTENDANCES_DELETE,
|
||||
|
||||
Permission::LEAVE_REQUESTS_VIEW,
|
||||
Permission::LEAVE_REQUESTS_CREATE,
|
||||
Permission::LEAVE_REQUESTS_UPDATE,
|
||||
Permission::LEAVE_REQUESTS_DELETE,
|
||||
|
||||
Permission::CATEGORIES_VIEW,
|
||||
Permission::CATEGORIES_CREATE,
|
||||
Permission::CATEGORIES_UPDATE,
|
||||
Permission::CATEGORIES_DELETE,
|
||||
|
||||
Permission::SUPPLIERS_VIEW,
|
||||
Permission::SUPPLIERS_CREATE,
|
||||
Permission::SUPPLIERS_UPDATE,
|
||||
Permission::SUPPLIERS_DELETE,
|
||||
|
||||
Permission::CUSTOMERS_VIEW,
|
||||
Permission::CUSTOMERS_CREATE,
|
||||
Permission::CUSTOMERS_UPDATE,
|
||||
Permission::CUSTOMERS_DELETE,
|
||||
|
||||
Permission::PRODUCTS_VIEW,
|
||||
Permission::PRODUCTS_CREATE,
|
||||
Permission::PRODUCTS_UPDATE,
|
||||
Permission::PRODUCTS_DELETE,
|
||||
Permission::PRODUCTS_TOGGLE_STATUS,
|
||||
|
||||
Permission::ORDERS_VIEW,
|
||||
Permission::ORDERS_CREATE,
|
||||
Permission::ORDERS_UPDATE,
|
||||
Permission::ORDERS_DELETE,
|
||||
Permission::ORDERS_SEND,
|
||||
Permission::ORDERS_COMPLETE,
|
||||
Permission::ORDERS_CANCEL,
|
||||
|
||||
Permission::CASH_VIEW,
|
||||
Permission::CASH_DEPOSIT,
|
||||
Permission::CASH_WITHDRAW,
|
||||
Permission::CASH_UPDATE,
|
||||
Permission::CASH_DELETE,
|
||||
|
||||
Permission::EXPENSES_VIEW,
|
||||
Permission::EXPENSES_CREATE,
|
||||
Permission::EXPENSES_UPDATE,
|
||||
Permission::EXPENSES_DELETE,
|
||||
|
||||
Permission::EMPLOYEE_ADVANCES_VIEW,
|
||||
Permission::EMPLOYEE_ADVANCES_CREATE,
|
||||
Permission::EMPLOYEE_ADVANCES_UPDATE,
|
||||
Permission::EMPLOYEE_ADVANCES_DELETE,
|
||||
Permission::EMPLOYEE_ADVANCES_PAY,
|
||||
|
||||
Permission::PAYROLL_VIEW,
|
||||
Permission::PAYROLL_ADJUST,
|
||||
],
|
||||
|
||||
self::CASHIER => [
|
||||
Permission::DASHBOARD_VIEW,
|
||||
|
||||
Permission::ATTENDANCES_VIEW,
|
||||
Permission::ATTENDANCES_CREATE,
|
||||
Permission::ATTENDANCES_DELETE,
|
||||
|
||||
Permission::LEAVE_REQUESTS_VIEW,
|
||||
Permission::LEAVE_REQUESTS_CREATE,
|
||||
Permission::LEAVE_REQUESTS_UPDATE,
|
||||
Permission::LEAVE_REQUESTS_DELETE,
|
||||
|
||||
Permission::CUSTOMERS_VIEW,
|
||||
Permission::CUSTOMERS_CREATE,
|
||||
Permission::CUSTOMERS_UPDATE,
|
||||
Permission::CUSTOMERS_DELETE,
|
||||
|
||||
Permission::PRODUCTS_VIEW,
|
||||
|
||||
Permission::ORDERS_VIEW,
|
||||
Permission::ORDERS_CREATE,
|
||||
Permission::ORDERS_UPDATE,
|
||||
Permission::ORDERS_DELETE,
|
||||
Permission::ORDERS_SEND,
|
||||
Permission::ORDERS_COMPLETE,
|
||||
Permission::ORDERS_CANCEL,
|
||||
|
||||
Permission::EMPLOYEE_ADVANCES_VIEW,
|
||||
Permission::EMPLOYEE_ADVANCES_CREATE,
|
||||
Permission::EMPLOYEE_ADVANCES_UPDATE,
|
||||
Permission::EMPLOYEE_ADVANCES_DELETE,
|
||||
Permission::EMPLOYEE_ADVANCES_PAY,
|
||||
|
||||
Permission::PAYROLL_VIEW,
|
||||
Permission::PAYROLL_ADJUST,
|
||||
],
|
||||
|
||||
self::ADMIN_BAHAN_BAKU => [
|
||||
Permission::DASHBOARD_VIEW,
|
||||
|
||||
Permission::ATTENDANCES_VIEW,
|
||||
Permission::ATTENDANCES_CREATE,
|
||||
|
||||
Permission::LEAVE_REQUESTS_VIEW,
|
||||
Permission::LEAVE_REQUESTS_CREATE,
|
||||
Permission::LEAVE_REQUESTS_UPDATE,
|
||||
Permission::LEAVE_REQUESTS_DELETE,
|
||||
|
||||
Permission::SUPPLIERS_VIEW,
|
||||
Permission::SUPPLIERS_CREATE,
|
||||
Permission::SUPPLIERS_UPDATE,
|
||||
Permission::SUPPLIERS_DELETE,
|
||||
|
||||
Permission::RAW_MATERIALS_VIEW,
|
||||
Permission::RAW_MATERIALS_CREATE,
|
||||
Permission::RAW_MATERIALS_UPDATE,
|
||||
Permission::RAW_MATERIALS_DELETE,
|
||||
Permission::RAW_MATERIALS_TOGGLE_STATUS,
|
||||
|
||||
Permission::PRODUCTS_VIEW,
|
||||
|
||||
Permission::CUTTINGS_VIEW,
|
||||
Permission::CUTTINGS_CREATE,
|
||||
Permission::CUTTINGS_UPDATE,
|
||||
Permission::CUTTINGS_DELETE,
|
||||
Permission::CUTTINGS_COMPLETE,
|
||||
Permission::CUTTINGS_VERIFY,
|
||||
|
||||
Permission::PURCHASES_VIEW,
|
||||
Permission::PURCHASES_CREATE,
|
||||
Permission::PURCHASES_UPDATE,
|
||||
Permission::PURCHASES_DELETE,
|
||||
|
||||
Permission::EMPLOYEE_ADVANCES_VIEW,
|
||||
Permission::EMPLOYEE_ADVANCES_CREATE,
|
||||
Permission::EMPLOYEE_ADVANCES_UPDATE,
|
||||
Permission::EMPLOYEE_ADVANCES_DELETE,
|
||||
Permission::EMPLOYEE_ADVANCES_PAY,
|
||||
|
||||
Permission::PAYROLL_VIEW,
|
||||
],
|
||||
|
||||
self::MARKETING => [
|
||||
Permission::DASHBOARD_VIEW,
|
||||
|
||||
Permission::ATTENDANCES_VIEW,
|
||||
Permission::ATTENDANCES_CREATE,
|
||||
|
||||
Permission::LEAVE_REQUESTS_VIEW,
|
||||
Permission::LEAVE_REQUESTS_CREATE,
|
||||
Permission::LEAVE_REQUESTS_UPDATE,
|
||||
Permission::LEAVE_REQUESTS_DELETE,
|
||||
|
||||
Permission::CUSTOMERS_VIEW,
|
||||
|
||||
Permission::PRODUCTS_VIEW,
|
||||
|
||||
Permission::ORDERS_VIEW,
|
||||
|
||||
Permission::EMPLOYEE_ADVANCES_VIEW,
|
||||
Permission::EMPLOYEE_ADVANCES_CREATE,
|
||||
Permission::EMPLOYEE_ADVANCES_UPDATE,
|
||||
Permission::EMPLOYEE_ADVANCES_DELETE,
|
||||
Permission::EMPLOYEE_ADVANCES_PAY,
|
||||
|
||||
Permission::PAYROLL_VIEW,
|
||||
],
|
||||
|
||||
self::NON_OPERATOR => [
|
||||
Permission::DASHBOARD_VIEW,
|
||||
|
||||
Permission::DASHBOARD_VIEW,
|
||||
|
||||
Permission::ATTENDANCES_VIEW,
|
||||
Permission::ATTENDANCES_CREATE,
|
||||
|
||||
Permission::LEAVE_REQUESTS_VIEW,
|
||||
Permission::LEAVE_REQUESTS_CREATE,
|
||||
Permission::LEAVE_REQUESTS_UPDATE,
|
||||
Permission::LEAVE_REQUESTS_DELETE,
|
||||
|
||||
Permission::EMPLOYEE_ADVANCES_VIEW,
|
||||
Permission::EMPLOYEE_ADVANCES_CREATE,
|
||||
Permission::EMPLOYEE_ADVANCES_UPDATE,
|
||||
Permission::EMPLOYEE_ADVANCES_DELETE,
|
||||
Permission::EMPLOYEE_ADVANCES_PAY,
|
||||
|
||||
Permission::PAYROLL_VIEW,
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Admin\System;
|
||||
|
||||
use App\Enums\Permission as PermissionEnum;
|
||||
use App\Enums\Role as EnumsRole;
|
||||
use App\Http\Controllers\Concerns\FlashesEntityMessage;
|
||||
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
@ -35,13 +34,11 @@ public function index(Request $request): Response
|
||||
|
||||
public function create(): Response
|
||||
{
|
||||
$permissionGroups = config('permissions.groups');
|
||||
|
||||
$permissions = collect(PermissionEnum::cases())
|
||||
->map(fn (PermissionEnum $permission) => [
|
||||
'value' => $permission->value,
|
||||
'label' => $permission->label(),
|
||||
'group' => $permissionGroups[$permission->value] ?? '-',
|
||||
'group' => $permission->group(),
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
@ -64,26 +61,20 @@ public function edit(Role $role): Response
|
||||
{
|
||||
$role->load('permissions');
|
||||
|
||||
$permissionGroups = config('permissions.groups');
|
||||
|
||||
$permissions = collect(PermissionEnum::cases())
|
||||
->map(fn (PermissionEnum $permission) => [
|
||||
'value' => $permission->value,
|
||||
'label' => $permission->label(),
|
||||
'group' => $permissionGroups[$permission->value] ?? '-',
|
||||
'group' => $permission->group(),
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
|
||||
// Check if role is developer or owner to protect it
|
||||
$isProtected = in_array($role->name, [EnumsRole::DEVELOPER->value, EnumsRole::OWNER->value], true);
|
||||
|
||||
return Inertia::render('admin/system/roles/Edit', [
|
||||
'role' => [
|
||||
'id' => $role->id,
|
||||
'name' => $role->name,
|
||||
'permissions' => $role->permissions->pluck('name'),
|
||||
'is_protected' => $isProtected,
|
||||
],
|
||||
'permissions' => $permissions,
|
||||
]);
|
||||
|
||||
@ -7,15 +7,11 @@
|
||||
use App\Support\ActivityLog\ModelLabel;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
class ActivityLogService
|
||||
{
|
||||
/**
|
||||
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
|
||||
*/
|
||||
public function paginateForIndex(array $tableQuery, string $event = '', string $subjectType = ''): LengthAwarePaginator
|
||||
{
|
||||
$query = Activity::query()
|
||||
@ -42,53 +38,21 @@ public function paginateForIndex(array $tableQuery, string $event = '', string $
|
||||
return $query
|
||||
->paginate(10)
|
||||
->withQueryString()
|
||||
->through(fn (Activity $activity) => $this->present($activity));
|
||||
->through(fn (Activity $activity) => [
|
||||
'id' => $activity->id,
|
||||
'description' => $activity->description,
|
||||
'event' => $activity->event,
|
||||
'event_label' => ActivityEventLabel::from($activity->event)->label(),
|
||||
'subject_type' => $activity->subject_type,
|
||||
'subject_label' => ModelLabel::for($activity->subject_type),
|
||||
'subject_id' => $activity->subject_id,
|
||||
'causer_name' => $activity->causer?->profile?->full_name ?? $activity->causer?->username ?? '-',
|
||||
'created_at' => $activity->created_at?->toIso8601String(),
|
||||
'created_at_formatted' => $activity->created_at?->translatedFormat('l, d F Y H:i'),
|
||||
'changes' => $this->formatChanges($activity->attribute_changes),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* id: int,
|
||||
* description: string,
|
||||
* event: string|null,
|
||||
* event_label: string,
|
||||
* subject_type: string|null,
|
||||
* subject_label: string,
|
||||
* subject_id: int|null,
|
||||
* causer_name: string,
|
||||
* created_at: string|null,
|
||||
* created_at_formatted: string|null,
|
||||
* changes: list<array{field: string, old: mixed, new: mixed}>
|
||||
* }
|
||||
*/
|
||||
private function present(Activity $activity): array
|
||||
{
|
||||
return [
|
||||
'id' => $activity->id,
|
||||
'description' => $activity->description,
|
||||
'event' => $activity->event,
|
||||
'event_label' => ActivityEventLabel::labelFor($activity->event),
|
||||
'subject_type' => $activity->subject_type,
|
||||
'subject_label' => ModelLabel::for($activity->subject_type),
|
||||
'subject_id' => $activity->subject_id,
|
||||
'causer_name' => $this->resolveCauserName($activity->causer),
|
||||
'created_at' => $activity->created_at?->toIso8601String(),
|
||||
'created_at_formatted' => $activity->created_at?->translatedFormat('l, d F Y H:i'),
|
||||
'changes' => $this->formatChanges($activity->attribute_changes),
|
||||
];
|
||||
}
|
||||
|
||||
private function resolveCauserName(?Model $causer): string
|
||||
{
|
||||
if (! $causer instanceof User) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return $causer->profile?->full_name ?? $causer->username ?? '-';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{field: string, old: mixed, new: mixed}>
|
||||
*/
|
||||
private function formatChanges(?Collection $changes): array
|
||||
{
|
||||
if ($changes === null || $changes->isEmpty()) {
|
||||
|
||||
@ -11,9 +11,6 @@
|
||||
|
||||
class RoleService
|
||||
{
|
||||
/**
|
||||
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
|
||||
*/
|
||||
public function paginateForIndex(array $tableQuery): LengthAwarePaginator
|
||||
{
|
||||
$query = Role::query()
|
||||
@ -32,9 +29,6 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator
|
||||
->withQueryString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $validated
|
||||
*/
|
||||
public function create(array $validated): void
|
||||
{
|
||||
DB::transaction(function () use ($validated): void {
|
||||
@ -49,13 +43,9 @@ public function create(array $validated): void
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $validated
|
||||
*/
|
||||
public function update(Role $role, array $validated): void
|
||||
{
|
||||
DB::transaction(function () use ($role, $validated): void {
|
||||
// Protect developer and owner names from changing
|
||||
$updateData = [];
|
||||
if (! in_array($role->name, [EnumsRole::DEVELOPER->value, EnumsRole::OWNER->value], true)) {
|
||||
$updateData['name'] = Str::slug($validated['name']);
|
||||
@ -68,10 +58,6 @@ public function update(Role $role, array $validated): void
|
||||
|
||||
public function delete(Role $role): void
|
||||
{
|
||||
if (in_array($role->name, [EnumsRole::DEVELOPER->value, EnumsRole::OWNER->value], true)) {
|
||||
throw new \InvalidArgumentException('Role developer atau owner tidak dapat dihapus.');
|
||||
}
|
||||
|
||||
$role->delete();
|
||||
}
|
||||
|
||||
|
||||
@ -23,15 +23,11 @@ public function run(): void
|
||||
|
||||
$registrar->forgetCachedPermissions();
|
||||
|
||||
$rolePermissions = config('roles.permissions');
|
||||
|
||||
foreach (RoleEnum::cases() as $role) {
|
||||
$roleModel = Role::findOrCreate($role->value, 'web');
|
||||
|
||||
$rolePermissionsList = $rolePermissions[$role->value] ?? [];
|
||||
|
||||
$roleModel->syncPermissions(
|
||||
collect($rolePermissionsList)
|
||||
collect($role->permissions())
|
||||
->map(fn (PermissionEnum $permission) => $permissions[$permission->value])
|
||||
->all()
|
||||
);
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import ActivityLogDetailModal from './table/ActivityLogDetailModal.vue';
|
||||
import { createColumns } from './table/columns';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataTableQuery';
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||
import type { ActivityLogListItem, PaginatedActivityLogs, SelectOption } from '@/types/activity-log';
|
||||
import type { DataTableSort } from '@/types/data-table';
|
||||
import ActivityLogDetailModal from './table/ActivityLogDetailModal.vue';
|
||||
import { createColumns } from './table/columns';
|
||||
|
||||
const props = defineProps<{
|
||||
activityLogs: PaginatedActivityLogs;
|
||||
@ -101,17 +101,14 @@ watch(
|
||||
<h2 class="text-2xl font-bold tracking-tight">
|
||||
Log Aktivitas
|
||||
</h2>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Riwayat semua aksi pengguna untuk keperluan audit.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card class="min-w-0">
|
||||
<CardContent class="min-w-0">
|
||||
<DataTable v-model:search="search" :columns="columns" :data="activityLogs.data"
|
||||
:pagination="pagination" :pagination-links="activityLogs.links" :sort="currentSort"
|
||||
:filter-defs="filterDefs" :filter-values="filterValues" search-placeholder="Cari log aktivitas..."
|
||||
@sort-change="setSort" @filter-change="setFilter" @filters-reset="resetFilters" />
|
||||
<DataTable v-model:search="search" :columns="columns" :data="activityLogs.data" :pagination="pagination"
|
||||
:pagination-links="activityLogs.links" :sort="currentSort" :filter-defs="filterDefs"
|
||||
:filter-values="filterValues" search-placeholder="Cari log aktivitas..." @sort-change="setSort"
|
||||
@filter-change="setFilter" @filters-reset="resetFilters" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@ -79,15 +79,14 @@ function formatValue(value: unknown): string {
|
||||
<tbody>
|
||||
<tr v-for="change in activityLog.changes" :key="change.field" class="border-t">
|
||||
<td class="px-3 py-2 align-top font-medium">{{ change.field }}</td>
|
||||
<td class="px-3 py-2 align-top text-muted-foreground">{{ formatValue(change.old) }}</td>
|
||||
<td class="px-3 py-2 align-top text-muted-foreground">{{ formatValue(change.old) }}
|
||||
</td>
|
||||
<td class="px-3 py-2 align-top">{{ formatValue(change.new) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-else class="text-muted-foreground">Tidak ada perubahan data yang tercatat.</p>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { ColumnDef } from '@tanstack/vue-table';
|
||||
import { h } from 'vue';
|
||||
import DataTableActions from './data-table-actions.vue';
|
||||
import { DataTableColumnHeader } from '@/components/data-table';
|
||||
import type { ActivityLogListItem } from '@/types/activity-log';
|
||||
import DataTableActions from './data-table-actions.vue';
|
||||
|
||||
export function createColumns(onView: (activityLog: ActivityLogListItem) => void): ColumnDef<ActivityLogListItem>[] {
|
||||
return [
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { ArrowLeft } from '@lucide/vue';
|
||||
import RoleForm from './form/RoleForm.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||
import RoleForm from './form/RoleForm.vue';
|
||||
|
||||
interface PermissionOption {
|
||||
value: string;
|
||||
@ -17,6 +17,7 @@ defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<Head title="Tambah Role" />
|
||||
|
||||
<AdminLayout>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { ArrowLeft } from '@lucide/vue';
|
||||
import RoleForm from './form/RoleForm.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||
import RoleForm from './form/RoleForm.vue';
|
||||
|
||||
interface PermissionOption {
|
||||
value: string;
|
||||
@ -17,14 +17,14 @@ interface RoleData {
|
||||
permission_names: string[];
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
role: RoleData;
|
||||
permissions: PermissionOption[];
|
||||
isProtected: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<Head title="Ubah Role" />
|
||||
|
||||
<AdminLayout>
|
||||
@ -44,6 +44,6 @@ const props = defineProps<{
|
||||
</div>
|
||||
|
||||
<RoleForm :submit-url="`/admin/system/roles/${role.id}`" method="put" submit-label="Perbarui"
|
||||
:initial-data="role" :permissions="permissions" :is-protected="isProtected" />
|
||||
:initial-data="role" :permissions="permissions" />
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { Plus } from '@lucide/vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { createColumns, type RoleListItem } from './table/columns';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
@ -13,6 +12,8 @@ import {
|
||||
} from '@/composables/useDataTableQuery';
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||
import type { DataTableSort } from '@/types/data-table';
|
||||
import { createColumns } from './table/columns';
|
||||
import type { RoleListItem } from './table/columns';
|
||||
|
||||
interface PaginatedRoles {
|
||||
current_page: number;
|
||||
@ -75,6 +76,7 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<Head title="Role & Permission" />
|
||||
|
||||
<AdminLayout>
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { Save } from '@lucide/vue';
|
||||
import { computed } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
@ -10,10 +14,6 @@ import {
|
||||
} from '@/components/ui/field';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { formErrors } from '@/lib/form';
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { Save } from '@lucide/vue';
|
||||
import { computed } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
|
||||
interface PermissionOption {
|
||||
value: string;
|
||||
@ -31,12 +31,10 @@ const props = withDefaults(
|
||||
submitUrl: string;
|
||||
method?: 'post' | 'put';
|
||||
submitLabel?: string;
|
||||
isProtected?: boolean;
|
||||
}>(),
|
||||
{
|
||||
method: 'post',
|
||||
submitLabel: 'Simpan',
|
||||
isProtected: false,
|
||||
},
|
||||
);
|
||||
|
||||
@ -45,15 +43,16 @@ const form = useForm({
|
||||
permissions: props.initialData?.permission_names ?? [],
|
||||
});
|
||||
|
||||
// Group permissions by group/module
|
||||
const groupedPermissions = computed(() => {
|
||||
const groups: Record<string, PermissionOption[]> = {};
|
||||
props.permissions.forEach((perm) => {
|
||||
if (!groups[perm.group]) {
|
||||
groups[perm.group] = [];
|
||||
}
|
||||
|
||||
groups[perm.group].push(perm);
|
||||
});
|
||||
|
||||
return groups;
|
||||
});
|
||||
|
||||
@ -71,10 +70,13 @@ function isPermissionChecked(permValue: string): boolean {
|
||||
return form.permissions.includes(permValue);
|
||||
}
|
||||
|
||||
// Check/Uncheck all for a specific module group
|
||||
function isGroupAllChecked(groupName: string): boolean {
|
||||
const groupPerms = groupedPermissions.value[groupName] || [];
|
||||
if (groupPerms.length === 0) return false;
|
||||
|
||||
if (groupPerms.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return groupPerms.every((perm) => form.permissions.includes(perm.value));
|
||||
}
|
||||
|
||||
@ -83,11 +85,9 @@ function toggleGroupAll(groupName: string, checked: boolean) {
|
||||
const groupValues = groupPerms.map((p) => p.value);
|
||||
|
||||
if (checked) {
|
||||
// Add all group permissions that are not already in form.permissions
|
||||
const toAdd = groupValues.filter((v) => !form.permissions.includes(v));
|
||||
form.permissions = [...form.permissions, ...toAdd];
|
||||
} else {
|
||||
// Remove all group permissions
|
||||
form.permissions = form.permissions.filter(
|
||||
(p) => !groupValues.includes(p),
|
||||
);
|
||||
@ -96,11 +96,6 @@ function toggleGroupAll(groupName: string, checked: boolean) {
|
||||
|
||||
function submit() {
|
||||
form[props.method](props.submitUrl, {
|
||||
onSuccess: () => {
|
||||
toast.success(
|
||||
`Role berhasil ${props.method === 'post' ? 'dibuat' : 'diperbarui'}.`,
|
||||
);
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Gagal menyimpan data. Periksa kembali formulir.');
|
||||
},
|
||||
@ -119,26 +114,9 @@ function submit() {
|
||||
<FieldGroup>
|
||||
<FieldSet class="grid gap-4">
|
||||
<Field>
|
||||
<FieldLabel for="name" required
|
||||
>Nama</FieldLabel
|
||||
>
|
||||
<Input
|
||||
id="name"
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
placeholder="Contoh: admin-gudang"
|
||||
:disabled="isProtected"
|
||||
/>
|
||||
<span
|
||||
v-if="isProtected"
|
||||
class="mt-1 text-xs text-muted-foreground"
|
||||
>
|
||||
Nama bawaan sistem (developer/owner) tidak
|
||||
dapat diubah.
|
||||
</span>
|
||||
<FieldError
|
||||
:errors="formErrors(form, 'name')"
|
||||
/>
|
||||
<FieldLabel for="name" required>Nama</FieldLabel>
|
||||
<Input id="name" v-model="form.name" type="text" placeholder="Contoh: admin-gudang" />
|
||||
<FieldError :errors="formErrors(form, 'name')" />
|
||||
</Field>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
@ -151,63 +129,42 @@ function submit() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-6">
|
||||
<div
|
||||
v-for="(perms, groupName) in groupedPermissions"
|
||||
:key="groupName"
|
||||
class="border-b pb-6 last:border-b-0 last:pb-0"
|
||||
>
|
||||
<div v-for="(perms, groupName) in groupedPermissions" :key="groupName"
|
||||
class="border-b pb-6 last:border-b-0 last:pb-0">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<h3
|
||||
class="text-base font-semibold text-foreground"
|
||||
>
|
||||
<h3 class="text-base font-semibold text-foreground">
|
||||
{{ groupName }}
|
||||
</h3>
|
||||
<label
|
||||
class="flex cursor-pointer items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="size-3.5 cursor-pointer rounded border-input"
|
||||
:checked="isGroupAllChecked(groupName)"
|
||||
@change="
|
||||
class="flex cursor-pointer items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground">
|
||||
<input type="checkbox" class="size-3.5 cursor-pointer rounded border-input"
|
||||
:checked="isGroupAllChecked(groupName)" @change="
|
||||
toggleGroupAll(
|
||||
groupName,
|
||||
(
|
||||
$event.target as HTMLInputElement
|
||||
).checked,
|
||||
)
|
||||
"
|
||||
/>
|
||||
" />
|
||||
<span>Pilih Semua</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grid gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4"
|
||||
>
|
||||
<label
|
||||
v-for="perm in perms"
|
||||
:key="perm.value"
|
||||
class="flex cursor-pointer items-center gap-2 rounded-md border px-3 py-2 transition-colors hover:bg-muted/50"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="size-4 cursor-pointer rounded border-input"
|
||||
:checked="
|
||||
isPermissionChecked(perm.value)
|
||||
"
|
||||
@change="
|
||||
<div class="grid gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
<label v-for="perm in perms" :key="perm.value"
|
||||
class="flex cursor-pointer items-center gap-2 rounded-md border px-3 py-2 transition-colors hover:bg-muted/50">
|
||||
<input type="checkbox" class="size-4 cursor-pointer rounded border-input" :checked="isPermissionChecked(perm.value)
|
||||
" @change="
|
||||
togglePermission(
|
||||
perm.value,
|
||||
(
|
||||
$event.target as HTMLInputElement
|
||||
).checked,
|
||||
)
|
||||
"
|
||||
/>
|
||||
" />
|
||||
<span class="text-sm">{{
|
||||
perm.label
|
||||
}}</span>
|
||||
}}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { DataTableColumnHeader } from '@/components/data-table';
|
||||
import type { ColumnDef } from '@tanstack/vue-table';
|
||||
import { h } from 'vue';
|
||||
import { DataTableColumnHeader } from '@/components/data-table';
|
||||
import DataTableActions from './data-table-actions.vue';
|
||||
|
||||
export interface RoleListItem {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Link, router } from '@inertiajs/vue3';
|
||||
import { Pencil, Trash2 } from '@lucide/vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@ -18,23 +18,13 @@ const { can } = useCan();
|
||||
const deleteConfirmOpen = ref(false);
|
||||
const deleteProcessing = ref(false);
|
||||
|
||||
const isSystemRole = computed(() => {
|
||||
return ['developer', 'owner'].includes(props.role.name);
|
||||
});
|
||||
|
||||
function destroyRole() {
|
||||
if (isSystemRole.value) {
|
||||
toast.error('Role sistem tidak dapat dihapus.');
|
||||
return;
|
||||
}
|
||||
|
||||
deleteProcessing.value = true;
|
||||
|
||||
router.delete(`/admin/system/roles/${props.role.id}`, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
deleteConfirmOpen.value = false;
|
||||
toast.success('Role berhasil dihapus.');
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Gagal menghapus role.');
|
||||
@ -63,9 +53,7 @@ function destroyRole() {
|
||||
<Tooltip v-if="can('roles.delete')">
|
||||
<TooltipTrigger as-child>
|
||||
<span>
|
||||
<Button variant="ghost" size="icon"
|
||||
class="text-destructive hover:text-destructive size-8"
|
||||
:disabled="isSystemRole"
|
||||
<Button variant="ghost" size="icon" class="text-destructive hover:text-destructive size-8"
|
||||
@click="deleteConfirmOpen = true">
|
||||
<Trash2 class="size-4" />
|
||||
<span class="sr-only">Hapus</span>
|
||||
@ -73,7 +61,7 @@ function destroyRole() {
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{{ isSystemRole ? 'Role sistem tidak dapat dihapus' : 'Hapus' }}
|
||||
Hapus
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@ -1,11 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
import { ref } from 'vue';
|
||||
import HomepageSection from './form/HomepageSection.vue';
|
||||
import HrSection from './form/HrSection.vue';
|
||||
import MarketplaceSection from './form/MarketplaceSection.vue';
|
||||
import SocialMediaSection from './form/SocialMediaSection.vue';
|
||||
import SystemSection from './form/SystemSection.vue';
|
||||
import { SettingSection as SettingSectionConst } from '@/constants/setting-section';
|
||||
import SettingLayout from '@/layouts/SettingLayout.vue';
|
||||
import type {
|
||||
@ -16,6 +11,11 @@ import type {
|
||||
SocialMediaSettingsData,
|
||||
SystemSettingsData,
|
||||
} from '@/types/setting';
|
||||
import HomepageSection from './form/HomepageSection.vue';
|
||||
import HrSection from './form/HrSection.vue';
|
||||
import MarketplaceSection from './form/MarketplaceSection.vue';
|
||||
import SocialMediaSection from './form/SocialMediaSection.vue';
|
||||
import SystemSection from './form/SystemSection.vue';
|
||||
|
||||
defineProps<{
|
||||
system: SystemSettingsData;
|
||||
|
||||
@ -8,8 +8,8 @@ import MultipleImageUploadField from '@/components/form/image-upload-field/Multi
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { createMediaUploadState } from '@/types/media';
|
||||
import type { HomepageSettingsData } from '@/types/setting';
|
||||
import type { MediaUploadState } from '@/types/media';
|
||||
import type { HomepageSettingsData } from '@/types/setting';
|
||||
|
||||
const props = defineProps<{
|
||||
data: HomepageSettingsData;
|
||||
@ -46,7 +46,6 @@ function submit() {
|
||||
router.put('/admin/system/setting/homepage', formData, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
toast.success('Pengaturan homepage berhasil disimpan.');
|
||||
heroImage.value = null;
|
||||
aboutImage.value = null;
|
||||
},
|
||||
@ -64,44 +63,29 @@ function submit() {
|
||||
<form @submit.prevent="submit">
|
||||
<div class="grid gap-4">
|
||||
|
||||
<!-- Hero Image -->
|
||||
<Card>
|
||||
<CardContent>
|
||||
<ImageUploadField
|
||||
id="hero_image"
|
||||
label="Foto Hero"
|
||||
<ImageUploadField id="hero_image" label="Foto Hero"
|
||||
description="Foto utama yang ditampilkan di bagian hero homepage."
|
||||
:current-url="props.data.hero_image_url"
|
||||
v-model="heroImage"
|
||||
preview-class="aspect-[3/4] max-w-xs"
|
||||
/>
|
||||
:current-url="props.data.hero_image_url" v-model="heroImage"
|
||||
preview-class="aspect-[3/4] max-w-xs" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Gallery / Lookbook -->
|
||||
<Card>
|
||||
<CardContent>
|
||||
<MultipleImageUploadField
|
||||
id="gallery_images"
|
||||
label="Koleksi Lookbook"
|
||||
description="Foto-foto yang ditampilkan di galeri lookbook."
|
||||
:max-files="10"
|
||||
v-model="galleryState"
|
||||
/>
|
||||
<MultipleImageUploadField id="gallery_images" label="Koleksi Lookbook"
|
||||
description="Foto-foto yang ditampilkan di galeri lookbook." :max-files="10"
|
||||
v-model="galleryState" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- About Image -->
|
||||
<Card>
|
||||
<CardContent>
|
||||
<ImageUploadField
|
||||
id="about_image"
|
||||
label="Foto Tentang Kami"
|
||||
<ImageUploadField id="about_image" label="Foto Tentang Kami"
|
||||
description="Foto yang ditampilkan di bagian tentang kami."
|
||||
:current-url="props.data.about_image_url"
|
||||
v-model="aboutImage"
|
||||
preview-class="aspect-video max-w-md"
|
||||
/>
|
||||
:current-url="props.data.about_image_url" v-model="aboutImage"
|
||||
preview-class="aspect-video max-w-md" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@ -67,7 +67,8 @@ function submit() {
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="absent_penalty_amount">Denda Bolos</FieldLabel>
|
||||
<RupiahInput id="absent_penalty_amount" v-model="form.absent_penalty_amount" placeholder="0" />
|
||||
<RupiahInput id="absent_penalty_amount" v-model="form.absent_penalty_amount"
|
||||
placeholder="0" />
|
||||
<p class="text-xs text-muted-foreground mt-1">
|
||||
Potongan gaji otomatis jika pegawai tidak melakukan presensi masuk dan pulang (bolos).
|
||||
</p>
|
||||
|
||||
@ -56,10 +56,8 @@ function updateValue(value: string | number) {
|
||||
<FieldLabel :for="`${id}-scope`" class="text-xs text-muted-foreground">
|
||||
Dasar
|
||||
</FieldLabel>
|
||||
<Select
|
||||
:model-value="modelValue.scope"
|
||||
@update:model-value="update({ scope: $event as MarketplaceFeeRule['scope'] })"
|
||||
>
|
||||
<Select :model-value="modelValue.scope"
|
||||
@update:model-value="update({ scope: $event as MarketplaceFeeRule['scope'] })">
|
||||
<SelectTrigger :id="`${id}-scope`">
|
||||
<SelectValue placeholder="Pilih dasar" />
|
||||
</SelectTrigger>
|
||||
@ -78,10 +76,8 @@ function updateValue(value: string | number) {
|
||||
<FieldLabel :for="`${id}-value-type`" class="text-xs text-muted-foreground">
|
||||
Tipe
|
||||
</FieldLabel>
|
||||
<Select
|
||||
:model-value="modelValue.value_type"
|
||||
@update:model-value="update({ value_type: $event as MarketplaceFeeRule['value_type'] })"
|
||||
>
|
||||
<Select :model-value="modelValue.value_type"
|
||||
@update:model-value="update({ value_type: $event as MarketplaceFeeRule['value_type'] })">
|
||||
<SelectTrigger :id="`${id}-value-type`">
|
||||
<SelectValue placeholder="Pilih tipe" />
|
||||
</SelectTrigger>
|
||||
@ -100,23 +96,10 @@ function updateValue(value: string | number) {
|
||||
<FieldLabel :for="`${id}-value`" class="text-xs text-muted-foreground">
|
||||
Nilai
|
||||
</FieldLabel>
|
||||
<RupiahInput
|
||||
v-if="!isPercent"
|
||||
:id="`${id}-value`"
|
||||
:model-value="modelValue.value"
|
||||
placeholder="0"
|
||||
@update:model-value="updateValue"
|
||||
/>
|
||||
<Input
|
||||
v-else
|
||||
:id="`${id}-value`"
|
||||
:model-value="modelValue.value"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
max="100"
|
||||
@update:model-value="updateValue"
|
||||
/>
|
||||
<RupiahInput v-if="!isPercent" :id="`${id}-value`" :model-value="modelValue.value" placeholder="0"
|
||||
@update:model-value="updateValue" />
|
||||
<Input v-else :id="`${id}-value`" :model-value="modelValue.value" type="number" step="0.01" min="0"
|
||||
max="100" @update:model-value="updateValue" />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
|
||||
@ -3,7 +3,6 @@ import { useForm } from '@inertiajs/vue3';
|
||||
import { Save, ShoppingBag, Store } from '@lucide/vue';
|
||||
import { ref } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import MarketplaceFeeField from './MarketplaceFeeField.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
@ -20,6 +19,7 @@ import type {
|
||||
MarketplacePlatform,
|
||||
MarketplaceSettingsData,
|
||||
} from '@/types/setting';
|
||||
import MarketplaceFeeField from './MarketplaceFeeField.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
data: MarketplaceSettingsData;
|
||||
@ -93,47 +93,28 @@ function submit() {
|
||||
<CardContent>
|
||||
<FieldSet>
|
||||
<FieldGroup class="grid gap-4 sm:grid-cols-2">
|
||||
<MarketplaceFeeField
|
||||
id="tiktok_shop_platform_commission"
|
||||
v-model="form.tiktok_shop_platform_commission"
|
||||
label="Biaya Komisi Platform"
|
||||
:errors="feeErrors('tiktok_shop_platform_commission')"
|
||||
/>
|
||||
<MarketplaceFeeField id="tiktok_shop_platform_commission"
|
||||
v-model="form.tiktok_shop_platform_commission" label="Biaya Komisi Platform"
|
||||
:errors="feeErrors('tiktok_shop_platform_commission')" />
|
||||
|
||||
<MarketplaceFeeField
|
||||
id="tiktok_shop_logistics_service_fee"
|
||||
v-model="form.tiktok_shop_logistics_service_fee"
|
||||
label="Biaya Layanan Logistik"
|
||||
:errors="feeErrors('tiktok_shop_logistics_service_fee')"
|
||||
/>
|
||||
<MarketplaceFeeField id="tiktok_shop_logistics_service_fee"
|
||||
v-model="form.tiktok_shop_logistics_service_fee" label="Biaya Layanan Logistik"
|
||||
:errors="feeErrors('tiktok_shop_logistics_service_fee')" />
|
||||
|
||||
<MarketplaceFeeField
|
||||
id="tiktok_shop_dynamic_commission"
|
||||
v-model="form.tiktok_shop_dynamic_commission"
|
||||
label="Komisi Dinamis"
|
||||
:errors="feeErrors('tiktok_shop_dynamic_commission')"
|
||||
/>
|
||||
<MarketplaceFeeField id="tiktok_shop_dynamic_commission"
|
||||
v-model="form.tiktok_shop_dynamic_commission" label="Komisi Dinamis"
|
||||
:errors="feeErrors('tiktok_shop_dynamic_commission')" />
|
||||
|
||||
<MarketplaceFeeField
|
||||
id="tiktok_shop_order_processing_fee"
|
||||
v-model="form.tiktok_shop_order_processing_fee"
|
||||
label="Biaya Pemrosesan Pesanan"
|
||||
:errors="feeErrors('tiktok_shop_order_processing_fee')"
|
||||
/>
|
||||
<MarketplaceFeeField id="tiktok_shop_order_processing_fee"
|
||||
v-model="form.tiktok_shop_order_processing_fee" label="Biaya Pemrosesan Pesanan"
|
||||
:errors="feeErrors('tiktok_shop_order_processing_fee')" />
|
||||
|
||||
<MarketplaceFeeField
|
||||
id="tiktok_shop_affiliate"
|
||||
v-model="form.tiktok_shop_affiliate"
|
||||
label="Affiliate"
|
||||
:errors="feeErrors('tiktok_shop_affiliate')"
|
||||
/>
|
||||
<MarketplaceFeeField id="tiktok_shop_affiliate" v-model="form.tiktok_shop_affiliate"
|
||||
label="Affiliate" :errors="feeErrors('tiktok_shop_affiliate')" />
|
||||
|
||||
<MarketplaceFeeField
|
||||
id="tiktok_shop_pre_order_service_fee"
|
||||
v-model="form.tiktok_shop_pre_order_service_fee"
|
||||
label="Biaya Layanan Pre Order"
|
||||
:errors="feeErrors('tiktok_shop_pre_order_service_fee')"
|
||||
/>
|
||||
<MarketplaceFeeField id="tiktok_shop_pre_order_service_fee"
|
||||
v-model="form.tiktok_shop_pre_order_service_fee" label="Biaya Layanan Pre Order"
|
||||
:errors="feeErrors('tiktok_shop_pre_order_service_fee')" />
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</CardContent>
|
||||
@ -143,68 +124,34 @@ function submit() {
|
||||
<CardContent>
|
||||
<FieldSet>
|
||||
<FieldGroup class="grid gap-4 sm:grid-cols-2">
|
||||
<MarketplaceFeeField
|
||||
id="shopee_admin_fee"
|
||||
v-model="form.shopee_admin_fee"
|
||||
label="Biaya Administrasi"
|
||||
:errors="feeErrors('shopee_admin_fee')"
|
||||
/>
|
||||
<MarketplaceFeeField id="shopee_admin_fee" v-model="form.shopee_admin_fee"
|
||||
label="Biaya Administrasi" :errors="feeErrors('shopee_admin_fee')" />
|
||||
|
||||
<MarketplaceFeeField
|
||||
id="shopee_program_fee"
|
||||
v-model="form.shopee_program_fee"
|
||||
label="Biaya Program"
|
||||
:errors="feeErrors('shopee_program_fee')"
|
||||
/>
|
||||
<MarketplaceFeeField id="shopee_program_fee" v-model="form.shopee_program_fee"
|
||||
label="Biaya Program" :errors="feeErrors('shopee_program_fee')" />
|
||||
|
||||
<MarketplaceFeeField
|
||||
id="shopee_shipping_savings"
|
||||
v-model="form.shopee_shipping_savings"
|
||||
label="Hemat Biaya Kirim"
|
||||
:errors="feeErrors('shopee_shipping_savings')"
|
||||
/>
|
||||
<MarketplaceFeeField id="shopee_shipping_savings" v-model="form.shopee_shipping_savings"
|
||||
label="Hemat Biaya Kirim" :errors="feeErrors('shopee_shipping_savings')" />
|
||||
|
||||
<MarketplaceFeeField
|
||||
id="shopee_premium"
|
||||
v-model="form.shopee_premium"
|
||||
label="Premi"
|
||||
:errors="feeErrors('shopee_premium')"
|
||||
/>
|
||||
<MarketplaceFeeField id="shopee_premium" v-model="form.shopee_premium" label="Premi"
|
||||
:errors="feeErrors('shopee_premium')" />
|
||||
|
||||
<MarketplaceFeeField
|
||||
id="shopee_service_fee"
|
||||
v-model="form.shopee_service_fee"
|
||||
label="Biaya Layanan"
|
||||
:errors="feeErrors('shopee_service_fee')"
|
||||
/>
|
||||
<MarketplaceFeeField id="shopee_service_fee" v-model="form.shopee_service_fee"
|
||||
label="Biaya Layanan" :errors="feeErrors('shopee_service_fee')" />
|
||||
|
||||
<MarketplaceFeeField
|
||||
id="shopee_order_processing_fee"
|
||||
v-model="form.shopee_order_processing_fee"
|
||||
label="Biaya Proses Pesanan"
|
||||
:errors="feeErrors('shopee_order_processing_fee')"
|
||||
/>
|
||||
<MarketplaceFeeField id="shopee_order_processing_fee"
|
||||
v-model="form.shopee_order_processing_fee" label="Biaya Proses Pesanan"
|
||||
:errors="feeErrors('shopee_order_processing_fee')" />
|
||||
|
||||
<MarketplaceFeeField
|
||||
id="shopee_ams_commission_fee"
|
||||
v-model="form.shopee_ams_commission_fee"
|
||||
label="Biaya Komisi AMS"
|
||||
:errors="feeErrors('shopee_ams_commission_fee')"
|
||||
/>
|
||||
<MarketplaceFeeField id="shopee_ams_commission_fee"
|
||||
v-model="form.shopee_ams_commission_fee" label="Biaya Komisi AMS"
|
||||
:errors="feeErrors('shopee_ams_commission_fee')" />
|
||||
|
||||
<MarketplaceFeeField
|
||||
id="shopee_pre_order"
|
||||
v-model="form.shopee_pre_order"
|
||||
label="Pre Order"
|
||||
:errors="feeErrors('shopee_pre_order')"
|
||||
/>
|
||||
<MarketplaceFeeField id="shopee_pre_order" v-model="form.shopee_pre_order"
|
||||
label="Pre Order" :errors="feeErrors('shopee_pre_order')" />
|
||||
|
||||
<MarketplaceFeeField
|
||||
id="shopee_live_extra"
|
||||
v-model="form.shopee_live_extra"
|
||||
label="Live Extra"
|
||||
:errors="feeErrors('shopee_live_extra')"
|
||||
/>
|
||||
<MarketplaceFeeField id="shopee_live_extra" v-model="form.shopee_live_extra"
|
||||
label="Live Extra" :errors="feeErrors('shopee_live_extra')" />
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</CardContent>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { router, useForm } from '@inertiajs/vue3';
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { Save } from '@lucide/vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { ImageUploadField } from '@/components/form/image-upload-field';
|
||||
@ -56,9 +56,6 @@ function submit() {
|
||||
|
||||
form.transform(() => payload).post('/admin/system/setting/system', {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
router.reload({ only: ['name', 'address'] });
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Gagal menyimpan pengaturan sistem.');
|
||||
},
|
||||
@ -67,14 +64,6 @@ function submit() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="space-y-1">
|
||||
<h2 class="text-2xl font-bold tracking-tight">
|
||||
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div class="grid gap-6">
|
||||
<Card>
|
||||
|
||||
@ -328,18 +328,23 @@
|
||||
->middleware('permission:'.Permission::ROLES_VIEW->value)
|
||||
->group(function () {
|
||||
Route::get('/', [RoleController::class, 'index'])->name('index');
|
||||
|
||||
Route::get('create', [RoleController::class, 'create'])
|
||||
->middleware('permission:'.Permission::ROLES_CREATE->value)
|
||||
->name('create');
|
||||
|
||||
Route::post('/', [RoleController::class, 'store'])
|
||||
->middleware('permission:'.Permission::ROLES_CREATE->value)
|
||||
->name('store');
|
||||
|
||||
Route::get('{role}/edit', [RoleController::class, 'edit'])
|
||||
->middleware('permission:'.Permission::ROLES_UPDATE->value)
|
||||
->name('edit');
|
||||
|
||||
Route::put('{role}', [RoleController::class, 'update'])
|
||||
->middleware('permission:'.Permission::ROLES_UPDATE->value)
|
||||
->name('update');
|
||||
|
||||
Route::delete('{role}', [RoleController::class, 'destroy'])
|
||||
->middleware('permission:'.Permission::ROLES_DELETE->value)
|
||||
->name('destroy');
|
||||
@ -356,7 +361,7 @@
|
||||
->group(function () {
|
||||
Route::get('/', [SettingController::class, 'index'])->name('index');
|
||||
|
||||
Route::post('system', [SettingController::class, 'updateSystem'])
|
||||
Route::put('system', [SettingController::class, 'updateSystem'])
|
||||
->middleware('permission:'.Permission::SETTINGS_UPDATE->value)
|
||||
->name('system.update');
|
||||
|
||||
|
||||
243
tests/Feature/Admin/System/ActivityLogTest.php
Normal file
243
tests/Feature/Admin/System/ActivityLogTest.php
Normal file
@ -0,0 +1,243 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\Permission as PermissionEnum;
|
||||
use App\Models\Category;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
});
|
||||
|
||||
// ─── Helper ───────────────────────────────────────────────
|
||||
|
||||
function createActivityLogUserWithPermission(PermissionEnum ...$permissions): User
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$user->givePermissionTo(
|
||||
array_merge(
|
||||
[PermissionEnum::DASHBOARD_VIEW->value],
|
||||
array_map(fn (PermissionEnum $p) => $p->value, $permissions)
|
||||
)
|
||||
);
|
||||
|
||||
$user->forgetCachedPermissions();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
function createActivityLogs(User $causer, int $count = 3): void
|
||||
{
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$category = Category::factory()->create(['name' => "Kategori {$i}"]);
|
||||
|
||||
activity()
|
||||
->causedBy($causer)
|
||||
->performedOn($category)
|
||||
->event('created')
|
||||
->withProperties(['attributes' => ['name' => "Kategori {$i}"]])
|
||||
->log("Kategori {$i} ditambahkan");
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Index ────────────────────────────────────────────────
|
||||
|
||||
describe('Activity Log Index', function () {
|
||||
test('authenticated user with permission can view activity log index', function () {
|
||||
$user = createActivityLogUserWithPermission(PermissionEnum::ACTIVITY_LOGS_VIEW);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.system.activity_logs.index'))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('guest is redirected to login', function () {
|
||||
$this->get(route('admin.system.activity_logs.index'))
|
||||
->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('user without permission is forbidden', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.system.activity_logs.index'))
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('index displays activity logs', function () {
|
||||
$user = createActivityLogUserWithPermission(PermissionEnum::ACTIVITY_LOGS_VIEW);
|
||||
|
||||
createActivityLogs($user, 3);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.system.activity_logs.index'))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('index returns pagination data', function () {
|
||||
$user = createActivityLogUserWithPermission(PermissionEnum::ACTIVITY_LOGS_VIEW);
|
||||
|
||||
createActivityLogs($user, 15);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.system.activity_logs.index'))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('index can search activity logs by description', function () {
|
||||
$user = createActivityLogUserWithPermission(PermissionEnum::ACTIVITY_LOGS_VIEW);
|
||||
|
||||
activity()
|
||||
->causedBy($user)
|
||||
->event('created')
|
||||
->log('Produk baru ditambahkan');
|
||||
|
||||
activity()
|
||||
->causedBy($user)
|
||||
->event('updated')
|
||||
->log('Supplier diperbarui');
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.system.activity_logs.index', ['search' => 'Produk']))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('index can filter by event type', function () {
|
||||
$user = createActivityLogUserWithPermission(PermissionEnum::ACTIVITY_LOGS_VIEW);
|
||||
|
||||
activity()->causedBy($user)->event('created')->log('Test created');
|
||||
activity()->causedBy($user)->event('updated')->log('Test updated');
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.system.activity_logs.index', ['event' => 'created']))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('index can filter by subject type', function () {
|
||||
$user = createActivityLogUserWithPermission(PermissionEnum::ACTIVITY_LOGS_VIEW);
|
||||
|
||||
Category::factory()->create();
|
||||
$user->update(['email' => 'new@example.com']);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.system.activity_logs.index', ['subject_type' => Category::class]))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('index can sort by created_at', function () {
|
||||
$user = createActivityLogUserWithPermission(PermissionEnum::ACTIVITY_LOGS_VIEW);
|
||||
|
||||
createActivityLogs($user, 3);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.system.activity_logs.index', ['sort' => 'created_at', 'direction' => 'asc']))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('index can sort by event', function () {
|
||||
$user = createActivityLogUserWithPermission(PermissionEnum::ACTIVITY_LOGS_VIEW);
|
||||
|
||||
createActivityLogs($user, 3);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.system.activity_logs.index', ['sort' => 'event', 'direction' => 'desc']))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('index can sort by description', function () {
|
||||
$user = createActivityLogUserWithPermission(PermissionEnum::ACTIVITY_LOGS_VIEW);
|
||||
|
||||
createActivityLogs($user, 3);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.system.activity_logs.index', ['sort' => 'description', 'direction' => 'asc']))
|
||||
->assertOk();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Activity Log Creation via Model ──────────────────────
|
||||
|
||||
describe('Activity Log Creation', function () {
|
||||
test('activity log is created when category is created', function () {
|
||||
$user = createActivityLogUserWithPermission(PermissionEnum::CATEGORIES_VIEW, PermissionEnum::CATEGORIES_CREATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.master.categories.store'), [
|
||||
'name' => 'Test Kategori',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('activity_log', [
|
||||
'event' => 'created',
|
||||
'subject_type' => Category::class,
|
||||
]);
|
||||
});
|
||||
|
||||
test('activity log is created when category is updated', function () {
|
||||
$user = createActivityLogUserWithPermission(PermissionEnum::CATEGORIES_VIEW, PermissionEnum::CATEGORIES_UPDATE);
|
||||
|
||||
$category = Category::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.master.categories.update', $category), [
|
||||
'name' => 'Updated Name',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('activity_log', [
|
||||
'event' => 'updated',
|
||||
'subject_type' => Category::class,
|
||||
'subject_id' => $category->id,
|
||||
]);
|
||||
});
|
||||
|
||||
test('activity log is created when category is deleted', function () {
|
||||
$user = createActivityLogUserWithPermission(PermissionEnum::CATEGORIES_VIEW, PermissionEnum::CATEGORIES_DELETE);
|
||||
|
||||
$category = Category::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->delete(route('admin.master.categories.destroy', $category));
|
||||
|
||||
$this->assertDatabaseHas('activity_log', [
|
||||
'event' => 'deleted',
|
||||
'subject_type' => Category::class,
|
||||
'subject_id' => $category->id,
|
||||
]);
|
||||
});
|
||||
|
||||
test('activity log stores causer information', function () {
|
||||
$user = createActivityLogUserWithPermission(PermissionEnum::ACTIVITY_LOGS_VIEW);
|
||||
|
||||
activity()
|
||||
->causedBy($user)
|
||||
->event('created')
|
||||
->log('Test log entry');
|
||||
|
||||
$activity = Activity::latest()->first();
|
||||
|
||||
expect($activity->causer_type)->toBe(User::class);
|
||||
expect($activity->causer_id)->toBe($user->id);
|
||||
});
|
||||
|
||||
test('activity log stores properties correctly', function () {
|
||||
$user = createActivityLogUserWithPermission(PermissionEnum::ACTIVITY_LOGS_VIEW);
|
||||
|
||||
activity()
|
||||
->causedBy($user)
|
||||
->event('updated')
|
||||
->withProperties([
|
||||
'attributes' => ['name' => 'New Value'],
|
||||
'old' => ['name' => 'Old Value'],
|
||||
])
|
||||
->log('Test with properties');
|
||||
|
||||
$activity = Activity::latest()->first();
|
||||
|
||||
expect($activity->properties->get('attributes')['name'])->toBe('New Value');
|
||||
expect($activity->properties->get('old')['name'])->toBe('Old Value');
|
||||
});
|
||||
});
|
||||
418
tests/Feature/Admin/System/RoleTest.php
Normal file
418
tests/Feature/Admin/System/RoleTest.php
Normal file
@ -0,0 +1,418 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\Permission as PermissionEnum;
|
||||
use App\Enums\Role as RoleEnum;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
});
|
||||
|
||||
// ─── Helper ───────────────────────────────────────────────
|
||||
|
||||
function createRoleUserWithPermission(PermissionEnum ...$permissions): User
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$user->givePermissionTo(
|
||||
array_merge(
|
||||
[PermissionEnum::DASHBOARD_VIEW->value],
|
||||
array_map(fn (PermissionEnum $p) => $p->value, $permissions)
|
||||
)
|
||||
);
|
||||
|
||||
$user->forgetCachedPermissions();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
// ─── Index ────────────────────────────────────────────────
|
||||
|
||||
describe('Role Index', function () {
|
||||
test('authenticated user with permission can view role index', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.system.roles.index'))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('guest is redirected to login', function () {
|
||||
$this->get(route('admin.system.roles.index'))
|
||||
->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('user without permission is forbidden', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.system.roles.index'))
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('index displays all seeded roles', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.system.roles.index'))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('index can search roles by name', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.system.roles.index', ['search' => 'developer']))
|
||||
->assertOk();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Create ───────────────────────────────────────────────
|
||||
|
||||
describe('Role Create', function () {
|
||||
test('authenticated user with permission can view create form', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW, PermissionEnum::ROLES_CREATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.system.roles.create'))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('guest is redirected to login', function () {
|
||||
$this->get(route('admin.system.roles.create'))
|
||||
->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('user without create permission is forbidden', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.system.roles.create'))
|
||||
->assertForbidden();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Store ────────────────────────────────────────────────
|
||||
|
||||
describe('Role Store', function () {
|
||||
test('authenticated user with permission can create a role', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW, PermissionEnum::ROLES_CREATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.system.roles.store'), [
|
||||
'name' => 'Custom Role',
|
||||
'permissions' => [PermissionEnum::DASHBOARD_VIEW->value, PermissionEnum::CATEGORIES_VIEW->value],
|
||||
])
|
||||
->assertRedirect(route('admin.system.roles.index'));
|
||||
|
||||
$this->assertDatabaseHas('roles', [
|
||||
'name' => 'custom-role',
|
||||
]);
|
||||
|
||||
$role = Role::findByName('custom-role');
|
||||
expect($role->permissions->pluck('name')->toArray())
|
||||
->toContain(PermissionEnum::DASHBOARD_VIEW->value, PermissionEnum::CATEGORIES_VIEW->value);
|
||||
});
|
||||
|
||||
test('guest cannot create a role', function () {
|
||||
$this->post(route('admin.system.roles.store'), [
|
||||
'name' => 'New Role',
|
||||
])->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('user without create permission is forbidden', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.system.roles.store'), [
|
||||
'name' => 'New Role',
|
||||
])
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('name is required', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW, PermissionEnum::ROLES_CREATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.system.roles.store'), [
|
||||
'name' => '',
|
||||
])
|
||||
->assertSessionHasErrors('name');
|
||||
});
|
||||
|
||||
test('name must not exceed 50 characters', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW, PermissionEnum::ROLES_CREATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.system.roles.store'), [
|
||||
'name' => str_repeat('a', 51),
|
||||
])
|
||||
->assertSessionHasErrors('name');
|
||||
});
|
||||
|
||||
test('name must be unique', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW, PermissionEnum::ROLES_CREATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.system.roles.store'), [
|
||||
'name' => RoleEnum::DEVELOPER->value,
|
||||
])
|
||||
->assertSessionHasErrors('name');
|
||||
});
|
||||
|
||||
test('name is slugified before saving', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW, PermissionEnum::ROLES_CREATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.system.roles.store'), [
|
||||
'name' => 'My Custom Role',
|
||||
])
|
||||
->assertRedirect(route('admin.system.roles.index'));
|
||||
|
||||
$this->assertDatabaseHas('roles', [
|
||||
'name' => 'my-custom-role',
|
||||
]);
|
||||
});
|
||||
|
||||
test('permissions must exist in database', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW, PermissionEnum::ROLES_CREATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.system.roles.store'), [
|
||||
'name' => 'new-role',
|
||||
'permissions' => ['nonexistent.permission'],
|
||||
])
|
||||
->assertSessionHasErrors('permissions.0');
|
||||
});
|
||||
|
||||
test('role can be created without permissions', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW, PermissionEnum::ROLES_CREATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.system.roles.store'), [
|
||||
'name' => 'empty-role',
|
||||
'permissions' => [],
|
||||
])
|
||||
->assertRedirect(route('admin.system.roles.index'));
|
||||
|
||||
$this->assertDatabaseHas('roles', [
|
||||
'name' => 'empty-role',
|
||||
]);
|
||||
|
||||
$role = Role::findByName('empty-role');
|
||||
expect($role->permissions)->toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Edit ─────────────────────────────────────────────────
|
||||
|
||||
describe('Role Edit', function () {
|
||||
test('authenticated user with permission can view edit form', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW, PermissionEnum::ROLES_UPDATE);
|
||||
|
||||
$role = Role::findByName(RoleEnum::ADMIN_TOKO->value);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.system.roles.edit', $role))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('guest is redirected to login', function () {
|
||||
$role = Role::findByName(RoleEnum::ADMIN_TOKO->value);
|
||||
|
||||
$this->get(route('admin.system.roles.edit', $role))
|
||||
->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('user without update permission is forbidden', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW);
|
||||
|
||||
$role = Role::findByName(RoleEnum::ADMIN_TOKO->value);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.system.roles.edit', $role))
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('edit page shows role data with permissions', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW, PermissionEnum::ROLES_UPDATE);
|
||||
|
||||
$role = Role::findByName(RoleEnum::ADMIN_TOKO->value);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.system.roles.edit', $role))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('protected roles are flagged as protected', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW, PermissionEnum::ROLES_UPDATE);
|
||||
|
||||
$role = Role::findByName(RoleEnum::DEVELOPER->value);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.system.roles.edit', $role))
|
||||
->assertOk();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Update ───────────────────────────────────────────────
|
||||
|
||||
describe('Role Update', function () {
|
||||
test('authenticated user with permission can update a role', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW, PermissionEnum::ROLES_UPDATE);
|
||||
|
||||
$role = Role::create(['name' => 'updatable-role', 'guard_name' => 'web']);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.roles.update', $role), [
|
||||
'name' => 'updated-role-name',
|
||||
'permissions' => [PermissionEnum::DASHBOARD_VIEW->value],
|
||||
])
|
||||
->assertRedirect(route('admin.system.roles.index'));
|
||||
|
||||
$this->assertDatabaseHas('roles', [
|
||||
'id' => $role->id,
|
||||
'name' => 'updated-role-name',
|
||||
]);
|
||||
|
||||
$role->refresh();
|
||||
expect($role->permissions->pluck('name')->toArray())
|
||||
->toContain(PermissionEnum::DASHBOARD_VIEW->value);
|
||||
});
|
||||
|
||||
test('guest cannot update a role', function () {
|
||||
$role = Role::create(['name' => 'test-role', 'guard_name' => 'web']);
|
||||
|
||||
$this->put(route('admin.system.roles.update', $role), [
|
||||
'name' => 'new-name',
|
||||
])->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('user without update permission is forbidden', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW);
|
||||
|
||||
$role = Role::create(['name' => 'test-role', 'guard_name' => 'web']);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.roles.update', $role), [
|
||||
'name' => 'new-name',
|
||||
])
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('name is required on update', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW, PermissionEnum::ROLES_UPDATE);
|
||||
|
||||
$role = Role::create(['name' => 'test-role', 'guard_name' => 'web']);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.roles.update', $role), [
|
||||
'name' => '',
|
||||
])
|
||||
->assertSessionHasErrors('name');
|
||||
});
|
||||
|
||||
test('name must be unique on update', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW, PermissionEnum::ROLES_UPDATE);
|
||||
|
||||
$role = Role::create(['name' => 'test-role', 'guard_name' => 'web']);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.roles.update', $role), [
|
||||
'name' => RoleEnum::OWNER->value,
|
||||
])
|
||||
->assertSessionHasErrors('name');
|
||||
});
|
||||
|
||||
test('name can stay the same on update', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW, PermissionEnum::ROLES_UPDATE);
|
||||
|
||||
$role = Role::create(['name' => 'test-role', 'guard_name' => 'web']);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.roles.update', $role), [
|
||||
'name' => 'test-role',
|
||||
'permissions' => [],
|
||||
])
|
||||
->assertRedirect(route('admin.system.roles.index'));
|
||||
});
|
||||
|
||||
test('protected role name cannot be changed', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW, PermissionEnum::ROLES_UPDATE);
|
||||
|
||||
$role = Role::findByName(RoleEnum::DEVELOPER->value);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.roles.update', $role), [
|
||||
'name' => 'renamed-developer',
|
||||
'permissions' => $role->permissions->pluck('name')->toArray(),
|
||||
])
|
||||
->assertRedirect(route('admin.system.roles.index'));
|
||||
|
||||
$this->assertDatabaseHas('roles', [
|
||||
'id' => $role->id,
|
||||
'name' => RoleEnum::DEVELOPER->value,
|
||||
]);
|
||||
});
|
||||
|
||||
test('owner role name cannot be changed', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW, PermissionEnum::ROLES_UPDATE);
|
||||
|
||||
$role = Role::findByName(RoleEnum::OWNER->value);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.roles.update', $role), [
|
||||
'name' => 'renamed-owner',
|
||||
'permissions' => $role->permissions->pluck('name')->toArray(),
|
||||
])
|
||||
->assertRedirect(route('admin.system.roles.index'));
|
||||
|
||||
$this->assertDatabaseHas('roles', [
|
||||
'id' => $role->id,
|
||||
'name' => RoleEnum::OWNER->value,
|
||||
]);
|
||||
});
|
||||
|
||||
test('role permissions can be updated', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW, PermissionEnum::ROLES_UPDATE);
|
||||
|
||||
$role = Role::create(['name' => 'perm-test-role', 'guard_name' => 'web']);
|
||||
$role->givePermissionTo(PermissionEnum::DASHBOARD_VIEW->value);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.roles.update', $role), [
|
||||
'name' => 'perm-test-role',
|
||||
'permissions' => [PermissionEnum::CATEGORIES_VIEW->value, PermissionEnum::ORDERS_VIEW->value],
|
||||
])
|
||||
->assertRedirect(route('admin.system.roles.index'));
|
||||
|
||||
$role->refresh();
|
||||
$permissionNames = $role->permissions->pluck('name')->toArray();
|
||||
|
||||
expect($permissionNames)->toContain(PermissionEnum::CATEGORIES_VIEW->value);
|
||||
expect($permissionNames)->toContain(PermissionEnum::ORDERS_VIEW->value);
|
||||
expect($permissionNames)->not->toContain(PermissionEnum::DASHBOARD_VIEW->value);
|
||||
});
|
||||
|
||||
test('all permissions can be removed from role', function () {
|
||||
$user = createRoleUserWithPermission(PermissionEnum::ROLES_VIEW, PermissionEnum::ROLES_UPDATE);
|
||||
|
||||
$role = Role::create(['name' => 'clear-perm-role', 'guard_name' => 'web']);
|
||||
$role->givePermissionTo(PermissionEnum::DASHBOARD_VIEW->value);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.roles.update', $role), [
|
||||
'name' => 'clear-perm-role',
|
||||
'permissions' => [],
|
||||
])
|
||||
->assertRedirect(route('admin.system.roles.index'));
|
||||
|
||||
$role->refresh();
|
||||
expect($role->permissions)->toHaveCount(0);
|
||||
});
|
||||
});
|
||||
616
tests/Feature/Admin/System/SettingTest.php
Normal file
616
tests/Feature/Admin/System/SettingTest.php
Normal file
@ -0,0 +1,616 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\Permission as PermissionEnum;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
Storage::fake('public');
|
||||
});
|
||||
|
||||
// ─── Helper ───────────────────────────────────────────────
|
||||
|
||||
function createSettingUserWithPermission(PermissionEnum ...$permissions): User
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$user->givePermissionTo(
|
||||
array_merge(
|
||||
[PermissionEnum::DASHBOARD_VIEW->value],
|
||||
array_map(fn (PermissionEnum $p) => $p->value, $permissions)
|
||||
)
|
||||
);
|
||||
|
||||
$user->forgetCachedPermissions();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
function validSystemPayload(): array
|
||||
{
|
||||
return [
|
||||
'app_name' => 'DST Collection',
|
||||
'about_app' => 'Tentang aplikasi kami',
|
||||
'email' => 'info@dstcollection.com',
|
||||
'phone' => '081234567890',
|
||||
'address' => 'Jl. Merdeka No. 1',
|
||||
];
|
||||
}
|
||||
|
||||
function validMarketplaceFeeRule(): array
|
||||
{
|
||||
return [
|
||||
'scope' => 'product',
|
||||
'value_type' => 'percent',
|
||||
'value' => 5.0,
|
||||
];
|
||||
}
|
||||
|
||||
function allMarketplaceFeeFields(): array
|
||||
{
|
||||
$fields = [
|
||||
'tiktok_shop_platform_commission',
|
||||
'tiktok_shop_logistics_service_fee',
|
||||
'tiktok_shop_dynamic_commission',
|
||||
'tiktok_shop_order_processing_fee',
|
||||
'tiktok_shop_affiliate',
|
||||
'tiktok_shop_pre_order_service_fee',
|
||||
'shopee_admin_fee',
|
||||
'shopee_program_fee',
|
||||
'shopee_shipping_savings',
|
||||
'shopee_premium',
|
||||
'shopee_service_fee',
|
||||
'shopee_order_processing_fee',
|
||||
'shopee_ams_commission_fee',
|
||||
'shopee_pre_order',
|
||||
'shopee_live_extra',
|
||||
];
|
||||
|
||||
return collect($fields)->mapWithKeys(fn (string $key) => [$key => validMarketplaceFeeRule()])->all();
|
||||
}
|
||||
|
||||
// ─── Index ────────────────────────────────────────────────
|
||||
|
||||
describe('Setting Index', function () {
|
||||
test('authenticated user with permission can view settings page', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.system.settings.index'))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('guest is redirected to login', function () {
|
||||
$this->get(route('admin.system.settings.index'))
|
||||
->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('user without permission is forbidden', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.system.settings.index'))
|
||||
->assertForbidden();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Update System ────────────────────────────────────────
|
||||
|
||||
describe('Setting Update System', function () {
|
||||
test('authenticated user with permission can update system settings', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$payload = validSystemPayload();
|
||||
$payload['logo'] = UploadedFile::fake()->image('logo.jpg', 100, 100);
|
||||
$payload['favicon'] = UploadedFile::fake()->image('favicon.png', 32, 32);
|
||||
$payload['login_cover'] = UploadedFile::fake()->image('login-cover.jpg', 800, 600);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.system.update'), $payload)
|
||||
->assertRedirect(route('admin.system.settings.index'));
|
||||
|
||||
$this->assertDatabaseHas('settings', [
|
||||
'group' => 'system',
|
||||
'name' => 'app_name',
|
||||
]);
|
||||
});
|
||||
|
||||
test('guest cannot update system settings', function () {
|
||||
$this->put(route('admin.system.settings.system.update'), validSystemPayload())
|
||||
->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('user without update permission is forbidden', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.system.update'), validSystemPayload())
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('app_name is required', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$payload = validSystemPayload();
|
||||
$payload['app_name'] = '';
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.system.update'), $payload)
|
||||
->assertSessionHasErrors('app_name');
|
||||
});
|
||||
|
||||
test('app_name must not exceed 100 characters', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$payload = validSystemPayload();
|
||||
$payload['app_name'] = str_repeat('a', 101);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.system.update'), $payload)
|
||||
->assertSessionHasErrors('app_name');
|
||||
});
|
||||
|
||||
test('email must be valid', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$payload = validSystemPayload();
|
||||
$payload['email'] = 'not-an-email';
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.system.update'), $payload)
|
||||
->assertSessionHasErrors('email');
|
||||
});
|
||||
|
||||
test('email is required', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$payload = validSystemPayload();
|
||||
$payload['email'] = '';
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.system.update'), $payload)
|
||||
->assertSessionHasErrors('email');
|
||||
});
|
||||
|
||||
test('phone must be valid phone number', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$payload = validSystemPayload();
|
||||
$payload['phone'] = '123invalid';
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.system.update'), $payload)
|
||||
->assertSessionHasErrors('phone');
|
||||
});
|
||||
|
||||
test('phone is required', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$payload = validSystemPayload();
|
||||
$payload['phone'] = '';
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.system.update'), $payload)
|
||||
->assertSessionHasErrors('phone');
|
||||
});
|
||||
|
||||
test('about_app is required', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$payload = validSystemPayload();
|
||||
$payload['about_app'] = '';
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.system.update'), $payload)
|
||||
->assertSessionHasErrors('about_app');
|
||||
});
|
||||
|
||||
test('address is required', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$payload = validSystemPayload();
|
||||
$payload['address'] = '';
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.system.update'), $payload)
|
||||
->assertSessionHasErrors('address');
|
||||
});
|
||||
|
||||
test('logo must be an image when provided', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$payload = validSystemPayload();
|
||||
$payload['logo'] = UploadedFile::fake()->create('document.pdf', 100);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.system.update'), $payload)
|
||||
->assertSessionHasErrors('logo');
|
||||
});
|
||||
|
||||
test('favicon must be an image when provided', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$payload = validSystemPayload();
|
||||
$payload['favicon'] = UploadedFile::fake()->create('document.pdf', 100);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.system.update'), $payload)
|
||||
->assertSessionHasErrors('favicon');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Update Social Media ──────────────────────────────────
|
||||
|
||||
describe('Setting Update Social Media', function () {
|
||||
test('authenticated user can update social media settings', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.social_media.update'), [
|
||||
'instagram_url' => 'https://instagram.com/dstcollection',
|
||||
'facebook_url' => 'https://facebook.com/dstcollection',
|
||||
'tiktok_url' => 'https://tiktok.com/@dstcollection',
|
||||
])
|
||||
->assertRedirect(route('admin.system.settings.index'));
|
||||
|
||||
$this->assertDatabaseHas('settings', [
|
||||
'group' => 'social_media',
|
||||
'name' => 'instagram_url',
|
||||
]);
|
||||
});
|
||||
|
||||
test('guest cannot update social media settings', function () {
|
||||
$this->put(route('admin.system.settings.social_media.update'), [
|
||||
'instagram_url' => 'https://instagram.com/test',
|
||||
])->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('user without update permission is forbidden', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.social_media.update'), [
|
||||
'instagram_url' => 'https://instagram.com/test',
|
||||
])
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('social media urls can be null', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.social_media.update'), [
|
||||
'instagram_url' => null,
|
||||
'facebook_url' => null,
|
||||
'tiktok_url' => null,
|
||||
])
|
||||
->assertRedirect(route('admin.system.settings.index'));
|
||||
|
||||
$this->assertDatabaseHas('settings', [
|
||||
'group' => 'social_media',
|
||||
'name' => 'instagram_url',
|
||||
]);
|
||||
});
|
||||
|
||||
test('instagram_url must be a valid url', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.social_media.update'), [
|
||||
'instagram_url' => 'not-a-url',
|
||||
])
|
||||
->assertSessionHasErrors('instagram_url');
|
||||
});
|
||||
|
||||
test('facebook_url must be a valid url', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.social_media.update'), [
|
||||
'facebook_url' => 'not-a-url',
|
||||
])
|
||||
->assertSessionHasErrors('facebook_url');
|
||||
});
|
||||
|
||||
test('tiktok_url must be a valid url', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.social_media.update'), [
|
||||
'tiktok_url' => 'not-a-url',
|
||||
])
|
||||
->assertSessionHasErrors('tiktok_url');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Update Marketplace ───────────────────────────────────
|
||||
|
||||
describe('Setting Update Marketplace', function () {
|
||||
test('authenticated user can update marketplace settings', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$payload = allMarketplaceFeeFields();
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.marketplace.update'), $payload)
|
||||
->assertRedirect(route('admin.system.settings.index'));
|
||||
|
||||
$this->assertDatabaseHas('settings', [
|
||||
'group' => 'marketplace',
|
||||
'name' => 'tiktok_shop_platform_commission',
|
||||
]);
|
||||
});
|
||||
|
||||
test('guest cannot update marketplace settings', function () {
|
||||
$this->put(route('admin.system.settings.marketplace.update'), allMarketplaceFeeFields())
|
||||
->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('user without update permission is forbidden', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.marketplace.update'), allMarketplaceFeeFields())
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('marketplace fee scope must be valid enum', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$payload = allMarketplaceFeeFields();
|
||||
$payload['tiktok_shop_platform_commission']['scope'] = 'invalid';
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.marketplace.update'), $payload)
|
||||
->assertSessionHasErrors('tiktok_shop_platform_commission.scope');
|
||||
});
|
||||
|
||||
test('marketplace fee value_type must be valid enum', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$payload = allMarketplaceFeeFields();
|
||||
$payload['tiktok_shop_platform_commission']['value_type'] = 'invalid';
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.marketplace.update'), $payload)
|
||||
->assertSessionHasErrors('tiktok_shop_platform_commission.value_type');
|
||||
});
|
||||
|
||||
test('marketplace fee value must be numeric', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$payload = allMarketplaceFeeFields();
|
||||
$payload['tiktok_shop_platform_commission']['value'] = 'not-a-number';
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.marketplace.update'), $payload)
|
||||
->assertSessionHasErrors('tiktok_shop_platform_commission.value');
|
||||
});
|
||||
|
||||
test('marketplace fee percent value must not exceed 100', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$payload = allMarketplaceFeeFields();
|
||||
$payload['tiktok_shop_platform_commission']['value_type'] = 'percent';
|
||||
$payload['tiktok_shop_platform_commission']['value'] = 150.0;
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.marketplace.update'), $payload)
|
||||
->assertSessionHasErrors('tiktok_shop_platform_commission.value');
|
||||
});
|
||||
|
||||
test('marketplace flat fee must not have decimals', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$payload = allMarketplaceFeeFields();
|
||||
$payload['shopee_admin_fee']['value_type'] = 'flat';
|
||||
$payload['shopee_admin_fee']['value'] = 1500.5;
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.marketplace.update'), $payload)
|
||||
->assertSessionHasErrors('shopee_admin_fee.value');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Update HR ────────────────────────────────────────────
|
||||
|
||||
describe('Setting Update HR', function () {
|
||||
test('authenticated user can update HR settings', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.hr.update'), [
|
||||
'scheduled_check_in_time' => '09:00',
|
||||
'scheduled_check_out_time' => '18:00',
|
||||
'late_penalty_amount' => 50000,
|
||||
'absent_penalty_amount' => 100000,
|
||||
])
|
||||
->assertRedirect(route('admin.system.settings.index'));
|
||||
|
||||
$this->assertDatabaseHas('settings', [
|
||||
'group' => 'hr',
|
||||
'name' => 'scheduled_check_in_time',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('settings', [
|
||||
'group' => 'hr',
|
||||
'name' => 'late_penalty_amount',
|
||||
]);
|
||||
});
|
||||
|
||||
test('guest cannot update HR settings', function () {
|
||||
$this->put(route('admin.system.settings.hr.update'), [
|
||||
'scheduled_check_in_time' => '09:00',
|
||||
'scheduled_check_out_time' => '18:00',
|
||||
'late_penalty_amount' => 0,
|
||||
'absent_penalty_amount' => 0,
|
||||
])->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('user without update permission is forbidden', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.hr.update'), [
|
||||
'scheduled_check_in_time' => '09:00',
|
||||
'scheduled_check_out_time' => '18:00',
|
||||
'late_penalty_amount' => 0,
|
||||
'absent_penalty_amount' => 0,
|
||||
])
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('scheduled_check_in_time is required', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.hr.update'), [
|
||||
'scheduled_check_in_time' => '',
|
||||
'scheduled_check_out_time' => '18:00',
|
||||
'late_penalty_amount' => 0,
|
||||
'absent_penalty_amount' => 0,
|
||||
])
|
||||
->assertSessionHasErrors('scheduled_check_in_time');
|
||||
});
|
||||
|
||||
test('scheduled_check_in_time must match HH:MM format', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.hr.update'), [
|
||||
'scheduled_check_in_time' => '9:00',
|
||||
'scheduled_check_out_time' => '18:00',
|
||||
'late_penalty_amount' => 0,
|
||||
'absent_penalty_amount' => 0,
|
||||
])
|
||||
->assertSessionHasErrors('scheduled_check_in_time');
|
||||
});
|
||||
|
||||
test('scheduled_check_out_time is required', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.hr.update'), [
|
||||
'scheduled_check_in_time' => '09:00',
|
||||
'scheduled_check_out_time' => '',
|
||||
'late_penalty_amount' => 0,
|
||||
'absent_penalty_amount' => 0,
|
||||
])
|
||||
->assertSessionHasErrors('scheduled_check_out_time');
|
||||
});
|
||||
|
||||
test('late_penalty_amount must be integer', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.hr.update'), [
|
||||
'scheduled_check_in_time' => '09:00',
|
||||
'scheduled_check_out_time' => '18:00',
|
||||
'late_penalty_amount' => 'not-a-number',
|
||||
'absent_penalty_amount' => 0,
|
||||
])
|
||||
->assertSessionHasErrors('late_penalty_amount');
|
||||
});
|
||||
|
||||
test('late_penalty_amount must not be negative', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.hr.update'), [
|
||||
'scheduled_check_in_time' => '09:00',
|
||||
'scheduled_check_out_time' => '18:00',
|
||||
'late_penalty_amount' => -1000,
|
||||
'absent_penalty_amount' => 0,
|
||||
])
|
||||
->assertSessionHasErrors('late_penalty_amount');
|
||||
});
|
||||
|
||||
test('absent_penalty_amount must be integer', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.hr.update'), [
|
||||
'scheduled_check_in_time' => '09:00',
|
||||
'scheduled_check_out_time' => '18:00',
|
||||
'late_penalty_amount' => 0,
|
||||
'absent_penalty_amount' => 'not-a-number',
|
||||
])
|
||||
->assertSessionHasErrors('absent_penalty_amount');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Update Homepage ──────────────────────────────────────
|
||||
|
||||
describe('Setting Update Homepage', function () {
|
||||
test('authenticated user can update homepage settings', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.homepage.update'), [])
|
||||
->assertRedirect(route('admin.system.settings.index'));
|
||||
});
|
||||
|
||||
test('guest cannot update homepage settings', function () {
|
||||
$this->put(route('admin.system.settings.homepage.update'), [])
|
||||
->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('user without update permission is forbidden', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.homepage.update'), [])
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('hero_image must be an image when provided', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.homepage.update'), [
|
||||
'hero_image' => UploadedFile::fake()->create('document.pdf', 100),
|
||||
])
|
||||
->assertSessionHasErrors('hero_image');
|
||||
});
|
||||
|
||||
test('about_image must be an image when provided', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.homepage.update'), [
|
||||
'about_image' => UploadedFile::fake()->create('document.pdf', 100),
|
||||
])
|
||||
->assertSessionHasErrors('about_image');
|
||||
});
|
||||
|
||||
test('gallery_images must be an array of images', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.homepage.update'), [
|
||||
'gallery_images' => ['not-a-file'],
|
||||
])
|
||||
->assertSessionHasErrors('gallery_images.0');
|
||||
});
|
||||
|
||||
test('gallery_images must not exceed 10 items', function () {
|
||||
$user = createSettingUserWithPermission(PermissionEnum::SETTINGS_VIEW, PermissionEnum::SETTINGS_UPDATE);
|
||||
|
||||
$images = [];
|
||||
for ($i = 0; $i < 11; $i++) {
|
||||
$images[] = UploadedFile::fake()->image("photo-{$i}.jpg");
|
||||
}
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('admin.system.settings.homepage.update'), [
|
||||
'gallery_images' => $images,
|
||||
])
|
||||
->assertSessionHasErrors('gallery_images');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user