1
0

Compare commits

..

9 Commits

Author SHA1 Message Date
Wian Drs b0d08211ec Giant Update 2026-07-31 13:57:11 +07:00
Wian Drs f68d967c8b ketinggalan 2026-07-27 16:35:41 +07:00
Wian Drs 1dd02baa72 big update base struktur tahap 1 2026-07-27 16:24:09 +07:00
Wian Drs 2b3592c4c2 update pagging data table material 2026-07-03 11:50:28 +07:00
Wian Drs c574ada5c1 update material index update 2026-07-02 23:42:12 +07:00
Wian Drs b0f3f03b73 update material index 2026-07-02 23:34:17 +07:00
Wian Drs af1c98b38b update api ticket type 2026-06-25 17:11:48 +07:00
Wian Drs ad472a9880 CRUD Ticket Insidace Type 2026-06-25 13:06:24 +07:00
Wian Drs faab64e0dd sample about material 2026-06-23 16:04:37 +07:00
318 changed files with 33371 additions and 900 deletions
+7
View File
@@ -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
+10 -1
View File
@@ -1,8 +1,9 @@
APP_NAME=API Tiket
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_TIMEZONE=Asia/Jakarta
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
@@ -62,4 +63,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}"
+1 -1
View File
@@ -6,7 +6,7 @@
/storage/*.key
/storage/pail
/vendor
.env
# .env
.env.backup
.env.production
.phpactor.json
+8
View File
@@ -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)
+8895
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,24 @@
<?php
namespace App\Console\Commands;
use App\Services\Billing\InvoiceService;
use Illuminate\Console\Command;
class ProcessBillingSchedules extends Command
{
protected $signature = 'billing:process-schedules';
protected $description = 'Membuat tagihan customer dan memperbarui status sesuai jadwal profile tagihan';
public function handle(InvoiceService $service): int
{
$result = $service->processSchedules();
$this->info(
"Scheduler tagihan selesai: {$result['created']} dibuat, {$result['updated']} status diperbarui.",
);
return self::SUCCESS;
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Console\Commands;
use App\Models\NotificationChannel;
use App\Models\NotificationWebhookLog;
use App\Models\UserNotification;
use Illuminate\Console\Command;
class PruneNotifications extends Command
{
protected $signature = 'notifications:prune';
protected $description = 'Menghapus notifikasi aplikasi dan log webhook yang melewati masa retensi';
public function handle(): int
{
$deleted = 0;
NotificationChannel::where('channel', 'system')->where('enabled', true)->chunkById(100, function ($channels) use (&$deleted) {
foreach ($channels as $channel) {
$days = max(7, (int) data_get($channel->settings, 'retention_days', 90));
$deleted += UserNotification::where('tenant_id', $channel->tenant_id)
->where('created_at', '<', now()->subDays($days))->delete();
}
});
NotificationWebhookLog::where('created_at', '<', now()->subDays(90))->delete();
$this->info("{$deleted} notifikasi lama dihapus.");
return self::SUCCESS;
}
}
+21
View File
@@ -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,
};
}
}
@@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\Audit;
use App\Http\Controllers\ApiController;
use App\Http\Requests\Audit\AuditLogFilterRequest;
use App\Http\Resources\Audit\AuditLogResource;
use App\Services\Audit\AuditLogService;
class AuditLogController extends ApiController
{
protected $service;
public function __construct(AuditLogService $service)
{
$this->service = $service;
}
public function index(AuditLogFilterRequest $request)
{
$data = $this->service->list(
$request->validated()
);
return $this->successPaginated(
$data,
AuditLogResource::class,
'Data audit log berhasil diambil'
);
}
public function show($id)
{
$data = $this->service->detail($id);
return $this->success(
new AuditLogResource($data),
'Detail audit log berhasil diambil'
);
}
}
+102 -8
View File
@@ -2,37 +2,131 @@
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;
return response()->json([
'success' => true,
'token' => $token,
'user' => $user
'token' => $token,
'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,61 @@
<?php
namespace App\Http\Controllers\Billing;
use App\Http\Controllers\ApiController;
use App\Http\Requests\Billing\IndexBillingRequest;
use App\Http\Requests\Billing\StoreBillingProfileRequest;
use App\Http\Requests\Billing\UpdateBillingProfileRequest;
use App\Http\Resources\Billing\BillingProfileResource;
use App\Services\Billing\BillingProfileService;
use Illuminate\Http\Request;
class BillingProfileController extends ApiController
{
public function __construct(protected BillingProfileService $service) {}
public function index(IndexBillingRequest $request)
{
return $this->successPaginated($this->service->list($request->validated(), $request->user(), $this->tenantId($request)), BillingProfileResource::class, 'Daftar profile tagihan berhasil diambil');
}
public function store(StoreBillingProfileRequest $request)
{
return $this->created(new BillingProfileResource($this->service->create($request->validated(), $request->user(), $this->tenantId($request))), 'Profile tagihan berhasil dibuat');
}
public function show(Request $request, int $id)
{
$profile = $this->service->find($id);
$this->authorizeTenant($request, $profile->tenant_id);
return $this->success(new BillingProfileResource($profile), 'Detail profile tagihan berhasil diambil');
}
public function update(UpdateBillingProfileRequest $request, int $id)
{
$profile = $this->service->find($id);
$this->authorizeTenant($request, $profile->tenant_id);
return $this->updated(new BillingProfileResource($this->service->update($profile, $request->validated(), $request->user())), 'Profile tagihan berhasil diperbarui');
}
public function destroy(Request $request, int $id)
{
$profile = $this->service->find($id);
$this->authorizeTenant($request, $profile->tenant_id);
$this->service->delete($profile);
return $this->deleted('Profile tagihan berhasil dihapus');
}
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,79 @@
<?php
namespace App\Http\Controllers\Billing;
use App\Http\Controllers\ApiController;
use App\Http\Requests\Billing\IndexBillingRequest;
use App\Http\Requests\Billing\PayInvoiceRequest;
use App\Http\Requests\Billing\StoreInvoiceRequest;
use App\Http\Requests\Billing\UpdateInvoiceRequest;
use App\Http\Resources\Billing\InvoiceResource;
use App\Services\Billing\InvoiceService;
use Illuminate\Http\Request;
class InvoiceController extends ApiController
{
public function __construct(protected InvoiceService $service) {}
public function index(IndexBillingRequest $request)
{
return $this->successPaginated($this->service->list($this->scope($request), $request->validated(), $request->user(), $this->tenantId($request)), InvoiceResource::class, 'Daftar tagihan berhasil diambil');
}
public function options(Request $request)
{
return $this->success($this->service->options($request->user(), $this->tenantId($request)), 'Pilihan tagihan berhasil diambil');
}
public function store(StoreInvoiceRequest $request)
{
return $this->created(new InvoiceResource($this->service->create($request->validated(), $request->user(), $this->tenantId($request))), 'Tagihan berhasil dibuat');
}
public function show(Request $request, int $id)
{
$invoice = $this->service->find($id);
$this->authorizeTenant($request, $invoice->tenant_id);
return $this->success(new InvoiceResource($invoice), 'Detail tagihan berhasil diambil');
}
public function update(UpdateInvoiceRequest $request, int $id)
{
$invoice = $this->service->find($id);
$this->authorizeTenant($request, $invoice->tenant_id);
return $this->updated(new InvoiceResource($this->service->update($invoice, $request->validated(), $request->user())), 'Tagihan berhasil diperbarui');
}
public function pay(PayInvoiceRequest $request, int $id)
{
$invoice = $this->service->find($id);
$this->authorizeTenant($request, $invoice->tenant_id);
return $this->updated(new InvoiceResource($this->service->pay($invoice, $request->validated(), $request->user())), 'Pembayaran manual berhasil dicatat');
}
public function cancel(Request $request, int $id)
{
$invoice = $this->service->find($id);
$this->authorizeTenant($request, $invoice->tenant_id);
return $this->updated(new InvoiceResource($this->service->cancel($invoice, $request->user())), 'Tagihan berhasil dibatalkan');
}
private function scope(Request $request): string
{
return $request->route()->defaults['invoice_scope'] ?? 'running';
}
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,118 @@
<?php
namespace App\Http\Controllers\Customer;
use App\Http\Controllers\ApiController;
use App\Http\Requests\Customer\ActivateCustomerRequest;
use App\Http\Requests\Customer\IndexCustomerRequest;
use App\Http\Requests\Customer\StoreCustomerRequest;
use App\Http\Requests\Customer\UpdateCustomerRequest;
use App\Http\Resources\Customer\CustomerResource;
use App\Services\Customer\CustomerService;
use Illuminate\Http\Request;
class CustomerController extends ApiController
{
public function __construct(protected CustomerService $service) {}
public function index(IndexCustomerRequest $request)
{
return $this->successPaginated(
$this->service->list($this->scope($request), $request->validated(), $request->user(), $this->tenantId($request)),
CustomerResource::class,
'Daftar customer berhasil diambil'
);
}
public function options(Request $request)
{
return $this->success(
$this->service->options($request->user(), $this->tenantId($request)),
'Pilihan relasi customer berhasil diambil'
);
}
public function store(StoreCustomerRequest $request)
{
return $this->created(
new CustomerResource($this->service->create($request->validated(), $request->user(), $this->tenantId($request))),
'Customer berhasil didaftarkan'
);
}
public function show(Request $request, int $id)
{
$customer = $this->service->find($id, $this->scope($request) === 'trash');
$this->authorizeTenant($request, $customer->tenant_id);
return $this->success(new CustomerResource($customer), 'Detail customer berhasil diambil');
}
public function update(UpdateCustomerRequest $request, int $id)
{
$customer = $this->service->find($id);
$this->authorizeTenant($request, $customer->tenant_id);
return $this->updated(
new CustomerResource($this->service->update($customer, $request->validated(), $request->user())),
'Data customer berhasil diperbarui'
);
}
public function activate(ActivateCustomerRequest $request, int $id)
{
$customer = $this->service->find($id);
$this->authorizeTenant($request, $customer->tenant_id);
return $this->updated(new CustomerResource($this->service->activate($customer, $request->validated())), 'Customer berhasil diaktifkan');
}
public function deactivate(Request $request, int $id)
{
$customer = $this->service->find($id);
$this->authorizeTenant($request, $customer->tenant_id);
return $this->updated(new CustomerResource($this->service->deactivate($customer)), 'Customer berhasil dinonaktifkan');
}
public function destroy(Request $request, int $id)
{
$customer = $this->service->find($id);
$this->authorizeTenant($request, $customer->tenant_id);
$this->service->trash($customer);
return $this->deleted('Customer dipindahkan ke Sampah');
}
public function restore(Request $request, int $id)
{
$customer = $this->service->find($id, true);
$this->authorizeTenant($request, $customer->tenant_id);
return $this->updated(new CustomerResource($this->service->restore($customer)), 'Customer berhasil dipulihkan');
}
public function forceDelete(Request $request, int $id)
{
$customer = $this->service->find($id, true);
$this->authorizeTenant($request, $customer->tenant_id);
$this->service->forceDelete($customer);
return $this->deleted('Customer dihapus permanen');
}
private function scope(Request $request): string
{
return $request->route()->defaults['customer_scope'] ?? 'orders';
}
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,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');
}
}
@@ -0,0 +1,82 @@
<?php
namespace App\Http\Controllers\Material;
use App\Http\Controllers\ApiController;
use App\Http\Requests\Material\AssignMaterialRequest;
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;
class MaterialController extends ApiController
{
protected $service;
public function __construct(MaterialService $service)
{
$this->service = $service;
}
public function index(IndexMaterialRequest $request)
{
$data = $this->service->list($request->validated());
return $this->successPaginated(
$data,
MaterialResource::class,
'List material user berhasil diambil'
);
}
public function assign(AssignMaterialRequest $request)
{
$result = $this->service->assign($request->validated());
return $this->created(
new MaterialResource($result),
'Material berhasil di-assign'
);
}
public function transfer(TransferMaterialRequest $request)
{
$result = $this->service->transfer($request->validated());
return $this->updated(
new MaterialResource($result),
'Material berhasil di-transfer'
);
}
public function return(ReturnMaterialRequest $request)
{
$result = $this->service->return($request->validated());
return $this->updated(
new MaterialResource($result),
'Material berhasil direturn'
);
}
public function listByUser($user_id)
{
$data = $this->service->listByUser($user_id);
return $this->success(
MaterialResource::collection($data),
'List material user berhasil diambil'
);
}
public function history($barcode_id)
{
$data = $this->service->history($barcode_id);
return $this->success(
$data,
'History material 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,192 @@
<?php
namespace App\Http\Controllers\Notification;
use App\Http\Controllers\ApiController;
use App\Http\Requests\Notification\IndexNotificationRequest;
use App\Http\Requests\Notification\SaveNotificationPreferencesRequest;
use App\Http\Requests\Notification\StoreCustomerContactRequest;
use App\Http\Requests\Notification\StoreNotificationBroadcastRequest;
use App\Http\Requests\Notification\StoreNotificationChannelRequest;
use App\Http\Requests\Notification\StoreNotificationTemplateRequest;
use App\Http\Resources\Notification\NotificationDeliveryResource;
use App\Models\Customer;
use App\Models\CustomerContact;
use App\Models\NotificationBroadcast;
use App\Models\NotificationChannel;
use App\Models\NotificationEvent;
use App\Models\NotificationTemplate;
use App\Services\Notification\NotificationService;
use Illuminate\Http\Request;
class NotificationController extends ApiController
{
public function __construct(protected NotificationService $service) {}
public function events()
{
return $this->success(NotificationEvent::where('enabled', true)->orderBy('category')->orderBy('name')->get(), 'Daftar event notifikasi berhasil diambil');
}
public function channels(Request $request)
{
return $this->success($this->service->channels($this->channel($request), $request->user(), $this->tenantId($request)), 'Konfigurasi channel berhasil diambil');
}
public function storeChannel(StoreNotificationChannelRequest $request)
{
abort_unless($request->validated('channel') === $this->channel($request), 422);
return $this->created($this->service->saveChannel($request->validated(), $request->user(), $this->tenantId($request)), 'Konfigurasi channel berhasil dibuat');
}
public function updateChannel(StoreNotificationChannelRequest $request, NotificationChannel $notificationChannel)
{
$this->authorizeTenant($request, $notificationChannel->tenant_id);
abort_unless($notificationChannel->channel === $this->channel($request), 404);
return $this->updated($this->service->saveChannel($request->validated(), $request->user(), $this->tenantId($request), $notificationChannel), 'Konfigurasi channel berhasil diperbarui');
}
public function testChannel(Request $request, NotificationChannel $notificationChannel)
{
$this->authorizeTenant($request, $notificationChannel->tenant_id);
abort_unless($notificationChannel->channel === $this->channel($request), 404);
return $this->success($this->service->testChannel($notificationChannel), 'Pengujian channel selesai');
}
public function templates(Request $request)
{
return $this->success($this->service->templates($this->channel($request), $request->user(), $this->tenantId($request)), 'Daftar template berhasil diambil');
}
public function storeTemplate(StoreNotificationTemplateRequest $request)
{
abort_unless($request->validated('channel') === $this->channel($request), 422);
return $this->created($this->service->saveTemplate($request->validated(), $request->user(), $this->tenantId($request)), 'Template berhasil dibuat');
}
public function updateTemplate(StoreNotificationTemplateRequest $request, NotificationTemplate $notificationTemplate)
{
if ($notificationTemplate->tenant_id) {
$this->authorizeTenant($request, $notificationTemplate->tenant_id);
}
abort_unless($notificationTemplate->channel === $this->channel($request), 404);
return $this->updated($this->service->saveTemplate($request->validated(), $request->user(), $this->tenantId($request), $notificationTemplate), 'Template berhasil diperbarui');
}
public function preferences(Request $request)
{
$tenantId = $request->user()->isMasterAdmin() && $request->integer('tenant_id')
? $request->integer('tenant_id')
: $this->tenantId($request);
return $this->success($this->service->preferences($request->user(), $tenantId), 'Preference notifikasi berhasil diambil');
}
public function savePreferences(SaveNotificationPreferencesRequest $request)
{
$this->service->savePreferences($request->validated('preferences'), $request->user(), $this->tenantId($request));
return $this->updated(null, 'Preference notifikasi berhasil disimpan');
}
public function deliveries(IndexNotificationRequest $request)
{
return $this->successPaginated(
$this->service->deliveries($this->channel($request), $request->validated(), $request->user(), $this->tenantId($request)),
NotificationDeliveryResource::class,
'Log pesan berhasil diambil',
);
}
public function broadcasts(IndexNotificationRequest $request)
{
return $this->success($this->service->broadcasts($request->validated(), $request->user(), $this->tenantId($request)), 'Daftar pesan siaran berhasil diambil');
}
public function previewBroadcast(StoreNotificationBroadcastRequest $request)
{
return $this->success($this->service->previewBroadcast($request->validated(), $request->user(), $this->tenantId($request)), 'Preview penerima berhasil dihitung');
}
public function storeBroadcast(StoreNotificationBroadcastRequest $request)
{
return $this->created($this->service->createBroadcast($request->validated(), $request->user(), $this->tenantId($request)), 'Pesan siaran masuk antrean');
}
public function showBroadcast(Request $request, NotificationBroadcast $notificationBroadcast)
{
$this->authorizeTenant($request, $notificationBroadcast->tenant_id);
return $this->success($notificationBroadcast->load(['template', 'channelConfig', 'recipients.customer:id,customer_code,name']), 'Detail siaran berhasil diambil');
}
public function approveBroadcast(Request $request, NotificationBroadcast $notificationBroadcast)
{
$this->authorizeTenant($request, $notificationBroadcast->tenant_id);
return $this->updated($this->service->approveBroadcast($notificationBroadcast, $request->user()), 'Pesan siaran berhasil disetujui');
}
public function myNotifications(IndexNotificationRequest $request)
{
return $this->success($this->service->userNotifications($request->user(), $request->validated()), 'Notifikasi aplikasi berhasil diambil');
}
public function markRead(Request $request, string $uuid)
{
$notification = $request->user()->userNotifications()->where('uuid', $uuid)->firstOrFail();
$notification->update(['read_at' => now()]);
return $this->updated($notification, 'Notifikasi ditandai sudah dibaca');
}
public function contacts(Request $request, Customer $customer)
{
$this->authorizeTenant($request, $customer->tenant_id);
return $this->success($customer->contacts()->orderBy('type')->orderByDesc('is_primary')->get(), 'Kontak notifikasi berhasil diambil');
}
public function storeContact(StoreCustomerContactRequest $request, Customer $customer)
{
$this->authorizeTenant($request, $customer->tenant_id);
return $this->created($this->service->saveContact($customer, $request->validated()), 'Kontak notifikasi berhasil dibuat');
}
public function updateContact(StoreCustomerContactRequest $request, Customer $customer, CustomerContact $contact)
{
$this->authorizeTenant($request, $customer->tenant_id);
return $this->updated($this->service->saveContact($customer, $request->validated(), $contact), 'Kontak notifikasi berhasil diperbarui');
}
public function destroyContact(Request $request, Customer $customer, CustomerContact $contact)
{
$this->authorizeTenant($request, $customer->tenant_id);
abort_unless((int) $contact->customer_id === (int) $customer->id, 404);
$contact->delete();
return $this->deleted('Kontak notifikasi berhasil dihapus');
}
private function channel(Request $request): string
{
return $request->route()->defaults['notification_channel'];
}
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,90 @@
<?php
namespace App\Http\Controllers\Notification;
use App\Http\Controllers\Controller;
use App\Models\NotificationChannel;
use App\Models\NotificationDelivery;
use App\Models\NotificationWebhookLog;
use Illuminate\Http\Request;
class NotificationWebhookController extends Controller
{
public function verify(Request $request, NotificationChannel $notificationChannel)
{
abort_unless(
$request->query('hub.mode') === 'subscribe'
&& hash_equals((string) data_get($notificationChannel->credentials, 'webhook_verify_token'), (string) $request->query('hub.verify_token')),
403,
);
return response($request->query('hub.challenge'), 200);
}
public function handle(Request $request, NotificationChannel $notificationChannel)
{
$valid = $this->validSignature($request, $notificationChannel);
$payload = $request->all();
$updates = $this->updates($payload);
$log = NotificationWebhookLog::create([
'notification_channel_id' => $notificationChannel->id,
'provider' => $notificationChannel->provider,
'event_type' => data_get($payload, 'entry.0.changes.0.field') ?? data_get($payload, 'event'),
'provider_message_id' => data_get($updates, '0.id'),
'signature_valid' => $valid,
'payload' => $payload,
]);
abort_unless($valid, 401);
foreach ($updates as $update) {
$delivery = NotificationDelivery::where('provider_message_id', $update['id'] ?? null)->first();
if (! $delivery) {
continue;
}
$status = match ($update['status'] ?? null) {
'delivered' => 'delivered',
'read' => 'read',
'failed' => 'failed',
default => 'sent',
};
$delivery->update([
'status' => $status,
'delivered_at' => $status === 'delivered' ? now() : $delivery->delivered_at,
'read_at' => $status === 'read' ? now() : $delivery->read_at,
'failed_at' => $status === 'failed' ? now() : $delivery->failed_at,
'error_code' => data_get($update, 'errors.0.code'),
'error_message' => data_get($update, 'errors.0.title'),
]);
}
$log->update(['processed_at' => now()]);
return response()->json(['success' => true]);
}
private function validSignature(Request $request, NotificationChannel $channel): bool
{
if ($channel->channel === 'whatsapp_official') {
$secret = data_get($channel->credentials, 'app_secret');
$signature = $request->header('X-Hub-Signature-256');
return $secret && $signature && hash_equals('sha256='.hash_hmac('sha256', $request->getContent(), $secret), $signature);
}
$secret = data_get($channel->credentials, 'webhook_secret');
return $secret && hash_equals((string) $secret, (string) $request->header('X-Webhook-Secret'));
}
private function updates(array $payload): array
{
$statuses = data_get($payload, 'entry.0.changes.0.value.statuses');
if (is_array($statuses)) {
return $statuses;
}
return [[
'id' => data_get($payload, 'message_id') ?? data_get($payload, 'id'),
'status' => data_get($payload, 'status') ?? data_get($payload, 'event'),
'errors' => data_get($payload, 'errors', []),
]];
}
}
@@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers\Payment;
use App\Http\Controllers\ApiController;
use App\Http\Requests\Payment\IndexPaymentRequest;
use App\Http\Requests\Payment\SaveTenantPaymentSettingRequest;
use App\Http\Requests\Payment\StoreGatewayAccountRequest;
use App\Http\Resources\Payment\PaymentTransactionResource;
use App\Models\PaymentGatewayAccount;
use App\Services\Payment\PaymentGatewayService;
use Illuminate\Http\Request;
class PaymentGatewayController extends ApiController
{
public function __construct(protected PaymentGatewayService $service) {}
public function providers()
{
return $this->success($this->service->providers(), 'Daftar provider berhasil diambil');
}
public function settings(Request $request)
{
return $this->success($this->service->settings($request->user(), $this->tenantId($request)), 'Pengaturan payment gateway berhasil diambil');
}
public function storeAccount(StoreGatewayAccountRequest $request)
{
return $this->created($this->service->saveAccount($request->validated(), $request->user(), $this->tenantId($request)), 'Akun gateway berhasil dibuat');
}
public function updateAccount(StoreGatewayAccountRequest $request, PaymentGatewayAccount $account)
{
return $this->updated($this->service->saveAccount($request->validated(), $request->user(), $this->tenantId($request), $account), 'Akun gateway berhasil diperbarui');
}
public function saveSetting(SaveTenantPaymentSettingRequest $request)
{
return $this->updated($this->service->saveTenantSetting($request->validated(), $request->user(), $this->tenantId($request)), 'Mode pembayaran tenant berhasil disimpan');
}
public function transactions(IndexPaymentRequest $request)
{
return $this->successPaginated($this->service->transactions($request->validated(), $request->user(), $this->tenantId($request)), PaymentTransactionResource::class, 'Log pembayaran berhasil diambil');
}
private function tenantId(Request $request): ?int
{
return $request->attributes->get('tenant_id');
}
}
@@ -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)
@@ -67,4 +96,4 @@ class TicketController extends ApiController
'Ticket berhasil dibatalkan'
);
}
}
}
@@ -0,0 +1,78 @@
<?php
namespace App\Http\Controllers\Ticket;
use App\Http\Controllers\ApiController;
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
{
public function __construct(
private TicketIncidentTypeService $service
) {}
public function index(IndexTicketIncidentTypeRequest $request): JsonResponse
{
return $this->successPaginated(
$this->service->list($request->validated()),
TicketIncidentTypeResource::class,
'Data Incident Type berhasil diambil'
);
}
public function store(
StoreTicketIncidentTypeRequest $request
): JsonResponse {
$incidentType = $this->service->create(
$request->validated()
);
return $this->created(
new TicketIncidentTypeResource($incidentType),
'Incident Type berhasil dibuat'
);
}
public function show(
TicketIncidentType $ticketIncidentType
): JsonResponse {
return $this->success(
new TicketIncidentTypeResource(
$ticketIncidentType
)
);
}
public function update(
UpdateTicketIncidentTypeRequest $request,
TicketIncidentType $ticketIncidentType
): JsonResponse {
$incidentType = $this->service->update(
$ticketIncidentType,
$request->validated()
);
return $this->updated(
new TicketIncidentTypeResource($incidentType),
'Incident Type berhasil diupdate'
);
}
public function destroy(
TicketIncidentType $ticketIncidentType
): JsonResponse {
$this->service->delete(
$ticketIncidentType
);
return $this->deleted(
'Incident Type berhasil dihapus'
);
}
}
@@ -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,23 +16,27 @@ class TicketTypeController extends ApiController
protected TicketTypeService $service
) {}
public function index(Request $request)
public function index(IndexTicketTypeRequest $request)
{
$data = TicketType::latest()->paginate(
$request->get('per_page', 10)
$data = $this->service->list(
$request->validated()
);
return $this->success($data);
return $this->successPaginated(
$data,
TicketTypeResource::class,
'Data Ticket Type berhasil diambil'
);
}
public function store(StoreTicketTypeRequest $request)
{
$type = $this->service->create(
$data = $this->service->create(
$request->validated()
);
return $this->created(
new TicketTypeResource($type),
new TicketTypeResource($data),
'Ticket Type berhasil dibuat'
);
}
@@ -40,7 +44,8 @@ class TicketTypeController extends ApiController
public function show(TicketType $ticket_type)
{
return $this->success(
new TicketTypeResource($ticket_type)
new TicketTypeResource($ticket_type),
'Detail Ticket Type berhasil diambil'
);
}
@@ -48,13 +53,13 @@ class TicketTypeController extends ApiController
UpdateTicketTypeRequest $request,
TicketType $ticket_type
) {
$type = $this->service->update(
$data = $this->service->update(
$ticket_type,
$request->validated()
);
return $this->updated(
new TicketTypeResource($type),
new TicketTypeResource($data),
'Ticket Type berhasil diupdate'
);
}
@@ -0,0 +1,154 @@
<?php
namespace App\Http\Controllers\Topology;
use App\Http\Controllers\ApiController;
use App\Http\Requests\Topology\IndexTopologyRequest;
use App\Http\Requests\Topology\StoreCustomerConnectionRequest;
use App\Http\Requests\Topology\StoreTopologyRequest;
use App\Http\Requests\Topology\UpdateTopologyRequest;
use App\Http\Resources\Topology\TopologyResource;
use App\Models\Customer;
use App\Models\TopologyNode;
use App\Models\TopologyPort;
use App\Services\Topology\TopologyService;
use Illuminate\Http\Request;
class TopologyController extends ApiController
{
public function __construct(protected TopologyService $service) {}
public function index(IndexTopologyRequest $request)
{
return $this->successPaginated(
$this->service->list($this->resource($request), $request->validated(), $request->user(), $this->tenantId($request)),
TopologyResource::class,
'Daftar topologi berhasil diambil',
);
}
public function store(StoreTopologyRequest $request)
{
return $this->created(
new TopologyResource($this->service->create($this->resource($request), $request->validated(), $request->user(), $this->tenantId($request))),
'Data topologi berhasil dibuat',
);
}
public function show(Request $request, int $id)
{
$model = $this->service->find($this->resource($request), $id);
$this->authorizeModel($request, $model);
return $this->success(new TopologyResource($model), 'Detail topologi berhasil diambil');
}
public function update(UpdateTopologyRequest $request, int $id)
{
$resource = $this->resource($request);
$model = $this->service->find($resource, $id);
$this->authorizeModel($request, $model, true);
return $this->updated(
new TopologyResource($this->service->update($resource, $model, $request->validated(), $request->user())),
'Data topologi berhasil diperbarui',
);
}
public function destroy(Request $request, int $id)
{
$resource = $this->resource($request);
$model = $this->service->find($resource, $id);
$this->authorizeModel($request, $model, true);
$this->service->delete($resource, $model);
return $this->deleted('Data topologi berhasil dihapus');
}
public function options(Request $request)
{
return $this->success($this->service->options($request->user(), $this->tenantId($request)), 'Pilihan topologi berhasil diambil');
}
public function map(IndexTopologyRequest $request)
{
return $this->success($this->service->map($request->validated(), $request->user(), $this->tenantId($request)), 'Data peta topologi berhasil diambil');
}
public function ports(Request $request, TopologyNode $node)
{
$this->authorizeTenant($request, $node->tenant_id);
return $this->success($this->service->ports($node), 'Daftar port berhasil diambil');
}
public function storePort(StoreTopologyRequest $request, TopologyNode $node)
{
$this->authorizeTenant($request, $node->tenant_id);
return $this->created($this->service->createPort($node, $request->validated()), 'Port berhasil dibuat');
}
public function updatePort(UpdateTopologyRequest $request, TopologyPort $port)
{
$this->authorizeTenant($request, $port->tenant_id);
return $this->updated($this->service->updatePort($port, $request->validated()), 'Port berhasil diperbarui');
}
public function destroyPort(Request $request, TopologyPort $port)
{
$this->authorizeTenant($request, $port->tenant_id);
$this->service->deletePort($port);
return $this->deleted('Port berhasil dihapus');
}
public function trace(Request $request, TopologyNode $node, string $direction)
{
abort_unless(in_array($direction, ['upstream', 'downstream'], true), 404);
$this->authorizeTenant($request, $node->tenant_id);
return $this->success($this->service->trace($node, $direction), 'Jalur topologi berhasil ditelusuri');
}
public function connectCustomer(StoreCustomerConnectionRequest $request)
{
return $this->created(
$this->service->connectCustomer($request->validated(), $request->user(), $this->tenantId($request)),
'Customer berhasil dihubungkan ke ODP',
);
}
public function customerPath(Request $request, Customer $customer)
{
$this->authorizeTenant($request, $customer->tenant_id);
return $this->success($this->service->customerPath($customer), 'Jalur customer berhasil ditelusuri');
}
private function resource(Request $request): string
{
return $request->route()->defaults['topology_resource'];
}
private function tenantId(Request $request): ?int
{
return $request->attributes->get('tenant_id');
}
private function authorizeModel(Request $request, $model, bool $write = false): void
{
if ($model->tenant_id === null) {
abort_if($write && ! $request->user()->isMasterAdmin(), 403);
return;
}
$this->authorizeTenant($request, $model->tenant_id);
}
private function authorizeTenant(Request $request, int $tenantId): void
{
abort_unless($request->user()->isMasterAdmin() || $tenantId === $this->tenantId($request), 403);
}
}
@@ -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,40 @@
<?php
namespace App\Http\Controllers\Wallet;
use App\Http\Controllers\ApiController;
use App\Http\Requests\Wallet\StoreWithdrawalRequest;
use App\Http\Requests\Wallet\TransferWalletRequest;
use App\Models\Wallet;
use App\Services\Wallet\WalletService;
use Illuminate\Http\Request;
class WalletController extends ApiController
{
public function __construct(protected WalletService $service) {}
public function index(Request $request)
{
return $this->success($this->service->wallets($request->user(), $this->tenantId($request)), 'Daftar dompet berhasil diambil');
}
public function ledger(Request $request, Wallet $wallet)
{
return $this->success($this->service->ledger($wallet, $request->user(), $this->tenantId($request), $request->validate(['per_page' => ['nullable', 'integer', 'min:1', 'max:100']])), 'Mutasi dompet berhasil diambil');
}
public function transfer(TransferWalletRequest $request, Wallet $wallet)
{
return $this->created($this->service->transfer($wallet, Wallet::findOrFail($request->integer('destination_wallet_id')), $request->float('amount'), $request->user(), $this->tenantId($request)), 'Saldo berhasil dipindahkan');
}
public function withdraw(StoreWithdrawalRequest $request, Wallet $wallet)
{
return $this->created($this->service->requestWithdrawal($wallet, $request->validated(), $request->user(), $this->tenantId($request)), 'Permintaan penarikan berhasil dibuat');
}
private function tenantId(Request $request): ?int
{
return $request->attributes->get('tenant_id');
}
}
@@ -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'
);
}
}
+25
View File
@@ -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);
}
}
@@ -0,0 +1,70 @@
<?php
namespace App\Http\Requests\Audit;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class AuditLogFilterRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
/** Filter berdasarkan nama modul audit. */
'module' => [
'nullable',
'string',
'max:100',
],
/** Filter berdasarkan nama action audit. */
'action' => [
'nullable',
'string',
'max:100',
],
/** Filter berdasarkan ID user pelaku. */
'user_id' => [
'nullable',
'integer',
'exists:users,id',
],
/** Filter berdasarkan ID tenant. */
'tenant_id' => [
'nullable',
'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',
],
];
}
}
@@ -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,28 @@
<?php
namespace App\Http\Requests\Billing;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class IndexBillingRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'search' => ['nullable', 'string', 'max:255'],
'status' => ['nullable', 'string', 'max:30'],
'date_from' => ['nullable', 'date'],
'date_to' => ['nullable', 'date', 'after_or_equal:date_from'],
'page' => ['nullable', 'integer', 'min:1'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
'sort_by' => ['nullable', Rule::in(['id', 'name', 'invoice_number', 'issue_date', 'isolation_date', 'total', 'balance', 'status', 'created_at', 'paid_at'])],
'sort_direction' => ['nullable', Rule::in(['asc', 'desc'])],
];
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Requests\Billing;
use Illuminate\Foundation\Http\FormRequest;
class PayInvoiceRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'amount' => ['required', 'numeric', 'gt:0'],
'payment_method' => ['nullable', 'string', 'max:50'],
'payment_reference' => ['nullable', 'string', 'max:255'],
'paid_at' => ['nullable', 'date'],
'notes' => ['nullable', 'string'],
];
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests\Billing;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreBillingProfileRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
'name' => ['required', 'string', 'max:255'],
'schedule_type' => ['required', Rule::in(['fixed_date', 'installation_date'])],
'invoice_day' => ['required_if:schedule_type,fixed_date', 'nullable', 'integer', 'between:1,28'],
'send_day' => ['required_if:schedule_type,fixed_date', 'nullable', 'integer', 'between:1,28', 'gte:invoice_day'],
'warning_day' => ['required_if:schedule_type,fixed_date', 'nullable', 'integer', 'between:1,28', 'gte:send_day'],
'isolation_day' => ['required_if:schedule_type,fixed_date', 'nullable', 'integer', 'between:1,28', 'gte:warning_day'],
'invoice_days_before' => ['required_if:schedule_type,installation_date', 'nullable', 'integer', 'between:0,90'],
'send_days_before' => ['required_if:schedule_type,installation_date', 'nullable', 'integer', 'between:0,90', 'lte:invoice_days_before'],
'warning_days_before' => ['required_if:schedule_type,installation_date', 'nullable', 'integer', 'between:0,90', 'lte:send_days_before'],
'isolation_days_after' => ['required_if:schedule_type,installation_date', 'nullable', 'integer', 'between:0,90'],
'tax_type' => ['required', Rule::in(['inclusive', 'exclusive', 'non_tax'])],
'tax_rate' => ['nullable', 'numeric', 'between:0,100'],
'status' => ['nullable', Rule::in(['active', 'inactive'])],
'notes' => ['nullable', 'string'],
];
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests\Billing;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreInvoiceRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
'customer_id' => ['required', 'integer', 'exists:customers,id'],
'invoice_number' => ['nullable', 'string', 'max:100'],
'period_start' => ['required', 'date'],
'period_end' => ['required', 'date', 'after_or_equal:period_start'],
'issue_date' => ['required', 'date'],
'send_date' => ['nullable', 'date', 'after_or_equal:issue_date'],
'warning_date' => ['nullable', 'date', 'after_or_equal:send_date'],
'isolation_date' => ['nullable', 'date', 'after_or_equal:warning_date'],
'subtotal' => ['required', 'numeric', 'min:0'],
'notes' => ['nullable', 'string'],
'metadata' => ['nullable', 'array'],
'status' => ['nullable', Rule::in(['draft', 'published', 'sent', 'warning', 'overdue', 'paid', 'cancelled'])],
];
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Http\Requests\Billing;
class UpdateBillingProfileRequest extends StoreBillingProfileRequest
{
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,18 @@
<?php
namespace App\Http\Requests\Billing;
class UpdateInvoiceRequest extends StoreInvoiceRequest
{
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,24 @@
<?php
namespace App\Http\Requests\Customer;
use Illuminate\Foundation\Http\FormRequest;
class ActivateCustomerRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'package_profile_id' => ['required', 'integer', 'exists:nas_package_profiles,id'],
'nas_mikrotik_id' => ['required', 'integer', 'exists:nas_mikrotiks,id'],
'billing_profile_id' => ['required', 'integer', 'exists:billing_profiles,id'],
'topology_id' => ['nullable', 'integer', 'min:1'],
'external_username' => ['nullable', 'string', 'max:255'],
];
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Http\Requests\Customer;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class IndexCustomerRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'search' => ['nullable', 'string', 'max:255'],
'source' => ['nullable', Rule::in(['manual', 'mikrotik', 'radius'])],
'page' => ['nullable', 'integer', 'min:1'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
'sort_by' => ['nullable', Rule::in(['id', 'customer_code', 'name', 'email', 'whatsapp_number', 'status', 'order_stage', 'created_at', 'activated_at'])],
'sort_direction' => ['nullable', Rule::in(['asc', 'desc'])],
];
}
}
@@ -0,0 +1,48 @@
<?php
namespace App\Http\Requests\Customer;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreCustomerRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
'user_id' => ['nullable', 'integer', 'exists:users,id'],
'customer_code' => ['nullable', 'string', 'max:100'],
'name' => ['required', 'string', 'max:255'],
'identity_number' => ['nullable', 'string', 'max:100'],
'email' => ['nullable', 'email', 'max:255'],
'whatsapp_number' => ['nullable', 'string', 'max:50'],
'installation_address' => ['nullable', 'string'],
'provinsi_id' => ['nullable', 'integer', 'exists:wilayah_provinsi,id'],
'kabupaten_id' => ['nullable', 'integer', 'exists:wilayah_kabupaten,id'],
'kecamatan_id' => ['nullable', 'integer', 'exists:wilayah_kecamatan,id'],
'desa_id' => ['nullable', 'integer', 'exists:wilayah_desa,id'],
'latitude' => ['nullable', 'numeric', 'between:-90,90'],
'longitude' => ['nullable', 'numeric', 'between:-180,180'],
'package_profile_id' => ['nullable', 'integer', 'exists:nas_package_profiles,id'],
'nas_mikrotik_id' => ['nullable', 'integer', 'exists:nas_mikrotiks,id'],
'billing_profile_id' => ['nullable', 'integer', 'min:1'],
'topology_id' => ['nullable', 'integer', 'min:1'],
'external_username' => ['nullable', 'string', 'max:255'],
'source' => ['nullable', Rule::in(['manual', 'mikrotik', 'radius'])],
'order_stage' => ['nullable', Rule::in(['registration', 'data_completion', 'ready_activation', 'activation_failed'])],
'notes' => ['nullable', 'string'],
'images' => ['nullable', 'array', 'max:20'],
'images.*.stored_file_id' => ['required', 'integer', 'exists:stored_files,id'],
'images.*.category' => ['required', Rule::in(['house', 'installation', 'modem', 'odp', 'cable', 'identity', 'other'])],
'images.*.caption' => ['nullable', 'string', 'max:255'],
'images.*.is_cover' => ['nullable', 'boolean'],
'images.*.sort_order' => ['nullable', 'integer', 'min:0', 'max:1000'],
];
}
}
@@ -0,0 +1,14 @@
<?php
namespace App\Http\Requests\Customer;
class UpdateCustomerRequest extends StoreCustomerRequest
{
public function rules(): array
{
$rules = parent::rules();
$rules['name'] = ['sometimes', 'required', 'string', 'max:255'];
return $rules;
}
}
@@ -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,39 @@
<?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
{
$maxSize = $this->input('category') === 'customer-image' ? 500 : 20480;
return [
'file' => [
'required',
'file',
"max:{$maxSize}",
'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' => $this->input('category') === 'customer-image'
? 'Gambar customer maksimal 500 KB setelah kompresi.'
: '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,37 @@
<?php
namespace App\Http\Requests\Material;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class AssignMaterialRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'user_id' => 'required|integer',
'barcode_id' => 'required|string',
'material_name' => 'required|string',
'type' => 'required|in:serialized,consumable',
'serial_number' => 'nullable|string',
'qty' => 'nullable|numeric',
'unit' => 'nullable|string',
];
}
}
@@ -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,30 @@
<?php
namespace App\Http\Requests\Material;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class ReturnMaterialRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'barcode_id' => 'required|string',
'user_id' => 'required|integer',
];
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests\Material;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class TransferMaterialRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'barcode_id' => 'required|string',
'from_user_id' => 'required|integer',
'to_user_id' => 'required|integer',
];
}
}
@@ -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,24 @@
<?php
namespace App\Http\Requests\Notification;
use Illuminate\Foundation\Http\FormRequest;
class IndexNotificationRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'search' => ['nullable', 'string', 'max:255'],
'status' => ['nullable', 'string', 'max:30'],
'unread' => ['nullable', 'boolean'],
'page' => ['nullable', 'integer', 'min:1'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
];
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\Notification;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class SaveNotificationPreferencesRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'preferences' => ['required', 'array'],
'preferences.*.tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
'preferences.*.notification_event_id' => ['required', 'integer', 'exists:notification_events,id'],
'preferences.*.notification_channel_id' => ['nullable', 'integer', 'exists:notification_channels,id'],
'preferences.*.channel' => ['required', Rule::in(['system', 'whatsapp_official', 'whatsapp_unofficial', 'email', 'telegram'])],
'preferences.*.enabled' => ['required', 'boolean'],
'preferences.*.priority' => ['nullable', 'integer', 'between:1,20'],
'preferences.*.send_delay_minutes' => ['nullable', 'integer', 'between:0,10080'],
'preferences.*.quiet_hours_start' => ['nullable', 'date_format:H:i'],
'preferences.*.quiet_hours_end' => ['nullable', 'date_format:H:i'],
];
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\Notification;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreCustomerContactRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'type' => ['required', Rule::in(['email', 'whatsapp', 'telegram'])],
'value' => ['required', 'string', 'max:255'],
'label' => ['nullable', 'string', 'max:100'],
'is_primary' => ['nullable', 'boolean'],
'is_verified' => ['nullable', 'boolean'],
'can_receive_transactional' => ['nullable', 'boolean'],
'can_receive_broadcast' => ['nullable', 'boolean'],
'opted_out_at' => ['nullable', 'date'],
];
}
}
@@ -0,0 +1,39 @@
<?php
namespace App\Http\Requests\Notification;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreNotificationBroadcastRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
'name' => ['required_without:preview', 'string', 'max:255'],
'channel' => ['required', Rule::in(['system', 'whatsapp_official', 'whatsapp_unofficial', 'email', 'telegram'])],
'notification_template_id' => ['nullable', 'integer', 'exists:notification_templates,id'],
'notification_channel_id' => ['nullable', 'integer', 'exists:notification_channels,id'],
'subject' => ['nullable', 'string', 'max:255'],
'body' => ['required_without:notification_template_id', 'nullable', 'string', 'max:20000'],
'scheduled_at' => ['nullable', 'date'],
'filters' => ['nullable', 'array'],
'filters.customer_status' => ['nullable', 'array'],
'filters.customer_status.*' => [Rule::in(['order', 'active', 'inactive', 'unmanaged'])],
'filters.source' => ['nullable', Rule::in(['manual', 'nas'])],
'filters.package_profile_id' => ['nullable', 'integer'],
'filters.billing_profile_id' => ['nullable', 'integer'],
'filters.topology_node_id' => ['nullable', 'integer', 'exists:topology_nodes,id'],
'filters.provinsi_id' => ['nullable', 'integer'],
'filters.kabupaten_id' => ['nullable', 'integer'],
'filters.kecamatan_id' => ['nullable', 'integer'],
'filters.desa_id' => ['nullable', 'integer'],
];
}
}
@@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\Notification;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreNotificationChannelRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
'channel' => ['required', Rule::in(['system', 'whatsapp_official', 'whatsapp_unofficial', 'email', 'telegram'])],
'provider' => ['nullable', 'string', 'max:50'],
'name' => ['required', 'string', 'max:255'],
'enabled' => ['nullable', 'boolean'],
'is_default' => ['nullable', 'boolean'],
'credentials' => ['nullable', 'array'],
'credentials.*' => ['nullable', 'string', 'max:4000'],
'settings' => ['nullable', 'array'],
];
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Http\Requests\Notification;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreNotificationTemplateRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
'notification_event_id' => ['required', 'integer', 'exists:notification_events,id'],
'notification_channel_id' => ['nullable', 'integer', 'exists:notification_channels,id'],
'channel' => ['required', Rule::in(['system', 'whatsapp_official', 'whatsapp_unofficial', 'email', 'telegram'])],
'name' => ['required', 'string', 'max:255'],
'subject' => ['nullable', 'string', 'max:255'],
'body' => ['required', 'string', 'max:20000'],
'provider_template_name' => ['nullable', 'string', 'max:255'],
'provider_template_id' => ['nullable', 'string', 'max:255'],
'language' => ['nullable', 'string', 'max:10'],
'approval_status' => ['nullable', Rule::in(['not_required', 'pending', 'approved', 'rejected'])],
'enabled' => ['nullable', 'boolean'],
];
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Http\Requests\Payment;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class IndexPaymentRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'search' => ['nullable', 'string', 'max:100'], 'status' => ['nullable', 'string', 'max:30'],
'provider_code' => ['nullable', 'string', 'max:50'], 'purpose' => ['nullable', 'string', 'max:30'],
'date_from' => ['nullable', 'date'], 'date_to' => ['nullable', 'date', 'after_or_equal:date_from'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
'sort_by' => ['nullable', Rule::in(['id', 'transaction_number', 'amount', 'status', 'created_at', 'paid_at'])],
'sort_direction' => ['nullable', Rule::in(['asc', 'desc'])],
];
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\Payment;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class SaveTenantPaymentSettingRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
'payment_mode' => ['required', Rule::in(['disabled', 'tenant_gateway', 'platform_gateway'])],
'tenant_gateway_account_id' => ['nullable', 'integer', 'exists:payment_gateway_accounts,id'],
'platform_gateway_account_id' => ['nullable', 'integer', 'exists:payment_gateway_accounts,id'],
'fee_bearer' => ['required', Rule::in(['customer', 'tenant', 'platform'])],
'default_expiry_minutes' => ['required', 'integer', 'min:5', 'max:10080'],
'enabled_methods' => ['nullable', 'array'],
'enabled_methods.*' => ['string', 'max:50'],
];
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests\Payment;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreGatewayAccountRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'payment_provider_id' => ['required', 'integer', 'exists:payment_providers,id'],
'tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
'scope' => ['required', Rule::in(['tenant', 'platform'])],
'name' => ['required', 'string', 'max:150'],
'environment' => ['required', Rule::in(['sandbox', 'production'])],
'credentials' => ['nullable', 'array'],
'credentials.*' => ['nullable', 'string', 'max:4000'],
'webhook_secret' => ['nullable', 'string', 'max:4000'],
'settings' => ['nullable', 'array'],
'enabled' => ['sometimes', 'boolean'],
'is_default' => ['sometimes', 'boolean'],
];
}
}
@@ -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,39 @@
<?php
namespace App\Http\Requests\TicketIncidentType;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class StoreTicketIncidentTypeRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => [
'required',
'string',
'max:255',
'unique:ticket_incident_types,name'
],
'is_active' => [
'sometimes',
'boolean'
]
];
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Http\Requests\TicketIncidentType;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateTicketIncidentTypeRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => [
'required',
'string',
'max:255',
Rule::unique('ticket_incident_types', 'name')
->ignore($this->route('ticketIncidentType'))
],
'is_active' => [
'required',
'boolean'
]
];
}
}
@@ -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'],
];
}
}
@@ -23,10 +23,81 @@ class StoreTicketTypeRequest extends FormRequest
public function rules(): array
{
return [
'code' => 'required|string|max:50|unique:ticket_types,code',
'name' => 'required|string|max:100',
'need_approval' => 'boolean',
'sla_minutes' => 'nullable|integer'
'code' => [
'required',
'string',
'max:50',
'unique:ticket_types,code'
],
'name' => [
'required',
'string',
'max:100'
],
'need_approval' => [
'nullable',
'boolean'
],
'sla_minutes' => [
'nullable',
'integer',
'min:1'
],
'require_photo' => [
'nullable',
'boolean'
],
'require_material' => [
'nullable',
'boolean'
],
'need_customer' => [
'nullable',
'boolean'
],
'need_incident_type' => [
'nullable',
'boolean'
]
];
}
protected function prepareForValidation(): void
{
$this->merge([
'need_approval' => filter_var(
$this->need_approval,
FILTER_VALIDATE_BOOLEAN
),
'require_photo' => filter_var(
$this->require_photo,
FILTER_VALIDATE_BOOLEAN
),
'require_material' => filter_var(
$this->require_material,
FILTER_VALIDATE_BOOLEAN
),
'need_customer' => filter_var(
$this->need_customer,
FILTER_VALIDATE_BOOLEAN
),
'need_incident_type' => filter_var(
$this->need_incident_type,
FILTER_VALIDATE_BOOLEAN
)
]);
}
}
@@ -4,6 +4,7 @@ namespace App\Http\Requests\TicketType;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateTicketTypeRequest extends FormRequest
{
@@ -23,10 +24,82 @@ class UpdateTicketTypeRequest extends FormRequest
public function rules(): array
{
return [
'code' => 'sometimes|string|max:50|unique:ticket_types,code,' . $this->route('ticket_type'),
'name' => 'sometimes|string|max:100',
'need_approval' => 'sometimes|boolean',
'sla_minutes' => 'nullable|integer'
'code' => [
'required',
'string',
'max:50',
Rule::unique('ticket_types', 'code')
->ignore($this->ticket_type->id)
],
'name' => [
'required',
'string',
'max:100'
],
'need_approval' => [
'nullable',
'boolean'
],
'sla_minutes' => [
'nullable',
'integer',
'min:1'
],
'require_photo' => [
'nullable',
'boolean'
],
'require_material' => [
'nullable',
'boolean'
],
'need_customer' => [
'nullable',
'boolean'
],
'need_incident_type' => [
'nullable',
'boolean'
]
];
}
protected function prepareForValidation(): void
{
$this->merge([
'need_approval' => filter_var(
$this->need_approval,
FILTER_VALIDATE_BOOLEAN
),
'require_photo' => filter_var(
$this->require_photo,
FILTER_VALIDATE_BOOLEAN
),
'require_material' => filter_var(
$this->require_material,
FILTER_VALIDATE_BOOLEAN
),
'need_customer' => filter_var(
$this->need_customer,
FILTER_VALIDATE_BOOLEAN
),
'need_incident_type' => filter_var(
$this->need_incident_type,
FILTER_VALIDATE_BOOLEAN
)
]);
}
}
@@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\Topology;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class IndexTopologyRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'search' => ['nullable', 'string', 'max:255'],
'status' => ['nullable', 'string', 'max:30'],
'device_type_id' => ['nullable', 'integer', 'exists:topology_device_types,id'],
'link_type' => ['nullable', Rule::in(['fiber', 'wireless', 'ethernet', 'logical'])],
'bbox' => ['nullable', 'regex:/^-?\d+(\.\d+)?,-?\d+(\.\d+)?,-?\d+(\.\d+)?,-?\d+(\.\d+)?$/'],
'page' => ['nullable', 'integer', 'min:1'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
'sort_by' => ['nullable', Rule::in(['id', 'code', 'name', 'status', 'category', 'link_type', 'installed_at', 'created_at'])],
'sort_direction' => ['nullable', Rule::in(['asc', 'desc'])],
];
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Requests\Topology;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreCustomerConnectionRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'customer_id' => ['required', 'integer', 'exists:customers,id'],
'odp_node_id' => ['required', 'integer', 'exists:topology_nodes,id'],
'odp_port_id' => ['nullable', 'integer', 'exists:topology_ports,id'],
'drop_cable_link_id' => ['nullable', 'integer', 'exists:topology_links,id'],
'connected_at' => ['nullable', 'date'],
'status' => ['nullable', Rule::in(['active', 'inactive'])],
'metadata' => ['nullable', 'array'],
];
}
}
@@ -0,0 +1,89 @@
<?php
namespace App\Http\Requests\Topology;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreTopologyRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return match ($this->route()->defaults['topology_resource'] ?? null) {
'device-type' => [
...$this->tenantRule(),
'code' => ['required', 'string', 'max:50'],
'name' => ['required', 'string', 'max:255'],
'category' => ['required', Rule::in(['site', 'termination', 'closure', 'distribution', 'splitter', 'other'])],
'can_have_ports' => ['nullable', 'boolean'],
'can_have_splitters' => ['nullable', 'boolean'],
'icon' => ['nullable', 'string', 'max:100'],
'color' => ['nullable', 'regex:/^#[0-9a-fA-F]{6}$/'],
'status' => ['nullable', Rule::in(['active', 'inactive'])],
],
'node' => [
...$this->tenantRule(),
'device_type_id' => ['required', 'integer', 'exists:topology_device_types,id'],
'parent_node_id' => ['nullable', 'integer', 'exists:topology_nodes,id'],
'code' => ['required', 'string', 'max:100'],
'name' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string'],
'status' => ['nullable', Rule::in(['active', 'inactive', 'maintenance', 'damaged'])],
'latitude' => ['required', 'numeric', 'between:-90,90'],
'longitude' => ['required', 'numeric', 'between:-180,180'],
'address' => ['nullable', 'string'],
'provinsi_id' => ['nullable', 'integer', 'exists:wilayah_provinsi,id'],
'kabupaten_id' => ['nullable', 'integer', 'exists:wilayah_kabupaten,id'],
'kecamatan_id' => ['nullable', 'integer', 'exists:wilayah_kecamatan,id'],
'desa_id' => ['nullable', 'integer', 'exists:wilayah_desa,id'],
'capacity' => ['nullable', 'integer', 'min:0'],
'installed_at' => ['nullable', 'date'],
'diagram_x' => ['nullable', 'numeric'],
'diagram_y' => ['nullable', 'numeric'],
'metadata' => ['nullable', 'array'],
],
'link' => [
...$this->tenantRule(),
'source_node_id' => ['required', 'integer', 'exists:topology_nodes,id', 'different:target_node_id'],
'source_port_id' => ['nullable', 'integer', 'exists:topology_ports,id'],
'target_node_id' => ['required', 'integer', 'exists:topology_nodes,id'],
'target_port_id' => ['nullable', 'integer', 'exists:topology_ports,id'],
'code' => ['required', 'string', 'max:100'],
'name' => ['required', 'string', 'max:255'],
'link_type' => ['required', Rule::in(['fiber', 'wireless', 'ethernet', 'logical'])],
'fiber_type' => ['nullable', 'string', 'max:50'],
'core_count' => ['nullable', 'integer', 'min:1'],
'cable_length' => ['nullable', 'numeric', 'min:0'],
'measured_length' => ['nullable', 'numeric', 'min:0'],
'path_geojson' => ['nullable', 'array'],
'path_geojson.type' => ['required_with:path_geojson', Rule::in(['LineString'])],
'path_geojson.coordinates' => ['required_with:path_geojson', 'array', 'min:2'],
'path_geojson.coordinates.*' => ['array', 'size:2'],
'path_geojson.coordinates.*.*' => ['numeric'],
'status' => ['nullable', Rule::in(['active', 'inactive', 'maintenance', 'damaged'])],
'installed_at' => ['nullable', 'date'],
'metadata' => ['nullable', 'array'],
],
'port' => [
'name' => ['required', 'string', 'max:100'],
'port_number' => ['required', 'integer', 'min:1'],
'port_type' => ['required', Rule::in(['fiber', 'ethernet', 'pon', 'uplink', 'splitter', 'other'])],
'direction' => ['required', Rule::in(['input', 'output', 'bidirectional'])],
'capacity' => ['nullable', 'integer', 'min:1'],
'status' => ['nullable', Rule::in(['available', 'used', 'reserved', 'damaged'])],
'metadata' => ['nullable', 'array'],
],
default => [],
};
}
private function tenantRule(): array
{
return ['tenant_id' => ['nullable', 'integer', 'exists:tenants,id']];
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Http\Requests\Topology;
class UpdateTopologyRequest extends StoreTopologyRequest
{
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,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,22 @@
<?php
namespace App\Http\Requests\Wallet;
use Illuminate\Foundation\Http\FormRequest;
class StoreWithdrawalRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'amount' => ['required', 'numeric', 'min:10000'], 'fee' => ['nullable', 'numeric', 'min:0'],
'bank_code' => ['required', 'string', 'max:30'], 'account_number' => ['required', 'string', 'max:50'],
'account_name' => ['required', 'string', 'max:150'],
];
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Http\Requests\Wallet;
use Illuminate\Foundation\Http\FormRequest;
class TransferWalletRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return ['destination_wallet_id' => ['required', 'integer', 'exists:wallets,id'], 'amount' => ['required', 'numeric', 'min:1']];
}
}
@@ -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',
],
];
}
}

Some files were not shown because too many files have changed in this diff Show More