initial commit
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
/**
|
||||
* Define the application's command schedule.
|
||||
*/
|
||||
protected function schedule(Schedule $schedule): void
|
||||
{
|
||||
// $schedule->command('inspire')->hourly();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the commands for the application.
|
||||
*/
|
||||
protected function commands(): void
|
||||
{
|
||||
$this->load(__DIR__.'/Commands');
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
protected $levels = [];
|
||||
protected $dontReport = [];
|
||||
protected $dontFlash = [
|
||||
'current_password',
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
$this->reportable(function (Throwable $e) {
|
||||
//
|
||||
});
|
||||
}
|
||||
|
||||
public function render($request, Throwable $exception)
|
||||
{
|
||||
if ($request->is('api/*') || $request->wantsJson()) {
|
||||
return $this->handleApiException($request, $exception);
|
||||
}
|
||||
|
||||
return parent::render($request, $exception);
|
||||
}
|
||||
|
||||
private function handleApiException($request, Throwable $exception)
|
||||
{
|
||||
$statusCode = 500;
|
||||
$response = [
|
||||
'success' => false,
|
||||
'message' => 'Internal Server Error',
|
||||
'data' => []
|
||||
];
|
||||
|
||||
if ($exception instanceof AuthenticationException) {
|
||||
$statusCode = 401;
|
||||
$response['message'] = 'Unauthenticated';
|
||||
} elseif ($exception instanceof AuthorizationException) {
|
||||
$statusCode = 403;
|
||||
$response['message'] = 'Forbidden';
|
||||
} elseif ($exception instanceof ValidationException) {
|
||||
$statusCode = 422;
|
||||
$response['message'] = 'Validation Error';
|
||||
$response['data'] = $exception->errors();
|
||||
} elseif ($exception instanceof NotFoundHttpException) {
|
||||
$statusCode = 404;
|
||||
$response['message'] = 'Not Found';
|
||||
} else {
|
||||
$response['message'] = $exception->getMessage() ?: 'Internal Server Error';
|
||||
}
|
||||
|
||||
if (config('app.debug')) {
|
||||
$response['debug'] = [
|
||||
'message' => $exception->getMessage(),
|
||||
'file' => $exception->getFile(),
|
||||
'line' => $exception->getLine(),
|
||||
'trace' => $exception->getTrace()
|
||||
];
|
||||
}
|
||||
|
||||
return response()->json($response, $statusCode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response as ResponseAlias;
|
||||
|
||||
class ResponseJson
|
||||
{
|
||||
public static function successResponse(string $message, array|Collection|Model $data = []): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $data,
|
||||
'message' => $message
|
||||
], ResponseAlias::HTTP_OK);
|
||||
}
|
||||
|
||||
public static function pageNotFoundResponse(string $message, array|Collection|Model $data = []): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'data' => $data,
|
||||
'message' => $message
|
||||
], ResponseAlias::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
public static function failedResponse(string $message, array|Collection|Model $data = []): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'data' => $data,
|
||||
'message' => $message
|
||||
], ResponseAlias::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
public static function validationErrorResponse(string $message, array|Collection|Model $data = []): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'data' => $data,
|
||||
'message' => $message
|
||||
], ResponseAlias::HTTP_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
public static function unauthorizeResponse(string $message, array|Collection|Model $data = []): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'data' => $data,
|
||||
'message' => $message
|
||||
], ResponseAlias::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
public static function forbidenResponse(string $message = "Forbidden", array|Collection|Model $data = []): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'data' => $data,
|
||||
'message' => $message
|
||||
], ResponseAlias::HTTP_FORBIDDEN);
|
||||
}
|
||||
|
||||
public static function errorResponse(array|Collection|Model $data = []): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'data' => $data,
|
||||
'message' => 'Server is busy right now!'
|
||||
], ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
class SignedUrlGenerator
|
||||
{
|
||||
public function generateDiseaseRecordsExportUrl($diseaseId, $expiryMinutes = 30)
|
||||
{
|
||||
return URL::temporarySignedRoute(
|
||||
'disease.records.export',
|
||||
now()->addMinutes($expiryMinutes),
|
||||
['diseaseId' => $diseaseId]
|
||||
);
|
||||
}
|
||||
|
||||
public function generateSingleRecordExportUrl($diseaseId, $recordId, $expiryMinutes = 30)
|
||||
{
|
||||
return URL::temporarySignedRoute(
|
||||
'disease.record.export',
|
||||
now()->addMinutes($expiryMinutes),
|
||||
['diseaseId' => $diseaseId, 'recordId' => $recordId]
|
||||
);
|
||||
}
|
||||
|
||||
public function generateFileDownloadUrl($path, $expiryMinutes = 30)
|
||||
{
|
||||
return URL::temporarySignedRoute(
|
||||
'files.download.record',
|
||||
now()->addMinutes($expiryMinutes),
|
||||
['path' => $path]
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Helpers\ResponseJson; // Import the ResponseJson helper
|
||||
use App\Http\Requests\CreateUserRequest;
|
||||
use App\Http\Requests\EditUserRequest;
|
||||
use App\Http\Requests\UserIdRequest;
|
||||
use App\Services\AdminUserService;
|
||||
use App\Services\DiseaseService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class AdminUserController extends Controller
|
||||
{
|
||||
protected AdminUserService $adminUserService;
|
||||
|
||||
public function __construct(AdminUserService $adminUserService)
|
||||
{
|
||||
$this->adminUserService = $adminUserService;
|
||||
}
|
||||
|
||||
public function createUser(CreateUserRequest $request): JsonResponse
|
||||
{
|
||||
|
||||
$validatedData = $request->validated();
|
||||
|
||||
[$success, $message, $data] = $this->adminUserService->createUser($validatedData);
|
||||
|
||||
if(!$success){
|
||||
return ResponseJson::failedResponse($message, $validatedData);
|
||||
}
|
||||
|
||||
if ($validatedData['role'] === 'operator' && isset($validatedData['disease_ids'])) {
|
||||
[$assignSuccess, $assignMessage, $data] = $this->adminUserService->assignOperatorToDiseases($data['id'], $validatedData['disease_ids']);
|
||||
|
||||
if ($assignSuccess) {
|
||||
$message .= ' ' . $assignMessage;
|
||||
} else {
|
||||
return ResponseJson::failedResponse($assignMessage);
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseJson::successResponse($message, $data);
|
||||
}
|
||||
|
||||
public function approveUser(UserIdRequest $request, $userId): JsonResponse
|
||||
{
|
||||
[$success, $message, $data] = $this->adminUserService->approveUser($userId);
|
||||
|
||||
if(!$success){
|
||||
return ResponseJson::failedResponse($message, $data);
|
||||
}
|
||||
|
||||
return ResponseJson::successResponse('User approved successfully.', $data);
|
||||
}
|
||||
|
||||
public function rejectUser(UserIdRequest $request, $userId): JsonResponse
|
||||
{
|
||||
[$success, $message, $data] = $this->adminUserService->rejectUser($userId);
|
||||
|
||||
if(!$success){
|
||||
return ResponseJson::failedResponse($message, $data);
|
||||
}
|
||||
|
||||
return ResponseJson::successResponse('User rejected successfully.', $data);
|
||||
}
|
||||
|
||||
public function editUser(EditUserRequest $request, $id): JsonResponse
|
||||
{
|
||||
|
||||
[$success, $message, $data] = $this->adminUserService->editUser($id, $request->validated());
|
||||
|
||||
if(!$success){
|
||||
return ResponseJson::failedResponse($message, $data);
|
||||
}
|
||||
|
||||
return ResponseJson::successResponse('User updated successfully.', $data);
|
||||
}
|
||||
|
||||
public function deleteUser(UserIdRequest $request, $userId): JsonResponse
|
||||
{
|
||||
[$success, $message, $data] = $this->adminUserService->deleteUser($userId);
|
||||
|
||||
if(!$success){
|
||||
return ResponseJson::failedResponse($message, $data);
|
||||
}
|
||||
|
||||
return ResponseJson::successResponse('User deleted successfully.', $data);
|
||||
}
|
||||
|
||||
public function getUsers(Request $request): JsonResponse
|
||||
{
|
||||
[$success, $message, $data]= $this->adminUserService->getUsers($request->all());
|
||||
|
||||
if(!$success){
|
||||
return ResponseJson::failedResponse($message, $data);
|
||||
}
|
||||
return ResponseJson::successResponse('Users retrieved successfully.', $data);
|
||||
}
|
||||
|
||||
public function getUserDetails(UserIdRequest $request, $userId): JsonResponse
|
||||
{
|
||||
[$success, $message, $data] = $this->adminUserService->getUserDetails($userId);
|
||||
|
||||
if(!$success){
|
||||
return ResponseJson::failedResponse($message, $data);
|
||||
}
|
||||
|
||||
return ResponseJson::successResponse('User details retrieved successfully.', $data);
|
||||
}
|
||||
|
||||
public function getUserProfile(Request $request)
|
||||
{
|
||||
$user = $request->user(); // Get the authenticated user
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Profile retrieved successfully.',
|
||||
'data' => [
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'role' => $user->role,
|
||||
'institution' => $user->institution,
|
||||
'gender' => $user->gender,
|
||||
'phone' => $user->phone_number,
|
||||
'approval_status' => $user->approval_status,
|
||||
'created_at' => $user->created_at,
|
||||
'updated_at' => $user->updated_at,
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Helpers\ResponseJson;
|
||||
use App\Http\Requests\LoginUserRequest;
|
||||
use App\Http\Requests\RegisterUserRequest;
|
||||
use App\Services\AuthUserService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
private AuthUserService $authUserService;
|
||||
|
||||
public function __construct(AuthUserService $authUserService)
|
||||
{
|
||||
$this->authUserService = $authUserService;
|
||||
}
|
||||
|
||||
public function register(RegisterUserRequest $request)
|
||||
{
|
||||
[$success, $message, $data] = $this->authUserService->register(
|
||||
$request->name,
|
||||
$request->email,
|
||||
$request->password,
|
||||
$request->institution,
|
||||
$request->gender,
|
||||
$request->phone_number,
|
||||
$request->tujuan_permohonan,
|
||||
);
|
||||
|
||||
if (!$success) {
|
||||
return ResponseJson::failedResponse($message, $data);
|
||||
}
|
||||
|
||||
return ResponseJson::successResponse($message, $data);
|
||||
}
|
||||
|
||||
public function login(LoginUserRequest $request)
|
||||
{
|
||||
[$success, $message, $data] = $this->authUserService->login(
|
||||
$request->email,
|
||||
$request->password
|
||||
);
|
||||
|
||||
if (!$success) {
|
||||
return ResponseJson::failedResponse($message, $data);
|
||||
}
|
||||
|
||||
return ResponseJson::successResponse($message, $data);
|
||||
}
|
||||
|
||||
public function logout(Request $request)
|
||||
{
|
||||
$this->authUserService->logout($request->user());
|
||||
return ResponseJson::successResponse('Berhasil Logout', []);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\CommentService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Helpers\ResponseJson;
|
||||
use App\Http\Requests\CreateCommentRequest;
|
||||
use App\Http\Requests\DeleteCommentRequest;
|
||||
use App\Http\Requests\EditCommentRequest;
|
||||
|
||||
class CommentController extends Controller
|
||||
{
|
||||
protected CommentService $commentService;
|
||||
|
||||
public function __construct(CommentService $commentService)
|
||||
{
|
||||
$this->commentService = $commentService;
|
||||
}
|
||||
|
||||
public function createComment(CreateCommentRequest $request): JsonResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
|
||||
$userId = $request->user()->id;
|
||||
|
||||
[$success, $message, $comment] = $this->commentService->createComment($data, $userId);
|
||||
|
||||
if (!$success) {
|
||||
return ResponseJson::failedResponse($message, $comment);
|
||||
}
|
||||
|
||||
return ResponseJson::successResponse($message, $comment);
|
||||
}
|
||||
|
||||
public function editComment(EditCommentRequest $request, $diseaseId, $commentId): JsonResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
|
||||
$userId = $request->user()->id;
|
||||
|
||||
[$success, $message, $comment] = $this->commentService->editComment($commentId, $data, $userId);
|
||||
|
||||
if (!$success) {
|
||||
return ResponseJson::failedResponse($message, $comment);
|
||||
}
|
||||
|
||||
return ResponseJson::successResponse($message, $comment);
|
||||
}
|
||||
|
||||
public function deleteComment(DeleteCommentRequest $request, $diseaseId, $commentId): JsonResponse
|
||||
{
|
||||
$userId = request()->user()->id;
|
||||
|
||||
[$success, $message, $comment] = $this->commentService->deleteComment($commentId, $userId);
|
||||
|
||||
if (!$success) {
|
||||
return ResponseJson::failedResponse($message, $comment);
|
||||
}
|
||||
|
||||
return ResponseJson::successResponse($message, $comment);
|
||||
}
|
||||
|
||||
public function getComments($diseaseId): JsonResponse
|
||||
{
|
||||
[$success, $message, $comments] = $this->commentService->getComments($diseaseId);
|
||||
|
||||
if (!$success) {
|
||||
return ResponseJson::failedResponse($message, $comments);
|
||||
}
|
||||
|
||||
return ResponseJson::successResponse($message, $comments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
|
||||
class Controller extends BaseController
|
||||
{
|
||||
use AuthorizesRequests, ValidatesRequests;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Helpers\ResponseJson; // Import the ResponseJson helper
|
||||
use App\Services\DiseaseService;
|
||||
use App\Http\Requests\CreateDiseaseRequest;
|
||||
use App\Http\Requests\EditDiseaseRequest;
|
||||
use App\Http\Requests\DiseaseIdRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class DiseaseController extends Controller
|
||||
{
|
||||
protected DiseaseService $diseaseService;
|
||||
|
||||
public function __construct(DiseaseService $diseaseService)
|
||||
{
|
||||
$this->diseaseService = $diseaseService;
|
||||
}
|
||||
|
||||
public function createDisease(CreateDiseaseRequest $request): JsonResponse
|
||||
{
|
||||
$userId = $request->user()->id;
|
||||
|
||||
[$success, $message, $data] = $this->diseaseService->createDisease($request->validated(), $userId);
|
||||
|
||||
if (!$success) {
|
||||
return ResponseJson::failedResponse($message, $data);
|
||||
}
|
||||
|
||||
return ResponseJson::successResponse($message, $data);
|
||||
}
|
||||
|
||||
public function editDisease(EditDiseaseRequest $request, $diseaseId): JsonResponse
|
||||
{
|
||||
// Log::info('EditDisease Request Data: ', $request->validated());
|
||||
|
||||
$userId = $request->user()->id;
|
||||
|
||||
[$success, $message, $data] = $this->diseaseService->editDisease($diseaseId, $request->validated(), $userId);
|
||||
|
||||
if(!$success){
|
||||
return ResponseJson::failedResponse($message, $data);
|
||||
}
|
||||
|
||||
return ResponseJson::successResponse('Disease updated successfully.', $data);
|
||||
}
|
||||
|
||||
public function deleteDisease(DiseaseIdRequest $request, $diseaseId): JsonResponse
|
||||
{
|
||||
$userId = $request->user()->id;
|
||||
|
||||
[$success, $message, $data] = $this->diseaseService->deleteDisease($diseaseId, $userId);
|
||||
|
||||
if(!$success){
|
||||
return ResponseJson::failedResponse($message, $data);
|
||||
}
|
||||
|
||||
return ResponseJson::successResponse('Disease deleted successfully.', $data);
|
||||
}
|
||||
|
||||
public function getDiseases(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
[$success, $message, $data] = $this->diseaseService->getDisease($request->all(), $user);
|
||||
|
||||
if (!$success) {
|
||||
return ResponseJson::failedResponse($message, $data);
|
||||
}
|
||||
|
||||
return ResponseJson::successResponse('Disease retrieved successfully.', $data);
|
||||
}
|
||||
|
||||
public function getDiseaseDetails(DiseaseIdRequest $request, $diseaseId): JsonResponse
|
||||
{
|
||||
[$success, $message, $data] = $this->diseaseService->getDiseaseDetails($diseaseId);
|
||||
|
||||
if(!$success){
|
||||
return ResponseJson::failedResponse($message, $data);
|
||||
}
|
||||
|
||||
return ResponseJson::successResponse('Disease details retrieved successfully.', $data);
|
||||
}
|
||||
|
||||
public function getStatistics(Request $request): JsonResponse
|
||||
{
|
||||
[$success, $message, $data]= $this->diseaseService->getStatistics($request->all());
|
||||
|
||||
if(!$success){
|
||||
return ResponseJson::failedResponse($message, $data);
|
||||
}
|
||||
return ResponseJson::successResponse('Disease statistics retrieved successfully.', $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Helpers\ResponseJson; // Import the ResponseJson helper
|
||||
use App\Services\DiseaseRecordService;
|
||||
use App\Http\Requests\CreateDiseaseRecordRequest;
|
||||
use App\Http\Requests\CreateDiseaseRequest;
|
||||
use App\Http\Requests\ShowDiseaseRecordRequest;
|
||||
use App\Http\Requests\EditDiseaseRecordRequest;
|
||||
use App\Http\Requests\DeleteDiseaseRecordRequest;
|
||||
use App\Http\Requests\IndexDiseaseRecordRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class DiseaseRecordController extends Controller
|
||||
{
|
||||
protected DiseaseRecordService $diseaseRecordService;
|
||||
|
||||
public function __construct(DiseaseRecordService $diseaseRecordService)
|
||||
{
|
||||
$this->diseaseRecordService = $diseaseRecordService;
|
||||
}
|
||||
|
||||
public function createDiseaseRecord(CreateDiseaseRecordRequest $request): JsonResponse
|
||||
{
|
||||
//var_dump($request->all());
|
||||
// return ResponseJson::failedResponse("tez", []);
|
||||
$userId = $request->user()->id;
|
||||
//echo(is_array($request->input('record_data')));
|
||||
[$success, $message, $data] = $this->diseaseRecordService->createDiseaseRecord($request->validated(), $userId);
|
||||
|
||||
if (!$success) {
|
||||
return ResponseJson::failedResponse($message, $data);
|
||||
}
|
||||
|
||||
return ResponseJson::successResponse($message, $data);
|
||||
}
|
||||
|
||||
public function editDiseaseRecord(EditDiseaseRecordRequest $request, $diseaseId, $recordId): JsonResponse
|
||||
{
|
||||
$userId = $request->user()->id;
|
||||
[$success, $message, $data] = $this->diseaseRecordService->editDiseaseRecord($recordId, $request->validated(), $userId);
|
||||
|
||||
if (!$success) {
|
||||
return ResponseJson::failedResponse($message, $data);
|
||||
}
|
||||
|
||||
return ResponseJson::successResponse('Disease record updated successfully.', $data);
|
||||
}
|
||||
|
||||
public function deleteDiseaseRecord(DeleteDiseaseRecordRequest $request, $diseaseId, $recordId): JsonResponse
|
||||
{
|
||||
$userId = $request->user()->id;
|
||||
[$success, $message] = $this->diseaseRecordService->deleteDiseaseRecord($recordId, $userId);
|
||||
|
||||
if (!$success) {
|
||||
return ResponseJson::failedResponse($message);
|
||||
}
|
||||
|
||||
return ResponseJson::successResponse($message);
|
||||
}
|
||||
|
||||
public function getDiseaseRecords(IndexDiseaseRecordRequest $request, $diseaseId): JsonResponse
|
||||
{
|
||||
[$success, $message, $data] = $this->diseaseRecordService->getDiseaseRecords($diseaseId, $request->all());
|
||||
|
||||
if (!$success) {
|
||||
return ResponseJson::failedResponse($message, $data);
|
||||
}
|
||||
|
||||
return ResponseJson::successResponse('Disease records retrieved successfully.', $data);
|
||||
}
|
||||
|
||||
|
||||
public function getDiseaseRecordDetails(ShowDiseaseRecordRequest $request, $diseaseId, $recordId): JsonResponse
|
||||
{
|
||||
[$success, $message, $data] = $this->diseaseRecordService->getDiseaseRecordDetails($diseaseId, $recordId);
|
||||
|
||||
if (!$success) {
|
||||
return ResponseJson::failedResponse($message, $data);
|
||||
}
|
||||
|
||||
return ResponseJson::successResponse($message, $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Disease;
|
||||
use App\Models\DiseaseRecord;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use App\Services\FileStorageService;
|
||||
use App\Http\Requests\IndexDiseaseRecordRequest;
|
||||
use App\Http\Requests\ShowDiseaseRecordRequest;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
class DiseaseRecordExportController extends Controller
|
||||
{
|
||||
protected $fileStorage;
|
||||
|
||||
public function __construct(FileStorageService $fileStorage)
|
||||
{
|
||||
$this->fileStorage = $fileStorage;
|
||||
$this->middleware(['auth:sanctum', 'checkDiseaseAccess']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all disease records to CSV
|
||||
*/
|
||||
public function exportToCsv(IndexDiseaseRecordRequest $request, $diseaseId)
|
||||
{
|
||||
// Validate disease exists
|
||||
$disease = Disease::findOrFail($diseaseId);
|
||||
|
||||
// Get all columns from schema
|
||||
$columns = collect($disease->schema['columns'])
|
||||
->pluck('name')
|
||||
->toArray();
|
||||
|
||||
// Generate filename
|
||||
$filename = sprintf(
|
||||
'disease-records-%s-%s.csv',
|
||||
$disease->name,
|
||||
now()->format('Y-m-d-His')
|
||||
);
|
||||
|
||||
return $this->generateCsvResponse($disease, $columns, $filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export single record to CSV
|
||||
*/
|
||||
public function exportSingleRecord(ShowDiseaseRecordRequest $request, $diseaseId, $recordId)
|
||||
{
|
||||
$disease = Disease::findOrFail($diseaseId);
|
||||
$record = DiseaseRecord::where('disease_id', $diseaseId)
|
||||
->where('id', $recordId)
|
||||
->firstOrFail();
|
||||
|
||||
$columns = collect($disease->schema['columns'])
|
||||
->pluck('name')
|
||||
->toArray();
|
||||
|
||||
$filename = sprintf(
|
||||
'disease-record-%s-%d-%s.csv',
|
||||
$disease->name,
|
||||
$recordId,
|
||||
now()->format('Y-m-d-His')
|
||||
);
|
||||
|
||||
return $this->generateCsvResponse($disease, $columns, $filename, $record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate CSV response for both single and multiple records
|
||||
*/
|
||||
private function generateCsvResponse($disease, $columns, $filename, $singleRecord = null)
|
||||
{
|
||||
return new StreamedResponse(function () use ($disease, $columns, $singleRecord) {
|
||||
$handle = fopen('php://output', 'w');
|
||||
|
||||
// Write headers
|
||||
fputcsv($handle, [...$columns, 'created_at', 'updated_at']);
|
||||
|
||||
if ($singleRecord) {
|
||||
// Write single record
|
||||
$this->writeRecordToCsv($handle, $singleRecord, $columns);
|
||||
} else {
|
||||
// Write all records
|
||||
DiseaseRecord::where('disease_id', $disease->id)
|
||||
->chunk(100, function ($records) use ($handle, $columns) {
|
||||
foreach ($records as $record) {
|
||||
$this->writeRecordToCsv($handle, $record, $columns);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
}, 200, [
|
||||
'Content-Type' => 'text/csv',
|
||||
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
|
||||
'Cache-Control' => 'no-cache, no-store, must-revalidate',
|
||||
'Pragma' => 'no-cache',
|
||||
'Expires' => '0',
|
||||
]);
|
||||
}
|
||||
|
||||
private function generateSignedFileUrl($path)
|
||||
{
|
||||
// Remove any leading slashes
|
||||
$path = ltrim($path, '/');
|
||||
|
||||
return URL::temporarySignedRoute(
|
||||
'files.download.record',
|
||||
now()->addHours(24), // URLs valid for 24 hours
|
||||
['path' => $path]
|
||||
);
|
||||
}
|
||||
|
||||
private function generatePublicFileUrl($path)
|
||||
{
|
||||
$domain = env('APP_URL');
|
||||
return $domain . '/storage/public/' . $path;
|
||||
}
|
||||
/**
|
||||
* Write a single record to CSV
|
||||
*/
|
||||
private function writeRecordToCsv($handle, $record, $columns)
|
||||
{
|
||||
$row = [];
|
||||
foreach ($columns as $column) {
|
||||
$value = $record->data[$column] ?? '';
|
||||
|
||||
// Handle file paths
|
||||
if (is_array($value)) {
|
||||
// Convert array of file paths to public URLs
|
||||
$urls = array_map(
|
||||
fn($path) => $this->generatePublicFileUrl($path),
|
||||
$value
|
||||
);
|
||||
$value = implode(' | ', $urls);
|
||||
} elseif (is_string($value) && str_contains($value, 'diseases/records/')) {
|
||||
// Convert single file path to public URL
|
||||
$value = $this->generatePublicFileUrl($value);
|
||||
}
|
||||
|
||||
$row[] = $value;
|
||||
}
|
||||
|
||||
// Add timestamps
|
||||
$row[] = $record->created_at->format('Y-m-d H:i:s');
|
||||
$row[] = $record->updated_at->format('Y-m-d H:i:s');
|
||||
|
||||
fputcsv($handle, $row);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\FileStorageService;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\DiseaseRecord;
|
||||
use App\Models\Disease;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class FileController extends Controller
|
||||
{
|
||||
protected $fileStorage;
|
||||
|
||||
public function __construct(FileStorageService $fileStorage)
|
||||
{
|
||||
$this->fileStorage = $fileStorage;
|
||||
$this->middleware(['auth:sanctum', 'checkDiseaseAccess'])->only(['previewFile']);
|
||||
// $this->middleware('signed')->only(['downloadRecord']); // Add signed middleware for downloads
|
||||
}
|
||||
|
||||
public function previewFile(Request $request, string $path)
|
||||
{
|
||||
try {
|
||||
$path = $this->sanitizePath($path);
|
||||
$storagePath = storage_path('app/public/' . $path);
|
||||
|
||||
if (!file_exists($storagePath)) {
|
||||
Log::error('File not found', ['path' => $path]);
|
||||
abort(404, 'File not found');
|
||||
}
|
||||
|
||||
// Determine MIME type
|
||||
$mimeType = mime_content_type($storagePath);
|
||||
|
||||
// Only allow audio files for preview (extendable later)
|
||||
if (!str_starts_with($mimeType, 'audio/')) {
|
||||
abort(415, 'Unsupported media type for inline preview');
|
||||
}
|
||||
|
||||
// Stream audio file
|
||||
return response()->file($storagePath, [
|
||||
'Content-Type' => $mimeType,
|
||||
'Content-Disposition' => 'inline',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('File preview failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'path' => $path,
|
||||
'user_id' => $request->user()->id ?? null,
|
||||
]);
|
||||
abort(500, 'Failed to preview file');
|
||||
}
|
||||
}
|
||||
|
||||
public function downloadRecord(Request $request, string $path)
|
||||
{
|
||||
try {
|
||||
if (!$request->hasValidSignature()) {
|
||||
abort(401, 'Invalid signature');
|
||||
}
|
||||
|
||||
// Clean up the path to match storage structure
|
||||
$path = $this->sanitizePath($path);
|
||||
|
||||
// Determine the full storage path
|
||||
$storagePath = storage_path('app/public/' . $path);
|
||||
|
||||
if (!file_exists($storagePath)) {
|
||||
Log::error('File not found', [
|
||||
'path' => $path,
|
||||
'full_path' => $storagePath
|
||||
]);
|
||||
abort(404, 'File not found');
|
||||
}
|
||||
|
||||
// Find record containing this file path
|
||||
$record = $this->findRecordByPath($path);
|
||||
if (!$record) {
|
||||
abort(404, 'Associated record not found');
|
||||
}
|
||||
|
||||
// Log download attempt
|
||||
Log::info('File download initiated via signed URL', [
|
||||
'path' => $path,
|
||||
'disease_id' => $record->disease_id,
|
||||
'record_id' => $record->id
|
||||
]);
|
||||
|
||||
return $this->fileStorage->streamFile('public/' . $path);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('File download failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'path' => $path ?? 'undefined'
|
||||
]);
|
||||
|
||||
abort(500, 'Failed to download file: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function sanitizePath(string $path): string
|
||||
{
|
||||
// Remove any directory traversal attempts and extra slashes
|
||||
$path = str_replace(['..', '//'], ['', '/'], $path);
|
||||
|
||||
// Remove /api/files/records if present at the start
|
||||
$path = preg_replace('/^\/?(api\/files\/records\/)?/', '', $path);
|
||||
|
||||
return trim($path, '/');
|
||||
}
|
||||
|
||||
private function findRecordByPath(string $path): ?DiseaseRecord
|
||||
{
|
||||
// First find all diseases that have file fields
|
||||
$diseasesWithFiles = Disease::whereRaw("jsonb_path_exists(schema->'columns',
|
||||
'$.** ? (@.type == \"file\")')")
|
||||
->get();
|
||||
|
||||
if ($diseasesWithFiles->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get all file field names from the schemas
|
||||
$fileFields = [];
|
||||
foreach ($diseasesWithFiles as $disease) {
|
||||
$columns = $disease->schema['columns'] ?? [];
|
||||
foreach ($columns as $column) {
|
||||
if (($column['type'] ?? '') === 'file') {
|
||||
$fileFields[$disease->id][] = [
|
||||
'name' => $column['name'],
|
||||
'multiple' => $column['multiple'] ?? false
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build query based on discovered file fields
|
||||
return DiseaseRecord::where(function ($query) use ($path, $fileFields) {
|
||||
foreach ($fileFields as $diseaseId => $fields) {
|
||||
foreach ($fields as $field) {
|
||||
if ($field['multiple']) {
|
||||
// Handle array fields using ? operator
|
||||
$query->orWhere(function ($q) use ($path, $diseaseId, $field) {
|
||||
$q->where('disease_id', $diseaseId)
|
||||
->whereRaw("data->? ?? ?", [$field['name'], $path]);
|
||||
});
|
||||
} else {
|
||||
// Handle single file fields
|
||||
$query->orWhere(function ($q) use ($path, $diseaseId, $field) {
|
||||
$q->where('disease_id', $diseaseId)
|
||||
->whereRaw("data->>? = ?", [$field['name'], $path]);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
})->first();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Helpers\ResponseJson;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use App\Services\LogActionService;
|
||||
|
||||
class LogActionController extends Controller
|
||||
{
|
||||
protected LogActionService $logActionService;
|
||||
|
||||
public function __construct(LogActionService $logActinoService)
|
||||
{
|
||||
$this->logActionService = $logActinoService;
|
||||
}
|
||||
|
||||
public function getLogActions(Request $request){
|
||||
|
||||
[$success, $message, $logs] = $this->logActionService->getLogs($request->all());
|
||||
|
||||
if (!$success) {
|
||||
return ResponseJson::errorResponse($message);
|
||||
}
|
||||
|
||||
return ResponseJson::successResponse($message, $logs);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http;
|
||||
|
||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||
|
||||
class Kernel extends HttpKernel
|
||||
{
|
||||
/**
|
||||
* The application's global HTTP middleware stack.
|
||||
*
|
||||
* These middleware are run during every request to your application.
|
||||
*
|
||||
* @var array<int, class-string|string>
|
||||
*/
|
||||
protected $middleware = [
|
||||
// \App\Http\Middleware\TrustHosts::class,
|
||||
\App\Http\Middleware\TrustProxies::class,
|
||||
\Illuminate\Http\Middleware\HandleCors::class,
|
||||
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
||||
\App\Http\Middleware\TrimStrings::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
||||
//\App\Http\Middleware\EnsureAcceptJson::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's route middleware groups.
|
||||
*
|
||||
* @var array<string, array<int, class-string|string>>
|
||||
*/
|
||||
protected $middlewareGroups = [
|
||||
'web' => [
|
||||
\App\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
|
||||
'api' => [
|
||||
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
|
||||
\App\Http\Middleware\CheckApiKey::class,
|
||||
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
\App\Http\Middleware\EnsureAcceptJson::class,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's middleware aliases.
|
||||
*
|
||||
* Aliases may be used to conveniently assign middleware to routes and groups.
|
||||
*
|
||||
* @var array<string, class-string|string>
|
||||
*/
|
||||
protected $middlewareAliases = [
|
||||
'auth' => \App\Http\Middleware\Authenticate::class,
|
||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
||||
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
|
||||
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
|
||||
'signed' => \App\Http\Middleware\ValidateSignature::class,
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
|
||||
'checkRole' => \App\Http\Middleware\CheckRole::class,
|
||||
'checkDiseaseAccess' => \App\Http\Middleware\CheckDiseaseAccess::class,
|
||||
'checkCommentAuthorOrAdmin' => \App\Http\Middleware\CheckCommentAuthorOrAdmin::class,
|
||||
'ensureOperatorManagesDisease' => \App\Http\Middleware\EnsureOperatorManagesDisease::class,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ApiKeyMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
// TO be implemented in middleware
|
||||
$apiKey = $request->header('X-API-Key');
|
||||
|
||||
if (!$apiKey || !DB::table('api_keys')->where('key', $apiKey)->exists()) {
|
||||
return response()->json(['message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Helpers\ResponseJson;
|
||||
use Illuminate\Auth\Middleware\Authenticate as Middleware;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class Authenticate extends Middleware
|
||||
{
|
||||
/**
|
||||
* Get the path the user should be redirected to when they are not authenticated.
|
||||
*/
|
||||
protected function redirectTo(Request $request): ?string
|
||||
{
|
||||
if ($request->expectsJson()) {
|
||||
return ResponseJson::unauthorizeResponse('You are not logged in', []);
|
||||
}
|
||||
|
||||
// return ResponseJson::failedResponse('Bad Request', []);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use App\Helpers\ResponseJson;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CheckApiKey
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$apiKey = $request->header('X-API-Key');
|
||||
|
||||
if (!$apiKey || $apiKey !== config('app.api_key')) {
|
||||
return ResponseJson::unauthorizeResponse('Invalid API Key', []);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Comment;
|
||||
use App\Helpers\ResponseJson;
|
||||
|
||||
class CheckCommentAuthorOrAdmin
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$user = $request->user();
|
||||
$commentId = $request->route('commentId');
|
||||
$comment = Comment::find($commentId);
|
||||
|
||||
if (!$comment) {
|
||||
return ResponseJson::pageNotFoundResponse('Comment not found', []);
|
||||
}
|
||||
|
||||
if ($comment->user_id !== $user->id && $user->role !== 'admin') {
|
||||
return ResponseJson::forbidenResponse('Unauthorized to edit or delete this comment.', []);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
// app/Http/Middleware/CheckDiseaseAccess.php
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Helpers\ResponseJson;
|
||||
|
||||
class CheckDiseaseAccess
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if (!$user) {
|
||||
return ResponseJson::unauthorizeResponse('Unauthorized Access', []);
|
||||
}
|
||||
|
||||
if (in_array($user->role, ['operator', 'admin'])) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if (($user->role === 'peneliti') && $user->approval_status === 'approved') {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
return ResponseJson::forbidenResponse('User is not approved', []);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class CheckRole
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, ...$roles): Response
|
||||
{
|
||||
if (!in_array(Auth::user()->role, $roles)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Forbidden',
|
||||
'data' => []
|
||||
], 403);
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
|
||||
|
||||
class EncryptCookies extends Middleware
|
||||
{
|
||||
/**
|
||||
* The names of the cookies that should not be encrypted.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class EnsureAcceptJson
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
// Allow multipart/form-data for POST and PUT
|
||||
if (($request->isMethod('post') || $request->isMethod('put')) &&
|
||||
$request->hasHeader('Content-Type', 'multipart/form-data')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Allow file downloads to pass through
|
||||
if ($request->route() && strpos($request->route()->getName(), 'download') !== false) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Set Accept header to JSON for other requests
|
||||
$request->headers->set('Accept', 'application/json');
|
||||
|
||||
$response = $next($request);
|
||||
|
||||
// Only transform non-file responses
|
||||
if (!$response instanceof BinaryFileResponse &&
|
||||
!$response instanceof StreamedResponse) {
|
||||
if (!$response instanceof JsonResponse) {
|
||||
if ($response instanceof Response) {
|
||||
$content = $response->getContent();
|
||||
|
||||
$decoded = json_decode($content);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$data = $decoded;
|
||||
} else {
|
||||
$data = ['message' => $content];
|
||||
}
|
||||
|
||||
$response = response()->json(
|
||||
$data,
|
||||
$response->status()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$response->header('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Helpers\ResponseJson;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class EnsureOperatorManagesDisease
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @param string $diseaseParam
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$userId = auth()->id();
|
||||
|
||||
$user = User::findOrFail($userId);
|
||||
// echo($user);
|
||||
// Ensure user is authenticated
|
||||
if (!$user) {
|
||||
return ResponseJson::unauthorizeResponse('Unauthorized', []);
|
||||
}
|
||||
|
||||
// Allow admins to bypass the check
|
||||
if ($user->role === 'admin') {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Check if the user is an operator and manages the specified disease
|
||||
|
||||
$diseaseId = $request->route('diseaseId');
|
||||
// echo($diseaseId);
|
||||
|
||||
$managesDisease = $user->managed_diseases['disease_id'];
|
||||
// echo($managesDisease);
|
||||
// echo($managesDisease === $diseaseId);
|
||||
if ($user->role === 'operator') {
|
||||
if ($managesDisease != $diseaseId) {
|
||||
return ResponseJson::forbidenResponse('Access denied: Unauthorized to manage this disease', []);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
return ResponseJson::forbidenResponse('Access denied', []);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureUserIsApproved
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if ($request->user() && $request->user()->approval_status !== 'approved') {
|
||||
return response()->json(['message' => 'Your account is not approved yet.'], 403);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
|
||||
|
||||
class PreventRequestsDuringMaintenance extends Middleware
|
||||
{
|
||||
/**
|
||||
* The URIs that should be reachable while maintenance mode is enabled.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class RedirectIfAuthenticated
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, string ...$guards): Response
|
||||
{
|
||||
$guards = empty($guards) ? [null] : $guards;
|
||||
|
||||
foreach ($guards as $guard) {
|
||||
if (Auth::guard($guard)->check()) {
|
||||
return redirect(RouteServiceProvider::HOME);
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
|
||||
|
||||
class TrimStrings extends Middleware
|
||||
{
|
||||
/**
|
||||
* The names of the attributes that should not be trimmed.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
'current_password',
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Http\Middleware\TrustHosts as Middleware;
|
||||
|
||||
class TrustHosts extends Middleware
|
||||
{
|
||||
/**
|
||||
* Get the host patterns that should be trusted.
|
||||
*
|
||||
* @return array<int, string|null>
|
||||
*/
|
||||
public function hosts(): array
|
||||
{
|
||||
return [
|
||||
$this->allSubdomainsOfApplicationUrl(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Http\Middleware\TrustProxies as Middleware;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TrustProxies extends Middleware
|
||||
{
|
||||
/**
|
||||
* The trusted proxies for this application.
|
||||
*
|
||||
* @var array<int, string>|string|null
|
||||
*/
|
||||
protected $proxies;
|
||||
|
||||
/**
|
||||
* The headers that should be used to detect proxies.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $headers =
|
||||
Request::HEADER_X_FORWARDED_FOR |
|
||||
Request::HEADER_X_FORWARDED_HOST |
|
||||
Request::HEADER_X_FORWARDED_PORT |
|
||||
Request::HEADER_X_FORWARDED_PROTO |
|
||||
Request::HEADER_X_FORWARDED_AWS_ELB;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Routing\Middleware\ValidateSignature as Middleware;
|
||||
|
||||
class ValidateSignature extends Middleware
|
||||
{
|
||||
/**
|
||||
* The names of the query string parameters that should be ignored.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
// 'fbclid',
|
||||
// 'utm_campaign',
|
||||
// 'utm_content',
|
||||
// 'utm_medium',
|
||||
// 'utm_source',
|
||||
// 'utm_term',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
|
||||
|
||||
class VerifyCsrfToken extends Middleware
|
||||
{
|
||||
/**
|
||||
* The URIs that should be excluded from CSRF verification.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CreateCommentRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'diseaseId' => $this->route('diseaseId')
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'diseaseId' => 'required|exists:diseases,id',
|
||||
'content' => 'required|string|max:1000',
|
||||
'parent_id' => 'nullable|exists:comments,id',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use App\Helpers\ResponseJson;
|
||||
use App\Models\Disease;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CreateDiseaseRecordRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'diseaseId' => $this->route('diseaseId')
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
|
||||
$mimeTypeMap = [
|
||||
'audio' => [
|
||||
'aac', 'midi', 'mp3', 'ogg', 'wav', 'webm', 'flac', 'aiff', 'amr', 'opus'
|
||||
],
|
||||
'video' => [
|
||||
'mp4', 'avi', 'mkv', 'webm', 'ogg', '3gp', 'flv', 'mov',
|
||||
'wmv', 'mpg', 'mpeg', 'm4v', 'h264', 'hevc'
|
||||
],
|
||||
'image' => [
|
||||
'jpeg', 'jpg', 'png', 'gif', 'bmp', 'webp', 'tiff', 'svg', 'heif', 'heic',
|
||||
'ico', 'jp2', 'j2k', 'avif'
|
||||
],
|
||||
'text-document' => [
|
||||
'pdf', 'doc', 'docx', 'txt'
|
||||
],
|
||||
'compressed-document' => [
|
||||
'zip', '7z', 'tar', 'gz', 'rar', 'bz2', 'xz'
|
||||
],
|
||||
'spreadsheet' => [
|
||||
'xls', 'xlsx', 'csv', 'ods'
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
|
||||
$rules = [
|
||||
'diseaseId' => 'required|integer|exists:diseases,id',
|
||||
];
|
||||
|
||||
if ($this->filled('diseaseId')) {
|
||||
try {
|
||||
$disease = Disease::findOrFail($this->route('diseaseId'));
|
||||
|
||||
if ($disease && isset($disease->schema['columns'])){
|
||||
foreach ($disease->schema['columns'] as $column) {
|
||||
$columnName = $column['name'];
|
||||
$columnRules = [];
|
||||
|
||||
// Base validation based on type
|
||||
switch ($column['type']) {
|
||||
case 'string':
|
||||
case 'text':
|
||||
$columnRules[] = 'required|string';
|
||||
break;
|
||||
case 'integer':
|
||||
$columnRules[] = 'required|integer';
|
||||
break;
|
||||
case 'decimal':
|
||||
case 'float':
|
||||
$columnRules[] = 'required|numeric';
|
||||
break;
|
||||
case 'datetime':
|
||||
case 'date':
|
||||
$columnRules[] = 'required|date';
|
||||
break;
|
||||
case 'time':
|
||||
$columnRules[] = 'required|date_format:H:i';
|
||||
break;
|
||||
case 'file':
|
||||
$columnRules[] = 'required';
|
||||
|
||||
if (!empty($column['multiple'])) {
|
||||
// Multiple file upload handling
|
||||
$rules[$columnName] = 'required|array|min:1';
|
||||
|
||||
$fileRules = ['file', 'max:20480'];
|
||||
|
||||
if (!empty($column['format'])) {
|
||||
$category = trim($column['format'], '.');
|
||||
if (array_key_exists($category, $mimeTypeMap)) {
|
||||
$formats = $mimeTypeMap[$category];
|
||||
$fileRules[] = 'mimes:' . implode(',', $formats);
|
||||
} else {
|
||||
Log::warning("Unsupported format '{$column['format']}' for column '{$columnName}'");
|
||||
}
|
||||
}
|
||||
$rules[$columnName . '.*'] = implode('|', $fileRules);
|
||||
// print_r($rules);
|
||||
} else {
|
||||
// Single file upload handling.
|
||||
$columnRules[] = 'required|file|max:20480';
|
||||
|
||||
if (!empty($column['format'])) {
|
||||
$category = trim($column['format'], '.');
|
||||
if (array_key_exists($category, $mimeTypeMap)) {
|
||||
$formats = $mimeTypeMap[$category];
|
||||
$columnRules[] = 'mimes:' . implode(',', $formats);
|
||||
} else {
|
||||
Log::warning("Unsupported format '{$column['format']}' for column '{$columnName}'");
|
||||
}
|
||||
}
|
||||
|
||||
$rules[$columnName] = implode('|', $columnRules);
|
||||
// print_r($rules);
|
||||
}
|
||||
|
||||
// print_r($columnRules);
|
||||
break;
|
||||
case 'boolean':
|
||||
$columnRules[] = 'required|boolean';
|
||||
break;
|
||||
case 'enum':
|
||||
if (!empty($column['options']) && is_array($column['options'])) {
|
||||
$columnRules[] = 'required|string';
|
||||
$columnRules[] = 'in:' . implode(',', $column['options']);
|
||||
}
|
||||
break;
|
||||
case 'email':
|
||||
$columnRules[] = 'required|email';
|
||||
break;
|
||||
case 'phone':
|
||||
$columnRules[] = 'required|string';
|
||||
$columnRules[] = 'regex:/^([0-9\s\-\+\(\)]*)$/';
|
||||
break;
|
||||
}
|
||||
|
||||
if (!empty($columnRules)) {
|
||||
$rules[$columnName] = implode('|', $columnRules);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Error loading disease schema: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
|
||||
public function validated($key = null, $default = null): array
|
||||
{
|
||||
$validated = parent::validated();
|
||||
|
||||
return [
|
||||
'diseaseId' => $validated['diseaseId'],
|
||||
'data' => collect($validated)
|
||||
->except('diseaseId')
|
||||
->toArray()
|
||||
];
|
||||
}
|
||||
|
||||
protected function failedValidation(Validator $validator)
|
||||
{
|
||||
throw new HttpResponseException(
|
||||
ResponseJson::failedResponse('Validation error', $validator->errors()->toArray())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// public function messages(): array
|
||||
// {
|
||||
// return [
|
||||
// 'diseaseId.required' => 'ID penyakit harus diisi.',
|
||||
// 'diseaseId.integer' => 'ID penyakit harus berupa angka.',
|
||||
// 'diseaseId.exists' => 'ID penyakit tidak ditemukan dalam database.',
|
||||
|
||||
// 'data.*.required' => ':attribute harus diisi.',
|
||||
// 'data.*.string' => ':attribute harus berupa teks.',
|
||||
// 'data.*.integer' => ':attribute harus berupa angka bulat.',
|
||||
// 'data.*.numeric' => ':attribute harus berupa angka.',
|
||||
// 'data.*.date' => ':attribute harus berupa tanggal yang valid.',
|
||||
// 'data.*.date_format' => ':attribute harus dalam format waktu yang valid (HH:MM:SS).',
|
||||
// 'data.*.boolean' => ':attribute harus berupa benar atau salah.',
|
||||
// 'data.*.in' => ':attribute harus salah satu dari pilihan yang tersedia: :values.',
|
||||
// 'data.*.email' => ':attribute harus berupa alamat email yang valid.',
|
||||
// 'data.*.regex' => ':attribute harus berupa nomor telepon yang valid.',
|
||||
|
||||
// // 'data.*.file' => ':attribute harus berupa file.',
|
||||
// // 'data.*.mimes' => ':attribute harus bertipe: :values.',
|
||||
|
||||
// // 'data.*.array' => ':attribute harus dalam format array jika berisi banyak file.',
|
||||
// // Uncommented additional validation for required/nullable
|
||||
// // 'data.*.nullable' => ':attribute boleh dikosongkan.',
|
||||
// // Rules for minimum and maximum values
|
||||
// // 'data.*.min' => ':attribute harus minimal :min.',
|
||||
// // 'data.*.min_digits' => ':attribute harus minimal :min digit.',
|
||||
// // 'data.*.max' => ':attribute maksimal :max.',
|
||||
// // 'data.*.max_digits' => ':attribute maksimal :max digit.',
|
||||
// ];
|
||||
// }
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use App\Helpers\ResponseJson;
|
||||
|
||||
class CreateDiseaseRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string|max:255',
|
||||
'deskripsi' => 'nullable|string|max:65535',
|
||||
'visibilitas' => 'required|in:publik,privat',
|
||||
'schema' => 'required|array',
|
||||
'schema.columns' => 'required|array',
|
||||
'schema.columns.*.name' => [
|
||||
'required',
|
||||
'string',
|
||||
'regex:/^[a-zA-Z0-9_]+$/',
|
||||
'distinct'
|
||||
],
|
||||
'schema.columns.*.type' => 'required|string|in:string,text,integer,decimal,float,date,datetime,time,file,boolean,email,phone',
|
||||
//'schema.columns.*.type' => 'required|string|in:string,integer,enum,decimal,date,file,time,datetime,boolean,array,float,text,email,phone,json,range',
|
||||
'schema.columns.*.options' => 'required_if:schema.columns.*.type,enum|array',
|
||||
'schema.columns.*.format' => 'required_if:schema.columns.*.type,file|string',
|
||||
'schema.columns.*.multiple' => 'boolean|nullable',
|
||||
'schema.columns.*.is_visible' => 'boolean|nullable',
|
||||
//'schema.columns.*.required' => 'boolean|nullable',
|
||||
//'schema.columns.*.min' => 'numeric|nullable',
|
||||
//'schema.columns.*.max' => 'numeric|nullable',
|
||||
//'schema.columns.*.step' => 'numeric|nullable',
|
||||
//'schema.columns.*.default' => 'nullable',
|
||||
//'schema.columns.*.description' => 'string|nullable',
|
||||
//'schema.columns.*.unit' => 'string|nullable',
|
||||
'cover_page' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'Nama penyakit harus diisi.',
|
||||
'name.max' => 'Nama penyakit maksimal 255 karakter.',
|
||||
'deskripsi.max' => 'Deskripsi penyakit maksimal 65535 karakter.',
|
||||
'schema.required' => 'Schema harus diisi.',
|
||||
'schema.array' => 'Schema harus dalam format yang valid.',
|
||||
'schema.columns.required' => 'Columns dalam schema harus diisi.',
|
||||
'schema.columns.array' => 'Columns harus dalam format array.',
|
||||
'schema.columns.*.name.required' => 'Nama kolom harus diisi.',
|
||||
'schema.columns.*.name.distinct' => 'Nama kolom tidak boleh duplikat',
|
||||
'schema.columns.*.name.regex' => 'Nama kolom hanya boleh berisi huruf, angka, dan underscore tanpa spasi.',
|
||||
'schema.columns.*.type.required' => 'Tipe kolom harus diisi.',
|
||||
'schema.columns.*.type.in' => 'Tipe kolom tidak valid.',
|
||||
'schema.columns.*.options.required_if' => 'Options harus diisi untuk tipe enum.',
|
||||
'schema.columns.*.format.required_if' => 'Format harus diisi untuk tipe file.',
|
||||
'cover_page.image' => 'Cover page harus berupa file gambar.',
|
||||
'cover_page.mimes' => 'Cover page harus berupa file bertipe: jpeg, png, jpg, gif, atau svg.',
|
||||
'cover_page.max' => 'Ukuran cover page maksimal 2MB.',
|
||||
];
|
||||
}
|
||||
|
||||
protected function failedValidation(Validator $validator)
|
||||
{
|
||||
$errors = $validator->errors()->toArray();
|
||||
$pesan = "Field Error";
|
||||
|
||||
foreach ($errors as $field => $message) {
|
||||
if ($message[0] === 'Nama kolom tidak boleh duplikat') {
|
||||
$pesan .= " - Nama kolom tidak boleh duplikat";
|
||||
}
|
||||
|
||||
throw new HttpResponseException(ResponseJson::failedResponse($pesan, [$field => $message[0]]));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use App\Helpers\ResponseJson;
|
||||
|
||||
|
||||
class CreateUserRequest extends FormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string|max:100',
|
||||
'email' => 'required|email|unique:users,email|max:100',
|
||||
'password' => 'required|string|min:8|max:64|confirmed',
|
||||
// Front end add 'password_confirmation' field
|
||||
'role' => 'required|in:admin,operator,peneliti',
|
||||
'institution' => 'nullable|string|max:255',
|
||||
'gender' => 'nullable|in:male,female,prefer not to say',
|
||||
'phone_number' => 'nullable|string|max:50',
|
||||
'tujuan_permohonan' => 'nullable|string|max:65535',
|
||||
'disease_id' => 'required_if:role,operator|integer|exists:diseases,id'
|
||||
];
|
||||
}
|
||||
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
'name.required' => 'Nama harus diisi.',
|
||||
'name.max' => 'Nama maksimal 100 karakter',
|
||||
'email.required' => 'Email harus diisi.',
|
||||
'email.email' => 'Email tidak valid.',
|
||||
'email.max' => 'Email maksimal 100 karakter',
|
||||
'email.unique' => 'Email tersebut sudah terdaftar',
|
||||
'password.required' => 'Password harus diisi.',
|
||||
'password.confirmed' => 'Password konfirmasi tidak sesuai.',
|
||||
'password.min' => 'Password minimal 8 karakter',
|
||||
'password.max' => 'Password maksimal 64 karakter',
|
||||
'role.required' => 'Role harus diisi',
|
||||
'role.in' => 'Role tidak sesuai',
|
||||
'institution.max' => 'Institusi maksimal 255 karakter.',
|
||||
'gender.in' => 'Gender tidak sesuai.',
|
||||
'phone_number.max' => 'Nomor telepon maksimal 50 karakter.',
|
||||
'tujuan_permohonan.max' => 'Tujuan permohonan maksimal 65535 karakter',
|
||||
'disease_id.required_if' => 'Disease IDs harus diisi jika role adalah operator.',
|
||||
'disease_id.integer' => 'Disease IDs harus berupa integer.',
|
||||
'disease_id.*.exists' => 'Setiap Disease ID yang dipilih harus valid dan ada dalam database.',
|
||||
];
|
||||
}
|
||||
|
||||
protected function failedValidation(Validator $validator)
|
||||
{
|
||||
$errors = $validator->errors()->toArray();
|
||||
|
||||
foreach ($errors as $field => $message) {
|
||||
throw new HttpResponseException(ResponseJson::failedResponse("field error", [$field => $message[0]]));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class DeleteCommentRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'diseaseId' => $this->route('diseaseId'),
|
||||
'commentId' => $this->route('commentId')
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'diseaseId' => 'required|exists:diseases,id',
|
||||
'commentId' => 'required|exists:comments,id',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use App\Helpers\ResponseJson;
|
||||
|
||||
class DeleteDiseaseRecordRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'diseaseId' => $this->route('diseaseId'),
|
||||
'recordId' => $this->route('recordId'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'diseaseId' => 'required|integer|exists:diseases,id',
|
||||
'recordId' => [
|
||||
'required',
|
||||
'integer',
|
||||
'exists:disease_records,id',
|
||||
function ($attribute, $value, $fail) {
|
||||
$exists = \App\Models\DiseaseRecord::where('id', $value)
|
||||
->where('disease_id', $this->route('diseaseId'))
|
||||
->exists();
|
||||
|
||||
if (!$exists) {
|
||||
$fail('The selected record does not belong to the specified disease.');
|
||||
}
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
protected function failedValidation(Validator $validator)
|
||||
{
|
||||
throw new HttpResponseException(
|
||||
ResponseJson::failedResponse('Validation error', $validator->errors()->toArray())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use App\Helpers\ResponseJson;
|
||||
|
||||
class DiseaseIdRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'diseaseId' => $this->route('diseaseId'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'diseaseId' => 'required|integer|exists:diseases,id',
|
||||
];
|
||||
}
|
||||
|
||||
protected function failedValidation(Validator $validator)
|
||||
{
|
||||
throw new HttpResponseException(
|
||||
ResponseJson::failedResponse('Validation error', $validator->errors()->toArray())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class EditCommentRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'diseaseId' => $this->route('diseaseId'),
|
||||
'commentId' => $this->route('commentId')
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'diseaseId' => 'required|integer|exists:diseases,id',
|
||||
'commentId' => 'required|integer|exists:comments,id',
|
||||
'content' => 'required|string|max:1000',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use App\Helpers\ResponseJson;
|
||||
use App\Models\Disease;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class EditDiseaseRecordRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'diseaseId' => $this->route('diseaseId'),
|
||||
'recordId' => $this->route('recordId')
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
|
||||
$mimeTypeMap = [
|
||||
'audio' => [
|
||||
'aac', 'midi', 'mp3', 'ogg', 'wav', 'webm', 'flac', 'aiff', 'amr', 'opus'
|
||||
],
|
||||
'video' => [
|
||||
'mp4', 'avi', 'mkv', 'webm', 'ogg', '3gp', 'flv', 'mov',
|
||||
'wmv', 'mpg', 'mpeg', 'm4v', 'h264', 'hevc'
|
||||
],
|
||||
'image' => [
|
||||
'jpeg', 'jpg', 'png', 'gif', 'bmp', 'webp', 'tiff', 'svg', 'heif', 'heic',
|
||||
'ico', 'jp2', 'j2k', 'avif'
|
||||
],
|
||||
'text-document' => [
|
||||
'pdf', 'doc', 'docx', 'txt'
|
||||
],
|
||||
'compressed-document' => [
|
||||
'zip', '7z', 'tar', 'gz', 'rar', 'bz2', 'xz'
|
||||
],
|
||||
'spreadsheet' => [
|
||||
'xls', 'xlsx', 'csv', 'ods'
|
||||
],
|
||||
];
|
||||
|
||||
$rules = [
|
||||
'diseaseId' => 'required|integer|exists:diseases,id',
|
||||
'recordId' => [
|
||||
'required',
|
||||
'integer',
|
||||
'exists:disease_records,id',
|
||||
function ($attribute, $value, $fail) {
|
||||
$exists = \App\Models\DiseaseRecord::where('id', $value)
|
||||
->where('disease_id', $this->route('diseaseId'))
|
||||
->exists();
|
||||
|
||||
if (!$exists) {
|
||||
$fail('The selected record does not belong to the specified disease.');
|
||||
}
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
if ($this->filled('diseaseId')) {
|
||||
try {
|
||||
$disease = Disease::findOrFail($this->route('diseaseId'));
|
||||
if ($disease && isset($disease->schema['columns'])) {
|
||||
|
||||
foreach ($disease->schema['columns'] as $column) {
|
||||
$columnName = $column['name'];
|
||||
$columnRules = [];
|
||||
|
||||
switch ($column['type']) {
|
||||
case 'string':
|
||||
case 'text':
|
||||
$columnRules[] = 'sometimes|required|string';
|
||||
break;
|
||||
case 'integer':
|
||||
$columnRules[] = 'sometimes|required|integer';
|
||||
break;
|
||||
case 'decimal':
|
||||
case 'float':
|
||||
$columnRules[] = 'sometimes|required|numeric';
|
||||
break;
|
||||
case 'datetime':
|
||||
case 'date':
|
||||
$columnRules[] = 'sometimes|required|date';
|
||||
break;
|
||||
case 'time':
|
||||
$columnRules[] = 'sometimes|required|date_format:H:i';
|
||||
break;
|
||||
case 'file':
|
||||
$columnRules[] = 'sometimes|required'; // Ensures a file is uploaded.
|
||||
|
||||
// Check if a 'format' is defined in the schema for the column.
|
||||
if (!empty($column['multiple'])) {
|
||||
// Ensure the field is an array of files.
|
||||
$rules[$columnName] = 'sometimes|required|array|min:1';
|
||||
|
||||
// Apply validation to each file in the array.
|
||||
$fileRules = ['file', 'max:20480']; // Start with basic file validation.
|
||||
|
||||
// Add MIME type validation if a format is specified.
|
||||
if (!empty($column['format'])) {
|
||||
$category = trim($column['format'], '.'); // Strip the dot from the format.
|
||||
if (array_key_exists($category, $mimeTypeMap)) {
|
||||
$formats = $mimeTypeMap[$category];
|
||||
$fileRules[] = 'mimes:' . implode(',', $formats);
|
||||
} else {
|
||||
Log::warning("Unsupported format '{$column['format']}' for column '{$columnName}'");
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the rules to each file in the array.
|
||||
$rules[$columnName . '.*'] = implode('|', $fileRules);
|
||||
// print_r($rules);
|
||||
} else {
|
||||
// Single file upload handling.
|
||||
$columnRules[] = 'sometimes|required|file|max:20480';
|
||||
|
||||
// Add MIME type validation if a format is specified.
|
||||
if (!empty($column['format'])) {
|
||||
$category = trim($column['format'], '.'); // Strip the dot from the format.
|
||||
if (array_key_exists($category, $mimeTypeMap)) {
|
||||
$formats = $mimeTypeMap[$category];
|
||||
$columnRules[] = 'mimes:' . implode(',', $formats);
|
||||
} else {
|
||||
Log::warning("Unsupported format '{$column['format']}' for column '{$columnName}'");
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the accumulated rules for a single file.
|
||||
$rules[$columnName] = implode('|', $columnRules);
|
||||
// print_r($rules);
|
||||
}
|
||||
|
||||
// print_r($columnRules);
|
||||
break;
|
||||
case 'boolean':
|
||||
$columnRules[] = 'sometimes|required|boolean';
|
||||
break;
|
||||
case 'enum':
|
||||
if (!empty($column['options']) && is_array($column['options'])) {
|
||||
$columnRules[] = 'sometimes|required|string';
|
||||
$columnRules[] = 'in:' . implode(',', $column['options']);
|
||||
}
|
||||
break;
|
||||
case 'email':
|
||||
$columnRules[] = 'sometimes|required|email';
|
||||
break;
|
||||
case 'phone':
|
||||
$columnRules[] = 'sometimes|required|string';
|
||||
$columnRules[] = 'regex:/^([0-9\s\-\+\(\)]*)$/';
|
||||
break;
|
||||
}
|
||||
|
||||
// // Add required/nullable validation
|
||||
// if (!empty($column['required'])) {
|
||||
// $columnRules[] = 'required';
|
||||
// } else {
|
||||
// $columnRules[] = 'nullable';
|
||||
// }
|
||||
|
||||
// // Add min/max validation if specified
|
||||
// if (!empty($column['min'])) {
|
||||
// switch ($column['type']) {
|
||||
// case 'string':
|
||||
// case 'text':
|
||||
// $columnRules[] = 'min:' . $column['min'];
|
||||
// break;
|
||||
// case 'integer':
|
||||
// case 'decimal':
|
||||
// case 'float':
|
||||
// $columnRules[] = 'min_digits:' . $column['min'];
|
||||
// break;
|
||||
// case 'file':
|
||||
// $columnRules[] = 'min:' . $column['min']; // min in kilobytes
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (!empty($column['max'])) {
|
||||
// switch ($column['type']) {
|
||||
// case 'string':
|
||||
// case 'text':
|
||||
// $columnRules[] = 'max:' . $column['max'];
|
||||
// break;
|
||||
// case 'integer':
|
||||
// case 'decimal':
|
||||
// case 'float':
|
||||
// $columnRules[] = 'max_digits:' . $column['max'];
|
||||
// break;
|
||||
// case 'file':
|
||||
// $columnRules[] = 'max:' . $column['max']; // max in kilobytes
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
if (!empty($columnRules)) {
|
||||
$rules[$columnName] = implode('|', $columnRules);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Error loading disease schema: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function validated($key = null, $default = null): array
|
||||
{
|
||||
$validated = parent::validated();
|
||||
|
||||
return [
|
||||
'diseaseId' => $validated['diseaseId'],
|
||||
'data' => collect($validated)
|
||||
->except('diseaseId', 'recordId')
|
||||
->toArray()
|
||||
];
|
||||
}
|
||||
|
||||
protected function failedValidation(Validator $validator)
|
||||
{
|
||||
throw new HttpResponseException(
|
||||
ResponseJson::failedResponse('Validation error', $validator->errors()->toArray())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use App\Helpers\ResponseJson;
|
||||
|
||||
class EditDiseaseRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'diseaseId' => $this->route('diseaseId'),
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'diseaseId' => 'required|integer|exists:diseases,id',
|
||||
'name' => 'sometimes|required|string|max:255',
|
||||
'deskripsi' => 'sometimes|nullable|string|max:65535',
|
||||
'visibilitas' => 'sometimes|required|in:publik,privat',
|
||||
//'schema' => 'required|json',
|
||||
'cover_page' => 'sometimes|nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
'name.required' => 'Nama penyakit harus diisi.',
|
||||
'name.max' => 'Nama penyakit maksimal 255 karakter.',
|
||||
'deskripsi.max' => 'Deskripsi penyakit maksimal 65535 karakter.',
|
||||
//'schema.required' => 'Schema harus diisi.',
|
||||
//'schema.json' => 'Schema harus dalam format JSON yang valid.',
|
||||
'cover_page.image' => 'Cover page harus berupa file gambar.',
|
||||
'cover_page.mimes' => 'Cover page harus berupa file bertipe: jpeg, png, jpg, gif, atau svg.',
|
||||
'cover_page.max' => 'Ukuran cover page maksimal 2MB.',
|
||||
];
|
||||
}
|
||||
|
||||
protected function failedValidation(Validator $validator)
|
||||
{
|
||||
$errors = $validator->errors()->toArray();
|
||||
|
||||
foreach ($errors as $field => $message) {
|
||||
throw new HttpResponseException(ResponseJson::failedResponse("field error", [$field => $message[0]]));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use App\Helpers\ResponseJson;
|
||||
use App\Models\User;
|
||||
|
||||
class EditUserRequest extends FormRequest
|
||||
{
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
|
||||
$user = $this->route('userId');
|
||||
$userRecord = User::find($user);
|
||||
$userRole = $userRecord ? $userRecord->role : null;
|
||||
|
||||
return [
|
||||
'name' => 'sometimes|required|string|max:100',
|
||||
'email' => 'sometimes|nullable|email|unique:users,email,' . $user . '|max:100',
|
||||
'password' => 'sometimes|nullable|string|min:8|max:64|confirmed',
|
||||
'role' => 'sometimes|required|in:admin,operator,peneliti',
|
||||
'institution' => 'nullable|string|max:255',
|
||||
'gender' => 'nullable|in:male,female,prefer not to say',
|
||||
'phone_number' => 'nullable|string|max:50',
|
||||
'approval_status' => 'sometimes|in:approved,pending,rejected',
|
||||
'disease_id' => 'sometimes|integer|exists:diseases,id|required_if:role,operator'
|
||||
];
|
||||
}
|
||||
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
'name.required' => 'Nama harus diisi.',
|
||||
'name.max' => 'Nama maksimal 100 karakter',
|
||||
'email.email' => 'Email tidak valid.',
|
||||
'email.max' => 'Email maksimal 100 karakter',
|
||||
'email.unique' => 'Email tersebut sudah terdaftar',
|
||||
'password.min' => 'Password minimal 8 karakter',
|
||||
'password.max' => 'Password maksimal 64 karakter',
|
||||
'password.confirmed' => 'Password konfirmasi tidak sesuai.',
|
||||
'role.required' => 'Role harus diisi',
|
||||
'role.in' => 'Role tidak sesuai',
|
||||
'institution.max' => 'Institusi maksimal 255 karakter.',
|
||||
'gender.in' => 'Gender tidak sesuai.',
|
||||
'phone_number.max' => 'Nomor telepon maksimal 50 karakter.',
|
||||
'approval_status.required' => 'Status Approval harus diisi.',
|
||||
'approval_status.in' => 'Status Approval tidak sesuai.',
|
||||
'disease_id.required_if' => 'Disease IDs harus diisi jika role adalah operator.',
|
||||
'disease_id.integer' => 'Disease IDs harus berupa integer.',
|
||||
'disease_id.*.exists' => 'Setiap Disease ID yang dipilih harus valid dan ada dalam database.',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
protected function failedValidation(Validator $validator)
|
||||
{
|
||||
$errors = $validator->errors()->toArray();
|
||||
|
||||
foreach ($errors as $field => $message) {
|
||||
throw new HttpResponseException(ResponseJson::failedResponse("field error", [$field => $message[0]]));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use App\Helpers\ResponseJson;
|
||||
|
||||
class IndexDiseaseRecordRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'diseaseId' => $this->route('diseaseId'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'diseaseId' => 'required|integer|exists:diseases,id',
|
||||
];
|
||||
}
|
||||
|
||||
protected function failedValidation(Validator $validator)
|
||||
{
|
||||
throw new HttpResponseException(
|
||||
ResponseJson::failedResponse('Validation error', $validator->errors()->toArray())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use App\Helpers\ResponseJson;
|
||||
|
||||
|
||||
class LoginUserRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => 'required|email|max:100',
|
||||
'password' => 'required|min:8|max:64',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
'email.required' => 'Email harus diisi',
|
||||
'email.email' => 'Email tidak valid',
|
||||
'email.max' => 'Email Maksimal 100 Karakter',
|
||||
'password.required' => 'Password Harus diisi',
|
||||
'password.min' => 'Password Minimal 8 Karakter',
|
||||
'password.max' => 'Password Maksimal 64 Karakter',
|
||||
];
|
||||
}
|
||||
|
||||
protected function failedValidation(Validator $validator)
|
||||
{
|
||||
$errors = $validator->errors()->toArray();
|
||||
|
||||
foreach ($errors as $field => $message) {
|
||||
throw new HttpResponseException(ResponseJson::failedResponse("field error", [$field => $message[0]]));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use App\Helpers\ResponseJson;
|
||||
|
||||
class RegisterUserRequest extends FormRequest
|
||||
{
|
||||
public function authorize()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string|max:100',
|
||||
'email' => 'required|email|unique:users,email|max:100',
|
||||
'password' => 'required|string|min:8|max:64|confirmed',
|
||||
//Front end include: 'password_confirmation' field
|
||||
'institution' => 'required|string|max:255',
|
||||
'gender' => 'required|in:male,female,prefer not to say',
|
||||
'phone_number' => 'required|string|max:50',
|
||||
'tujuan_permohonan' => 'required|string|max:65535',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
'name.required' => 'Nama harus diisi.',
|
||||
'name.max' => 'Nama maksimal 100 karakter',
|
||||
'email.required' => 'Email harus diisi.',
|
||||
'email.email' => 'Email tidak valid.',
|
||||
'email.max' => 'Email maksimal 100 karakter',
|
||||
'email.unique' => 'Email tersebut sudah terdaftar, silahkan login.',
|
||||
'password.required' => 'Password harus diisi.',
|
||||
'password.confirmed' => 'Password konfirmasi tidak sesuai.',
|
||||
'password.min' => 'Password minimal 8 karakter',
|
||||
'password.max' => 'Password maksimal 64 karakter',
|
||||
'institution.max' => 'Institusi maksimal 255 karakter.',
|
||||
'gender.in' => 'Gender tidak sesuai.',
|
||||
'phone_number.max' => 'Nomor telepon maksimal 50 karakter.',
|
||||
'tujuan_permohonan.max' => 'Tujuan permohonan maksimal 65535 karakter',
|
||||
];
|
||||
}
|
||||
|
||||
protected function failedValidation(Validator $validator)
|
||||
{
|
||||
$errors = $validator->errors()->toArray();
|
||||
|
||||
foreach ($errors as $field => $message) {
|
||||
throw new HttpResponseException(ResponseJson::failedResponse("field error", [$field => $message[0]]));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use App\Helpers\ResponseJson;
|
||||
|
||||
class ShowDiseaseRecordRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'diseaseId' => $this->route('diseaseId'),
|
||||
'recordId' => $this->route('recordId')
|
||||
]);
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'diseaseId' => 'required|integer|exists:diseases,id',
|
||||
'recordId' => [
|
||||
'required',
|
||||
'integer',
|
||||
'exists:disease_records,id',
|
||||
function ($attribute, $value, $fail) {
|
||||
$exists = \App\Models\DiseaseRecord::where('id', $value)
|
||||
->where('disease_id', $this->route('diseaseId'))
|
||||
->exists();
|
||||
|
||||
if (!$exists) {
|
||||
$fail('The selected record does not belong to the specified disease.');
|
||||
}
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
protected function failedValidation(Validator $validator)
|
||||
{
|
||||
throw new HttpResponseException(
|
||||
ResponseJson::failedResponse('Validation error', $validator->errors()->toArray())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use App\Helpers\ResponseJson;
|
||||
|
||||
class UserIdRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'userId' => $this->route('userId'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'userId' => 'required|integer|exists:users,id',
|
||||
];
|
||||
}
|
||||
|
||||
protected function failedValidation(Validator $validator)
|
||||
{
|
||||
throw new HttpResponseException(
|
||||
ResponseJson::failedResponse('Validation error', $validator->errors()->toArray())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class UserResource extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* Transform the resource collection into an array.
|
||||
*
|
||||
* @return array<int|string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return parent::toArray($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Comment extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = ['disease_id', 'user_id', 'content', 'parent_id', 'status'];
|
||||
|
||||
public function disease()
|
||||
{
|
||||
return $this->belongsTo(Disease::class);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function replies()
|
||||
{
|
||||
return $this->hasMany(Comment::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function parent()
|
||||
{
|
||||
return $this->belongsTo(Comment::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function getContentWithPlaceholderAttribute()
|
||||
{
|
||||
return $this->status === 'deleted' ? 'Deleted Comment' : $this->content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class Disease extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'deskripsi',
|
||||
'visibilitas',
|
||||
'schema',
|
||||
'cover_page'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'schema' => 'json'
|
||||
];
|
||||
|
||||
public function operators()
|
||||
{
|
||||
return $this->belongsToMany(User::class, 'disease_operator', 'disease_id', 'user_id');
|
||||
}
|
||||
|
||||
public function diseaseRecords()
|
||||
{
|
||||
return $this->hasMany(DiseaseRecord::class);
|
||||
}
|
||||
|
||||
protected function getCoverPageUrlAttribute(): ?string
|
||||
{
|
||||
if ($this->cover_page) {
|
||||
//$domain = env('APP_URL');
|
||||
return Storage::url('public/' . $this->cover_page);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getExportUrlAttribute(): string
|
||||
{
|
||||
return url("/api/diseases/{$this->id}/export");
|
||||
}
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
$array = parent::toArray();
|
||||
$array['cover_page_url'] = $this->cover_page_url;
|
||||
$array['export_url'] = $this->export_url;
|
||||
return $array;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class DiseaseRecord extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['disease_id', 'data'];
|
||||
|
||||
protected $casts = [
|
||||
'data' => 'json',
|
||||
];
|
||||
|
||||
public function disease()
|
||||
{
|
||||
return $this->belongsTo(Disease::class);
|
||||
}
|
||||
|
||||
protected function getExportUrlAttribute(): string
|
||||
{
|
||||
return url("/api/diseases/{$this->disease_id}/records/{$this->id}/export");
|
||||
}
|
||||
|
||||
protected function getPublicFileUrl($path): string
|
||||
{
|
||||
if (empty($path)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return url("/storage/public/{$path}");
|
||||
}
|
||||
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
$array = parent::toArray();
|
||||
$array['export_url'] = $this->export_url;
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Log extends Model
|
||||
{
|
||||
protected $fillable = ['user_id', 'action', 'model', 'model_id', 'changes', 'description', 'is_success', 'timestamp'];
|
||||
|
||||
/**
|
||||
* Relationship with User
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically resolve the related model instance
|
||||
*/
|
||||
public function relatedModel()
|
||||
{
|
||||
if (!class_exists($this->model)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->model::find($this->model_id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasApiTokens, HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'institution',
|
||||
'gender',
|
||||
'phone_number',
|
||||
'tujuan_permohonan',
|
||||
'role',
|
||||
'approval_status',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $casts = [
|
||||
'email_verified_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $appends = ['managed_diseases'];
|
||||
|
||||
public function managedDiseases()
|
||||
{
|
||||
return $this->belongsToMany(Disease::class, 'disease_operator', 'user_id', 'disease_id');
|
||||
}
|
||||
|
||||
public function getManagedDiseasesAttribute()
|
||||
{
|
||||
if ($this->role === 'operator') {
|
||||
|
||||
$disease = $this->managedDiseases()
|
||||
->select(['diseases.id as disease_id', 'diseases.name'])
|
||||
->first();
|
||||
|
||||
if ($disease) {
|
||||
return [
|
||||
'disease_id' => $disease->disease_id,
|
||||
'name' => $disease->name,
|
||||
'pivot' => [
|
||||
'user_id' => $disease->pivot->user_id,
|
||||
'disease_id' => $disease->pivot->disease_id,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
// use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The model to policy mappings for the application.
|
||||
*
|
||||
* @var array<class-string, class-string>
|
||||
*/
|
||||
protected $policies = [
|
||||
// 'App\Models\Model' => 'App\Policies\ModelPolicy',
|
||||
];
|
||||
|
||||
/**
|
||||
* Register any authentication / authorization services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->registerPolicies();
|
||||
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class BroadcastServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
Broadcast::routes();
|
||||
|
||||
require base_path('routes/channels.php');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The event to listener mappings for the application.
|
||||
*
|
||||
* @var array<class-string, array<int, class-string>>
|
||||
*/
|
||||
protected $listen = [
|
||||
Registered::class => [
|
||||
SendEmailVerificationNotification::class,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Register any events for your application.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if events and listeners should be automatically discovered.
|
||||
*/
|
||||
public function shouldDiscoverEvents(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The path to the "home" route for your application.
|
||||
*
|
||||
* Typically, users are redirected here after authentication.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const HOME = '/home';
|
||||
|
||||
/**
|
||||
* Define your route model bindings, pattern filters, and other route configuration.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->configureRateLimiting();
|
||||
|
||||
$this->routes(function () {
|
||||
Route::middleware('api')
|
||||
->prefix('api')
|
||||
->group(base_path('routes/api.php'));
|
||||
|
||||
Route::middleware('web')
|
||||
->group(base_path('routes/web.php'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the rate limiters for the application.
|
||||
*/
|
||||
protected function configureRateLimiting(): void
|
||||
{
|
||||
RateLimiter::for('api', function (Request $request) {
|
||||
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
|
||||
class EnsureArray implements ValidationRule
|
||||
{
|
||||
/**
|
||||
* Indicates whether the rule should be implicit.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $implicit = false;
|
||||
|
||||
/**
|
||||
* Run the validation rule.
|
||||
*
|
||||
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
|
||||
*/
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
// print_r($value);
|
||||
if (!is_array($value)) {
|
||||
$fail("The :attribute must be an array.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AdminUserService
|
||||
{
|
||||
private const DEFAULT_PER_PAGE = 10;
|
||||
private const MAX_PER_PAGE = 100;
|
||||
|
||||
public function createUser(array $data): array
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$user = User::create([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => Hash::make($data['password']),
|
||||
'role' => $data['role'],
|
||||
'approval_status' => 'approved',
|
||||
'institution' => $data['institution'] ?? null,
|
||||
'gender' => $data['gender'] ?? null,
|
||||
'phone_number' => $data['phone_number'] ?? null,
|
||||
]);
|
||||
|
||||
if ($user->role === 'operator' && isset($data['disease_id'])) {
|
||||
$user->managedDiseases()->attach($data['disease_id']);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
LogActionService::logAction(
|
||||
auth()->id(),
|
||||
'create',
|
||||
'User',
|
||||
$user->id,
|
||||
$user->toArray(),
|
||||
"Created user: {$user->name}",
|
||||
true
|
||||
);
|
||||
return [true, 'User created successfully.', $user->toArray()];
|
||||
} catch (\Throwable $exception) {
|
||||
DB::rollBack();
|
||||
|
||||
LogActionService::logAction(
|
||||
auth()->id(),
|
||||
'create',
|
||||
'User',
|
||||
null,
|
||||
[
|
||||
'name' => $data['name'] ?? null,
|
||||
'email' => $data['email'] ?? null,
|
||||
'error' => $exception->getMessage()
|
||||
],
|
||||
"Failed to create user: {$exception->getMessage()}",
|
||||
false
|
||||
);
|
||||
return [false, 'User creation failed: ' . $exception->getMessage(), []];
|
||||
}
|
||||
}
|
||||
|
||||
public function approveUser($id): array
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
$user = User::find($id);
|
||||
if (!$user) {
|
||||
DB::rollBack();
|
||||
LogActionService::logAction(
|
||||
$user,
|
||||
'edit',
|
||||
'User',
|
||||
$id,
|
||||
['error' => 'User not found'],
|
||||
"Failed to approve User: User not found",
|
||||
false
|
||||
);
|
||||
return [false, 'User not found.', []];
|
||||
}
|
||||
|
||||
$oldStatus = $user->approval_status;
|
||||
$user->approval_status = 'approved';
|
||||
$user->save();
|
||||
|
||||
DB::commit();
|
||||
|
||||
LogActionService::logAction(
|
||||
auth()->id(),
|
||||
'edit',
|
||||
'User',
|
||||
$user->id,
|
||||
[
|
||||
'old_status' => $oldStatus,
|
||||
'new_status' => 'approved'
|
||||
],
|
||||
"Approved user: {$user->name}",
|
||||
true
|
||||
);
|
||||
return [true, 'User approved successfully.', $user];
|
||||
} catch (\Throwable $exception) {
|
||||
DB::rollBack();
|
||||
|
||||
LogActionService::logAction(
|
||||
auth()->id(),
|
||||
'edit',
|
||||
'User',
|
||||
$id,
|
||||
['error' => $exception->getMessage()],
|
||||
"Failed to approve user: {$exception->getMessage()}",
|
||||
false
|
||||
);
|
||||
|
||||
return [false, 'User approval failed: ' . $exception->getMessage(), []];
|
||||
}
|
||||
}
|
||||
|
||||
public function rejectUser($id): array
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
$user = User::find($id);
|
||||
if (!$user) {
|
||||
DB::rollBack();
|
||||
LogActionService::logAction(
|
||||
$user,
|
||||
'edit',
|
||||
'User',
|
||||
$id,
|
||||
['error' => 'User not found'],
|
||||
"Failed to reject User: User not found",
|
||||
false
|
||||
);
|
||||
return [false, 'User not found.', []];
|
||||
}
|
||||
|
||||
$oldStatus = $user->approval_status;
|
||||
$user->approval_status = 'rejected';
|
||||
$user->save();
|
||||
|
||||
DB::commit();
|
||||
|
||||
LogActionService::logAction(
|
||||
auth()->id(),
|
||||
'edit',
|
||||
'User',
|
||||
$user->id,
|
||||
[
|
||||
'old_status' => $oldStatus,
|
||||
'new_status' => 'rejected'
|
||||
],
|
||||
"Rejected user: {$user->name}",
|
||||
true
|
||||
);
|
||||
return [true, 'User rejected successfully.', $user];
|
||||
} catch (\Throwable $exception) {
|
||||
DB::rollBack();
|
||||
|
||||
LogActionService::logAction(
|
||||
auth()->id(),
|
||||
'edit',
|
||||
'User',
|
||||
$id,
|
||||
['error' => $exception->getMessage()],
|
||||
"Failed to reject user: {$exception->getMessage()}",
|
||||
false
|
||||
);
|
||||
return [false, 'User rejection failed: ' . $exception->getMessage(), []];
|
||||
}
|
||||
}
|
||||
|
||||
public function editUser($id, array $data): array
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$user = User::find($id);
|
||||
if (!$user) {
|
||||
DB::rollBack();
|
||||
LogActionService::logAction(
|
||||
auth()->id(),
|
||||
'edit',
|
||||
'User',
|
||||
$id,
|
||||
['error' => 'User not found'],
|
||||
"Failed to edit user: User not found",
|
||||
false
|
||||
);
|
||||
return [false, 'User not found.', []];
|
||||
}
|
||||
if (($user->is_primary_admin)) {
|
||||
DB::rollBack();
|
||||
LogActionService::logAction(
|
||||
auth()->id(),
|
||||
'edit',
|
||||
'User',
|
||||
$id,
|
||||
['error' => 'Attempted to edit primary admin account'],
|
||||
"Failed to edit user: Cannot edit primary admin account",
|
||||
false
|
||||
);
|
||||
return [false, 'The primary admin account cannot be edited.', []];
|
||||
}
|
||||
|
||||
$oldData = $user->toArray();
|
||||
|
||||
if (isset($data['password']) && $data['password']) {
|
||||
$data['password'] = Hash::make($data['password']);
|
||||
} else {
|
||||
unset($data['password']);
|
||||
}
|
||||
|
||||
if (isset($data['email']) && $data['email'] !== $user->email) {
|
||||
$existingUser = User::where('email', $data['email'])->first();
|
||||
if ($existingUser) {
|
||||
return [false, 'Email address is already in use.', []];
|
||||
}
|
||||
}
|
||||
|
||||
$changedFields = array_keys(array_diff_assoc($data, $oldData));
|
||||
|
||||
// Get only the values that are actually changing
|
||||
|
||||
$user->update($data);
|
||||
|
||||
if ($user->role === 'operator' && isset($data['disease_id'])) {
|
||||
// Detach old disease (if any) and attach the new one
|
||||
$user->managedDiseases()->sync([$data['disease_id']]);
|
||||
}
|
||||
|
||||
$relevantOldData = array_intersect_key($oldData, $data);
|
||||
$relevantNewData = array_intersect_key($data, $oldData);
|
||||
|
||||
DB::commit();
|
||||
|
||||
LogActionService::logAction(
|
||||
auth()->id(),
|
||||
'edit',
|
||||
'User',
|
||||
$user->id,
|
||||
[
|
||||
'old' => $relevantOldData,
|
||||
'new' => $relevantNewData,
|
||||
'changed_fields' => $changedFields
|
||||
],
|
||||
"Updated user: {$user->name}",
|
||||
true
|
||||
);
|
||||
return [true, 'User updated successfully.', $user];
|
||||
} catch (\Throwable $exception) {
|
||||
DB::rollBack();
|
||||
|
||||
LogActionService::logAction(
|
||||
auth()->id(),
|
||||
'edit',
|
||||
'User',
|
||||
$id,
|
||||
[
|
||||
'attempted_changes' => array_keys($data),
|
||||
'error' => $exception->getMessage()
|
||||
],
|
||||
"Failed to update user: {$exception->getMessage()}",
|
||||
false
|
||||
);
|
||||
return [false, 'User update failed: ' . $exception->getMessage(), []];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function deleteUser($id): array
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
$user = User::find($id);
|
||||
if (!$user) {
|
||||
DB::rollBack();
|
||||
LogActionService::logAction(
|
||||
auth()->id(),
|
||||
'delete',
|
||||
'User',
|
||||
$id,
|
||||
['error' => 'User not found'],
|
||||
"Failed to delete user: User not found",
|
||||
false
|
||||
);
|
||||
return [false, 'User not found.', []];
|
||||
}
|
||||
|
||||
if ((int) auth()->id() === (int) $id) {
|
||||
DB::rollBack();
|
||||
LogActionService::logAction(
|
||||
auth()->id(),
|
||||
'delete',
|
||||
'User',
|
||||
$id,
|
||||
['error' => 'Attempted to delete own account'],
|
||||
"Failed to delete user: Cannot delete own account",
|
||||
false
|
||||
);
|
||||
return [false, 'You cannot delete your own account.', []];
|
||||
}
|
||||
|
||||
if ($user->is_primary_admin) {
|
||||
DB::rollBack();
|
||||
LogActionService::logAction(
|
||||
auth()->id(),
|
||||
'delete',
|
||||
'User',
|
||||
$id,
|
||||
['error' => 'Attempted to delete primary admin account'],
|
||||
"Failed to delete user: Cannot delete primary admin account",
|
||||
false
|
||||
);
|
||||
return [false, 'The primary admin account cannot be deleted.', []];
|
||||
}
|
||||
|
||||
$userInfo = [
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'role' => $user->role
|
||||
];
|
||||
|
||||
$user->delete();
|
||||
|
||||
DB::commit();
|
||||
|
||||
LogActionService::logAction(
|
||||
auth()->id(),
|
||||
'delete',
|
||||
'User',
|
||||
$id,
|
||||
$userInfo,
|
||||
"Deleted user: {$user->name}",
|
||||
true
|
||||
);
|
||||
return [true, 'User deleted successfully.', []];
|
||||
} catch (\Throwable $exception) {
|
||||
DB::rollBack();
|
||||
|
||||
LogActionService::logAction(
|
||||
auth()->id(),
|
||||
'delete',
|
||||
'User',
|
||||
$id,
|
||||
['error' => $exception->getMessage()],
|
||||
"Failed to delete user: {$exception->getMessage()}",
|
||||
false
|
||||
);
|
||||
return [false, 'User deletion failed: ' . $exception->getMessage(), []];
|
||||
}
|
||||
}
|
||||
|
||||
public function getUsers(array $filters): array
|
||||
{
|
||||
try {
|
||||
$query = User::query();
|
||||
|
||||
$this->applyUserFilters($query, $filters);
|
||||
|
||||
$paginatedData = $this->paginateResults($query, $filters, 'users');
|
||||
|
||||
return [true, 'Users retrieved successfully.', $paginatedData];
|
||||
} catch (\Throwable $exception) {
|
||||
return [false, 'Failed to retrieve users: ' . $exception->getMessage(), []];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getUserDetails($id): array
|
||||
{
|
||||
try {
|
||||
$user = User::findOrFail($id);
|
||||
return [true, 'User details retrieved successfully.', $user];
|
||||
} catch (\Throwable $exception) {
|
||||
// TO DO logging
|
||||
return [false, 'Failed to retrieve user details: ' . $exception->getMessage(), []];
|
||||
}
|
||||
}
|
||||
|
||||
public function assignOperatorToDiseases(int $userId, array $diseaseIds): array
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
$user = User::findOrFail($userId);
|
||||
|
||||
$user->managedDiseases()->sync($diseaseIds);
|
||||
|
||||
DB::commit();
|
||||
return [true, 'Diseases assigned to operator successfully.', $user];
|
||||
} catch (\Throwable $exception) {
|
||||
// Log exception
|
||||
DB::rollBack();
|
||||
return [false, 'Failed to assign diseases to operator: ' . $exception->getMessage(), []];
|
||||
}
|
||||
}
|
||||
|
||||
private function applyUserFilters(Builder $query, array $filters): void
|
||||
{
|
||||
if (!empty($filters['role'])) {
|
||||
$query->where('role', $filters['role']);
|
||||
}
|
||||
|
||||
if (!empty($filters['name'])) {
|
||||
$query->where('name', 'ILIKE', '%' . trim($filters['name']) . '%');
|
||||
}
|
||||
|
||||
if (!empty($filters['approval_status'])) {
|
||||
$query->where('approval_status', $filters['approval_status']);
|
||||
}
|
||||
|
||||
// Add sorting if needed
|
||||
$query->orderBy('updated_at', 'desc');
|
||||
}
|
||||
|
||||
private function paginateResults(Builder $query, array $filters, string $itemsKey): array
|
||||
{
|
||||
$perPage = isset($filters['per_page']) &&
|
||||
is_numeric($filters['per_page']) &&
|
||||
$filters['per_page'] > 0 &&
|
||||
$filters['per_page'] <= self::MAX_PER_PAGE
|
||||
? (int) $filters['per_page']
|
||||
: self::DEFAULT_PER_PAGE;
|
||||
|
||||
$page = isset($filters['page']) &&
|
||||
is_numeric($filters['page']) &&
|
||||
$filters['page'] > 0
|
||||
? (int) $filters['page']
|
||||
: 1;
|
||||
|
||||
$results = $query->paginate($perPage, ['*'], 'page', $page);
|
||||
|
||||
if ($results->isEmpty() && $results->currentPage() > 1) {
|
||||
$results = $query->paginate($perPage, ['*'], 'page', $results->lastPage());
|
||||
}
|
||||
|
||||
return [
|
||||
$itemsKey => $results->items(),
|
||||
'pagination' => [
|
||||
'current_page' => $results->currentPage(),
|
||||
'last_page' => $results->lastPage(),
|
||||
'per_page' => (int) $results->perPage(),
|
||||
'total' => $results->total(),
|
||||
'from' => $results->firstItem(),
|
||||
'to' => $results->lastItem(),
|
||||
'has_more_pages' => $results->hasMorePages(),
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class AuthUserService
|
||||
{
|
||||
public function register(
|
||||
string $name,
|
||||
string $email,
|
||||
string $password,
|
||||
?string $institution = null,
|
||||
?string $gender = null,
|
||||
?string $phoneNumber = null,
|
||||
?string $tujuanPermohonan = null
|
||||
): array {
|
||||
try {
|
||||
$user = User::create([
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
'password' => Hash::make($password),
|
||||
'institution' => $institution,
|
||||
'gender' => $gender,
|
||||
'phone_number' => $phoneNumber,
|
||||
'tujuan_permohonan' => $tujuanPermohonan,
|
||||
'role' => 'peneliti',
|
||||
'approval_status' => 'pending',
|
||||
]);
|
||||
|
||||
return [true, 'User registered successfully.', [
|
||||
'approval_status' => $user->approval_status,
|
||||
]];
|
||||
} catch (\Throwable $exception) {
|
||||
// TO DO logging
|
||||
return [false, 'Registration failed: ', []];
|
||||
}
|
||||
}
|
||||
|
||||
public function login(string $email, string $password): array
|
||||
{
|
||||
try {
|
||||
$user = User::where('email', $email)->first();
|
||||
|
||||
if (!$user || !Hash::check($password, $user->password)) {
|
||||
return [false, 'The provided credentials are incorrect.', []];
|
||||
}
|
||||
|
||||
$token = $user->createToken('MyApp')->plainTextToken;
|
||||
|
||||
$response = [
|
||||
'token' => $token,
|
||||
'approval_status' => $user->approval_status,
|
||||
'role' => $user->role,
|
||||
];
|
||||
|
||||
if ($user->role === 'operator') {
|
||||
$response['disease_id'] = $user->managedDiseases["disease_id"] ?? null;
|
||||
}
|
||||
|
||||
return [true, 'Login successful.', $response];
|
||||
} catch (\Throwable $exception) {
|
||||
// TO DO logging
|
||||
return [false, 'Login Failed: ' . $exception->getMessage(), []];
|
||||
}
|
||||
}
|
||||
public function logout(User $user): void
|
||||
{
|
||||
$user->tokens()->delete();
|
||||
}
|
||||
|
||||
private function deleteTokenSanctum(int $userId): void
|
||||
{
|
||||
\Laravel\Sanctum\PersonalAccessToken::where('tokenable_id', $userId)
|
||||
->where('abilities', '["accessLoginMember"]')
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Comment;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CommentService
|
||||
{
|
||||
public function createComment(array $data, int $userId): array
|
||||
{
|
||||
try {
|
||||
$comment = Comment::create([
|
||||
'disease_id' => $data['diseaseId'],
|
||||
'user_id' => $userId,
|
||||
'content' => $data['content'],
|
||||
'parent_id' => $data['parent_id'] ?? null,
|
||||
]);
|
||||
return [true, 'Comment created successfully.', $comment];
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
return [false, 'Failed to create comment.', []];
|
||||
}
|
||||
}
|
||||
|
||||
public function editComment(int $commentId, array $data, int $userId): array
|
||||
{
|
||||
try{
|
||||
$comment = Comment::where('id', $commentId)->where('user_id', $userId)->first();
|
||||
|
||||
if (!$comment) {
|
||||
return [false, 'Comment not found or unauthorized.', []];
|
||||
}
|
||||
|
||||
$comment->update(['content' => $data['content']]);
|
||||
|
||||
return [true, 'Comment updated successfully.', $comment];
|
||||
} catch (\Exception $e){
|
||||
Log::error($e->getMessage());
|
||||
return [false, 'Failed to edit comment.', []];
|
||||
}
|
||||
}
|
||||
public function deleteComment(int $commentId, int $userId): array
|
||||
{
|
||||
try{
|
||||
$comment = Comment::where('id', $commentId)->where('user_id', $userId)->first();
|
||||
|
||||
if (!$comment) {
|
||||
return [false, 'Comment not found or unauthorized.', []];
|
||||
}
|
||||
|
||||
// Soft delete and mark as deleted
|
||||
$comment->update(['status' => 'deleted', 'content' => null]);
|
||||
$comment->delete();
|
||||
|
||||
return [true, 'Comment deleted successfully.', []];
|
||||
} catch (\Exception $e){
|
||||
Log::error($e->getMessage());
|
||||
return [false, 'Failed to delete comment.', []];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getComments(int $diseaseId): array
|
||||
{
|
||||
$comments = Comment::with([
|
||||
'replies' => function ($query) {
|
||||
$query->withTrashed(); // Include soft-deleted replies
|
||||
},
|
||||
'user:id,name,email,role', // Include user details for parent comments
|
||||
'replies.user:id,name,email,role' // Include user details for replies
|
||||
])
|
||||
->where('disease_id', $diseaseId)
|
||||
->whereNull('parent_id') // Fetch only parent comments
|
||||
->withTrashed() // Include soft-deleted parents
|
||||
->get()
|
||||
->map(function ($comment) {
|
||||
return [
|
||||
'id' => $comment->id,
|
||||
'content' => $comment->content_with_placeholder, // Use placeholder content for deleted comments
|
||||
'status' => $comment->status,
|
||||
'created_at' => $comment->created_at,
|
||||
'updated_at' => $comment->updated_at,
|
||||
'user' => $comment->user ? [
|
||||
'id' => $comment->user->id,
|
||||
'name' => $comment->user->name,
|
||||
'email' => $comment->user->email,
|
||||
'role' => $comment->user->role
|
||||
] : null,
|
||||
'replies' => $comment->replies->map(function ($reply) {
|
||||
return [
|
||||
'id' => $reply->id,
|
||||
'content' => $reply->content_with_placeholder,
|
||||
'status' => $reply->status,
|
||||
'created_at' => $reply->created_at,
|
||||
'updated_at' => $reply->updated_at,
|
||||
'user' => $reply->user ? [
|
||||
'id' => $reply->user->id,
|
||||
'name' => $reply->user->name,
|
||||
'email' => $reply->user->email,
|
||||
'role' => $reply->user->role
|
||||
] : null
|
||||
];
|
||||
})->toArray()
|
||||
];
|
||||
})->toArray(); // Convert to array for the response
|
||||
|
||||
return [true, 'Comments retrieved successfully.', $comments];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,481 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Disease;
|
||||
use App\Models\DiseaseRecord;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Services\LogActionService;
|
||||
|
||||
class DiseaseRecordService
|
||||
{
|
||||
protected $diseaseService;
|
||||
protected $fileStorage;
|
||||
|
||||
private const DEFAULT_PER_PAGE = 10;
|
||||
private const MAX_PER_PAGE = 100;
|
||||
|
||||
public function __construct(
|
||||
DiseaseService $diseaseService,
|
||||
FileStorageService $fileStorage)
|
||||
{
|
||||
$this->diseaseService = $diseaseService;
|
||||
$this->fileStorage = $fileStorage;
|
||||
}
|
||||
|
||||
public function createDiseaseRecord(array $data, $userId): array
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$diseaseId = $data['diseaseId'];
|
||||
unset($data['diseaseId']);
|
||||
|
||||
[$schema, $message] = $this->diseaseService->getSchemaField($diseaseId);
|
||||
|
||||
foreach ($data['data'] as $key => $value) {
|
||||
|
||||
$fieldSchema = collect($schema)->firstWhere('name', $key);
|
||||
|
||||
if (!$fieldSchema) {
|
||||
throw new \Exception("Field '{$key}' is not defined in the schema.");
|
||||
}
|
||||
|
||||
if ($fieldSchema['type'] === 'file') {
|
||||
$isMultiple = $fieldSchema['multiple'] ?? false;
|
||||
|
||||
if ($isMultiple) {
|
||||
$value = is_array($value) ? $value : [$value];
|
||||
$fileUrls = [];
|
||||
foreach ($value as $index => $file) {
|
||||
if ($file && is_file($file)) {
|
||||
$fileUrls[] = $this->fileStorage->storeRecordFile(
|
||||
$file,
|
||||
$diseaseId,
|
||||
$key . '-' . ($index + 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$data['data'][$key] = $fileUrls;
|
||||
} else if (is_file($value)){
|
||||
$data['data'][$key] = $this->fileStorage->storeRecordFile($value, $diseaseId, $key);
|
||||
}
|
||||
} else {
|
||||
// Process non-file fields normally
|
||||
$data['data'][$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$diseaseRecord = DiseaseRecord::create([
|
||||
'disease_id' => $diseaseId,
|
||||
'data' => $data['data'],
|
||||
]);
|
||||
|
||||
|
||||
DB::commit();
|
||||
|
||||
LogActionService::logAction(
|
||||
$userId,
|
||||
'create',
|
||||
'DiseaseRecord',
|
||||
$diseaseRecord->id,
|
||||
$diseaseRecord->toArray(),
|
||||
"Created disease record for disease ID: {$diseaseId}",
|
||||
true
|
||||
);
|
||||
return [true, 'Disease record created successfully.', $diseaseRecord->toArray()];
|
||||
} catch (\Throwable $exception) {
|
||||
DB::rollBack();
|
||||
|
||||
LogActionService::logAction(
|
||||
$userId,
|
||||
'create',
|
||||
'DiseaseRecord',
|
||||
null,
|
||||
[
|
||||
'disease_id' => $diseaseId ?? null,
|
||||
'error' => $exception->getMessage()
|
||||
],
|
||||
"Failed to create disease record: {$exception->getMessage()}",
|
||||
false
|
||||
);
|
||||
return [false, 'Disease record creation failed: ' . $exception->getMessage(), []];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function editDiseaseRecord($recordId, array $data, $userId): array
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
$diseaseRecord = DiseaseRecord::find($recordId);
|
||||
if (!$diseaseRecord) {
|
||||
DB::rollBack();
|
||||
LogActionService::logAction(
|
||||
$userId,
|
||||
'delete',
|
||||
'Disease',
|
||||
$recordId,
|
||||
['error' => 'Disease Record not found'],
|
||||
"Failed to delete disease record: Disease Record not found",
|
||||
false
|
||||
);
|
||||
return [false, 'Disease Record not found.', []];
|
||||
}
|
||||
$oldData = $diseaseRecord->toArray();
|
||||
$existingData = $diseaseRecord->data;
|
||||
|
||||
$diseaseId = $data['diseaseId'];
|
||||
unset($data['diseaseId']);
|
||||
|
||||
[$schema, $message] = $this->diseaseService->getSchemaField($diseaseId);
|
||||
|
||||
foreach ($data['data'] as $key => $value) {
|
||||
|
||||
$fieldSchema = collect($schema)->firstWhere('name', $key);
|
||||
|
||||
if (!$fieldSchema) {
|
||||
throw new \Exception("Field '{$key}' is not defined in the schema.");
|
||||
}
|
||||
|
||||
if ($fieldSchema['type'] === 'file') {
|
||||
$isMultiple = $fieldSchema['multiple'] ?? false;
|
||||
|
||||
if ($isMultiple) {
|
||||
$value = is_array($value) ? $value : [$value];
|
||||
$fileUrls = [];
|
||||
|
||||
// Delete old files if they exist
|
||||
if (isset($existingData[$key]) && is_array($existingData[$key])) {
|
||||
foreach ($existingData[$key] as $oldFile) {
|
||||
$this->fileStorage->deleteFile($oldFile, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Store new files
|
||||
foreach ($value as $index => $file) {
|
||||
if ($file && is_file($file)) {
|
||||
$fileUrls[] = $this->fileStorage->storeRecordFile(
|
||||
$file,
|
||||
$diseaseId,
|
||||
$key . '-' . ($index + 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$existingData[$key] = $fileUrls;
|
||||
} else {
|
||||
// Single file handling
|
||||
if (isset($existingData[$key])) {
|
||||
// echo($existingData[$key]);
|
||||
$this->fileStorage->deleteFile($existingData[$key], true);
|
||||
}
|
||||
|
||||
if (is_file($value)) {
|
||||
$existingData[$key] = $this->fileStorage->storeRecordFile(
|
||||
$value,
|
||||
$diseaseId,
|
||||
$key
|
||||
);
|
||||
} else {
|
||||
$existingData[$key] = null; // Clear the field if no new file is provided
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Non-file fields
|
||||
$existingData[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$diseaseRecord->update([
|
||||
'data' => $existingData,
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
LogActionService::logAction(
|
||||
$userId,
|
||||
'edit',
|
||||
'DiseaseRecord',
|
||||
$diseaseRecord->id,
|
||||
[
|
||||
'old' => $oldData,
|
||||
'new' => $diseaseRecord->toArray(),
|
||||
'changed_fields' => array_keys($data['data'])
|
||||
],
|
||||
"Updated disease record ID: {$recordId}",
|
||||
true
|
||||
);
|
||||
return [true, 'Disease record updated successfully.', $diseaseRecord->toArray()];
|
||||
} catch (\Throwable $exception) {
|
||||
DB::rollBack();
|
||||
|
||||
LogActionService::logAction(
|
||||
$userId,
|
||||
'edit',
|
||||
'DiseaseRecord',
|
||||
$recordId,
|
||||
[
|
||||
'attempted_changes' => array_keys($data['data']),
|
||||
'error' => $exception->getMessage()
|
||||
],
|
||||
"Failed to update disease record: {$exception->getMessage()}",
|
||||
false
|
||||
);
|
||||
|
||||
return [false, 'Disease record update failed: ' . $exception->getMessage(), []];
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteDiseaseRecord($recordId, $userId): array
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
$diseaseRecord = DiseaseRecord::find($recordId);
|
||||
$diseaseId = $diseaseRecord->disease_id;
|
||||
|
||||
if (!$diseaseRecord) {
|
||||
DB::rollBack();
|
||||
LogActionService::logAction(
|
||||
$userId,
|
||||
'delete',
|
||||
'Disease',
|
||||
$recordId,
|
||||
['error' => 'Disease Record not found'],
|
||||
"Failed to delete disease record: Disease Record not found",
|
||||
false
|
||||
);
|
||||
return [false, 'Disease Record not found.', []];
|
||||
}
|
||||
|
||||
$recordInfo = [
|
||||
'disease_id' => $diseaseId,
|
||||
'data_fields' => array_keys($diseaseRecord->data),
|
||||
'file_count' => collect($diseaseRecord->data)
|
||||
->flatten()
|
||||
->filter(function($value) {
|
||||
return is_string($value) && file_exists(storage_path("app/public/{$value}"));
|
||||
})
|
||||
->count()
|
||||
];
|
||||
|
||||
[$schema, $message] = $this->diseaseService->getSchemaField($diseaseId);
|
||||
$recordData = $diseaseRecord->data;
|
||||
|
||||
$this->deleteRecordFiles($recordData, $schema);
|
||||
|
||||
$diseaseRecord->delete();
|
||||
|
||||
DB::commit();
|
||||
|
||||
LogActionService::logAction(
|
||||
$userId,
|
||||
'delete',
|
||||
'DiseaseRecord',
|
||||
$recordId,
|
||||
$recordInfo,
|
||||
"Deleted disease record ID: {$recordId}",
|
||||
true
|
||||
);
|
||||
return [true, 'Disease record deleted successfully.', []];
|
||||
} catch (\Throwable $exception) {
|
||||
DB::rollBack();
|
||||
|
||||
LogActionService::logAction(
|
||||
$userId,
|
||||
'delete',
|
||||
'DiseaseRecord',
|
||||
$recordId,
|
||||
['error' => $exception->getMessage()],
|
||||
"Failed to delete disease record: {$exception->getMessage()}",
|
||||
false
|
||||
);
|
||||
|
||||
return [false, 'Disease record deletion failed: ' . $exception->getMessage(), []];
|
||||
}
|
||||
}
|
||||
|
||||
private function transformFileFields(array $data, array $schema): array
|
||||
{
|
||||
foreach ($schema as $field) {
|
||||
// print_r($field);
|
||||
if ($field['type'] === 'file' && isset($data[$field['name']])) {
|
||||
// Handle single file
|
||||
if (empty($field['multiple'])) {
|
||||
$data[$field['name']] = $this->generatePublicFileUrl($data[$field['name']]);
|
||||
} else {
|
||||
// Handle multiple files
|
||||
$data[$field['name']] = array_map(function ($filePath) {
|
||||
return $this->generatePublicFileUrl($filePath);
|
||||
}, $data[$field['name']]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate a public URL for a given file path.
|
||||
*/
|
||||
private function generatePublicFileUrl(string $filePath): string
|
||||
{
|
||||
return Storage::url('public/' . $filePath);
|
||||
//return asset('storage/' . $filePath);
|
||||
}
|
||||
|
||||
public function getDiseaseRecords($diseaseId, array $filters)
|
||||
{
|
||||
try {
|
||||
[$schema, $message] = $this->diseaseService->getSchemaField($diseaseId);
|
||||
|
||||
$disease = Disease::findOrFail($diseaseId);
|
||||
|
||||
if (empty($schema)) {
|
||||
if ($message === 'Access denied. This disease is private.'){
|
||||
return [false, 'Cant Access Private Disease', []];
|
||||
}
|
||||
return [false, 'Schema not found for this disease.', []];
|
||||
}
|
||||
|
||||
$query = $this->buildDiseaseRecordQuery($diseaseId, $filters);
|
||||
$paginatedData = $this->paginateResults($query, $filters);
|
||||
|
||||
// Check if records exist and transform them
|
||||
$records = !empty($paginatedData['records'])
|
||||
? array_map(function ($record) use ($schema) {
|
||||
$record['data'] = $this->transformFileFields($record['data'], $schema);
|
||||
return $record;
|
||||
}, $paginatedData['records'])
|
||||
: [];
|
||||
|
||||
$response = [
|
||||
'name' => $disease['name'],
|
||||
'deskripsi' => $disease['deskripsi'],
|
||||
'schema' => $schema,
|
||||
'export_url' => $disease->export_url,
|
||||
'records' => $records,
|
||||
'pagination' => $paginatedData['pagination']
|
||||
];
|
||||
|
||||
|
||||
|
||||
return [true, 'Disease records retrieved successfully.', $response];
|
||||
} catch (\Throwable $exception) {
|
||||
return [false, 'Failed to retrieve disease records: ' . $exception->getMessage(), []];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getDiseaseRecordDetails($diseaseId, $recordId): array
|
||||
{
|
||||
try {
|
||||
[$schema, $message] = $this->diseaseService->getSchemaField($diseaseId);
|
||||
|
||||
if (empty($schema)) {
|
||||
return [false, 'Schema not found for this disease.', []];
|
||||
}
|
||||
|
||||
$record = DiseaseRecord::where('disease_id', $diseaseId)
|
||||
->where('id', $recordId)
|
||||
->firstOrFail();
|
||||
|
||||
// Transform file fields in the record
|
||||
$record->data = $this->transformFileFields($record->data, $schema);
|
||||
|
||||
$response = [
|
||||
'schema' => $schema,
|
||||
'record' => $record->toArray(),
|
||||
];
|
||||
|
||||
return [true, 'Disease record details retrieved successfully.', $response];
|
||||
} catch (\Throwable $exception) {
|
||||
return [false, 'Failed to retrieve disease record details: ' . $exception->getMessage(), []];
|
||||
}
|
||||
}
|
||||
|
||||
private function buildDiseaseRecordQuery(int $diseaseId, array $filters): Builder
|
||||
{
|
||||
$query = DiseaseRecord::where('disease_id', $diseaseId);
|
||||
|
||||
// $this->applyDiseaseRecordFilters($query, $filters);
|
||||
if(!empty($filters['order_by'])){
|
||||
if($filters['order_by'] === 'desc'){
|
||||
$query->orderBy('created_at', 'desc');
|
||||
}else if($filters['order_by'] === 'desc'){
|
||||
$query->orderBy('created_at', 'asc');
|
||||
}
|
||||
}
|
||||
|
||||
$query->orderBy('updated_at', 'desc');
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
private function paginateResults(Builder $query, array $filters): array
|
||||
{
|
||||
$perPage = isset($filters['per_page']) &&
|
||||
is_numeric($filters['per_page']) &&
|
||||
$filters['per_page'] > 0 &&
|
||||
$filters['per_page'] <= self::MAX_PER_PAGE
|
||||
? (int) $filters['per_page']
|
||||
: self::DEFAULT_PER_PAGE;
|
||||
|
||||
$page = isset($filters['page']) &&
|
||||
is_numeric($filters['page']) &&
|
||||
$filters['page'] > 0
|
||||
? (int) $filters['page']
|
||||
: 1;
|
||||
|
||||
$results = $query->paginate($perPage, ['*'], 'page', $page);
|
||||
|
||||
if ($results->isEmpty() && $results->currentPage() > 1) {
|
||||
$results = $query->paginate($perPage, ['*'], 'page', $results->lastPage());
|
||||
}
|
||||
|
||||
return [
|
||||
'records' => $results->items(),
|
||||
'pagination' => [
|
||||
'current_page' => $results->currentPage(),
|
||||
'last_page' => $results->lastPage(),
|
||||
'per_page' => (int) $results->perPage(),
|
||||
'total' => $results->total(),
|
||||
'from' => $results->firstItem(),
|
||||
'to' => $results->lastItem(),
|
||||
'has_more_pages' => $results->hasMorePages(),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
private function isFileArray(array $value): bool
|
||||
{
|
||||
return count(array_filter($value, function ($item) {
|
||||
return is_object($item) && method_exists($item, 'getClientOriginalExtension');
|
||||
})) > 0;
|
||||
}
|
||||
|
||||
public function deleteRecordFiles(array $data, array $schema): void
|
||||
{
|
||||
foreach ($data as $key => $value) {
|
||||
$fieldSchema = collect($schema)->firstWhere('name', $key);
|
||||
|
||||
// Only proceed if this is a file field
|
||||
if ($fieldSchema && $fieldSchema['type'] === 'file') {
|
||||
if (is_array($value)) {
|
||||
// Multiple files
|
||||
foreach ($value as $filePath) {
|
||||
$this->fileStorage->deleteFile($filePath, true);
|
||||
}
|
||||
} elseif (is_string($value)) {
|
||||
// Single file
|
||||
$this->fileStorage->deleteFile($value, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Disease;
|
||||
use App\Http\Requests\CreateDiseaseRequest;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Services\LogActionService;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class DiseaseService
|
||||
{
|
||||
private const DEFAULT_PER_PAGE = 10;
|
||||
private const MAX_PER_PAGE = 100;
|
||||
|
||||
protected $fileStorage;
|
||||
|
||||
public function __construct(
|
||||
FileStorageService $fileStorage)
|
||||
{
|
||||
$this->fileStorage = $fileStorage;
|
||||
}
|
||||
|
||||
|
||||
public function createDisease(array $data, $userId): array
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$image_url = null;
|
||||
if (isset($data['cover_page']) && $data['cover_page']) {
|
||||
$disease = Disease::create([
|
||||
'name' => $data['name'],
|
||||
'deskripsi' => $data['deskripsi'],
|
||||
'visibilitas' => $data['visibilitas'],
|
||||
'schema' => $data['schema'],
|
||||
'cover_page' => null,
|
||||
]);
|
||||
|
||||
$image_url = $this->fileStorage->storeCoverImage($data['cover_page'], $disease->id);
|
||||
|
||||
$disease->update(['cover_page' => $image_url]);
|
||||
} else {
|
||||
$disease = Disease::create([
|
||||
'name' => $data['name'],
|
||||
'deskripsi' => $data['deskripsi'],
|
||||
'visibilitas' => $data['visibilitas'],
|
||||
'schema' => $data['schema'],
|
||||
'cover_page' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
LogActionService::logAction(
|
||||
$userId,
|
||||
'create',
|
||||
'Disease',
|
||||
$disease->id,
|
||||
$disease->toArray(),
|
||||
"Created disease: {$disease->name}",
|
||||
true
|
||||
);
|
||||
|
||||
return [true, 'Disease created successfully.', $disease->toArray()];
|
||||
} catch (\Throwable $exception) {
|
||||
DB::rollBack();
|
||||
|
||||
LogActionService::logAction(
|
||||
$userId,
|
||||
'create',
|
||||
'Disease',
|
||||
null,
|
||||
[
|
||||
'name' => $data['name'] ?? null,
|
||||
'error' => $exception->getMessage()
|
||||
],
|
||||
"Failed to create disease: {$exception->getMessage()}",
|
||||
false
|
||||
);
|
||||
|
||||
return [false, 'Disease creation failed: ' . $exception->getMessage(), []];
|
||||
}
|
||||
}
|
||||
|
||||
public function editDisease($id, array $data, $userId): array
|
||||
{
|
||||
try {
|
||||
// var_dump($data);
|
||||
DB::beginTransaction();
|
||||
|
||||
$disease = Disease::find($id);
|
||||
if (!$disease) {
|
||||
DB::rollBack();
|
||||
LogActionService::logAction(
|
||||
$userId,
|
||||
'edit',
|
||||
'Disease',
|
||||
$id,
|
||||
['error' => 'Disease not found'],
|
||||
"Failed to edit disease: Disease not found",
|
||||
false
|
||||
);
|
||||
return [false, 'Disease not found.', []];
|
||||
}
|
||||
|
||||
$oldData = $disease->toArray();
|
||||
|
||||
if (isset($data['cover_page']) && $data['cover_page']) {
|
||||
if ($disease->cover_page) {
|
||||
$this->fileStorage->deleteFile($disease->cover_page, true);
|
||||
}
|
||||
|
||||
$data['cover_page'] = $this->fileStorage->storeCoverImage($data['cover_page'], $disease->id);
|
||||
} else {
|
||||
unset($data['cover_page']);
|
||||
}
|
||||
|
||||
$changedFields = array_keys(array_diff_assoc($data, $oldData));
|
||||
|
||||
// Get only the values that are actually changing
|
||||
$relevantOldData = array_intersect_key($oldData, $data);
|
||||
$relevantNewData = array_intersect_key($data, $oldData);
|
||||
|
||||
$disease->update($data);
|
||||
DB::commit();
|
||||
|
||||
LogActionService::logAction(
|
||||
$userId,
|
||||
'edit',
|
||||
'Disease',
|
||||
$disease->id,
|
||||
[
|
||||
'old' => $relevantOldData,
|
||||
'new' => $relevantNewData,
|
||||
'changed_fields' => $changedFields
|
||||
],
|
||||
"Updated disease: {$disease->name}",
|
||||
true
|
||||
);
|
||||
|
||||
return [true, 'Disease updated successfully.', $disease];
|
||||
} catch (\Throwable $exception) {
|
||||
DB::rollBack();
|
||||
|
||||
LogActionService::logAction(
|
||||
$userId,
|
||||
'edit',
|
||||
'Disease',
|
||||
$id,
|
||||
[
|
||||
'attempted_changes' => array_keys($data),
|
||||
'error' => $exception->getMessage()
|
||||
],
|
||||
"Failed to update disease: {$exception->getMessage()}",
|
||||
false
|
||||
);
|
||||
return [false, 'Disease update failed: ' . $exception->getMessage(), []];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function deleteDisease($id, $userId): array
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
$disease = Disease::find($id);
|
||||
if (!$disease) {
|
||||
DB::rollBack();
|
||||
LogActionService::logAction(
|
||||
$userId,
|
||||
'delete',
|
||||
'Disease',
|
||||
$id,
|
||||
['error' => 'Disease not found'],
|
||||
"Failed to delete disease: Disease not found",
|
||||
false
|
||||
);
|
||||
return [false, 'Disease not found.', []];
|
||||
}
|
||||
|
||||
$diseaseInfo = [
|
||||
'name' => $disease->name,
|
||||
'had_cover_page' => !is_null($disease->cover_page),
|
||||
'schema_columns_count' => count($disease->schema['columns'] ?? [])
|
||||
];
|
||||
|
||||
if ($disease->cover_page) {
|
||||
$this->fileStorage->deleteFile($disease->cover_page, true);
|
||||
}
|
||||
|
||||
$directoryPath = "diseases/records/$id";
|
||||
$this->fileStorage->deleteDirectory($directoryPath, true);
|
||||
|
||||
$disease->delete();
|
||||
|
||||
DB::commit();
|
||||
LogActionService::logAction(
|
||||
$userId,
|
||||
'delete',
|
||||
'Disease',
|
||||
$id,
|
||||
$diseaseInfo,
|
||||
"Deleted disease: {$disease->name}",
|
||||
true
|
||||
);
|
||||
return [true, 'Disease deleted successfully.', []];
|
||||
} catch (\Throwable $exception) {
|
||||
DB::rollBack();
|
||||
|
||||
LogActionService::logAction(
|
||||
$userId,
|
||||
'delete',
|
||||
'Disease',
|
||||
$id,
|
||||
['error' => $exception->getMessage()],
|
||||
"Failed to delete disease: {$exception->getMessage()}",
|
||||
false
|
||||
);
|
||||
return [false, 'Disease deletion failed: ' . $exception->getMessage(), []];
|
||||
}
|
||||
}
|
||||
|
||||
public function getDisease(array $filters, $user): array
|
||||
{
|
||||
try {
|
||||
$query = Disease::query();
|
||||
$query->withCount('diseaseRecords');
|
||||
|
||||
if ($user->role === 'operator') {
|
||||
$query->where('id', $user->disease_id);
|
||||
} elseif ($user->role === 'peneliti') {
|
||||
$query->where('visibilitas', 'publik');
|
||||
}
|
||||
|
||||
$this->applyDiseaseFilters($query, $filters);
|
||||
|
||||
$paginatedData = $this->paginateResults($query, $filters, 'diseases');
|
||||
|
||||
return [true, 'Diseases retrieved successfully.', $paginatedData];
|
||||
} catch (\Throwable $exception) {
|
||||
return [false, 'Failed to retrieve diseases: ' . $exception->getMessage(), []];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getDiseaseDetails($id): array
|
||||
{
|
||||
try {
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
$disease = Disease::withCount('diseaseRecords')->findOrFail($id);
|
||||
|
||||
if (!$disease) {
|
||||
return [false, 'Disease not found.', []];
|
||||
}
|
||||
|
||||
// Admins can access all diseases
|
||||
if ($user->role === 'admin') {
|
||||
return [true, 'Disease details retrieved successfully.', $disease];
|
||||
}
|
||||
|
||||
// Operators can only access their assigned diseases
|
||||
if ($user->role === 'operator') {
|
||||
$managesDisease = $user->managed_diseases['disease_id'];
|
||||
if ($managesDisease != $id) {
|
||||
return [false, 'Access denied. You are not assigned to this disease.', []];
|
||||
}
|
||||
}
|
||||
|
||||
// Researchers can only access public diseases
|
||||
if ($user->role === 'peneliti') {
|
||||
if ($user->approval_status !== 'approved') {
|
||||
return [false, 'Access denied. Your account is not approved.', []];
|
||||
}
|
||||
if ($disease->visibilitas !== 'publik') {
|
||||
return [false, 'Access denied. This disease is private.', []];
|
||||
}
|
||||
}
|
||||
|
||||
return [true, 'Disease details retrieved successfully.', $disease];
|
||||
} catch (\Throwable $exception) {
|
||||
return [false, 'Failed to retrieve disease details: ' . $exception->getMessage(), []];
|
||||
}
|
||||
}
|
||||
|
||||
public function getStatistics(array $filters): array
|
||||
{
|
||||
try {
|
||||
$query = Disease::query();
|
||||
$query->withCount('diseaseRecords');
|
||||
|
||||
$this->applyDiseaseFilters($query, $filters);
|
||||
|
||||
$diseases = $query->get(['id', 'name', 'disease_records_count']);
|
||||
|
||||
$totalDiseaseRecords = $diseases->sum('disease_records_count');
|
||||
|
||||
$stats = [
|
||||
'total_disease_records' => $totalDiseaseRecords,
|
||||
'diseases' => $diseases->map(function ($disease) {
|
||||
return [
|
||||
'id' => $disease->id,
|
||||
'name' => $disease->name,
|
||||
'disease_records_count' => $disease->disease_records_count,
|
||||
];
|
||||
}),
|
||||
];
|
||||
|
||||
return [true, 'Disease statistics retrieved successfully.', $stats];
|
||||
} catch (\Throwable $exception) {
|
||||
return [false, 'Failed to retrieve statistics: ' . $exception->getMessage(), []];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getSchemaField(int $diseaseId): array
|
||||
{
|
||||
try {
|
||||
[$success, $message, $disease] = $this->getDiseaseDetails($diseaseId);
|
||||
|
||||
if (!$success) {
|
||||
return [[], $message];
|
||||
}
|
||||
|
||||
$schema = $disease->schema['columns'] ?? null;
|
||||
|
||||
if (!$schema) {
|
||||
return [[], $message];
|
||||
}
|
||||
|
||||
$formattedSchema = [];
|
||||
foreach ($schema as $column) {
|
||||
$field = [
|
||||
'name' => $column['name'] ?? null,
|
||||
'type' => $column['type'] ?? null,
|
||||
];
|
||||
|
||||
if ($field['type'] === 'file') {
|
||||
$field['format'] = $column['format'];
|
||||
|
||||
$field['multiple'] = isset($column['multiple']) ? (bool) $column['multiple'] : false; // Ensure multiple is set with default false
|
||||
}
|
||||
$field['is_visible'] = filter_var($column['is_visible'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
$formattedSchema[] = $field;
|
||||
}
|
||||
|
||||
return [$formattedSchema, 'Schema Retrieved Successfully'];
|
||||
} catch (\Throwable $exception) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private function applyDiseaseFilters(Builder $query, array $filters): void
|
||||
{
|
||||
if (!empty($filters['visibilitas'])) {
|
||||
$query->where('visibilitas', $filters['visibilitas']);
|
||||
}
|
||||
|
||||
if (!empty($filters['name'])) {
|
||||
$query->where('name', 'ILIKE', '%' . trim($filters['name']) . '%');
|
||||
}
|
||||
|
||||
if (!empty($filters['column_type'])) {
|
||||
$query->whereJsonContains('schema->columns', ['type' => $filters['column_type']]);
|
||||
}
|
||||
|
||||
if (!empty($filters['column_name'])) {
|
||||
$query->whereJsonContains('schema->columns', ['name' => $filters['column_name']]);
|
||||
}
|
||||
|
||||
//Sorting
|
||||
$query->orderBy('updated_at', 'desc');
|
||||
}
|
||||
|
||||
private function paginateResults(Builder $query, array $filters, string $itemsKey): array
|
||||
{
|
||||
$perPage = isset($filters['per_page']) &&
|
||||
is_numeric($filters['per_page']) &&
|
||||
$filters['per_page'] > 0 &&
|
||||
$filters['per_page'] <= self::MAX_PER_PAGE
|
||||
? (int) $filters['per_page']
|
||||
: self::DEFAULT_PER_PAGE;
|
||||
|
||||
$page = isset($filters['page']) &&
|
||||
is_numeric($filters['page']) &&
|
||||
$filters['page'] > 0
|
||||
? (int) $filters['page']
|
||||
: 1;
|
||||
|
||||
$results = $query->paginate($perPage, ['*'], 'page', $page);
|
||||
|
||||
if ($results->isEmpty() && $results->currentPage() > 1) {
|
||||
$results = $query->paginate($perPage, ['*'], 'page', $results->lastPage());
|
||||
}
|
||||
|
||||
return [
|
||||
$itemsKey => $results->items(),
|
||||
'pagination' => [
|
||||
'current_page' => $results->currentPage(),
|
||||
'last_page' => $results->lastPage(),
|
||||
'per_page' => (int) $results->perPage(),
|
||||
'total' => $results->total(),
|
||||
'from' => $results->firstItem(),
|
||||
'to' => $results->lastItem(),
|
||||
'has_more_pages' => $results->hasMorePages(),
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class FileStorageService
|
||||
{
|
||||
public function storeFile($file, string $directory, ?string $filename = null, bool $isPublic = false): string
|
||||
{
|
||||
if (!$filename) {
|
||||
$filename = Str::random(40) . '.' . $file->getClientOriginalExtension();
|
||||
}
|
||||
|
||||
$baseDirectory = $isPublic ? "public/$directory" : $directory;
|
||||
$path = $file->storeAs($baseDirectory, $filename);
|
||||
|
||||
return $isPublic ? str_replace('public/', '', $path) : $path;
|
||||
}
|
||||
|
||||
public function storeCoverImage($file, int $diseaseId): string
|
||||
{
|
||||
$filename = "disease-{$diseaseId}-cover." . $file->getClientOriginalExtension();
|
||||
return $this->storeFile($file, 'diseases/covers', $filename, true);
|
||||
}
|
||||
|
||||
public function storeRecordFile($file, int $diseaseId, string $fieldName): string
|
||||
{
|
||||
$filename = Str::slug($fieldName) . '-' . time() . '.' . $file->getClientOriginalExtension();
|
||||
return $this->storeFile($file, "diseases/records/$diseaseId", $filename, true);
|
||||
}
|
||||
|
||||
public function deleteFile(?string $path, bool $isPublic = false): bool
|
||||
{
|
||||
if (!$path) return true;
|
||||
|
||||
$fullPath = $isPublic ? 'public/' . $path : $path;
|
||||
return Storage::delete($fullPath);
|
||||
}
|
||||
|
||||
public function streamFile(string $filePath): StreamedResponse
|
||||
{
|
||||
return response()->streamDownload(function () use ($filePath) {
|
||||
$stream = fopen(storage_path('app/' . $filePath), 'rb');
|
||||
if ($stream) {
|
||||
fpassthru($stream);
|
||||
fclose($stream);
|
||||
} else {
|
||||
abort(404, 'File not found');
|
||||
}
|
||||
}, basename($filePath));
|
||||
}
|
||||
|
||||
public function deleteDirectory(string $directory, bool $isPublic = false): bool
|
||||
{
|
||||
$fullPath = $isPublic ? 'public/' . $directory : $directory;
|
||||
|
||||
$fullPathResolved = realpath(storage_path('app/' . $fullPath));
|
||||
$allowedDirectory = realpath(storage_path('app/diseases/'));
|
||||
|
||||
if (strpos($fullPathResolved, $allowedDirectory) !== 0) {
|
||||
throw new \Exception('Directory traversal attempt detected!');
|
||||
}
|
||||
|
||||
if(Storage::exists($fullPath)){
|
||||
Storage::deleteDirectory($fullPath);
|
||||
}
|
||||
|
||||
return !Storage::exists($fullPath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Log;
|
||||
use Illuminate\Support\Facades\Log as LogFacade;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class LogActionService{
|
||||
|
||||
const MAX_PER_PAGE = 50;
|
||||
const DEFAULT_PER_PAGE = 10;
|
||||
|
||||
public static function logAction(int $userId,
|
||||
string $action,
|
||||
?string $model = null,
|
||||
?int $modelId = null,
|
||||
?array $changes = null,
|
||||
?string $description = null,
|
||||
$success)
|
||||
{
|
||||
try {
|
||||
Log::create([
|
||||
'user_id' => $userId,
|
||||
'action' => $action,
|
||||
'model' => $model,
|
||||
'model_id' => $modelId,
|
||||
'changes' => $changes ? json_encode($changes) : null,
|
||||
'description' => $description,
|
||||
'timestamp' => now(),
|
||||
'is_success' => $success,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
LogFacade::info('Error inserting log: ' . $e->getMessage());
|
||||
throw $e; // or handle it gracefully
|
||||
}
|
||||
}
|
||||
|
||||
public function getLogs(array $filters = []): array
|
||||
{
|
||||
try {
|
||||
$query = Log::query();
|
||||
$query->with(['user:id,name,role,email']);
|
||||
|
||||
$this->applyLogFilters($query, $filters);
|
||||
|
||||
$paginatedData = $this->paginateResults($query, $filters, 'logs');
|
||||
|
||||
return [true, 'Logs retrieved successfully.', $paginatedData];
|
||||
} catch (\Throwable $exception) {
|
||||
return [false, 'Failed to retrieve logs: ' . $exception->getMessage(), []];
|
||||
}
|
||||
}
|
||||
|
||||
private function applyLogFilters(Builder $query, array $filters): void
|
||||
{
|
||||
if (!empty($filters['user_id'])) {
|
||||
$query->where('user_id', $filters['user_id']);
|
||||
}
|
||||
|
||||
if (!empty($filters['action'])) {
|
||||
$query->where('action', $filters['action']);
|
||||
}
|
||||
|
||||
if (!empty($filters['model'])) {
|
||||
$query->where('model', $filters['model']);
|
||||
}
|
||||
|
||||
if (!empty($filters['date_from'])) {
|
||||
$query->whereDate('timestamp', '>=', $filters['date_from']);
|
||||
}
|
||||
|
||||
if (!empty($filters['date_to'])) {
|
||||
$query->whereDate('timestamp', '<=', $filters['date_to']);
|
||||
}
|
||||
|
||||
if (isset($filters['is_success'])) {
|
||||
|
||||
$query->where('is_success', $filters['is_success']);
|
||||
// $successFilter = filter_var($filters['is_success'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||
// if (!is_null($successFilter)) {
|
||||
// $query->where('is_success', $successFilter);
|
||||
// }
|
||||
}
|
||||
|
||||
// Default sorting by timestamp
|
||||
$query->orderBy('timestamp', 'desc');
|
||||
}
|
||||
|
||||
private function paginateResults(Builder $query, array $filters, string $itemsKey): array
|
||||
{
|
||||
$perPage = isset($filters['per_page']) &&
|
||||
is_numeric($filters['per_page']) &&
|
||||
$filters['per_page'] > 0 &&
|
||||
$filters['per_page'] <= self::MAX_PER_PAGE
|
||||
? (int)$filters['per_page']
|
||||
: self::DEFAULT_PER_PAGE;
|
||||
|
||||
$page = isset($filters['page']) &&
|
||||
is_numeric($filters['page']) &&
|
||||
$filters['page'] > 0
|
||||
? (int)$filters['page']
|
||||
: 1;
|
||||
|
||||
$results = $query->paginate($perPage, ['*'], 'page', $page);
|
||||
|
||||
if ($results->isEmpty() && $results->currentPage() > 1) {
|
||||
$results = $query->paginate($perPage, ['*'], 'page', $results->lastPage());
|
||||
}
|
||||
|
||||
return [
|
||||
$itemsKey => $results->items(),
|
||||
'pagination' => [
|
||||
'current_page' => $results->currentPage(),
|
||||
'last_page' => $results->lastPage(),
|
||||
'per_page' => (int)$results->perPage(),
|
||||
'total' => $results->total(),
|
||||
'from' => $results->firstItem(),
|
||||
'to' => $results->lastItem(),
|
||||
'has_more_pages' => $results->hasMorePages(),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user