forked from admin/services_core
big update base struktur tahap 1
This commit is contained in:
@@ -1,4 +1,11 @@
|
||||
APP_NAME=Laravel
|
||||
MINIO_ACCESS_KEY_ID=N7uKWNiKTmuMK8T3uxL8
|
||||
MINIO_SECRET_ACCESS_KEY=pANDWUYWAGJymyuRrWyx7WVduYrOyOtjaLcl9Xi6
|
||||
MINIO_DEFAULT_REGION=us-east-1
|
||||
MINIO_BUCKET=file
|
||||
MINIO_ENDPOINT=https://storage.manjapro.net
|
||||
MINIO_USE_PATH_STYLE_ENDPOINT=true
|
||||
MINIO_CA_BUNDLE="C:/Program Files/Git/usr/ssl/certs/ca-bundle.crt"
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:nAnc26mWBQ848KLUo97mNg1/iXHA09c9oSrOLW9CWQ0=
|
||||
APP_DEBUG=true
|
||||
|
||||
@@ -62,4 +62,12 @@ AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
MINIO_ACCESS_KEY_ID=
|
||||
MINIO_SECRET_ACCESS_KEY=
|
||||
MINIO_DEFAULT_REGION=us-east-1
|
||||
MINIO_BUCKET=
|
||||
MINIO_ENDPOINT=
|
||||
MINIO_USE_PATH_STYLE_ENDPOINT=true
|
||||
MINIO_CA_BUNDLE=
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# TODO - Menu Group Available Menus & Restriction
|
||||
|
||||
- [x] Add service method to fetch allowed menu IDs for authenticated user
|
||||
- [x] Add service method to provide available menu list for a target menu group based on authenticated user access
|
||||
- [x] Restrict `syncMenus` so assigned menu IDs must be subset of authenticated user allowed menus (except master user)
|
||||
- [x] Add controller endpoint for `GET /menu-groups/{menu_group}/available-menus`
|
||||
- [x] Register route for `available-menus` endpoint in `routes/api.php`
|
||||
- [ ] Run API testing (critical/thorough as requested)
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum UserAccessLevel: string
|
||||
{
|
||||
case CUSTOMER = 'customer';
|
||||
case STAFF = 'staff';
|
||||
case TENANT_OWNER = 'tenant_owner';
|
||||
case MASTER_ADMIN = 'master_admin';
|
||||
|
||||
public function rank(): int
|
||||
{
|
||||
return match ($this) {
|
||||
self::CUSTOMER => 1,
|
||||
self::STAFF => 2,
|
||||
self::TENANT_OWNER => 3,
|
||||
self::MASTER_ADMIN => 4,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,9 @@ class AuditLogController extends ApiController
|
||||
$request->validated()
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
AuditLogResource::collection($data),
|
||||
return $this->successPaginated(
|
||||
$data,
|
||||
AuditLogResource::class,
|
||||
'Data audit log berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,29 +2,89 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\Auth\RegisterRequest;
|
||||
use App\Http\Requests\Auth\ResendVerificationCodeRequest;
|
||||
use App\Http\Requests\Auth\VerifyRegistrationRequest;
|
||||
use App\Models\User;
|
||||
use App\Services\Auth\AuthService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected AuthService $authService
|
||||
) {}
|
||||
|
||||
public function register(RegisterRequest $request)
|
||||
{
|
||||
$result = $this->authService->register(
|
||||
$request->validated()
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Register berhasil, silakan verifikasi kode.',
|
||||
'data' => [
|
||||
'user' => $result['user'],
|
||||
'verification' => $result['verification'],
|
||||
],
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function verifyRegistration(
|
||||
VerifyRegistrationRequest $request
|
||||
) {
|
||||
$user = $this->authService->verifyRegistration(
|
||||
$request->validated()
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Verifikasi berhasil, user sudah aktif.',
|
||||
'data' => $user,
|
||||
]);
|
||||
}
|
||||
|
||||
public function resendVerificationCode(
|
||||
ResendVerificationCodeRequest $request
|
||||
) {
|
||||
$verification = $this->authService->resendVerificationCode(
|
||||
$request->validated()
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Kode verifikasi baru berhasil dibuat.',
|
||||
'data' => [
|
||||
'verification' => $verification,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function login(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'email' => 'required|email',
|
||||
'password' => 'required'
|
||||
'login' => 'required|string',
|
||||
'password' => 'required|string',
|
||||
]);
|
||||
|
||||
$user = User::where('email', $request->email)->first();
|
||||
$user = User::where('email', $request->login)
|
||||
->orWhere('username', $request->login)
|
||||
->first();
|
||||
|
||||
if (!$user || !Hash::check($request->password, $user->password)) {
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Email atau password salah'
|
||||
'message' => 'Username/email atau password salah.'
|
||||
], 401);
|
||||
}
|
||||
|
||||
if ($user->status !== 'active') {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Akun belum aktif. Silakan verifikasi terlebih dahulu.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
$token = $user->createToken('services-core')->plainTextToken;
|
||||
@@ -32,7 +92,41 @@ class AuthController extends Controller
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'token' => $token,
|
||||
'user' => $user
|
||||
'user' => $user,
|
||||
]);
|
||||
}
|
||||
|
||||
// public function login(Request $request)
|
||||
// {
|
||||
// $request->validate([
|
||||
// 'email' => 'required|email',
|
||||
// 'password' => 'required'
|
||||
// ]);
|
||||
|
||||
// $user = User::where('email', $request->email)->first();
|
||||
|
||||
// if (!$user || !Hash::check($request->password, $user->password)) {
|
||||
|
||||
// return response()->json([
|
||||
// 'success' => false,
|
||||
// 'message' => 'Email atau password salah'
|
||||
// ], 401);
|
||||
|
||||
// }
|
||||
|
||||
// if ($user->status !== 'active') {
|
||||
// return response()->json([
|
||||
// 'success' => false,
|
||||
// 'message' => 'Akun belum aktif. Silakan verifikasi terlebih dahulu.'
|
||||
// ], 403);
|
||||
// }
|
||||
|
||||
// $token = $user->createToken('services-core')->plainTextToken;
|
||||
|
||||
// return response()->json([
|
||||
// 'success' => true,
|
||||
// 'token' => $token,
|
||||
// 'user' => $user
|
||||
// ]);
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\File;
|
||||
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Http\Requests\File\IndexStoredFileRequest;
|
||||
use App\Http\Requests\File\StoreFileRequest;
|
||||
use App\Http\Resources\File\StoredFileResource;
|
||||
use App\Models\StoredFile;
|
||||
use App\Services\File\StoredFileService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class StoredFileController extends ApiController
|
||||
{
|
||||
public function __construct(protected StoredFileService $service) {}
|
||||
|
||||
public function index(IndexStoredFileRequest $request)
|
||||
{
|
||||
return $this->successPaginated(
|
||||
$this->service->list(
|
||||
$request->validated(),
|
||||
$request->user(),
|
||||
$request->attributes->get('tenant_id')
|
||||
),
|
||||
StoredFileResource::class,
|
||||
'Daftar file berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function store(StoreFileRequest $request)
|
||||
{
|
||||
$file = $this->service->store(
|
||||
$request->file('file'),
|
||||
$request->user(),
|
||||
$request->attributes->get('tenant_id'),
|
||||
$request->validated('category', 'general')
|
||||
);
|
||||
|
||||
return $this->created(new StoredFileResource($file), 'File berhasil diunggah');
|
||||
}
|
||||
|
||||
public function show(Request $request, StoredFile $storedFile)
|
||||
{
|
||||
$this->service->authorizeAccess($storedFile, $request->user());
|
||||
|
||||
return $this->success(new StoredFileResource($storedFile), 'Detail file berhasil diambil');
|
||||
}
|
||||
|
||||
public function temporaryUrl(Request $request, StoredFile $storedFile)
|
||||
{
|
||||
return $this->success(
|
||||
$this->service->temporaryUrl($storedFile, $request->user()),
|
||||
'URL sementara berhasil dibuat'
|
||||
);
|
||||
}
|
||||
|
||||
public function destroy(Request $request, StoredFile $storedFile)
|
||||
{
|
||||
$this->service->delete($storedFile, $request->user());
|
||||
|
||||
return $this->deleted('File berhasil dihapus');
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,11 @@ namespace App\Http\Controllers\Material;
|
||||
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Http\Requests\Material\AssignMaterialRequest;
|
||||
use App\Http\Requests\Material\TransferMaterialRequest;
|
||||
use App\Http\Requests\Material\IndexMaterialRequest;
|
||||
use App\Http\Requests\Material\ReturnMaterialRequest;
|
||||
use App\Http\Requests\Material\TransferMaterialRequest;
|
||||
use App\Http\Resources\Material\MaterialResource;
|
||||
use App\Services\Material\MaterialService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class MaterialController extends ApiController
|
||||
{
|
||||
@@ -19,20 +19,9 @@ class MaterialController extends ApiController
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
public function index(IndexMaterialRequest $request)
|
||||
{
|
||||
$filters = $request->only([
|
||||
'search',
|
||||
'technician_id',
|
||||
'warehouse_id',
|
||||
'type',
|
||||
'page',
|
||||
'per_page',
|
||||
'sort_by',
|
||||
'sort_order',
|
||||
]);
|
||||
|
||||
$data = $this->service->list($filters);
|
||||
$data = $this->service->list($request->validated());
|
||||
|
||||
return $this->successPaginated(
|
||||
$data,
|
||||
@@ -76,7 +65,7 @@ class MaterialController extends ApiController
|
||||
$data = $this->service->listByUser($user_id);
|
||||
|
||||
return $this->success(
|
||||
\App\Http\Resources\Material\MaterialResource::collection($data),
|
||||
MaterialResource::collection($data),
|
||||
'List material user berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Menu;
|
||||
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Models\MenuList;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class MenuController extends ApiController
|
||||
{
|
||||
public function myMenus()
|
||||
{
|
||||
$user = auth()->user();
|
||||
$tenantId = request()->attributes->get('tenant_id');
|
||||
|
||||
if (! $user) {
|
||||
return $this->error('Unauthenticated', 401);
|
||||
}
|
||||
|
||||
if ($user->isMasterAdmin()) {
|
||||
$menus = MenuList::query()
|
||||
->where('is_active', true)
|
||||
->orderBy('sort_order')
|
||||
->orderBy('id')
|
||||
->get()
|
||||
->map(function (MenuList $menu) {
|
||||
return [
|
||||
'id' => $menu->id,
|
||||
'parent_id' => $menu->parent_id,
|
||||
'name' => $menu->name,
|
||||
'slug' => $menu->slug,
|
||||
'url' => $menu->url,
|
||||
'icon' => $menu->icon,
|
||||
'component' => $menu->component,
|
||||
'sort_order' => $menu->sort_order,
|
||||
'permissions' => [
|
||||
'can_view' => true,
|
||||
'can_create' => true,
|
||||
'can_update' => true,
|
||||
'can_delete' => true,
|
||||
'can_approve' => true,
|
||||
'can_export' => true,
|
||||
],
|
||||
];
|
||||
});
|
||||
} else {
|
||||
$menuRows = DB::table('menu_lists as m')
|
||||
->join('menu_group_menus as mgm', 'mgm.menu_id', '=', 'm.id')
|
||||
->join('user_menu_groups as umg', 'umg.menu_group_id', '=', 'mgm.menu_group_id')
|
||||
->join('menu_groups as mg', 'mg.id', '=', 'umg.menu_group_id')
|
||||
->where('umg.user_id', $user->id)
|
||||
->where('umg.tenant_id', $tenantId)
|
||||
->where('mg.is_active', true)
|
||||
->where('m.is_active', true)
|
||||
->where('mgm.can_view', true)
|
||||
->groupBy(
|
||||
'm.id',
|
||||
'm.parent_id',
|
||||
'm.name',
|
||||
'm.slug',
|
||||
'm.url',
|
||||
'm.icon',
|
||||
'm.component',
|
||||
'm.sort_order'
|
||||
)
|
||||
->select(
|
||||
'm.id',
|
||||
'm.parent_id',
|
||||
'm.name',
|
||||
'm.slug',
|
||||
'm.url',
|
||||
'm.icon',
|
||||
'm.component',
|
||||
'm.sort_order',
|
||||
DB::raw('MAX(CASE WHEN mgm.can_view THEN 1 ELSE 0 END) as can_view'),
|
||||
DB::raw('MAX(CASE WHEN mgm.can_create THEN 1 ELSE 0 END) as can_create'),
|
||||
DB::raw('MAX(CASE WHEN mgm.can_update THEN 1 ELSE 0 END) as can_update'),
|
||||
DB::raw('MAX(CASE WHEN mgm.can_delete THEN 1 ELSE 0 END) as can_delete'),
|
||||
DB::raw('MAX(CASE WHEN mgm.can_approve THEN 1 ELSE 0 END) as can_approve'),
|
||||
DB::raw('MAX(CASE WHEN mgm.can_export THEN 1 ELSE 0 END) as can_export')
|
||||
)
|
||||
->orderBy('m.sort_order')
|
||||
->orderBy('m.id')
|
||||
->get();
|
||||
|
||||
$menus = $menuRows->map(function ($row) {
|
||||
return [
|
||||
'id' => (int) $row->id,
|
||||
'parent_id' => $row->parent_id ? (int) $row->parent_id : null,
|
||||
'name' => $row->name,
|
||||
'slug' => $row->slug,
|
||||
'url' => $row->url,
|
||||
'icon' => $row->icon,
|
||||
'component' => $row->component,
|
||||
'sort_order' => (int) $row->sort_order,
|
||||
'permissions' => [
|
||||
'can_view' => (bool) $row->can_view,
|
||||
'can_create' => (bool) $row->can_create,
|
||||
'can_update' => (bool) $row->can_update,
|
||||
'can_delete' => (bool) $row->can_delete,
|
||||
'can_approve' => (bool) $row->can_approve,
|
||||
'can_export' => (bool) $row->can_export,
|
||||
],
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
$tree = $this->buildMenuTree($menus);
|
||||
|
||||
return $this->success(
|
||||
[
|
||||
'is_master' => (bool) $user->is_master,
|
||||
'access_level' => $user->access_level->value,
|
||||
'tenant_id' => $tenantId,
|
||||
'menus' => $tree,
|
||||
],
|
||||
'Menu berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
private function buildMenuTree(Collection $menus, ?int $parentId = null): array
|
||||
{
|
||||
return $menus
|
||||
->filter(fn ($menu) => $menu['parent_id'] === $parentId)
|
||||
->sortBy('sort_order')
|
||||
->values()
|
||||
->map(function ($menu) use ($menus) {
|
||||
$children = $this->buildMenuTree($menus, $menu['id']);
|
||||
|
||||
return [
|
||||
'id' => $menu['id'],
|
||||
'parent_id' => $menu['parent_id'],
|
||||
'name' => $menu['name'],
|
||||
'slug' => $menu['slug'],
|
||||
'url' => $menu['url'],
|
||||
'icon' => $menu['icon'],
|
||||
'component' => $menu['component'],
|
||||
'sort_order' => $menu['sort_order'],
|
||||
'permissions' => $menu['permissions'],
|
||||
'children' => $children,
|
||||
];
|
||||
})
|
||||
->all();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Menu;
|
||||
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Http\Requests\MenuGroup\IndexMenuGroupRequest;
|
||||
use App\Http\Requests\MenuGroup\StoreMenuGroupRequest;
|
||||
use App\Http\Requests\MenuGroup\SyncMenuGroupMenusRequest;
|
||||
use App\Http\Requests\MenuGroup\SyncUserMenuGroupsRequest;
|
||||
use App\Http\Requests\MenuGroup\UpdateMenuGroupRequest;
|
||||
use App\Http\Resources\MenuGroup\MenuGroupResource;
|
||||
use App\Models\MenuGroup;
|
||||
use App\Models\User;
|
||||
use App\Services\Access\AccessService;
|
||||
use App\Services\Menu\MenuGroupService;
|
||||
|
||||
class MenuGroupController extends ApiController
|
||||
{
|
||||
public function __construct(
|
||||
protected MenuGroupService $service,
|
||||
protected AccessService $access
|
||||
) {}
|
||||
|
||||
public function index(IndexMenuGroupRequest $request)
|
||||
{
|
||||
$data = $this->service->list(
|
||||
$request->validated(),
|
||||
$request->user(),
|
||||
$request->attributes->get('tenant_id')
|
||||
);
|
||||
|
||||
return $this->successPaginated(
|
||||
$data,
|
||||
MenuGroupResource::class,
|
||||
'Data Menu Group berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function store(StoreMenuGroupRequest $request)
|
||||
{
|
||||
$data = $this->service->create(
|
||||
$request->validated(),
|
||||
$request->user(),
|
||||
$request->attributes->get('tenant_id')
|
||||
);
|
||||
|
||||
return $this->created(
|
||||
new MenuGroupResource($data),
|
||||
'Menu Group berhasil dibuat'
|
||||
);
|
||||
}
|
||||
|
||||
public function show(MenuGroup $menu_group)
|
||||
{
|
||||
$this->authorizeGroup($menu_group, false);
|
||||
|
||||
return $this->success(
|
||||
new MenuGroupResource($menu_group->load('tenant')),
|
||||
'Detail Menu Group berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function update(
|
||||
UpdateMenuGroupRequest $request,
|
||||
MenuGroup $menu_group
|
||||
) {
|
||||
$this->authorizeGroup($menu_group, true);
|
||||
$data = $this->service->update(
|
||||
$menu_group,
|
||||
$request->validated(),
|
||||
$request->user(),
|
||||
$request->attributes->get('tenant_id')
|
||||
);
|
||||
|
||||
return $this->updated(
|
||||
new MenuGroupResource($data),
|
||||
'Menu Group berhasil diupdate'
|
||||
);
|
||||
}
|
||||
|
||||
public function destroy(MenuGroup $menu_group)
|
||||
{
|
||||
$this->authorizeGroup($menu_group, true);
|
||||
$this->service->delete($menu_group);
|
||||
|
||||
return $this->deleted(
|
||||
'Menu Group berhasil dihapus'
|
||||
);
|
||||
}
|
||||
|
||||
public function menus(MenuGroup $menu_group)
|
||||
{
|
||||
$this->authorizeGroup($menu_group, false);
|
||||
$data = $this->service->listMenus($menu_group);
|
||||
|
||||
return $this->success(
|
||||
[
|
||||
'menu_group_id' => $menu_group->id,
|
||||
'menu_group' => new MenuGroupResource($menu_group),
|
||||
'menus' => $data,
|
||||
],
|
||||
'Mapping menu group berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function availableMenus(MenuGroup $menu_group)
|
||||
{
|
||||
$this->authorizeGroup($menu_group, false);
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $user) {
|
||||
return $this->error('Unauthenticated', 401);
|
||||
}
|
||||
|
||||
$data = $this->service->listAvailableMenusForGroup(
|
||||
$menu_group,
|
||||
$user
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
[
|
||||
'menu_group_id' => $menu_group->id,
|
||||
'menu_group' => new MenuGroupResource($menu_group),
|
||||
'menus' => $data,
|
||||
],
|
||||
'Daftar menu tersedia berdasarkan akses user berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function syncMenus(
|
||||
SyncMenuGroupMenusRequest $request,
|
||||
MenuGroup $menu_group
|
||||
) {
|
||||
$this->authorizeGroup($menu_group, true);
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $user) {
|
||||
return $this->error('Unauthenticated', 401);
|
||||
}
|
||||
|
||||
$this->service->syncMenus(
|
||||
$menu_group,
|
||||
$request->validated('menus'),
|
||||
$user
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
[
|
||||
'menu_group_id' => $menu_group->id,
|
||||
'menu_group' => new MenuGroupResource($menu_group->fresh('tenant')),
|
||||
'menus' => $this->service->listMenus($menu_group),
|
||||
],
|
||||
'Mapping menu group berhasil disimpan'
|
||||
);
|
||||
}
|
||||
|
||||
public function userAssignments(User $user)
|
||||
{
|
||||
$tenantId = request()->attributes->get('tenant_id');
|
||||
abort_unless($this->access->canManageUser(auth()->user(), $user, $tenantId), 403);
|
||||
|
||||
return $this->success(
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'assignments' => $this->service->listUserAssignments($user, $tenantId),
|
||||
],
|
||||
'Assignment user menu group berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function syncUserAssignments(
|
||||
SyncUserMenuGroupsRequest $request,
|
||||
User $user
|
||||
) {
|
||||
$tenantId = $request->attributes->get('tenant_id');
|
||||
abort_unless($this->access->canManageUser($request->user(), $user, $tenantId), 403);
|
||||
|
||||
$this->service->syncUserAssignments(
|
||||
$user,
|
||||
$request->validated('assignments'),
|
||||
$tenantId
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'assignments' => $this->service->listUserAssignments($user, $tenantId),
|
||||
],
|
||||
'Assignment user menu group berhasil disimpan'
|
||||
);
|
||||
}
|
||||
|
||||
private function authorizeGroup(MenuGroup $menuGroup, bool $write): void
|
||||
{
|
||||
$user = auth()->user();
|
||||
$tenantId = request()->attributes->get('tenant_id');
|
||||
|
||||
if ($user->isMasterAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$allowed = (int) $menuGroup->tenant_id === (int) $tenantId;
|
||||
|
||||
abort_unless($allowed, 403);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Menu;
|
||||
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Http\Requests\MenuList\IndexMenuListRequest;
|
||||
use App\Http\Requests\MenuList\StoreMenuListRequest;
|
||||
use App\Http\Requests\MenuList\UpdateMenuListRequest;
|
||||
use App\Http\Resources\MenuList\MenuListResource;
|
||||
use App\Models\MenuList;
|
||||
use App\Services\Menu\MenuListService;
|
||||
|
||||
class MenuListController extends ApiController
|
||||
{
|
||||
public function __construct(
|
||||
protected MenuListService $service
|
||||
) {}
|
||||
|
||||
public function index(IndexMenuListRequest $request)
|
||||
{
|
||||
$data = $this->service->list(
|
||||
$request->validated()
|
||||
);
|
||||
|
||||
return $this->successPaginated(
|
||||
$data,
|
||||
MenuListResource::class,
|
||||
'Data Menu List berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function store(StoreMenuListRequest $request)
|
||||
{
|
||||
$data = $this->service->create(
|
||||
$request->validated()
|
||||
);
|
||||
|
||||
return $this->created(
|
||||
new MenuListResource($data),
|
||||
'Menu List berhasil dibuat'
|
||||
);
|
||||
}
|
||||
|
||||
public function show(MenuList $menu_list)
|
||||
{
|
||||
return $this->success(
|
||||
new MenuListResource($menu_list),
|
||||
'Detail Menu List berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function update(
|
||||
UpdateMenuListRequest $request,
|
||||
MenuList $menu_list
|
||||
) {
|
||||
$data = $this->service->update(
|
||||
$menu_list,
|
||||
$request->validated()
|
||||
);
|
||||
|
||||
return $this->updated(
|
||||
new MenuListResource($data),
|
||||
'Menu List berhasil diupdate'
|
||||
);
|
||||
}
|
||||
|
||||
public function destroy(MenuList $menu_list)
|
||||
{
|
||||
$this->service->delete($menu_list);
|
||||
|
||||
return $this->deleted(
|
||||
'Menu List berhasil dihapus'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Nas;
|
||||
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Http\Requests\Nas\IndexNasResourceRequest;
|
||||
use App\Http\Requests\Nas\StoreNasResourceRequest;
|
||||
use App\Http\Requests\Nas\UpdateNasResourceRequest;
|
||||
use App\Http\Resources\Nas\NasResource;
|
||||
use App\Services\Nas\NasResourceService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class NasResourceController extends ApiController
|
||||
{
|
||||
public function __construct(protected NasResourceService $service) {}
|
||||
|
||||
public function index(IndexNasResourceRequest $request)
|
||||
{
|
||||
$data = $this->service->list($this->resource($request), $request->validated(), $request->user(), $this->tenantId($request));
|
||||
|
||||
return $this->successPaginated($data, NasResource::class, 'Data NAS berhasil diambil');
|
||||
}
|
||||
|
||||
public function store(StoreNasResourceRequest $request)
|
||||
{
|
||||
$data = $this->service->create($this->resource($request), $request->validated(), $request->user(), $this->tenantId($request));
|
||||
|
||||
return $this->created(new NasResource($data), 'Data NAS berhasil dibuat');
|
||||
}
|
||||
|
||||
public function show(Request $request, int $id)
|
||||
{
|
||||
$data = $this->service->find($this->resource($request), $id);
|
||||
$this->authorizeTenant($request, $data->tenant_id);
|
||||
|
||||
return $this->success(new NasResource($data), 'Detail NAS berhasil diambil');
|
||||
}
|
||||
|
||||
public function update(UpdateNasResourceRequest $request, int $id)
|
||||
{
|
||||
$resource = $this->resource($request);
|
||||
$model = $this->service->find($resource, $id);
|
||||
$this->authorizeTenant($request, $model->tenant_id);
|
||||
$data = $this->service->update($resource, $model, $request->validated(), $request->user());
|
||||
|
||||
return $this->updated(new NasResource($data), 'Data NAS berhasil diupdate');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, int $id)
|
||||
{
|
||||
$model = $this->service->find($this->resource($request), $id);
|
||||
$this->authorizeTenant($request, $model->tenant_id);
|
||||
$this->service->delete($model);
|
||||
|
||||
return $this->deleted('Data NAS berhasil dihapus');
|
||||
}
|
||||
|
||||
private function resource(Request $request): string
|
||||
{
|
||||
return $request->route()->defaults['nas_resource'];
|
||||
}
|
||||
|
||||
private function tenantId(Request $request): ?int
|
||||
{
|
||||
return $request->attributes->get('tenant_id');
|
||||
}
|
||||
|
||||
private function authorizeTenant(Request $request, int $tenantId): void
|
||||
{
|
||||
abort_unless($request->user()->isMasterAdmin() || $tenantId === $this->tenantId($request), 403);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Profile;
|
||||
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Http\Requests\Profile\UpdateProfileRequest;
|
||||
use App\Http\Resources\User\UserResource;
|
||||
use App\Services\User\UserService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ProfileController extends ApiController
|
||||
{
|
||||
public function __construct(
|
||||
protected UserService $service
|
||||
) {}
|
||||
|
||||
public function show(Request $request)
|
||||
{
|
||||
return $this->success(
|
||||
new UserResource($this->service->show($request->user())),
|
||||
'Profile berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function update(UpdateProfileRequest $request)
|
||||
{
|
||||
$user = $this->service->update(
|
||||
$request->user(),
|
||||
$request->safe()->except(['current_password', 'password_confirmation']),
|
||||
$request->user()
|
||||
);
|
||||
|
||||
return $this->updated(
|
||||
new UserResource($user),
|
||||
'Profile berhasil diperbarui'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Tenant;
|
||||
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Http\Requests\Tenant\IndexTenantRequest;
|
||||
use App\Http\Requests\Tenant\StoreTenantRequest;
|
||||
use App\Http\Requests\Tenant\UpdateTenantRequest;
|
||||
use App\Http\Resources\Tenant\TenantResource;
|
||||
use App\Models\Tenant;
|
||||
use App\Services\Tenant\TenantService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TenantController extends ApiController
|
||||
{
|
||||
public function __construct(
|
||||
protected TenantService $service
|
||||
) {}
|
||||
|
||||
public function index(IndexTenantRequest $request)
|
||||
{
|
||||
$data = $this->service->list(
|
||||
$request->validated(),
|
||||
$request->user()
|
||||
);
|
||||
|
||||
return $this->successPaginated(
|
||||
$data,
|
||||
TenantResource::class,
|
||||
'Data Tenant berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function store(StoreTenantRequest $request)
|
||||
{
|
||||
$data = $this->service->create(
|
||||
$request->validated()
|
||||
);
|
||||
|
||||
return $this->created(
|
||||
new TenantResource($data),
|
||||
'Tenant berhasil dibuat'
|
||||
);
|
||||
}
|
||||
|
||||
public function show(Request $request, Tenant $tenant)
|
||||
{
|
||||
$this->authorizeTenantAccess($request, $tenant);
|
||||
|
||||
return $this->success(
|
||||
new TenantResource($tenant),
|
||||
'Detail Tenant berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function update(
|
||||
UpdateTenantRequest $request,
|
||||
Tenant $tenant
|
||||
) {
|
||||
$data = $this->service->update(
|
||||
$tenant,
|
||||
$request->validated()
|
||||
);
|
||||
|
||||
return $this->updated(
|
||||
new TenantResource($data),
|
||||
'Tenant berhasil diupdate'
|
||||
);
|
||||
}
|
||||
|
||||
public function destroy(Tenant $tenant)
|
||||
{
|
||||
$this->service->delete($tenant);
|
||||
|
||||
return $this->deleted(
|
||||
'Tenant berhasil dihapus'
|
||||
);
|
||||
}
|
||||
|
||||
private function authorizeTenantAccess(Request $request, Tenant $tenant): void
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if ($user->isMasterAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
abort_unless(
|
||||
$user->userTenants()->where('tenant_id', $tenant->id)->exists(),
|
||||
403
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,12 @@
|
||||
namespace App\Http\Controllers\Ticket;
|
||||
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Http\Requests\Ticket\IndexTicketRequest;
|
||||
use App\Http\Requests\Ticket\StoreTicketRequest;
|
||||
use App\Http\Requests\Ticket\UpdateTicketRequest;
|
||||
use App\Http\Resources\Ticket\TicketResource;
|
||||
use App\Models\Ticket;
|
||||
use App\Services\Ticket\TicketService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TicketController extends ApiController
|
||||
{
|
||||
@@ -16,13 +16,42 @@ class TicketController extends ApiController
|
||||
protected TicketService $service
|
||||
) {}
|
||||
|
||||
public function index(Request $request)
|
||||
public function index(IndexTicketRequest $request)
|
||||
{
|
||||
$tickets = Ticket::latest()->paginate(
|
||||
$request->get('per_page', 10)
|
||||
);
|
||||
$filters = $request->validated();
|
||||
$tickets = Ticket::query()
|
||||
->when(
|
||||
! empty($filters['search']),
|
||||
fn ($query) => $query->where(function ($searchQuery) use ($filters) {
|
||||
$searchQuery->where('ticket_no', 'ILIKE', "%{$filters['search']}%")
|
||||
->orWhere('title', 'ILIKE', "%{$filters['search']}%")
|
||||
->orWhere('description', 'ILIKE', "%{$filters['search']}%");
|
||||
})
|
||||
)
|
||||
->when(
|
||||
! empty($filters['status']),
|
||||
fn ($query) => $query->where('status', $filters['status'])
|
||||
)
|
||||
->when(
|
||||
! empty($filters['priority']),
|
||||
fn ($query) => $query->where('priority', $filters['priority'])
|
||||
)
|
||||
->when(
|
||||
! empty($filters['ticket_type_id']),
|
||||
fn ($query) => $query->where('ticket_type_id', $filters['ticket_type_id'])
|
||||
)
|
||||
->when(
|
||||
! empty($filters['customer_id']),
|
||||
fn ($query) => $query->where('customer_id', $filters['customer_id'])
|
||||
)
|
||||
->latest()
|
||||
->paginate($filters['per_page'] ?? 10);
|
||||
|
||||
return $this->success($tickets);
|
||||
return $this->successPaginated(
|
||||
$tickets,
|
||||
TicketResource::class,
|
||||
'Data Ticket berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function store(StoreTicketRequest $request)
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
namespace App\Http\Controllers\Ticket;
|
||||
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Models\TicketIncidentType;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Services\Ticket\TicketIncidentTypeService;
|
||||
use App\Http\Resources\TicketIncidentType\TicketIncidentTypeResource;
|
||||
use App\Http\Requests\TicketIncidentType\IndexTicketIncidentTypeRequest;
|
||||
use App\Http\Requests\TicketIncidentType\StoreTicketIncidentTypeRequest;
|
||||
use App\Http\Requests\TicketIncidentType\UpdateTicketIncidentTypeRequest;
|
||||
use App\Http\Resources\TicketIncidentType\TicketIncidentTypeResource;
|
||||
use App\Models\TicketIncidentType;
|
||||
use App\Services\Ticket\TicketIncidentTypeService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class TicketIncidentTypeController extends ApiController
|
||||
{
|
||||
@@ -16,12 +17,12 @@ class TicketIncidentTypeController extends ApiController
|
||||
private TicketIncidentTypeService $service
|
||||
) {}
|
||||
|
||||
public function index(): JsonResponse
|
||||
public function index(IndexTicketIncidentTypeRequest $request): JsonResponse
|
||||
{
|
||||
return $this->success(
|
||||
TicketIncidentTypeResource::collection(
|
||||
$this->service->getAll()
|
||||
)
|
||||
return $this->successPaginated(
|
||||
$this->service->list($request->validated()),
|
||||
TicketIncidentTypeResource::class,
|
||||
'Data Incident Type berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
namespace App\Http\Controllers\Ticket;
|
||||
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Http\Requests\TicketType\IndexTicketTypeRequest;
|
||||
use App\Http\Requests\TicketType\StoreTicketTypeRequest;
|
||||
use App\Http\Requests\TicketType\UpdateTicketTypeRequest;
|
||||
use App\Http\Resources\TicketType\TicketTypeResource;
|
||||
use App\Models\TicketType;
|
||||
use App\Services\Ticket\TicketTypeService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TicketTypeController extends ApiController
|
||||
{
|
||||
@@ -16,14 +16,15 @@ class TicketTypeController extends ApiController
|
||||
protected TicketTypeService $service
|
||||
) {}
|
||||
|
||||
public function index(Request $request)
|
||||
public function index(IndexTicketTypeRequest $request)
|
||||
{
|
||||
$data = $this->service->list(
|
||||
$request->all()
|
||||
$request->validated()
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
TicketTypeResource::collection($data),
|
||||
return $this->successPaginated(
|
||||
$data,
|
||||
TicketTypeResource::class,
|
||||
'Data Ticket Type berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\User;
|
||||
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Http\Requests\User\IndexUserRequest;
|
||||
use App\Http\Requests\User\StoreUserRequest;
|
||||
use App\Http\Requests\User\UpdateUserRequest;
|
||||
use App\Http\Resources\User\UserResource;
|
||||
use App\Models\User;
|
||||
use App\Services\Access\AccessService;
|
||||
use App\Services\User\UserService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UserController extends ApiController
|
||||
{
|
||||
public function __construct(
|
||||
protected UserService $service,
|
||||
protected AccessService $access
|
||||
) {}
|
||||
|
||||
public function index(IndexUserRequest $request)
|
||||
{
|
||||
$data = $this->service->list(
|
||||
$request->validated(),
|
||||
$request->user(),
|
||||
$request->attributes->get('tenant_id')
|
||||
);
|
||||
|
||||
return $this->successPaginated(
|
||||
$data,
|
||||
UserResource::class,
|
||||
'Data User berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function store(StoreUserRequest $request)
|
||||
{
|
||||
$data = $this->service->create(
|
||||
$request->validated(),
|
||||
$request->user()
|
||||
);
|
||||
|
||||
return $this->created(
|
||||
new UserResource($data),
|
||||
'User berhasil dibuat'
|
||||
);
|
||||
}
|
||||
|
||||
public function show(Request $request, User $user)
|
||||
{
|
||||
abort_unless(
|
||||
$this->access->canManageUser($request->user(), $user, $request->attributes->get('tenant_id')),
|
||||
403
|
||||
);
|
||||
|
||||
$data = $this->service->show($user);
|
||||
|
||||
return $this->success(
|
||||
new UserResource($data),
|
||||
'Detail User berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function update(UpdateUserRequest $request, User $user)
|
||||
{
|
||||
abort_unless(
|
||||
$this->access->canManageUser($request->user(), $user, $request->attributes->get('tenant_id')),
|
||||
403
|
||||
);
|
||||
|
||||
$data = $this->service->update(
|
||||
$user,
|
||||
$request->validated(),
|
||||
$request->user()
|
||||
);
|
||||
|
||||
return $this->updated(
|
||||
new UserResource($data),
|
||||
'User berhasil diupdate'
|
||||
);
|
||||
}
|
||||
|
||||
public function destroy(Request $request, User $user)
|
||||
{
|
||||
abort_unless(
|
||||
$this->access->canManageUser($request->user(), $user, $request->attributes->get('tenant_id')),
|
||||
403
|
||||
);
|
||||
|
||||
$this->service->delete($user);
|
||||
|
||||
return $this->deleted('User berhasil dihapus');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Wilayah;
|
||||
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Http\Requests\Wilayah\AlamatLengkapRequest;
|
||||
use App\Http\Requests\Wilayah\IndexWilayahRequest;
|
||||
use App\Http\Requests\Wilayah\LookupWilayahRequest;
|
||||
use App\Http\Requests\Wilayah\StoreWilayahRequest;
|
||||
use App\Http\Requests\Wilayah\UpdateWilayahRequest;
|
||||
use App\Http\Resources\Wilayah\WilayahResource;
|
||||
use App\Models\WilayahKabupaten;
|
||||
use App\Models\WilayahKecamatan;
|
||||
use App\Models\WilayahProvinsi;
|
||||
use App\Services\Wilayah\WilayahService;
|
||||
|
||||
class WilayahController extends ApiController
|
||||
{
|
||||
public function __construct(protected WilayahService $service) {}
|
||||
|
||||
public function index(IndexWilayahRequest $request, string $tingkat)
|
||||
{
|
||||
return $this->successPaginated(
|
||||
$this->service->list($tingkat, $request->validated()),
|
||||
WilayahResource::class,
|
||||
"Daftar {$tingkat} berhasil diambil"
|
||||
);
|
||||
}
|
||||
|
||||
public function store(StoreWilayahRequest $request, string $tingkat)
|
||||
{
|
||||
return $this->created(
|
||||
new WilayahResource($this->service->create($tingkat, $request->validated())),
|
||||
ucfirst($tingkat).' berhasil dibuat'
|
||||
);
|
||||
}
|
||||
|
||||
public function show(int $wilayah, string $tingkat)
|
||||
{
|
||||
return $this->success(
|
||||
new WilayahResource($this->service->find($tingkat, $wilayah)),
|
||||
'Detail wilayah berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function update(UpdateWilayahRequest $request, int $wilayah, string $tingkat)
|
||||
{
|
||||
return $this->updated(
|
||||
new WilayahResource($this->service->update($tingkat, $wilayah, $request->validated())),
|
||||
ucfirst($tingkat).' berhasil diperbarui'
|
||||
);
|
||||
}
|
||||
|
||||
public function destroy(int $wilayah, string $tingkat)
|
||||
{
|
||||
$this->service->delete($tingkat, $wilayah);
|
||||
|
||||
return $this->deleted(ucfirst($tingkat).' berhasil dihapus');
|
||||
}
|
||||
|
||||
public function alamatLengkap(AlamatLengkapRequest $request)
|
||||
{
|
||||
return $this->success(
|
||||
$this->service->alamatLengkap($request->validated()),
|
||||
'Alamat wilayah lengkap berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function provinsiOptions(LookupWilayahRequest $request)
|
||||
{
|
||||
return $this->success(
|
||||
$this->service->options('provinsi', $request->validated()),
|
||||
'Pilihan provinsi berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function kabupatenOptions(
|
||||
LookupWilayahRequest $request,
|
||||
WilayahProvinsi $provinsi
|
||||
) {
|
||||
return $this->success(
|
||||
$this->service->options('kabupaten', $request->validated(), $provinsi->id),
|
||||
'Pilihan kabupaten berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function kecamatanOptions(
|
||||
LookupWilayahRequest $request,
|
||||
WilayahKabupaten $kabupaten
|
||||
) {
|
||||
return $this->success(
|
||||
$this->service->options('kecamatan', $request->validated(), $kabupaten->id),
|
||||
'Pilihan kecamatan berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function desaOptions(
|
||||
LookupWilayahRequest $request,
|
||||
WilayahKecamatan $kecamatan
|
||||
) {
|
||||
return $this->success(
|
||||
$this->service->options('desa', $request->validated(), $kecamatan->id),
|
||||
'Pilihan desa berhasil diambil'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureAccessLevel
|
||||
{
|
||||
public function handle(Request $request, Closure $next, string ...$levels): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if (! $user || (! $user->isMasterAdmin() && ! in_array($user->access_level->value, $levels, true))) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'code' => 403,
|
||||
'message' => 'Anda tidak memiliki level akses yang diperlukan.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\Access\AccessService;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureMenuPermission
|
||||
{
|
||||
public function __construct(
|
||||
protected AccessService $access
|
||||
) {}
|
||||
|
||||
public function handle(Request $request, Closure $next, string $menuSlug, string $permission): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
$tenantId = $request->attributes->get('tenant_id');
|
||||
|
||||
if (! $user || ! $this->access->hasMenuPermission($user, $menuSlug, $permission, $tenantId)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'code' => 403,
|
||||
'message' => 'Anda tidak memiliki permission untuk tindakan ini.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureMenuResourcePermission
|
||||
{
|
||||
public function handle(Request $request, Closure $next, string $menuSlug): Response
|
||||
{
|
||||
$permission = match ($request->method()) {
|
||||
'GET', 'HEAD' => 'view',
|
||||
'POST' => 'create',
|
||||
'PUT', 'PATCH' => 'update',
|
||||
'DELETE' => 'delete',
|
||||
default => null,
|
||||
};
|
||||
|
||||
if (! $permission) {
|
||||
abort(405);
|
||||
}
|
||||
|
||||
return app(EnsureMenuPermission::class)
|
||||
->handle($request, $next, $menuSlug, $permission);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\Access\AccessService;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ResolveTenantContext
|
||||
{
|
||||
public function __construct(
|
||||
protected AccessService $access
|
||||
) {}
|
||||
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
$requested = $request->header('X-Tenant-ID');
|
||||
$requestedTenantId = is_numeric($requested) ? (int) $requested : null;
|
||||
$tenantId = $this->access->tenantId($user, $requestedTenantId);
|
||||
|
||||
if ($requestedTenantId && ! $tenantId) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'code' => 403,
|
||||
'message' => 'User tidak memiliki akses ke tenant yang dipilih.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$request->attributes->set('tenant_id', $tenantId);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -23,33 +23,48 @@ class AuditLogFilterRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
|
||||
/** Filter berdasarkan nama modul audit. */
|
||||
'module' => [
|
||||
'nullable',
|
||||
'string'
|
||||
'string',
|
||||
'max:100',
|
||||
],
|
||||
|
||||
/** Filter berdasarkan nama action audit. */
|
||||
'action' => [
|
||||
'nullable',
|
||||
'string'
|
||||
'string',
|
||||
'max:100',
|
||||
],
|
||||
|
||||
/** Filter berdasarkan ID user pelaku. */
|
||||
'user_id' => [
|
||||
'nullable',
|
||||
'integer'
|
||||
'integer',
|
||||
'exists:users,id',
|
||||
],
|
||||
|
||||
/** Filter berdasarkan ID tenant. */
|
||||
'tenant_id' => [
|
||||
'nullable',
|
||||
'integer'
|
||||
'integer',
|
||||
'exists:tenants,id',
|
||||
],
|
||||
|
||||
/** Nomor halaman yang ingin diambil. */
|
||||
'page' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
'min:1',
|
||||
],
|
||||
|
||||
/** Jumlah data per halaman, maksimal 100. */
|
||||
'per_page' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
'min:1',
|
||||
'max:100'
|
||||
]
|
||||
'max:100',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Auth;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class RegisterRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:100'],
|
||||
'username' => ['nullable', 'string', 'max:50', 'unique:users,username'],
|
||||
'email' => ['required', 'email', 'max:100', 'unique:users,email'],
|
||||
'whatsapp_number' => ['required', 'string', 'max:20'],
|
||||
'password' => ['required', 'string', 'min:8', 'max:100'],
|
||||
'verification_channel' => ['required', Rule::in(['email', 'whatsapp'])],
|
||||
'verification_target' => ['required', 'string', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Auth;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ResendVerificationCodeRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'verification_channel' => ['required', Rule::in(['email', 'whatsapp'])],
|
||||
'verification_target' => ['required', 'string', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Auth;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class VerifyRegistrationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'verification_channel' => ['required', Rule::in(['email', 'whatsapp'])],
|
||||
'verification_target' => ['required', 'string', 'max:100'],
|
||||
'verification_code' => ['required', 'digits:6'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\File;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class IndexStoredFileRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'search' => ['nullable', 'string', 'max:255'],
|
||||
'category' => ['nullable', 'string', 'max:80'],
|
||||
'mime_type' => ['nullable', 'string', 'max:150'],
|
||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\File;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreFileRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'file' => [
|
||||
'required',
|
||||
'file',
|
||||
'max:20480',
|
||||
'mimes:jpg,jpeg,png,webp,gif,pdf,doc,docx,xls,xlsx,csv,txt,zip',
|
||||
],
|
||||
'category' => ['nullable', 'string', 'max:80', 'regex:/^[a-z0-9_-]+$/'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'file.max' => 'Ukuran file maksimal 20 MB.',
|
||||
'file.mimes' => 'Jenis file tidak didukung.',
|
||||
'category.regex' => 'Kategori hanya boleh berisi huruf kecil, angka, garis bawah, atau tanda hubung.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Material;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class IndexMaterialRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
/** Kata kunci untuk mencari barcode atau nama material. */
|
||||
'search' => ['nullable', 'string', 'max:255'],
|
||||
/** Filter material berdasarkan ID user/teknisi. */
|
||||
'user_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||
/** Filter jenis material. */
|
||||
'type' => ['nullable', Rule::in(['serialized', 'consumable'])],
|
||||
/** Filter status material. */
|
||||
'status' => ['nullable', 'string', 'max:50'],
|
||||
/** Nomor halaman yang ingin diambil. */
|
||||
'page' => ['nullable', 'integer', 'min:1'],
|
||||
/** Jumlah data per halaman, maksimal 100. */
|
||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\MenuGroup;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class IndexMenuGroupRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
/** Kata kunci untuk mencari nama atau deskripsi menu group. */
|
||||
'search' => ['nullable', 'string', 'max:255'],
|
||||
/** Filter status aktif menu group. */
|
||||
'is_active' => ['nullable', 'boolean'],
|
||||
/** Nomor halaman yang ingin diambil. */
|
||||
'page' => ['nullable', 'integer', 'min:1'],
|
||||
/** Jumlah data per halaman, maksimal 100. */
|
||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||
/** Kolom pengurutan data. */
|
||||
'sort_by' => ['nullable', Rule::in(['id', 'name', 'description', 'is_active', 'is_system', 'created_at'])],
|
||||
/** Arah pengurutan data. */
|
||||
'sort_direction' => ['nullable', Rule::in(['asc', 'desc'])],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\MenuGroup;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreMenuGroupRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$isMaster = $this->user()?->isMasterAdmin() ?? false;
|
||||
$tenantId = $isMaster
|
||||
? $this->input('tenant_id')
|
||||
: $this->attributes->get('tenant_id');
|
||||
|
||||
return [
|
||||
'tenant_id' => [
|
||||
$isMaster ? 'nullable' : 'prohibited',
|
||||
'integer',
|
||||
'exists:tenants,id',
|
||||
],
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:100',
|
||||
Rule::unique('menu_groups', 'name')
|
||||
->where(fn ($query) => $tenantId
|
||||
? $query->where('tenant_id', $tenantId)
|
||||
: $query->whereNull('tenant_id')),
|
||||
],
|
||||
'description' => [
|
||||
'nullable',
|
||||
'string',
|
||||
],
|
||||
'is_active' => [
|
||||
'nullable',
|
||||
'boolean',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'is_active' => filter_var(
|
||||
$this->is_active,
|
||||
FILTER_VALIDATE_BOOLEAN,
|
||||
FILTER_NULL_ON_FAILURE
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\MenuGroup;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class SyncMenuGroupMenusRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'menus' => [
|
||||
'required',
|
||||
'array',
|
||||
'min:1',
|
||||
],
|
||||
'menus.*.menu_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
'distinct',
|
||||
'exists:menu_lists,id',
|
||||
],
|
||||
'menus.*.can_view' => [
|
||||
'nullable',
|
||||
'boolean',
|
||||
],
|
||||
'menus.*.can_create' => [
|
||||
'nullable',
|
||||
'boolean',
|
||||
],
|
||||
'menus.*.can_update' => [
|
||||
'nullable',
|
||||
'boolean',
|
||||
],
|
||||
'menus.*.can_delete' => [
|
||||
'nullable',
|
||||
'boolean',
|
||||
],
|
||||
'menus.*.can_approve' => [
|
||||
'nullable',
|
||||
'boolean',
|
||||
],
|
||||
'menus.*.can_export' => [
|
||||
'nullable',
|
||||
'boolean',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\MenuGroup;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class SyncUserMenuGroupsRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'assignments' => [
|
||||
'required',
|
||||
'array',
|
||||
'min:1',
|
||||
],
|
||||
'assignments.*.menu_group_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
'exists:menu_groups,id',
|
||||
],
|
||||
'assignments.*.tenant_id' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
'exists:tenants,id',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function withValidator($validator): void
|
||||
{
|
||||
$validator->after(function ($validator) {
|
||||
$assignments = $this->input('assignments', []);
|
||||
$seen = [];
|
||||
|
||||
foreach ($assignments as $index => $assignment) {
|
||||
$menuGroupId = $assignment['menu_group_id'] ?? null;
|
||||
$tenantId = $assignment['tenant_id'] ?? null;
|
||||
$key = $menuGroupId.'|'.($tenantId ?? 'null');
|
||||
|
||||
if (isset($seen[$key])) {
|
||||
$validator->errors()->add(
|
||||
"assignments.$index",
|
||||
'Kombinasi menu_group_id dan tenant_id harus unik.'
|
||||
);
|
||||
}
|
||||
|
||||
$seen[$key] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\MenuGroup;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateMenuGroupRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$menuGroupId = $this->route('menu_group')?->id;
|
||||
$isMaster = $this->user()?->isMasterAdmin() ?? false;
|
||||
$tenantId = $isMaster
|
||||
? ($this->exists('tenant_id')
|
||||
? $this->input('tenant_id')
|
||||
: $this->route('menu_group')?->tenant_id)
|
||||
: $this->route('menu_group')?->tenant_id;
|
||||
|
||||
return [
|
||||
'tenant_id' => [
|
||||
$isMaster ? 'nullable' : 'prohibited',
|
||||
'integer',
|
||||
'exists:tenants,id',
|
||||
],
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:100',
|
||||
Rule::unique('menu_groups', 'name')
|
||||
->where(fn ($query) => $tenantId
|
||||
? $query->where('tenant_id', $tenantId)
|
||||
: $query->whereNull('tenant_id'))
|
||||
->ignore($menuGroupId),
|
||||
],
|
||||
'description' => [
|
||||
'nullable',
|
||||
'string',
|
||||
],
|
||||
'is_active' => [
|
||||
'nullable',
|
||||
'boolean',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'is_active' => filter_var(
|
||||
$this->is_active,
|
||||
FILTER_VALIDATE_BOOLEAN,
|
||||
FILTER_NULL_ON_FAILURE
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\MenuList;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class IndexMenuListRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
/** Kata kunci untuk mencari nama, slug, atau URL menu. */
|
||||
'search' => ['nullable', 'string', 'max:255'],
|
||||
/** Filter status aktif menu. */
|
||||
'is_active' => ['nullable', 'boolean'],
|
||||
/** Nomor halaman yang ingin diambil. */
|
||||
'page' => ['nullable', 'integer', 'min:1'],
|
||||
/** Jumlah data per halaman, maksimal 100. */
|
||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\MenuList;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreMenuListRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'parent_id' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
Rule::exists('menu_lists', 'id'),
|
||||
],
|
||||
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
|
||||
'slug' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
'unique:menu_lists,slug',
|
||||
],
|
||||
|
||||
'url' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
|
||||
'icon' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
|
||||
'component' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
|
||||
'sort_order' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
'min:0',
|
||||
],
|
||||
|
||||
'is_active' => [
|
||||
'nullable',
|
||||
'boolean',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'is_active' => filter_var(
|
||||
$this->is_active,
|
||||
FILTER_VALIDATE_BOOLEAN
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\MenuList;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateMenuListRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'parent_id' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
Rule::exists('menu_lists', 'id'),
|
||||
'different:menu_list.id',
|
||||
],
|
||||
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
|
||||
'slug' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::unique('menu_lists', 'slug')
|
||||
->ignore($this->menu_list->id),
|
||||
],
|
||||
|
||||
'url' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
|
||||
'icon' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
|
||||
'component' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
|
||||
'sort_order' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
'min:0',
|
||||
],
|
||||
|
||||
'is_active' => [
|
||||
'nullable',
|
||||
'boolean',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'is_active' => filter_var(
|
||||
$this->is_active,
|
||||
FILTER_VALIDATE_BOOLEAN
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Nas;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class IndexNasResourceRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'search' => ['nullable', 'string', 'max:255'],
|
||||
'status' => ['nullable', Rule::in(['active', 'inactive'])],
|
||||
'connection_type' => ['nullable', Rule::in(['api', 'radius'])],
|
||||
'service_type' => ['nullable', Rule::in(['ppp', 'hotspot', 'radius'])],
|
||||
'page' => ['nullable', 'integer', 'min:1'],
|
||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||
'sort_by' => ['nullable', Rule::in(['id', 'name', 'host', 'connection_type', 'service_type', 'vendor', 'device_type', 'status', 'created_at'])],
|
||||
'sort_direction' => ['nullable', Rule::in(['asc', 'desc'])],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Nas;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreNasResourceRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return array_merge($this->commonRules(), match ($this->route()->defaults['nas_resource'] ?? null) {
|
||||
'mikrotik' => [
|
||||
'connection_type' => ['required', Rule::in(['api', 'radius'])],
|
||||
'host' => ['required', 'string', 'max:255'],
|
||||
'api_port' => ['nullable', 'integer', 'between:1,65535'],
|
||||
'api_username' => ['nullable', 'string', 'max:255'],
|
||||
'api_password' => ['nullable', 'string', 'max:1000'],
|
||||
'radius_auth_port' => ['nullable', 'integer', 'between:1,65535'],
|
||||
'radius_accounting_port' => ['nullable', 'integer', 'between:1,65535'],
|
||||
'radius_secret' => ['nullable', 'string', 'max:1000'],
|
||||
'timeout' => ['nullable', 'integer', 'between:1,300'],
|
||||
],
|
||||
'package-profile' => [
|
||||
'service_type' => ['required', Rule::in(['ppp', 'hotspot', 'radius'])],
|
||||
'external_profile_name' => ['nullable', 'string', 'max:255'],
|
||||
'download_kbps' => ['nullable', 'integer', 'min:0'],
|
||||
'upload_kbps' => ['nullable', 'integer', 'min:0'],
|
||||
'local_address' => ['nullable', 'string', 'max:255'],
|
||||
'remote_address_pool' => ['nullable', 'string', 'max:255'],
|
||||
'session_timeout' => ['nullable', 'integer', 'min:0'],
|
||||
'idle_timeout' => ['nullable', 'integer', 'min:0'],
|
||||
'shared_users' => ['nullable', 'integer', 'min:1'],
|
||||
'price' => ['nullable', 'numeric', 'min:0'],
|
||||
],
|
||||
'olt' => [
|
||||
'vendor' => ['nullable', 'string', 'max:255'],
|
||||
'model' => ['nullable', 'string', 'max:255'],
|
||||
'host' => ['required', 'string', 'max:255'],
|
||||
'snmp_port' => ['nullable', 'integer', 'between:1,65535'],
|
||||
'snmp_version' => ['required', Rule::in(['v1', 'v2c', 'v3'])],
|
||||
'community' => ['nullable', 'string', 'max:1000'],
|
||||
'security_level' => ['nullable', Rule::in(['noAuthNoPriv', 'authNoPriv', 'authPriv'])],
|
||||
'security_name' => ['nullable', 'string', 'max:255'],
|
||||
'auth_protocol' => ['nullable', Rule::in(['MD5', 'SHA', 'SHA224', 'SHA256', 'SHA384', 'SHA512'])],
|
||||
'auth_password' => ['nullable', 'string', 'max:1000'],
|
||||
'privacy_protocol' => ['nullable', Rule::in(['DES', 'AES', 'AES192', 'AES256'])],
|
||||
'privacy_password' => ['nullable', 'string', 'max:1000'],
|
||||
'timeout' => ['nullable', 'integer', 'between:1,300'],
|
||||
],
|
||||
'webfig' => [
|
||||
'device_type' => ['nullable', 'string', 'max:100'],
|
||||
'url' => ['required', 'url:http,https', 'max:2000'],
|
||||
'username' => ['nullable', 'string', 'max:255'],
|
||||
'password' => ['nullable', 'string', 'max:1000'],
|
||||
],
|
||||
default => [],
|
||||
});
|
||||
}
|
||||
|
||||
protected function commonRules(): array
|
||||
{
|
||||
return [
|
||||
'tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'status' => ['nullable', Rule::in(['active', 'inactive'])],
|
||||
'notes' => ['nullable', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Nas;
|
||||
|
||||
class UpdateNasResourceRequest extends StoreNasResourceRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return collect(parent::rules())
|
||||
->map(function ($rules) {
|
||||
$rules = is_array($rules) ? $rules : [$rules];
|
||||
if (($index = array_search('required', $rules, true)) !== false) {
|
||||
$rules[$index] = 'sometimes';
|
||||
}
|
||||
|
||||
return $rules;
|
||||
})
|
||||
->all();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Profile;
|
||||
|
||||
use App\Models\StoredFile;
|
||||
use App\Services\File\StoredFileService;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
class UpdateProfileRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$userId = $this->user()->id;
|
||||
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'username' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::unique('users', 'username')->ignore($userId),
|
||||
],
|
||||
'email' => [
|
||||
'required',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique('users', 'email')->ignore($userId),
|
||||
],
|
||||
'whatsapp_number' => ['nullable', 'string', 'max:20'],
|
||||
'profile_photo_file_id' => ['nullable', 'integer', 'exists:stored_files,id'],
|
||||
'current_password' => ['nullable', 'required_with:password', 'string'],
|
||||
'password' => ['nullable', 'string', 'min:8', 'confirmed'],
|
||||
|
||||
'user_profile' => ['nullable', 'array'],
|
||||
'user_profile.nik' => ['nullable', 'string', 'max:30'],
|
||||
'user_profile.address' => ['nullable', 'string'],
|
||||
'user_profile.provinsi_id' => ['nullable', 'integer', 'exists:wilayah_provinsi,id'],
|
||||
'user_profile.kabupaten_id' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
Rule::exists('wilayah_kabupaten', 'id')->where(
|
||||
'provinsi_id',
|
||||
$this->input('user_profile.provinsi_id')
|
||||
),
|
||||
],
|
||||
'user_profile.kecamatan_id' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
Rule::exists('wilayah_kecamatan', 'id')->where(
|
||||
'kabupaten_id',
|
||||
$this->input('user_profile.kabupaten_id')
|
||||
),
|
||||
],
|
||||
'user_profile.desa_id' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
Rule::exists('wilayah_desa', 'id')->where(
|
||||
'kecamatan_id',
|
||||
$this->input('user_profile.kecamatan_id')
|
||||
),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function after(): array
|
||||
{
|
||||
return [
|
||||
function (Validator $validator) {
|
||||
if ($this->filled('password')
|
||||
&& ! Hash::check((string) $this->input('current_password'), $this->user()->password)) {
|
||||
$validator->errors()->add(
|
||||
'current_password',
|
||||
'Password saat ini tidak sesuai.'
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->filled('profile_photo_file_id')) {
|
||||
$file = StoredFile::find($this->integer('profile_photo_file_id'));
|
||||
|
||||
if ($file) {
|
||||
app(StoredFileService::class)->validateProfilePhoto($file, $this->user());
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Tenant;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class IndexTenantRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
/** Kata kunci untuk mencari kode, nama, atau email tenant. */
|
||||
'search' => ['nullable', 'string', 'max:255'],
|
||||
/** Filter status tenant. */
|
||||
'status' => ['nullable', Rule::in(['active', 'inactive'])],
|
||||
/** Nomor halaman yang ingin diambil. */
|
||||
'page' => ['nullable', 'integer', 'min:1'],
|
||||
/** Jumlah data per halaman, maksimal 100. */
|
||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||
/** Kolom pengurutan data. */
|
||||
'sort_by' => ['nullable', Rule::in(['id', 'tenant_code', 'tenant_name', 'email', 'status', 'created_at'])],
|
||||
/** Arah pengurutan data. */
|
||||
'sort_direction' => ['nullable', Rule::in(['asc', 'desc'])],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Tenant;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreTenantRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'tenant_code' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:100',
|
||||
'unique:tenants,tenant_code',
|
||||
],
|
||||
'tenant_name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'phone' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:50',
|
||||
],
|
||||
'email' => [
|
||||
'nullable',
|
||||
'email',
|
||||
'max:255',
|
||||
],
|
||||
'address' => [
|
||||
'nullable',
|
||||
'string',
|
||||
],
|
||||
'status' => [
|
||||
'nullable',
|
||||
'in:active,inactive',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Tenant;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateTenantRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$tenantId = $this->route('tenant')?->id;
|
||||
|
||||
return [
|
||||
'tenant_code' => [
|
||||
'sometimes',
|
||||
'required',
|
||||
'string',
|
||||
'max:100',
|
||||
Rule::unique('tenants', 'tenant_code')->ignore($tenantId),
|
||||
],
|
||||
'tenant_name' => [
|
||||
'sometimes',
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'phone' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:50',
|
||||
],
|
||||
'email' => [
|
||||
'nullable',
|
||||
'email',
|
||||
'max:255',
|
||||
],
|
||||
'address' => [
|
||||
'nullable',
|
||||
'string',
|
||||
],
|
||||
'status' => [
|
||||
'sometimes',
|
||||
'required',
|
||||
'in:active,inactive',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Ticket;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class IndexTicketRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
/** Kata kunci untuk mencari nomor, judul, atau deskripsi ticket. */
|
||||
'search' => ['nullable', 'string', 'max:255'],
|
||||
/** Filter berdasarkan status ticket. */
|
||||
'status' => ['nullable', Rule::in([
|
||||
'draft', 'open', 'approved', 'assigned', 'progress', 'pending',
|
||||
'resolved', 'closed', 'cancelled', 'rejected',
|
||||
])],
|
||||
/** Filter berdasarkan prioritas ticket. */
|
||||
'priority' => ['nullable', Rule::in(['low', 'medium', 'high', 'critical'])],
|
||||
/** Filter berdasarkan ID tipe ticket. */
|
||||
'ticket_type_id' => ['nullable', 'integer', 'exists:ticket_types,id'],
|
||||
/** Filter berdasarkan ID customer. */
|
||||
'customer_id' => ['nullable', 'integer'],
|
||||
/** Nomor halaman yang ingin diambil. */
|
||||
'page' => ['nullable', 'integer', 'min:1'],
|
||||
/** Jumlah data per halaman, maksimal 100. */
|
||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\TicketIncidentType;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class IndexTicketIncidentTypeRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
/** Kata kunci untuk mencari nama incident type. */
|
||||
'search' => ['nullable', 'string', 'max:255'],
|
||||
/** Filter status aktif incident type. */
|
||||
'is_active' => ['nullable', 'boolean'],
|
||||
/** Nomor halaman yang ingin diambil. */
|
||||
'page' => ['nullable', 'integer', 'min:1'],
|
||||
/** Jumlah data per halaman, maksimal 100. */
|
||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\TicketType;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class IndexTicketTypeRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
/** Kata kunci untuk mencari kode atau nama tipe ticket. */
|
||||
'search' => ['nullable', 'string', 'max:255'],
|
||||
/** Nomor halaman yang ingin diambil. */
|
||||
'page' => ['nullable', 'integer', 'min:1'],
|
||||
/** Jumlah data per halaman, maksimal 100. */
|
||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\User;
|
||||
|
||||
use App\Enums\UserAccessLevel;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class IndexUserRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
/** Kata kunci untuk mencari nama, username, email, atau nomor WhatsApp. */
|
||||
'search' => ['nullable', 'string', 'max:255'],
|
||||
/** Filter status akun user. */
|
||||
'status' => ['nullable', Rule::in(['active', 'inactive', 'suspended'])],
|
||||
/** Filter level akses user. */
|
||||
'access_level' => ['nullable', Rule::enum(UserAccessLevel::class)],
|
||||
/** Nomor halaman yang ingin diambil. */
|
||||
'page' => ['nullable', 'integer', 'min:1'],
|
||||
/** Jumlah data per halaman, maksimal 100. */
|
||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||
/** Kolom pengurutan data. */
|
||||
'sort_by' => ['nullable', Rule::in(['id', 'name', 'username', 'email', 'whatsapp_number', 'access_level', 'status', 'created_at'])],
|
||||
/** Arah pengurutan data. */
|
||||
'sort_direction' => ['nullable', Rule::in(['asc', 'desc'])],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\User;
|
||||
|
||||
use App\Enums\UserAccessLevel;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreUserRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'username' => ['nullable', 'string', 'max:255', 'unique:users,username'],
|
||||
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
|
||||
'whatsapp_number' => ['nullable', 'string', 'max:20'],
|
||||
'password' => ['required', 'string', 'min:8'],
|
||||
'is_master' => ['nullable', 'boolean'],
|
||||
'access_level' => ['nullable', Rule::enum(UserAccessLevel::class)],
|
||||
'status' => ['required', Rule::in(['active', 'inactive', 'suspended'])],
|
||||
|
||||
'user_profile' => ['nullable', 'array'],
|
||||
'user_profile.nik' => ['nullable', 'string', 'max:30'],
|
||||
'user_profile.address' => ['nullable', 'string'],
|
||||
'user_profile.provinsi_id' => ['nullable', 'integer', 'exists:wilayah_provinsi,id'],
|
||||
'user_profile.kabupaten_id' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
Rule::exists('wilayah_kabupaten', 'id')->where(
|
||||
'provinsi_id',
|
||||
$this->input('user_profile.provinsi_id')
|
||||
),
|
||||
],
|
||||
'user_profile.kecamatan_id' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
Rule::exists('wilayah_kecamatan', 'id')->where(
|
||||
'kabupaten_id',
|
||||
$this->input('user_profile.kabupaten_id')
|
||||
),
|
||||
],
|
||||
'user_profile.desa_id' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
Rule::exists('wilayah_desa', 'id')->where(
|
||||
'kecamatan_id',
|
||||
$this->input('user_profile.kecamatan_id')
|
||||
),
|
||||
],
|
||||
|
||||
'user_tenants' => ['nullable', 'array'],
|
||||
'user_tenants.*.tenant_id' => ['required_with:user_tenants', 'integer', 'exists:tenants,id'],
|
||||
'user_tenants.*.is_default' => ['nullable', 'boolean'],
|
||||
|
||||
'user_menu_groups' => ['nullable', 'array'],
|
||||
'user_menu_groups.*.menu_group_id' => ['required_with:user_menu_groups', 'integer', 'exists:menu_groups,id'],
|
||||
'user_menu_groups.*.tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\User;
|
||||
|
||||
use App\Enums\UserAccessLevel;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateUserRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$userId = $this->route('user')?->id ?? $this->route('user');
|
||||
|
||||
return [
|
||||
'name' => ['sometimes', 'required', 'string', 'max:255'],
|
||||
'username' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::unique('users', 'username')->ignore($userId),
|
||||
],
|
||||
'email' => [
|
||||
'sometimes',
|
||||
'required',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique('users', 'email')->ignore($userId),
|
||||
],
|
||||
'whatsapp_number' => ['nullable', 'string', 'max:20'],
|
||||
'password' => ['nullable', 'string', 'min:8'],
|
||||
'is_master' => ['nullable', 'boolean'],
|
||||
'access_level' => ['sometimes', 'required', Rule::enum(UserAccessLevel::class)],
|
||||
'status' => ['sometimes', 'required', Rule::in(['active', 'inactive', 'suspended'])],
|
||||
|
||||
'user_profile' => ['nullable', 'array'],
|
||||
'user_profile.nik' => ['nullable', 'string', 'max:30'],
|
||||
'user_profile.address' => ['nullable', 'string'],
|
||||
'user_profile.provinsi_id' => ['nullable', 'integer', 'exists:wilayah_provinsi,id'],
|
||||
'user_profile.kabupaten_id' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
Rule::exists('wilayah_kabupaten', 'id')->where(
|
||||
'provinsi_id',
|
||||
$this->input('user_profile.provinsi_id')
|
||||
),
|
||||
],
|
||||
'user_profile.kecamatan_id' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
Rule::exists('wilayah_kecamatan', 'id')->where(
|
||||
'kabupaten_id',
|
||||
$this->input('user_profile.kabupaten_id')
|
||||
),
|
||||
],
|
||||
'user_profile.desa_id' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
Rule::exists('wilayah_desa', 'id')->where(
|
||||
'kecamatan_id',
|
||||
$this->input('user_profile.kecamatan_id')
|
||||
),
|
||||
],
|
||||
|
||||
'user_tenants' => ['nullable', 'array'],
|
||||
'user_tenants.*.tenant_id' => ['required_with:user_tenants', 'integer', 'exists:tenants,id'],
|
||||
'user_tenants.*.is_default' => ['nullable', 'boolean'],
|
||||
|
||||
'user_menu_groups' => ['nullable', 'array'],
|
||||
'user_menu_groups.*.menu_group_id' => ['required_with:user_menu_groups', 'integer', 'exists:menu_groups,id'],
|
||||
'user_menu_groups.*.tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Wilayah;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AlamatLengkapRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'desa_id' => ['nullable', 'required_without_all:kecamatan_id,kabupaten_id,provinsi_id', 'integer', 'exists:wilayah_desa,id'],
|
||||
'kecamatan_id' => ['nullable', 'required_without_all:desa_id,kabupaten_id,provinsi_id', 'integer', 'exists:wilayah_kecamatan,id'],
|
||||
'kabupaten_id' => ['nullable', 'required_without_all:desa_id,kecamatan_id,provinsi_id', 'integer', 'exists:wilayah_kabupaten,id'],
|
||||
'provinsi_id' => ['nullable', 'required_without_all:desa_id,kecamatan_id,kabupaten_id', 'integer', 'exists:wilayah_provinsi,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Wilayah;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class IndexWilayahRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
/** Pencarian berdasarkan kode atau nama wilayah. */
|
||||
'search' => ['nullable', 'string', 'max:255'],
|
||||
/** Filter kabupaten berdasarkan provinsi. */
|
||||
'provinsi_id' => ['nullable', 'integer', 'exists:wilayah_provinsi,id'],
|
||||
/** Filter kecamatan berdasarkan kabupaten. */
|
||||
'kabupaten_id' => ['nullable', 'integer', 'exists:wilayah_kabupaten,id'],
|
||||
/** Filter desa berdasarkan kecamatan. */
|
||||
'kecamatan_id' => ['nullable', 'integer', 'exists:wilayah_kecamatan,id'],
|
||||
/** Nomor halaman. */
|
||||
'page' => ['nullable', 'integer', 'min:1'],
|
||||
/** Jumlah data per halaman, maksimal 100. */
|
||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||
/** Kolom pengurutan data. */
|
||||
'sort_by' => ['nullable', Rule::in(['id', 'kode', 'nama', 'created_at'])],
|
||||
/** Arah pengurutan data. */
|
||||
'sort_direction' => ['nullable', Rule::in(['asc', 'desc'])],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Wilayah;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class LookupWilayahRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
/** Pencarian opsional berdasarkan kode atau nama wilayah. */
|
||||
'search' => ['nullable', 'string', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Wilayah;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreWilayahRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$tingkat = $this->route('tingkat');
|
||||
$table = "wilayah_{$tingkat}";
|
||||
|
||||
return [
|
||||
'kode' => ['required', 'string', 'max:20', Rule::unique($table, 'kode')],
|
||||
'nama' => ['required', 'string', 'max:255'],
|
||||
'provinsi_id' => [
|
||||
Rule::requiredIf($tingkat === 'kabupaten'),
|
||||
'integer',
|
||||
'exists:wilayah_provinsi,id',
|
||||
],
|
||||
'kabupaten_id' => [
|
||||
Rule::requiredIf($tingkat === 'kecamatan'),
|
||||
'integer',
|
||||
'exists:wilayah_kabupaten,id',
|
||||
],
|
||||
'kecamatan_id' => [
|
||||
Rule::requiredIf($tingkat === 'desa'),
|
||||
'integer',
|
||||
'exists:wilayah_kecamatan,id',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Wilayah;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateWilayahRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$tingkat = $this->route('tingkat');
|
||||
$table = "wilayah_{$tingkat}";
|
||||
|
||||
return [
|
||||
'kode' => [
|
||||
'sometimes',
|
||||
'required',
|
||||
'string',
|
||||
'max:20',
|
||||
Rule::unique($table, 'kode')->ignore($this->route('wilayah')),
|
||||
],
|
||||
'nama' => ['sometimes', 'required', 'string', 'max:255'],
|
||||
'provinsi_id' => [
|
||||
Rule::excludeIf($tingkat !== 'kabupaten'),
|
||||
'sometimes',
|
||||
'required',
|
||||
'integer',
|
||||
'exists:wilayah_provinsi,id',
|
||||
],
|
||||
'kabupaten_id' => [
|
||||
Rule::excludeIf($tingkat !== 'kecamatan'),
|
||||
'sometimes',
|
||||
'required',
|
||||
'integer',
|
||||
'exists:wilayah_kabupaten,id',
|
||||
],
|
||||
'kecamatan_id' => [
|
||||
Rule::excludeIf($tingkat !== 'desa'),
|
||||
'sometimes',
|
||||
'required',
|
||||
'integer',
|
||||
'exists:wilayah_kecamatan,id',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\File;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class StoredFileResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'uuid' => $this->uuid,
|
||||
'tenant_id' => $this->tenant_id,
|
||||
'original_name' => $this->original_name,
|
||||
'extension' => $this->extension,
|
||||
'mime_type' => $this->mime_type,
|
||||
'size' => $this->size,
|
||||
'category' => $this->category,
|
||||
'created_at' => $this->created_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\MenuGroup;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class MenuGroupResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'tenant_id' => $this->tenant_id,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'is_system' => (bool) $this->is_system,
|
||||
'is_active' => (bool) $this->is_active,
|
||||
'tenant' => $this->whenLoaded('tenant', fn () => $this->tenant ? [
|
||||
'id' => $this->tenant->id,
|
||||
'tenant_code' => $this->tenant->tenant_code,
|
||||
'tenant_name' => $this->tenant->tenant_name,
|
||||
] : null),
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\MenuList;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class MenuListResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'parent_id' => $this->parent_id,
|
||||
'name' => $this->name,
|
||||
'slug' => $this->slug,
|
||||
'url' => $this->url,
|
||||
'icon' => $this->icon,
|
||||
'component' => $this->component,
|
||||
'sort_order' => $this->sort_order,
|
||||
'is_active' => $this->is_active,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Nas;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class NasResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$data = $this->resource->attributesToArray();
|
||||
unset($data['api_password'], $data['radius_secret'], $data['community'], $data['auth_password'], $data['privacy_password'], $data['password']);
|
||||
|
||||
$data['tenant'] = $this->whenLoaded('tenant', fn () => [
|
||||
'id' => $this->tenant->id,
|
||||
'tenant_code' => $this->tenant->tenant_code,
|
||||
'tenant_name' => $this->tenant->tenant_name,
|
||||
]);
|
||||
|
||||
foreach ([
|
||||
'api_password' => 'has_api_password',
|
||||
'radius_secret' => 'has_radius_secret',
|
||||
'community' => 'has_community',
|
||||
'auth_password' => 'has_auth_password',
|
||||
'privacy_password' => 'has_privacy_password',
|
||||
'password' => 'has_password',
|
||||
] as $column => $flag) {
|
||||
if (array_key_exists($column, $this->resource->getAttributes())) {
|
||||
$data[$flag] = filled($this->resource->getRawOriginal($column));
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Tenant;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class TenantResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'tenant_code' => $this->tenant_code,
|
||||
'tenant_name' => $this->tenant_name,
|
||||
'phone' => $this->phone,
|
||||
'email' => $this->email,
|
||||
'address' => $this->address,
|
||||
'status' => $this->status,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\User;
|
||||
|
||||
use App\Http\Resources\File\StoredFileResource;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class UserResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'username' => $this->username,
|
||||
'email' => $this->email,
|
||||
'whatsapp_number' => $this->whatsapp_number,
|
||||
'profile_photo' => $this->whenLoaded(
|
||||
'profilePhoto',
|
||||
fn () => $this->profilePhoto ? new StoredFileResource($this->profilePhoto) : null
|
||||
),
|
||||
'is_master' => (bool) $this->is_master,
|
||||
'access_level' => $this->access_level->value,
|
||||
'status' => $this->status,
|
||||
'verification_channel' => $this->verification_channel,
|
||||
'verification_target' => $this->verification_target,
|
||||
'verified_at' => $this->verified_at,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
|
||||
'user_profile' => $this->whenLoaded('userProfile', function () {
|
||||
return $this->userProfile ? [
|
||||
'id' => $this->userProfile->id,
|
||||
'user_id' => $this->userProfile->user_id,
|
||||
'nik' => $this->userProfile->nik,
|
||||
'address' => $this->userProfile->address,
|
||||
'provinsi_id' => $this->userProfile->provinsi_id,
|
||||
'kabupaten_id' => $this->userProfile->kabupaten_id,
|
||||
'kecamatan_id' => $this->userProfile->kecamatan_id,
|
||||
'desa_id' => $this->userProfile->desa_id,
|
||||
'created_at' => $this->userProfile->created_at,
|
||||
'updated_at' => $this->userProfile->updated_at,
|
||||
] : null;
|
||||
}),
|
||||
|
||||
'user_tenants' => $this->whenLoaded('userTenants', function () {
|
||||
return $this->userTenants->map(function ($item) {
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'user_id' => $item->user_id,
|
||||
'tenant_id' => $item->tenant_id,
|
||||
'is_default' => (bool) $item->is_default,
|
||||
'created_at' => $item->created_at,
|
||||
'updated_at' => $item->updated_at,
|
||||
'tenant' => $item->relationLoaded('tenant') && $item->tenant ? [
|
||||
'id' => $item->tenant->id,
|
||||
'tenant_code' => $item->tenant->tenant_code ?? null,
|
||||
'tenant_name' => $item->tenant->tenant_name ?? null,
|
||||
] : null,
|
||||
];
|
||||
})->values();
|
||||
}),
|
||||
|
||||
'user_menu_groups' => $this->whenLoaded('userMenuGroups', function () {
|
||||
return $this->userMenuGroups->map(function ($item) {
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'user_id' => $item->user_id,
|
||||
'menu_group_id' => $item->menu_group_id,
|
||||
'tenant_id' => $item->tenant_id,
|
||||
'created_at' => $item->created_at,
|
||||
'updated_at' => $item->updated_at,
|
||||
'menu_group' => $item->relationLoaded('menuGroup') && $item->menuGroup ? [
|
||||
'id' => $item->menuGroup->id,
|
||||
'name' => $item->menuGroup->name ?? null,
|
||||
'menu_group_name' => $item->menuGroup->menu_group_name ?? null,
|
||||
] : null,
|
||||
'tenant' => $item->relationLoaded('tenant') && $item->tenant ? [
|
||||
'id' => $item->tenant->id,
|
||||
'tenant_code' => $item->tenant->tenant_code ?? null,
|
||||
'tenant_name' => $item->tenant->tenant_name ?? null,
|
||||
] : null,
|
||||
];
|
||||
})->values();
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Wilayah;
|
||||
|
||||
use App\Models\WilayahDesa;
|
||||
use App\Models\WilayahKabupaten;
|
||||
use App\Models\WilayahKecamatan;
|
||||
use App\Models\WilayahProvinsi;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class WilayahResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$data = [
|
||||
'id' => $this->id,
|
||||
'kode' => $this->kode,
|
||||
'nama' => $this->nama,
|
||||
'tingkat' => match (true) {
|
||||
$this->resource instanceof WilayahDesa => 'desa',
|
||||
$this->resource instanceof WilayahKecamatan => 'kecamatan',
|
||||
$this->resource instanceof WilayahKabupaten => 'kabupaten',
|
||||
$this->resource instanceof WilayahProvinsi => 'provinsi',
|
||||
},
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
];
|
||||
|
||||
if ($this->resource instanceof WilayahKabupaten) {
|
||||
$data['provinsi_id'] = $this->provinsi_id;
|
||||
$data['provinsi'] = $this->whenLoaded('provinsi', fn () => $this->node($this->provinsi));
|
||||
}
|
||||
|
||||
if ($this->resource instanceof WilayahKecamatan) {
|
||||
$data['kabupaten_id'] = $this->kabupaten_id;
|
||||
$data['kabupaten'] = $this->whenLoaded('kabupaten', fn () => $this->node($this->kabupaten));
|
||||
$data['provinsi'] = $this->when(
|
||||
$this->relationLoaded('kabupaten') && $this->kabupaten?->relationLoaded('provinsi'),
|
||||
fn () => $this->node($this->kabupaten?->provinsi)
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->resource instanceof WilayahDesa) {
|
||||
$data['kecamatan_id'] = $this->kecamatan_id;
|
||||
$data['kecamatan'] = $this->whenLoaded('kecamatan', fn () => $this->node($this->kecamatan));
|
||||
$data['kabupaten'] = $this->when(
|
||||
$this->relationLoaded('kecamatan') && $this->kecamatan?->relationLoaded('kabupaten'),
|
||||
fn () => $this->node($this->kecamatan?->kabupaten)
|
||||
);
|
||||
$data['provinsi'] = $this->when(
|
||||
$this->relationLoaded('kecamatan')
|
||||
&& $this->kecamatan?->relationLoaded('kabupaten')
|
||||
&& $this->kecamatan?->kabupaten?->relationLoaded('provinsi'),
|
||||
fn () => $this->node($this->kecamatan?->kabupaten?->provinsi)
|
||||
);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function node($wilayah): ?array
|
||||
{
|
||||
return $wilayah ? [
|
||||
'id' => $wilayah->id,
|
||||
'kode' => $wilayah->kode,
|
||||
'nama' => $wilayah->nama,
|
||||
] : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MenuGroup extends Model
|
||||
{
|
||||
protected $table = 'menu_groups';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'name',
|
||||
'description',
|
||||
'is_system',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'is_system' => 'boolean',
|
||||
];
|
||||
|
||||
public function menus()
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
MenuList::class,
|
||||
'menu_group_menus',
|
||||
'menu_group_id',
|
||||
'menu_id'
|
||||
)->withPivot([
|
||||
'can_view',
|
||||
'can_create',
|
||||
'can_update',
|
||||
'can_delete',
|
||||
'can_approve',
|
||||
'can_export',
|
||||
])->withTimestamps();
|
||||
}
|
||||
|
||||
public function userMenuGroups()
|
||||
{
|
||||
return $this->hasMany(UserMenuGroup::class, 'menu_group_id');
|
||||
}
|
||||
|
||||
public function tenant()
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MenuGroupMenu extends Model
|
||||
{
|
||||
protected $table = 'menu_group_menus';
|
||||
|
||||
protected $fillable = [
|
||||
'menu_group_id',
|
||||
'menu_id',
|
||||
'can_view',
|
||||
'can_create',
|
||||
'can_update',
|
||||
'can_delete',
|
||||
'can_approve',
|
||||
'can_export',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'can_view' => 'boolean',
|
||||
'can_create' => 'boolean',
|
||||
'can_update' => 'boolean',
|
||||
'can_delete' => 'boolean',
|
||||
'can_approve' => 'boolean',
|
||||
'can_export' => 'boolean',
|
||||
];
|
||||
|
||||
public function menuGroup()
|
||||
{
|
||||
return $this->belongsTo(MenuGroup::class, 'menu_group_id');
|
||||
}
|
||||
|
||||
public function menu()
|
||||
{
|
||||
return $this->belongsTo(MenuList::class, 'menu_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MenuList extends Model
|
||||
{
|
||||
use Auditable;
|
||||
|
||||
protected $table = 'menu_lists';
|
||||
|
||||
protected $fillable = [
|
||||
'parent_id',
|
||||
'name',
|
||||
'slug',
|
||||
'url',
|
||||
'icon',
|
||||
'component',
|
||||
'sort_order',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Relationships
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
public function parent()
|
||||
{
|
||||
return $this->belongsTo(self::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function children()
|
||||
{
|
||||
return $this->hasMany(self::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function menuGroups()
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
MenuGroup::class,
|
||||
'menu_group_menus',
|
||||
'menu_id',
|
||||
'menu_group_id'
|
||||
)->withPivot([
|
||||
'can_view',
|
||||
'can_create',
|
||||
'can_update',
|
||||
'can_delete',
|
||||
'can_approve',
|
||||
'can_export',
|
||||
])->withTimestamps();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class NasMikrotik extends Model
|
||||
{
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $hidden = ['api_password', 'radius_secret'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['api_password' => 'encrypted', 'radius_secret' => 'encrypted'];
|
||||
}
|
||||
|
||||
public function tenant()
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class NasOlt extends Model
|
||||
{
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $hidden = ['community', 'auth_password', 'privacy_password'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'community' => 'encrypted',
|
||||
'auth_password' => 'encrypted',
|
||||
'privacy_password' => 'encrypted',
|
||||
];
|
||||
}
|
||||
|
||||
public function tenant()
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class NasPackageProfile extends Model
|
||||
{
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['price' => 'decimal:2'];
|
||||
}
|
||||
|
||||
public function tenant()
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class NasWebfigDevice extends Model
|
||||
{
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $hidden = ['password'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['password' => 'encrypted'];
|
||||
}
|
||||
|
||||
public function tenant()
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StoredFile extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
'tenant_id',
|
||||
'uploaded_by',
|
||||
'disk',
|
||||
'bucket',
|
||||
'object_key',
|
||||
'original_name',
|
||||
'extension',
|
||||
'mime_type',
|
||||
'size',
|
||||
'category',
|
||||
'visibility',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['size' => 'integer'];
|
||||
}
|
||||
|
||||
public function getRouteKeyName(): string
|
||||
{
|
||||
return 'uuid';
|
||||
}
|
||||
|
||||
public function uploader(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'uploaded_by');
|
||||
}
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Tenant extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'tenant_code',
|
||||
'tenant_name',
|
||||
'phone',
|
||||
'email',
|
||||
'address',
|
||||
'status',
|
||||
];
|
||||
}
|
||||
+63
-15
@@ -2,19 +2,25 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\UserAccessLevel;
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
// use App\Traits\Auditable;
|
||||
|
||||
// use App\Traits\Auditable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
use HasApiTokens;
|
||||
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
use SoftDeletes;
|
||||
// use Audittable;
|
||||
|
||||
/**
|
||||
@@ -24,8 +30,19 @@ class User extends Authenticatable
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'username',
|
||||
'email',
|
||||
'whatsapp_number',
|
||||
'profile_photo_file_id',
|
||||
'password',
|
||||
'is_master',
|
||||
'access_level',
|
||||
'status',
|
||||
'verification_channel',
|
||||
'verification_target',
|
||||
'verification_code',
|
||||
'verification_code_expires_at',
|
||||
'verified_at',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -48,24 +65,55 @@ class User extends Authenticatable
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'is_master' => 'boolean',
|
||||
'access_level' => UserAccessLevel::class,
|
||||
'verification_code_expires_at' => 'datetime',
|
||||
'verified_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function login(Request $request)
|
||||
public function menuGroups()
|
||||
{
|
||||
$user = User::where('email', $request->email)->first();
|
||||
|
||||
if (!$user || !Hash::check($request->password, $user->password)) {
|
||||
return response()->json([
|
||||
'message' => 'Login gagal'
|
||||
], 401);
|
||||
return $this->belongsToMany(
|
||||
MenuGroup::class,
|
||||
'user_menu_groups',
|
||||
'user_id',
|
||||
'menu_group_id'
|
||||
)->withPivot(['tenant_id'])->withTimestamps();
|
||||
}
|
||||
|
||||
$token = $user->createToken('services-core')->plainTextToken;
|
||||
public function userProfile()
|
||||
{
|
||||
return $this->hasOne(UserProfile::class, 'user_id');
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'token' => $token,
|
||||
'user' => $user
|
||||
]);
|
||||
public function profilePhoto()
|
||||
{
|
||||
return $this->belongsTo(StoredFile::class, 'profile_photo_file_id');
|
||||
}
|
||||
|
||||
public function userTenants()
|
||||
{
|
||||
return $this->hasMany(UserTenant::class, 'user_id');
|
||||
}
|
||||
|
||||
public function userMenuGroups()
|
||||
{
|
||||
return $this->hasMany(UserMenuGroup::class, 'user_id');
|
||||
}
|
||||
|
||||
public function isMasterAdmin(): bool
|
||||
{
|
||||
return $this->is_master || $this->access_level === UserAccessLevel::MASTER_ADMIN;
|
||||
}
|
||||
|
||||
public function isTenantOwner(): bool
|
||||
{
|
||||
return $this->access_level === UserAccessLevel::TENANT_OWNER;
|
||||
}
|
||||
}
|
||||
|
||||
// {
|
||||
// "email": "master@services-core.local",
|
||||
// "password": "Master@12345"
|
||||
// }
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UserMenuGroup extends Model
|
||||
{
|
||||
protected $table = 'user_menu_groups';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'menu_group_id',
|
||||
'tenant_id',
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
public function menuGroup()
|
||||
{
|
||||
return $this->belongsTo(MenuGroup::class, 'menu_group_id');
|
||||
}
|
||||
|
||||
public function tenant()
|
||||
{
|
||||
return $this->belongsTo(Tenant::class, 'tenant_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UserProfile extends Model
|
||||
{
|
||||
protected $table = 'user_profiles';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'nik',
|
||||
'address',
|
||||
'provinsi_id',
|
||||
'kabupaten_id',
|
||||
'kecamatan_id',
|
||||
'desa_id',
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UserTenant extends Model
|
||||
{
|
||||
protected $table = 'user_tenants';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'tenant_id',
|
||||
'is_default',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_default' => 'boolean',
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
public function tenant()
|
||||
{
|
||||
return $this->belongsTo(Tenant::class, 'tenant_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class WilayahDesa extends Model
|
||||
{
|
||||
protected $table = 'wilayah_desa';
|
||||
|
||||
protected $fillable = ['kecamatan_id', 'kode', 'nama'];
|
||||
|
||||
public function kecamatan(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(WilayahKecamatan::class, 'kecamatan_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class WilayahKabupaten extends Model
|
||||
{
|
||||
protected $table = 'wilayah_kabupaten';
|
||||
|
||||
protected $fillable = ['provinsi_id', 'kode', 'nama'];
|
||||
|
||||
public function provinsi(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(WilayahProvinsi::class, 'provinsi_id');
|
||||
}
|
||||
|
||||
public function kecamatan(): HasMany
|
||||
{
|
||||
return $this->hasMany(WilayahKecamatan::class, 'kabupaten_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class WilayahKecamatan extends Model
|
||||
{
|
||||
protected $table = 'wilayah_kecamatan';
|
||||
|
||||
protected $fillable = ['kabupaten_id', 'kode', 'nama'];
|
||||
|
||||
public function kabupaten(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(WilayahKabupaten::class, 'kabupaten_id');
|
||||
}
|
||||
|
||||
public function desa(): HasMany
|
||||
{
|
||||
return $this->hasMany(WilayahDesa::class, 'kecamatan_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class WilayahProvinsi extends Model
|
||||
{
|
||||
protected $table = 'wilayah_provinsi';
|
||||
|
||||
protected $fillable = ['kode', 'nama'];
|
||||
|
||||
public function kabupaten(): HasMany
|
||||
{
|
||||
return $this->hasMany(WilayahKabupaten::class, 'provinsi_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Access;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AccessService
|
||||
{
|
||||
public function tenantId(User $user, ?int $requestedTenantId = null): ?int
|
||||
{
|
||||
if ($requestedTenantId) {
|
||||
if ($user->isMasterAdmin() || $user->userTenants()->where('tenant_id', $requestedTenantId)->exists()) {
|
||||
return $requestedTenantId;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $user->userTenants()
|
||||
->orderByDesc('is_default')
|
||||
->orderBy('id')
|
||||
->value('tenant_id');
|
||||
}
|
||||
|
||||
public function hasMenuPermission(
|
||||
User $user,
|
||||
string $menuSlug,
|
||||
string $permission,
|
||||
?int $tenantId
|
||||
): bool {
|
||||
if ($user->isMasterAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$column = 'can_'.$permission;
|
||||
$allowed = [
|
||||
'can_view',
|
||||
'can_create',
|
||||
'can_update',
|
||||
'can_delete',
|
||||
'can_approve',
|
||||
'can_export',
|
||||
];
|
||||
|
||||
if (! in_array($column, $allowed, true) || ! $tenantId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return DB::table('user_menu_groups as umg')
|
||||
->join('menu_groups as mg', 'mg.id', '=', 'umg.menu_group_id')
|
||||
->join('menu_group_menus as mgm', 'mgm.menu_group_id', '=', 'mg.id')
|
||||
->join('menu_lists as ml', 'ml.id', '=', 'mgm.menu_id')
|
||||
->where('umg.user_id', $user->id)
|
||||
->where('umg.tenant_id', $tenantId)
|
||||
->where('mg.is_active', true)
|
||||
->where('ml.is_active', true)
|
||||
->where('ml.slug', $menuSlug)
|
||||
->where("mgm.$column", true)
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function canManageUser(User $actor, User $target, ?int $tenantId): bool
|
||||
{
|
||||
if ($actor->isMasterAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! $actor->isTenantOwner() || ! $tenantId || $target->isMasterAdmin()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $target->userTenants()->where('tenant_id', $tenantId)->exists();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class AuthService
|
||||
{
|
||||
public function register(array $data): array
|
||||
{
|
||||
$code = $this->generateCode();
|
||||
|
||||
$user = User::create([
|
||||
'name' => $data['name'],
|
||||
'username' => $data['username'] ?? null,
|
||||
'email' => $data['email'],
|
||||
'whatsapp_number' => $data['whatsapp_number'],
|
||||
'password' => Hash::make($data['password']),
|
||||
'is_master' => false,
|
||||
'status' => 'inactive',
|
||||
'verification_channel' => $data['verification_channel'],
|
||||
'verification_target' => $data['verification_target'],
|
||||
'verification_code' => $code,
|
||||
'verification_code_expires_at' => now()->addMinutes(10),
|
||||
'verified_at' => null,
|
||||
]);
|
||||
|
||||
return [
|
||||
'user' => $user,
|
||||
'verification' => [
|
||||
'channel' => $user->verification_channel,
|
||||
'target' => $user->verification_target,
|
||||
'code' => $code,
|
||||
'expires_at' => $user->verification_code_expires_at,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function verifyRegistration(array $data): User
|
||||
{
|
||||
$user = User::query()
|
||||
->where('verification_channel', $data['verification_channel'])
|
||||
->where('verification_target', $data['verification_target'])
|
||||
->first();
|
||||
|
||||
if (!$user) {
|
||||
throw ValidationException::withMessages([
|
||||
'verification_target' => 'Target verifikasi tidak ditemukan.',
|
||||
]);
|
||||
}
|
||||
|
||||
if (empty($user->verification_code) || $user->verification_code !== $data['verification_code']) {
|
||||
throw ValidationException::withMessages([
|
||||
'verification_code' => 'Kode verifikasi tidak valid.',
|
||||
]);
|
||||
}
|
||||
|
||||
if (
|
||||
empty($user->verification_code_expires_at) ||
|
||||
now()->greaterThan($user->verification_code_expires_at)
|
||||
) {
|
||||
throw ValidationException::withMessages([
|
||||
'verification_code' => 'Kode verifikasi sudah kadaluarsa.',
|
||||
]);
|
||||
}
|
||||
|
||||
$user->update([
|
||||
'status' => 'active',
|
||||
'verified_at' => now(),
|
||||
'verification_code' => null,
|
||||
'verification_code_expires_at' => null,
|
||||
]);
|
||||
|
||||
return $user->fresh();
|
||||
}
|
||||
|
||||
public function resendVerificationCode(array $data): array
|
||||
{
|
||||
$user = User::query()
|
||||
->where('verification_channel', $data['verification_channel'])
|
||||
->where('verification_target', $data['verification_target'])
|
||||
->first();
|
||||
|
||||
if (!$user) {
|
||||
throw ValidationException::withMessages([
|
||||
'verification_target' => 'Target verifikasi tidak ditemukan.',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($user->status === 'active') {
|
||||
throw ValidationException::withMessages([
|
||||
'user' => 'User sudah aktif dan tidak memerlukan verifikasi ulang.',
|
||||
]);
|
||||
}
|
||||
|
||||
$code = $this->generateCode();
|
||||
|
||||
$user->update([
|
||||
'verification_code' => $code,
|
||||
'verification_code_expires_at' => now()->addMinutes(10),
|
||||
]);
|
||||
|
||||
return [
|
||||
'channel' => $user->verification_channel,
|
||||
'target' => $user->verification_target,
|
||||
'code' => $code,
|
||||
'expires_at' => $user->verification_code_expires_at,
|
||||
];
|
||||
}
|
||||
|
||||
protected function generateCode(): string
|
||||
{
|
||||
return str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\File;
|
||||
|
||||
use App\Models\StoredFile;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class StoredFileService
|
||||
{
|
||||
public function list(array $filters, User $user, ?int $tenantId): LengthAwarePaginator
|
||||
{
|
||||
return StoredFile::query()
|
||||
->when(! $user->isMasterAdmin(), function ($query) use ($user, $tenantId) {
|
||||
$query->where(function ($scope) use ($user, $tenantId) {
|
||||
$scope->where('uploaded_by', $user->id)
|
||||
->when($tenantId, fn ($tenantScope) => $tenantScope->orWhere('tenant_id', $tenantId));
|
||||
});
|
||||
})
|
||||
->when($filters['search'] ?? null, function ($query, $search) {
|
||||
$query->where('original_name', 'like', "%{$search}%");
|
||||
})
|
||||
->when($filters['category'] ?? null, fn ($query, $category) => $query->where('category', $category))
|
||||
->when($filters['mime_type'] ?? null, fn ($query, $mimeType) => $query->where('mime_type', $mimeType))
|
||||
->latest('id')
|
||||
->paginate($filters['per_page'] ?? 10);
|
||||
}
|
||||
|
||||
public function store(UploadedFile $upload, User $user, ?int $tenantId, string $category): StoredFile
|
||||
{
|
||||
$uuid = (string) Str::uuid();
|
||||
$extension = strtolower($upload->getClientOriginalExtension() ?: $upload->extension() ?: 'bin');
|
||||
$scope = $tenantId ? "tenants/{$tenantId}" : 'global';
|
||||
$objectKey = "{$scope}/{$category}/".now()->format('Y/m')."/{$uuid}.{$extension}";
|
||||
$disk = Storage::disk('minio');
|
||||
|
||||
$disk->putFileAs(dirname($objectKey), $upload, basename($objectKey), [
|
||||
'visibility' => 'private',
|
||||
'ContentType' => $upload->getMimeType() ?: 'application/octet-stream',
|
||||
]);
|
||||
|
||||
try {
|
||||
return DB::transaction(fn () => StoredFile::create([
|
||||
'uuid' => $uuid,
|
||||
'tenant_id' => $tenantId,
|
||||
'uploaded_by' => $user->id,
|
||||
'disk' => 'minio',
|
||||
'bucket' => (string) config('filesystems.disks.minio.bucket'),
|
||||
'object_key' => $objectKey,
|
||||
'original_name' => $upload->getClientOriginalName(),
|
||||
'extension' => $extension,
|
||||
'mime_type' => $upload->getMimeType(),
|
||||
'size' => $upload->getSize(),
|
||||
'category' => $category,
|
||||
'visibility' => 'private',
|
||||
]));
|
||||
} catch (\Throwable $exception) {
|
||||
$disk->delete($objectKey);
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
public function temporaryUrl(StoredFile $file, User $user): array
|
||||
{
|
||||
$this->authorizeAccess($file, $user);
|
||||
$expiresAt = now()->addMinutes(10);
|
||||
|
||||
return [
|
||||
'url' => Storage::disk($file->disk)->temporaryUrl(
|
||||
$file->object_key,
|
||||
$expiresAt,
|
||||
['ResponseContentDisposition' => 'inline; filename="'.addslashes($file->original_name).'"']
|
||||
),
|
||||
'expires_at' => $expiresAt->toIso8601String(),
|
||||
];
|
||||
}
|
||||
|
||||
public function delete(StoredFile $file, User $user): void
|
||||
{
|
||||
if (! $user->isMasterAdmin() && $file->uploaded_by !== $user->id) {
|
||||
abort(403, 'File hanya dapat dihapus oleh pengunggah atau Master Admin.');
|
||||
}
|
||||
|
||||
Storage::disk($file->disk)->delete($file->object_key);
|
||||
$file->delete();
|
||||
}
|
||||
|
||||
public function authorizeAccess(StoredFile $file, User $user): void
|
||||
{
|
||||
if ($user->isMasterAdmin() || $file->uploaded_by === $user->id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$canAccessTenant = $file->tenant_id && $user->userTenants()
|
||||
->where('tenant_id', $file->tenant_id)
|
||||
->exists();
|
||||
|
||||
abort_unless($canAccessTenant, 403, 'Anda tidak memiliki akses ke file ini.');
|
||||
}
|
||||
|
||||
public function validateProfilePhoto(StoredFile $file, User $user): void
|
||||
{
|
||||
if ($file->uploaded_by !== $user->id
|
||||
|| $file->category !== 'profile'
|
||||
|| ! str_starts_with((string) $file->mime_type, 'image/')
|
||||
|| $file->size > 5 * 1024 * 1024) {
|
||||
throw ValidationException::withMessages([
|
||||
'profile_photo_file_id' => 'Foto profil harus berupa file gambar kategori profile yang Anda unggah sendiri.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
namespace App\Services\Material;
|
||||
|
||||
use App\Models\TechnicianMaterial;
|
||||
use App\Models\TechnicianConsumable;
|
||||
use App\Models\MaterialMovement;
|
||||
use Carbon\Carbon;
|
||||
use App\Models\TechnicianConsumable;
|
||||
use App\Models\TechnicianMaterial;
|
||||
use DB;
|
||||
|
||||
class MaterialService
|
||||
@@ -57,6 +56,31 @@ class MaterialService
|
||||
$consumable->where('user_id', $filters['user_id']);
|
||||
}
|
||||
|
||||
if (! empty($filters['search'])) {
|
||||
$search = $filters['search'];
|
||||
$serialized->where(function ($query) use ($search) {
|
||||
$query->where('barcode_id', 'ILIKE', "%$search%")
|
||||
->orWhere('material_name', 'ILIKE', "%$search%");
|
||||
});
|
||||
$consumable->where(function ($query) use ($search) {
|
||||
$query->where('barcode_id', 'ILIKE', "%$search%")
|
||||
->orWhere('material_name', 'ILIKE', "%$search%");
|
||||
});
|
||||
}
|
||||
|
||||
if (! empty($filters['status'])) {
|
||||
$serialized->where('status', $filters['status']);
|
||||
$consumable->where('status', $filters['status']);
|
||||
}
|
||||
|
||||
if (($filters['type'] ?? null) === 'serialized') {
|
||||
$consumable->whereRaw('1 = 0');
|
||||
}
|
||||
|
||||
if (($filters['type'] ?? null) === 'consumable') {
|
||||
$serialized->whereRaw('1 = 0');
|
||||
}
|
||||
|
||||
$query = $serialized->unionAll($consumable);
|
||||
|
||||
return DB::query()
|
||||
@@ -64,6 +88,7 @@ class MaterialService
|
||||
->orderByDesc('created_at')
|
||||
->paginate($filters['per_page'] ?? 15);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| ASSIGN MATERIAL (AUTO DETECT TYPE)
|
||||
@@ -154,7 +179,7 @@ class MaterialService
|
||||
if ($serialized) {
|
||||
|
||||
$serialized->update([
|
||||
'user_id' => $data['to_user_id']
|
||||
'user_id' => $data['to_user_id'],
|
||||
]);
|
||||
|
||||
MaterialMovement::create([
|
||||
@@ -173,7 +198,7 @@ class MaterialService
|
||||
$consumable = TechnicianConsumable::where('barcode_id', $barcode)->first();
|
||||
|
||||
$consumable->update([
|
||||
'user_id' => $data['to_user_id']
|
||||
'user_id' => $data['to_user_id'],
|
||||
]);
|
||||
|
||||
MaterialMovement::create([
|
||||
@@ -207,7 +232,7 @@ class MaterialService
|
||||
|
||||
$serialized->update([
|
||||
'user_id' => null,
|
||||
'status' => 'returned'
|
||||
'status' => 'returned',
|
||||
]);
|
||||
|
||||
MaterialMovement::create([
|
||||
@@ -226,7 +251,7 @@ class MaterialService
|
||||
|
||||
$consumable->update([
|
||||
'user_id' => null,
|
||||
'status' => 'returned'
|
||||
'status' => 'returned',
|
||||
]);
|
||||
|
||||
MaterialMovement::create([
|
||||
@@ -249,6 +274,7 @@ class MaterialService
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
$item->type = 'serialized';
|
||||
|
||||
return $item;
|
||||
});
|
||||
|
||||
@@ -256,6 +282,7 @@ class MaterialService
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
$item->type = 'consumable';
|
||||
|
||||
return $item;
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Menu;
|
||||
|
||||
use App\Models\MenuGroup;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class MenuGroupService
|
||||
{
|
||||
public function list(array $filters = [], ?User $actor = null, ?int $tenantId = null)
|
||||
{
|
||||
return MenuGroup::query()
|
||||
->with('tenant')
|
||||
->when(
|
||||
$actor && ! $actor->isMasterAdmin(),
|
||||
fn ($q) => $q->where('tenant_id', $tenantId)
|
||||
)
|
||||
->when(
|
||||
! empty($filters['search']),
|
||||
fn ($q) => $q->where(function ($x) use ($filters) {
|
||||
$x->where('name', 'ILIKE', "%{$filters['search']}%")
|
||||
->orWhere('description', 'ILIKE', "%{$filters['search']}%");
|
||||
})
|
||||
)
|
||||
->when(
|
||||
array_key_exists('is_active', $filters) && $filters['is_active'] !== null && $filters['is_active'] !== '',
|
||||
fn ($q) => $q->where('is_active', filter_var($filters['is_active'], FILTER_VALIDATE_BOOLEAN))
|
||||
)
|
||||
->orderBy(
|
||||
$filters['sort_by'] ?? 'id',
|
||||
$filters['sort_direction'] ?? 'desc'
|
||||
)
|
||||
->paginate($filters['per_page'] ?? 10);
|
||||
}
|
||||
|
||||
public function create(array $data, User $actor, ?int $tenantId): MenuGroup
|
||||
{
|
||||
if (! array_key_exists('is_active', $data) || $data['is_active'] === null) {
|
||||
$data['is_active'] = true;
|
||||
}
|
||||
|
||||
$scopeTenantId = $actor->isMasterAdmin()
|
||||
? ($data['tenant_id'] ?? null)
|
||||
: $tenantId;
|
||||
|
||||
if (! $actor->isMasterAdmin() && ! $scopeTenantId) {
|
||||
throw ValidationException::withMessages([
|
||||
'tenant_id' => 'Tenant aktif wajib tersedia untuk membuat Menu Group.',
|
||||
]);
|
||||
}
|
||||
|
||||
$data['tenant_id'] = $scopeTenantId;
|
||||
$data['is_system'] = $actor->isMasterAdmin() && ! $scopeTenantId;
|
||||
|
||||
return MenuGroup::create($data)->load('tenant');
|
||||
}
|
||||
|
||||
public function update(
|
||||
MenuGroup $menuGroup,
|
||||
array $data,
|
||||
User $actor,
|
||||
?int $tenantId
|
||||
): MenuGroup {
|
||||
if (array_key_exists('is_active', $data) && $data['is_active'] === null) {
|
||||
unset($data['is_active']);
|
||||
}
|
||||
|
||||
if ($actor->isMasterAdmin()) {
|
||||
if (array_key_exists('tenant_id', $data)) {
|
||||
$data['is_system'] = ! $data['tenant_id'];
|
||||
}
|
||||
} else {
|
||||
unset($data['tenant_id'], $data['is_system']);
|
||||
if ((int) $menuGroup->tenant_id !== (int) $tenantId) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
|
||||
$menuGroup->update($data);
|
||||
|
||||
return $menuGroup->fresh('tenant');
|
||||
}
|
||||
|
||||
public function delete(MenuGroup $menuGroup): bool
|
||||
{
|
||||
$hasMenuMappings = $menuGroup->menus()->exists();
|
||||
$hasUserMappings = $menuGroup->userMenuGroups()->exists();
|
||||
|
||||
if ($hasMenuMappings || $hasUserMappings) {
|
||||
throw ValidationException::withMessages([
|
||||
'menu_group' => 'Menu Group tidak dapat dihapus karena masih memiliki relasi menu/user.',
|
||||
]);
|
||||
}
|
||||
|
||||
return $menuGroup->delete();
|
||||
}
|
||||
|
||||
public function listMenus(MenuGroup $menuGroup): array
|
||||
{
|
||||
$rows = DB::table('menu_lists as m')
|
||||
->leftJoin('menu_group_menus as mgm', function ($join) use ($menuGroup) {
|
||||
$join->on('mgm.menu_id', '=', 'm.id')
|
||||
->where('mgm.menu_group_id', '=', $menuGroup->id);
|
||||
})
|
||||
->where('m.is_active', true)
|
||||
->select(
|
||||
'm.id',
|
||||
'm.parent_id',
|
||||
'm.name',
|
||||
'm.slug',
|
||||
'm.url',
|
||||
'm.icon',
|
||||
'm.component',
|
||||
'm.sort_order',
|
||||
DB::raw('COALESCE(mgm.can_view, false) as can_view'),
|
||||
DB::raw('COALESCE(mgm.can_create, false) as can_create'),
|
||||
DB::raw('COALESCE(mgm.can_update, false) as can_update'),
|
||||
DB::raw('COALESCE(mgm.can_delete, false) as can_delete'),
|
||||
DB::raw('COALESCE(mgm.can_approve, false) as can_approve'),
|
||||
DB::raw('COALESCE(mgm.can_export, false) as can_export')
|
||||
)
|
||||
->orderBy('m.sort_order')
|
||||
->orderBy('m.id')
|
||||
->get();
|
||||
|
||||
return $rows->map(function ($row) {
|
||||
return [
|
||||
'menu_id' => (int) $row->id,
|
||||
'parent_id' => $row->parent_id ? (int) $row->parent_id : null,
|
||||
'name' => $row->name,
|
||||
'slug' => $row->slug,
|
||||
'url' => $row->url,
|
||||
'icon' => $row->icon,
|
||||
'component' => $row->component,
|
||||
'sort_order' => (int) $row->sort_order,
|
||||
'permissions' => [
|
||||
'can_view' => (bool) $row->can_view,
|
||||
'can_create' => (bool) $row->can_create,
|
||||
'can_update' => (bool) $row->can_update,
|
||||
'can_delete' => (bool) $row->can_delete,
|
||||
'can_approve' => (bool) $row->can_approve,
|
||||
'can_export' => (bool) $row->can_export,
|
||||
],
|
||||
'available_permissions' => [
|
||||
'can_view' => true,
|
||||
'can_create' => true,
|
||||
'can_update' => true,
|
||||
'can_delete' => true,
|
||||
'can_approve' => true,
|
||||
'can_export' => true,
|
||||
],
|
||||
];
|
||||
})->all();
|
||||
}
|
||||
|
||||
public function syncMenus(MenuGroup $menuGroup, array $menus, ?User $actor = null): void
|
||||
{
|
||||
if ($actor && ! $actor->isMasterAdmin()) {
|
||||
$allowed = $this->getAllowedMenuPermissionsForUser(
|
||||
$actor,
|
||||
request()->attributes->get('tenant_id')
|
||||
);
|
||||
|
||||
foreach ($menus as $menu) {
|
||||
$actorPermissions = $allowed[(int) $menu['menu_id']] ?? null;
|
||||
|
||||
if (! $actorPermissions) {
|
||||
throw ValidationException::withMessages([
|
||||
'menus' => 'Terdapat menu di luar hak akses user login.',
|
||||
]);
|
||||
}
|
||||
|
||||
foreach (['view', 'create', 'update', 'delete', 'approve', 'export'] as $permission) {
|
||||
if (($menu["can_$permission"] ?? false) && ! $actorPermissions["can_$permission"]) {
|
||||
throw ValidationException::withMessages([
|
||||
'menus' => "Permission can_$permission tidak boleh melebihi akses Anda.",
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$syncData = [];
|
||||
|
||||
foreach ($menus as $menu) {
|
||||
$syncData[$menu['menu_id']] = [
|
||||
'can_view' => (bool) ($menu['can_view'] ?? false),
|
||||
'can_create' => (bool) ($menu['can_create'] ?? false),
|
||||
'can_update' => (bool) ($menu['can_update'] ?? false),
|
||||
'can_delete' => (bool) ($menu['can_delete'] ?? false),
|
||||
'can_approve' => (bool) ($menu['can_approve'] ?? false),
|
||||
'can_export' => (bool) ($menu['can_export'] ?? false),
|
||||
'updated_at' => now(),
|
||||
'created_at' => now(),
|
||||
];
|
||||
}
|
||||
|
||||
$menuGroup->menus()->sync($syncData);
|
||||
}
|
||||
|
||||
public function listAvailableMenusForGroup(MenuGroup $menuGroup, User $actor): array
|
||||
{
|
||||
if ($actor->isMasterAdmin()) {
|
||||
return $this->listMenus($menuGroup);
|
||||
}
|
||||
|
||||
$allowedPermissions = $this->getAllowedMenuPermissionsForUser(
|
||||
$actor,
|
||||
request()->attributes->get('tenant_id')
|
||||
);
|
||||
$allowedMenuIds = array_keys($allowedPermissions);
|
||||
|
||||
$rows = DB::table('menu_lists as m')
|
||||
->leftJoin('menu_group_menus as mgm', function ($join) use ($menuGroup) {
|
||||
$join->on('mgm.menu_id', '=', 'm.id')
|
||||
->where('mgm.menu_group_id', '=', $menuGroup->id);
|
||||
})
|
||||
->where('m.is_active', true)
|
||||
->whereIn('m.id', $allowedMenuIds)
|
||||
->select(
|
||||
'm.id',
|
||||
'm.parent_id',
|
||||
'm.name',
|
||||
'm.sort_order',
|
||||
DB::raw('COALESCE(mgm.can_view, false) as can_view'),
|
||||
DB::raw('COALESCE(mgm.can_create, false) as can_create'),
|
||||
DB::raw('COALESCE(mgm.can_update, false) as can_update'),
|
||||
DB::raw('COALESCE(mgm.can_delete, false) as can_delete'),
|
||||
DB::raw('COALESCE(mgm.can_approve, false) as can_approve'),
|
||||
DB::raw('COALESCE(mgm.can_export, false) as can_export')
|
||||
)
|
||||
->orderBy('m.sort_order')
|
||||
->orderBy('m.id')
|
||||
->get();
|
||||
|
||||
return $rows->map(function ($row) use ($allowedPermissions) {
|
||||
$available = $allowedPermissions[(int) $row->id];
|
||||
|
||||
return [
|
||||
'menu_id' => (int) $row->id,
|
||||
'parent_id' => $row->parent_id ? (int) $row->parent_id : null,
|
||||
'name' => $row->name,
|
||||
'sort_order' => (int) $row->sort_order,
|
||||
'permissions' => [
|
||||
'can_view' => (bool) $row->can_view,
|
||||
'can_create' => (bool) $row->can_create,
|
||||
'can_update' => (bool) $row->can_update,
|
||||
'can_delete' => (bool) $row->can_delete,
|
||||
'can_approve' => (bool) $row->can_approve,
|
||||
'can_export' => (bool) $row->can_export,
|
||||
],
|
||||
'available_permissions' => $available,
|
||||
];
|
||||
})->all();
|
||||
}
|
||||
|
||||
public function getAllowedMenuIdsForUser(User $user, ?int $tenantId = null): array
|
||||
{
|
||||
if ($user->isMasterAdmin()) {
|
||||
return DB::table('menu_lists')
|
||||
->where('is_active', true)
|
||||
->pluck('id')
|
||||
->map(fn ($id) => (int) $id)
|
||||
->all();
|
||||
}
|
||||
|
||||
return DB::table('menu_group_menus as mgm')
|
||||
->join('user_menu_groups as umg', 'umg.menu_group_id', '=', 'mgm.menu_group_id')
|
||||
->join('menu_lists as m', 'm.id', '=', 'mgm.menu_id')
|
||||
->where('umg.user_id', $user->id)
|
||||
->where('umg.tenant_id', $tenantId)
|
||||
->where('m.is_active', true)
|
||||
->where(function ($q) {
|
||||
$q->where('mgm.can_view', true)
|
||||
->orWhere('mgm.can_create', true)
|
||||
->orWhere('mgm.can_update', true)
|
||||
->orWhere('mgm.can_delete', true)
|
||||
->orWhere('mgm.can_approve', true)
|
||||
->orWhere('mgm.can_export', true);
|
||||
})
|
||||
->distinct()
|
||||
->pluck('mgm.menu_id')
|
||||
->map(fn ($id) => (int) $id)
|
||||
->all();
|
||||
}
|
||||
|
||||
public function getAllowedMenuPermissionsForUser(User $user, ?int $tenantId): array
|
||||
{
|
||||
return DB::table('menu_group_menus as mgm')
|
||||
->join('user_menu_groups as umg', 'umg.menu_group_id', '=', 'mgm.menu_group_id')
|
||||
->join('menu_groups as mg', 'mg.id', '=', 'mgm.menu_group_id')
|
||||
->join('menu_lists as m', 'm.id', '=', 'mgm.menu_id')
|
||||
->where('umg.user_id', $user->id)
|
||||
->where('umg.tenant_id', $tenantId)
|
||||
->where('mg.is_active', true)
|
||||
->where('m.is_active', true)
|
||||
->groupBy('mgm.menu_id')
|
||||
->select(
|
||||
'mgm.menu_id',
|
||||
DB::raw('MAX(CASE WHEN mgm.can_view THEN 1 ELSE 0 END) as can_view'),
|
||||
DB::raw('MAX(CASE WHEN mgm.can_create THEN 1 ELSE 0 END) as can_create'),
|
||||
DB::raw('MAX(CASE WHEN mgm.can_update THEN 1 ELSE 0 END) as can_update'),
|
||||
DB::raw('MAX(CASE WHEN mgm.can_delete THEN 1 ELSE 0 END) as can_delete'),
|
||||
DB::raw('MAX(CASE WHEN mgm.can_approve THEN 1 ELSE 0 END) as can_approve'),
|
||||
DB::raw('MAX(CASE WHEN mgm.can_export THEN 1 ELSE 0 END) as can_export')
|
||||
)
|
||||
->get()
|
||||
->mapWithKeys(fn ($row) => [(int) $row->menu_id => [
|
||||
'can_view' => (bool) $row->can_view,
|
||||
'can_create' => (bool) $row->can_create,
|
||||
'can_update' => (bool) $row->can_update,
|
||||
'can_delete' => (bool) $row->can_delete,
|
||||
'can_approve' => (bool) $row->can_approve,
|
||||
'can_export' => (bool) $row->can_export,
|
||||
]])
|
||||
->all();
|
||||
}
|
||||
|
||||
public function listUserAssignments(User $user, ?int $tenantId = null): array
|
||||
{
|
||||
return DB::table('user_menu_groups as umg')
|
||||
->join('menu_groups as mg', 'mg.id', '=', 'umg.menu_group_id')
|
||||
->leftJoin('tenants as t', 't.id', '=', 'umg.tenant_id')
|
||||
->where('umg.user_id', $user->id)
|
||||
->when($tenantId, fn ($q) => $q->where('umg.tenant_id', $tenantId))
|
||||
->select(
|
||||
'umg.menu_group_id',
|
||||
'mg.name as menu_group_name',
|
||||
'mg.is_active as menu_group_is_active',
|
||||
'umg.tenant_id',
|
||||
't.tenant_name as tenant_name',
|
||||
'umg.created_at',
|
||||
'umg.updated_at'
|
||||
)
|
||||
->orderBy('umg.menu_group_id')
|
||||
->orderBy('umg.tenant_id')
|
||||
->get()
|
||||
->map(function ($row) {
|
||||
return [
|
||||
'menu_group_id' => (int) $row->menu_group_id,
|
||||
'menu_group_name' => $row->menu_group_name,
|
||||
'menu_group_is_active' => (bool) $row->menu_group_is_active,
|
||||
'tenant_id' => $row->tenant_id ? (int) $row->tenant_id : null,
|
||||
'tenant_name' => $row->tenant_name,
|
||||
'created_at' => $row->created_at,
|
||||
'updated_at' => $row->updated_at,
|
||||
];
|
||||
})
|
||||
->all();
|
||||
}
|
||||
|
||||
public function syncUserAssignments(User $user, array $assignments, ?int $tenantId = null): void
|
||||
{
|
||||
$rows = [];
|
||||
|
||||
foreach ($assignments as $assignment) {
|
||||
if ($tenantId && (int) ($assignment['tenant_id'] ?? 0) !== $tenantId) {
|
||||
throw ValidationException::withMessages([
|
||||
'assignments' => 'Assignment hanya boleh dibuat pada tenant aktif.',
|
||||
]);
|
||||
}
|
||||
|
||||
$group = MenuGroup::find($assignment['menu_group_id']);
|
||||
if ($tenantId && $group && (int) $group->tenant_id !== $tenantId) {
|
||||
throw ValidationException::withMessages([
|
||||
'assignments' => 'Menu group bukan milik tenant aktif.',
|
||||
]);
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'user_id' => $user->id,
|
||||
'menu_group_id' => (int) $assignment['menu_group_id'],
|
||||
'tenant_id' => $assignment['tenant_id'] ?? null,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($user, $rows) {
|
||||
DB::table('user_menu_groups')
|
||||
->where('user_id', $user->id)
|
||||
->when($tenantId, fn ($q) => $q->where('tenant_id', $tenantId))
|
||||
->delete();
|
||||
|
||||
if (! empty($rows)) {
|
||||
DB::table('user_menu_groups')->insert($rows);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Menu;
|
||||
|
||||
use App\Models\MenuList;
|
||||
|
||||
class MenuListService
|
||||
{
|
||||
public function list(array $filters = [])
|
||||
{
|
||||
return MenuList::query()
|
||||
->when(
|
||||
!empty($filters['search']),
|
||||
fn($q) => $q->where(function ($x) use ($filters) {
|
||||
$x->where('name', 'ILIKE', "%{$filters['search']}%")
|
||||
->orWhere('slug', 'ILIKE', "%{$filters['search']}%")
|
||||
->orWhere('url', 'ILIKE', "%{$filters['search']}%");
|
||||
})
|
||||
)
|
||||
->when(
|
||||
isset($filters['is_active']) && $filters['is_active'] !== '',
|
||||
fn($q) => $q->where(
|
||||
'is_active',
|
||||
filter_var($filters['is_active'], FILTER_VALIDATE_BOOLEAN)
|
||||
)
|
||||
)
|
||||
->latest('id')
|
||||
->paginate($filters['per_page'] ?? 10);
|
||||
}
|
||||
|
||||
public function create(array $data): MenuList
|
||||
{
|
||||
return MenuList::create($data);
|
||||
}
|
||||
|
||||
public function update(MenuList $menuList, array $data): MenuList
|
||||
{
|
||||
$menuList->update($data);
|
||||
|
||||
return $menuList->fresh();
|
||||
}
|
||||
|
||||
public function delete(MenuList $menuList): bool
|
||||
{
|
||||
return $menuList->delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Nas;
|
||||
|
||||
use App\Models\NasMikrotik;
|
||||
use App\Models\NasOlt;
|
||||
use App\Models\NasPackageProfile;
|
||||
use App\Models\NasWebfigDevice;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class NasResourceService
|
||||
{
|
||||
private const MODELS = [
|
||||
'mikrotik' => NasMikrotik::class,
|
||||
'package-profile' => NasPackageProfile::class,
|
||||
'olt' => NasOlt::class,
|
||||
'webfig' => NasWebfigDevice::class,
|
||||
];
|
||||
|
||||
private const SECRET_FIELDS = [
|
||||
'mikrotik' => ['api_password', 'radius_secret'],
|
||||
'olt' => ['community', 'auth_password', 'privacy_password'],
|
||||
'webfig' => ['password'],
|
||||
'package-profile' => [],
|
||||
];
|
||||
|
||||
public function list(string $resource, array $filters, User $actor, ?int $tenantId)
|
||||
{
|
||||
$model = $this->model($resource);
|
||||
|
||||
return $model::query()
|
||||
->with('tenant:id,tenant_code,tenant_name')
|
||||
->when(! $actor->isMasterAdmin(), fn ($q) => $q->where('tenant_id', $tenantId))
|
||||
->when($actor->isMasterAdmin() && $tenantId, fn ($q) => $q->where('tenant_id', $tenantId))
|
||||
->when(! empty($filters['status']), fn ($q) => $q->where('status', $filters['status']))
|
||||
->when(! empty($filters['connection_type']) && $resource === 'mikrotik', fn ($q) => $q->where('connection_type', $filters['connection_type']))
|
||||
->when(! empty($filters['service_type']) && $resource === 'package-profile', fn ($q) => $q->where('service_type', $filters['service_type']))
|
||||
->when(! empty($filters['search']), function ($q) use ($filters, $resource) {
|
||||
$columns = match ($resource) {
|
||||
'mikrotik' => ['name', 'host', 'api_username'],
|
||||
'package-profile' => ['name', 'external_profile_name'],
|
||||
'olt' => ['name', 'host', 'vendor', 'model'],
|
||||
'webfig' => ['name', 'url', 'device_type'],
|
||||
};
|
||||
$q->where(function ($search) use ($columns, $filters) {
|
||||
foreach ($columns as $index => $column) {
|
||||
$method = $index === 0 ? 'where' : 'orWhere';
|
||||
$search->{$method}($column, 'ILIKE', "%{$filters['search']}%");
|
||||
}
|
||||
});
|
||||
})
|
||||
->orderBy($filters['sort_by'] ?? 'id', $filters['sort_direction'] ?? 'desc')
|
||||
->paginate($filters['per_page'] ?? 10);
|
||||
}
|
||||
|
||||
public function find(string $resource, int $id): Model
|
||||
{
|
||||
$model = $this->model($resource);
|
||||
|
||||
return $model::with('tenant:id,tenant_code,tenant_name')->findOrFail($id);
|
||||
}
|
||||
|
||||
public function create(string $resource, array $data, User $actor, ?int $tenantId): Model
|
||||
{
|
||||
$data['tenant_id'] = $actor->isMasterAdmin() ? ($data['tenant_id'] ?? $tenantId) : $tenantId;
|
||||
if (! $data['tenant_id']) {
|
||||
throw ValidationException::withMessages(['tenant_id' => 'Tenant wajib dipilih.']);
|
||||
}
|
||||
$data['status'] ??= 'active';
|
||||
$model = $this->model($resource);
|
||||
|
||||
return $model::create($data)->load('tenant:id,tenant_code,tenant_name');
|
||||
}
|
||||
|
||||
public function update(string $resource, Model $model, array $data, User $actor): Model
|
||||
{
|
||||
if (! $actor->isMasterAdmin()) {
|
||||
unset($data['tenant_id']);
|
||||
}
|
||||
foreach (self::SECRET_FIELDS[$resource] as $field) {
|
||||
if (array_key_exists($field, $data) && blank($data[$field])) {
|
||||
unset($data[$field]);
|
||||
}
|
||||
}
|
||||
$model->update($data);
|
||||
|
||||
return $model->fresh()->load('tenant:id,tenant_code,tenant_name');
|
||||
}
|
||||
|
||||
public function delete(Model $model): bool
|
||||
{
|
||||
return $model->delete();
|
||||
}
|
||||
|
||||
private function model(string $resource): string
|
||||
{
|
||||
abort_unless(isset(self::MODELS[$resource]), 404);
|
||||
|
||||
return self::MODELS[$resource];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Tenant;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
|
||||
class TenantService
|
||||
{
|
||||
public function list(array $filters = [], ?User $actor = null)
|
||||
{
|
||||
return Tenant::query()
|
||||
->when(
|
||||
$actor && ! $actor->isMasterAdmin(),
|
||||
fn ($q) => $q->whereIn(
|
||||
'id',
|
||||
$actor->userTenants()->select('tenant_id')
|
||||
)
|
||||
)
|
||||
->when(
|
||||
! empty($filters['search']),
|
||||
fn ($q) => $q->where(function ($x) use ($filters) {
|
||||
$x->where('tenant_code', 'ILIKE', "%{$filters['search']}%")
|
||||
->orWhere('tenant_name', 'ILIKE', "%{$filters['search']}%")
|
||||
->orWhere('email', 'ILIKE', "%{$filters['search']}%");
|
||||
})
|
||||
)
|
||||
->when(
|
||||
! empty($filters['status']),
|
||||
fn ($q) => $q->where('status', $filters['status'])
|
||||
)
|
||||
->orderBy(
|
||||
$filters['sort_by'] ?? 'id',
|
||||
$filters['sort_direction'] ?? 'desc'
|
||||
)
|
||||
->paginate($filters['per_page'] ?? 10);
|
||||
}
|
||||
|
||||
public function create(array $data): Tenant
|
||||
{
|
||||
if (! array_key_exists('status', $data) || empty($data['status'])) {
|
||||
$data['status'] = 'active';
|
||||
}
|
||||
|
||||
return Tenant::create($data);
|
||||
}
|
||||
|
||||
public function update(Tenant $tenant, array $data): Tenant
|
||||
{
|
||||
$tenant->update($data);
|
||||
|
||||
return $tenant->fresh();
|
||||
}
|
||||
|
||||
public function delete(Tenant $tenant): bool
|
||||
{
|
||||
return $tenant->delete();
|
||||
}
|
||||
}
|
||||
@@ -6,16 +6,29 @@ use App\Models\TicketIncidentType;
|
||||
|
||||
class TicketIncidentTypeService
|
||||
{
|
||||
public function getAll()
|
||||
public function list(array $filters = [])
|
||||
{
|
||||
return TicketIncidentType::orderBy('name')->get();
|
||||
return TicketIncidentType::query()
|
||||
->when(
|
||||
! empty($filters['search']),
|
||||
fn ($query) => $query->where('name', 'ILIKE', "%{$filters['search']}%")
|
||||
)
|
||||
->when(
|
||||
array_key_exists('is_active', $filters) && $filters['is_active'] !== '',
|
||||
fn ($query) => $query->where(
|
||||
'is_active',
|
||||
filter_var($filters['is_active'], FILTER_VALIDATE_BOOLEAN)
|
||||
)
|
||||
)
|
||||
->orderBy('name')
|
||||
->paginate($filters['per_page'] ?? 10);
|
||||
}
|
||||
|
||||
public function create(array $data): TicketIncidentType
|
||||
{
|
||||
return TicketIncidentType::create([
|
||||
'name' => $data['name'],
|
||||
'is_active' => $data['is_active'] ?? true
|
||||
'is_active' => $data['is_active'] ?? true,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -25,7 +38,7 @@ class TicketIncidentTypeService
|
||||
): TicketIncidentType {
|
||||
$ticketIncidentType->update([
|
||||
'name' => $data['name'],
|
||||
'is_active' => $data['is_active']
|
||||
'is_active' => $data['is_active'],
|
||||
]);
|
||||
|
||||
return $ticketIncidentType->fresh();
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\User;
|
||||
|
||||
use App\Enums\UserAccessLevel;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class UserService
|
||||
{
|
||||
public function list(array $filters = [], ?User $actor = null, ?int $tenantId = null)
|
||||
{
|
||||
return User::query()
|
||||
->with(['profilePhoto', 'userProfile', 'userTenants.tenant', 'userMenuGroups.menuGroup', 'userMenuGroups.tenant'])
|
||||
->when(
|
||||
! empty($filters['search']),
|
||||
fn ($q) => $q->where(function ($x) use ($filters) {
|
||||
$x->where('name', 'ILIKE', "%{$filters['search']}%")
|
||||
->orWhere('username', 'ILIKE', "%{$filters['search']}%")
|
||||
->orWhere('email', 'ILIKE', "%{$filters['search']}%")
|
||||
->orWhere('whatsapp_number', 'ILIKE', "%{$filters['search']}%");
|
||||
})
|
||||
)
|
||||
->when(
|
||||
isset($filters['status']) && $filters['status'] !== '',
|
||||
fn ($q) => $q->where('status', $filters['status'])
|
||||
)
|
||||
->when(
|
||||
! empty($filters['access_level']),
|
||||
fn ($q) => $q->where('access_level', $filters['access_level'])
|
||||
)
|
||||
->when(
|
||||
$actor && ! $actor->isMasterAdmin(),
|
||||
fn ($q) => $q->whereHas(
|
||||
'userTenants',
|
||||
fn ($tenantQuery) => $tenantQuery->where('tenant_id', $tenantId)
|
||||
)->where('is_master', false)
|
||||
)
|
||||
->orderBy(
|
||||
$filters['sort_by'] ?? 'id',
|
||||
$filters['sort_direction'] ?? 'desc'
|
||||
)
|
||||
->paginate($filters['per_page'] ?? 10);
|
||||
}
|
||||
|
||||
public function create(array $payload, ?User $actor = null): User
|
||||
{
|
||||
$this->guardPrivilegeChange($payload, $actor);
|
||||
$this->guardTenantPayload($payload, $actor);
|
||||
|
||||
if ($actor && ! $actor->isMasterAdmin() && empty($payload['user_tenants'])) {
|
||||
throw ValidationException::withMessages([
|
||||
'user_tenants' => 'Tenant Owner wajib membuat user pada tenant aktif.',
|
||||
]);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($payload) {
|
||||
$baseData = Arr::only($payload, [
|
||||
'name',
|
||||
'username',
|
||||
'email',
|
||||
'whatsapp_number',
|
||||
'profile_photo_file_id',
|
||||
'password',
|
||||
'is_master',
|
||||
'access_level',
|
||||
'status',
|
||||
]);
|
||||
|
||||
$user = User::create($baseData);
|
||||
|
||||
$this->syncProfile($user, $payload['user_profile'] ?? null);
|
||||
$this->syncTenants($user, $payload['user_tenants'] ?? []);
|
||||
$this->syncMenuGroups($user, $payload['user_menu_groups'] ?? []);
|
||||
|
||||
return $user->fresh([
|
||||
'userProfile',
|
||||
'profilePhoto',
|
||||
'userTenants.tenant',
|
||||
'userMenuGroups.menuGroup',
|
||||
'userMenuGroups.tenant',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function show(User $user): User
|
||||
{
|
||||
return $user->load([
|
||||
'userProfile',
|
||||
'profilePhoto',
|
||||
'userTenants.tenant',
|
||||
'userMenuGroups.menuGroup',
|
||||
'userMenuGroups.tenant',
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(User $user, array $payload, ?User $actor = null): User
|
||||
{
|
||||
$this->guardPrivilegeChange($payload, $actor, $user);
|
||||
$this->guardTenantPayload($payload, $actor);
|
||||
|
||||
return DB::transaction(function () use ($user, $payload) {
|
||||
$baseData = Arr::only($payload, [
|
||||
'name',
|
||||
'username',
|
||||
'email',
|
||||
'whatsapp_number',
|
||||
'profile_photo_file_id',
|
||||
'is_master',
|
||||
'access_level',
|
||||
'status',
|
||||
]);
|
||||
|
||||
if (! empty($payload['password'])) {
|
||||
$baseData['password'] = $payload['password'];
|
||||
}
|
||||
|
||||
if (! empty($baseData)) {
|
||||
$user->update($baseData);
|
||||
}
|
||||
|
||||
if (array_key_exists('user_profile', $payload)) {
|
||||
$this->syncProfile($user, $payload['user_profile']);
|
||||
}
|
||||
|
||||
if (array_key_exists('user_tenants', $payload)) {
|
||||
$this->syncTenants($user, $payload['user_tenants'] ?? []);
|
||||
}
|
||||
|
||||
if (array_key_exists('user_menu_groups', $payload)) {
|
||||
$this->syncMenuGroups($user, $payload['user_menu_groups'] ?? []);
|
||||
}
|
||||
|
||||
return $user->fresh([
|
||||
'userProfile',
|
||||
'profilePhoto',
|
||||
'userTenants.tenant',
|
||||
'userMenuGroups.menuGroup',
|
||||
'userMenuGroups.tenant',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function delete(User $user): void
|
||||
{
|
||||
DB::transaction(function () use ($user) {
|
||||
$user->userProfile()->delete();
|
||||
$user->userTenants()->delete();
|
||||
$user->userMenuGroups()->delete();
|
||||
$user->delete();
|
||||
});
|
||||
}
|
||||
|
||||
protected function syncProfile(User $user, ?array $profile): void
|
||||
{
|
||||
if ($profile === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = Arr::only($profile, [
|
||||
'nik',
|
||||
'address',
|
||||
'provinsi_id',
|
||||
'kabupaten_id',
|
||||
'kecamatan_id',
|
||||
'desa_id',
|
||||
]);
|
||||
|
||||
$user->userProfile()->updateOrCreate(
|
||||
['user_id' => $user->id],
|
||||
$data
|
||||
);
|
||||
}
|
||||
|
||||
protected function syncTenants(User $user, array $rows): void
|
||||
{
|
||||
$normalized = collect($rows)
|
||||
->map(fn ($row) => [
|
||||
'tenant_id' => $row['tenant_id'] ?? null,
|
||||
'is_default' => (bool) ($row['is_default'] ?? false),
|
||||
])
|
||||
->filter(fn ($row) => ! empty($row['tenant_id']))
|
||||
->unique('tenant_id')
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$defaultSet = false;
|
||||
foreach ($normalized as $index => $row) {
|
||||
if ($row['is_default'] && ! $defaultSet) {
|
||||
$defaultSet = true;
|
||||
} else {
|
||||
$normalized[$index]['is_default'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $defaultSet && count($normalized) > 0) {
|
||||
$normalized[0]['is_default'] = true;
|
||||
}
|
||||
|
||||
$user->userTenants()->delete();
|
||||
|
||||
if (! empty($normalized)) {
|
||||
$user->userTenants()->createMany($normalized);
|
||||
}
|
||||
}
|
||||
|
||||
protected function syncMenuGroups(User $user, array $rows): void
|
||||
{
|
||||
$normalized = collect($rows)
|
||||
->map(fn ($row) => [
|
||||
'menu_group_id' => $row['menu_group_id'] ?? null,
|
||||
'tenant_id' => $row['tenant_id'] ?? null,
|
||||
])
|
||||
->filter(fn ($row) => ! empty($row['menu_group_id']))
|
||||
->unique(fn ($row) => ($row['menu_group_id'] ?? '').'|'.($row['tenant_id'] ?? ''))
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$user->userMenuGroups()->delete();
|
||||
|
||||
if (! empty($normalized)) {
|
||||
$user->userMenuGroups()->createMany($normalized);
|
||||
}
|
||||
}
|
||||
|
||||
protected function guardPrivilegeChange(array &$payload, ?User $actor, ?User $target = null): void
|
||||
{
|
||||
if (! $actor) {
|
||||
unset($payload['is_master'], $payload['access_level']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($actor->isMasterAdmin()) {
|
||||
if (($payload['access_level'] ?? null) === UserAccessLevel::MASTER_ADMIN->value) {
|
||||
$payload['is_master'] = true;
|
||||
} elseif (array_key_exists('access_level', $payload)) {
|
||||
$payload['is_master'] = false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($target?->isMasterAdmin()) {
|
||||
throw ValidationException::withMessages([
|
||||
'user' => 'Master Admin hanya dapat diubah oleh Master Admin.',
|
||||
]);
|
||||
}
|
||||
|
||||
if (($payload['is_master'] ?? false)
|
||||
|| ($payload['access_level'] ?? null) === UserAccessLevel::MASTER_ADMIN->value
|
||||
|| ($payload['access_level'] ?? null) === UserAccessLevel::TENANT_OWNER->value) {
|
||||
throw ValidationException::withMessages([
|
||||
'access_level' => 'Anda tidak dapat memberikan level akses ini.',
|
||||
]);
|
||||
}
|
||||
|
||||
unset($payload['is_master']);
|
||||
}
|
||||
|
||||
protected function guardTenantPayload(array $payload, ?User $actor): void
|
||||
{
|
||||
if (! $actor || $actor->isMasterAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tenantId = request()->attributes->get('tenant_id');
|
||||
|
||||
foreach (['user_tenants', 'user_menu_groups'] as $key) {
|
||||
foreach ($payload[$key] ?? [] as $row) {
|
||||
if ((int) ($row['tenant_id'] ?? 0) !== (int) $tenantId) {
|
||||
throw ValidationException::withMessages([
|
||||
$key => 'Assignment hanya boleh dibuat untuk tenant yang sedang aktif.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Wilayah;
|
||||
|
||||
use App\Models\WilayahDesa;
|
||||
use App\Models\WilayahKabupaten;
|
||||
use App\Models\WilayahKecamatan;
|
||||
use App\Models\WilayahProvinsi;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class WilayahService
|
||||
{
|
||||
private const MODELS = [
|
||||
'provinsi' => WilayahProvinsi::class,
|
||||
'kabupaten' => WilayahKabupaten::class,
|
||||
'kecamatan' => WilayahKecamatan::class,
|
||||
'desa' => WilayahDesa::class,
|
||||
];
|
||||
|
||||
private const PARENTS = [
|
||||
'kabupaten' => 'provinsi_id',
|
||||
'kecamatan' => 'kabupaten_id',
|
||||
'desa' => 'kecamatan_id',
|
||||
];
|
||||
|
||||
private const RELATIONS = [
|
||||
'provinsi' => [],
|
||||
'kabupaten' => ['provinsi'],
|
||||
'kecamatan' => ['kabupaten.provinsi'],
|
||||
'desa' => ['kecamatan.kabupaten.provinsi'],
|
||||
];
|
||||
|
||||
public function list(string $tingkat, array $filters = [])
|
||||
{
|
||||
$model = $this->model($tingkat);
|
||||
|
||||
return $model::query()
|
||||
->with(self::RELATIONS[$tingkat])
|
||||
->when($filters['search'] ?? null, function ($query, $search) {
|
||||
$value = mb_strtolower($search);
|
||||
$query->where(function ($scope) use ($value) {
|
||||
$scope->whereRaw('LOWER(kode) LIKE ?', ["%{$value}%"])
|
||||
->orWhereRaw('LOWER(nama) LIKE ?', ["%{$value}%"]);
|
||||
});
|
||||
})
|
||||
->when(
|
||||
isset(self::PARENTS[$tingkat]) && ! empty($filters[self::PARENTS[$tingkat]]),
|
||||
fn ($query) => $query->where(
|
||||
self::PARENTS[$tingkat],
|
||||
$filters[self::PARENTS[$tingkat]]
|
||||
)
|
||||
)
|
||||
->orderBy(
|
||||
$filters['sort_by'] ?? 'nama',
|
||||
$filters['sort_direction'] ?? 'asc'
|
||||
)
|
||||
->paginate($filters['per_page'] ?? 10);
|
||||
}
|
||||
|
||||
public function options(string $tingkat, array $filters = [], ?int $parentId = null)
|
||||
{
|
||||
$model = $this->model($tingkat);
|
||||
$parentColumn = self::PARENTS[$tingkat] ?? null;
|
||||
|
||||
return $model::query()
|
||||
->select(['id', 'kode', 'nama'])
|
||||
->when(
|
||||
$parentColumn && $parentId,
|
||||
fn ($query) => $query->where($parentColumn, $parentId)
|
||||
)
|
||||
->when($filters['search'] ?? null, function ($query, $search) {
|
||||
$value = mb_strtolower($search);
|
||||
$query->where(function ($scope) use ($value) {
|
||||
$scope->whereRaw('LOWER(kode) LIKE ?', ["%{$value}%"])
|
||||
->orWhereRaw('LOWER(nama) LIKE ?', ["%{$value}%"]);
|
||||
});
|
||||
})
|
||||
->orderBy('nama')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function create(string $tingkat, array $data): Model
|
||||
{
|
||||
$model = $this->model($tingkat);
|
||||
|
||||
return $model::create($this->payload($tingkat, $data))
|
||||
->load(self::RELATIONS[$tingkat]);
|
||||
}
|
||||
|
||||
public function find(string $tingkat, int $id): Model
|
||||
{
|
||||
$model = $this->model($tingkat);
|
||||
|
||||
return $model::with(self::RELATIONS[$tingkat])->findOrFail($id);
|
||||
}
|
||||
|
||||
public function update(string $tingkat, int $id, array $data): Model
|
||||
{
|
||||
$wilayah = $this->find($tingkat, $id);
|
||||
$wilayah->update($this->payload($tingkat, $data));
|
||||
|
||||
return $wilayah->fresh(self::RELATIONS[$tingkat]);
|
||||
}
|
||||
|
||||
public function delete(string $tingkat, int $id): void
|
||||
{
|
||||
$wilayah = $this->find($tingkat, $id);
|
||||
$childRelation = match ($tingkat) {
|
||||
'provinsi' => 'kabupaten',
|
||||
'kabupaten' => 'kecamatan',
|
||||
'kecamatan' => 'desa',
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($childRelation && $wilayah->{$childRelation}()->exists()) {
|
||||
throw ValidationException::withMessages([
|
||||
'wilayah' => "Wilayah {$tingkat} tidak dapat dihapus karena masih memiliki data di bawahnya.",
|
||||
]);
|
||||
}
|
||||
|
||||
$wilayah->delete();
|
||||
}
|
||||
|
||||
public function alamatLengkap(array $filters): array
|
||||
{
|
||||
if (! empty($filters['desa_id'])) {
|
||||
$desa = WilayahDesa::with('kecamatan.kabupaten.provinsi')->findOrFail($filters['desa_id']);
|
||||
|
||||
return $this->addressPayload(
|
||||
$desa->kecamatan?->kabupaten?->provinsi,
|
||||
$desa->kecamatan?->kabupaten,
|
||||
$desa->kecamatan,
|
||||
$desa
|
||||
);
|
||||
}
|
||||
|
||||
if (! empty($filters['kecamatan_id'])) {
|
||||
$kecamatan = WilayahKecamatan::with('kabupaten.provinsi')->findOrFail($filters['kecamatan_id']);
|
||||
|
||||
return $this->addressPayload(
|
||||
$kecamatan->kabupaten?->provinsi,
|
||||
$kecamatan->kabupaten,
|
||||
$kecamatan
|
||||
);
|
||||
}
|
||||
|
||||
if (! empty($filters['kabupaten_id'])) {
|
||||
$kabupaten = WilayahKabupaten::with('provinsi')->findOrFail($filters['kabupaten_id']);
|
||||
|
||||
return $this->addressPayload($kabupaten->provinsi, $kabupaten);
|
||||
}
|
||||
|
||||
$provinsi = WilayahProvinsi::findOrFail($filters['provinsi_id']);
|
||||
|
||||
return $this->addressPayload($provinsi);
|
||||
}
|
||||
|
||||
private function model(string $tingkat): string
|
||||
{
|
||||
abort_unless(isset(self::MODELS[$tingkat]), 404, 'Tingkat wilayah tidak ditemukan.');
|
||||
|
||||
return self::MODELS[$tingkat];
|
||||
}
|
||||
|
||||
private function payload(string $tingkat, array $data): array
|
||||
{
|
||||
$fields = ['kode', 'nama'];
|
||||
if (isset(self::PARENTS[$tingkat])) {
|
||||
$fields[] = self::PARENTS[$tingkat];
|
||||
}
|
||||
|
||||
return collect($data)->only($fields)->all();
|
||||
}
|
||||
|
||||
private function addressPayload(
|
||||
?WilayahProvinsi $provinsi,
|
||||
?WilayahKabupaten $kabupaten = null,
|
||||
?WilayahKecamatan $kecamatan = null,
|
||||
?WilayahDesa $desa = null
|
||||
): array {
|
||||
$node = fn ($item) => $item ? [
|
||||
'id' => $item->id,
|
||||
'kode' => $item->kode,
|
||||
'nama' => $item->nama,
|
||||
] : null;
|
||||
|
||||
$parts = array_filter([
|
||||
$desa ? "Desa/Kelurahan {$desa->nama}" : null,
|
||||
$kecamatan ? "Kecamatan {$kecamatan->nama}" : null,
|
||||
$kabupaten?->nama,
|
||||
$provinsi ? $provinsi->nama : null,
|
||||
]);
|
||||
|
||||
return [
|
||||
'desa' => $node($desa),
|
||||
'kecamatan' => $node($kecamatan),
|
||||
'kabupaten' => $node($kabupaten),
|
||||
'provinsi' => $node($provinsi),
|
||||
'alamat_wilayah' => implode(', ', $parts),
|
||||
];
|
||||
}
|
||||
}
|
||||
+11
-40
@@ -3,7 +3,6 @@
|
||||
namespace App\Traits;
|
||||
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
trait ApiResponse
|
||||
{
|
||||
@@ -13,7 +12,7 @@ trait ApiResponse
|
||||
'success' => true,
|
||||
'code' => $code,
|
||||
'message' => $message,
|
||||
'data' => $data
|
||||
'data' => $data,
|
||||
], $code);
|
||||
}
|
||||
|
||||
@@ -23,7 +22,7 @@ trait ApiResponse
|
||||
'success' => true,
|
||||
'code' => 201,
|
||||
'message' => $message,
|
||||
'data' => $data
|
||||
'data' => $data,
|
||||
], 201);
|
||||
}
|
||||
|
||||
@@ -33,7 +32,7 @@ trait ApiResponse
|
||||
'success' => true,
|
||||
'code' => 200,
|
||||
'message' => $message,
|
||||
'data' => $data
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -42,7 +41,7 @@ trait ApiResponse
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'code' => 200,
|
||||
'message' => $message
|
||||
'message' => $message,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -51,7 +50,7 @@ trait ApiResponse
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'code' => $code,
|
||||
'message' => $message
|
||||
'message' => $message,
|
||||
], $code);
|
||||
}
|
||||
|
||||
@@ -61,7 +60,7 @@ trait ApiResponse
|
||||
'success' => false,
|
||||
'code' => 422,
|
||||
'message' => 'Validation Error',
|
||||
'errors' => $errors
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
|
||||
@@ -70,7 +69,7 @@ trait ApiResponse
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'code' => 404,
|
||||
'message' => $message
|
||||
'message' => $message,
|
||||
], 404);
|
||||
}
|
||||
|
||||
@@ -80,38 +79,10 @@ trait ApiResponse
|
||||
string $message = 'Success',
|
||||
int $code = 200
|
||||
) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'code' => $code,
|
||||
'message' => $message,
|
||||
$data->through(
|
||||
fn ($item) => (new $resource($item))->resolve(request())
|
||||
);
|
||||
|
||||
'data' => $resource::collection($data->items()),
|
||||
|
||||
'pagination' => [
|
||||
'current_page' => $data->currentPage(),
|
||||
'per_page' => $data->perPage(),
|
||||
'from' => $data->firstItem(),
|
||||
'to' => $data->lastItem(),
|
||||
'last_page' => $data->lastPage(),
|
||||
'total' => $data->total(),
|
||||
|
||||
'has_more' => $data->hasMorePages(),
|
||||
'count' => count($data->items()),
|
||||
|
||||
'next_page' => $data->nextPageUrl(),
|
||||
'prev_page' => $data->previousPageUrl(),
|
||||
'first_page' => $data->url(1),
|
||||
'last_page_url'=> $data->url($data->lastPage()),
|
||||
],
|
||||
|
||||
'filters' => [
|
||||
'page' => (int) request()->input('page', 1),
|
||||
'per_page' => (int) request()->input('per_page', 15),
|
||||
'search' => request()->input('search'),
|
||||
'sort_by' => request()->input('sort_by'),
|
||||
'sort_order' => request()->input('sort_order', 'desc'),
|
||||
'type' => request()->input('type'),
|
||||
]
|
||||
], $code);
|
||||
return $this->success($data, $message, $code);
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -1,5 +1,9 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\EnsureAccessLevel;
|
||||
use App\Http\Middleware\EnsureMenuPermission;
|
||||
use App\Http\Middleware\EnsureMenuResourcePermission;
|
||||
use App\Http\Middleware\ResolveTenantContext;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
@@ -13,7 +17,12 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
)
|
||||
|
||||
->withMiddleware(function (Middleware $middleware) {
|
||||
//
|
||||
$middleware->alias([
|
||||
'access.level' => EnsureAccessLevel::class,
|
||||
'menu.permission' => EnsureMenuPermission::class,
|
||||
'menu.resource' => EnsureMenuResourcePermission::class,
|
||||
'tenant.context' => ResolveTenantContext::class,
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions) {
|
||||
//
|
||||
|
||||
+2
-1
@@ -9,7 +9,8 @@
|
||||
"php": "^8.2",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/sanctum": "^4.3",
|
||||
"laravel/tinker": "^2.10.1"
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"league/flysystem-aws-s3-v3": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dedoc/scramble": "^0.13.28",
|
||||
|
||||
Generated
+386
-44
@@ -4,8 +4,159 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "dc5dc467fbab2c5682c8e676f4c2ffb4",
|
||||
"content-hash": "43e4a419e3226169717b5475e08cf4d8",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-crt-php",
|
||||
"version": "v1.2.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/awslabs/aws-crt-php.git",
|
||||
"reference": "d71d9906c7bb63a28295447ba12e74723bd3730e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/d71d9906c7bb63a28295447ba12e74723bd3730e",
|
||||
"reference": "d71d9906c7bb63a28295447ba12e74723bd3730e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8.35||^5.6.3||^9.5",
|
||||
"yoast/phpunit-polyfills": "^1.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality."
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "AWS SDK Common Runtime Team",
|
||||
"email": "aws-sdk-common-runtime@amazon.com"
|
||||
}
|
||||
],
|
||||
"description": "AWS Common Runtime for PHP",
|
||||
"homepage": "https://github.com/awslabs/aws-crt-php",
|
||||
"keywords": [
|
||||
"amazon",
|
||||
"aws",
|
||||
"crt",
|
||||
"sdk"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/awslabs/aws-crt-php/issues",
|
||||
"source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7"
|
||||
},
|
||||
"time": "2024-10-18T22:15:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "aws/aws-sdk-php",
|
||||
"version": "3.389.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/aws/aws-sdk-php.git",
|
||||
"reference": "e6e6649e58826c7edaa9f546f444461a25a22719"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/e6e6649e58826c7edaa9f546f444461a25a22719",
|
||||
"reference": "e6e6649e58826c7edaa9f546f444461a25a22719",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"aws/aws-crt-php": "^1.2.3",
|
||||
"ext-json": "*",
|
||||
"ext-pcre": "*",
|
||||
"ext-simplexml": "*",
|
||||
"guzzlehttp/guzzle": "^7.8.2 || ^8.0",
|
||||
"guzzlehttp/promises": "^2.0.3 || ^3.0",
|
||||
"guzzlehttp/psr7": "^2.6.3 || ^3.0",
|
||||
"mtdowling/jmespath.php": "^2.9.1",
|
||||
"php": ">=8.1",
|
||||
"psr/http-message": "^1.0 || ^2.0",
|
||||
"symfony/filesystem": "^v5.4.45 || ^v6.4.3 || ^v7.1.0 || ^v8.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"andrewsville/php-token-reflection": "^1.4",
|
||||
"aws/aws-php-sns-message-validator": "~1.0",
|
||||
"behat/behat": "~3.0",
|
||||
"composer/composer": "^2.7.8",
|
||||
"dms/phpunit-arraysubset-asserts": "^v0.5.0",
|
||||
"doctrine/cache": "~1.4",
|
||||
"ext-dom": "*",
|
||||
"ext-openssl": "*",
|
||||
"ext-sockets": "*",
|
||||
"phpunit/phpunit": "^10.0",
|
||||
"psr/cache": "^2.0 || ^3.0",
|
||||
"psr/simple-cache": "^2.0 || ^3.0",
|
||||
"sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0",
|
||||
"yoast/phpunit-polyfills": "^2.0"
|
||||
},
|
||||
"suggest": {
|
||||
"aws/aws-php-sns-message-validator": "To validate incoming SNS notifications",
|
||||
"doctrine/cache": "To use the DoctrineCacheAdapter",
|
||||
"ext-curl": "To send requests using cURL",
|
||||
"ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages",
|
||||
"ext-pcntl": "To use client-side monitoring",
|
||||
"ext-sockets": "To use client-side monitoring"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Aws\\": "src/"
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"src/data/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Amazon Web Services",
|
||||
"homepage": "https://aws.amazon.com"
|
||||
}
|
||||
],
|
||||
"description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project",
|
||||
"homepage": "https://aws.amazon.com/sdk-for-php",
|
||||
"keywords": [
|
||||
"amazon",
|
||||
"aws",
|
||||
"cloud",
|
||||
"dynamodb",
|
||||
"ec2",
|
||||
"glacier",
|
||||
"s3",
|
||||
"sdk"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://github.com/aws/aws-sdk-php/discussions",
|
||||
"issues": "https://github.com/aws/aws-sdk-php/issues",
|
||||
"source": "https://github.com/aws/aws-sdk-php/tree/3.389.0"
|
||||
},
|
||||
"time": "2026-07-24T18:05:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brick/math",
|
||||
"version": "0.14.8",
|
||||
@@ -643,26 +794,26 @@
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/guzzle",
|
||||
"version": "7.11.2",
|
||||
"version": "7.15.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/guzzle/guzzle.git",
|
||||
"reference": "bf5f35ad4b774b9d7c5766c02035e865e7e3fdab"
|
||||
"reference": "61443dfb33c62f308ee8add20f45b4d6e4bf8d2f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/bf5f35ad4b774b9d7c5766c02035e865e7e3fdab",
|
||||
"reference": "bf5f35ad4b774b9d7c5766c02035e865e7e3fdab",
|
||||
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/61443dfb33c62f308ee8add20f45b4d6e4bf8d2f",
|
||||
"reference": "61443dfb33c62f308ee8add20f45b4d6e4bf8d2f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"guzzlehttp/promises": "^2.5",
|
||||
"guzzlehttp/psr7": "^2.11",
|
||||
"guzzlehttp/promises": "^2.5.1",
|
||||
"guzzlehttp/psr7": "^2.13",
|
||||
"php": "^7.2.5 || ^8.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"symfony/deprecation-contracts": "^2.5 || ^3.0",
|
||||
"symfony/polyfill-php80": "^1.24"
|
||||
"symfony/polyfill-php80": "^1.25"
|
||||
},
|
||||
"provide": {
|
||||
"psr/http-client-implementation": "1.0"
|
||||
@@ -670,8 +821,8 @@
|
||||
"require-dev": {
|
||||
"bamarni/composer-bin-plugin": "^1.8.2",
|
||||
"ext-curl": "*",
|
||||
"guzzle/client-integration-tests": "3.0.2",
|
||||
"guzzlehttp/test-server": "^0.5",
|
||||
"guzzle/client-integration-tests": "3.0.3",
|
||||
"guzzlehttp/test-server": "^0.7",
|
||||
"php-http/message-factory": "^1.1",
|
||||
"phpunit/phpunit": "^8.5.52 || ^9.6.34",
|
||||
"psr/log": "^1.1 || ^2.0 || ^3.0"
|
||||
@@ -751,7 +902,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/guzzle/guzzle/issues",
|
||||
"source": "https://github.com/guzzle/guzzle/tree/7.11.2"
|
||||
"source": "https://github.com/guzzle/guzzle/tree/7.15.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -767,20 +918,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-06-12T21:49:57+00:00"
|
||||
"time": "2026-07-18T11:23:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/promises",
|
||||
"version": "2.5.0",
|
||||
"version": "2.5.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/guzzle/promises.git",
|
||||
"reference": "4360e982f87f5f258bf872d094647791db2f4c8e"
|
||||
"reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/guzzle/promises/zipball/4360e982f87f5f258bf872d094647791db2f4c8e",
|
||||
"reference": "4360e982f87f5f258bf872d094647791db2f4c8e",
|
||||
"url": "https://api.github.com/repos/guzzle/promises/zipball/9ad1e4fc607446a055b95870c7f668e93b5cff29",
|
||||
"reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -835,7 +986,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/guzzle/promises/issues",
|
||||
"source": "https://github.com/guzzle/promises/tree/2.5.0"
|
||||
"source": "https://github.com/guzzle/promises/tree/2.5.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -851,20 +1002,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-06-02T12:23:43+00:00"
|
||||
"time": "2026-07-08T15:48:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/psr7",
|
||||
"version": "2.11.1",
|
||||
"version": "2.13.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/guzzle/psr7.git",
|
||||
"reference": "640e2897bbee822dbc8af761d49e1a29b1f2a6b1"
|
||||
"reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/guzzle/psr7/zipball/640e2897bbee822dbc8af761d49e1a29b1f2a6b1",
|
||||
"reference": "640e2897bbee822dbc8af761d49e1a29b1f2a6b1",
|
||||
"url": "https://api.github.com/repos/guzzle/psr7/zipball/dad89620b7a6edb60c15858442eb2e408b45d8f4",
|
||||
"reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -873,7 +1024,7 @@
|
||||
"psr/http-message": "^1.1 || ^2.0",
|
||||
"ralouphie/getallheaders": "^3.0",
|
||||
"symfony/deprecation-contracts": "^2.5 || ^3.0",
|
||||
"symfony/polyfill-php80": "^1.24"
|
||||
"symfony/polyfill-php80": "^1.25"
|
||||
},
|
||||
"provide": {
|
||||
"psr/http-factory-implementation": "1.0",
|
||||
@@ -954,7 +1105,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/guzzle/psr7/issues",
|
||||
"source": "https://github.com/guzzle/psr7/tree/2.11.1"
|
||||
"source": "https://github.com/guzzle/psr7/tree/2.13.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -970,7 +1121,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-06-12T21:50:12+00:00"
|
||||
"time": "2026-07-16T22:23:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/uri-template",
|
||||
@@ -1720,16 +1871,16 @@
|
||||
},
|
||||
{
|
||||
"name": "league/flysystem",
|
||||
"version": "3.34.0",
|
||||
"version": "3.35.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/flysystem.git",
|
||||
"reference": "2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e"
|
||||
"reference": "b277b5dc3d56650b68904117124e79c851e12376"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e",
|
||||
"reference": "2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e",
|
||||
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/b277b5dc3d56650b68904117124e79c851e12376",
|
||||
"reference": "b277b5dc3d56650b68904117124e79c851e12376",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1797,9 +1948,64 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/thephpleague/flysystem/issues",
|
||||
"source": "https://github.com/thephpleague/flysystem/tree/3.34.0"
|
||||
"source": "https://github.com/thephpleague/flysystem/tree/3.35.2"
|
||||
},
|
||||
"time": "2026-05-14T10:28:08+00:00"
|
||||
"time": "2026-07-06T14:42:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/flysystem-aws-s3-v3",
|
||||
"version": "3.35.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
|
||||
"reference": "8475ef9adfc6498b85469e2abec6fe3118cd08c4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/8475ef9adfc6498b85469e2abec6fe3118cd08c4",
|
||||
"reference": "8475ef9adfc6498b85469e2abec6fe3118cd08c4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"aws/aws-sdk-php": "^3.371.5",
|
||||
"league/flysystem": "^3.10.0",
|
||||
"league/mime-type-detection": "^1.0.0",
|
||||
"php": "^8.0.2"
|
||||
},
|
||||
"conflict": {
|
||||
"guzzlehttp/guzzle": "<7.0",
|
||||
"guzzlehttp/ringphp": "<1.1.1"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\Flysystem\\AwsS3V3\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Frank de Jonge",
|
||||
"email": "info@frankdejonge.nl"
|
||||
}
|
||||
],
|
||||
"description": "AWS S3 filesystem adapter for Flysystem.",
|
||||
"keywords": [
|
||||
"Flysystem",
|
||||
"aws",
|
||||
"file",
|
||||
"files",
|
||||
"filesystem",
|
||||
"s3",
|
||||
"storage"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.35.2"
|
||||
},
|
||||
"time": "2026-07-01T23:25:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/flysystem-local",
|
||||
@@ -1852,16 +2058,16 @@
|
||||
},
|
||||
{
|
||||
"name": "league/mime-type-detection",
|
||||
"version": "1.16.0",
|
||||
"version": "1.17.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/mime-type-detection.git",
|
||||
"reference": "2d6702ff215bf922936ccc1ad31007edc76451b9"
|
||||
"reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9",
|
||||
"reference": "2d6702ff215bf922936ccc1ad31007edc76451b9",
|
||||
"url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/f5f47eff7c48ed1003069a2ca67f316fb4021c76",
|
||||
"reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1871,7 +2077,7 @@
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.2",
|
||||
"phpstan/phpstan": "^0.12.68",
|
||||
"phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0"
|
||||
"phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0 || ^11.0 || ^12.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
@@ -1892,7 +2098,7 @@
|
||||
"description": "Mime-type detection for Flysystem",
|
||||
"support": {
|
||||
"issues": "https://github.com/thephpleague/mime-type-detection/issues",
|
||||
"source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0"
|
||||
"source": "https://github.com/thephpleague/mime-type-detection/tree/1.17.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1904,7 +2110,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-21T08:32:55+00:00"
|
||||
"time": "2026-07-09T11:49:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/uri",
|
||||
@@ -2191,6 +2397,72 @@
|
||||
],
|
||||
"time": "2026-01-02T08:56:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "mtdowling/jmespath.php",
|
||||
"version": "2.9.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/jmespath/jmespath.php.git",
|
||||
"reference": "2157c5e50e813ec6a96c1eed3be7f64a20fb32a8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/2157c5e50e813ec6a96c1eed3be7f64a20fb32a8",
|
||||
"reference": "2157c5e50e813ec6a96c1eed3be7f64a20fb32a8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2.5 || ^8.0",
|
||||
"symfony/polyfill-mbstring": "^1.17"
|
||||
},
|
||||
"require-dev": {
|
||||
"composer/xdebug-handler": "^3.0.3",
|
||||
"phpunit/phpunit": "^8.5.52"
|
||||
},
|
||||
"bin": [
|
||||
"bin/jp.php"
|
||||
],
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.9-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/JmesPath.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"JmesPath\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Graham Campbell",
|
||||
"email": "hello@gjcampbell.co.uk",
|
||||
"homepage": "https://github.com/GrahamCampbell"
|
||||
},
|
||||
{
|
||||
"name": "Michael Dowling",
|
||||
"email": "mtdowling@gmail.com",
|
||||
"homepage": "https://github.com/mtdowling"
|
||||
}
|
||||
],
|
||||
"description": "Declaratively specify how to extract elements from a JSON document",
|
||||
"keywords": [
|
||||
"json",
|
||||
"jsonpath"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/jmespath/jmespath.php/issues",
|
||||
"source": "https://github.com/jmespath/jmespath.php/tree/2.9.2"
|
||||
},
|
||||
"time": "2026-07-06T18:56:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nesbot/carbon",
|
||||
"version": "3.12.3",
|
||||
@@ -3610,16 +3882,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
"version": "v3.7.0",
|
||||
"version": "v3.7.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/deprecation-contracts.git",
|
||||
"reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b"
|
||||
"reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b",
|
||||
"reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b",
|
||||
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d",
|
||||
"reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3657,7 +3929,7 @@
|
||||
"description": "A generic function and convention to trigger deprecation notices",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0"
|
||||
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -3677,7 +3949,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-13T15:52:40+00:00"
|
||||
"time": "2026-06-05T06:23:12+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/error-handler",
|
||||
@@ -3926,6 +4198,76 @@
|
||||
],
|
||||
"time": "2026-01-05T13:30:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/filesystem",
|
||||
"version": "v7.4.11",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/filesystem.git",
|
||||
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
|
||||
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/polyfill-ctype": "~1.8",
|
||||
"symfony/polyfill-mbstring": "~1.8"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/process": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Filesystem\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides basic utilities for the filesystem",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/filesystem/tree/v7.4.11"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-11T16:38:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v7.4.8",
|
||||
|
||||
@@ -60,6 +60,21 @@ return [
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
'minio' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('MINIO_ACCESS_KEY_ID'),
|
||||
'secret' => env('MINIO_SECRET_ACCESS_KEY'),
|
||||
'region' => env('MINIO_DEFAULT_REGION', 'us-east-1'),
|
||||
'bucket' => env('MINIO_BUCKET'),
|
||||
'endpoint' => env('MINIO_ENDPOINT'),
|
||||
'use_path_style_endpoint' => env('MINIO_USE_PATH_STYLE_ENDPOINT', true),
|
||||
'http' => [
|
||||
'verify' => env('MINIO_CA_BUNDLE') ?: true,
|
||||
],
|
||||
'throw' => true,
|
||||
'report' => true,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->boolean('is_master')
|
||||
->default(false)
|
||||
->after('password');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('is_master');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('users', 'verification_channel')) {
|
||||
$table->string('verification_channel')->nullable()->after('status');
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('users', 'verification_target')) {
|
||||
$table->string('verification_target')->nullable()->after('verification_channel');
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('users', 'verification_code')) {
|
||||
$table->string('verification_code')->nullable()->after('verification_target');
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('users', 'verification_code_expires_at')) {
|
||||
$table->timestamp('verification_code_expires_at')->nullable()->after('verification_code');
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('users', 'verified_at')) {
|
||||
$table->timestamp('verified_at')->nullable()->after('verification_code_expires_at');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$columns = [];
|
||||
|
||||
foreach ([
|
||||
'verification_channel',
|
||||
'verification_target',
|
||||
'verification_code',
|
||||
'verification_code_expires_at',
|
||||
'verified_at',
|
||||
] as $column) {
|
||||
if (Schema::hasColumn('users', $column)) {
|
||||
$columns[] = $column;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($columns)) {
|
||||
$table->dropColumn($columns);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('whatsapp_number', 20)->nullable()->after('email');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('whatsapp_number');
|
||||
});
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user