diff --git a/backend/.gitignore b/backend/.gitignore index b71b1ea..2f164e3 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,6 +1,7 @@ *.log .DS_Store .env +.env.testing .env.backup .env.production .phpactor.json diff --git a/backend/app/Http/Controllers/Auth/AuthController.php b/backend/app/Http/Controllers/Auth/AuthController.php new file mode 100644 index 0000000..87397d3 --- /dev/null +++ b/backend/app/Http/Controllers/Auth/AuthController.php @@ -0,0 +1,25 @@ +user(); + + $relation = match ($user->role) { + 'patient' => 'patient', + default => 'patient', // expand di EPIC 2 + }; + + return response()->json([ + 'data' => new UserResource($user->load($relation)), + ]); + } +} diff --git a/backend/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/backend/app/Http/Controllers/Auth/AuthenticatedSessionController.php new file mode 100644 index 0000000..e8574a5 --- /dev/null +++ b/backend/app/Http/Controllers/Auth/AuthenticatedSessionController.php @@ -0,0 +1,54 @@ +authenticate(); + + $request->session()->regenerate(); + + $user = Auth::user(); + + $relation = match ($user->role) { + 'patient' => 'patient', + default => 'patient', // expand di EPIC 2 + }; + + return response()->json([ + 'message' => 'Login berhasil.', + 'data' => [ + 'user' => new UserResource($user->load($relation)), + ], + ]); + } + + /** + * Destroy an authenticated session. + */ + public function destroy(Request $request): JsonResponse + { + Auth::guard('web')->logout(); + + $request->session()->invalidate(); + + $request->session()->regenerateToken(); + + return response()->json([ + 'message' => 'Logout Successful.', + ]); + } +} diff --git a/backend/app/Http/Controllers/Auth/EmailVerificationNotificationController.php b/backend/app/Http/Controllers/Auth/EmailVerificationNotificationController.php new file mode 100644 index 0000000..0550fbd --- /dev/null +++ b/backend/app/Http/Controllers/Auth/EmailVerificationNotificationController.php @@ -0,0 +1,25 @@ +user()->hasVerifiedEmail()) { + return redirect()->intended('/dashboard'); + } + + $request->user()->sendEmailVerificationNotification(); + + return response()->json(['status' => 'verification-link-sent']); + } +} diff --git a/backend/app/Http/Controllers/Auth/NewPasswordController.php b/backend/app/Http/Controllers/Auth/NewPasswordController.php new file mode 100644 index 0000000..8c0959b --- /dev/null +++ b/backend/app/Http/Controllers/Auth/NewPasswordController.php @@ -0,0 +1,53 @@ +validate([ + 'token' => ['required'], + 'email' => ['required', 'email'], + 'password' => ['required', 'confirmed', Rules\Password::defaults()], + ]); + + // Here we will attempt to reset the user's password. If it is successful we + // will update the password on an actual user model and persist it to the + // database. Otherwise we will parse the error and return the response. + $status = Password::reset( + $request->only('email', 'password', 'password_confirmation', 'token'), + function ($user) use ($request) { + $user->forceFill([ + 'password' => Hash::make($request->string('password')), + 'remember_token' => Str::random(60), + ])->save(); + + event(new PasswordReset($user)); + } + ); + + if ($status != Password::PASSWORD_RESET) { + throw ValidationException::withMessages([ + 'email' => [__($status)], + ]); + } + + return response()->json(['status' => __($status)]); + } +} diff --git a/backend/app/Http/Controllers/Auth/PasswordResetLinkController.php b/backend/app/Http/Controllers/Auth/PasswordResetLinkController.php new file mode 100644 index 0000000..d555988 --- /dev/null +++ b/backend/app/Http/Controllers/Auth/PasswordResetLinkController.php @@ -0,0 +1,39 @@ +validate([ + 'email' => ['required', 'email'], + ]); + + // We will send the password reset link to this user. Once we have attempted + // to send the link, we will examine the response then see the message we + // need to show to the user. Finally, we'll send out a proper response. + $status = Password::sendResetLink( + $request->only('email') + ); + + if ($status != Password::RESET_LINK_SENT) { + throw ValidationException::withMessages([ + 'email' => [__($status)], + ]); + } + + return response()->json(['status' => __($status)]); + } +} diff --git a/backend/app/Http/Controllers/Auth/RegisteredUserController.php b/backend/app/Http/Controllers/Auth/RegisteredUserController.php new file mode 100644 index 0000000..bb939b7 --- /dev/null +++ b/backend/app/Http/Controllers/Auth/RegisteredUserController.php @@ -0,0 +1,32 @@ +authService->registerPatient($request->validated()); + + return response()->json([ + 'message' => 'Register Success', + 'data' => new UserResource($user), + ], 201); + } +} diff --git a/backend/app/Http/Controllers/Auth/VerifyEmailController.php b/backend/app/Http/Controllers/Auth/VerifyEmailController.php new file mode 100644 index 0000000..33cbed4 --- /dev/null +++ b/backend/app/Http/Controllers/Auth/VerifyEmailController.php @@ -0,0 +1,31 @@ +user()->hasVerifiedEmail()) { + return redirect()->intended( + config('app.frontend_url').'/dashboard?verified=1' + ); + } + + if ($request->user()->markEmailAsVerified()) { + event(new Verified($request->user())); + } + + return redirect()->intended( + config('app.frontend_url').'/dashboard?verified=1' + ); + } +} diff --git a/backend/app/Http/Middleware/EnsureEmailIsVerified.php b/backend/app/Http/Middleware/EnsureEmailIsVerified.php new file mode 100644 index 0000000..2a7df86 --- /dev/null +++ b/backend/app/Http/Middleware/EnsureEmailIsVerified.php @@ -0,0 +1,27 @@ +user() || + ($request->user() instanceof MustVerifyEmail && + ! $request->user()->hasVerifiedEmail())) { + return response()->json(['message' => 'Your email address is not verified.'], 409); + } + + return $next($request); + } +} diff --git a/backend/app/Http/Middleware/RedirectIfAuthenticated.php b/backend/app/Http/Middleware/RedirectIfAuthenticated.php new file mode 100644 index 0000000..050f8eb --- /dev/null +++ b/backend/app/Http/Middleware/RedirectIfAuthenticated.php @@ -0,0 +1,35 @@ +check()) { + if ($request->expectsJson()) { + return response()->json([ + 'message' => 'Already authenticated.', + ], 409); + } + + return redirect('/'); + } + } + + return $next($request); + } +} diff --git a/backend/app/Http/Requests/Auth/LoginRequest.php b/backend/app/Http/Requests/Auth/LoginRequest.php new file mode 100644 index 0000000..9dc38aa --- /dev/null +++ b/backend/app/Http/Requests/Auth/LoginRequest.php @@ -0,0 +1,86 @@ +|string> + */ + public function rules(): array + { + return [ + 'email' => ['required', 'string', 'email'], + 'password' => ['required', 'string'], + ]; + } + + /** + * Attempt to authenticate the request's credentials. + * + * @throws ValidationException + */ + public function authenticate(): void + { + $this->ensureIsNotRateLimited(); + + if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { + RateLimiter::hit($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => __('auth.failed'), + ]); + } + + RateLimiter::clear($this->throttleKey()); + } + + /** + * Ensure the login request is not rate limited. + * + * @throws ValidationException + */ + public function ensureIsNotRateLimited(): void + { + if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { + return; + } + + event(new Lockout($this)); + + $seconds = RateLimiter::availableIn($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => trans('auth.throttle', [ + 'seconds' => $seconds, + 'minutes' => ceil($seconds / 60), + ]), + ]); + } + + /** + * Get the rate limiting throttle key for the request. + */ + public function throttleKey(): string + { + return Str::transliterate(Str::lower($this->input('email')).'|'.$this->ip()); + } +} diff --git a/backend/app/Http/Requests/Auth/RegisterRequest.php b/backend/app/Http/Requests/Auth/RegisterRequest.php new file mode 100644 index 0000000..42dbef5 --- /dev/null +++ b/backend/app/Http/Requests/Auth/RegisterRequest.php @@ -0,0 +1,32 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'], + 'password' => ['required', 'string', 'min:8', 'confirmed'], + 'phone' => ['required', 'string', 'max:15'], + ]; + } +} diff --git a/backend/app/Http/Resources/PatientProfileResource.php b/backend/app/Http/Resources/PatientProfileResource.php new file mode 100644 index 0000000..b6265e1 --- /dev/null +++ b/backend/app/Http/Resources/PatientProfileResource.php @@ -0,0 +1,28 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'patient_id' => $this->patient_id, + 'name' => $this->name, + 'phone' => $this->phone, + 'bpjs_number' => $this->bpjs_number, + 'birth_place' => $this->birth_place, + 'birth_date' => $this->birth_date?->format('Y-m-d'), + 'gender' => $this->gender, + 'user_id' => $this->user_id, + ]; + } +} diff --git a/backend/app/Http/Resources/UserResource.php b/backend/app/Http/Resources/UserResource.php new file mode 100644 index 0000000..582aec1 --- /dev/null +++ b/backend/app/Http/Resources/UserResource.php @@ -0,0 +1,35 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'email' => $this->email, + 'role' => $this->role, + 'status' => $this->status, + 'profile' => $this->resolveProfile(), + ]; + } + + private function resolveProfile(): mixed + { + return match ($this->role) { + 'patient' => $this->patient + ? new PatientProfileResource($this->patient) + : null, + default => null, + }; + } +} diff --git a/backend/app/Models/Patient.php b/backend/app/Models/Patient.php new file mode 100644 index 0000000..5cb45a2 --- /dev/null +++ b/backend/app/Models/Patient.php @@ -0,0 +1,38 @@ + */ + use HasFactory; + + protected $primaryKey = 'id'; + + protected $fillable = [ + 'user_id', + 'name', + 'phone', + 'bpjs_number', + 'birth_place', + 'birth_date', + 'gender', + ]; + + protected function casts(): array + { + return [ + 'birth_date' => 'date', + ]; + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id', 'id'); + } +} diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php index 68f3a66..0cf2a97 100644 --- a/backend/app/Models/User.php +++ b/backend/app/Models/User.php @@ -5,13 +5,17 @@ // use Illuminate\Contracts\Auth\MustVerifyEmail; use Database\Factories\UserFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { /** @use HasFactory */ - use HasFactory, Notifiable; + use HasFactory, Notifiable, HasApiTokens; + + protected $primaryKey = 'id'; /** * The attributes that are mass assignable. @@ -22,6 +26,8 @@ class User extends Authenticatable 'name', 'email', 'password', + 'role', + 'status', ]; /** @@ -46,4 +52,9 @@ protected function casts(): array 'password' => 'hashed', ]; } + + public function patient(): HasOne + { + return $this->hasOne(Patient::class, 'user_id', 'id'); + } } diff --git a/backend/app/Providers/AppServiceProvider.php b/backend/app/Providers/AppServiceProvider.php index 452e6b6..96ddc37 100644 --- a/backend/app/Providers/AppServiceProvider.php +++ b/backend/app/Providers/AppServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use Illuminate\Auth\Notifications\ResetPassword; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -19,6 +20,8 @@ public function register(): void */ public function boot(): void { - // + ResetPassword::createUrlUsing(function (object $notifiable, string $token) { + return config('app.frontend_url')."/password-reset/$token?email={$notifiable->getEmailForPasswordReset()}"; + }); } } diff --git a/backend/app/Providers/AuthServiceProvider.php b/backend/app/Providers/AuthServiceProvider.php new file mode 100644 index 0000000..f5789a8 --- /dev/null +++ b/backend/app/Providers/AuthServiceProvider.php @@ -0,0 +1,29 @@ + \App\Services\impl\AuthService::class + ]; + + /** + * Register services. + */ + public function register(): void + { + // + } + + /** + * Bootstrap services. + */ + public function boot(): void + { + // + } +} diff --git a/backend/app/Services/AuthService.php b/backend/app/Services/AuthService.php new file mode 100644 index 0000000..c3c8b1e --- /dev/null +++ b/backend/app/Services/AuthService.php @@ -0,0 +1,11 @@ + $data['email'], + 'password' => $data['password'], + 'role' => 'patient', + 'status' => 'active', + ]); + + Patient::create([ + 'user_id' => $user->id, + 'name' => $data['name'], + 'phone' => $data['phone'], + ]); + + return $user->load('patient'); + } +} diff --git a/backend/bootstrap/app.php b/backend/bootstrap/app.php index c3928c5..b09060e 100644 --- a/backend/bootstrap/app.php +++ b/backend/bootstrap/app.php @@ -12,7 +12,15 @@ health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { - // + $middleware->statefulApi(); + $middleware->alias([ + 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, + 'verified' => \App\Http\Middleware\EnsureEmailIsVerified::class, + ]); + $middleware->appendToGroup('api', [ + \Illuminate\Session\Middleware\StartSession::class, + ]); + }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/backend/bootstrap/providers.php b/backend/bootstrap/providers.php index fc94ae6..8dc3a48 100644 --- a/backend/bootstrap/providers.php +++ b/backend/bootstrap/providers.php @@ -1,7 +1,6 @@ ['*'], + + 'paths' => ['api/*', 'sanctum/csrf-cookie'], + + 'allowed_methods' => ['*'], + + 'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')], + + 'allowed_origins_patterns' => [], + + 'allowed_headers' => ['*'], + + 'exposed_headers' => [], + + 'max_age' => 0, + + 'supports_credentials' => true, + +]; diff --git a/backend/config/sanctum.php b/backend/config/sanctum.php index cde73cf..8ae01bf 100644 --- a/backend/config/sanctum.php +++ b/backend/config/sanctum.php @@ -19,10 +19,10 @@ */ 'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( - '%s%s', - 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + '%s%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:3000,127.0.0.1:8000,::1', Sanctum::currentApplicationUrlWithPort(), - // Sanctum::currentRequestHost(), + env('FRONTEND_URL') ? ','.parse_url(env('FRONTEND_URL'), PHP_URL_HOST) : '' ))), /* diff --git a/backend/database/factories/PatientFactory.php b/backend/database/factories/PatientFactory.php new file mode 100644 index 0000000..ac6b320 --- /dev/null +++ b/backend/database/factories/PatientFactory.php @@ -0,0 +1,29 @@ + + */ +class PatientFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => $this->faker->name(), + 'phone' => $this->faker->numerify('08##########'), + 'bpjs_number' => null, + 'birth_place' => null, + 'birth_date' => null, + 'gender' => null, + ]; + } +} diff --git a/backend/database/factories/UserFactory.php b/backend/database/factories/UserFactory.php index c4ceb07..de48345 100644 --- a/backend/database/factories/UserFactory.php +++ b/backend/database/factories/UserFactory.php @@ -25,14 +25,18 @@ class UserFactory extends Factory public function definition(): array { return [ - 'name' => fake()->name(), 'email' => fake()->unique()->safeEmail(), 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), + 'role' => 'patient', + 'status' => 'active', 'remember_token' => Str::random(10), ]; } +// public function doctor(): static +// public function nurse(): static + /** * Indicate that the model's email address should be unverified. */ diff --git a/backend/database/migrations/0001_01_01_000000_create_users_table.php b/backend/database/migrations/0001_01_01_000000_create_users_table.php index 05fb5d9..02839d6 100644 --- a/backend/database/migrations/0001_01_01_000000_create_users_table.php +++ b/backend/database/migrations/0001_01_01_000000_create_users_table.php @@ -13,10 +13,11 @@ public function up(): void { Schema::create('users', function (Blueprint $table) { $table->id(); - $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); + $table->enum('role', ['patient', 'doctor', 'nurse', 'admin']); + $table->enum('status', ['active', 'inactive'])->default('active'); $table->rememberToken(); $table->timestamps(); }); diff --git a/backend/database/migrations/2026_06_12_125654_create_patients_table.php b/backend/database/migrations/2026_06_12_125654_create_patients_table.php new file mode 100644 index 0000000..148bf7f --- /dev/null +++ b/backend/database/migrations/2026_06_12_125654_create_patients_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('user_id')->constrained('users', 'id')->cascadeOnDelete(); + $table->string('name'); + $table->string('phone', 15); + $table->string('bpjs_number', 50)->nullable(); + $table->string('birth_place', 100)->nullable(); + $table->date('birth_date')->nullable(); + $table->enum('gender', ['male', 'female'])->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('patients'); + } +}; diff --git a/backend/database/seeders/PatientSeeder.php b/backend/database/seeders/PatientSeeder.php new file mode 100644 index 0000000..2ae1ea6 --- /dev/null +++ b/backend/database/seeders/PatientSeeder.php @@ -0,0 +1,17 @@ + - - - - - - + + + diff --git a/backend/resources/css/app.css b/backend/resources/css/app.css deleted file mode 100644 index 3e6abea..0000000 --- a/backend/resources/css/app.css +++ /dev/null @@ -1,11 +0,0 @@ -@import 'tailwindcss'; - -@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; -@source '../../storage/framework/views/*.php'; -@source '../**/*.blade.php'; -@source '../**/*.js'; - -@theme { - --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', - 'Segoe UI Symbol', 'Noto Color Emoji'; -} diff --git a/backend/resources/js/app.js b/backend/resources/js/app.js deleted file mode 100644 index e59d6a0..0000000 --- a/backend/resources/js/app.js +++ /dev/null @@ -1 +0,0 @@ -import './bootstrap'; diff --git a/backend/resources/js/bootstrap.js b/backend/resources/js/bootstrap.js deleted file mode 100644 index 5f1390b..0000000 --- a/backend/resources/js/bootstrap.js +++ /dev/null @@ -1,4 +0,0 @@ -import axios from 'axios'; -window.axios = axios; - -window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; diff --git a/backend/resources/views/.gitkeep b/backend/resources/views/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/resources/views/.gitkeep @@ -0,0 +1 @@ + diff --git a/backend/resources/views/welcome.blade.php b/backend/resources/views/welcome.blade.php deleted file mode 100644 index b7355d7..0000000 --- a/backend/resources/views/welcome.blade.php +++ /dev/null @@ -1,277 +0,0 @@ - - - - - - - {{ config('app.name', 'Laravel') }} - - - - - - - @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))) - @vite(['resources/css/app.css', 'resources/js/app.js']) - @else - - @endif - - -
- @if (Route::has('login')) - - @endif -
-
-
-
-

Let's get started

-

Laravel has an incredibly rich ecosystem.
We suggest starting with the following.

- - -
-
- {{-- Laravel Logo --}} - - - - - - - - - - - {{-- Light Mode 12 SVG --}} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{-- Dark Mode 12 SVG --}} - -
-
-
-
- - @if (Route::has('login')) - - @endif - - diff --git a/backend/routes/api.php b/backend/routes/api.php index ccc387f..c8ff9bd 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -3,6 +3,10 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; -Route::get('/user', function (Request $request) { +Route::middleware(['auth:sanctum'])->get('/user', function (Request $request) { return $request->user(); -})->middleware('auth:sanctum'); +}); + +Route::prefix('auth')->group(function () { + require __DIR__.'/auth.php'; +}); diff --git a/backend/routes/auth.php b/backend/routes/auth.php new file mode 100644 index 0000000..27d13f5 --- /dev/null +++ b/backend/routes/auth.php @@ -0,0 +1,45 @@ +middleware('guest') + ->name('register'); + +Route::post('/login', [AuthenticatedSessionController::class, 'store']) + ->middleware('guest') + ->name('login'); + +// Protected +Route::middleware('auth:sanctum')->group(function () { + Route::post('/logout', [AuthenticatedSessionController::class, 'destroy']) + ->name('logout'); + + Route::get('/me', [AuthController::class, 'me']) + ->name('me'); +}); + +Route::post('/forgot-password', [PasswordResetLinkController::class, 'store']) + ->middleware('guest') + ->name('password.email'); + +Route::post('/reset-password', [NewPasswordController::class, 'store']) + ->middleware('guest') + ->name('password.store'); + +Route::get('/verify-email/{id}/{hash}', VerifyEmailController::class) + ->middleware(['auth', 'signed', 'throttle:6,1']) + ->name('verification.verify'); + +Route::post('/email/verification-notification', [EmailVerificationNotificationController::class, 'store']) + ->middleware(['auth', 'throttle:6,1']) + ->name('verification.send'); diff --git a/backend/routes/web.php b/backend/routes/web.php index 86a06c5..cdd4027 100644 --- a/backend/routes/web.php +++ b/backend/routes/web.php @@ -3,5 +3,7 @@ use Illuminate\Support\Facades\Route; Route::get('/', function () { - return view('welcome'); + return ['Laravel' => app()->version()]; }); + +require __DIR__.'/auth.php'; diff --git a/backend/test.http b/backend/test.http new file mode 100644 index 0000000..ae3807e --- /dev/null +++ b/backend/test.http @@ -0,0 +1,44 @@ +@baseUrl = http://localhost:8000 + +### User Data +@name = Handika Testing +@email = handika.testing@example.com +@password = Password123! +@phone = 081234567890 + +### Sanctum +@xsrfToken = eyJpdiI6IkV3S24vTEVqK3VLMWJyQ1hrV3hkOEE9PSIsInZhbHVlIjoiZFB3dGQ3azNUakZqWUxIZUEwdldUTjZIMFRJVW9LQUFwRlIxR2JxaWNDWHdtRzNwL1dRN1UyZlVBYW9JMGdtbHV5dVZHa0w0UGtRdUFaZDE0cTFQZEVDK2pyeGNtSWJRRUIzSk1lMndGdGtPNkpGY1M5ZWcvc3hDWVc2a2RKdUkiLCJtYWMiOiI4ZTkxZGYwNjNjOTg4MWIzZWJhYzk4N2ZiNzk3ZTE1MmM1OTQzYTMyMTVlZGE3M2Y0YzYwOThiNzM3N2Q2NWE4IiwidGFnIjoiIn0%3D +@sessionCookie = eyJpdiI6IlVLTkxwTVNqbFk3UGhXNjFTNk5vd3c9PSIsInZhbHVlIjoibi9qWGhPNkRkalpLZkNsS01tWGlnMFpOenhPQktES1RZQ01hdThUT3BRM0cxdDRNek96amRQZk5CbWtYamZ6YWEyTXVpQ3huWEpESTl6UVlGUDErSXJMeE5KM0t0bUdMdm1XMmMrcHlkaklHdTJnSXozNzlKNndrckVSdTgwNmciLCJtYWMiOiIxZTU3ODljOWFjMjIyYWZlOGRkMGZlMjNiY2I0ODg0YjNiYmFjZDkwY2EyOTc1MzI1ZmYxZDU3NmZmMWU4YzIzIiwidGFnIjoiIn0%3D + + +GET {{baseUrl}}/sanctum/csrf-cookie +Origin: http://localhost:3000 + +### Register User +POST {{baseUrl}}/api/auth/register +Content-Type: application/json +Origin: http://localhost:3000 +X-XSRF-TOKEN: {{xsrfToken}} +Cookie: XSRF-TOKEN={{xsrfToken}}; laravel-session={{sessionCookie}} + +{ + "name": "{{name}}", + "email": "{{email}}", + "password": "{{password}}", + "password_confirmation": "{{password}}", + "phone": "{{phone}}" +} + + +### Login +POST {{baseUrl}}/api/auth/login +Content-Type: application/json +Accept: application/json +Origin: http://localhost:3000 +X-XSRF-TOKEN: {{xsrfToken}} +Cookie: XSRF-TOKEN={{xsrfToken}}; laravel-session={{sessionCookie}} + +{ + "email": "{{email}}", + "password": "{{password}}" +} diff --git a/backend/tests/Feature/Api/Auth/LoginTest.php b/backend/tests/Feature/Api/Auth/LoginTest.php new file mode 100644 index 0000000..ec97d54 --- /dev/null +++ b/backend/tests/Feature/Api/Auth/LoginTest.php @@ -0,0 +1,185 @@ +create(array_merge([ + 'email' => 'handika@example.com', + 'password' => Hash::make('Password123!'), + 'role' => 'patient', + 'status' => 'active', + ], $overrides)); + + Patient::factory()->create([ + 'user_id' => $user->id, + 'name' => 'Handika Testing', + 'phone' => '081234567890', + ]); + + return $user; + } + + public function test_user_can_login_with_valid_credentials(): void + { + $this->createPatientUser(); + + $response = $this->postJson($this->endpoint, [ + 'email' => 'handika@example.com', + 'password' => 'Password123!', + ]); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'message', + 'data' => [ + 'user' => [ + 'id', + 'email', + 'role', + 'status', + 'profile' => [ + 'patient_id', + 'name', + 'phone', + ], + ], + ], + ]) + ->assertJsonPath('data.user.email', 'handika@example.com') + ->assertJsonPath('data.user.role', 'patient'); + } + + public function test_login_response_does_not_contain_bearer_token(): void + { + $this->createPatientUser(); + + $response = $this->postJson($this->endpoint, [ + 'email' => 'handika@example.com', + 'password' => 'Password123!', + ]); + + $response->assertJsonMissingPath('data.token'); + $response->assertJsonMissingPath('data.token_type'); + } + + public function test_login_authenticates_user_in_session(): void + { + $user = $this->createPatientUser(); + + $this->postJson($this->endpoint, [ + 'email' => 'handika@example.com', + 'password' => 'Password123!', + ]); + + $this->assertAuthenticatedAs($user); + } + + public function test_login_fails_with_wrong_password(): void + { + $this->createPatientUser(); + + $response = $this->postJson($this->endpoint, [ + 'email' => 'handika@example.com', + 'password' => 'WrongPassword!', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['email']); + + $this->assertGuest(); + } + + public function test_login_fails_when_email_not_registered(): void + { + $response = $this->postJson($this->endpoint, [ + 'email' => 'notfound@example.com', + 'password' => 'Password123!', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['email']); + + $this->assertGuest(); + } + + public function test_login_fails_when_email_missing(): void + { + $response = $this->postJson($this->endpoint, [ + 'password' => 'Password123!', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['email']); + } + + public function test_login_fails_when_password_missing(): void + { + $this->createPatientUser(); + + $response = $this->postJson($this->endpoint, [ + 'email' => 'handika@example.com', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['password']); + } + + public function test_login_fails_when_email_format_invalid(): void + { + $response = $this->postJson($this->endpoint, [ + 'email' => 'not-an-email', + 'password' => 'Password123!', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['email']); + } + + public function test_login_is_rate_limited_after_too_many_attempts(): void + { + $this->createPatientUser(); + + for ($i = 0; $i < 5; $i++) { + $this->postJson($this->endpoint, [ + 'email' => 'handika@example.com', + 'password' => 'WrongPassword!', + ]); + } + + $response = $this->postJson($this->endpoint, [ + 'email' => 'handika@example.com', + 'password' => 'Password123!', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['email']); + + $this->assertGuest(); + } + + public function test_already_authenticated_user_cannot_hit_login_due_to_guest_middleware(): void + { + $user = $this->createPatientUser(); + + $response = $this->actingAs($user)->postJson($this->endpoint, [ + 'email' => 'handika@example.com', + 'password' => 'Password123!', + ]); + + $response->assertStatus(409); + } +} diff --git a/backend/tests/Feature/Api/Auth/LogoutTest.php b/backend/tests/Feature/Api/Auth/LogoutTest.php new file mode 100644 index 0000000..44eb6d6 --- /dev/null +++ b/backend/tests/Feature/Api/Auth/LogoutTest.php @@ -0,0 +1,76 @@ +create([ + 'role' => 'patient', + 'status' => 'active', + ]); + + Patient::factory()->create([ + 'user_id' => $user->id, + ]); + + return $user; + } + + public function test_authenticated_user_can_logout(): void + { + $user = $this->createPatientUser(); + + $response = $this->actingAs($user, 'web')->postJson($this->endpoint); + + $response->assertStatus(200) + ->assertJson([ + 'message' => 'Logout Successful.', + ]); + } + + public function test_logout_invalidates_session(): void + { + $user = $this->createPatientUser(); + + $this->actingAs($user, 'web')->postJson($this->endpoint); + + $this->app['auth']->forgetGuards(); + $this->flushSession(); + + $response = $this->postJson($this->endpoint); + $response->assertStatus(401); + } + + public function test_guest_cannot_logout(): void + { + $response = $this->postJson($this->endpoint); + + $response->assertStatus(401); + } + + public function test_logout_followed_by_protected_route_returns_unauthenticated(): void + { + $response = $this->getJson('/api/auth/me'); + $response->assertStatus(401); + } + + public function test_logout_clears_session_data(): void + { + $user = $this->createPatientUser(); + + $this->actingAs($user, 'web')->postJson($this->endpoint); + $this->assertGuest('web'); + } +} diff --git a/backend/tests/Feature/Api/Auth/MeTest.php b/backend/tests/Feature/Api/Auth/MeTest.php new file mode 100644 index 0000000..a77d312 --- /dev/null +++ b/backend/tests/Feature/Api/Auth/MeTest.php @@ -0,0 +1,102 @@ +create([ + 'role' => 'patient', + 'status' => 'active', + ]); + + Patient::factory()->create(array_merge([ + 'user_id' => $user->id, + 'name' => 'Handika Testing', + 'phone' => '081234567890', + ], $patientOverrides)); + + return $user; + } + + public function test_authenticated_user_can_get_own_data(): void + { + $user = $this->createPatientUser(); + + $response = $this->actingAs($user)->getJson($this->endpoint); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'data' => [ + 'id', + 'email', + 'role', + 'status', + 'profile' => [ + 'patient_id', + 'name', + 'phone', + 'bpjs_number', + 'birth_place', + 'birth_date', + 'gender', + 'user_id', + ], + ], + ]) + ->assertJsonPath('data.id', $user->id) + ->assertJsonPath('data.email', $user->email) + ->assertJsonPath('data.role', 'patient') + ->assertJsonPath('data.profile.name', 'Handika Testing'); + } + + public function test_guest_cannot_access_me_endpoint(): void + { + $response = $this->getJson($this->endpoint); + + $response->assertStatus(401); + } + + public function test_me_returns_correct_user_when_multiple_users_exist(): void + { + $otherUser = $this->createPatientUser(); + $targetUser = $this->createPatientUser(); + + $response = $this->actingAs($targetUser)->getJson($this->endpoint); + + $response->assertStatus(200) + ->assertJsonPath('data.id', $targetUser->id) + ->assertJsonPath('data.email', $targetUser->email); + + $this->assertNotEquals($otherUser->id, $response->json('data.id')); + } + + public function test_me_profile_reflects_nullable_fields_as_null_when_not_filled(): void + { + $user = $this->createPatientUser([ + 'bpjs_number' => null, + 'birth_place' => null, + 'birth_date' => null, + 'gender' => null, + ]); + + $response = $this->actingAs($user)->getJson($this->endpoint); + + $response->assertStatus(200) + ->assertJsonPath('data.profile.bpjs_number', null) + ->assertJsonPath('data.profile.birth_place', null) + ->assertJsonPath('data.profile.birth_date', null) + ->assertJsonPath('data.profile.gender', null); + } +} diff --git a/backend/tests/Feature/Api/Auth/RegisterTest.php b/backend/tests/Feature/Api/Auth/RegisterTest.php new file mode 100644 index 0000000..36fc7e8 --- /dev/null +++ b/backend/tests/Feature/Api/Auth/RegisterTest.php @@ -0,0 +1,197 @@ + 'Handika Testing', + 'email' => 'handika@example.com', + 'password' => 'Password123!', + 'password_confirmation' => 'Password123!', + 'phone' => '081234567890', + ]; + + $response = $this->postJson($this->endpoint, $payload); + + $response->assertStatus(201) + ->assertJsonStructure([ + 'message', + 'data' => [ + 'id', + 'email', + 'role', + 'status', + 'profile' => [ + 'patient_id', + 'name', + 'phone', + 'bpjs_number', + 'birth_place', + 'birth_date', + 'gender', + 'user_id', + ], + ], + ]) + ->assertJsonPath('data.email', 'handika@example.com') + ->assertJsonPath('data.role', 'patient') + ->assertJsonPath('data.status', 'active') + ->assertJsonPath('data.profile.name', 'Handika Testing') + ->assertJsonPath('data.profile.phone', '081234567890'); + + $this->assertDatabaseHas('users', [ + 'email' => 'handika@example.com', + 'role' => 'patient', + ]); + + $user = User::where('email', 'handika@example.com')->first(); + + $this->assertDatabaseHas('patients', [ + 'user_id' => $user->id, + 'name' => 'Handika Testing', + 'phone' => '081234567890', + ]); + } + + public function test_register_hashes_password(): void + { + $payload = [ + 'name' => 'Handika Testing', + 'email' => 'handika@example.com', + 'password' => 'Password123!', + 'password_confirmation' => 'Password123!', + 'phone' => '081234567890', + ]; + + $this->postJson($this->endpoint, $payload); + + $user = User::where('email', 'handika@example.com')->first(); + + $this->assertNotEquals('Password123!', $user->password); + $this->assertTrue(\Illuminate\Support\Facades\Hash::check('Password123!', $user->password)); + } + + public function test_register_fails_when_email_already_taken(): void + { + User::factory()->create(['email' => 'handika@example.com']); + + $payload = [ + 'name' => 'Handika Testing', + 'email' => 'handika@example.com', + 'password' => 'Password123!', + 'password_confirmation' => 'Password123!', + 'phone' => '081234567890', + ]; + + $response = $this->postJson($this->endpoint, $payload); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['email']); + } + + public function test_register_fails_when_password_confirmation_does_not_match(): void + { + $payload = [ + 'name' => 'Handika Testing', + 'email' => 'handika@example.com', + 'password' => 'Password123!', + 'password_confirmation' => 'WrongPassword!', + 'phone' => '081234567890', + ]; + + $response = $this->postJson($this->endpoint, $payload); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['password']); + } + + public function test_register_fails_when_password_too_short(): void + { + $payload = [ + 'name' => 'Handika Testing', + 'email' => 'handika@example.com', + 'password' => 'short', + 'password_confirmation' => 'short', + 'phone' => '081234567890', + ]; + + $response = $this->postJson($this->endpoint, $payload); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['password']); + } + + #[DataProvider('missingFieldProvider')] + public function test_register_fails_when_required_field_missing(string $field): void + { + $payload = [ + 'name' => 'Handika Testing', + 'email' => 'handika@example.com', + 'password' => 'Password123!', + 'password_confirmation' => 'Password123!', + 'phone' => '081234567890', + ]; + + unset($payload[$field]); + + $response = $this->postJson($this->endpoint, $payload); + + $response->assertStatus(422) + ->assertJsonValidationErrors([$field]); + } + + public static function missingFieldProvider(): array + { + return [ + 'missing name' => ['name'], + 'missing email' => ['email'], + 'missing password' => ['password'], + 'missing phone' => ['phone'], + ]; + } + + public function test_register_fails_when_email_format_invalid(): void + { + $payload = [ + 'name' => 'Handika Testing', + 'email' => 'not-an-email', + 'password' => 'Password123!', + 'password_confirmation' => 'Password123!', + 'phone' => '081234567890', + ]; + + $response = $this->postJson($this->endpoint, $payload); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['email']); + } + + public function test_register_does_not_create_patient_record_if_validation_fails(): void + { + $payload = [ + 'name' => 'Handika Testing', + 'email' => 'not-an-email', + 'password' => 'Password123!', + 'password_confirmation' => 'Password123!', + 'phone' => '081234567890', + ]; + + $this->postJson($this->endpoint, $payload); + + $this->assertDatabaseCount('patients', 0); + $this->assertDatabaseCount('users', 0); + } +} diff --git a/backend/tests/Feature/ExampleTest.php b/backend/tests/Feature/ExampleTest.php deleted file mode 100644 index 8364a84..0000000 --- a/backend/tests/Feature/ExampleTest.php +++ /dev/null @@ -1,19 +0,0 @@ -get('/'); - - $response->assertStatus(200); - } -} diff --git a/backend/tests/Unit/ExampleTest.php b/backend/tests/Unit/SampleTest.php similarity index 52% rename from backend/tests/Unit/ExampleTest.php rename to backend/tests/Unit/SampleTest.php index 5773b0c..9e2b25e 100644 --- a/backend/tests/Unit/ExampleTest.php +++ b/backend/tests/Unit/SampleTest.php @@ -4,12 +4,12 @@ use PHPUnit\Framework\TestCase; -class ExampleTest extends TestCase +class SampleTest extends TestCase { /** - * A basic test example. + * A basic unit test example. */ - public function test_that_true_is_true(): void + public function test_example(): void { $this->assertTrue(true); } diff --git a/backend/vite.config.js b/backend/vite.config.js deleted file mode 100644 index f35b4e7..0000000 --- a/backend/vite.config.js +++ /dev/null @@ -1,18 +0,0 @@ -import { defineConfig } from 'vite'; -import laravel from 'laravel-vite-plugin'; -import tailwindcss from '@tailwindcss/vite'; - -export default defineConfig({ - plugins: [ - laravel({ - input: ['resources/css/app.css', 'resources/js/app.js'], - refresh: true, - }), - tailwindcss(), - ], - server: { - watch: { - ignored: ['**/storage/framework/views/**'], - }, - }, -}); diff --git a/docs/openapi_v2/openapi.yaml b/docs/openapi_v2/openapi.yaml new file mode 100644 index 0000000..59384d4 --- /dev/null +++ b/docs/openapi_v2/openapi.yaml @@ -0,0 +1,204 @@ +openapi: 3.0.3 + +# ============================================================================= +# Hospital Queue Management System — API Contract v2.1 (UI-First) +# ============================================================================= +# Stack : Laravel 11 (API) + React SPA +# Auth : Laravel Sanctum — plain-text Bearer Token +# Approach : UI-First — disesuaikan dengan Figma design +# ERD : v2 (schedule_templates + schedule_instances, field baru) +# +# PERUBAHAN UTAMA dari v2.0: +# --------------------------- +# 1. /schedules SPLIT menjadi: +# - /doctors/{id}/schedule-templates (recurring pattern) +# - /schedule-instances (tanggal aktual, auto-generated) +# 2. Patients: tambah bpjs_number, birth_place, birth_date, gender +# 3. Doctors : tambah sip_number, birth_date, gender +# 4. Nurses : tambah sip_number, birth_date, gender +# 5. Reservations: tambah complaint (keluhan pasien) +# 6. POST /reservations: FK ke instance_id (bukan schedule_id) +# 7. Endpoint baru: GET /patients/me (profil pasien sendiri) +# 8. Endpoint baru: PATCH /patients/me (update profil pasien) +# +# CARA PAKAI TOKEN SANCTUM +# ------------------------- +# 1. POST /auth/login → dapat data.token +# 2. Simpan token di React state/memory (BUKAN localStorage) +# 3. Sertakan di setiap request: Authorization: Bearer {token} +# 4. POST /auth/logout → token di-revoke +# +# ROLES +# ----- +# patient | doctor | nurse | admin +# +# QUEUE STATE MACHINE +# ------------------- +# booked → checked-in → waiting → called → in-progress → done +# ↘ no-show +# ↘ cancelled +# ============================================================================= + +info: + title: Hospital Queue Management System API + version: 2.1.0 + description: | + REST API untuk sistem manajemen antrian rumah sakit QueueNova Health. + Disesuaikan dengan UI Figma (UI-First approach). + + ### Perubahan dari v2.0 + - Schedule sekarang menggunakan sistem **template + instance** (Hybrid) + - Field baru di Patient, Doctor, Nurse (SIP, gender, tanggal lahir) + - Field `complaint` di Reservation (keluhan pasien) + - Endpoint profil pasien (`/patients/me`) + +servers: + - url: http://localhost:8000/api + description: Local Development Server + - url: https://api.queuenova.example.com/api + description: Production Server + +components: + securitySchemes: + sanctumToken: + type: http + scheme: bearer + bearerFormat: SanctumToken + description: | + Sanctum plain-text token dari POST /auth/login. + Header: `Authorization: Bearer {token}` + + schemas: + # Auth + RegisterRequest: { $ref: './schemas/auth.yaml#/RegisterRequest' } + LoginRequest: { $ref: './schemas/auth.yaml#/LoginRequest' } + LoginResponse: { $ref: './schemas/auth.yaml#/LoginResponse' } + UserResource: { $ref: './schemas/auth.yaml#/UserResource' } + + # Patient profile + PatientProfileResource: { $ref: './schemas/patient.yaml#/PatientProfileResource' } + PatientProfileRequest: { $ref: './schemas/patient.yaml#/PatientProfileRequest' } + + # Department + DepartmentResource: { $ref: './schemas/department.yaml#/DepartmentResource' } + DepartmentRequest: { $ref: './schemas/department.yaml#/DepartmentRequest' } + + # Doctor + DoctorResource: { $ref: './schemas/doctor.yaml#/DoctorResource' } + DoctorRequest: { $ref: './schemas/doctor.yaml#/DoctorRequest' } + DoctorUpdateRequest: { $ref: './schemas/doctor.yaml#/DoctorUpdateRequest' } + + # Nurse + NurseResource: { $ref: './schemas/nurse.yaml#/NurseResource' } + NurseRequest: { $ref: './schemas/nurse.yaml#/NurseRequest' } + NurseUpdateRequest: { $ref: './schemas/nurse.yaml#/NurseUpdateRequest' } + + # Schedule Template (recurring) + ScheduleTemplateResource: { $ref: './schemas/schedule.yaml#/ScheduleTemplateResource' } + ScheduleTemplateRequest: { $ref: './schemas/schedule.yaml#/ScheduleTemplateRequest' } + + # Schedule Instance (tanggal aktual) + ScheduleInstanceResource: { $ref: './schemas/schedule.yaml#/ScheduleInstanceResource' } + ScheduleInstanceRequest: { $ref: './schemas/schedule.yaml#/ScheduleInstanceRequest' } + + # Reservation + ReservationResource: { $ref: './schemas/reservation.yaml#/ReservationResource' } + ReservationRequest: { $ref: './schemas/reservation.yaml#/ReservationRequest' } + + # Queue + QueueResource: { $ref: './schemas/queue.yaml#/QueueResource' } + QueueStatusRequest: { $ref: './schemas/queue.yaml#/QueueStatusRequest' } + QueueReportResource: { $ref: './schemas/queue.yaml#/QueueReportResource' } + + # Common + SuccessResponse: { $ref: './schemas/common.yaml#/SuccessResponse' } + ErrorResponse: { $ref: './schemas/common.yaml#/ErrorResponse' } + ValidationErrorResponse: { $ref: './schemas/common.yaml#/ValidationErrorResponse' } + PaginationMeta: { $ref: './schemas/common.yaml#/PaginationMeta' } + +tags: + - name: Authentication + description: "**EPIC 1** — Register, login, logout" + - name: Patient Profile + description: "**EPIC 1/3** — Profil & data diri pasien" + - name: Departments + description: "**EPIC 2** — CRUD departemen (Admin)" + - name: Doctors + description: "**EPIC 2** — CRUD dokter (Admin)" + - name: Nurses + description: "**EPIC 2** — CRUD suster (Admin)" + - name: Schedule Templates + description: "**EPIC 2** — Template jadwal berulang per dokter (Admin)" + - name: Schedule Instances + description: "**EPIC 2/3** — Jadwal aktual per tanggal (auto-generated + override)" + - name: Booking + description: "**EPIC 3** — Reservasi pasien" + - name: Queue + description: "**EPIC 4** — Antrian (Nurse update, Doctor & Nurse view)" + - name: Reports + description: "**EPIC 4** — Laporan antrian (Admin)" + +paths: + # ---- EPIC 1: Auth ---- + /auth/register: + $ref: './paths/auth.yaml#/~1auth~1register' + /auth/login: + $ref: './paths/auth.yaml#/~1auth~1login' + /auth/logout: + $ref: './paths/auth.yaml#/~1auth~1logout' + /auth/me: + $ref: './paths/auth.yaml#/~1auth~1me' + + # ---- Patient Profile ---- + /patients/me: + $ref: './paths/patients.yaml#/~1patients~1me' + + # ---- EPIC 2: Departments ---- + /departments: + $ref: './paths/departments.yaml#/~1departments' + /departments/{id}: + $ref: './paths/departments.yaml#/~1departments~1{id}' + + # ---- EPIC 2: Doctors ---- + /doctors: + $ref: './paths/doctors.yaml#/~1doctors' + /doctors/{id}: + $ref: './paths/doctors.yaml#/~1doctors~1{id}' + + # ---- EPIC 2: Nurses ---- + /nurses: + $ref: './paths/nurses.yaml#/~1nurses' + /nurses/{id}: + $ref: './paths/nurses.yaml#/~1nurses~1{id}' + + # ---- EPIC 2: Schedule Templates (recurring) ---- + /doctors/{id}/schedule-templates: + $ref: './paths/schedule_templates.yaml#/~1doctors~1{id}~1schedule-templates' + /doctors/{id}/schedule-templates/{templateId}: + $ref: './paths/schedule_templates.yaml#/~1doctors~1{id}~1schedule-templates~1{templateId}' + + # ---- EPIC 2/3: Schedule Instances (tanggal aktual) ---- + # Admin: override/cancel per instance + # Patient: GET untuk melihat jadwal tersedia sebelum booking + /schedule-instances: + $ref: './paths/schedule_instances.yaml#/~1schedule-instances' + /schedule-instances/{instanceId}: + $ref: './paths/schedule_instances.yaml#/~1schedule-instances~1{instanceId}' + + # ---- EPIC 3: Reservations ---- + /reservations: + $ref: './paths/reservations.yaml#/~1reservations' + /reservations/{id}: + $ref: './paths/reservations.yaml#/~1reservations~1{id}' + /reservations/{id}/cancel: + $ref: './paths/reservations.yaml#/~1reservations~1{id}~1cancel' + + # ---- EPIC 4: Queue ---- + /queues: + $ref: './paths/queues.yaml#/~1queues' + /queues/{id}/status: + $ref: './paths/queues.yaml#/~1queues~1{id}~1status' + + # ---- EPIC 4: Reports ---- + /reports/queues: + $ref: './paths/reports.yaml#/~1reports~1queues' diff --git a/docs/openapi_v2/paths/auth.yaml b/docs/openapi_v2/paths/auth.yaml new file mode 100644 index 0000000..2064e43 --- /dev/null +++ b/docs/openapi_v2/paths/auth.yaml @@ -0,0 +1,174 @@ +# ============================================================================= +# Paths — EPIC 1: Authentication +# Dari Figma: app name = "QueueNova Health", brand = "QueueNova" +# Login screen: email + password + "Remember me" + "Forgot Password?" +# Register screen: nama, email, password, konfirmasi password +# ============================================================================= + +/auth/register: + post: + tags: [Authentication] + summary: Register pasien baru + description: | + Registrasi khusus **Pasien**. Doctor/Nurse/Admin didaftarkan oleh Admin. + Dari Figma register screen: field Name, Email, Password, Password Confirmation. + Data profil lengkap (BPJS, tempat lahir, dll) dilengkapi via `PATCH /patients/me`. + operationId: registerPatient + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '../schemas/auth.yaml#/RegisterRequest' + example: + name: "Ucok Sitorus" + email: "ucok@example.com" + password: "password123" + password_confirmation: "password123" + phone: "081234567890" + responses: + '201': + description: Registrasi berhasil + content: + application/json: + example: + message: "Registrasi berhasil. Silakan login." + data: + user_id: 10 + email: "ucok@example.com" + role: "patient" + status: "active" + profile: + patient_id: 5 + name: "Ucok Sitorus" + phone: "081234567890" + bpjs_number: null + birth_place: null + birth_date: null + gender: null + '422': + description: Validasi gagal + content: + application/json: + schema: + $ref: '../schemas/common.yaml#/ValidationErrorResponse' + example: + message: "The given data was invalid." + errors: + email: ["The email has already been taken."] + +/auth/login: + post: + tags: [Authentication] + summary: Login — semua role (Patient, Doctor, Nurse, Admin) + description: | + Dari Figma login screen (admin & patient): + - Field: Email (`example@gmail.com`), Password (`@#*%`) + - Checkbox "Remember me" — implementasi di frontend (tidak di API) + - Link "Forgot Password?" — belum dalam scope + + Mengembalikan Sanctum plain-text Bearer Token. + operationId: loginUser + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '../schemas/auth.yaml#/LoginRequest' + example: + email: "ucok@example.com" + password: "password123" + responses: + '200': + description: Login berhasil — token dikembalikan + content: + application/json: + schema: + $ref: '../schemas/auth.yaml#/LoginResponse' + example: + message: "Login berhasil." + data: + token: "3|aB1cUcokD2eF3gH4iJ5kL6" + token_type: "Bearer" + user: + user_id: 10 + email: "ucok@example.com" + role: "patient" + status: "active" + profile: + patient_id: 5 + name: "Ucok Sitorus" + phone: "081234567890" + bpjs_number: "0001234567890" + birth_place: "Medan" + birth_date: "1995-06-15" + gender: "laki-laki" + '401': + description: Email atau password salah + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + example: { message: "Email atau password salah." } + '422': + description: Validasi gagal + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ValidationErrorResponse' } + +/auth/logout: + post: + tags: [Authentication] + summary: Logout — revoke token aktif + operationId: logoutUser + security: + - sanctumToken: [] + responses: + '200': + description: Logout berhasil + content: + application/json: + example: { message: "Logout berhasil." } + '401': + description: Token tidak valid + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + example: { message: "Unauthenticated." } + +/auth/me: + get: + tags: [Authentication] + summary: Ambil data user yang sedang login + description: | + Berguna untuk inisialisasi React SPA setelah refresh. + Profile bervariasi per role — lihat masing-masing Resource schema. + operationId: getAuthenticatedUser + security: + - sanctumToken: [] + responses: + '200': + description: Data user + content: + application/json: + example: + data: + user_id: 10 + email: "ucok@example.com" + role: "nurse" + status: "active" + profile: + nurse_id: 2 + name: "Ucok Sihombing" + sip_number: "112345678" + gender: "laki-laki" + birth_date: "1990-07-10" + department: + department_id: 1 + name: "Poli Umum" + '401': + description: Token tidak valid + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } diff --git a/docs/openapi_v2/paths/departments.yaml b/docs/openapi_v2/paths/departments.yaml new file mode 100644 index 0000000..d44904b --- /dev/null +++ b/docs/openapi_v2/paths/departments.yaml @@ -0,0 +1,184 @@ +# ============================================================================= +# Paths — EPIC 2: Departments +# Dari Figma admin-department-dashboard: list card dengan nama + aksi Delete/Edit/Info +# Dari Figma patient-view-department: card poli dengan nama, deskripsi, jumlah dokter +# ============================================================================= + +/departments: + get: + tags: [Departments] + summary: Lihat daftar departemen / poli + description: | + Semua role yang login dapat melihat daftar poli. + Pasien menggunakan ini sebagai halaman awal booking. + Dari Figma: menampilkan nama, deskripsi singkat, jumlah dokter tersedia. + operationId: getDepartments + security: + - sanctumToken: [] + parameters: + - name: search + in: query + required: false + description: "Dari Figma: 'Cari dokter atau poli...'" + schema: { type: string, example: "Poli Umum" } + responses: + '200': + description: Daftar departemen + content: + application/json: + example: + data: + - department_id: 1 + name: "Poli Umum" + description: "Pemeriksaan Kesehatan Umum" + doctors_count: 3 + - department_id: 2 + name: "Poli Mata" + description: "Pemeriksaan Penglihatan dan Kesehatan Mata" + doctors_count: 2 + - department_id: 3 + name: "Poli Kebidanan dan Kandungan" + description: "Pemeriksaan Kesehatan Kandungan" + doctors_count: 2 + '401': + description: Tidak terautentikasi + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + post: + tags: [Departments] + summary: Buat departemen baru + description: "**Admin only**. Dari Figma form: Department Name + Description." + operationId: createDepartment + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '../schemas/department.yaml#/DepartmentRequest' } + example: + name: "Poli Umum" + description: "Pusat layanan kesehatan primer untuk diagnosa awal, pengobatan umum, dan rujukan" + responses: + '201': + description: Departemen dibuat + content: + application/json: + example: + message: "Departemen berhasil dibuat." + data: + department_id: 4 + name: "Poli Umum" + description: "Pusat layanan kesehatan primer untuk diagnosa awal, pengobatan umum, dan rujukan" + doctors_count: 0 + '403': + description: Bukan Admin + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '422': + description: Validasi gagal + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ValidationErrorResponse' } + +/departments/{id}: + parameters: + - name: id + in: path + required: true + schema: { type: integer, example: 1 } + + get: + tags: [Departments] + summary: Detail departemen + list dokter + description: | + Dari Figma admin-department-detail: + - Info: Nama poli, deskripsi panjang + - "List Dokter Poli Umum": tabel No., Nama Dokter, No SIP, Jenis Kelamin, Aksi + operationId: getDepartmentById + security: + - sanctumToken: [] + responses: + '200': + description: Detail departemen + content: + application/json: + example: + data: + department_id: 1 + name: "Poli Umum" + description: "Pusat layanan kesehatan primer untuk diagnosa awal, pengobatan umum, dan rujukan" + doctors_count: 4 + doctors: + - doctor_id: 1 + name: "dr. Ucok Napitupulu" + sip_number: "12345" + gender: "laki-laki" + - doctor_id: 2 + name: "dr. Tirta Pengpeng" + sip_number: "321321" + gender: "laki-laki" + '404': + description: Tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + patch: + tags: [Departments] + summary: Update departemen + description: "**Admin only** — partial update. Dari Figma: field Department Name + Description." + operationId: updateDepartment + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '../schemas/department.yaml#/DepartmentRequest' } + example: + name: "Poli Umum & Gawat Darurat" + responses: + '200': + description: Departemen diperbarui + content: + application/json: + example: + message: "Departemen berhasil diperbarui." + data: + department_id: 1 + name: "Poli Umum & Gawat Darurat" + description: "Pusat layanan kesehatan primer untuk diagnosa awal, pengobatan umum, dan rujukan" + doctors_count: 4 + '403': + description: Bukan Admin + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '404': + description: Tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + delete: + tags: [Departments] + summary: Hapus departemen + description: "**Admin only**. Gagal jika masih ada dokter aktif." + operationId: deleteDepartment + security: + - sanctumToken: [] + responses: + '200': + description: Dihapus + content: + application/json: + example: { message: "Departemen berhasil dihapus." } + '409': + description: Masih ada dokter aktif + content: + application/json: + example: { message: "Departemen tidak dapat dihapus karena masih memiliki dokter aktif." } diff --git a/docs/openapi_v2/paths/doctors.yaml b/docs/openapi_v2/paths/doctors.yaml new file mode 100644 index 0000000..5e5f5af --- /dev/null +++ b/docs/openapi_v2/paths/doctors.yaml @@ -0,0 +1,211 @@ +# ============================================================================= +# Paths — EPIC 2: Doctors +# Dari Figma admin-doctor-dashboard: list nama dokter + Delete/Edit/Info +# Dari Figma admin-doctor-form: Doctor Name, No SIP, Jenis Kelamin, +# Tanggal Lahir, Spesialisasi, Department, Email, Password +# Dari Figma admin-doctor-detail: Info Dokter + tabel Schedule (Hari, Pukul, Aksi) +# ============================================================================= + +/doctors: + get: + tags: [Doctors] + summary: Lihat daftar dokter + operationId: getDoctors + security: + - sanctumToken: [] + parameters: + - name: department_id + in: query + required: false + schema: { type: integer, example: 1 } + - name: search + in: query + required: false + schema: { type: string, example: "dr. Ucok" } + responses: + '200': + description: Daftar dokter + content: + application/json: + example: + data: + - doctor_id: 1 + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam" + sip_number: "112345678" + gender: "laki-laki" + birth_date: "1980-03-20" + doctor_status: "active" + department: + department_id: 1 + name: "Poli Umum" + user_id: 5 + + post: + tags: [Doctors] + summary: Daftarkan dokter baru + description: | + **Admin only**. Dari Figma form doctor: + Doctor Name, No SIP, Jenis Kelamin, Tanggal Lahir, Spesialisasi, Department, Email, Password. + operationId: createDoctor + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '../schemas/doctor.yaml#/DoctorRequest' } + example: + name: "dr. Ucok Napitupulu, Sp.PD" + email: "ucok.dokter@hospital.com" + password: "password123" + specialization: "Spesialis Penyakit Dalam" + department_id: 1 + sip_number: "112345678" + birth_date: "1980-03-20" + gender: "laki-laki" + responses: + '201': + description: Dokter didaftarkan + content: + application/json: + example: + message: "Dokter berhasil didaftarkan." + data: + doctor_id: 1 + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam" + sip_number: "112345678" + gender: "laki-laki" + birth_date: "1980-03-20" + doctor_status: "active" + department: + department_id: 1 + name: "Poli Umum" + user_id: 5 + '403': + description: Bukan Admin + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '422': + description: Validasi gagal + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ValidationErrorResponse' } + +/doctors/{id}: + parameters: + - name: id + in: path + required: true + schema: { type: integer, example: 1 } + + get: + tags: [Doctors] + summary: Detail dokter + daftar template jadwal + description: | + Dari Figma admin-doctor-detail: + - Section "Informasi Dokter": Nama, No SIP, Jenis Kelamin, Spesialisasi, Email, Department + - Section "Schedule": tabel Hari + Jam + Aksi (Edit/Delete) + tombol "Add Schedule" + operationId: getDoctorById + security: + - sanctumToken: [] + responses: + '200': + description: Detail dokter + content: + application/json: + example: + data: + doctor_id: 1 + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam" + sip_number: "112345678" + gender: "laki-laki" + birth_date: "1980-03-20" + doctor_status: "active" + department: + department_id: 1 + name: "Poli Umum" + user_id: 5 + # Template jadwal ditampilkan di halaman detail dokter Figma + schedule_templates: + - template_id: 1 + day_of_week: "senin" + start_time: "09:00" + end_time: "15:00" + max_patients: 10 + is_active: true + - template_id: 2 + day_of_week: "selasa" + start_time: "09:00" + end_time: "15:00" + max_patients: 10 + is_active: true + '404': + description: Tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + patch: + tags: [Doctors] + summary: Update data dokter + description: "**Admin only** — partial update. Email/password tidak diubah di sini." + operationId: updateDoctor + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '../schemas/doctor.yaml#/DoctorUpdateRequest' } + example: + specialization: "Spesialis Penyakit Dalam & Konsultan Ginjal" + sip_number: "112345679" + responses: + '200': + description: Data diperbarui + content: + application/json: + example: + message: "Data dokter berhasil diperbarui." + data: + doctor_id: 1 + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam & Konsultan Ginjal" + sip_number: "112345679" + doctor_status: "active" + department: + department_id: 1 + name: "Poli Umum" + '403': + description: Bukan Admin + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '404': + description: Tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + delete: + tags: [Doctors] + summary: Hapus dokter + description: "**Admin only**. Hapus users + doctors. Gagal jika ada reservasi aktif." + operationId: deleteDoctor + security: + - sanctumToken: [] + responses: + '200': + description: Dihapus + content: + application/json: + example: { message: "Dokter berhasil dihapus." } + '409': + description: Masih ada reservasi aktif + content: + application/json: + example: { message: "Dokter tidak dapat dihapus karena masih memiliki reservasi aktif." } diff --git a/docs/openapi_v2/paths/nurses.yaml b/docs/openapi_v2/paths/nurses.yaml new file mode 100644 index 0000000..550acd8 --- /dev/null +++ b/docs/openapi_v2/paths/nurses.yaml @@ -0,0 +1,197 @@ +# ============================================================================= +# Paths — EPIC 2: Nurses +# Dari Figma admin-nurse-form: Nurse Name, No SIP, Jenis Kelamin, +# Tanggal Lahir, Email, Password +# Dari Figma admin-nurse-detail: Informasi Nurse — Nama, No SIP, Jenis Kelamin, Email +# ============================================================================= + +/nurses: + get: + tags: [Nurses] + summary: Lihat daftar suster + description: "**Admin only**" + operationId: getNurses + security: + - sanctumToken: [] + parameters: + - name: search + in: query + required: false + schema: { type: string, example: "Ucok" } + - name: department_id + in: query + required: false + schema: { type: integer, example: 1 } + responses: + '200': + description: Daftar suster + content: + application/json: + example: + data: + - nurse_id: 1 + name: "Ucok Sihombing" + sip_number: "112345678" + gender: "laki-laki" + birth_date: "1990-07-10" + department: + department_id: 1 + name: "Poli Umum" + user_id: 8 + - nurse_id: 2 + name: "Bunda Rahma" + sip_number: "123456789" + gender: "perempuan" + birth_date: "1988-03-15" + department: null + user_id: 9 + '403': + description: Bukan Admin + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + post: + tags: [Nurses] + summary: Daftarkan suster baru + description: | + **Admin only**. Dari Figma form nurse: + Nurse Name, No SIP, Jenis Kelamin, Tanggal Lahir, Email, Password. + operationId: createNurse + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '../schemas/nurse.yaml#/NurseRequest' } + example: + name: "Ucok Sihombing" + email: "ucok.nurse@hospital.com" + password: "password123" + department_id: 1 + sip_number: "112345678" + birth_date: "1990-07-10" + gender: "laki-laki" + responses: + '201': + description: Suster didaftarkan + content: + application/json: + example: + message: "Suster berhasil didaftarkan." + data: + nurse_id: 1 + name: "Ucok Sihombing" + sip_number: "112345678" + gender: "laki-laki" + birth_date: "1990-07-10" + department: + department_id: 1 + name: "Poli Umum" + user_id: 8 + '403': + description: Bukan Admin + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '422': + description: Validasi gagal + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ValidationErrorResponse' } + +/nurses/{id}: + parameters: + - name: id + in: path + required: true + schema: { type: integer, example: 1 } + + get: + tags: [Nurses] + summary: Detail suster + description: | + **Admin only**. Dari Figma admin-nurse-detail: + Informasi Nurse: No SIP, Jenis Kelamin, Email, Nama Nurse. + operationId: getNurseById + security: + - sanctumToken: [] + responses: + '200': + description: Detail suster + content: + application/json: + example: + data: + nurse_id: 2 + name: "Bunda Rahma" + sip_number: "123456789" + gender: "perempuan" + birth_date: "1988-03-15" + department: null + user_id: 9 + '404': + description: Tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + patch: + tags: [Nurses] + summary: Update data suster + description: "**Admin only** — partial update." + operationId: updateNurse + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '../schemas/nurse.yaml#/NurseUpdateRequest' } + example: + name: "Bunda Rahma, S.Kep" + sip_number: "123456789" + responses: + '200': + description: Data diperbarui + content: + application/json: + example: + message: "Data suster berhasil diperbarui." + data: + nurse_id: 2 + name: "Bunda Rahma, S.Kep" + sip_number: "123456789" + gender: "perempuan" + department: null + user_id: 9 + '403': + description: Bukan Admin + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '404': + description: Tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + delete: + tags: [Nurses] + summary: Hapus suster + description: "**Admin only**. Hapus users + nurses." + operationId: deleteNurse + security: + - sanctumToken: [] + responses: + '200': + description: Dihapus + content: + application/json: + example: { message: "Suster berhasil dihapus." } + '404': + description: Tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } diff --git a/docs/openapi_v2/paths/patients.yaml b/docs/openapi_v2/paths/patients.yaml new file mode 100644 index 0000000..f0e76c2 --- /dev/null +++ b/docs/openapi_v2/paths/patients.yaml @@ -0,0 +1,104 @@ +# ============================================================================= +# Paths — Patient Profile +# [BARU v2.1] Dari Figma form Create Reservation: +# Section "Informasi Pasien" menampilkan: +# Nama, No. BPJS, Tempat lahir, Tanggal lahir +# Data ini perlu ada di profil pasien sebelum booking. +# Pasien bisa lihat dan update profil sendiri. +# ============================================================================= + +/patients/me: + get: + tags: [Patient Profile] + summary: Lihat profil pasien yang sedang login + description: | + Mengembalikan profil lengkap pasien yang sedang login. + Data ini ditampilkan di section "Informasi Pasien" pada form Create Reservation Figma: + Nama, No. BPJS, Tempat Lahir, Tanggal Lahir. + operationId: getMyPatientProfile + security: + - sanctumToken: [] + responses: + '200': + description: Profil pasien + content: + application/json: + schema: + type: object + properties: + data: + $ref: '../schemas/patient.yaml#/PatientProfileResource' + example: + data: + patient_id: 5 + name: "Ucok Sitorus" + phone: "081234567890" + bpjs_number: "0001234567890" + birth_place: "Medan" + birth_date: "1995-06-15" + gender: "laki-laki" + user_id: 10 + '401': + description: Tidak terautentikasi + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '403': + description: Bukan role patient + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + example: { message: "Endpoint ini hanya untuk pasien." } + + patch: + tags: [Patient Profile] + summary: Update profil pasien sendiri + description: | + Pasien melengkapi data profil — BPJS, tempat lahir, tanggal lahir, gender. + Data ini muncul di form Create Reservation (pre-filled dari profil). + Partial update — kirim hanya field yang ingin diubah. + operationId: updateMyPatientProfile + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '../schemas/patient.yaml#/PatientProfileRequest' + example: + bpjs_number: "0001234567890" + birth_place: "Medan" + birth_date: "1995-06-15" + gender: "laki-laki" + responses: + '200': + description: Profil berhasil diperbarui + content: + application/json: + example: + message: "Profil berhasil diperbarui." + data: + patient_id: 5 + name: "Ucok Sitorus" + phone: "081234567890" + bpjs_number: "0001234567890" + birth_place: "Medan" + birth_date: "1995-06-15" + gender: "laki-laki" + user_id: 10 + '401': + description: Tidak terautentikasi + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '403': + description: Bukan role patient + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '422': + description: Validasi gagal + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ValidationErrorResponse' } diff --git a/docs/openapi_v2/paths/queues.yaml b/docs/openapi_v2/paths/queues.yaml new file mode 100644 index 0000000..be709fa --- /dev/null +++ b/docs/openapi_v2/paths/queues.yaml @@ -0,0 +1,193 @@ +# ============================================================================= +# Paths — EPIC 4: Queue +# Dari Figma nurse-queue-list: +# - Header: "Poli Anda hari ini", "Lihat dan kelola semua bagian Anda" +# - Jadwal cards: Poli + Jam + Total Antrian + "Yang sudah check-in" +# - Filter tabs: Semua | Booked | Called | Done +# - Tabel: No. Antrian, Nama Pasien, Status, Estimasi Waktu Tunggu, Catatan +# Dari Figma nurse-chaos-mode: +# - Card alert: "TERLAMBAT 25 MENIT", nama dokter, tombol "BERITAHU PASIEN" +# - "Poli yang tersedia untuk perpindahan pasien" + slot antrian +# Dari Figma doctor-queue-list: +# - Kolom tambahan: Keluhan, Hasil Skrining, Jenis Kelamin +# ============================================================================= + +/queues: + get: + tags: [Queue] + summary: Lihat daftar antrian (Nurse & Doctor) + description: | + **Nurse**: melihat semua antrian hari ini, terkelompok per schedule instance. + **Doctor**: hanya antrian jadwal miliknya (filter by doctor_id dari token). + + Dari Figma nurse-queue-list: antrian dikelompokkan per jadwal + (misal "Poli Umum 08:00-09:45", "Poli Umum 10:00-11:45"). + + Response menyertakan `schedule_summary` per instance dan `queue_items` per antrian. + operationId: getQueue + security: + - sanctumToken: [] + parameters: + - name: date + in: query + description: "Filter by tanggal. Default: hari ini." + required: false + schema: { type: string, format: date, example: "2025-08-15" } + - name: status + in: query + required: false + schema: + type: string + enum: [booked, checked-in, waiting, called, in-progress, done, no-show, cancelled] + - name: instance_id + in: query + required: false + description: "Filter by jadwal tertentu" + schema: { type: integer, example: 1 } + responses: + '200': + description: Daftar antrian + content: + application/json: + example: + data: + # Grouped by schedule instance — dari Figma + - instance_id: 1 + schedule_summary: + department_name: "Poli Umum" + start_time: "08:00" + end_time: "09:45" + total_antrian: 20 + checked_in_count: 15 + queue_items: + - queue_id: 1 + queue_number: 1 + status: "called" + estimated_wait_minutes: 0 + nurse_notes: null + reservation: + reservation_id: 1 + complaint: "Batuk dan pilek" + patient: + patient_id: 1 + name: "Muhamad Rival Maulana" + gender: "laki-laki" + phone: "081234567890" + schedule: + instance_id: 1 + date: "2025-08-15" + start_time: "08:00" + end_time: "09:45" + - queue_id: 2 + queue_number: 2 + status: "waiting" + estimated_wait_minutes: 15 + nurse_notes: null + reservation: + reservation_id: 2 + complaint: "Demam" + patient: + patient_id: 2 + name: "Handika Chandra Pratama" + gender: "laki-laki" + phone: "082345678901" + schedule: + instance_id: 1 + date: "2025-08-15" + start_time: "08:00" + end_time: "09:45" + '403': + description: Bukan Nurse atau Doctor + content: + application/json: + example: { message: "Akses ditolak. Endpoint ini hanya untuk nurse dan doctor." } + +/queues/{id}/status: + parameters: + - name: id + in: path + required: true + schema: { type: integer, example: 2 } + + patch: + tags: [Queue] + summary: Update status antrian (Nurse only) + description: | + **Nurse only**. Dari Figma: nurse klik pasien di list → update status. + + Transisi valid: + ``` + booked → checked-in + checked-in → waiting + waiting → called (trigger notif ke pasien) + waiting → no-show + called → in-progress + in-progress → done + booked/checked-in → cancelled + ``` + + Nurse dapat menambahkan `nurse_notes` — tampil sebagai "Catatan" di nurse queue + dan "Hasil Skrining" di doctor queue (dari Figma). + + Setelah update, sistem otomatis: + 1. Sync `reservations.status` via QueueObserver + 2. Recalculate estimasi antrian semua pasien di instance yang sama + 3. Kirim notifikasi ke pasien jika status = `called` + operationId: updateQueueStatus + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '../schemas/queue.yaml#/QueueStatusRequest' } + example: + status: "called" + nurse_notes: "Pasien sudah siap, tekanan darah normal" + responses: + '200': + description: Status diperbarui + content: + application/json: + example: + message: "Status antrian berhasil diperbarui." + data: + queue_id: 2 + queue_number: 2 + status: "called" + estimated_wait_minutes: null + nurse_notes: "Pasien sudah siap, tekanan darah normal" + updated_at: "2025-08-15T10:05:00Z" + reservation: + reservation_id: 2 + complaint: "Demam" + patient: + patient_id: 2 + name: "Handika Chandra Pratama" + gender: "laki-laki" + phone: "082345678901" + schedule: + instance_id: 1 + date: "2025-08-15" + start_time: "08:00" + end_time: "09:45" + '403': + description: Bukan Nurse + content: + application/json: + example: { message: "Akses ditolak. Endpoint ini hanya untuk nurse." } + '404': + description: Antrian tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '409': + description: Transisi status tidak valid + content: + application/json: + example: { message: "Transisi status tidak valid. Status 'done' tidak dapat diubah lagi." } + '422': + description: Nilai status tidak valid + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ValidationErrorResponse' } diff --git a/docs/openapi_v2/paths/reports.yaml b/docs/openapi_v2/paths/reports.yaml new file mode 100644 index 0000000..dd34383 --- /dev/null +++ b/docs/openapi_v2/paths/reports.yaml @@ -0,0 +1,110 @@ +# ============================================================================= +# Paths — EPIC 4: Reports +# Dari Figma admin-reports: +# - Filter tabs: Semua | Bulan Ini | Bulanan | Tahunan +# - "Cari berdasarkan tanggal..." +# - Tabel: # | Profesi | Hadir | Izin | Aktif | Selesai +# - Summary cards: "Total antrian hari ini: 240", "Total antrian aktif: 60", +# "Total antrian dibatalkan: 10" +# ============================================================================= + +/reports/queues: + get: + tags: [Reports] + summary: Laporan antrian berdasarkan periode (Admin) + description: | + **Admin only**. Dari Figma admin-reports: + - Filter: Semua | Bulan Ini | Bulanan | Tahunan + input tanggal + - Summary: Total antrian hari ini, Total aktif, Total dibatalkan + - Breakdown per departemen dan per dokter + operationId: getQueueReports + security: + - sanctumToken: [] + parameters: + - name: period + in: query + required: true + description: "Dari Figma tabs: Semua=all, Bulan Ini=current_month, Bulanan=monthly, Tahunan=yearly" + schema: + type: string + enum: [all, current_month, monthly, yearly] + example: "monthly" + - name: date + in: query + required: false + description: | + Tanggal referensi (YYYY-MM-DD): + - `monthly` → sistem ambil bulan dari tanggal ini + - `yearly` → sistem ambil tahun dari tanggal ini + - `all` / `current_month` → diabaikan + schema: + type: string + format: date + example: "2025-08-01" + - name: department_id + in: query + required: false + schema: { type: integer, example: 1 } + - name: doctor_id + in: query + required: false + schema: { type: integer, example: 1 } + responses: + '200': + description: Laporan antrian + content: + application/json: + schema: { $ref: '../schemas/queue.yaml#/QueueReportResource' } + example: + data: + period: "monthly" + period_label: "Agustus 2025" + date_range: + from: "2025-08-01" + to: "2025-08-31" + # Dari Figma summary cards + summary: + total_reservations: 240 + completed: 180 + cancelled: 10 + active: 50 + # "Total antrian hari ini" — khusus hari ini + today_total: 24 + by_department: + - department_id: 1 + department_name: "Poli Umum" + total: 100 + completed: 75 + cancelled: 5 + active: 20 + - department_id: 2 + department_name: "Poli Mata" + total: 80 + completed: 60 + cancelled: 3 + active: 17 + by_doctor: + - doctor_id: 1 + doctor_name: "dr. Ucok Napitupulu, Sp.PD" + department_name: "Poli Umum" + total: 60 + completed: 45 + cancelled: 3 + active: 12 + - doctor_id: 2 + doctor_name: "dr. Tirta Pengpeng" + department_name: "Poli Umum" + total: 40 + completed: 30 + cancelled: 2 + active: 8 + '403': + description: Bukan Admin + content: + application/json: + example: { message: "Akses ditolak. Endpoint ini hanya untuk admin." } + '422': + description: Parameter tidak valid + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ValidationErrorResponse' } diff --git a/docs/openapi_v2/paths/reservations.yaml b/docs/openapi_v2/paths/reservations.yaml new file mode 100644 index 0000000..16dfc04 --- /dev/null +++ b/docs/openapi_v2/paths/reservations.yaml @@ -0,0 +1,316 @@ +# ============================================================================= +# Paths — EPIC 3: Reservations +# Dari Figma patient-create-reservation: +# - "Informasi Pasien": Nama, No. BPJS, Tempat lahir, Tanggal lahir +# - "Keluhan Pasien" textarea +# - Info jadwal: Dokter, Estimasi nomor, Jam, Tanggal +# - "Dianjurkan datang pada 15:45 untuk check-in" +# - Checkbox "Checklist bila data sudah sesuai" +# - Tombol: Kirim | Kembali +# +# Dari Figma patient-reservation (history): +# - Tabs: Semua | Aktif | Selesai | Dibatalkan +# - Card: Poli, Dokter, "Estimasi nomor antrian: 16", Jam, Tanggal, Status badge +# +# Dari Figma patient-detail-reservation: +# - Panel kiri: list reservasi (history) +# - Panel kanan: "DETAIL RESERVATION" — info jadwal + info pasien + tombol Batalkan +# ============================================================================= + +/reservations: + get: + tags: [Booking] + summary: Riwayat reservasi pasien + description: | + **Patient only**. Dari Figma patient-reservation: + - Tab filter: Semua | Aktif | Selesai | Dibatalkan + - Card: Poli, Dokter, Estimasi nomor antrian, Jam, Tanggal, Status + operationId: getMyReservations + security: + - sanctumToken: [] + parameters: + - name: status + in: query + required: false + schema: + type: string + enum: [active, completed, cancelled] + example: "active" + - name: page + in: query + required: false + schema: { type: integer, default: 1 } + - name: per_page + in: query + required: false + schema: { type: integer, default: 10 } + responses: + '200': + description: Riwayat reservasi + content: + application/json: + example: + data: + - reservation_id: 1 + status: "active" + complaint: "Batuk dan pilek" + created_at: "2025-08-10T09:00:00Z" + doctor: + doctor_id: 1 + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam" + department: + department_id: 1 + name: "Poli Umum" + schedule: + instance_id: 1 + date: "2025-08-18" + start_time: "16:00" + end_time: "18:00" + recommended_checkin_time: "15:45" + queue: + queue_id: 1 + queue_number: 16 + status: "booked" + estimated_wait_minutes: null + estimated_queue_number: 16 + - reservation_id: 2 + status: "completed" + complaint: "Demam" + created_at: "2025-08-05T08:00:00Z" + doctor: + doctor_id: 1 + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam" + department: + department_id: 1 + name: "Poli Umum" + schedule: + instance_id: 2 + date: "2025-08-07" + start_time: "16:00" + end_time: "18:00" + recommended_checkin_time: "15:45" + queue: + queue_id: 2 + queue_number: 16 + status: "done" + estimated_wait_minutes: null + estimated_queue_number: 16 + - reservation_id: 3 + status: "cancelled" + complaint: null + created_at: "2025-08-01T07:00:00Z" + doctor: + doctor_id: 1 + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam" + department: + department_id: 1 + name: "Poli Umum" + schedule: + instance_id: 3 + date: "2025-08-03" + start_time: "16:00" + end_time: "18:00" + recommended_checkin_time: "15:45" + queue: + queue_id: 3 + queue_number: 16 + # Dari Figma: "Estimasi nomor antrian: Dibatalkan oleh pasien." + status: "cancelled" + estimated_wait_minutes: null + estimated_queue_number: null + meta: + current_page: 1 + per_page: 10 + total: 3 + last_page: 1 + from: 1 + to: 3 + '403': + description: Bukan Patient + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + post: + tags: [Booking] + summary: Buat reservasi baru + description: | + **Patient only**. Dari Figma patient-create-reservation: + - Input: instance_id (dari halaman pilih jadwal), complaint (keluhan pasien) + - Informasi Pasien ditampilkan dari `GET /patients/me` (pre-filled) + - Setelah submit: sistem generate queue_number otomatis dengan status `booked` + - [BARU] `instance_id` menggantikan `schedule_id` dari versi sebelumnya + operationId: createReservation + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '../schemas/reservation.yaml#/ReservationRequest' } + example: + instance_id: 1 + complaint: "Batuk dan pilek sudah 3 hari, disertai demam ringan" + responses: + '201': + description: Reservasi berhasil — nomor antrian digenerate + content: + application/json: + example: + message: "Reservasi berhasil dibuat." + data: + reservation_id: 5 + status: "active" + complaint: "Batuk dan pilek sudah 3 hari, disertai demam ringan" + created_at: "2025-08-10T09:30:00Z" + doctor: + doctor_id: 1 + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam" + department: + department_id: 1 + name: "Poli Umum" + schedule: + instance_id: 1 + date: "2025-08-18" + start_time: "16:00" + end_time: "18:00" + # Dari Figma: "Dianjurkan datang pada 15:45 untuk check-in" + recommended_checkin_time: "15:45" + queue: + queue_id: 5 + queue_number: 16 + status: "booked" + estimated_wait_minutes: null + estimated_queue_number: 16 + '409': + description: Slot penuh atau double booking + content: + application/json: + example: + message: "Slot jadwal sudah penuh atau Anda sudah memiliki reservasi aktif pada jadwal ini." + '422': + description: Validasi gagal + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ValidationErrorResponse' } + example: + message: "The given data was invalid." + errors: + instance_id: ["Jadwal tidak tersedia atau sudah penuh."] + +/reservations/{id}: + parameters: + - name: id + in: path + required: true + schema: { type: integer, example: 1 } + + get: + tags: [Booking] + summary: Detail reservasi + description: | + **Patient only** — hanya reservasi milik pasien yang login. + Dari Figma patient-detail-reservation: + - "DETAIL RESERVATION" panel kanan + - Info jadwal: Dokter, Estimasi nomor, Jam, Tanggal + - "Dianjurkan datang pada 15:45 untuk check-in" + - Informasi Pasien: Nama, No.BPJS, Tempat lahir, Tanggal lahir, Keluhan + - Tombol: Batalkan | Kembali + - Queue status real-time (polling dari React SPA) + operationId: getReservationById + security: + - sanctumToken: [] + responses: + '200': + description: Detail reservasi + content: + application/json: + example: + data: + reservation_id: 1 + status: "active" + complaint: "Batuk dan pilek sudah 3 hari" + created_at: "2025-08-10T09:00:00Z" + patient: + patient_id: 5 + name: "Ucok Sitorus" + gender: "laki-laki" + bpjs_number: "0001234567890" + birth_place: "Medan" + birth_date: "1995-06-15" + doctor: + doctor_id: 1 + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam" + department: + department_id: 1 + name: "Poli Umum" + schedule: + instance_id: 1 + date: "2025-08-18" + start_time: "16:00" + end_time: "18:00" + recommended_checkin_time: "15:45" + queue: + queue_id: 1 + queue_number: 16 + status: "waiting" + estimated_wait_minutes: 25 + estimated_queue_number: 16 + '403': + description: Bukan milik pasien ini + content: + application/json: + example: { message: "Akses ditolak. Reservasi ini bukan milik Anda." } + '404': + description: Tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + +/reservations/{id}/cancel: + parameters: + - name: id + in: path + required: true + schema: { type: integer, example: 1 } + + patch: + tags: [Booking] + summary: Batalkan reservasi + description: | + **Patient only** — hanya reservasi milik pasien yang login. + Dari Figma: tombol "Batalkan" di detail reservasi. + Tidak perlu request body. + Gagal jika antrian sudah `called/in-progress/done`. + operationId: cancelReservation + security: + - sanctumToken: [] + responses: + '200': + description: Reservasi dibatalkan + content: + application/json: + example: + message: "Reservasi berhasil dibatalkan." + data: + reservation_id: 1 + status: "cancelled" + queue: + queue_id: 1 + queue_number: 16 + status: "cancelled" + '403': + description: Bukan milik pasien ini + content: + application/json: + example: { message: "Akses ditolak. Reservasi ini bukan milik Anda." } + '409': + description: Tidak bisa dibatalkan + content: + application/json: + example: { message: "Reservasi tidak dapat dibatalkan karena pasien sudah dipanggil atau sedang konsultasi." } diff --git a/docs/openapi_v2/paths/schedule_instances.yaml b/docs/openapi_v2/paths/schedule_instances.yaml new file mode 100644 index 0000000..53838c6 --- /dev/null +++ b/docs/openapi_v2/paths/schedule_instances.yaml @@ -0,0 +1,207 @@ +# ============================================================================= +# Paths — EPIC 2/3: Schedule Instances (Tanggal Aktual) +# [BARU v2.1] +# +# Dari Figma patien-view-detail-department: +# - Card jadwal: "dr. Gia Pratama", "POLI UMUM", "Selasa - --/--/2026", +# "08:00", "Estimasi nomor antrian: 06", badge "Tersedia"/"Penuh" +# - Tombol "Pilih Jadwal" — mengarah ke Create Reservation +# +# Instances di-generate otomatis oleh Laravel Scheduler (2 minggu ke depan) +# Admin bisa override jam/kapasitas atau cancel instance tertentu +# ============================================================================= + +/schedule-instances: + get: + tags: [Schedule Instances] + summary: Lihat jadwal aktual tersedia + description: | + Mengembalikan daftar schedule instances (tanggal aktual). + Pasien menggunakan ini untuk melihat jadwal sebelum booking. + Dari Figma: menampilkan dokter, poli, tanggal, jam, estimasi antrian, status slot. + + Filter wajib minimal salah satu: `department_id` atau `doctor_id` atau `date`. + Default: hanya instances dengan `status = active` dan `date >= today`. + operationId: getScheduleInstances + security: + - sanctumToken: [] + parameters: + - name: department_id + in: query + required: false + description: "Filter by departemen — alur booking patient dari poli ke jadwal" + schema: { type: integer, example: 1 } + - name: doctor_id + in: query + required: false + description: "Filter by dokter tertentu" + schema: { type: integer, example: 1 } + - name: date + in: query + required: false + description: "Filter by tanggal spesifik (YYYY-MM-DD)" + schema: { type: string, format: date, example: "2025-08-18" } + - name: from_date + in: query + required: false + description: "Tanggal awal range. Default: hari ini." + schema: { type: string, format: date, example: "2025-08-18" } + - name: to_date + in: query + required: false + description: "Tanggal akhir range. Default: 2 minggu dari sekarang." + schema: { type: string, format: date, example: "2025-09-01" } + responses: + '200': + description: Daftar jadwal aktual + content: + application/json: + example: + data: + - instance_id: 1 + template_id: 1 + doctor_id: 1 + doctor: + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam" + department: + department_id: 1 + name: "Poli Umum" + date: "2025-08-18" + start_time: "08:00" + end_time: "12:00" + max_patients: 10 + booked_slots: 5 + available_slots: 5 + # Dari Figma: "Estimasi nomor antrian: 06" + estimated_queue_number: 6 + is_available: true + status: "active" + is_override: false + override_note: null + - instance_id: 2 + template_id: 1 + doctor_id: 1 + doctor: + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam" + department: + department_id: 1 + name: "Poli Umum" + date: "2025-08-18" + start_time: "10:00" + end_time: "14:00" + max_patients: 10 + booked_slots: 10 + available_slots: 0 + estimated_queue_number: 20 + # Dari Figma: badge "Penuh" + is_available: false + status: "active" + is_override: false + override_note: null + '401': + description: Tidak terautentikasi + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + +/schedule-instances/{instanceId}: + parameters: + - name: instanceId + in: path + required: true + schema: { type: integer, example: 1 } + + get: + tags: [Schedule Instances] + summary: Detail satu schedule instance + operationId: getScheduleInstanceById + security: + - sanctumToken: [] + responses: + '200': + description: Detail instance + content: + application/json: + example: + data: + instance_id: 1 + template_id: 1 + doctor_id: 1 + doctor: + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam" + department: + department_id: 1 + name: "Poli Umum" + date: "2025-08-18" + start_time: "08:00" + end_time: "12:00" + max_patients: 10 + booked_slots: 5 + available_slots: 5 + estimated_queue_number: 6 + is_available: true + status: "active" + is_override: false + override_note: null + '404': + description: Tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + patch: + tags: [Schedule Instances] + summary: Override atau cancel satu instance + description: | + **Admin only**. Override jam/kapasitas atau cancel instance tertentu. + Jika `status = cancelled`: + - Reservasi dengan status `booked` di instance ini akan dibatalkan otomatis + - Notifikasi dikirim ke pasien terdampak + - Reservasi yang sudah `checked-in/waiting/called/in-progress` → 409 Conflict + operationId: updateScheduleInstance + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '../schemas/schedule.yaml#/ScheduleInstanceRequest' } + example: + start_time: "09:00" + end_time: "13:00" + override_note: "Jam diubah karena rapat koordinasi dokter" + responses: + '200': + description: Instance diperbarui + content: + application/json: + example: + message: "Jadwal berhasil diperbarui." + data: + instance_id: 1 + date: "2025-08-18" + start_time: "09:00" + end_time: "13:00" + max_patients: 10 + status: "active" + is_override: true + override_note: "Jam diubah karena rapat koordinasi dokter" + '403': + description: Bukan Admin + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '404': + description: Instance tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '409': + description: Ada pasien aktif (checked-in/waiting/called/in-progress) + content: + application/json: + example: + message: "Jadwal tidak dapat diubah karena ada pasien yang sedang aktif di klinik." diff --git a/docs/openapi_v2/paths/schedule_templates.yaml b/docs/openapi_v2/paths/schedule_templates.yaml new file mode 100644 index 0000000..ae33a2d --- /dev/null +++ b/docs/openapi_v2/paths/schedule_templates.yaml @@ -0,0 +1,210 @@ +# ============================================================================= +# Paths — EPIC 2: Schedule Templates (Recurring) +# [BARU v2.1] Menggantikan /doctors/{id}/schedules untuk bagian recurring +# +# Dari Figma admin-doctor-detail (tabel Schedule): +# - Kolom: No., Hari (Senin/Selasa/dll), Pukul (09:00-15:00), Aksi (Delete/Edit) +# - Tombol "Add Schedule" di atas tabel +# Dari Figma admin-create/update-doctor-schedule: +# - Form: Doctor Name, No SIP, Hari Praktek (dropdown hari), Waktu +# ============================================================================= + +/doctors/{id}/schedule-templates: + parameters: + - name: id + in: path + required: true + description: "ID dokter" + schema: { type: integer, example: 1 } + + get: + tags: [Schedule Templates] + summary: Lihat template jadwal dokter + description: | + Menampilkan semua template jadwal recurring dokter. + Dari Figma doctor-detail: tabel berisi Hari + Jam praktik. + Dapat diakses semua role yang login. + operationId: getDoctorScheduleTemplates + security: + - sanctumToken: [] + responses: + '200': + description: Daftar template jadwal + content: + application/json: + example: + data: + - template_id: 1 + doctor_id: 1 + day_of_week: "senin" + start_time: "09:00" + end_time: "15:00" + max_patients: 10 + is_active: true + valid_from: "2025-08-01" + valid_until: null + - template_id: 2 + doctor_id: 1 + day_of_week: "selasa" + start_time: "09:00" + end_time: "15:00" + max_patients: 10 + is_active: true + valid_from: "2025-08-01" + valid_until: null + - template_id: 3 + doctor_id: 1 + day_of_week: "kamis" + start_time: "09:00" + end_time: "12:00" + max_patients: 8 + is_active: true + valid_from: "2025-08-01" + valid_until: null + '404': + description: Dokter tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + post: + tags: [Schedule Templates] + summary: Tambah template jadwal dokter + description: | + **Admin only**. Dari Figma "Add Doctor's Schedule" form: + Doctor Name (read-only), No SIP (read-only), Hari Praktek (dropdown), Waktu. + Setelah template dibuat, Laravel Scheduler akan auto-generate instances + untuk 2 minggu ke depan. + operationId: createDoctorScheduleTemplate + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '../schemas/schedule.yaml#/ScheduleTemplateRequest' } + example: + day_of_week: "jumat" + start_time: "09:00" + end_time: "12:00" + max_patients: 10 + is_active: true + valid_from: "2025-08-01" + valid_until: null + responses: + '201': + description: Template dibuat — instances akan di-generate otomatis + content: + application/json: + example: + message: "Template jadwal berhasil dibuat. Instances akan di-generate otomatis." + data: + template_id: 4 + doctor_id: 1 + day_of_week: "jumat" + start_time: "09:00" + end_time: "12:00" + max_patients: 10 + is_active: true + valid_from: "2025-08-01" + valid_until: null + '403': + description: Bukan Admin + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '409': + description: Dokter sudah punya template aktif di hari yang sama + content: + application/json: + example: { message: "Dokter sudah memiliki jadwal aktif di hari Jumat." } + '422': + description: Validasi gagal + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ValidationErrorResponse' } + +/doctors/{id}/schedule-templates/{templateId}: + parameters: + - name: id + in: path + required: true + schema: { type: integer, example: 1 } + - name: templateId + in: path + required: true + schema: { type: integer, example: 1 } + + patch: + tags: [Schedule Templates] + summary: Update template jadwal + description: | + **Admin only**. Dari Figma "Update Doctor's Schedule" form. + Update template tidak mengubah instances yang sudah ada — + hanya mempengaruhi instances yang di-generate setelah update ini. + operationId: updateDoctorScheduleTemplate + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '../schemas/schedule.yaml#/ScheduleTemplateRequest' } + example: + start_time: "10:00" + end_time: "14:00" + max_patients: 8 + responses: + '200': + description: Template diperbarui + content: + application/json: + example: + message: "Template jadwal berhasil diperbarui." + data: + template_id: 1 + doctor_id: 1 + day_of_week: "senin" + start_time: "10:00" + end_time: "14:00" + max_patients: 8 + is_active: true + valid_from: "2025-08-01" + valid_until: null + '403': + description: Bukan Admin + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '404': + description: Template tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + delete: + tags: [Schedule Templates] + summary: Hapus template jadwal + description: | + **Admin only**. Menonaktifkan template (is_active = false) atau menghapus. + Instances yang sudah ada dan belum completed tidak dihapus otomatis. + Gunakan PATCH /schedule-instances/{id} untuk cancel instance per instance. + operationId: deleteDoctorScheduleTemplate + security: + - sanctumToken: [] + responses: + '200': + description: Template dihapus + content: + application/json: + example: { message: "Template jadwal berhasil dihapus." } + '403': + description: Bukan Admin + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '404': + description: Template tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } diff --git a/docs/openapi_v2/schemas/auth.yaml b/docs/openapi_v2/schemas/auth.yaml new file mode 100644 index 0000000..222decd --- /dev/null +++ b/docs/openapi_v2/schemas/auth.yaml @@ -0,0 +1,98 @@ +# ============================================================================= +# Schemas — Authentication +# ============================================================================= + +RegisterRequest: + type: object + required: [name, email, password, password_confirmation, phone] + properties: + name: + type: string + maxLength: 255 + example: "Ucok Sitorus" + email: + type: string + format: email + example: "ucok@example.com" + password: + type: string + format: password + minLength: 8 + example: "password123" + password_confirmation: + type: string + format: password + example: "password123" + phone: + type: string + maxLength: 15 + example: "081234567890" + +LoginRequest: + type: object + required: [email, password] + properties: + email: + type: string + format: email + example: "ucok@example.com" + password: + type: string + format: password + example: "password123" + +# Response setelah login — mengembalikan Sanctum plain-text token +# Simpan di React state/memory, BUKAN localStorage +LoginResponse: + type: object + properties: + message: + type: string + example: "Login berhasil." + data: + type: object + properties: + token: + type: string + description: "Sanctum plain-text token. Gunakan: Authorization: Bearer {token}" + example: "3|aB1cUcokD2eF3gH4iJ5kL6" + token_type: + type: string + example: "Bearer" + user: + $ref: '#/UserResource' + +# Resource user — profile bervariasi per role: +# patient → { patient_id, name, phone, bpjs_number, birth_place, birth_date, gender } +# doctor → { doctor_id, name, specialization, sip_number, gender, birth_date, doctor_status, department } +# nurse → { nurse_id, name, sip_number, gender, birth_date, department } +# admin → { admin_id, name } +UserResource: + type: object + properties: + user_id: + type: integer + example: 10 + email: + type: string + format: email + example: "ucok@example.com" + role: + type: string + enum: [patient, doctor, nurse, admin] + example: "patient" + status: + type: string + enum: [active, inactive] + example: "active" + profile: + type: object + description: "Struktur berbeda per role. Lihat PatientProfileResource / DoctorResource / NurseResource." + example: + patient_id: 5 + name: "Ucok Sitorus" + phone: "081234567890" + bpjs_number: "0001234567890" + birth_place: "Medan" + birth_date: "1995-06-15" + gender: "laki-laki" diff --git a/docs/openapi_v2/schemas/common.yaml b/docs/openapi_v2/schemas/common.yaml new file mode 100644 index 0000000..14ed172 --- /dev/null +++ b/docs/openapi_v2/schemas/common.yaml @@ -0,0 +1,45 @@ +# ============================================================================= +# Schemas — Common / Shared +# ============================================================================= + +SuccessResponse: + type: object + properties: + message: + type: string + example: "Operasi berhasil." + +ErrorResponse: + type: object + properties: + message: + type: string + example: "Terjadi kesalahan." + +# Format default Laravel 422 validation error +ValidationErrorResponse: + type: object + properties: + message: + type: string + example: "The given data was invalid." + errors: + type: object + additionalProperties: + type: array + items: + type: string + example: + email: ["The email has already been taken."] + password: ["The password must be at least 8 characters."] + +# Format Laravel Resource Pagination +PaginationMeta: + type: object + properties: + current_page: { type: integer, example: 1 } + per_page: { type: integer, example: 10 } + total: { type: integer, example: 35 } + last_page: { type: integer, example: 4 } + from: { type: integer, example: 1 } + to: { type: integer, example: 10 } diff --git a/docs/openapi_v2/schemas/department.yaml b/docs/openapi_v2/schemas/department.yaml new file mode 100644 index 0000000..6246b75 --- /dev/null +++ b/docs/openapi_v2/schemas/department.yaml @@ -0,0 +1,46 @@ +# ============================================================================= +# Schemas — Department +# Dari Figma: card poli menampilkan nama, deskripsi, jumlah dokter tersedia +# Screen: patient-view-department — "3 dokter tersedia", "Poli Umum" +# ============================================================================= + +DepartmentResource: + type: object + properties: + department_id: + type: integer + example: 1 + name: + type: string + example: "Poli Umum" + description: + type: string + nullable: true + # Dari Figma: "Pemeriksaan Kesehatan Umum" — deskripsi singkat di card + example: "Pusat layanan kesehatan primer untuk diagnosa awal, pengobatan umum, dan rujukan" + # Dari Figma: card menampilkan "3 dokter tersedia" — computed field + doctors_count: + type: integer + description: "Jumlah dokter aktif di departemen ini." + example: 3 + created_at: + type: string + format: date-time + example: "2025-01-01T00:00:00Z" + updated_at: + type: string + format: date-time + example: "2025-08-01T10:00:00Z" + +# Digunakan untuk POST create dan PATCH update (partial) +DepartmentRequest: + type: object + properties: + name: + type: string + maxLength: 255 + example: "Poli Umum" + description: + type: string + nullable: true + example: "Pusat layanan kesehatan primer untuk diagnosa awal, pengobatan umum, dan rujukan" diff --git a/docs/openapi_v2/schemas/doctor.yaml b/docs/openapi_v2/schemas/doctor.yaml new file mode 100644 index 0000000..f1d29b3 --- /dev/null +++ b/docs/openapi_v2/schemas/doctor.yaml @@ -0,0 +1,128 @@ +# ============================================================================= +# Schemas — Doctor +# [BARU v2.1] Dari Figma form Doctor: +# - sip_number : field "No SIP" di form create/update doctor +# - birth_date : field "Tanggal Lahir" di form +# - gender : field "Jenis Kelamin" di form +# Dari Figma doctor-detail: tabel schedule menampilkan Hari + Jam +# Dari Figma doctor-queue-list: kolom tambahan Keluhan + Jenis Kelamin +# ============================================================================= + +DoctorResource: + type: object + properties: + doctor_id: + type: integer + example: 1 + name: + type: string + example: "dr. Ucok Napitupulu, Sp.PD" + specialization: + type: string + example: "Spesialis Penyakit Dalam" + # [BARU] dari Figma form doctor + sip_number: + type: string + nullable: true + description: "Nomor Surat Izin Praktik dokter." + example: "112345678" + birth_date: + type: string + format: date + nullable: true + example: "1980-03-20" + gender: + type: string + nullable: true + enum: ["laki-laki", "perempuan"] + example: "laki-laki" + # doctor_status: dikelola DoctorDelayDetectionService (auto) dan Nurse/Admin (manual) + doctor_status: + type: string + enum: [active, late, absent] + example: "active" + department: + type: object + properties: + department_id: { type: integer, example: 1 } + name: { type: string, example: "Poli Umum" } + user_id: + type: integer + example: 5 + created_at: + type: string + format: date-time + example: "2025-01-15T08:00:00Z" + updated_at: + type: string + format: date-time + example: "2025-08-01T10:00:00Z" + +# Request body POST — Register Doctor (Admin) +# Membuat users + doctors sekaligus dalam satu transaksi +DoctorRequest: + type: object + required: [name, email, password, specialization, department_id] + properties: + name: + type: string + maxLength: 255 + example: "dr. Ucok Napitupulu, Sp.PD" + email: + type: string + format: email + example: "ucok.dokter@hospital.com" + password: + type: string + format: password + minLength: 8 + example: "password123" + specialization: + type: string + example: "Spesialis Penyakit Dalam" + department_id: + type: integer + example: 1 + # [BARU] field dari Figma + sip_number: + type: string + nullable: true + example: "112345678" + birth_date: + type: string + format: date + nullable: true + example: "1980-03-20" + gender: + type: string + nullable: true + enum: ["laki-laki", "perempuan"] + example: "laki-laki" + +# Request body PATCH — Update Doctor (partial update) +DoctorUpdateRequest: + type: object + properties: + name: + type: string + example: "dr. Ucok Napitupulu, Sp.PD" + specialization: + type: string + example: "Spesialis Penyakit Dalam" + department_id: + type: integer + example: 1 + sip_number: + type: string + nullable: true + example: "112345678" + birth_date: + type: string + format: date + nullable: true + example: "1980-03-20" + gender: + type: string + nullable: true + enum: ["laki-laki", "perempuan"] + example: "laki-laki" diff --git a/docs/openapi_v2/schemas/nurse.yaml b/docs/openapi_v2/schemas/nurse.yaml new file mode 100644 index 0000000..43a81ba --- /dev/null +++ b/docs/openapi_v2/schemas/nurse.yaml @@ -0,0 +1,116 @@ +# ============================================================================= +# Schemas — Nurse +# [BARU v2.1] Dari Figma form Nurse: +# - sip_number : field "No SIP" di form +# - birth_date : field "Tanggal Lahir" +# - gender : field "Jenis Kelamin" +# ============================================================================= + +NurseResource: + type: object + properties: + nurse_id: + type: integer + example: 1 + name: + type: string + example: "Ucok Sihombing" + # [BARU] dari Figma form nurse + sip_number: + type: string + nullable: true + description: "Nomor Surat Izin Praktik suster." + example: "112345678" + birth_date: + type: string + format: date + nullable: true + example: "1990-07-10" + gender: + type: string + nullable: true + enum: ["laki-laki", "perempuan"] + example: "perempuan" + # department nullable — nurse bisa lintas departemen + department: + type: object + nullable: true + properties: + department_id: { type: integer, example: 1 } + name: { type: string, example: "Poli Umum" } + user_id: + type: integer + example: 8 + created_at: + type: string + format: date-time + example: "2025-01-15T08:00:00Z" + updated_at: + type: string + format: date-time + example: "2025-08-01T10:00:00Z" + +# Request body POST — Register Nurse (Admin) +NurseRequest: + type: object + required: [name, email, password] + properties: + name: + type: string + maxLength: 255 + example: "Ucok Sihombing" + email: + type: string + format: email + example: "ucok.nurse@hospital.com" + password: + type: string + format: password + minLength: 8 + example: "password123" + # department_id opsional — nurse bisa lintas departemen + department_id: + type: integer + nullable: true + example: 1 + # [BARU] field dari Figma + sip_number: + type: string + nullable: true + example: "112345678" + birth_date: + type: string + format: date + nullable: true + example: "1990-07-10" + gender: + type: string + nullable: true + enum: ["laki-laki", "perempuan"] + example: "perempuan" + +# Request body PATCH — Update Nurse (partial update) +NurseUpdateRequest: + type: object + properties: + name: + type: string + example: "Ucok Sihombing, S.Kep" + department_id: + type: integer + nullable: true + example: 2 + sip_number: + type: string + nullable: true + example: "112345678" + birth_date: + type: string + format: date + nullable: true + example: "1990-07-10" + gender: + type: string + nullable: true + enum: ["laki-laki", "perempuan"] + example: "perempuan" diff --git a/docs/openapi_v2/schemas/patient.yaml b/docs/openapi_v2/schemas/patient.yaml new file mode 100644 index 0000000..83a8def --- /dev/null +++ b/docs/openapi_v2/schemas/patient.yaml @@ -0,0 +1,80 @@ +# ============================================================================= +# Schemas — Patient Profile +# [BARU v2.1] Dari analisis Figma form Create Reservation: +# - bpjs_number, birth_place, birth_date, gender ditampilkan di form +# - Data ini perlu ada sebelum pasien bisa submit reservasi +# ============================================================================= + +# Resource profil pasien lengkap +PatientProfileResource: + type: object + properties: + patient_id: + type: integer + example: 5 + name: + type: string + example: "Ucok Sitorus" + phone: + type: string + example: "081234567890" + # Field dari Figma form Create Reservation + # "Informasi Pasien" section menampilkan semua field ini + bpjs_number: + type: string + nullable: true + description: "Nomor BPJS pasien. Nullable — bisa dilengkapi saat booking." + example: "0001234567890" + birth_place: + type: string + nullable: true + description: "Tempat lahir pasien." + example: "Medan" + birth_date: + type: string + format: date + nullable: true + description: "Tanggal lahir pasien (YYYY-MM-DD)." + example: "1995-06-15" + gender: + type: string + nullable: true + enum: ["laki-laki", "perempuan"] + example: "laki-laki" + user_id: + type: integer + example: 10 + +# Request body untuk PATCH /patients/me (update profil sendiri) +# Semua field opsional — partial update +PatientProfileRequest: + type: object + properties: + name: + type: string + maxLength: 255 + example: "Ucok Sitorus" + phone: + type: string + maxLength: 15 + example: "081234567890" + bpjs_number: + type: string + nullable: true + maxLength: 50 + example: "0001234567890" + birth_place: + type: string + nullable: true + maxLength: 100 + example: "Medan" + birth_date: + type: string + format: date + nullable: true + example: "1995-06-15" + gender: + type: string + nullable: true + enum: ["laki-laki", "perempuan"] + example: "laki-laki" diff --git a/docs/openapi_v2/schemas/queue.yaml b/docs/openapi_v2/schemas/queue.yaml new file mode 100644 index 0000000..4ac93c5 --- /dev/null +++ b/docs/openapi_v2/schemas/queue.yaml @@ -0,0 +1,148 @@ +# ============================================================================= +# Schemas — Queue +# Dari Figma nurse-queue-list: +# - Kolom: No. Antrian, Nama Pasien, Status, Estimasi Waktu Tunggu, Catatan +# - Status badge: CALLED, WAITING, BOOKED, DONE +# - Filter tabs: Semua | Booked | Called | Done +# Dari Figma doctor-queue-list: +# - Kolom tambahan: Keluhan, Jenis Kelamin (dari patients/reservations) +# - "Hasil Skrining" — field catatan nurse (belum di-spec sebelumnya) +# ============================================================================= + +QueueResource: + type: object + properties: + queue_id: + type: integer + example: 4 + queue_number: + type: integer + description: "Nomor urut antrian dalam satu instance. Dimulai dari 1." + example: 4 + # State machine: booked→checked-in→waiting→called→in-progress→done/no-show + status: + type: string + enum: [booked, checked-in, waiting, called, in-progress, done, no-show, cancelled] + example: "waiting" + estimated_wait_minutes: + type: integer + nullable: true + description: "Estimasi menit tunggu. null jika belum dikalkulasi (status masih booked)." + example: 25 + # [BARU dari Figma doctor-queue-list] catatan nurse saat check-in + # Figma menampilkan kolom "Catatan" di nurse queue dan "Hasil Skrining" di doctor queue + nurse_notes: + type: string + nullable: true + description: "Catatan dari nurse saat check-in atau update status. Terlihat oleh dokter." + example: "Pasien terlihat lemas, tekanan darah 130/90" + updated_at: + type: string + format: date-time + example: "2025-08-15T09:05:00Z" + # Nested reservation — berisi info pasien untuk tampilan nurse/doctor + reservation: + type: object + properties: + reservation_id: { type: integer, example: 4 } + # complaint dari reservations — ditampilkan di doctor-queue-list Figma + complaint: + type: string + nullable: true + example: "Batuk dan pilek sudah 3 hari" + patient: + type: object + properties: + patient_id: { type: integer, example: 4 } + name: { type: string, example: "Ucok Siahaan" } + # gender — ditampilkan di doctor-queue-list Figma (kolom "Jenis Kelamin") + gender: + type: string + nullable: true + example: "laki-laki" + phone: { type: string, example: "084567890123" } + schedule: + type: object + properties: + instance_id: { type: integer, example: 1 } + date: { type: string, format: date, example: "2025-08-15" } + start_time: { type: string, example: "08:00" } + end_time: { type: string, example: "12:00" } + +# Request body PATCH /queues/{id}/status (Nurse only) +QueueStatusRequest: + type: object + required: [status] + properties: + status: + type: string + description: | + Status antrian baru. Transisi valid: + booked → checked-in + checked-in → waiting + waiting → called (trigger notifikasi ke pasien) + waiting → no-show + called → in-progress + in-progress → done + booked → cancelled + checked-in → cancelled + enum: [checked-in, waiting, called, in-progress, done, no-show, cancelled] + example: "called" + # [BARU dari Figma] kolom "Catatan" di nurse queue list + # Nurse bisa tambah catatan saat update status + nurse_notes: + type: string + nullable: true + description: "Catatan nurse — ditampilkan sebagai 'Hasil Skrining' di queue list dokter." + example: "Pasien terlihat lemas, tekanan darah 130/90" + +# Resource laporan antrian untuk Admin +QueueReportResource: + type: object + properties: + data: + type: object + properties: + period: + type: string + enum: [daily, monthly, yearly] + example: "monthly" + period_label: + type: string + example: "Agustus 2025" + date_range: + type: object + properties: + from: { type: string, format: date, example: "2025-08-01" } + to: { type: string, format: date, example: "2025-08-31" } + # Dari Figma admin-reports: "Total antrian hari ini: 240", dll + summary: + type: object + properties: + total_reservations: { type: integer, example: 240 } + completed: { type: integer, example: 180 } + cancelled: { type: integer, example: 10 } + active: { type: integer, example: 50 } + by_department: + type: array + items: + type: object + properties: + department_id: { type: integer, example: 1 } + department_name: { type: string, example: "Poli Umum" } + total: { type: integer, example: 70 } + completed: { type: integer, example: 50 } + cancelled: { type: integer, example: 5 } + active: { type: integer, example: 15 } + by_doctor: + type: array + items: + type: object + properties: + doctor_id: { type: integer, example: 1 } + doctor_name: { type: string, example: "dr. Ucok Napitupulu, Sp.PD" } + department_name: { type: string, example: "Poli Umum" } + total: { type: integer, example: 40 } + completed: { type: integer, example: 30 } + cancelled: { type: integer, example: 3 } + active: { type: integer, example: 7 } diff --git a/docs/openapi_v2/schemas/reservation.yaml b/docs/openapi_v2/schemas/reservation.yaml new file mode 100644 index 0000000..b9f4518 --- /dev/null +++ b/docs/openapi_v2/schemas/reservation.yaml @@ -0,0 +1,123 @@ +# ============================================================================= +# Schemas — Reservation +# [BARU v2.1]: +# - instance_id menggantikan schedule_id (FK ke schedule_instances) +# - complaint (text, nullable) — dari Figma form Create Reservation +# Tampil di: form booking, detail reservasi, dan queue list dokter +# ============================================================================= + +ReservationResource: + type: object + properties: + reservation_id: + type: integer + example: 1 + # status: derived dari queues.status via QueueObserver + # active = queue belum done/no-show/cancelled + # completed = queue done + # cancelled = queue no-show atau cancelled + status: + type: string + enum: [active, completed, cancelled] + example: "active" + # [BARU] keluhan pasien — dari Figma "Keluhan Pasien" di form booking + complaint: + type: string + nullable: true + description: "Keluhan pasien yang diisi saat membuat reservasi. Ditampilkan di queue list dokter." + example: "Batuk dan pilek sudah 3 hari" + created_at: + type: string + format: date-time + example: "2025-08-10T09:00:00Z" + # Info pasien — muncul di detail reservasi dan di queue list dokter + patient: + type: object + properties: + patient_id: { type: integer, example: 5 } + name: { type: string, example: "Ucok Sitorus" } + gender: + type: string + nullable: true + example: "laki-laki" + bpjs_number: + type: string + nullable: true + example: "0001234567890" + birth_place: + type: string + nullable: true + example: "Medan" + birth_date: + type: string + format: date + nullable: true + example: "1995-06-15" + doctor: + type: object + properties: + doctor_id: { type: integer, example: 1 } + name: { type: string, example: "dr. Ucok Napitupulu, Sp.PD" } + specialization: { type: string, example: "Spesialis Penyakit Dalam" } + department: + type: object + properties: + department_id: { type: integer, example: 1 } + name: { type: string, example: "Poli Umum" } + # Schedule info — dari instance (tanggal aktual) + schedule: + type: object + properties: + instance_id: { type: integer, example: 1 } + date: { type: string, format: date, example: "2025-08-18" } + start_time: { type: string, example: "08:00" } + end_time: { type: string, example: "12:00" } + # Dari Figma: "Dianjurkan datang pada 15:45 untuk check-in" + recommended_checkin_time: + type: string + description: "15 menit sebelum start_time — ditampilkan di detail reservasi Figma" + example: "07:45" + # Queue — real-time, di-join ke reservations + queue: + type: object + properties: + queue_id: { type: integer, example: 1 } + queue_number: { type: integer, example: 4 } + status: + type: string + enum: [booked, checked-in, waiting, called, in-progress, done, no-show, cancelled] + example: "waiting" + estimated_wait_minutes: + type: integer + nullable: true + example: 25 + # Dari Figma: "Estimasi nomor antrian: 16" — ditampilkan di card history + estimated_queue_number: + type: integer + description: "Nomor antrian estimasi — sama dengan queue_number setelah booking" + example: 16 + +# Request body POST — Create Reservation (Patient only) +# [BARU v2.1]: instance_id menggantikan schedule_id +# complaint ditambahkan — wajib diisi saat booking +ReservationRequest: + type: object + required: [instance_id] + properties: + # FK ke schedule_instances — bukan schedule_templates + # Pasien booking ke tanggal aktual (instance), bukan template recurring + instance_id: + type: integer + description: | + ID jadwal aktual (schedule_instances.instance_id). + doctor_id diambil otomatis dari instance.doctor_id di backend. + Validasi: slot tersedia, tidak double booking, tanggal belum lampau, + instance.status = active. + example: 1 + # [BARU] dari Figma form Create Reservation + complaint: + type: string + nullable: true + description: "Keluhan pasien. Opsional tapi direkomendasikan untuk diisi." + maxLength: 1000 + example: "Batuk dan pilek sudah 3 hari, disertai demam ringan" diff --git a/docs/openapi_v2/schemas/schedule.yaml b/docs/openapi_v2/schemas/schedule.yaml new file mode 100644 index 0000000..cf1c1d6 --- /dev/null +++ b/docs/openapi_v2/schemas/schedule.yaml @@ -0,0 +1,199 @@ +# ============================================================================= +# Schemas — Schedule (Template + Instance) +# [BARU v2.1] Tabel schedules lama DIGANTI dengan sistem hybrid: +# +# ScheduleTemplate → recurring pattern (Hari + Jam, misal "Senin 08:00-12:00") +# ScheduleInstance → tanggal aktual (misal "2025-08-18 08:00-12:00") +# +# Dari Figma admin-doctor-detail: +# - Jadwal ditampilkan: Hari (Senin/Selasa/dll) + Jam (09:00-15:00) +# - Ada tombol Edit + Delete per jadwal +# - Tombol "Add Schedule" untuk tambah jadwal baru +# +# Dari Figma patien-view-detail-department: +# - Pasien melihat: "Selasa - --/--/2026", "08:00", "Estimasi nomor antrian: 06" +# - Instance yang ditampilkan ke pasien, bukan template +# ============================================================================= + +# ---- TEMPLATE ---- + +ScheduleTemplateResource: + type: object + properties: + template_id: + type: integer + example: 1 + doctor_id: + type: integer + example: 1 + # Dari Figma: jadwal disimpan per hari (Senin, Selasa, dst) + day_of_week: + type: string + enum: [senin, selasa, rabu, kamis, jumat, sabtu, minggu] + example: "senin" + start_time: + type: string + description: "Format HH:MM" + example: "08:00" + end_time: + type: string + description: "Format HH:MM" + example: "12:00" + max_patients: + type: integer + example: 10 + is_active: + type: boolean + description: "false = template tidak generate instance baru, tapi instance lama tetap valid" + example: true + valid_from: + type: string + format: date + description: "Tanggal template mulai berlaku" + example: "2025-08-01" + valid_until: + type: string + format: date + nullable: true + description: "Tanggal template berakhir. null = berlaku selamanya." + example: null + created_at: + type: string + format: date-time + example: "2025-07-01T00:00:00Z" + +# Request body POST/PATCH template +ScheduleTemplateRequest: + type: object + required: [day_of_week, start_time, end_time] + properties: + day_of_week: + type: string + enum: [senin, selasa, rabu, kamis, jumat, sabtu, minggu] + example: "senin" + start_time: + type: string + description: "Format HH:MM" + example: "08:00" + end_time: + type: string + description: "Format HH:MM, harus setelah start_time" + example: "12:00" + max_patients: + type: integer + minimum: 1 + maximum: 100 + default: 10 + example: 10 + is_active: + type: boolean + default: true + example: true + valid_from: + type: string + format: date + example: "2025-08-01" + valid_until: + type: string + format: date + nullable: true + example: null + +# ---- INSTANCE ---- + +ScheduleInstanceResource: + type: object + properties: + instance_id: + type: integer + example: 1 + template_id: + type: integer + nullable: true + description: "null jika instance manual tanpa template (jadwal insidental)" + example: 1 + doctor_id: + type: integer + example: 1 + # Info dokter untuk tampilan pasien (Figma: "dr. Gia Pratama", "POLI UMUM") + doctor: + type: object + properties: + name: { type: string, example: "dr. Ucok Napitupulu, Sp.PD" } + specialization: { type: string, example: "Spesialis Penyakit Dalam" } + department: + type: object + properties: + department_id: { type: integer, example: 1 } + name: { type: string, example: "Poli Umum" } + date: + type: string + format: date + description: "Tanggal aktual jadwal praktik" + example: "2025-08-18" + start_time: + type: string + example: "08:00" + end_time: + type: string + example: "12:00" + max_patients: + type: integer + example: 10 + # Computed — dihitung runtime dari jumlah reservasi aktif + booked_slots: + type: integer + example: 4 + available_slots: + type: integer + example: 6 + # Dari Figma: "Estimasi nomor antrian: 06" — ditampilkan di card jadwal pasien + estimated_queue_number: + type: integer + description: "Estimasi nomor antrian yang akan didapat pasien jika booking sekarang (booked_slots + 1)" + example: 5 + is_available: + type: boolean + description: "true jika available_slots > 0 dan tanggal belum lampau" + example: true + status: + type: string + enum: [active, cancelled, completed] + example: "active" + is_override: + type: boolean + description: "true jika admin mengubah data dari template asli" + example: false + override_note: + type: string + nullable: true + description: "Catatan alasan override oleh admin" + example: null + +# Request body untuk Admin override instance +# Digunakan PATCH /schedule-instances/{id} +ScheduleInstanceRequest: + type: object + properties: + start_time: + type: string + description: "Format HH:MM — override jam mulai" + example: "09:00" + end_time: + type: string + description: "Format HH:MM — override jam selesai" + example: "13:00" + max_patients: + type: integer + minimum: 1 + example: 8 + status: + type: string + enum: [active, cancelled] + description: "cancelled = batalkan instance ini (pasien booked akan dinotif)" + example: "active" + override_note: + type: string + nullable: true + description: "Alasan perubahan — ditampilkan di notifikasi pasien terdampak" + example: "Jam berubah karena acara rapat rumah sakit"