forked from admin/services_core
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e7575ba1c0 | |||
| 75342dc7b7 | |||
| b0d08211ec | |||
| f68d967c8b | |||
| 1dd02baa72 | |||
| 2b3592c4c2 | |||
| c574ada5c1 | |||
| b0f3f03b73 | |||
| af1c98b38b | |||
| ad472a9880 | |||
| faab64e0dd |
@@ -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
|
||||
@@ -70,3 +77,5 @@ AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
REGISTRATION_SHOW_VERIFICATION_CODE=true
|
||||
|
||||
+17
-1
@@ -1,8 +1,13 @@
|
||||
APP_NAME=API Tiket
|
||||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
APP_TIMEZONE=Asia/Jakarta
|
||||
|
||||
NAS_HEALTH_QUEUE=nas-health
|
||||
NAS_HEALTH_ONLINE_INTERVAL=5
|
||||
NAS_HEALTH_OFFLINE_MAX_INTERVAL=30
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
@@ -62,4 +67,15 @@ 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}"
|
||||
|
||||
# Sementara aktifkan selama channel pengiriman kode registrasi belum tersedia.
|
||||
REGISTRATION_SHOW_VERIFICATION_CODE=true
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/vendor
|
||||
.env
|
||||
# .env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpactor.json
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# TODO - Menu Group Available Menus & Restriction
|
||||
|
||||
- [x] Add service method to fetch allowed menu IDs for authenticated user
|
||||
- [x] Add service method to provide available menu list for a target menu group based on authenticated user access
|
||||
- [x] Restrict `syncMenus` so assigned menu IDs must be subset of authenticated user allowed menus (except master user)
|
||||
- [x] Add controller endpoint for `GET /menu-groups/{menu_group}/available-menus`
|
||||
- [x] Register route for `available-menus` endpoint in `routes/api.php`
|
||||
- [ ] Run API testing (critical/thorough as requested)
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\NasMikrotik;
|
||||
use App\Jobs\CheckMikrotikConnection;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CheckMikrotikConnections extends Command
|
||||
{
|
||||
protected $signature = 'nas:check-mikrotiks {--id=* : ID Mikrotik tertentu}';
|
||||
|
||||
protected $description = 'Mengantrekan pemeriksaan koneksi NAS yang sudah jatuh tempo';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$queued = 0;
|
||||
NasMikrotik::query()
|
||||
->where('status', 'active')
|
||||
->where(fn ($query) => $query->whereNull('next_health_check_at')->orWhere('next_health_check_at', '<=', now()))
|
||||
->when($this->option('id'), fn ($query) => $query->whereIn('id', $this->option('id')))
|
||||
->select(['id'])
|
||||
->chunkById(100, function ($devices) use (&$queued) {
|
||||
foreach ($devices as $index => $device) {
|
||||
CheckMikrotikConnection::dispatch($device->id)->delay(now()->addSeconds($index % 60));
|
||||
$queued++;
|
||||
}
|
||||
});
|
||||
$this->info("{$queued} pemeriksaan NAS masuk antrean nas-health.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Access;
|
||||
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Http\Requests\Access\ReviewRoleApplicationRequest;
|
||||
use App\Http\Requests\Access\StoreRoleApplicationRequest;
|
||||
use App\Models\RoleApplication;
|
||||
use App\Services\Access\RoleApplicationService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class RoleApplicationController extends ApiController
|
||||
{
|
||||
public function __construct(protected RoleApplicationService $service) {}
|
||||
|
||||
public function mine(Request $request)
|
||||
{
|
||||
return $this->success($this->service->mine($request->user()), 'Daftar pengajuan berhasil diambil');
|
||||
}
|
||||
|
||||
public function store(StoreRoleApplicationRequest $request)
|
||||
{
|
||||
return $this->created($this->service->submit($request->validated(), $request->user()), 'Pengajuan berhasil dikirim');
|
||||
}
|
||||
|
||||
public function reviewList(Request $request, string $type)
|
||||
{
|
||||
$filters = $request->validate(['search' => ['nullable', 'string', 'max:100'], 'status' => ['nullable', 'in:pending,approved,rejected'], 'per_page' => ['nullable', 'integer', 'min:1', 'max:100']]);
|
||||
|
||||
return $this->success($this->service->reviewList($type, $filters, $request->user(), $this->tenantId($request)), 'Daftar pengajuan berhasil diambil');
|
||||
}
|
||||
|
||||
public function tenantMenuGroupOptions(Request $request)
|
||||
{
|
||||
return $this->success(
|
||||
$this->service->tenantMenuGroupOptions($request->user()),
|
||||
'Pilihan Group Menu tenant berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function staffMenuGroupOptions(Request $request)
|
||||
{
|
||||
return $this->success(
|
||||
$this->service->staffMenuGroupOptions($request->user(), $this->tenantId($request)),
|
||||
'Pilihan Group Menu staff berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function review(ReviewRoleApplicationRequest $request, RoleApplication $application)
|
||||
{
|
||||
return $this->updated($this->service->review($application, $request->validated(), $request->user(), $this->tenantId($request)), 'Pengajuan berhasil ditinjau');
|
||||
}
|
||||
|
||||
public function linkCustomer(Request $request)
|
||||
{
|
||||
$data = $request->validate(['customer_id' => ['required', 'integer', 'exists:customers,id'], 'user_id' => ['required', 'integer', 'exists:users,id'], 'relationship' => ['nullable', 'in:owner,family,staff'], 'is_primary' => ['nullable', 'boolean']]);
|
||||
|
||||
return $this->created($this->service->linkCustomer($data, $request->user(), (int) $this->tenantId($request)), 'User berhasil dihubungkan dengan customer');
|
||||
}
|
||||
|
||||
private function tenantId(Request $request): ?int
|
||||
{
|
||||
return $request->attributes->get('tenant_id');
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,213 @@
|
||||
<?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;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
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 odpPorts(Request $request, int $node)
|
||||
{
|
||||
$data=$request->validate(['customer_id'=>['nullable','integer','exists:customers,id']]);
|
||||
return $this->success($this->service->availableOdpPorts($node,$request->user(),$this->tenantId($request),$data['customer_id'] ?? null),'Port ODP tersedia berhasil diambil');
|
||||
}
|
||||
|
||||
public function ocrKtp(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'image' => ['required', 'image', 'mimes:jpeg,jpg,png,webp', 'max:5120'],
|
||||
'tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
|
||||
]);
|
||||
$tenantId = $request->user()->isMasterAdmin()
|
||||
? ($data['tenant_id'] ?? $request->attributes->get('tenant_id'))
|
||||
: $request->attributes->get('tenant_id');
|
||||
abort_unless($tenantId, 422, 'Tenant wajib dipilih sebelum menjalankan OCR.');
|
||||
if (! $request->user()->isMasterAdmin()) {
|
||||
abort_unless($request->user()->userTenants()->where('tenant_id', $tenantId)->exists(), 403);
|
||||
}
|
||||
$setting = \App\Models\TenantInstallationSetting::where('tenant_id', $tenantId)->first();
|
||||
abort_unless($setting?->ktp_ocr_enabled, 422, 'OCR KTP belum diaktifkan untuk tenant ini.');
|
||||
abort_unless($setting->ocr_provider && $setting->ocr_api_key, 422, 'Provider atau API key OCR belum dikonfigurasi.');
|
||||
|
||||
$file = $data['image'];
|
||||
$mime = $file->getMimeType();
|
||||
$base64 = base64_encode(file_get_contents($file->getRealPath()));
|
||||
$prompt = 'Baca KTP Indonesia ini. Kembalikan hanya JSON valid tanpa markdown dengan field: nik, nama, alamat, rt_rw, kelurahan_desa, kecamatan, kabupaten_kota, provinsi. Gunakan null bila tidak terbaca.';
|
||||
|
||||
if ($setting->ocr_provider === 'openai') {
|
||||
$response = Http::withToken($setting->ocr_api_key)->timeout(60)->post('https://api.openai.com/v1/chat/completions', [
|
||||
'model' => 'gpt-4.1-mini',
|
||||
'response_format' => ['type' => 'json_object'],
|
||||
'messages' => [['role' => 'user', 'content' => [
|
||||
['type' => 'text', 'text' => $prompt],
|
||||
['type' => 'image_url', 'image_url' => ['url' => "data:{$mime};base64,{$base64}"]],
|
||||
]]],
|
||||
]);
|
||||
$content = $response->successful() ? $response->json('choices.0.message.content') : null;
|
||||
} else {
|
||||
$response = Http::timeout(60)->post('https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key='.urlencode($setting->ocr_api_key), [
|
||||
'contents' => [['parts' => [['text' => $prompt], ['inline_data' => ['mime_type' => $mime, 'data' => $base64]]]]],
|
||||
'generationConfig' => ['responseMimeType' => 'application/json'],
|
||||
]);
|
||||
$content = $response->successful() ? $response->json('candidates.0.content.parts.0.text') : null;
|
||||
}
|
||||
if (! $response->successful() || ! $content) {
|
||||
throw ValidationException::withMessages(['image' => 'Provider AI gagal membaca KTP. Periksa API key, kuota, dan kualitas gambar.']);
|
||||
}
|
||||
$result = json_decode(trim(str_replace(['```json', '```'], '', $content)), true);
|
||||
if (! is_array($result)) throw ValidationException::withMessages(['image' => 'Respons OCR tidak dapat diproses.']);
|
||||
|
||||
return $this->success($result, 'KTP berhasil dibaca');
|
||||
}
|
||||
|
||||
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 prepareActivation(UpdateCustomerRequest $request, int $id)
|
||||
{
|
||||
$customer = $this->service->find($id);
|
||||
$this->authorizeTenant($request, $customer->tenant_id);
|
||||
$mode = \App\Models\TenantInstallationSetting::firstOrCreate(
|
||||
['tenant_id' => $customer->tenant_id],
|
||||
['activation_mode' => 'list_order']
|
||||
)->activation_mode;
|
||||
abort_unless($mode === 'list_order', 422, 'Tenant mengatur persiapan aktivasi melalui Ticket.');
|
||||
|
||||
$customer = $this->service->update($customer, $request->validated(), $request->user());
|
||||
$customer = $this->service->ensureActivationCode($customer);
|
||||
return $this->updated(
|
||||
new CustomerResource($customer),
|
||||
'Persiapan aktivasi berhasil disimpan'
|
||||
);
|
||||
}
|
||||
|
||||
public function updateRegistration(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 registrasi berhasil disimpan'
|
||||
);
|
||||
}
|
||||
|
||||
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(), $request->user())), '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 cancelOrder(Request $request, int $id)
|
||||
{
|
||||
$customer = $this->service->find($id);
|
||||
$this->authorizeTenant($request, $customer->tenant_id);
|
||||
abort_unless($customer->status === 'order', 422, 'Hanya customer dalam List Order yang dapat dihapus melalui aksi ini.');
|
||||
$this->service->trash($customer);
|
||||
|
||||
return $this->deleted('List Order 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,177 @@
|
||||
<?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');
|
||||
$tenantRole = $user?->tenantRole($tenantId);
|
||||
|
||||
if (! $user) {
|
||||
return $this->error('Unauthenticated', 401);
|
||||
}
|
||||
|
||||
if ($user->isMasterAdmin() || $user->is_platform_staff) {
|
||||
$menus = MenuList::query()
|
||||
->where('is_active', true)
|
||||
->when($user->is_platform_staff && ! $user->isMasterAdmin(), fn ($q) => $q->whereIn('slug', [
|
||||
'role-applications', 'role-applications-tenants', 'nas', 'nas-script-policies',
|
||||
]))
|
||||
->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_activate' => 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_activate THEN 1 ELSE 0 END) as can_activate'),
|
||||
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) use ($tenantRole) {
|
||||
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_activate' => $tenantRole === 'tenant_owner' || (bool) $row->can_activate,
|
||||
'can_export' => (bool) $row->can_export,
|
||||
],
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
$tree = $this->buildMenuTree($menus);
|
||||
|
||||
if (! $user->isMasterAdmin() && ! $user->is_platform_staff && ! in_array($tenantRole, ['staff', 'tenant_owner'], true)) {
|
||||
$restrictedSlugs = ['voucher-hotspot-agents', 'voucher-hotspot-login-page'];
|
||||
$tree = $this->removeRestrictedMenus($tree, $restrictedSlugs);
|
||||
}
|
||||
if (! $user->isMasterAdmin() && ! $user->is_platform_staff) {
|
||||
$tree = $this->removeRestrictedMenus($tree, ['role-applications-tenants', 'nas-script-policies', 'nas-radius-infrastructure']);
|
||||
}
|
||||
if (! $user->isTenantOwner($tenantId)) {
|
||||
$tree = $this->removeRestrictedMenus($tree, ['role-applications-staff']);
|
||||
}
|
||||
|
||||
return $this->success(
|
||||
[
|
||||
'is_master' => (bool) $user->is_master,
|
||||
'access_level' => $user->access_level->value,
|
||||
'tenant_role' => $tenantRole,
|
||||
'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();
|
||||
}
|
||||
|
||||
private function removeRestrictedMenus(array $menus, array $restrictedSlugs): array
|
||||
{
|
||||
return collect($menus)
|
||||
->reject(fn ($menu) => in_array($menu['slug'], $restrictedSlugs, true))
|
||||
->map(function ($menu) use ($restrictedSlugs) {
|
||||
$menu['children'] = $this->removeRestrictedMenus($menu['children'] ?? [], $restrictedSlugs);
|
||||
|
||||
return $menu;
|
||||
})
|
||||
->values()->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,88 @@
|
||||
<?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 App\Services\Nas\MikrotikHealthService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class NasResourceController extends ApiController
|
||||
{
|
||||
public function __construct(
|
||||
protected NasResourceService $service,
|
||||
protected MikrotikHealthService $mikrotikHealth,
|
||||
) {}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
public function testConnection(Request $request, int $id)
|
||||
{
|
||||
abort_unless($this->resource($request) === 'mikrotik', 404);
|
||||
$mikrotik = $this->service->find('mikrotik', $id);
|
||||
$this->authorizeTenant($request, $mikrotik->tenant_id);
|
||||
|
||||
return $this->success(
|
||||
$this->mikrotikHealth->check($mikrotik),
|
||||
'Koneksi RouterOS API berhasil'
|
||||
);
|
||||
}
|
||||
|
||||
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,19 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers\Nas;
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Models\NasPackageProfile;
|
||||
use App\Models\NasPackageProfileTarget;
|
||||
use App\Services\Nas\PackageProfileDeploymentService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use App\Models\NasScriptPolicy;
|
||||
|
||||
class PackageProfileDeploymentController extends ApiController
|
||||
{
|
||||
public function __construct(protected PackageProfileDeploymentService $service) {}
|
||||
public function index(Request $request, NasPackageProfile $profile) { $this->authorizeProfile($request,$profile); return $this->success($this->service->targets($profile),'Target deployment berhasil diambil'); }
|
||||
public function store(Request $request, NasPackageProfile $profile) { $this->authorizeProfile($request,$profile); $data=$request->validate(['nas_mikrotik_id'=>['required','integer','exists:nas_mikrotiks,id'],'external_profile_name'=>['required','string','max:255']]); return $this->created($this->service->saveTarget($profile,$data),'Target deployment berhasil disimpan'); }
|
||||
public function sync(Request $request, NasPackageProfileTarget $target) { $this->authorizeProfile($request,$target->packageProfile); $data=$request->validate(['mode'=>['required',Rule::in(['create','update','adopt'])]]); return $this->updated($this->service->sync($target,$data['mode']),'Profile berhasil disinkronkan'); }
|
||||
public function scriptOptions() { return $this->success(NasScriptPolicy::where('enabled',true)->with('latestVersion:id,nas_script_policy_id,version')->orderBy('service_type')->orderBy('event')->get(['id','code','name','service_type','event','current_version']),'Pilihan Script Policy berhasil diambil'); }
|
||||
private function authorizeProfile(Request $request, NasPackageProfile $profile): void { abort_unless($request->user()->isMasterAdmin() || (int)$profile->tenant_id === (int)$request->attributes->get('tenant_id'),403); }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Nas;
|
||||
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Models\RadiusCluster;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class RadiusClusterController extends ApiController
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->authorizeMaster($request);
|
||||
$data = $request->validate(['search'=>['nullable','string','max:255'],'status'=>['nullable',Rule::in(['planned','active','maintenance','inactive'])],'per_page'=>['nullable','integer','between:1,100']]);
|
||||
$rows = RadiusCluster::query()->when($data['search'] ?? null, fn ($q,$search) => $q->where(fn ($x) => $x->where('name','ILIKE',"%{$search}%")->orWhere('code','ILIKE',"%{$search}%")->orWhere('region','ILIKE',"%{$search}%")))->when($data['status'] ?? null, fn ($q,$status) => $q->where('status',$status))->orderBy('name')->paginate($data['per_page'] ?? 10);
|
||||
$rows->through(fn ($cluster) => $this->present($cluster));
|
||||
return $this->success($rows, 'Cluster RADIUS berhasil diambil');
|
||||
}
|
||||
|
||||
public function store(Request $request) { $this->authorizeMaster($request); return $this->created($this->present(RadiusCluster::create($this->validated($request))), 'Cluster RADIUS berhasil dibuat'); }
|
||||
public function show(Request $request, RadiusCluster $radiusCluster) { $this->authorizeMaster($request); return $this->success($this->present($radiusCluster), 'Detail cluster RADIUS berhasil diambil'); }
|
||||
public function update(Request $request, RadiusCluster $radiusCluster)
|
||||
{
|
||||
$this->authorizeMaster($request); $data=$this->validated($request,$radiusCluster);
|
||||
if (blank($data['provisioning_api_token'] ?? null)) unset($data['provisioning_api_token']);
|
||||
$radiusCluster->update($data); return $this->updated($this->present($radiusCluster->fresh()), 'Cluster RADIUS berhasil diperbarui');
|
||||
}
|
||||
public function destroy(Request $request, RadiusCluster $radiusCluster) { $this->authorizeMaster($request); $radiusCluster->delete(); return $this->deleted('Cluster RADIUS berhasil dihapus'); }
|
||||
|
||||
private function validated(Request $request, ?RadiusCluster $cluster=null): array
|
||||
{
|
||||
return $request->validate([
|
||||
'code'=>['required','alpha_dash','max:80',Rule::unique('radius_clusters','code')->ignore($cluster)], 'name'=>['required','string','max:255'], 'region'=>['nullable','string','max:80'],
|
||||
'auth_endpoints'=>['required','array','min:1','max:4'], 'auth_endpoints.*.host'=>['required','string','max:255'], 'auth_endpoints.*.port'=>['required','integer','between:1,65535'], 'auth_endpoints.*.priority'=>['required','integer','between:1,100'],
|
||||
'accounting_endpoints'=>['required','array','min:1','max:4'], 'accounting_endpoints.*.host'=>['required','string','max:255'], 'accounting_endpoints.*.port'=>['required','integer','between:1,65535'], 'accounting_endpoints.*.priority'=>['required','integer','between:1,100'],
|
||||
'coa_endpoints'=>['nullable','array','max:4'], 'coa_endpoints.*.host'=>['required','string','max:255'], 'coa_endpoints.*.port'=>['required','integer','between:1,65535'], 'coa_endpoints.*.priority'=>['required','integer','between:1,100'],
|
||||
'provisioning_api_url'=>['nullable','url:http,https','max:2000'], 'provisioning_api_token'=>['nullable','string','max:4000'], 'healthcheck_url'=>['nullable','url:http,https','max:2000'],
|
||||
'request_timeout'=>['required','integer','between:1,60'], 'retry_attempts'=>['required','integer','between:0,10'], 'status'=>['required',Rule::in(['planned','active','maintenance','inactive'])], 'notes'=>['nullable','string','max:5000'],
|
||||
]);
|
||||
}
|
||||
private function present(RadiusCluster $cluster): array { $data=$cluster->attributesToArray(); unset($data['provisioning_api_token']); return [...$data,'has_provisioning_api_token'=>filled($cluster->getRawOriginal('provisioning_api_token'))]; }
|
||||
private function authorizeMaster(Request $request): void { abort_unless($request->user()->isMasterAdmin(),403,'Infrastruktur RADIUS hanya dapat dikelola Master Admin.'); }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers\Nas;
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Models\NasScriptPolicy;
|
||||
use App\Services\Nas\ScriptPolicyService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ScriptPolicyController extends ApiController
|
||||
{
|
||||
public function __construct(protected ScriptPolicyService $service) {}
|
||||
public function index(Request $request) { $this->authorizePlatform($request); $data=$request->validate(['search'=>['nullable','string','max:255'],'per_page'=>['nullable','integer','between:1,100']]); return $this->success($this->service->list($data),'Script Policy berhasil diambil'); }
|
||||
public function store(Request $request) { $this->authorizePlatform($request); return $this->created($this->service->save($this->validated($request),$request->user()->id),'Script Policy berhasil dibuat'); }
|
||||
public function update(Request $request, NasScriptPolicy $policy) { $this->authorizePlatform($request); return $this->updated($this->service->save($this->validated($request,$policy),$request->user()->id,$policy),'Versi Script Policy berhasil dibuat'); }
|
||||
public function show(Request $request, NasScriptPolicy $policy) { $this->authorizePlatform($request); return $this->success($policy->load(['latestVersion','versions'=>fn($q)=>$q->latest('version')]),'Detail Script Policy berhasil diambil'); }
|
||||
private function validated(Request $request, ?NasScriptPolicy $policy=null): array { return $request->validate(['code'=>['required','alpha_dash','max:100',Rule::unique('nas_script_policies','code')->ignore($policy)],'name'=>['required','string','max:255'],'service_type'=>['required',Rule::in(['ppp','hotspot'])],'event'=>['required',Rule::in(['on_up','on_down','on_login','on_logout'])],'script_template'=>['required','string','max:65535'],'change_notes'=>['nullable','string','max:1000'],'enabled'=>['nullable','boolean']]); }
|
||||
private function authorizePlatform(Request $request): void { abort_unless($request->user()->isMasterAdmin() || $request->user()->is_platform_staff,403,'Script Policy hanya dapat dikelola staff platform.'); }
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Nas;
|
||||
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\TenantNasSetting;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class TenantNasSettingController extends ApiController
|
||||
{
|
||||
public function show(Request $request)
|
||||
{
|
||||
$tenant = $this->tenant($request);
|
||||
$setting = TenantNasSetting::firstOrCreate(['tenant_id' => $tenant->id], ['backend_mode' => 'routeros_api']);
|
||||
return $this->success($this->present($setting->load('radiusCluster:id,code,name,region,status')), 'Pengaturan backend NAS berhasil diambil');
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
$tenant = $this->tenant($request);
|
||||
$setting = TenantNasSetting::firstOrCreate(['tenant_id' => $tenant->id], ['backend_mode' => 'routeros_api']);
|
||||
$data = $request->validate([
|
||||
'tenant_id' => ['nullable','integer','exists:tenants,id'],
|
||||
'backend_mode' => ['required', Rule::in(['routeros_api','platform_radius','external_radius'])],
|
||||
'radius_cluster_id' => ['nullable','integer','exists:radius_clusters,id'],
|
||||
'external_radius_host' => ['nullable','string','max:255'],
|
||||
'external_auth_port' => ['nullable','integer','between:1,65535'],
|
||||
'external_accounting_port' => ['nullable','integer','between:1,65535'],
|
||||
'external_coa_port' => ['nullable','integer','between:1,65535'],
|
||||
'external_radius_secret' => ['nullable','string','max:1000'],
|
||||
'external_provisioning_api_url' => ['nullable','url:http,https','max:2000'],
|
||||
'external_provisioning_api_token' => ['nullable','string','max:2000'],
|
||||
'accounting_enabled' => ['nullable','boolean'],
|
||||
'coa_enabled' => ['nullable','boolean'],
|
||||
]);
|
||||
$usesExternal = $data['backend_mode'] === 'external_radius';
|
||||
if ($usesExternal && empty($data['external_radius_host'])) throw ValidationException::withMessages(['external_radius_host'=>'Host RADIUS eksternal wajib diisi.']);
|
||||
$data['hybrid_radius_source'] = null;
|
||||
foreach (['external_radius_secret','external_provisioning_api_token'] as $secret) if (blank($data[$secret] ?? null)) unset($data[$secret]);
|
||||
unset($data['tenant_id']);
|
||||
$setting->update($data);
|
||||
return $this->updated($this->present($setting->fresh()->load('radiusCluster:id,code,name,region,status')), 'Pengaturan backend NAS berhasil disimpan');
|
||||
}
|
||||
|
||||
private function tenant(Request $request): Tenant
|
||||
{
|
||||
$id = $request->user()->isMasterAdmin() ? ($request->integer('tenant_id') ?: $request->attributes->get('tenant_id')) : $request->attributes->get('tenant_id');
|
||||
abort_unless($id, 422, 'Tenant wajib dipilih.');
|
||||
return Tenant::findOrFail($id);
|
||||
}
|
||||
|
||||
private function present(TenantNasSetting $setting): array
|
||||
{
|
||||
return [...$setting->attributesToArray(), 'has_external_radius_secret'=>filled($setting->getRawOriginal('external_radius_secret')), 'has_external_provisioning_api_token'=>filled($setting->getRawOriginal('external_provisioning_api_token')), 'radius_cluster'=>$setting->radiusCluster];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
<?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\Requests\Notification\StoreSystemNotificationRequest;
|
||||
use App\Http\Resources\Notification\NotificationDeliveryResource;
|
||||
use App\Http\Resources\Notification\SystemNotificationCampaignResource;
|
||||
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 App\Services\Notification\WhapiIdService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class NotificationController extends ApiController
|
||||
{
|
||||
public function __construct(protected NotificationService $service, protected WhapiIdService $whapi) {}
|
||||
|
||||
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 systemCampaigns(IndexNotificationRequest $request)
|
||||
{
|
||||
return $this->successPaginated(
|
||||
$this->service->systemCampaigns($request->validated(), $request->user(), $this->tenantId($request)),
|
||||
SystemNotificationCampaignResource::class,
|
||||
'Riwayat notifikasi System berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function storeSystemCampaign(StoreSystemNotificationRequest $request)
|
||||
{
|
||||
return $this->created(
|
||||
$this->service->createSystemCampaign($request->validated(), $request->user(), $this->tenantId($request)),
|
||||
'Notifikasi System masuk antrean'
|
||||
);
|
||||
}
|
||||
|
||||
public function systemAudienceOptions(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
|
||||
'search' => ['nullable', 'string', 'max:100'],
|
||||
]);
|
||||
|
||||
return $this->success(
|
||||
$this->service->systemAudienceOptions(
|
||||
$request->user(), $this->tenantId($request), $data['tenant_id'] ?? null, $data['search'] ?? null
|
||||
),
|
||||
'Pilihan penerima notifikasi berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function storeChannel(StoreNotificationChannelRequest $request)
|
||||
{
|
||||
abort_unless($request->validated('channel') === $this->channel($request), 422);
|
||||
$this->authorizeChannelMutation($request);
|
||||
|
||||
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);
|
||||
$this->authorizeChannelMutation($request);
|
||||
|
||||
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);
|
||||
$this->authorizeChannelMutation($request);
|
||||
|
||||
return $this->success($this->service->testChannel($notificationChannel), 'Pengujian channel selesai');
|
||||
}
|
||||
|
||||
public function destroyChannel(Request $request, NotificationChannel $notificationChannel)
|
||||
{
|
||||
$this->authorizeTenant($request, $notificationChannel->tenant_id);
|
||||
abort_unless($notificationChannel->channel === $this->channel($request), 404);
|
||||
$this->authorizeChannelMutation($request);
|
||||
$this->service->deleteChannel($notificationChannel);
|
||||
|
||||
return $this->deleted('Konfigurasi channel berhasil dinonaktifkan atau dihapus');
|
||||
}
|
||||
|
||||
public function whapiState(Request $request, NotificationChannel $notificationChannel)
|
||||
{
|
||||
$this->authorizeWhapiAccess($request, $notificationChannel);
|
||||
|
||||
return $this->success($this->whapi->state($notificationChannel), 'Status perangkat WHAPI berhasil diambil');
|
||||
}
|
||||
|
||||
public function whapiStart(Request $request, NotificationChannel $notificationChannel)
|
||||
{
|
||||
$this->authorizeWhapiAccess($request, $notificationChannel);
|
||||
|
||||
return $this->success($this->whapi->start($notificationChannel), 'Layanan WHAPI berhasil dijalankan');
|
||||
}
|
||||
|
||||
public function whapiQr(Request $request, NotificationChannel $notificationChannel)
|
||||
{
|
||||
$this->authorizeWhapiAccess($request, $notificationChannel);
|
||||
|
||||
return $this->success($this->whapi->qr($notificationChannel), 'QR WHAPI berhasil diambil');
|
||||
}
|
||||
|
||||
public function whapiTestMessage(Request $request, NotificationChannel $notificationChannel)
|
||||
{
|
||||
$this->authorizeWhapiAccess($request, $notificationChannel);
|
||||
$data = $request->validate([
|
||||
'phone' => ['required', 'string', 'max:25', 'regex:/^\+?[0-9][0-9\s\-()]{7,24}$/'],
|
||||
'message' => ['required', 'string', 'max:4000'],
|
||||
]);
|
||||
|
||||
return $this->success(
|
||||
$this->whapi->send($notificationChannel, $data['phone'], $data['message']),
|
||||
'Pesan uji WHAPI berhasil dikirim'
|
||||
);
|
||||
}
|
||||
|
||||
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 !== null && $tenantId === $this->tenantId($request)), 403);
|
||||
}
|
||||
|
||||
private function authorizeChannelMutation(Request $request): void
|
||||
{
|
||||
if ($this->channel($request) === 'whatsapp_unofficial') {
|
||||
abort_unless($request->user()->isMasterAdmin(), 403, 'WhatsApp Unofficial hanya dapat dikonfigurasi oleh Master Admin.');
|
||||
}
|
||||
}
|
||||
|
||||
private function authorizeWhapiAccess(Request $request, NotificationChannel $channel): void
|
||||
{
|
||||
abort_unless($channel->channel === 'whatsapp_unofficial' && $channel->provider === 'whapi_id', 404);
|
||||
abort_unless($request->user()->isMasterAdmin()
|
||||
|| ((int) $channel->tenant_id === (int) $this->tenantId($request)
|
||||
&& $request->user()->isTenantOwner($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,135 @@
|
||||
<?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;
|
||||
use App\Models\TenantInstallationSetting;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
public function installationSetting(Request $request, Tenant $tenant)
|
||||
{
|
||||
$this->authorizeTenantAccess($request, $tenant);
|
||||
$setting = TenantInstallationSetting::firstOrCreate(['tenant_id' => $tenant->id], ['activation_mode' => 'list_order']);
|
||||
|
||||
return $this->success([
|
||||
...$setting->toArray(),
|
||||
'tenant' => new TenantResource($tenant),
|
||||
'ocr_api_key_configured' => filled($setting->ocr_api_key),
|
||||
'can_manage_ocr' => $request->user()->isMasterAdmin(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateInstallationSetting(Request $request, Tenant $tenant)
|
||||
{
|
||||
$this->authorizeTenantAccess($request, $tenant);
|
||||
$rules = ['activation_mode' => ['required', Rule::in(['list_order', 'ticket'])]];
|
||||
if ($request->user()->isMasterAdmin()) {
|
||||
$rules += [
|
||||
'ktp_ocr_enabled' => ['required', 'boolean'],
|
||||
'ocr_provider' => ['nullable', 'required_if:ktp_ocr_enabled,true', Rule::in(['gemini', 'openai'])],
|
||||
'ocr_api_key' => ['nullable', 'string', 'max:4096'],
|
||||
'clear_ocr_api_key' => ['nullable', 'boolean'],
|
||||
];
|
||||
}
|
||||
$data = $request->validate($rules);
|
||||
$setting = TenantInstallationSetting::firstOrCreate(['tenant_id' => $tenant->id], ['activation_mode' => 'list_order']);
|
||||
if ($request->user()->isMasterAdmin()) {
|
||||
if (! empty($data['clear_ocr_api_key'])) $data['ocr_api_key'] = null;
|
||||
elseif (blank($data['ocr_api_key'] ?? null)) unset($data['ocr_api_key']);
|
||||
unset($data['clear_ocr_api_key']);
|
||||
}
|
||||
$setting->update($data);
|
||||
|
||||
return $this->updated([
|
||||
...$setting->fresh()->toArray(),
|
||||
'ocr_api_key_configured' => filled($setting->fresh()->ocr_api_key),
|
||||
'can_manage_ocr' => $request->user()->isMasterAdmin(),
|
||||
], 'Setting tenant berhasil disimpan');
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,16 @@
|
||||
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 App\Models\Customer;
|
||||
use App\Models\TicketType;
|
||||
use App\Models\User;
|
||||
use App\Models\TicketMaterial;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TicketController extends ApiController
|
||||
@@ -16,19 +21,54 @@ 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();
|
||||
$tenantId = $request->attributes->get('tenant_id');
|
||||
$sortBy = $filters['sort_by'] ?? 'created_at';
|
||||
$sortDirection = $filters['sort_direction'] ?? 'desc';
|
||||
$tickets = Ticket::query()->with(['ticketType:id,code,name,need_approval,require_photo,require_material','customer:id,customer_code,name','tenant:id,tenant_name','creator:id,name','assignments.user:id,name'])
|
||||
->when(! $request->user()->isMasterAdmin(), fn ($query) => $query->where('tenant_id', $tenantId))
|
||||
->when($request->user()->isMasterAdmin() && $tenantId, fn ($query) => $query->where('tenant_id', $tenantId))
|
||||
->when(! empty($filters['assigned_to_me']), fn ($query) => $query->whereHas('assignments', fn ($assignment) => $assignment->where('user_id', $request->user()->id)))
|
||||
->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'])
|
||||
)
|
||||
->orderBy($sortBy, $sortDirection)
|
||||
->paginate($filters['per_page'] ?? 10);
|
||||
|
||||
return $this->success($tickets);
|
||||
return $this->successPaginated(
|
||||
$tickets,
|
||||
TicketResource::class,
|
||||
'Data Ticket berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function store(StoreTicketRequest $request)
|
||||
{
|
||||
$ticket = $this->service->create(
|
||||
$request->validated()
|
||||
$request->validated(), $request->user(), $request->attributes->get('tenant_id')
|
||||
);
|
||||
|
||||
return $this->created(
|
||||
@@ -37,10 +77,11 @@ class TicketController extends ApiController
|
||||
);
|
||||
}
|
||||
|
||||
public function show(Ticket $ticket)
|
||||
public function show(Request $request, Ticket $ticket)
|
||||
{
|
||||
$this->authorizeTenant($request, $ticket);
|
||||
return $this->success(
|
||||
new TicketResource($ticket)
|
||||
new TicketResource($ticket->load(['ticketType','customer','tenant','creator','assignments.user','logs','materials','attachments']))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,6 +89,7 @@ class TicketController extends ApiController
|
||||
UpdateTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$this->authorizeTenant($request, $ticket);
|
||||
$ticket = $this->service->update(
|
||||
$ticket,
|
||||
$request->validated()
|
||||
@@ -59,12 +101,72 @@ class TicketController extends ApiController
|
||||
);
|
||||
}
|
||||
|
||||
public function destroy(Ticket $ticket)
|
||||
public function destroy(Request $request, Ticket $ticket)
|
||||
{
|
||||
$this->authorizeTenant($request, $ticket);
|
||||
$this->service->delete($ticket);
|
||||
|
||||
return $this->deleted(
|
||||
'Ticket berhasil dibatalkan'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function options(Request $request)
|
||||
{
|
||||
$tenantId = $request->attributes->get('tenant_id');
|
||||
return $this->success([
|
||||
'ticket_types' => TicketType::orderBy('name')->get(['id','code','name','need_approval','require_photo','require_material']),
|
||||
'customers' => Customer::when(! $request->user()->isMasterAdmin() || $tenantId, fn ($q) => $q->where('tenant_id', $tenantId))->orderBy('name')->limit(500)->get(['id','customer_code','name','tenant_id']),
|
||||
'technicians' => User::whereHas('userTenants', fn ($q) => $q->where('tenant_id', $tenantId))->orderBy('name')->get(['id','name','email']),
|
||||
'installation_setting' => $tenantId
|
||||
? \App\Models\TenantInstallationSetting::firstOrCreate(['tenant_id' => $tenantId], ['activation_mode' => 'list_order'])
|
||||
: null,
|
||||
'package_profiles'=>\App\Models\NasPackageProfile::where('tenant_id',$tenantId)->where('status','active')->orderBy('name')->get(['id','name']),
|
||||
'nas_mikrotiks'=>\App\Models\NasMikrotik::where('tenant_id',$tenantId)->where('status','active')->orderBy('name')->get(['id','name']),
|
||||
'billing_profiles'=>\App\Models\BillingProfile::where('tenant_id',$tenantId)->where('status','active')->orderBy('name')->get(['id','name']),
|
||||
'topology_odps'=>\App\Models\TopologyNode::where('tenant_id',$tenantId)->whereHas('deviceType',fn($q)=>$q->whereRaw('UPPER(code) = ?',['ODP']))->orderBy('name')->get(['id','code','name']),
|
||||
], 'Pilihan ticket berhasil diambil');
|
||||
}
|
||||
|
||||
public function storeMaterial(Request $request, Ticket $ticket)
|
||||
{
|
||||
$this->authorizeTenant($request,$ticket);
|
||||
$data=$request->validate(['technician_material_id'=>['nullable','required_without:technician_consumable_id','integer','exists:technician_materials,id'],'technician_consumable_id'=>['nullable','required_without:technician_material_id','integer','exists:technician_consumables,id'],'qty'=>['required','numeric','gt:0'],'notes'=>['nullable','string','max:1000']]);
|
||||
abort_if(!empty($data['technician_material_id'])&&!empty($data['technician_consumable_id']),422,'Pilih satu material saja.');
|
||||
$material=\Illuminate\Support\Facades\DB::transaction(function()use($data,$ticket,$request){
|
||||
if(!empty($data['technician_material_id'])){$source=\App\Models\TechnicianMaterial::whereKey($data['technician_material_id'])->where('user_id',$request->user()->id)->where('status','assigned')->lockForUpdate()->firstOrFail();$qty=1;$source->update(['status'=>'used']);$name=$source->material_name;}
|
||||
else{$source=\App\Models\TechnicianConsumable::whereKey($data['technician_consumable_id'])->where('user_id',$request->user()->id)->where('status','assigned')->lockForUpdate()->firstOrFail();$qty=(float)$data['qty'];abort_if((float)$source->qty_remaining<$qty,422,'Jumlah material yang dibawa tidak mencukupi.');$source->update(['qty_used'=>(float)$source->qty_used+$qty,'qty_remaining'=>(float)$source->qty_remaining-$qty]);$name=$source->material_name;}
|
||||
\App\Models\MaterialMovement::create(['barcode_id'=>$source->barcode_id,'user_id'=>$request->user()->id,'from_user_id'=>$request->user()->id,'movement_type'=>'use','ticket_id'=>$ticket->id,'qty'=>$qty,'description'=>$data['notes']??null,'created_by'=>$request->user()->id,'created_at'=>now()]);
|
||||
$warehouseBarcode=\App\Models\ItemBarcode::where('barcode',$source->barcode_id)->first();
|
||||
if($warehouseBarcode)\App\Models\ItemMovement::create(['tenant_id'=>$ticket->tenant_id,'warehouse_item_id'=>$warehouseBarcode->warehouse_item_id,'item_barcode_id'=>$warehouseBarcode->id,'movement_type'=>'used_on_ticket','qty'=>$qty,'reference_type'=>'ticket','reference_id'=>$ticket->id,'from_user_id'=>$request->user()->id,'notes'=>$data['notes']??null,'created_by'=>$request->user()->id]);
|
||||
return $ticket->materials()->create([...$data,'material_name'=>$name,'qty'=>$qty,'used_by'=>$request->user()->id,'created_at'=>now()]);
|
||||
});
|
||||
\App\Models\TicketLog::create(['ticket_id'=>$ticket->id,'user_id'=>$request->user()->id,'activity'=>'Material Digunakan','notes'=>$material->material_name.' x '.$material->qty,'created_at'=>now()]);
|
||||
return $this->created($material->load('user:id,name'),'Material ticket berhasil ditambahkan');
|
||||
}
|
||||
|
||||
public function materialOptions(Request $request, Ticket $ticket)
|
||||
{
|
||||
$this->authorizeTenant($request,$ticket);$userId=$request->user()->id;
|
||||
return $this->success(['serialized'=>\App\Models\TechnicianMaterial::where('user_id',$userId)->where('status','assigned')->get(['id','barcode_id','material_name','serial_number']),'consumables'=>\App\Models\TechnicianConsumable::where('user_id',$userId)->where('status','assigned')->where('qty_remaining','>',0)->get(['id','barcode_id','material_name','unit','qty_remaining'])],'Material yang dibawa berhasil diambil');
|
||||
}
|
||||
public function installationPorts(Request $request,Ticket $ticket,int $node,\App\Services\Customer\CustomerService $customers){$this->authorizeTenant($request,$ticket);return $this->success($customers->availableOdpPorts($node,$request->user(),$ticket->tenant_id,$ticket->customer_id),'Port ODP berhasil diambil');}
|
||||
|
||||
public function destroyMaterial(Request $request, Ticket $ticket, TicketMaterial $material)
|
||||
{
|
||||
$this->authorizeTenant($request,$ticket);abort_unless((int)$material->ticket_id===(int)$ticket->id,404);
|
||||
abort_if(in_array($ticket->status,['resolved','closed'],true),422,'Material ticket yang sudah selesai tidak dapat diubah.');
|
||||
\Illuminate\Support\Facades\DB::transaction(function()use($request,$ticket,$material){
|
||||
if($material->technician_material_id){$source=\App\Models\TechnicianMaterial::lockForUpdate()->find($material->technician_material_id);if($source)$source->update(['status'=>'assigned']);}
|
||||
else{$source=\App\Models\TechnicianConsumable::lockForUpdate()->find($material->technician_consumable_id);if($source)$source->update(['qty_used'=>max(0,(float)$source->qty_used-(float)$material->qty),'qty_remaining'=>(float)$source->qty_remaining+(float)$material->qty,'status'=>'assigned']);}
|
||||
if(isset($source)&&$source){\App\Models\MaterialMovement::create(['barcode_id'=>$source->barcode_id,'user_id'=>$request->user()->id,'to_user_id'=>$request->user()->id,'movement_type'=>'return','ticket_id'=>$ticket->id,'qty'=>$material->qty,'description'=>'Pembatalan pemakaian material pada ticket','created_by'=>$request->user()->id,'created_at'=>now()]);$warehouseBarcode=\App\Models\ItemBarcode::where('barcode',$source->barcode_id)->first();if($warehouseBarcode)\App\Models\ItemMovement::create(['tenant_id'=>$ticket->tenant_id,'warehouse_item_id'=>$warehouseBarcode->warehouse_item_id,'item_barcode_id'=>$warehouseBarcode->id,'movement_type'=>'ticket_usage_cancelled','qty'=>$material->qty,'reference_type'=>'ticket','reference_id'=>$ticket->id,'to_user_id'=>$request->user()->id,'notes'=>'Material dikembalikan ke saldo teknisi','created_by'=>$request->user()->id]);}
|
||||
$material->delete();
|
||||
});
|
||||
return $this->deleted('Material ticket berhasil dihapus dan dikembalikan ke daftar bawaan user');
|
||||
}
|
||||
|
||||
private function authorizeTenant(Request $request, Ticket $ticket): void
|
||||
{
|
||||
abort_unless($request->user()->isMasterAdmin() || (int) $ticket->tenant_id === (int) $request->attributes->get('tenant_id'), 403);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ use App\Http\Requests\Ticket\CloseTicketRequest;
|
||||
use App\Http\Requests\Ticket\CancelTicketRequest;
|
||||
use App\Http\Requests\Ticket\EscalateTicketRequest;
|
||||
use App\Http\Requests\Ticket\StartTicketRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class TicketWorkflowController extends ApiController
|
||||
{
|
||||
@@ -29,6 +31,7 @@ class TicketWorkflowController extends ApiController
|
||||
*/
|
||||
public function history(Ticket $ticket)
|
||||
{
|
||||
$this->authorizeTicket($ticket);
|
||||
$data = $this->service->history($ticket);
|
||||
|
||||
return $this->success(
|
||||
@@ -44,6 +47,7 @@ class TicketWorkflowController extends ApiController
|
||||
AssignTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$this->authorizeTicket($ticket);
|
||||
$ticket = $this->service->assign(
|
||||
$ticket,
|
||||
$request->technicians,
|
||||
@@ -64,6 +68,7 @@ class TicketWorkflowController extends ApiController
|
||||
ReassignTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$this->authorizeTicket($ticket);
|
||||
$ticket = $this->service->reassign(
|
||||
$ticket,
|
||||
$request->technicians,
|
||||
@@ -84,6 +89,7 @@ class TicketWorkflowController extends ApiController
|
||||
ApproveTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$this->authorizeTicket($ticket);
|
||||
$ticket = $this->service->approve(
|
||||
$ticket,
|
||||
$request->notes,
|
||||
@@ -103,6 +109,7 @@ class TicketWorkflowController extends ApiController
|
||||
RejectTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$this->authorizeTicket($ticket);
|
||||
$ticket = $this->service->reject(
|
||||
$ticket,
|
||||
$request->reason,
|
||||
@@ -122,6 +129,7 @@ class TicketWorkflowController extends ApiController
|
||||
StartTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$this->authorizeTicket($ticket);
|
||||
$ticket = $this->service->start(
|
||||
$ticket,
|
||||
$request->validated(),
|
||||
@@ -141,10 +149,12 @@ class TicketWorkflowController extends ApiController
|
||||
PendingTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$this->authorizeTicket($ticket);
|
||||
$ticket = $this->service->pending(
|
||||
$ticket,
|
||||
$request->reason,
|
||||
auth()->id() ?? 1
|
||||
auth()->id() ?? 1,
|
||||
$request->validated('stored_file_ids')
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
@@ -158,6 +168,7 @@ class TicketWorkflowController extends ApiController
|
||||
*/
|
||||
public function resume(Ticket $ticket)
|
||||
{
|
||||
$this->authorizeTicket($ticket);
|
||||
$ticket = $this->service->resume(
|
||||
$ticket,
|
||||
auth()->id() ?? 1
|
||||
@@ -176,10 +187,12 @@ class TicketWorkflowController extends ApiController
|
||||
ResolveTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$this->authorizeTicket($ticket);
|
||||
$ticket = $this->service->resolve(
|
||||
$ticket,
|
||||
$request->notes,
|
||||
auth()->id() ?? 1
|
||||
auth()->id() ?? 1,
|
||||
$request->validated()
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
@@ -195,6 +208,7 @@ class TicketWorkflowController extends ApiController
|
||||
CloseTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$this->authorizeTicket($ticket);
|
||||
$ticket = $this->service->close(
|
||||
$ticket,
|
||||
$request->notes,
|
||||
@@ -207,6 +221,42 @@ class TicketWorkflowController extends ApiController
|
||||
);
|
||||
}
|
||||
|
||||
public function verify(Request $request, Ticket $ticket)
|
||||
{
|
||||
$this->authorizeTicket($ticket);
|
||||
abort_unless($ticket->status === 'resolved', 422, 'Hanya ticket selesai yang dapat diverifikasi.');
|
||||
$data=$request->validate(['notes'=>['required','string','max:2000']]);
|
||||
$ticket->update(['verified_by'=>$request->user()->id,'verified_at'=>now(),'verification_notes'=>$data['notes']]);
|
||||
\App\Models\TicketLog::create(['ticket_id'=>$ticket->id,'user_id'=>$request->user()->id,'activity'=>'Verified','notes'=>$data['notes'],'created_at'=>now()]);
|
||||
return $this->updated(new TicketResource($ticket->fresh()),'Ticket berhasil diverifikasi');
|
||||
}
|
||||
|
||||
public function installationStep(Request $request, Ticket $ticket, \App\Services\Customer\CustomerService $customers)
|
||||
{
|
||||
$this->authorizeTicket($ticket);
|
||||
abort_unless($ticket->source === 'new_installation' && $ticket->customer_id,422,'Ticket bukan pemasangan baru.');
|
||||
abort_unless(in_array($ticket->status,['progress','pending'],true),422,'Pekerjaan harus dimulai terlebih dahulu.');
|
||||
$step=$request->validate(['step'=>['required',Rule::in(['documentation','service','activation'])]])['step'];
|
||||
$expected=$ticket->installation_step ?: 'documentation'; abort_unless($step===$expected,422,"Tahap yang harus dikerjakan adalah {$expected}.");
|
||||
$customer=$customers->find($ticket->customer_id);
|
||||
if($step==='documentation'){
|
||||
$data=$request->validate(['stored_file_ids'=>['required','array','min:1'],'stored_file_ids.*'=>['integer','exists:stored_files,id'],'topology_id'=>['required','integer','exists:topology_nodes,id'],'topology_port_number'=>['required','integer','min:1']]);
|
||||
$images=collect($data['stored_file_ids'])->map(fn($id)=>['stored_file_id'=>$id,'category'=>'installation','is_cover'=>false])->all();
|
||||
$customers->update($customer,['tenant_id'=>$customer->tenant_id,'topology_id'=>$data['topology_id'],'topology_port_number'=>$data['topology_port_number'],'images'=>$images],$request->user());
|
||||
foreach($data['stored_file_ids'] as $id)\App\Models\TicketAttachment::firstOrCreate(['ticket_id'=>$ticket->id,'stored_file_id'=>$id,'stage'=>'installation_documentation'],['uploaded_by'=>$request->user()->id]);
|
||||
$next='service';
|
||||
}elseif($step==='service'){
|
||||
$data=$request->validate(['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']]);
|
||||
$customers->update($customer,['tenant_id'=>$customer->tenant_id,...$data],$request->user()); $next='activation';
|
||||
}else{
|
||||
$data=$request->validate(['package_profile_id'=>['required','integer'],'nas_mikrotik_id'=>['required','integer'],'billing_profile_id'=>['required','integer'],'external_username'=>['required','string','max:255'],'external_password'=>['required','string','min:4'],'serial_number'=>['nullable','string','max:120'],'mac_address'=>['nullable','regex:/^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/']]);
|
||||
$customers->activate($customer,['activation_source'=>'ticket',...$data],$request->user()); $next='completed';
|
||||
}
|
||||
$ticket->update(['installation_step'=>$next]);
|
||||
\App\Models\TicketLog::create(['ticket_id'=>$ticket->id,'user_id'=>$request->user()->id,'activity'=>'Tahap Pemasangan Selesai','notes'=>$step.' → '.$next,'created_at'=>now()]);
|
||||
return $this->updated(new TicketResource($ticket->fresh()->load(['customer','attachments'])),'Tahap pemasangan berhasil disimpan');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel
|
||||
*/
|
||||
@@ -214,6 +264,7 @@ class TicketWorkflowController extends ApiController
|
||||
CancelTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$this->authorizeTicket($ticket);
|
||||
$ticket = $this->service->cancel(
|
||||
$ticket,
|
||||
$request->reason,
|
||||
@@ -233,6 +284,7 @@ class TicketWorkflowController extends ApiController
|
||||
EscalateTicketRequest $request,
|
||||
Ticket $ticket
|
||||
) {
|
||||
$this->authorizeTicket($ticket);
|
||||
$ticket = $this->service->escalate(
|
||||
$ticket,
|
||||
$request->reason,
|
||||
@@ -244,4 +296,11 @@ class TicketWorkflowController extends ApiController
|
||||
'Ticket berhasil dieskalasi'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function authorizeTicket(Ticket $ticket): void
|
||||
{
|
||||
$user = auth()->user();
|
||||
$tenantId = request()->attributes->get('tenant_id');
|
||||
abort_unless($user?->isMasterAdmin() || (int) $ticket->tenant_id === (int) $tenantId, 403);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Voucher;
|
||||
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Http\Requests\Voucher\IndexVoucherRequest;
|
||||
use App\Http\Requests\Voucher\SaveVoucherLoginPageRequest;
|
||||
use App\Http\Requests\Voucher\StoreVoucherAgentRequest;
|
||||
use App\Http\Requests\Voucher\StoreVoucherSaleRequest;
|
||||
use App\Http\Resources\Voucher\VoucherAgentResource;
|
||||
use App\Http\Resources\Voucher\VoucherSaleResource;
|
||||
use App\Models\VoucherAgent;
|
||||
use App\Models\VoucherSale;
|
||||
use App\Services\Voucher\VoucherService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class VoucherController extends ApiController
|
||||
{
|
||||
public function __construct(protected VoucherService $service) {}
|
||||
|
||||
public function sales(IndexVoucherRequest $request)
|
||||
{
|
||||
return $this->successPaginated($this->service->sales($request->validated(), $request->user(), $this->tenantId($request)), VoucherSaleResource::class, 'Daftar penjualan voucher berhasil diambil');
|
||||
}
|
||||
|
||||
public function storeSale(StoreVoucherSaleRequest $request)
|
||||
{
|
||||
return $this->created(new VoucherSaleResource($this->service->createSale($request->validated(), $request->user(), $this->tenantId($request))), 'Voucher berhasil dibuat');
|
||||
}
|
||||
|
||||
public function showSale(Request $request, VoucherSale $sale)
|
||||
{
|
||||
return $this->success(new VoucherSaleResource($this->service->saleDetail($sale, $request->user(), $this->tenantId($request))), 'Detail voucher berhasil diambil');
|
||||
}
|
||||
|
||||
public function agents(IndexVoucherRequest $request)
|
||||
{
|
||||
return $this->successPaginated($this->service->agents($request->validated(), $request->user(), $this->tenantId($request)), VoucherAgentResource::class, 'Daftar agen voucher berhasil diambil');
|
||||
}
|
||||
|
||||
public function storeAgent(StoreVoucherAgentRequest $request)
|
||||
{
|
||||
return $this->created(new VoucherAgentResource($this->service->saveAgent($request->validated(), $request->user(), $this->tenantId($request))), 'Agen voucher berhasil dibuat');
|
||||
}
|
||||
|
||||
public function updateAgent(StoreVoucherAgentRequest $request, VoucherAgent $agent)
|
||||
{
|
||||
return $this->updated(new VoucherAgentResource($this->service->saveAgent($request->validated(), $request->user(), $this->tenantId($request), $agent)), 'Agen voucher berhasil diperbarui');
|
||||
}
|
||||
|
||||
public function destroyAgent(Request $request, VoucherAgent $agent)
|
||||
{
|
||||
$this->service->saveAgent([...$agent->toArray(), 'status' => 'inactive'], $request->user(), $this->tenantId($request), $agent);
|
||||
|
||||
return $this->deleted('Agen voucher berhasil dinonaktifkan');
|
||||
}
|
||||
|
||||
public function options(Request $request)
|
||||
{
|
||||
return $this->success($this->service->options((string) $request->route('voucher_scope'), $request->user(), $this->tenantId($request)), 'Opsi voucher berhasil diambil');
|
||||
}
|
||||
|
||||
public function loginPage(Request $request)
|
||||
{
|
||||
return $this->success($this->service->loginPage($request->user(), $this->tenantId($request)), 'Setting LoginPage berhasil diambil');
|
||||
}
|
||||
|
||||
public function saveLoginPage(SaveVoucherLoginPageRequest $request)
|
||||
{
|
||||
return $this->updated($this->service->saveLoginPage($request->validated(), $request->user(), $this->tenantId($request)), 'Setting LoginPage berhasil disimpan');
|
||||
}
|
||||
|
||||
private function tenantId(Request $request): ?int
|
||||
{
|
||||
return $request->attributes->get('tenant_id');
|
||||
}
|
||||
}
|
||||
@@ -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,33 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers\Warehouse;
|
||||
use App\Http\Controllers\ApiController;use App\Models\{ItemBarcode,ItemCode,ItemMovement,ItemStockLog,WarehouseItem};use Illuminate\Http\Request;use Illuminate\Support\Facades\DB;use Illuminate\Support\Str;use Illuminate\Validation\Rule;
|
||||
class WarehouseBatchController extends ApiController
|
||||
{
|
||||
public function index(Request $r){$t=$r->attributes->get('tenant_id');$status=$r->input('status','active');$q=WarehouseItem::with(['warehouse:id,code,name','itemCode','barcodes'])->when(!$r->user()->isMasterAdmin()||$t,fn($x)=>$x->where('tenant_id',$t))->where('status',$status)->when($r->search,fn($x,$s)=>$x->where(fn($y)=>$y->where('item_number','ilike',"%$s%")->orWhereHas('itemCode',fn($z)=>$z->where('name','ilike',"%$s%")->orWhere('code','ilike',"%$s%"))))->orderByDesc('id')->paginate(min((int)$r->input('per_page',10),100));return $this->successPaginated($q,\Illuminate\Http\Resources\Json\JsonResource::class,'Daftar barang berhasil diambil');}
|
||||
public function store(Request $r)
|
||||
{
|
||||
$t=$r->user()->isMasterAdmin()?($r->input('tenant_id')?:$r->attributes->get('tenant_id')):$r->attributes->get('tenant_id');
|
||||
$d=$r->validate(['tenant_id'=>['nullable','integer','exists:tenants,id'],'warehouse_id'=>['required','integer','exists:warehouses,id'],'item_code_id'=>['required','integer','exists:item_codes,id'],'name'=>['required','string','max:255'],'quantity'=>['required','numeric','gt:0'],'package_count'=>['nullable','integer','min:1'],'received_at'=>['required','date'],'notes'=>['nullable','string']]);
|
||||
$code=ItemCode::whereKey($d['item_code_id'])->where('tenant_id',$t)->firstOrFail();
|
||||
abort_unless(\App\Models\Warehouse::whereKey($d['warehouse_id'])->where('tenant_id',$t)->exists(),422,'Gudang bukan milik tenant aktif.');
|
||||
return DB::transaction(function()use($r,$d,$t,$code){
|
||||
$base=$code->code.'-'.now()->format('dmy');
|
||||
DB::select('SELECT pg_advisory_xact_lock(hashtext(?))',[$t.'|'.$base]);
|
||||
$seq=WarehouseItem::where('tenant_id',$t)->where('item_number','like',$base.'%')->count()+1;
|
||||
do{$number=$base.'-'.str_pad((string)$seq++,3,'0',STR_PAD_LEFT);}while(WarehouseItem::where('tenant_id',$t)->where('item_number',$number)->exists());
|
||||
$item=WarehouseItem::create(['tenant_id'=>$t,'warehouse_id'=>$d['warehouse_id'],'item_code_id'=>$code->id,'item_number'=>$number,'status'=>'draft','sku'=>$code->code,'name'=>$d['name'],'tracking_type'=>$code->tracking_type==='serialized'?'serialized':'bulk','unit'=>$code->unit,'minimum_stock'=>$code->minimum_stock,'received_qty'=>$d['quantity'],'received_at'=>$d['received_at'],'notes'=>$d['notes']??null,'is_active'=>false]);
|
||||
$count=$code->tracking_type==='serialized'?(int)$d['quantity']:max(1,(int)($d['package_count']??1));
|
||||
abort_if($code->tracking_type==='serialized'&&(float)$d['quantity']!==floor((float)$d['quantity']),422,'Barang serialized harus berjumlah bilangan bulat.');
|
||||
$remaining=(float)$d['quantity'];
|
||||
for($i=1;$i<=$count;$i++){$qty=$code->tracking_type==='serialized'?1:($i===$count?$remaining:round((float)$d['quantity']/$count,2));$remaining-=$qty;ItemBarcode::create(['tenant_id'=>$t,'warehouse_item_id'=>$item->id,'package_number'=>$i,'barcode'=>$this->barcode($code->code,$t),'tracking_type'=>$code->tracking_type,'unit'=>$code->unit,'initial_qty'=>$qty,'remaining_qty'=>$qty,'status'=>'draft']);}
|
||||
return $this->created($item->load(['warehouse','itemCode','barcodes']),'Barang draft dan barcode berhasil dibuat');
|
||||
});
|
||||
}
|
||||
public function show(Request $r,WarehouseItem $item){$this->auth($r,$item);return $this->success($item->load(['warehouse','itemCode','barcodes','movements']),'Detail barang berhasil diambil');}
|
||||
public function updateSerials(Request $r,WarehouseItem $item){$this->auth($r,$item);abort_unless($item->status==='draft',422,'Serial number hanya dapat diubah pada draft.');$d=$r->validate(['barcodes'=>['required','array','min:1'],'barcodes.*.id'=>['required','integer'],'barcodes.*.serial_number'=>['nullable','string','max:120']]);DB::transaction(function()use($d,$item){foreach($d['barcodes'] as $row){$barcode=$item->barcodes()->findOrFail($row['id']);$serial=$row['serial_number']?:null;if($serial&&ItemBarcode::where('tenant_id',$item->tenant_id)->where('serial_number',$serial)->whereKeyNot($barcode->id)->exists())abort(422,"Serial number {$serial} sudah digunakan.");$barcode->update(['serial_number'=>$serial]);}});return $this->updated($item->fresh()->load('barcodes'),'Serial number berhasil diperbarui');}
|
||||
public function post(Request $r,WarehouseItem $item){$this->auth($r,$item);abort_unless($item->status==='draft',422,'Hanya barang draft yang dapat diposting.');$item->load(['itemCode','barcodes']);abort_if($item->barcodes->isEmpty(),422,'Barcode belum tersedia.');if($item->itemCode->need_serial_number)abort_if($item->barcodes->contains(fn($b)=>blank($b->serial_number)),422,'Semua serial number wajib diisi sebelum posting.');return DB::transaction(function()use($r,$item){$item->barcodes()->update(['status'=>'available']);$item->update(['status'=>'active','is_active'=>true,'posted_by'=>$r->user()->id,'posted_at'=>now()]);$move=ItemMovement::create(['tenant_id'=>$item->tenant_id,'warehouse_item_id'=>$item->id,'movement_type'=>'stock_posted','qty'=>$item->received_qty,'notes'=>'Draft diposting menjadi stok aktif','created_by'=>$r->user()->id]);ItemStockLog::create(['tenant_id'=>$item->tenant_id,'warehouse_item_id'=>$item->id,'item_movement_id'=>$move->id,'qty_before'=>0,'qty_change'=>$item->received_qty,'qty_after'=>$item->received_qty]);return $this->updated($item->fresh()->load(['warehouse','itemCode','barcodes']),'Barang berhasil diposting ke Daftar Barang');});}
|
||||
public function destroy(Request $r,WarehouseItem $item){$this->auth($r,$item);abort_unless($item->status==='draft',422,'Hanya barang draft yang dapat dihapus.');$item->delete();return $this->deleted('Draft barang dan barcode berhasil dihapus');}
|
||||
public function barcodeMovements(Request $r,WarehouseItem $item,ItemBarcode $barcode){$this->auth($r,$item);abort_unless((int)$barcode->warehouse_item_id===(int)$item->id,404);return $this->success(ItemMovement::where('item_barcode_id',$barcode->id)->latest()->get(),'Histori barcode berhasil diambil');}
|
||||
private function barcode(string $code,int $tenant):string{do{$random='';$chars='ABCDEFGHJKLMNPQRSTUVWXYZ23456789';for($i=0;$i<6;$i++)$random.=$chars[random_int(0,strlen($chars)-1)];$value=$code.'-'.now()->format('dmy').'-'.$random;}while(ItemBarcode::where('barcode',$value)->exists());return $value;}
|
||||
private function auth(Request $r,WarehouseItem $i){abort_unless($r->user()->isMasterAdmin()||(int)$i->tenant_id===(int)$r->attributes->get('tenant_id'),403);}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers\Warehouse;
|
||||
use App\Http\Controllers\ApiController;use App\Models\{WarehouseItem,ItemBarcode,ItemCode,ItemMovement,ItemStockLog};use Illuminate\Http\Request;use Illuminate\Support\Facades\DB;use Illuminate\Validation\Rule;
|
||||
class WarehouseController extends ApiController
|
||||
{
|
||||
public function index(Request $r){$t=$r->attributes->get('tenant_id');$q=WarehouseItem::with(['itemCode','barcodes'])->when(!$r->user()->isMasterAdmin()||$t,fn($x)=>$x->where('tenant_id',$t))->when($r->search,fn($x,$s)=>$x->whereHas('itemCode',fn($y)=>$y->where('name','ilike',"%$s%")->orWhere('code','ilike',"%$s%")))->orderBy($r->input('sort_by','id'),$r->input('sort_direction','desc'))->paginate(min((int)$r->input('per_page',10),100));return $this->successPaginated($q,\Illuminate\Http\Resources\Json\JsonResource::class,'Daftar barang berhasil diambil');}
|
||||
public function store(Request $r){$d=$this->validateItem($r);$d['tenant_id']=$r->user()->isMasterAdmin()?($d['tenant_id']??$r->attributes->get('tenant_id')):$r->attributes->get('tenant_id');return $this->created(WarehouseItem::create($d),'Barang berhasil dibuat');}
|
||||
public function update(Request $r,WarehouseItem $item){$this->auth($r,$item);$item->update($this->validateItem($r,$item));return $this->updated($item->fresh(),'Barang berhasil diperbarui');}
|
||||
public function destroy(Request $r,WarehouseItem $item){$this->auth($r,$item);abort_if($item->barcodes()->where('remaining_qty','>',0)->exists(),422,'Barang yang masih memiliki stok tidak dapat dihapus.');$item->delete();return $this->deleted('Barang berhasil dihapus');}
|
||||
public function receive(Request $r,WarehouseItem $item){$this->auth($r,$item);$item->loadMissing('itemCode');$type=$item->itemCode?->tracking_type??($item->tracking_type==='serialized'?'serialized':'quantity');$needSerial=(bool)($item->itemCode?->need_serial_number);$d=$r->validate(['barcode'=>['required','string','max:120','unique:item_barcodes,barcode'],'serial_number'=>[$needSerial?'required':'nullable','string','max:120'],'qty'=>['required','numeric','gt:0'],'notes'=>['nullable','string']]);if($type==='serialized')$d['qty']=1;return DB::transaction(function()use($r,$item,$d,$type){$before=(float)$item->barcodes()->sum('remaining_qty');$barcode=ItemBarcode::create(['tenant_id'=>$item->tenant_id,'warehouse_item_id'=>$item->id,'barcode'=>$d['barcode'],'tracking_type'=>$type,'unit'=>$item->itemCode?->unit??$item->unit,'serial_number'=>$d['serial_number']??null,'initial_qty'=>$d['qty'],'remaining_qty'=>$d['qty'],'status'=>'available']);$move=ItemMovement::create(['tenant_id'=>$item->tenant_id,'warehouse_item_id'=>$item->id,'item_barcode_id'=>$barcode->id,'movement_type'=>'stock_in','qty'=>$d['qty'],'notes'=>$d['notes']??null,'created_by'=>$r->user()->id]);ItemStockLog::create(['tenant_id'=>$item->tenant_id,'warehouse_item_id'=>$item->id,'item_barcode_id'=>$barcode->id,'item_movement_id'=>$move->id,'qty_before'=>$before,'qty_change'=>$d['qty'],'qty_after'=>$before+$d['qty'],'barcode_qty_before'=>0,'barcode_qty_change'=>$d['qty'],'barcode_qty_after'=>$d['qty']]);return $this->created($barcode,'Stok masuk berhasil dicatat');});}
|
||||
public function options(Request $r){$t=$r->attributes->get('tenant_id');$users=\App\Models\UserTenant::with('user:id,name')->where('tenant_id',$t)->get()->pluck('user')->filter()->unique('id')->values();$codes=ItemCode::where('tenant_id',$t)->where('is_active',true)->orderBy('name')->get();return $this->success(['users'=>$users,'item_codes'=>$codes],'Pilihan gudang berhasil diambil');}
|
||||
public function issue(Request $r,ItemBarcode $barcode){$item=$barcode->item;$this->auth($r,$item);$d=$r->validate(['user_id'=>['required','integer','exists:users,id'],'qty'=>['required','numeric','gt:0'],'notes'=>['nullable','string','max:1000']]);abort_unless(\App\Models\UserTenant::where('tenant_id',$item->tenant_id)->where('user_id',$d['user_id'])->exists(),422,'Penerima bukan user tenant ini.');return DB::transaction(function()use($r,$item,$barcode,$d){$locked=ItemBarcode::whereKey($barcode->id)->lockForUpdate()->firstOrFail();$barcodeBefore=(float)$locked->remaining_qty;$serialized=($locked->tracking_type??$item->tracking_type)==='serialized';$qty=$serialized?1:(float)$d['qty'];abort_if($barcodeBefore<$qty,422,'Stok barcode tidak mencukupi.');$before=(float)$item->barcodes()->sum('remaining_qty');if($serialized){abort_if(\App\Models\TechnicianMaterial::where('barcode_id',$locked->barcode)->exists(),422,'Barcode sudah pernah dikeluarkan.');\App\Models\TechnicianMaterial::create(['user_id'=>$d['user_id'],'barcode_id'=>$locked->barcode,'inventory_item_id'=>$item->id,'material_name'=>$item->name,'serial_number'=>$locked->serial_number,'status'=>'assigned','assigned_at'=>now(),'assigned_by'=>$r->user()->id]);}else{abort_if(\App\Models\TechnicianConsumable::where('barcode_id',$locked->barcode)->exists(),422,'Satu barcode quantity hanya dapat diserahkan kepada satu user sampai dikembalikan.');\App\Models\TechnicianConsumable::create(['user_id'=>$d['user_id'],'barcode_id'=>$locked->barcode,'inventory_item_id'=>$item->id,'material_name'=>$item->name,'unit'=>$locked->unit??$item->unit,'qty_received'=>$qty,'qty_used'=>0,'qty_remaining'=>$qty,'status'=>'assigned','assigned_at'=>now(),'assigned_by'=>$r->user()->id]);}$barcodeAfter=$barcodeBefore-$qty;$locked->update(['remaining_qty'=>$barcodeAfter,'status'=>$barcodeAfter>0?'available':'issued']);$move=ItemMovement::create(['tenant_id'=>$item->tenant_id,'warehouse_item_id'=>$item->id,'item_barcode_id'=>$locked->id,'movement_type'=>'issue_to_technician','qty'=>$qty,'to_user_id'=>$d['user_id'],'notes'=>$d['notes']??null,'created_by'=>$r->user()->id]);ItemStockLog::create(['tenant_id'=>$item->tenant_id,'warehouse_item_id'=>$item->id,'item_barcode_id'=>$locked->id,'item_movement_id'=>$move->id,'qty_before'=>$before,'qty_change'=>-$qty,'qty_after'=>$before-$qty,'barcode_qty_before'=>$barcodeBefore,'barcode_qty_change'=>-$qty,'barcode_qty_after'=>$barcodeAfter]);return $this->created($move,'Barang berhasil dikeluarkan kepada user');});}
|
||||
public function movements(Request $r){$t=$r->attributes->get('tenant_id');return $this->successPaginated(ItemMovement::when(!$r->user()->isMasterAdmin()||$t,fn($q)=>$q->where('tenant_id',$t))->latest()->paginate(min((int)$r->input('per_page',20),100)),\Illuminate\Http\Resources\Json\JsonResource::class,'Pergerakan barang berhasil diambil');}
|
||||
public function technicianInventory(Request $r){$t=$r->attributes->get('tenant_id');$userIds=\App\Models\UserTenant::where('tenant_id',$t)->pluck('user_id');$serialized=\App\Models\TechnicianMaterial::with('user:id,name')->whereIn('user_id',$userIds)->where('status','assigned')->get()->map(fn($x)=>['id'=>'s-'.$x->id,'user'=>$x->user?->name,'material_name'=>$x->material_name,'barcode'=>$x->barcode_id,'tracking_type'=>'serialized','qty'=>1,'unit'=>'pcs','status'=>$x->status,'assigned_at'=>$x->assigned_at]);$quantity=\App\Models\TechnicianConsumable::with('user:id,name')->whereIn('user_id',$userIds)->where('status','assigned')->where('qty_remaining','>',0)->get()->map(fn($x)=>['id'=>'q-'.$x->id,'user'=>$x->user?->name,'material_name'=>$x->material_name,'barcode'=>$x->barcode_id,'tracking_type'=>'quantity','qty'=>$x->qty_remaining,'unit'=>$x->unit,'status'=>$x->status,'assigned_at'=>$x->assigned_at]);$all=$serialized->concat($quantity)->sortByDesc('assigned_at')->values();$page=max(1,(int)$r->input('page',1));$per=min(max(1,(int)$r->input('per_page',10)),100);$p=new \Illuminate\Pagination\LengthAwarePaginator($all->forPage($page,$per)->values(),$all->count(),$per,$page,['path'=>$r->url(),'query'=>$r->query()]);return $this->successPaginated($p,\Illuminate\Http\Resources\Json\JsonResource::class,'Peralatan teknisi berhasil diambil');}
|
||||
public function returnFromTechnician(Request $r){$d=$r->validate(['source_type'=>['required',Rule::in(['serialized','quantity'])],'source_id'=>['required','integer'],'qty'=>['required','numeric','gt:0'],'notes'=>['nullable','string','max:1000']]);$t=$r->attributes->get('tenant_id');return DB::transaction(function()use($r,$d,$t){$source=$d['source_type']==='serialized'?\App\Models\TechnicianMaterial::lockForUpdate()->findOrFail($d['source_id']):\App\Models\TechnicianConsumable::lockForUpdate()->findOrFail($d['source_id']);abort_unless(\App\Models\UserTenant::where('tenant_id',$t)->where('user_id',$source->user_id)->exists(),403);$barcode=ItemBarcode::where('barcode',$source->barcode_id)->lockForUpdate()->firstOrFail();$item=$barcode->item;$qty=$d['source_type']==='serialized'?1:(float)$d['qty'];if($d['source_type']==='serialized'){abort_unless($source->status==='assigned',422,'Barang sudah tidak berada pada teknisi.');$source->update(['status'=>'returned']);}else{abort_if((float)$source->qty_remaining<$qty,422,'Jumlah pengembalian melebihi sisa teknisi.');$remaining=(float)$source->qty_remaining-$qty;$source->update(['qty_remaining'=>$remaining,'status'=>$remaining>0?'assigned':'returned']);}$itemBefore=(float)$item->barcodes()->sum('remaining_qty');$barcodeBefore=(float)$barcode->remaining_qty;$barcodeAfter=$barcodeBefore+$qty;$barcode->update(['remaining_qty'=>$barcodeAfter,'status'=>'available']);$move=ItemMovement::create(['tenant_id'=>$item->tenant_id,'warehouse_item_id'=>$item->id,'item_barcode_id'=>$barcode->id,'movement_type'=>'return_to_warehouse','qty'=>$qty,'from_user_id'=>$source->user_id,'notes'=>$d['notes']??null,'created_by'=>$r->user()->id]);ItemStockLog::create(['tenant_id'=>$item->tenant_id,'warehouse_item_id'=>$item->id,'item_barcode_id'=>$barcode->id,'item_movement_id'=>$move->id,'qty_before'=>$itemBefore,'qty_change'=>$qty,'qty_after'=>$itemBefore+$qty,'barcode_qty_before'=>$barcodeBefore,'barcode_qty_change'=>$qty,'barcode_qty_after'=>$barcodeAfter]);return $this->created($move,'Barang berhasil dikembalikan ke gudang');});}
|
||||
public function codeIndex(Request $r){$t=$r->attributes->get('tenant_id');$q=ItemCode::when(!$r->user()->isMasterAdmin()||$t,fn($x)=>$x->where('tenant_id',$t))->when($r->search,fn($x,$s)=>$x->where(fn($y)=>$y->where('name','ilike',"%$s%")->orWhere('code','ilike',"%$s%")))->orderBy($r->input('sort_by','name'),$r->input('sort_direction','asc'))->paginate(min((int)$r->input('per_page',10),100));return $this->successPaginated($q,\Illuminate\Http\Resources\Json\JsonResource::class,'Kode barang berhasil diambil');}
|
||||
public function codeStore(Request $r){$d=$this->validateCode($r);$d['tenant_id']=$r->user()->isMasterAdmin()?($d['tenant_id']??$r->attributes->get('tenant_id')):$r->attributes->get('tenant_id');$d['created_by']=$r->user()->id;return $this->created(ItemCode::create($d),'Kode barang berhasil dibuat');}
|
||||
public function codeUpdate(Request $r,ItemCode $code){$this->authCode($r,$code);$code->update($this->validateCode($r,$code));return $this->updated($code->fresh(),'Kode barang berhasil diperbarui');}
|
||||
public function codeDestroy(Request $r,ItemCode $code){$this->authCode($r,$code);abort_if($code->items()->exists(),422,'Kode barang yang sudah digunakan tidak dapat dihapus.');$code->delete();return $this->deleted('Kode barang berhasil dihapus');}
|
||||
private function validateCode(Request $r,?ItemCode $code=null){$tenant=$r->input('tenant_id')?:$r->attributes->get('tenant_id');return $r->validate(['tenant_id'=>['nullable','integer','exists:tenants,id'],'code'=>['required','string','max:80',Rule::unique('item_codes')->where('tenant_id',$tenant)->ignore($code)],'name'=>['required','string','max:255'],'tracking_type'=>['required',Rule::in(['serialized','quantity'])],'need_serial_number'=>['boolean'],'unit'=>['required',Rule::in(['pcs','meter','lembar','roll','box','dus','set','unit'])],'minimum_stock'=>['nullable','numeric','min:0'],'description'=>['nullable','string'],'is_active'=>['boolean']]);}
|
||||
private function validateItem(Request $r,?WarehouseItem $i=null){$tenant=$r->user()->isMasterAdmin()?($r->input('tenant_id')?:$r->attributes->get('tenant_id')):$r->attributes->get('tenant_id');$d=$r->validate(['tenant_id'=>['nullable','integer','exists:tenants,id'],'item_code_id'=>['required','integer','exists:item_codes,id',Rule::unique('warehouse_items')->where('tenant_id',$tenant)->ignore($i)],'is_active'=>['boolean']]);$code=ItemCode::findOrFail($d['item_code_id']);abort_unless((int)$code->tenant_id===(int)$tenant,422,'Kode barang bukan milik tenant aktif.');return [...$d,'sku'=>$code->code,'name'=>$code->name,'tracking_type'=>$code->tracking_type==='serialized'?'serialized':'bulk','unit'=>$code->unit,'minimum_stock'=>$code->minimum_stock];}
|
||||
private function authCode(Request $r,ItemCode $code){abort_unless($r->user()->isMasterAdmin()||(int)$code->tenant_id===(int)$r->attributes->get('tenant_id'),403);}
|
||||
private function auth(Request $r,WarehouseItem $i){abort_unless($r->user()->isMasterAdmin()||(int)$i->tenant_id===(int)$r->attributes->get('tenant_id'),403);}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers\Warehouse;
|
||||
use App\Http\Controllers\ApiController;use App\Models\Warehouse;use Illuminate\Http\Request;use Illuminate\Validation\Rule;
|
||||
class WarehouseLocationController extends ApiController
|
||||
{
|
||||
public function index(Request $r){$t=$r->attributes->get('tenant_id');$q=Warehouse::withCount('items')->when(!$r->user()->isMasterAdmin()||$t,fn($x)=>$x->where('tenant_id',$t))->when($r->search,fn($x,$s)=>$x->where(fn($y)=>$y->where('name','ilike',"%$s%")->orWhere('code','ilike',"%$s%")))->orderBy('name')->paginate(min((int)$r->input('per_page',10),100));return $this->successPaginated($q,\Illuminate\Http\Resources\Json\JsonResource::class,'Daftar gudang berhasil diambil');}
|
||||
public function store(Request $r){$d=$this->data($r);$d['tenant_id']=$r->user()->isMasterAdmin()?($d['tenant_id']??$r->attributes->get('tenant_id')):$r->attributes->get('tenant_id');$d['created_by']=$r->user()->id;return $this->created(Warehouse::create($d),'Gudang berhasil dibuat');}
|
||||
public function update(Request $r,Warehouse $warehouse){$this->auth($r,$warehouse);$warehouse->update($this->data($r,$warehouse));return $this->updated($warehouse->fresh(),'Gudang berhasil diperbarui');}
|
||||
public function destroy(Request $r,Warehouse $warehouse){$this->auth($r,$warehouse);abort_if($warehouse->items()->exists(),422,'Gudang yang sudah memiliki barang tidak dapat dihapus.');$warehouse->delete();return $this->deleted('Gudang berhasil dihapus');}
|
||||
private function data(Request $r,?Warehouse $w=null){$t=$r->input('tenant_id')?:$r->attributes->get('tenant_id');return $r->validate(['tenant_id'=>['nullable','integer','exists:tenants,id'],'code'=>['required','string','max:50',Rule::unique('warehouses')->where('tenant_id',$t)->ignore($w)],'name'=>['required','string','max:255'],'address'=>['nullable','string'],'is_active'=>['boolean']]);}
|
||||
private function auth(Request $r,Warehouse $w){abort_unless($r->user()->isMasterAdmin()||(int)$w->tenant_id===(int)$r->attributes->get('tenant_id'),403);}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Wilayah;
|
||||
|
||||
use App\Http\Controllers\ApiController;
|
||||
use App\Http\Requests\Wilayah\AlamatLengkapRequest;
|
||||
use App\Http\Requests\Wilayah\IndexWilayahRequest;
|
||||
use App\Http\Requests\Wilayah\LookupWilayahRequest;
|
||||
use App\Http\Requests\Wilayah\StoreWilayahRequest;
|
||||
use App\Http\Requests\Wilayah\UpdateWilayahRequest;
|
||||
use App\Http\Resources\Wilayah\WilayahResource;
|
||||
use App\Models\WilayahKabupaten;
|
||||
use App\Models\WilayahKecamatan;
|
||||
use App\Models\WilayahProvinsi;
|
||||
use App\Services\Wilayah\WilayahService;
|
||||
|
||||
class WilayahController extends ApiController
|
||||
{
|
||||
public function __construct(protected WilayahService $service) {}
|
||||
|
||||
public function index(IndexWilayahRequest $request, string $tingkat)
|
||||
{
|
||||
return $this->successPaginated(
|
||||
$this->service->list($tingkat, $request->validated()),
|
||||
WilayahResource::class,
|
||||
"Daftar {$tingkat} berhasil diambil"
|
||||
);
|
||||
}
|
||||
|
||||
public function store(StoreWilayahRequest $request, string $tingkat)
|
||||
{
|
||||
return $this->created(
|
||||
new WilayahResource($this->service->create($tingkat, $request->validated())),
|
||||
ucfirst($tingkat).' berhasil dibuat'
|
||||
);
|
||||
}
|
||||
|
||||
public function show(int $wilayah, string $tingkat)
|
||||
{
|
||||
return $this->success(
|
||||
new WilayahResource($this->service->find($tingkat, $wilayah)),
|
||||
'Detail wilayah berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function update(UpdateWilayahRequest $request, int $wilayah, string $tingkat)
|
||||
{
|
||||
return $this->updated(
|
||||
new WilayahResource($this->service->update($tingkat, $wilayah, $request->validated())),
|
||||
ucfirst($tingkat).' berhasil diperbarui'
|
||||
);
|
||||
}
|
||||
|
||||
public function destroy(int $wilayah, string $tingkat)
|
||||
{
|
||||
$this->service->delete($tingkat, $wilayah);
|
||||
|
||||
return $this->deleted(ucfirst($tingkat).' berhasil dihapus');
|
||||
}
|
||||
|
||||
public function alamatLengkap(AlamatLengkapRequest $request)
|
||||
{
|
||||
return $this->success(
|
||||
$this->service->alamatLengkap($request->validated()),
|
||||
'Alamat wilayah lengkap berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function provinsiOptions(LookupWilayahRequest $request)
|
||||
{
|
||||
return $this->success(
|
||||
$this->service->options('provinsi', $request->validated()),
|
||||
'Pilihan provinsi berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function kabupatenOptions(
|
||||
LookupWilayahRequest $request,
|
||||
WilayahProvinsi $provinsi
|
||||
) {
|
||||
return $this->success(
|
||||
$this->service->options('kabupaten', $request->validated(), $provinsi->id),
|
||||
'Pilihan kabupaten berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function kecamatanOptions(
|
||||
LookupWilayahRequest $request,
|
||||
WilayahKabupaten $kabupaten
|
||||
) {
|
||||
return $this->success(
|
||||
$this->service->options('kecamatan', $request->validated(), $kabupaten->id),
|
||||
'Pilihan kecamatan berhasil diambil'
|
||||
);
|
||||
}
|
||||
|
||||
public function desaOptions(
|
||||
LookupWilayahRequest $request,
|
||||
WilayahKecamatan $kecamatan
|
||||
) {
|
||||
return $this->success(
|
||||
$this->service->options('desa', $request->validated(), $kecamatan->id),
|
||||
'Pilihan desa berhasil diambil'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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();
|
||||
|
||||
$tenantId = $request->attributes->get('tenant_id');
|
||||
$effectiveLevel = $tenantId ? $user?->tenantRole($tenantId) : $user?->access_level?->value;
|
||||
|
||||
if (! $user || (! $user->isMasterAdmin() && ! in_array($effectiveLevel, $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,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Access;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ReviewRoleApplicationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$application = $this->route('application');
|
||||
$requiresMenuGroup = $this->input('decision') === 'approved'
|
||||
&& in_array($application?->application_type, ['tenant', 'staff'], true);
|
||||
|
||||
return [
|
||||
'decision' => ['required', Rule::in(['approved', 'rejected'])], 'review_notes' => ['nullable', 'string', 'max:2000'],
|
||||
'menu_group_id' => [
|
||||
Rule::requiredIf($requiresMenuGroup),
|
||||
'nullable',
|
||||
'integer',
|
||||
Rule::exists('menu_groups', 'id')->where('is_active', true),
|
||||
],
|
||||
'payment_type' => ['nullable', Rule::in(['prepaid', 'postpaid'])],
|
||||
'price_mode' => ['nullable', Rule::in(['discount', 'commission'])],
|
||||
'discount_percent' => ['nullable', 'numeric', 'min:0', 'max:100'], 'commission_percent' => ['nullable', 'numeric', 'min:0', 'max:100'],
|
||||
'credit_limit' => ['nullable', 'numeric', 'min:0'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Access;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreRoleApplicationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'application_type' => ['required', Rule::in(['tenant', 'agent', 'staff'])],
|
||||
'tenant_id' => ['required_if:application_type,agent,staff', 'nullable', 'integer', 'exists:tenants,id'],
|
||||
'tenant_name' => ['required_if:application_type,tenant', 'nullable', 'string', 'max:150'],
|
||||
'tenant_code' => ['nullable', 'string', 'max:50'], 'phone' => ['nullable', 'string', 'max:30'],
|
||||
'email' => ['nullable', 'email', 'max:150'], 'address' => ['nullable', 'string', 'max:2000'],
|
||||
'reason' => ['required', 'string', 'max:2000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Customer;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ActivateCustomerRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'activation_source' => ['required', Rule::in(['list_order', 'ticket'])],
|
||||
'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' => ['required', 'string', 'max:255'],
|
||||
'external_password' => ['required', 'string', 'min:4', 'max:255'],
|
||||
'mac_address' => ['nullable', 'regex:/^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/'],
|
||||
'serial_number' => ['nullable', 'string', 'max:120'],
|
||||
'assigned_ip' => ['nullable', 'ip'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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,47 @@
|
||||
<?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'],
|
||||
'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', 'exists:topology_nodes,id'],
|
||||
'topology_port_number' => ['nullable', 'required_with:topology_id', 'integer', 'min:1'],
|
||||
'sales_user_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||
'source' => ['nullable', Rule::in(['manual', 'mikrotik', 'radius'])],
|
||||
'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,59 @@
|
||||
<?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_activate' => ['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', 'static_ip', 'dynamic_ip'])],
|
||||
'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,138 @@
|
||||
<?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'])],
|
||||
'backend_mode_override' => ['required', Rule::in(['routeros_api','platform_radius','external_radius'])],
|
||||
'host' => ['required', 'string', 'max:255'],
|
||||
'nas_identifier' => ['nullable', 'string', 'max:255'],
|
||||
'nas_ip_address' => ['nullable', 'ip'],
|
||||
'radius_host' => ['nullable', '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_coa_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', 'static_ip', 'dynamic_ip'])],
|
||||
'profile_code' => ['required', 'alpha_dash', 'max:80'],
|
||||
'usage_mode' => ['nullable', Rule::in(['one_time', 'periodic', 'recurring_member'])],
|
||||
'validity_start' => ['nullable', Rule::in(['activation', 'first_login'])],
|
||||
'validity_value' => ['nullable', 'integer', 'min:1'],
|
||||
'validity_unit' => ['nullable', Rule::in(['minutes', 'hours', 'days', 'months'])],
|
||||
'uptime_limit_seconds' => ['nullable', 'integer', 'min:0'],
|
||||
'data_limit_bytes' => ['nullable', 'integer', 'min:0'],
|
||||
'grace_period_minutes' => ['nullable', 'integer', 'min:0', 'max:10080'],
|
||||
'mac_lock_required' => ['nullable', 'boolean'],
|
||||
'ip_pool' => ['nullable', 'string', 'max:255'],
|
||||
'subnet_cidr' => ['nullable', 'string', 'max:64'],
|
||||
'gateway' => ['nullable', 'ip'],
|
||||
'dns_servers' => ['nullable', 'array', 'max:4'],
|
||||
'dns_servers.*' => ['required', 'ip'],
|
||||
'vlan_id' => ['nullable', 'integer', 'between:1,4094'],
|
||||
'service_parameters' => ['nullable', 'array'],
|
||||
'scripts' => ['nullable', 'array'],
|
||||
'scripts.*.policy_id' => ['required', 'integer', 'exists:nas_script_policies,id'],
|
||||
'scripts.*.version_id' => ['nullable', 'integer', 'exists:nas_script_policy_versions,id'],
|
||||
'scripts.*.event' => ['required', Rule::in(['on_up','on_down','on_login','on_logout'])],
|
||||
'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'],
|
||||
];
|
||||
}
|
||||
|
||||
public function withValidator($validator): void
|
||||
{
|
||||
$resource = $this->route()->defaults['nas_resource'] ?? null;
|
||||
if ($resource === 'mikrotik') {
|
||||
$validator->after(function ($validator) {
|
||||
$mode = $this->input('backend_mode_override');
|
||||
if (! $this->input('api_username')) $validator->errors()->add('api_username', 'API Username wajib diisi untuk monitoring Mikrotik.');
|
||||
if ($this->isMethod('post') && ! $this->input('api_password')) $validator->errors()->add('api_password', 'API Password wajib diisi.');
|
||||
if ($mode !== 'routeros_api') {
|
||||
if (! $this->input('nas_identifier')) $validator->errors()->add('nas_identifier', 'NAS Identifier wajib diisi untuk integrasi RADIUS.');
|
||||
if (! $this->input('nas_ip_address')) $validator->errors()->add('nas_ip_address', 'IP NAS wajib diisi untuk integrasi RADIUS.');
|
||||
if ($this->isMethod('post') && ! $this->input('radius_secret')) $validator->errors()->add('radius_secret', 'Shared secret khusus NAS wajib diisi.');
|
||||
if ($mode === 'external_radius' && ! $this->input('radius_host')) $validator->errors()->add('radius_host', 'Host RADIUS Eksternal wajib diisi.');
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if ($resource !== 'package-profile') return;
|
||||
|
||||
$validator->after(function ($validator) {
|
||||
$type = $this->input('service_type');
|
||||
$mode = $this->input('usage_mode');
|
||||
if ($type === 'hotspot' && ! in_array($mode, ['one_time', 'periodic', 'recurring_member'], true)) {
|
||||
$validator->errors()->add('usage_mode', 'Mode pemakaian wajib dipilih untuk profile Hotspot.');
|
||||
}
|
||||
if ($type === 'hotspot' && in_array($mode, ['one_time', 'periodic', 'recurring_member'], true)
|
||||
&& (! $this->input('validity_value') || ! $this->input('validity_unit'))) {
|
||||
$validator->errors()->add('validity_value', 'Durasi dan satuan masa aktif wajib diisi untuk profile Hotspot.');
|
||||
}
|
||||
if (in_array($type, ['static_ip', 'dynamic_ip'], true)) {
|
||||
if (! $this->boolean('mac_lock_required')) $validator->errors()->add('mac_lock_required', 'Penguncian MAC wajib aktif untuk profile IP Static/Dinamis.');
|
||||
if (! $this->input('subnet_cidr')) $validator->errors()->add('subnet_cidr', 'Subnet CIDR wajib diisi.');
|
||||
if (! $this->input('gateway')) $validator->errors()->add('gateway', 'Gateway wajib diisi.');
|
||||
if ($type === 'dynamic_ip' && ! $this->input('ip_pool')) $validator->errors()->add('ip_pool', 'IP Pool wajib diisi untuk IP Dinamis.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,25 @@
|
||||
<?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'],
|
||||
'tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
|
||||
'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,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Notification;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreSystemNotificationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
'body' => ['required', 'string', 'max:10000'],
|
||||
'action_url' => ['nullable', 'string', 'max:2000'],
|
||||
'icon' => ['nullable', 'string', 'max:100'],
|
||||
'target_scope' => ['required', Rule::in(['all_users', 'tenant', 'access_level', 'selected_users'])],
|
||||
'tenant_id' => [Rule::requiredIf($this->input('target_scope') === 'tenant' && $this->user()?->isMasterAdmin()), 'nullable', 'integer', 'exists:tenants,id'],
|
||||
'statuses' => ['nullable', 'array'],
|
||||
'statuses.*' => ['string', Rule::in(['active', 'inactive', 'suspended'])],
|
||||
'access_levels' => ['required_if:target_scope,access_level', 'nullable', 'array', 'min:1'],
|
||||
'access_levels.*' => ['string', Rule::in(['customer', 'staff', 'tenant_owner', 'master_admin'])],
|
||||
'user_ids' => ['required_if:target_scope,selected_users', 'nullable', 'array', 'min:1', 'max:1000'],
|
||||
'user_ids.*' => ['integer', 'exists:users,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,8 @@ class AssignTicketRequest extends FormRequest
|
||||
{
|
||||
return [
|
||||
'technicians' => 'required|array|min:1',
|
||||
'leader_id' => 'required|integer',
|
||||
'technicians.*' => 'required|integer|exists:users,id',
|
||||
'leader_id' => 'required|integer|in_array:technicians.*',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?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'],
|
||||
'sort_by' => ['nullable', Rule::in(['ticket_no', 'title', 'priority', 'status', 'created_at'])],
|
||||
'sort_direction' => ['nullable', Rule::in(['asc', 'desc'])],
|
||||
'assigned_to_me' => ['nullable', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,9 @@ class PendingTicketRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'reason' => 'required|string|max:1000'
|
||||
'reason' => 'required|string|max:1000',
|
||||
'stored_file_ids' => ['required','array','min:1'],
|
||||
'stored_file_ids.*' => ['integer','exists:stored_files,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,9 @@ class ResolveTicketRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'notes' => 'required|string|max:2000'
|
||||
'notes' => 'required|string|max:2000',
|
||||
'stored_file_ids' => ['required','array','min:1'],
|
||||
'stored_file_ids.*' => ['integer','exists:stored_files,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,9 @@ class StartTicketRequest extends FormRequest
|
||||
return [
|
||||
'latitude' => 'nullable|numeric',
|
||||
'longitude' => 'nullable|numeric',
|
||||
'notes' => 'nullable|string|max:1000'
|
||||
'notes' => 'nullable|string|max:1000',
|
||||
'stored_file_ids' => ['required','array','min:1'],
|
||||
'stored_file_ids.*' => ['integer','exists:stored_files,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,9 @@ class StoreTicketRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'ticket_type_id' => 'required|integer',
|
||||
'customer_id' => 'nullable|integer',
|
||||
'tenant_id' => 'nullable|integer|exists:tenants,id',
|
||||
'ticket_type_id' => 'required|integer|exists:ticket_types,id',
|
||||
'customer_id' => 'nullable|integer|exists:customers,id',
|
||||
'title' => 'required|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'priority' => 'required|in:low,medium,high,critical'
|
||||
|
||||
@@ -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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user