deploy commit

This commit is contained in:
2026-07-15 17:23:15 +07:00
commit b94ef4a695
148 changed files with 14831 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Filters;
use Illuminate\Http\Request;
class ApiFilter {
protected $safeParms = [];
protected $columnMap = [];
protected $operatorMap = [];
public function transform(Request $request) {
$eloQuery = [];
foreach ($this->safeParms as $parm => $operators) {
$query = $request->query($parm);
if (!isset($query)) {
continue;
}
$column = $this->columnMap[$parm] ?? $parm;
foreach ($operators as $operator) {
if(isset($query[$operator])) {
$eloQuery[] = [$column, $this->operatorMap[$operator], $query[$operator]];
}
}
}
return $eloQuery;
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Filters\V1;
use App\Filters\ApiFilter;
class AccountFilter extends ApiFilter {
protected $safeParms = [
'id' => ['eq'],
'name' => ['eq'],
'email' => ['eq'],
'phone' => ['eq'],
'dob' => ['eq'],
'role' => ['eq'],
];
protected $columnMap = [
];
protected $operatorMap = [
'eq' => '=',
'ne' => '!=',
'lt' => '<',
'lte' => '<=',
'gte' => '>=',
'gt' => '>',
];
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\Filters\V1;
use App\Filters\ApiFilter;
class DoctorProfileFilter extends ApiFilter
{
protected $safeParms = [
'user_id' => ['eq'],
'specialization' => ['eq'],
'license_number' => ['eq'],
'license_file_path' => ['eq'],
'diploma_file_path' => ['eq'],
'certification' => ['eq'],
'current_institution' => ['eq'],
'years_of_experience' => ['eq', 'gte', 'lte', 'gt', 'lt'],
'work_history' => ['eq'],
'publications' => ['eq']
];
protected $columnMap = [
//'user_id' => 'user_id',
];
protected $operatorMap = [
'eq' => '=',
'ne' => '!=',
'lt' => '<',
'lte' => '<=',
'gte' => '>=',
'gt' => '>',
];
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Filters\V1;
use App\Filters\ApiFilter;
class SubmissionFilter extends ApiFilter
{
protected $safeParms = [
'patient_id' => ['eq'],
'doctor_id' => ['eq'],
'image_path' => ['eq'],
'complaint' => ['eq'],
'status' => ['eq', 'ne'],
'diagnosis' => ['eq'],
'doctor_note' => ['eq'],
'submitted_at' => ['eq'],
'verified_at' => ['eq'],
];
protected $columnMap = [
//'patient_id' => 'patient_id',
//'doctor_id' => 'doctor_id',
//'verified_at' => 'verified_at',
];
protected $operatorMap = [
'eq' => '=',
'ne' => '!=',
'lt' => '<',
'lte' => '<=',
'gte' => '>=',
'gt' => '>',
];
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace App\Http\Controllers\Api;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}
@@ -0,0 +1,144 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Filters\V1\AccountFilter;
use App\Http\Controllers\Api\Controller;
use App\Http\Requests\V1\StoreAccountRequest;
use App\Http\Requests\V1\UpdateAccountRequest;
use App\Http\Resources\V1\AccountCollection;
use App\Http\Resources\V1\AccountResource;
use App\Http\Resources\V1\DoctorListResource;
use App\Models\Account;
use Illuminate\Http\Request;
class AccountController extends Controller
{
public function __construct()
{
$this->middleware('auth:sanctum');
$this->authorizeResource(Account::class,'account');
}
public function index(Request $request)
{
$filter = new AccountFilter();
$filterItems = $filter->transform($request);
$includeSubmissions = $request->query('includeSubmissions');
$includeDoctorProfiles = $request->query('includeDoctorProfiles');
$accounts = Account::where($filterItems);
if ($includeSubmissions) {
$accounts = $accounts->with('submission');
}
if ($includeDoctorProfiles) {
$accounts = $accounts->with('doctorProfile');
}
return new AccountCollection($accounts->paginate()->appends($request->query()));
}
public function listDoctors(Request $request)
{
$search = $request->query('search');
$query = Account::where('role', 'doctor');
if ($search) {
$query->where('name', 'like', "%{$search}%");
}
if ($limit = (int) $request->query('limit', 0)) {
$query->limit($limit);
}
$doctors = $query->orderBy('name')->get(['id', 'name']);
return DoctorListResource::collection($doctors);
}
public function welcome(Request $request)
{
$user = $request->user();
$firstName = explode(' ', trim($user->name))[0] ?? $user->name;
$id = $user->id;
$role = ucfirst($user->role);
$message = "Welcome back, {$firstName}! (You are logged in as {$role})";
return response()->json([
'accountId' => $id,
'message' => $message,
'firstName' => $firstName,
'role' => $user->role,
]);
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreAccountRequest $request)
{
//
return new AccountResource(Account::create($request->all()));
}
/**
* Display the specified resource.
*/
public function show(Account $account)
{
//
$includeSubmissions = request()->query('includeSubmissions');
if ($includeSubmissions) {
$account->loadMissing('submission');
}
$includeDoctorProfiles = request()->query('includeDoctorProfiles');
if ($includeDoctorProfiles) {
$account->loadMissing('doctorProfile');
}
return new AccountResource($account);
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Account $account)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateAccountRequest $request, Account $account)
{
//
$account->update($request->validated());
return new AccountResource($account->fresh());
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Account $account)
{
//
$account->delete();
return response()->json(['message' => 'Account deleted.']);
}
}
@@ -0,0 +1,114 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Api\Controller;
use App\Http\Requests\V1\LoginRequest;
use App\Http\Requests\V1\RegisterDoctorRequest;
use App\Http\Requests\V1\RegisterRequest;
use App\Models\Account;
use App\Models\DoctorProfile;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
// POST /api/v1/auth/register/doctor
public function registerDoctor(RegisterDoctorRequest $request)
{
$acct = Account::create([
'name' => $request->name,
'email' => $request->email,
'phone' => $request->phone,
'role' => 'doctor',
'password' => Hash::make($request->password),
]);
$licensePath = $request->file('license_file')
->store('doctor_profiles/licenses','public');
$diplomaPath = $request->file('diploma_file')
->store('doctor_profiles/diplomas','public');
$certPath = $request->file('certification_file')
->store('doctor_profiles/certifications','public');
$profile = DoctorProfile::create([
'user_id' => $acct->id,
'specialization' => $request->specialization,
'license_number' => $request->license_number,
'license_file_path' => $licensePath,
'diploma_file_path' => $diplomaPath,
'certification_file_path' => $certPath,
'current_institution' => $request->current_institution,
'work_history' => $request->work_history,
'publications' => $request->publications,
'practice_address' => $request->practice_address,
]);
$token = $acct->createToken('doctor-token', ['create','update','delete'])->plainTextToken;
return response()->json([
'data' => [
'account' => [
'id' => $acct->id,
'name' => $acct->name,
'email' => $acct->email,
'phone' => $acct->phone,
],
'profile' => [
'specialization' => $profile->specialization,
'licenseNumber' => $profile->license_number,
'licenseFileUrl' => asset("storage/{$profile->license_file_path}"),
'diplomaFileUrl' => asset("storage/{$profile->diploma_file_path}"),
'certificationUrl' => asset("storage/{$profile->certification_file_path}"),
'currentInstitution' => $profile->current_institution,
'workHistory' => $profile->work_history,
'publications' => $profile->publications,
'practiceAddress' => $profile->practice_address,
],
'token' => $token,
]
], 201);
}
// POST /api/v1/auth/register/patient
public function register(RegisterRequest $req)
{
$data = $req->validated();
$data['password'] = Hash::make($data['password']);
$data['role'] = 'patient';
$account = Account::create($data);
$token = $account->createToken('auth-token')->plainTextToken;
return response()->json([
'data' => $account,
'token' => $token
], 201);
}
// POST /api/v1/auth/login
public function login(LoginRequest $req)
{
$creds = $req->validated();
$account = Account::where('email', $creds['email'])->first();
if (!$account || !Hash::check($creds['password'], $account->password)) {
return response()->json(['message' => 'Invalid credentials'], 401);
}
$token = $account->createToken('auth-token')->plainTextToken;
return response()->json([
'data' => $account,
'token' => $token
]);
}
// POST /api/v1/auth/logout
public function logout(Request $req)
{
// Hapus token yang dipakai saat ini
$req->user()->currentAccessToken()->delete();
return response()->json(['message'=>'Logged out']);
}
}
@@ -0,0 +1,53 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Api\Controller;
use App\Http\Resources\V1\SubmissionListResource;
use App\Models\Submission;
use App\Models\Account;
use App\Http\Resources\V1\DashboardStatsResource;
use Illuminate\Http\Request;
class DoctorDashboardController extends Controller
{
public function stats(Request $request)
{
$user = $request->user();
$totalPatients = Submission::where('doctor_id', $user->id)
->distinct('patient_id')
->count('patient_id');
$pendingCount = Submission::where('doctor_id', $user->id)
->where('status', 'pending')
->count();
$verifiedCount = Submission::where('doctor_id', $user->id)
->where('status', 'verified')
->count();
return new DashboardStatsResource(compact(
'totalPatients', 'pendingCount', 'verifiedCount'
));
}
public function pendingVerifications(Request $request)
{
$user = $request->user();
$limit = (int) $request->query('limit', 0);
$query = Submission::pending()
->where('doctor_id', $user->id)
->with('patient:id,name')
->orderBy('submitted_at', 'desc');
if ($limit > 0) {
$subs = $query->limit($limit)->get();
} else {
$subs = $query->get();
}
return SubmissionListResource::collection($subs);
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Api\Controller;
use App\Models\Account;
use App\Http\Resources\V1\PatientListResource;
use Illuminate\Http\Request;
class DoctorPatientController extends Controller
{
public function index(Request $request)
{
$doctorId = $request->user()->id;
$limit = (int) $request->query('limit', 0);
$query = Account::where('role', 'patient')
->whereHas('submission', fn($q) => $q->where('doctor_id', $doctorId))
->withCount(['submission as submission_count' => fn($q) => $q->where('doctor_id', $doctorId)])
->orderBy('name');
if ($limit > 0) {
$patients = $query->limit($limit)->get(['id','name','phone']);
} else {
$patients = $query->get(['id','name','phone']);
}
return PatientListResource::collection($patients);
}
}
@@ -0,0 +1,88 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Filters\V1\DoctorProfileFilter;
use App\Http\Controllers\Api\Controller;
use App\Http\Requests\V1\StoreDoctorProfileRequest;
use App\Http\Requests\V1\UpdateDoctorProfileRequest;
use App\Http\Resources\V1\DoctorProfileCollection;
use App\Http\Resources\V1\DoctorProfileResource;
use App\Models\DoctorProfile;
use Illuminate\Http\Request;
class DoctorProfileController extends Controller
{
public function __construct()
{
$this->middleware('auth:sanctum');
$this->authorizeResource(DoctorProfile::class,'doctorProfile');
}
public function index(Request $r)
{
$this->authorize('viewAny', DoctorProfile::class);
// kalau dokter, return hanya profil dia sendiri:
if ($r->user()->role==='doctor') {
$p = DoctorProfile::where('user_id',$r->user()->id)->firstOrFail();
return new DoctorProfileCollection(collect([$p]));
}
// kalau admin, bisa lihat semua:
return new DoctorProfileCollection(DoctorProfile::paginate());
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreDoctorProfileRequest $request)
{
//
return new DoctorProfileResource(DoctorProfile::create($request->validated()));
}
/**
* Display the specified resource.
*/
public function show(DoctorProfile $doctorProfile)
{
//
return new DoctorProfileResource($doctorProfile);
}
/**
* Show the form for editing the specified resource.
*/
public function edit(DoctorProfile $doctorProfile)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateDoctorProfileRequest $request, DoctorProfile $doctorProfile)
{
//
$doctorProfile->update($request->validated());
return new DoctorProfileResource($doctorProfile->fresh());
}
/**
* Remove the specified resource from storage.
*/
public function destroy(DoctorProfile $doctorProfile)
{
//
$doctorProfile->delete();
return response()->json(['message' => 'Doctor profile deleted successfully'], 200);
}
}
@@ -0,0 +1,93 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Api\Controller;
use App\Http\Requests\V1\DoctorUpdateSubmissionRequest;
use App\Http\Resources\V1\DoctorHistorySubmissionResource;
use App\Http\Resources\V1\SubmissionResource;
use App\Models\Submission;
use App\Http\Resources\V1\SubmissionListResource;
use App\Http\Resources\V1\SubmissionDetailResource;
use Illuminate\Http\Request;
class DoctorSubmissionController extends Controller
{
public function update(DoctorUpdateSubmissionRequest $request, Submission $submission)
{
$this->authorize('update', $submission);
$valid = $request->validated();
$data = [
'diagnosis' => $valid['diagnosis'],
'doctor_note' => $valid['doctorNote'],
'doctor_id' => $request->user()->id,
'status' => 'verified',
'verified_at' => now(),
];
$submission->update($data);
return new SubmissionDetailResource(
$submission->fresh()->load('patient')
);
}
public function pending(Request $request)
{
$user = $request->user();
$request->merge(['only_percentage' => true]);
$subs = Submission::pending()
->where('doctor_id', $user->id)
->whereHas('patient', function ($q) use ($request) {
if ($search = $request->query('search')) {
$q->where('name', 'like', "%{$search}%");
}
})
->with('patient:id,name')
->orderBy('submitted_at','desc')
->get();
return SubmissionListResource::collection($subs);
}
public function history(Request $request)
{
$this->authorize('viewAny', Submission::class);
$doctorId = $request->user()->id;
$limit = (int) $request->query('limit', 0);
$query = Submission::where('status', 'verified')
->where('doctor_id', $doctorId)
->whereHas('patient', function ($q) use ($request) {
if ($search = $request->query('search')) {
$q->where('name', 'like', "%{$search}%");
}
})
->with('patient:id,name')
->orderBy('verified_at', 'desc');
if ($limit > 0) {
$subs = $query->limit($limit)->get();
} else {
$subs = $query->get();
}
return DoctorHistorySubmissionResource::collection($subs);
}
public function detail($id)
{
$sub = Submission::with('patient')
->findOrFail($id);
return new SubmissionDetailResource($sub);
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Api\Controller;
use App\Http\Requests\V1\PublicDetectionRequest;
use App\Services\SkinAiService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class PublicDetectionController extends Controller
{
public function detect(PublicDetectionRequest $request): JsonResponse
{
$image = $request->file('image');
$pct = SkinAiService::predict($image);
$threshold = config('ai.threshold');
$labelPos = config('ai.label_positive');
$labelNeg = config('ai.label_negative');
$label = is_numeric($pct)
? ($pct >= $threshold ? $labelPos : $labelNeg)
: null;
return response()->json([
'percentage' => $pct,
'diagnosisAi' => $label,
]);
}
}
@@ -0,0 +1,211 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Filters\V1\SubmissionFilter;
use App\Http\Controllers\Api\Controller;
use App\Http\Requests\V1\StoreSubmissionRequest;
use App\Http\Requests\V1\UpdateSubmissionRequest;
use App\Http\Resources\V1\PatientDetectionDetailResource;
use App\Http\Resources\V1\PatientDetectionHistoryResource;
use App\Http\Resources\V1\PatientSubmissionDetailResource;
use App\Http\Resources\V1\PatientSubmissionHistoryResource;
use App\Http\Resources\V1\SubmissionCollection;
use App\Http\Resources\V1\SubmissionResource;
use App\Models\Submission;
use App\Services\SkinAiService;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
class SubmissionController extends Controller
{
public function __construct()
{
$this->middleware('auth:sanctum');
//$this->authorizeResource(Submission::class,'submission');
}
public function pending(Request $request)
{
$this->authorize('viewAny', Submission::class);
$query = Submission::with('patient')
->where('status', 'pending');
$subs = $query->paginate()
->appends($request->query());
return new SubmissionCollection($subs);
}
public function history(Request $request)
{
$this->authorize('viewAny', Submission::class);
$query = Submission::with('patient')
->where('status', '!=', 'pending');
$subs = $query->paginate()
->appends($request->query());
return new SubmissionCollection($subs);
}
public function index(Request $request)
{
$this->authorize('viewAny', Submission::class);
$filter = new SubmissionFilter();
$filterItems = $filter->transform($request);
$query = Submission::where($filterItems);
if ($request->user()->role === 'patient') {
$query->where('patient_id', $request->user()->id);
}
$statusCounts = (clone $query)
->selectRaw('status, count(*) as total')
->groupBy('status')
->pluck('total', 'status')
->toArray();
$subs = $query
->paginate()
->appends($request->query());
return (new SubmissionCollection($subs))
->additional(['statusCounts' => $statusCounts]);
}
public function detectionHistory(Request $r)
{
$user = $r->user();
//$this->authorize('viewAny', Submission::class);
$subs = Submission::where('patient_id',$user->id)
->orderBy('submitted_at','desc')->get();
return PatientDetectionHistoryResource::collection($subs);
}
public function submissionHistory(Request $r)
{
$user = $r->user();
$this->authorize('viewAny', Submission::class);
$subs = Submission::where('patient_id',$user->id)
->where('status','!=','pending')
->orderBy('verified_at','desc')->get();
return PatientSubmissionHistoryResource::collection($subs);
}
public function detectionDetail($id)
{
$submission = Submission::findOrFail($id);
$this->authorize('viewAny', $submission);
return new PatientDetectionDetailResource($submission);
}
public function submissionDetail($id)
{
$submission = Submission::findOrFail($id);
$this->authorize('viewAny', $submission);
return new PatientSubmissionDetailResource($submission);
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreSubmissionRequest $request)
{
$data = $request->validated();
$path = $request->file('image')->store('submissions', 'public');
$data['image_path'] = $path;
$data['submitted_at'] = Carbon::now();
$percentage = SkinAiService::predict($request->file('image'));
$data['percentage'] = $percentage;
$submission = Submission::create($data);
return (new SubmissionResource($submission))
->response()
->setStatusCode(201);
}
/**
* Display the specified resource.
*/
public function show(Submission $submission)
{
//
return new SubmissionResource($submission);
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Submission $submission)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateSubmissionRequest $request, Submission $submission)
{
$data = $request->validated();
if ($request->has('doctorId')) {
$data['doctor_id'] = $request->input('doctorId');
}
// kalau pasien/sudah submit, set is_submitted
if (in_array($request->user()->role, ['patient','admin'])) {
$data['is_submitted'] = 'Sudah';
}
// untuk dokter/admin saat verifikasi
if (in_array($request->user()->role, ['doctor','admin'])
&& isset($data['status'])
&& $data['status'] === 'verified'
&& ! $submission->verified_at
) {
$data['verified_at'] = now();
}
$submission->update($data);
return new SubmissionResource($submission->refresh());
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Submission $submission)
{
$submission->delete();
return response()->json(['message' => 'Submission deleted'], 200);
}
}
@@ -0,0 +1,66 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Requests\V1\StoreVerificationRequest;
use App\Http\Requests\V1\UpdateVerificationRequest;
use App\Models\Verification;
class VerificationController
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreVerificationRequest $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(Verification $verification)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Verification $verification)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateVerificationRequest $request, Verification $verification)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Verification $verification)
{
//
}
}
@@ -0,0 +1,23 @@
<?php
namespace App\Http\Requests\V1;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class DoctorUpdateSubmissionRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'diagnosis' => ['required', Rule::in(['Melanoma', 'Bukan Melanoma'])],
'doctorNote' => ['required','string'],
];
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\V1;
use Illuminate\Foundation\Http\FormRequest;
class LoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'email' => ['required','email'],
'password' => ['required'],
];
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Http\Requests\V1;
use Illuminate\Foundation\Http\FormRequest;
class PublicDetectionRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'image' => ['required','image','max:2048'],
];
}
}
@@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\V1;
use Illuminate\Foundation\Http\FormRequest;
class RegisterDoctorRequest extends FormRequest
{
public function authorize() { return true; }
public function rules(): array
{
return [
'name' => ['required','string'],
'email' => ['required','email','unique:accounts,email'],
'phone' => ['required','string','unique:accounts,phone'],
'password' => ['required','string','min:8','confirmed'],
'practice_address' => ['required','string'],
'specialization' => ['required','string'],
'license_number' => ['required','string','unique:doctor_profiles,license_number'],
'license_file' => ['required','file','mimes:pdf,jpg,png','max:4096'],
'diploma_file' => ['required','file','mimes:pdf,jpg,png','max:4096'],
'certification_file' => ['sometimes','file','mimes:pdf,jpg,png','max:5120'],
'current_institution'=> ['required','string'],
'work_history' => ['required','string'],
'publications' => ['sometimes','string'],
];
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests\V1;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class RegisterRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => ['required','string'],
'email' => ['required','email','unique:accounts,email'],
'phone' => ['required','string','unique:accounts,phone'],
'dob' => ['required','date'],
'password' => ['required','min:6','confirmed'],
//'role' => ['required', Rule::in(['patient','doctor'])],
];
}
}
@@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests\V1;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreAccountRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => ['required'],
'email' => ['required', 'email'],
'phone' => ['required'],
'dob' => ['required'],
'role' => ['required', Rule::in(['patient', 'doctor', 'admin'])],
'password' => ['required'],
];
}
}
@@ -0,0 +1,55 @@
<?php
namespace App\Http\Requests\V1;
use Illuminate\Foundation\Http\FormRequest;
class StoreDoctorProfileRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'user_id' => ['required'],
'specialization' => ['required'],
'license_number' => ['required'],
'license_file_path' => ['required'],
'diploma_file_path' => ['required'],
'certification' => ['required'],
'current_institution' => ['required'],
//'years_of_experience' => ['required'],
'work_history' => ['required'],
'publications' => ['required'],
'practice_address' => ['required'],
];
}
protected function prepareForValidation() {
$data = [];
foreach ($this->validated() as $key => $obj) {
$obj['user_id'] = $obj['userId'] ?? null;
$obj['license_number'] = $obj['licenseNumber'] ?? null;
$obj['license_file_path'] = $obj['licenseFilePath'] ?? null;
$obj['diploma_file_path'] = $obj['diplomaFilePath'] ?? null;
$obj['current_institution'] = $obj['currentInstitution'] ?? null;
//$obj['years_of_experience'] = $obj['yearsOfExperience'] ?? null;
$obj['work_history'] = $obj['workHistory'] ?? null;
$data[] = $obj;
}
$this->merge($data);
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Http\Requests\V1;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreSubmissionRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'patient_id' => ['required', 'integer'],
'doctor_id' => ['sometimes', 'integer'],
'image' => ['required', 'image', 'max:2048'],
'complaint' => ['sometimes', 'string'],
'status' => ['sometimes', Rule::in(['pending','verified','rejected'])],
'diagnosis' => ['sometimes','string'],
'doctor_note'=> ['sometimes','string'],
// 'submitted_at' => ['sometimes','date'],
// 'verified_at' => ['sometimes','date'],
];
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\V1;
use Illuminate\Foundation\Http\FormRequest;
class StoreVerificationRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return false;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
//
];
}
}
@@ -0,0 +1,46 @@
<?php
namespace App\Http\Requests\V1;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateAccountRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
$method = $this->method();
if ($method == 'PUT') {
return [
'name' => ['required'],
'email' => ['required', 'email'],
'phone' => ['required'],
'dob' => ['required'],
'role' => ['required', Rule::in(['patient', 'doctor', 'admin'])],
'password' => ['required'],
];
} else {
return [
'name' => ['sometimes', 'required'],
'email' => ['sometimes', 'required', 'email'],
'phone' => ['sometimes', 'required'],
'dob' => ['sometimes', 'required'],
'role' => ['sometimes', 'required', Rule::in(['patient', 'doctor', 'admin'])],
'password' => ['sometimes', 'required'],
];
}
}
}
@@ -0,0 +1,69 @@
<?php
namespace App\Http\Requests\V1;
use Illuminate\Foundation\Http\FormRequest;
class UpdateDoctorProfileRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
$method = $this->method();
if ($method == 'PUT') {
return [
'userId' => ['required'],
'specialization' => ['required'],
'licenseNumber' => ['required'],
'licenseFilePath' => ['required'],
'diplomaFilePath' => ['required'],
'certification' => ['required'],
'currentInstitution' => ['required'],
//'yearsOfExperience' => ['required'],
'workHistory' => ['required'],
'publications' => ['required']
];
} else {
return [
'userId' => ['sometimes', 'required'],
'specialization' => ['sometimes', 'required'],
'licenseNumber' => ['sometimes', 'required'],
'licenseFilePath' => ['sometimes', 'required'],
'diplomaFilePath' => ['sometimes', 'required'],
'certification' => ['sometimes', 'required'],
'currentInstitution' => ['sometimes', 'required'],
//'yearsOfExperience' => ['sometimes', 'required'],
'workHistory' => ['sometimes', 'required'],
'publications' => ['sometimes', 'required'],
'practice_address' => ['sometimes', 'required'],
];
}
}
protected function prepareForValidation()
{
$data = $this->all();
$data['user_id'] = $data['userId'] ?? $data['user_id'] ?? null;
$data['license_number'] = $data['licenseNumber'] ?? $data['license_number'] ?? null;
$data['license_file_path'] = $data['licenseFilePath'] ?? $data['license_file_path'] ?? null;
$data['diploma_file_path'] = $data['diplomaFilePath'] ?? $data['diploma_file_path'] ?? null;
$data['current_institution'] = $data['currentInstitution'] ?? $data['current_institution'] ?? null;
//$data['years_of_experience'] = $data['yearsOfExperience'] ?? $data['years_of_experience'] ?? null;
$data['work_history'] = $data['workHistory'] ?? $data['work_history'] ?? null;
$this->merge($data);
}
}
@@ -0,0 +1,63 @@
<?php
namespace App\Http\Requests\V1;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateSubmissionRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
$method = $this->method();
if ($method == 'PUT') {
return [
'doctorId' => ['required'],
'status' => ['required', Rule::in(['pending', 'verified', 'rejected'])],
'diagnosis' => ['required'],
'doctorNote' => ['required'],
];
} else {
return [
'doctorId' => ['required'],
'status' => ['sometimes', 'required', Rule::in(['pending', 'verified', 'rejected'])],
'diagnosis' => ['sometimes', 'required'],
'doctorNote' => ['sometimes', 'required'],
'complaint' => ['sometimes', 'required'],
];
}
}
/**
protected function prepareForValidation()
{
$mapped = [];
if ($this->has('doctorId')) {
$mapped['doctor_id'] = $this->input('doctorId');
}
if ($this->has('doctorNote')) {
$mapped['doctor_note'] = $this->input('doctorNote');
}
if ($this->has('isSubmitted')) {
$mapped['is_submitted'] = $this->input('isSubmitted');
}
$this->merge($mapped);
} */
}
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\V1;
use Illuminate\Foundation\Http\FormRequest;
class UpdateVerificationRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return false;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
//
];
}
}
@@ -0,0 +1,23 @@
<?php
namespace App\Http\Resources\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class AccountCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
public function toArray(Request $request): array
{
// return [
// 'data' => $this->collection,
// 'status' => true,
// ];
return parent::toArray($request);
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Resources\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class AccountResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'phone' => $this->phone,
'dob' => $this->dob,
'role' => $this->role,
'doctorProfile' => DoctorProfileResource::make($this->whenLoaded('doctorProfile')),
'submission' => SubmissionResource::collection($this->whenLoaded('submission')),
];
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Http\Resources\V1;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Request;
class DashboardStatsResource extends JsonResource
{
public function toArray(Request $request)
{
return [
'totalPatients' => $this['totalPatients'],
'pending' => $this['pendingCount'],
'verified' => $this['verifiedCount'],
];
}
}
@@ -0,0 +1,34 @@
<?php
namespace App\Http\Resources\V1;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Storage;
class DoctorHistorySubmissionResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'patientId' => $this->patient->id,
'submittedAt' => optional($this->submitted_at)->format('Y-m-d'),
'patientName' => $this->patient->name,
'diagnosisAi' => $this->getDiagnosisAi(),
'diagnosis' => $this->diagnosis,
'doctorNote' => $this->doctor_note,
];
}
private function getDiagnosisAi(): ?string
{
$pct = $this->percentage;
$threshold = config('ai.threshold');
$labelPos = config('ai.label_positive');
$labelNeg = config('ai.label_negative');
return is_numeric($pct)
? sprintf('%d%% %s', round($pct * 1), $pct >= $threshold ? $labelPos : $labelNeg)
: null;
}
}
@@ -0,0 +1,22 @@
<?php
namespace App\Http\Resources\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class DoctorListResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Resources\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class DoctorProfileCollection 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,30 @@
<?php
namespace App\Http\Resources\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class DoctorProfileResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request)
{
return [
'userId' =>$this->user_id,
'specialization' =>$this->specialization,
'licenseNumber' =>$this->license_number,
'licenseFilePath' =>$this->license_file_path,
'diplomaFilePath' =>$this->diploma_file_path,
'certification' =>$this->certification,
'currentInstitution' =>$this->current_institution,
//'yearsOfExperience' =>$this->years_of_experience,
'workHistory' =>$this->work_history,
'publications' =>$this->publications
];
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Http\Resources\V1;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Storage;
class PatientDetectionDetailResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'doctorId' => $this->doctor_id,
'imageUrl' => Storage::disk('public')->url($this->image_path),
'diagnosis' => $this->diagnosis ?? 'Belum dapat dipastikan',
'diagnosisAi' => $this->getDiagnosisAi(),
'isSubmitted' => $this->is_submitted,
'status' => $this->status,
];
}
private function getDiagnosisAi(): ?string
{
$pct = $this->percentage;
$threshold = config('ai.threshold');
$labelPos = config('ai.label_positive');
$labelNeg = config('ai.label_negative');
return is_numeric($pct)
? sprintf('%d%% %s', round($pct * 1), $pct >= $threshold ? $labelPos : $labelNeg)
: null;
}
}
@@ -0,0 +1,34 @@
<?php
namespace App\Http\Resources\V1;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Storage;
class PatientDetectionHistoryResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'submittedAt' => optional($this->submitted_at)->format('Y-m-d'),
'diagnosisAi' => $this->getDiagnosisAi(),
'imageUrl' => Storage::disk('public')->url($this->image_path),
'isSubmitted' => $this->is_submitted,
'status' => $this->status,
'complaint' => $this->complaint,
];
}
private function getDiagnosisAi(): ?string
{
$pct = $this->percentage;
$threshold = config('ai.threshold');
$labelPos = config('ai.label_positive');
$labelNeg = config('ai.label_negative');
return is_numeric($pct)
? sprintf('%d%% %s', round($pct * 1), $pct >= $threshold ? $labelPos : $labelNeg)
: null;
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Http\Resources\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PatientListResource extends JsonResource
{
public function toArray(Request $request)
{
return [
'id' => $this->id,
'name' => $this->name,
'phone' => $this->phone,
'submissionCount' => $this->submission_count,
];
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Http\Resources\V1;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Storage;
class PatientSubmissionDetailResource extends JsonResource
{
public function toArray($request)
{
$user = $this->patient;
return [
'imageUrl' => Storage::disk('public')->url($this->image_path),
'verifiedBy' => $this->doctor?->name,
'patient' => [
'name' => $user->name,
'phone' => $user->phone,
'email' => $user->email,
'dob' => optional($user->dob)->format('Y-m-d'),
],
'complaint' => $this->complaint,
'diagnosis' => $this->diagnosis,
'diagnosisAi' => $this->getDiagnosisAi(),
'status' => $this->status,
'doctorNote' => $this->doctor_note,
];
}
private function getDiagnosisAi(): ?string
{
$pct = $this->percentage;
$threshold = config('ai.threshold');
$labelPos = config('ai.label_positive');
$labelNeg = config('ai.label_negative');
return is_numeric($pct)
? sprintf('%d%% %s', round($pct * 1), $pct >= $threshold ? $labelPos : $labelNeg)
: null;
}
}
@@ -0,0 +1,37 @@
<?php
namespace App\Http\Resources\V1;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Storage;
class PatientSubmissionHistoryResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'submittedAt' => optional($this->submitted_at)->format('Y-m-d'),
'diagnosisAi' => $this->getDiagnosisAi(),
'imageUrl' => Storage::disk('public')->url($this->image_path),
'complaint' => $this->complaint,
'status' => $this->status,
'verifiedAt' => optional($this->verified_at)->format('Y-m-d'),
'verifiedBy' => $this->doctor?->name,
'diagnosis' => $this->diagnosis,
'doctorNote' => $this->doctor_note,
];
}
private function getDiagnosisAi(): ?string
{
$pct = $this->percentage;
$threshold = config('ai.threshold');
$labelPos = config('ai.label_positive');
$labelNeg = config('ai.label_negative');
return is_numeric($pct)
? sprintf('%d%% %s', round($pct * 1), $pct >= $threshold ? $labelPos : $labelNeg)
: null;
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Resources\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PendingVerificationResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return parent::toArray($request);
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Resources\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class SubmissionCollection 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,45 @@
<?php
namespace App\Http\Resources\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Storage;
class SubmissionDetailResource extends JsonResource
{
public function toArray(Request $request)
{
return [
'id' => $this->id,
'doctorId' => $this->doctor_id,
'patientId' => $this->patient->id,
'patientName' => $this->patient->name,
'patientPhone' => $this->patient->phone,
'patientEmail' => $this->patient->email,
'patientDob' => $this->patient->dob,
'imageUrl' => Storage::disk('public')->url($this->image_path),
'complaint' => $this->complaint,
'status' => $this->status,
'percentage' => $this->percentage,
'diagnosis' => $this->diagnosis ?? 'Belum dapat dipastikan',
'diagnosisAi' => $this->getDiagnosisAi(),
'doctorNote' => $this->doctor_note,
'isSubmitted' => $this->is_submitted,
'submittedAt' => optional($this->submitted_at)->format('Y-m-d'),
'verifiedAt' => optional($this->verified_at)->format('Y-m-d'),
];
}
private function getDiagnosisAi(): ?string
{
$pct = $this->percentage;
$threshold = config('ai.threshold');
$labelPos = config('ai.label_positive');
$labelNeg = config('ai.label_negative');
return is_numeric($pct)
? sprintf('%d%% %s', round($pct * 1), $pct >= $threshold ? $labelPos : $labelNeg)
: null;
}
}
@@ -0,0 +1,45 @@
<?php
namespace App\Http\Resources\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Storage;
class SubmissionListResource extends JsonResource
{
public function toArray(Request $request)
{
$onlyPercentage = $request->get('only_percentage', false);
if ($onlyPercentage) {
return [
'id' => $this->id,
'patientId' => $this->patient->id,
'patientName' => $this->patient->name,
'submittedAt' => optional($this->submitted_at)->format('Y-m-d'),
'imageUrl' => Storage::disk('public')->url($this->image_path),
'diagnosisAi' => $this->percentage,
];
}
// versi full list
return [
'id' => $this->id,
'patientName' => $this->patient->name,
'submittedAt' => optional($this->submitted_at)->format('Y-m-d'),
'diagnosisAi' => $this->getDiagnosisAi(),
];
}
private function getDiagnosisAi(): ?string
{
$pct = $this->percentage;
$threshold = config('ai.threshold');
$labelPos = config('ai.label_positive');
$labelNeg = config('ai.label_negative');
return is_numeric($pct)
? sprintf('%d%% %s', round($pct * 1), $pct >= $threshold ? $labelPos : $labelNeg)
: null;
}
}
@@ -0,0 +1,45 @@
<?php
namespace App\Http\Resources\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Storage;
class SubmissionResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request)
{
$threshold = config('ai.threshold');
$labelPos = config('ai.label_positive');
$labelNeg = config('ai.label_negative');
$pct = $this->percentage;
$diagnosisAi = is_numeric($pct)
? ($pct >= $threshold ? $labelPos : $labelNeg)
: null;
return [
'id' => $this->id,
'patientId' => $this->patient_id,
'patientName' =>$this->patient->name,
'doctorId' => $this->doctor_id,
'imageUrl' => Storage::disk('public')->url($this->image_path),
'complaint' => $this->complaint,
'status' => $this->status,
'diagnosis' => $this->diagnosis ?? 'Belum dapat dipastikan',
'doctorNote' => $this->doctor_note,
'isSubmitted' => $this->is_submitted,
'submittedAt' => optional($this->submitted_at)->format('Y-m-d'),
'verifiedAt' => optional($this->verified_at)->format('Y-m-d'),
'percentage' => $this->percentage,
'diagnosisAi' => $diagnosisAi,
];
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Sanctum\HasApiTokens;
class Account extends Authenticatable
{
/** @use HasFactory<\Database\Factories\AccountFactory> */
use HasApiTokens, HasFactory;
protected $fillable = [
'name',
'email',
'phone',
'dob',
'role',
'password',
];
protected function casts(): array
{
return [
'password' => 'hashed',
'role' => 'string', // patient or doctor
];
}
public function submission()
{
return $this->hasMany(Submission::class, 'patient_id');
}
public function doctorProfile() {
return $this->hasOne(DoctorProfile::class, 'user_id');
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class DoctorProfile extends Model
{
/** @use HasFactory<\Database\Factories\DoctorProfileFactory> */
use HasFactory;
protected $fillable = [
'user_id',
'specialization',
'license_number',
'license_file_path',
'diploma_file_path',
'certification_file_path',
'current_institution',
'work_history',
'publications',
'practice_address'
];
public function account() {
return $this->hasOne(Account::class, 'user_id');
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace App\Models;
use Database\Factories\SubmissionFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Submission extends Model
{
/** @use HasFactory<SubmissionFactory> */
use HasFactory;
protected $fillable = [
'patient_id',
'doctor_id',
'image_path',
'complaint',
'status',
'diagnosis',
'doctor_note',
'submitted_at',
'verified_at',
'percentage',
'is_submitted',
];
protected $casts = [
'submitted_at' => 'datetime',
'verified_at' => 'datetime',
];
public function scopePending($q) { return $q->where('status','pending'); }
public function scopeHistory($q) { return $q->where('status','!=','pending'); }
public function patient()
{
return $this->belongsTo(Account::class, 'patient_id');
}
public function doctor() {
return $this->belongsTo(Account::class, 'doctor_id');
}
}
+49
View File
@@ -0,0 +1,49 @@
<?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 HasFactory<\Database\Factories\UserFactory> */
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Verification extends Model
{
/** @use HasFactory<\Database\Factories\VerificationFactory> */
use HasFactory;
}
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace App\Policies;
use App\Models\Account;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class AccountPolicy
{
public function viewAny($user)
{
return in_array($user->role, ['admin', 'patient']);
}
public function view($user, Account $account)
{
// admin boleh lihat semua, user bisa lihat diri sendiri
return $user->role==='admin' || $user->id === $account->id;
}
public function update($user, Account $account)
{
// admin boleh update semua, user bisa update diri sendiri
return $user->role==='admin' || $user->id === $account->id;
}
public function delete($user, Account $account)
{
// hanya admin
return $user->role==='admin';
}
public function create($user)
{
// pembuatan akun via API hanya lewat register
return false;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Account $account): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Account $account): bool
{
return false;
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
namespace App\Policies;
use App\Models\Account;
use App\Models\DoctorProfile;
use Illuminate\Auth\Access\Response;
class DoctorProfilePolicy
{
public function viewAny($user)
{
// hanya dokter & admin yg butuh list profil dokter
return in_array($user->role,['doctor','admin']);
}
public function view($user, DoctorProfile $p)
{
// dokter hanya lihat profil dirinya sendiri
return $user->role==='doctor' && $p->user_id === $user->id || $user->role==='admin';
}
public function update($user, DoctorProfile $p)
{
return $user->role==='doctor' && $p->user_id === $user->id || $user->role==='admin';
}
public function delete($user, DoctorProfile $p)
{
// biasanya nggak dihapus, tapi kalau perlu:
return $user->role==='admin';
}
public function create($user)
{
// profil dokter dibuat otomatis waktu register doctor
return false;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(Account $user, DoctorProfile $doctorProfile): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(Account $user, DoctorProfile $doctorProfile): bool
{
return false;
}
}
+79
View File
@@ -0,0 +1,79 @@
<?php
namespace App\Policies;
use App\Models\Submission;
use App\Models\Account;
use Illuminate\Auth\Access\Response;
class SubmissionPolicy
{
public function viewAny($user)
{
// pasien & dokter boleh lihat submission…
return in_array($user->role, ['patient','doctor', 'admin']);
}
public function view($user, Submission $s)
{
if($user->role==='patient'){
return $s->patient_id === $user->id;
}
if($user->role==='doctor' || $user->role==='admin'){
// dokter lihat semua (atau tambahkan filter assigned/idempot)
return true;
}
return false;
}
public function create($user)
{
return $user->role==='patient' || $user->role==='admin';
}
public function update($user, Submission $s)
{
if ($user->role === 'admin') {
return true;
}
if ($user->role === 'doctor') {
return true;
}
if ($user->role === 'patient') {
return $s->patient_id === $user->id
&& $s->status === 'pending';
}
return false;
}
public function delete($user, Submission $submission)
{
if ($user->role === 'admin') {
return true;
}
if ($user->role === 'doctor') {
return true;
}
if ($user->role === 'patient') {
return $submission->patient_id === $user->id;
}
return false;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(Account $user, Submission $submission): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(Account $user, Submission $submission): bool
{
return false;
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace App\Policies;
use App\Models\User;
use App\Models\Verification;
use Illuminate\Auth\Access\Response;
class VerificationPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return false;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Verification $verification): bool
{
return false;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return false;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Verification $verification): bool
{
return false;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Verification $verification): bool
{
return false;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Verification $verification): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Verification $verification): bool
{
return false;
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Providers;
use App\Services\SkinAiService;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
$this->app->singleton(SkinAiService::class);
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace App\Providers;
use App\Models\Account;
use App\Models\Submission;
use App\Models\DoctorProfile;
use App\Policies\AccountPolicy;
use App\Policies\SubmissionPolicy;
use App\Policies\DoctorProfilePolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
{
/**
* Map model policy
*
* @var array<class-string, class-string>
*/
protected $policies = [
Account::class => AccountPolicy::class,
Submission::class => SubmissionPolicy::class,
DoctorProfile::class => DoctorProfilePolicy::class,
];
/**
* Register any authentication / authorization services.
*/
public function boot(): void
{
Gate::define('access-doctor-section', fn($user)=> $user->role==='doctor' || $user->role==='admin');
$this->registerPolicies();
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
class RouteServiceProvider extends ServiceProvider
{
/**
* The path to the "home" route for your application.
*
* @var string
*/
public const HOME = '/home';
/**
* Define your route model bindings, pattern filters, etc.
*/
public function boot()
{
$this->routes(function () {
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));
Route::middleware('web')
->group(base_path('routes/web.php'));
});
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Services;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Http;
class SkinAiService
{
public static function predict(UploadedFile $image): ?float
{
$response = Http::timeout(5)
->withHeaders([
'Accept' => 'application/json',
// 'Authorization' => 'Bearer ' . config('ai.predict_key'),
])
->attach(
'image',
fopen($image->getRealPath(), 'r'),
$image->getClientOriginalName()
)
->post(config('ai.predict_url'));
if (! $response->ok() || ! isset($response['prediction'])) {
return null;
}
return (float) $response['prediction'];
}
}