MIF_E31231313/Website/s_gizi/app/Http/Controllers/Api/ConsultationController.php

1287 lines
52 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Api\Concerns\ResolvesApiUser;
use App\Http\Controllers\Controller;
use App\Models\Child;
use App\Models\ConsultationMessage;
use App\Models\ConsultationRoom;
use App\Models\Measurement;
use App\Models\Nutritionist;
use App\Models\NutritionistNote;
use App\Helpers\NutritionStatusHelper;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Schema;
class ConsultationController extends Controller
{
use ResolvesApiUser;
public function nutritionists(Request $request): JsonResponse
{
$user = $this->userFromToken($request);
if (!$user) {
return $this->unauthenticated();
}
$rows = Nutritionist::query()
->with('user')
->where('is_available', true)
->whereHas('user', function ($query) {
$query->whereIn('role', ['ahli_gizi', 'nutritionist', 'ahli gizi'])
->where(function ($nested) {
$nested->where('status', 'aktif')
->orWhereNull('status');
})
->where(function ($nested) {
$nested->where('account_status', 'Aktif')
->orWhereNull('account_status');
});
})
->get()
->map(function (Nutritionist $nutritionist): ?array {
$name = (string) ($nutritionist->user?->name ?? 'Ahli Gizi');
$expertId = trim((string) ($nutritionist->expert_id ?? ''));
if ($expertId === '') {
$expertId = 'expert-'.$nutritionist->user_id;
}
$consultationCount = ConsultationRoom::query()
->where('expert_id', $expertId)
->where('status', 'active')
->count();
if ($consultationCount >= (int) ($nutritionist->max_consultation ?: 25)) {
return null;
}
return [
'id' => $nutritionist->id,
'expert_id' => $expertId,
'name' => trim($name.($nutritionist->title ? ', '.$nutritionist->title : '')),
'specialization' => (string) ($nutritionist->specialization ?? 'Spesialis Gizi Anak'),
'focus_tag' => $this->focusTag($nutritionist->specialization),
'experience' => (string) (($nutritionist->experience_years ?? 0).' tahun pengalaman'),
'bio' => (string) ($nutritionist->bio ?? ''),
'str_sip' => (string) ($nutritionist->str_sip ?? ''),
'is_online' => (bool) ($nutritionist->user?->last_active_at?->gte(now()->subMinutes(5))),
'is_available' => (bool) $nutritionist->is_available,
'max_consultation' => (int) ($nutritionist->max_consultation ?: 25),
'consultation_count' => $consultationCount,
'photo_url' => $nutritionist->user?->avatar ? asset($nutritionist->user->avatar) : '',
];
})
->filter()
->sortBy([
['is_online', 'desc'],
['consultation_count', 'asc'],
])
->values();
return response()->json(['data' => $rows]);
}
public function nutritionistDashboard(Request $request): JsonResponse
{
$user = $this->userFromToken($request);
if (!$user) {
return $this->unauthenticated();
}
$role = mb_strtolower(trim((string) ($user->role ?? '')));
if (!in_array($role, ['nutritionist', 'ahli_gizi', 'ahli gizi'], true)) {
return response()->json(['message' => 'Akses khusus ahli gizi.'], 403);
}
$nutritionist = $user->nutritionist;
if (!$nutritionist) {
return response()->json(['message' => 'Data ahli gizi belum tersedia.'], 404);
}
$expertId = $this->resolveNutritionistExpertId($nutritionist, $user);
$rooms = $this->nutritionistRoomsQuery($expertId)
->where('status', 'active')
->latest('last_message_at')
->latest('updated_at')
->get();
$roomPayload = $rooms->map(fn (ConsultationRoom $room): array => $this->roomPayload($room))->values();
$riskRooms = $roomPayload->filter(fn (array $room): bool => $room['risk'] !== 'normal')->values();
$activities = $rooms->take(8)->map(function (ConsultationRoom $room): array {
$measurements = $room->child?->measurements()->orderBy('tanggal_ukur')->get() ?? collect();
$latest = $measurements->last();
$previous = $measurements->count() > 1 ? $measurements->slice(-2, 1)->first() : null;
$statusChanged = $latest && $previous && $latest->status_gabungan !== $previous->status_gabungan;
return [
'title' => $statusChanged
? 'Status anak berubah'
: ($latest ? 'Pengukuran baru diterima' : 'Konsultasi diperbarui'),
'description' => trim(($room->child?->nama ?? 'Anak').' - '.($room->last_message ?? 'Room konsultasi aktif')),
'time' => optional($room->last_message_at ?? $room->updated_at)->toISOString(),
];
})->values();
return response()->json([
'profile' => [
'name' => $user->name,
'phone' => $user->phone,
'email' => $user->email,
'gender' => $user->parent_gender,
'profile_image' => $user->avatar ? asset($user->avatar) : '',
'nutritionist' => [
'id' => $nutritionist->id,
'expert_id' => $expertId,
'title' => $nutritionist->title,
'specialization' => $nutritionist->specialization,
'experience' => $nutritionist->experience,
'experience_years' => $nutritionist->experience_years,
'bio' => $nutritionist->bio,
'str_sip' => $nutritionist->str_sip,
'is_online' => (bool) $nutritionist->is_online,
'is_available' => (bool) $nutritionist->is_available,
'max_consultation' => (int) ($nutritionist->max_consultation ?: 25),
],
],
'stats' => [
'active_consultations' => $rooms->count(),
'monitored_children' => $rooms->pluck('child_id')->unique()->count(),
'risk_children' => $riskRooms->count(),
'unanswered' => $roomPayload->where('unread_count', '>', 0)->count(),
'need_monitoring' => $roomPayload->where('monitoring_status', 'perlu_dipantau')->count(),
'need_remeasure' => $roomPayload->where('validation_status', 'perlu_ukur_ulang')->count(),
],
'rooms' => $roomPayload,
'risk_children' => $riskRooms,
'activities' => $activities,
]);
}
public function nutritionistRooms(Request $request): JsonResponse
{
$user = $this->userFromToken($request);
if (!$user) {
return $this->unauthenticated();
}
$nutritionist = $user->nutritionist;
if (!$nutritionist) {
return response()->json(['message' => 'Data ahli gizi belum tersedia.'], 404);
}
$expertId = $this->resolveNutritionistExpertId($nutritionist, $user);
$rooms = $this->nutritionistRoomsQuery($expertId)
->where('status', 'active')
->latest('last_message_at')
->latest('updated_at')
->get()
->map(fn (ConsultationRoom $room): array => $this->roomPayload($room))
->values();
return response()->json(['data' => $rooms]);
}
public function nutritionistNotifications(Request $request): JsonResponse
{
$user = $this->userFromToken($request);
if (!$user) {
return $this->unauthenticated();
}
$nutritionist = $user->nutritionist;
if (!$nutritionist) {
return response()->json(['message' => 'Data ahli gizi belum tersedia.'], 404);
}
$expertId = $this->resolveNutritionistExpertId($nutritionist, $user);
$rooms = $this->nutritionistRoomsQuery($expertId)
->where('status', 'active')
->latest('last_message_at')
->latest('updated_at')
->get();
$items = [];
foreach ($rooms as $room) {
$payload = $this->roomPayload($room);
if (($payload['unread_count'] ?? 0) > 0) {
$items[] = [
'id' => 'message-'.$room->id,
'type' => 'message',
'title' => 'Pesan Baru',
'description' => $payload['parent_name'].' mengirim pesan pada konsultasi '.$payload['child_name'].'.',
'child_name' => $payload['child_name'],
'priority' => 'Sedang',
'time' => $payload['last_message_at'],
'is_read' => false,
];
}
if (($payload['validation_status'] ?? '') === 'perlu_ukur_ulang') {
$items[] = [
'id' => 'remeasure-'.$room->id,
'type' => 'remeasure',
'title' => 'Perlu Ukur Ulang',
'description' => $payload['validation_note'] ?: 'Data pengukuran perlu dicek ulang.',
'child_name' => $payload['child_name'],
'priority' => 'Sedang',
'time' => $payload['last_message_at'],
'is_read' => false,
];
}
if (($payload['monitoring_status'] ?? '') === 'perlu_dipantau') {
$items[] = [
'id' => 'monitor-'.$room->id,
'type' => 'monitor',
'title' => 'Perlu Dipantau',
'description' => $payload['validation_note'] ?: 'Anak perlu dipantau melalui konsultasi gizi.',
'child_name' => $payload['child_name'],
'priority' => 'Tinggi',
'time' => $payload['last_message_at'],
'is_read' => false,
];
}
}
return response()->json(['data' => array_values($items)]);
}
public function nutritionistProfile(Request $request): JsonResponse
{
$user = $this->userFromToken($request);
if (!$user) {
return $this->unauthenticated();
}
$nutritionist = $user->nutritionist;
if (!$nutritionist) {
return response()->json(['message' => 'Data ahli gizi belum tersedia.'], 404);
}
return response()->json(['data' => $this->nutritionistProfilePayload($nutritionist, $user)]);
}
public function updateNutritionistProfileStatus(Request $request): JsonResponse
{
$user = $this->userFromToken($request);
if (!$user) {
return $this->unauthenticated();
}
$nutritionist = $user->nutritionist;
if (!$nutritionist) {
return response()->json(['message' => 'Data ahli gizi belum tersedia.'], 404);
}
$validated = $request->validate([
'is_active' => ['required', 'boolean'],
]);
$nutritionist->update(['is_available' => (bool) $validated['is_active']]);
return response()->json(['data' => $this->nutritionistProfilePayload($nutritionist->fresh(), $user)]);
}
public function nutritionistChildren(Request $request): JsonResponse
{
$user = $this->userFromToken($request);
if (!$user) {
return $this->unauthenticated();
}
$nutritionist = $user->nutritionist;
if (!$nutritionist) {
return response()->json(['message' => 'Data ahli gizi belum tersedia.'], 404);
}
$search = trim((string) $request->query('search', ''));
$filter = mb_strtolower(trim((string) $request->query('filter', 'all')));
$expertId = $this->resolveNutritionistExpertId($nutritionist, $user);
$rooms = $this->nutritionistRoomsQuery($expertId)
->latest('last_message_at')
->latest('updated_at')
->get()
->unique('child_id')
->values();
$children = $rooms->map(fn (ConsultationRoom $room): array => $this->nutritionistChildPayload($room))
->filter(function (array $row) use ($search, $filter): bool {
if ($search !== '') {
$haystack = mb_strtolower(implode(' ', [
$row['name'] ?? '',
$row['parent_name'] ?? '',
$row['parent_phone'] ?? '',
]));
if (!str_contains($haystack, mb_strtolower($search))) {
return false;
}
}
if ($filter === 'high_risk') {
return ($row['risk_status'] ?? '') === 'Risiko Tinggi';
}
if ($filter === 'anomaly') {
return (bool) ($row['is_anomaly'] ?? false);
}
if ($filter === 'normal') {
return ($row['risk_status'] ?? '') === 'Stabil';
}
return true;
})
->values();
return response()->json([
'data' => [
'summary' => [
'total' => $children->count(),
'high_risk' => $children->where('risk_status', 'Risiko Tinggi')->count(),
'anomaly' => $children->where('is_anomaly', true)->count(),
'normal' => $children->where('risk_status', 'Stabil')->count(),
],
'children' => $children,
],
]);
}
public function nutritionistChildDetail(Request $request, int $childId): JsonResponse
{
$user = $this->userFromToken($request);
if (!$user) {
return $this->unauthenticated();
}
$nutritionist = $user->nutritionist;
if (!$nutritionist) {
return response()->json(['message' => 'Data ahli gizi belum tersedia.'], 404);
}
$expertId = $this->resolveNutritionistExpertId($nutritionist, $user);
$room = $this->nutritionistRoomsQuery($expertId)
->where('child_id', $childId)
->latest('last_message_at')
->latest('updated_at')
->first();
if (!$room || !$room->child) {
return response()->json(['message' => 'Data anak tidak ditemukan.'], 404);
}
return response()->json(['data' => $this->nutritionistChildDetailPayload($room)]);
}
public function nutritionistChildDetailFromChat(Request $request, int $roomId): JsonResponse
{
$user = $this->userFromToken($request);
if (!$user) {
return $this->unauthenticated();
}
$nutritionist = $user->nutritionist;
if (!$nutritionist) {
return response()->json(['message' => 'Data ahli gizi belum tersedia.'], 404);
}
$expertId = $this->resolveNutritionistExpertId($nutritionist, $user);
$room = $this->nutritionistRoomsQuery($expertId)->where('id', $roomId)->first();
if (!$room || !$room->child) {
return response()->json(['message' => 'Room tidak ditemukan.'], 404);
}
$payload = $this->nutritionistChildDetailPayload($room);
$payload['notes'] = NutritionistNote::query()
->where('nutritionist_id', $nutritionist->id)
->where('consultation_room_id', $room->id)
->latest()
->get()
->map(fn (NutritionistNote $note): array => [
'id' => $note->id,
'child_id' => $note->child_id,
'note' => (string) $note->note,
'created_at' => optional($note->created_at)->toISOString(),
])
->values();
return response()->json(['data' => $payload]);
}
public function storeNutritionistNote(Request $request, int $roomId): JsonResponse
{
$user = $this->userFromToken($request);
if (!$user) {
return $this->unauthenticated();
}
$nutritionist = $user->nutritionist;
if (!$nutritionist) {
return response()->json(['message' => 'Data ahli gizi belum tersedia.'], 404);
}
$expertId = $this->resolveNutritionistExpertId($nutritionist, $user);
$room = $this->nutritionistRoomsQuery($expertId)->where('id', $roomId)->first();
if (!$room || !$room->child_id) {
return response()->json(['message' => 'Room tidak ditemukan.'], 404);
}
$validated = $request->validate([
'category' => ['nullable', 'string', 'max:80'],
'note' => ['required', 'string', 'max:2000'],
]);
$note = NutritionistNote::query()->create([
'consultation_room_id' => $room->id,
'nutritionist_id' => $nutritionist->id,
'child_id' => $room->child_id,
'category' => $validated['category'] ?? 'Catatan',
'note' => $validated['note'],
]);
return response()->json([
'data' => [
'id' => $note->id,
'child_id' => $note->child_id,
'note' => (string) $note->note,
'created_at' => optional($note->created_at)->toISOString(),
],
], 201);
}
public function markNutritionistNotificationRead(Request $request, int|string $id): JsonResponse
{
return response()->json(['message' => 'Notifikasi ditandai sudah dibaca.']);
}
public function markAllNutritionistNotificationsRead(Request $request): JsonResponse
{
return response()->json(['message' => 'Semua notifikasi ditandai sudah dibaca.']);
}
public function nutritionistMessages(Request $request, int $roomId): JsonResponse
{
$user = $this->userFromToken($request);
if (!$user) {
return $this->unauthenticated();
}
$nutritionist = $user->nutritionist;
if (!$nutritionist) {
return response()->json(['message' => 'Data ahli gizi belum tersedia.'], 404);
}
$expertId = $this->resolveNutritionistExpertId($nutritionist, $user);
$room = $this->nutritionistRoomsQuery($expertId)->where('id', $roomId)->first();
if (!$room) {
return response()->json(['message' => 'Room tidak ditemukan.'], 404);
}
ConsultationMessage::query()
->where('room_id', $room->id)
->where('sender_type', 'parent')
->where('is_read', false)
->update(['is_read' => true]);
$messages = ConsultationMessage::query()
->where('room_id', $room->id)
->orderBy('created_at')
->get()
->map(fn (ConsultationMessage $m): array => $this->messagePayload($m))
->values();
return response()->json([
'room' => $this->roomPayload($room->fresh(['user', 'child.measurements'])),
'data' => $messages,
]);
}
public function nutritionistSendMessage(Request $request, int $roomId): JsonResponse
{
$user = $this->userFromToken($request);
if (!$user) {
return $this->unauthenticated();
}
$nutritionist = $user->nutritionist;
if (!$nutritionist) {
return response()->json(['message' => 'Data ahli gizi belum tersedia.'], 404);
}
$expertId = $this->resolveNutritionistExpertId($nutritionist, $user);
$room = $this->nutritionistRoomsQuery($expertId)->where('id', $roomId)->first();
if (!$room) {
return response()->json(['message' => 'Room tidak ditemukan.'], 404);
}
if (in_array((string) $room->status, ['closed', 'selesai', 'resolved'], true)) {
return response()->json(['message' => 'Konsultasi ini sudah selesai.'], 403);
}
if (Schema::hasColumn('consultation_rooms', 'started_by') && (string) ($room->started_by ?? 'parent') !== 'parent') {
return response()->json(['message' => 'Anda tidak memiliki akses ke konsultasi ini.'], 403);
}
$hasParentMessage = ConsultationMessage::query()
->where('room_id', $room->id)
->where('sender_type', 'parent')
->exists();
if (!$hasParentMessage) {
return response()->json(['message' => 'Chat belum tersedia. Orang tua belum memulai konsultasi.'], 403);
}
$validated = $request->validate([
'message' => ['required', 'string', 'max:3000'],
]);
$message = ConsultationMessage::query()->create([
'room_id' => $room->id,
'sender_type' => 'expert',
'message' => $validated['message'],
'is_read' => false,
]);
$room->update([
'status' => 'active',
'last_message' => $validated['message'],
'last_message_at' => now(),
'unread_count' => ((int) $room->unread_count) + 1,
]);
return response()->json(['data' => $this->messagePayload($message)]);
}
public function nutritionistClose(Request $request, int $roomId): JsonResponse
{
$user = $this->userFromToken($request);
if (!$user) {
return $this->unauthenticated();
}
$nutritionist = $user->nutritionist;
if (!$nutritionist) {
return response()->json(['message' => 'Data ahli gizi belum tersedia.'], 404);
}
$expertId = $this->resolveNutritionistExpertId($nutritionist, $user);
$room = $this->nutritionistRoomsQuery($expertId)->where('id', $roomId)->first();
if (!$room) {
return response()->json(['message' => 'Room tidak ditemukan.'], 404);
}
if (Schema::hasColumn('consultation_rooms', 'started_by') && (string) ($room->started_by ?? 'parent') !== 'parent') {
return response()->json(['message' => 'Anda tidak memiliki akses ke konsultasi ini.'], 403);
}
$room->update(['status' => 'closed']);
return response()->json(['message' => 'Konsultasi ditandai selesai.']);
}
public function rooms(Request $request): JsonResponse
{
$user = $this->userFromToken($request);
if (!$user) {
return $this->unauthenticated();
}
$childId = (int) $request->query('child_id', 0);
$query = ConsultationRoom::query()->where('user_id', $user->id);
if ($childId > 0) {
$query->where('child_id', $childId);
}
$rooms = $query->latest('last_message_at')->latest('updated_at')->get()
->map(function (ConsultationRoom $room): array {
$nutritionist = Nutritionist::query()
->with('user')
->where('expert_id', (string) $room->expert_id)
->first();
$expert = $this->nutritionistDisplayPayload($nutritionist);
return [
'id' => $room->id,
'child_id' => $room->child_id,
'child_name' => (string) optional($room->child)->nama,
'expert_id' => (string) $room->expert_id,
'expert_name' => $expert['name'] ?: (string) $room->expert_name,
'specialization' => $expert['specialization'] ?: (string) ($room->specialization ?? ''),
'asset_image' => $expert['asset_image'] ?: (string) ($room->asset_image ?? ''),
'online' => $expert['online'] ?? (bool) $room->online,
'status' => (string) $room->status,
'last_message' => (string) ($room->last_message ?? ''),
'last_message_at' => optional($room->last_message_at)->toISOString(),
'unread_count' => (int) $room->unread_count,
'last_shared_measurement_id' => $room->last_shared_measurement_id,
'updated_at' => optional($room->updated_at)->toISOString(),
];
})
->values();
return response()->json(['data' => $rooms]);
}
public function openRoom(Request $request): JsonResponse
{
$user = $this->userFromToken($request);
if (!$user) {
return $this->unauthenticated();
}
$validated = $request->validate([
'child_id' => ['required', 'integer'],
'expert_id' => ['required', 'string', 'max:80'],
'expert_name' => ['required', 'string', 'max:150'],
'specialization' => ['nullable', 'string', 'max:150'],
'asset_image' => ['nullable', 'string', 'max:255'],
'online' => ['nullable', 'boolean'],
]);
$child = Child::query()->where('id', $validated['child_id'])->where('user_id', $user->id)->first();
if (!$child) {
return response()->json(['message' => 'Anak tidak ditemukan.'], 404);
}
$nutritionist = Nutritionist::query()
->with('user')
->where('expert_id', $validated['expert_id'])
->first();
$activeCount = ConsultationRoom::query()
->where('expert_id', $validated['expert_id'])
->where('status', 'active')
->count();
if (! $nutritionist
|| ! $nutritionist->is_available
|| $activeCount >= (int) ($nutritionist->max_consultation ?: 25)
|| ($nutritionist->user?->status === 'nonaktif')
|| ($nutritionist->user?->account_status === 'Nonaktif')) {
return response()->json(['message' => 'Ahli gizi tidak tersedia untuk konsultasi baru.'], 422);
}
$roomDefaults = [
'expert_name' => $validated['expert_name'],
'specialization' => $validated['specialization'] ?? '',
'asset_image' => $validated['asset_image'] ?? '',
'online' => (bool) ($validated['online'] ?? false),
'status' => 'pending',
'unread_count' => 0,
];
if (Schema::hasColumn('consultation_rooms', 'started_by')) {
$roomDefaults['started_by'] = 'parent';
}
$room = ConsultationRoom::query()->firstOrCreate(
[
'user_id' => $user->id,
'child_id' => $child->id,
'expert_id' => $validated['expert_id'],
],
$roomDefaults
);
$expert = $this->nutritionistDisplayPayload($nutritionist);
return response()->json([
'data' => [
'id' => $room->id,
'child_id' => $room->child_id,
'child_name' => (string) $child->nama,
'expert_id' => (string) $room->expert_id,
'expert_name' => $expert['name'] ?: (string) $room->expert_name,
'specialization' => $expert['specialization'] ?: (string) ($room->specialization ?? ''),
'asset_image' => $expert['asset_image'] ?: (string) ($room->asset_image ?? ''),
'online' => $expert['online'] ?? (bool) $room->online,
'status' => (string) $room->status,
'last_message' => (string) ($room->last_message ?? ''),
'last_message_at' => optional($room->last_message_at)->toISOString(),
'last_shared_measurement_id' => $room->last_shared_measurement_id,
'updated_at' => optional($room->updated_at)->toISOString(),
],
]);
}
public function messages(Request $request, int $roomId): JsonResponse
{
$user = $this->userFromToken($request);
if (!$user) {
return $this->unauthenticated();
}
$room = ConsultationRoom::query()->where('id', $roomId)->where('user_id', $user->id)->first();
if (!$room) {
return response()->json(['message' => 'Room tidak ditemukan.'], 404);
}
$messages = ConsultationMessage::query()
->where('room_id', $room->id)
->orderBy('created_at')
->get()
->map(fn (ConsultationMessage $m): array => [
'id' => $m->id,
'sender_type' => (string) $m->sender_type,
'message' => (string) $m->message,
'is_read' => (bool) $m->is_read,
'created_at' => optional($m->created_at)->toISOString(),
])
->values();
// Saat parent membuka room, tandai pesan dari ahli sebagai sudah dibaca.
ConsultationMessage::query()
->where('room_id', $room->id)
->where('sender_type', 'expert')
->where('is_read', false)
->update(['is_read' => true]);
if ($room->unread_count > 0) {
$room->update(['unread_count' => 0]);
}
return response()->json(['data' => $messages]);
}
public function sendMessage(Request $request, int $roomId): JsonResponse
{
// PRESENTASI TA: Pesan orang tua disimpan ke room konsultasi dan dibedakan lewat sender_type.
$user = $this->userFromToken($request);
if (!$user) {
return $this->unauthenticated();
}
$room = ConsultationRoom::query()->where('id', $roomId)->where('user_id', $user->id)->first();
if (!$room) {
return response()->json(['message' => 'Room tidak ditemukan.'], 404);
}
$validated = $request->validate([
'message' => ['required', 'string', 'max:3000'],
'measurement_id' => ['nullable', 'integer'],
]);
$isFirstParentMessage = !ConsultationMessage::query()
->where('room_id', $room->id)
->where('sender_type', 'parent')
->exists();
$message = ConsultationMessage::query()->create([
'room_id' => $room->id,
'sender_type' => 'parent',
'message' => $validated['message'],
'is_read' => false,
]);
$roomUpdate = [
'status' => 'active',
'last_message' => $validated['message'],
'last_message_at' => now(),
];
if (!empty($validated['measurement_id'])) {
$roomUpdate['last_shared_measurement_id'] = $validated['measurement_id'];
}
$room->update($roomUpdate);
if ($isFirstParentMessage) {
$analysis = $this->initialAnalysisMessage($room->fresh(['child.measurements']));
if ($analysis !== null) {
ConsultationMessage::query()->create([
'room_id' => $room->id,
'sender_type' => 'system',
'message' => $analysis,
'is_read' => true,
]);
}
}
return response()->json([
'data' => [
'id' => $message->id,
'sender_type' => (string) $message->sender_type,
'message' => (string) $message->message,
'is_read' => (bool) $message->is_read,
'created_at' => optional($message->created_at)->toISOString(),
],
]);
}
public function updateStatus(Request $request, int $roomId): JsonResponse
{
$user = $this->userFromToken($request);
if (!$user) {
return $this->unauthenticated();
}
$room = ConsultationRoom::query()->where('id', $roomId)->where('user_id', $user->id)->first();
if (!$room) {
return response()->json(['message' => 'Room tidak ditemukan.'], 404);
}
$validated = $request->validate([
'status' => ['required', 'in:pending,active,closed,aktif,menunggu,selesai'],
]);
$room->update(['status' => $this->normalizeRoomStatus($validated['status'])]);
return response()->json(['message' => 'Status berhasil diperbarui.']);
}
public function markMeasurementShared(Request $request, int $roomId): JsonResponse
{
$user = $this->userFromToken($request);
if (!$user) {
return $this->unauthenticated();
}
$room = ConsultationRoom::query()->where('id', $roomId)->where('user_id', $user->id)->first();
if (!$room) {
return response()->json(['message' => 'Room tidak ditemukan.'], 404);
}
$validated = $request->validate([
'measurement_id' => ['required', 'integer'],
]);
$room->update([
'last_shared_measurement_id' => $validated['measurement_id'],
]);
return response()->json([
'message' => 'Update perkembangan berhasil ditandai sudah dikirim.',
'last_shared_measurement_id' => $room->last_shared_measurement_id,
]);
}
public function sendExpertReply(Request $request, int $roomId): JsonResponse
{
// PRESENTASI TA: Balasan ahli gizi disimpan dan menambah penghitung pesan belum dibaca.
$user = $this->userFromToken($request);
if (!$user) {
return $this->unauthenticated();
}
$room = ConsultationRoom::query()->where('id', $roomId)->where('user_id', $user->id)->first();
if (!$room) {
return response()->json(['message' => 'Room tidak ditemukan.'], 404);
}
$validated = $request->validate([
'message' => ['required', 'string', 'max:3000'],
]);
$message = ConsultationMessage::query()->create([
'room_id' => $room->id,
'sender_type' => 'expert',
'message' => $validated['message'],
'is_read' => false,
]);
$room->update([
'status' => 'active',
'last_message' => $validated['message'],
'last_message_at' => now(),
'unread_count' => ((int) $room->unread_count) + 1,
]);
return response()->json([
'data' => [
'id' => $message->id,
'sender_type' => (string) $message->sender_type,
'message' => (string) $message->message,
'is_read' => (bool) $message->is_read,
'created_at' => optional($message->created_at)->toISOString(),
],
]);
}
private function nutritionistProfilePayload(?Nutritionist $nutritionist, $user): array
{
return [
'id' => (int) ($nutritionist?->id ?? 0),
'name' => (string) ($user?->name ?? 'Ahli Gizi'),
'phone' => (string) ($user?->phone ?? '-'),
'email' => (string) ($user?->email ?? '-'),
'profession' => trim((string) ($nutritionist?->title ?: 'Ahli Gizi')),
'workplace' => (string) ($nutritionist?->specialization ?: 'S-Gizi'),
'photo' => $user?->avatar ? asset($user->avatar) : null,
'is_active' => (bool) ($nutritionist?->is_available ?? true),
];
}
private function nutritionistChildPayload(ConsultationRoom $room): array
{
$child = $room->child;
$parent = $room->user;
$latest = $child?->measurements()->latest('tanggal_ukur')->latest('id')->first();
$status = NutritionStatusHelper::getStatus($latest, $latest?->status_gabungan);
return [
'id' => (int) ($child?->id ?? 0),
'name' => (string) ($child?->nama ?? $child?->nama_anak ?? 'Anak'),
'age_text' => $child?->tanggal_lahir ? $this->ageLabel($child->tanggal_lahir) : '-',
'gender' => $this->genderLabel((string) ($child?->jenis_kelamin ?? '')),
'parent_name' => (string) ($parent?->name ?? 'Orang Tua'),
'parent_phone' => (string) ($parent?->phone ?? '-'),
'risk_status' => NutritionStatusHelper::riskLabel($status),
'bbu_status' => NutritionStatusHelper::bbuStatus($latest),
'tbu_status' => NutritionStatusHelper::tbuStatus($latest),
'bbtb_status' => NutritionStatusHelper::bbtbStatus($latest),
'weight_kg' => (float) ($latest?->berat ?? 0),
'height_cm' => (float) ($latest?->tinggi ?? 0),
'zscore_tbu' => $latest?->z_tbu !== null ? (float) $latest->z_tbu : 0,
'last_measurement_date' => optional($latest?->tanggal_ukur)->toDateString() ?: '-',
'is_anomaly' => (bool) ($latest?->is_anomaly ?? false),
'has_consultation' => true,
'consultation_id' => (int) $room->id,
];
}
private function nutritionistChildDetailPayload(ConsultationRoom $room): array
{
$child = $room->child;
$latest = $child?->measurements()->latest('tanggal_ukur')->latest('id')->first();
$histories = $child?->measurements()->latest('tanggal_ukur')->latest('id')->take(8)->get() ?? collect();
$status = NutritionStatusHelper::getStatus($latest, $latest?->status_gabungan);
return [
'id' => (int) ($child?->id ?? 0),
'name' => (string) ($child?->nama ?? $child?->nama_anak ?? 'Anak'),
'age_text' => $child?->tanggal_lahir ? $this->ageLabel($child->tanggal_lahir) : '-',
'gender' => $this->genderLabel((string) ($child?->jenis_kelamin ?? '')),
'birth_date' => optional($child?->tanggal_lahir)->toDateString() ?: '-',
'parent_name' => (string) ($room->user?->name ?? 'Orang Tua'),
'parent_phone' => (string) ($room->user?->phone ?? '-'),
'risk_status' => NutritionStatusHelper::riskLabel($status),
'latest_measurement' => [
'measurement_id' => (int) ($latest?->id ?? 0),
'measurement_date' => optional($latest?->tanggal_ukur)->toDateString() ?: '-',
'age_at_measurement' => $latest?->umur_bulan !== null ? ((int) round((float) $latest->umur_bulan)).' Bulan' : '-',
'weight_kg' => (float) ($latest?->berat ?? 0),
'height_cm' => (float) ($latest?->tinggi ?? 0),
'position' => (string) ($latest?->cara_ukur ?? '-'),
],
'zscore_result' => [
'bbu_score' => $latest?->z_bbu !== null ? (float) $latest->z_bbu : 0,
'bbu_status' => NutritionStatusHelper::bbuStatus($latest),
'tbu_score' => $latest?->z_tbu !== null ? (float) $latest->z_tbu : 0,
'tbu_status' => NutritionStatusHelper::tbuStatus($latest),
'bbtb_score' => $latest?->z_bbtb !== null ? (float) $latest->z_bbtb : 0,
'bbtb_status' => NutritionStatusHelper::bbtbStatus($latest),
],
'interpretation' => $this->childInterpretation($status, $latest),
'short_histories' => $histories->map(fn (Measurement $measurement): array => [
'date' => optional($measurement->tanggal_ukur)->toDateString() ?: '-',
'weight_kg' => (float) $measurement->berat,
'height_cm' => (float) $measurement->tinggi,
'risk_status' => NutritionStatusHelper::riskLabel(NutritionStatusHelper::getStatus($measurement, $measurement->status_gabungan)),
])->values(),
'has_consultation' => true,
'consultation_id' => (int) $room->id,
'notes' => [],
];
}
private function childInterpretation(string $status, ?Measurement $measurement): string
{
if (!$measurement) {
return 'Belum ada pengukuran terbaru untuk anak ini.';
}
return match (NutritionStatusHelper::riskLabel($status)) {
'Risiko Tinggi' => 'Anak perlu pemantauan intensif dan tindak lanjut konsultasi gizi.',
'Perlu Pantau' => 'Anak perlu dipantau berkala sesuai hasil pengukuran terbaru.',
'Stabil' => 'Status anak stabil berdasarkan pengukuran terbaru.',
default => 'Lengkapi pengukuran untuk melihat interpretasi status gizi.',
};
}
private function genderLabel(string $gender): string
{
return match (mb_strtoupper(trim($gender))) {
'L' => 'Laki-laki',
'P' => 'Perempuan',
default => $gender !== '' ? $gender : '-',
};
}
private function nutritionistRoomsQuery(string $expertId)
{
return ConsultationRoom::query()
->with(['user', 'child.measurements'])
->where('expert_id', $expertId);
}
private function resolveNutritionistExpertId($nutritionist, $user): string
{
$expertId = Schema::hasColumn('nutritionists', 'expert_id')
? trim((string) ($nutritionist->expert_id ?? ''))
: '';
if ($expertId !== '') {
return $expertId;
}
$expertId = ConsultationRoom::query()
->where('expert_name', (string) ($user->name ?? ''))
->whereNotNull('expert_id')
->value('expert_id');
$expertId = trim((string) ($expertId ?: 'expert-'.$user->id));
if (Schema::hasColumn('nutritionists', 'expert_id')) {
$nutritionist->forceFill(['expert_id' => $expertId])->save();
}
return $expertId;
}
private function roomPayload(ConsultationRoom $room): array
{
$child = $room->child;
$parent = $room->user;
$measurements = $child?->measurements()
->orderBy('tanggal_ukur')
->get() ?? collect();
$latest = $measurements->last();
$previous = $measurements->count() > 1 ? $measurements->slice(-2, 1)->first() : null;
$status = (string) ($latest?->status_gabungan ?? 'Belum Diukur');
$risk = $this->riskLevel($status, $latest, $previous);
return [
'id' => $room->id,
'parent_id' => $room->user_id,
'parent_name' => (string) ($parent?->name ?? 'Orang Tua'),
'child_id' => $room->child_id,
'child_name' => (string) ($child?->nama ?? $child?->nama_anak ?? 'Anak'),
'child_age' => $child?->tanggal_lahir ? $this->ageLabel($child->tanggal_lahir) : '-',
'child_gender' => (string) ($child?->jenis_kelamin ?? '-'),
'status' => $status,
'risk' => $risk,
'last_message' => (string) ($room->last_message ?? ''),
'last_message_at' => optional($room->last_message_at ?? $room->updated_at)->toISOString(),
'unread_count' => ConsultationMessage::query()
->where('room_id', $room->id)
->where('sender_type', 'parent')
->where('is_read', false)
->count(),
'room_status' => (string) $room->status,
'latest_measurement' => $latest ? $this->measurementPayload($latest) : null,
'previous_measurement' => $previous ? $this->measurementPayload($previous) : null,
'weights' => $measurements->pluck('berat')->map(fn ($v) => (float) $v)->values(),
'heights' => $measurements->pluck('tinggi')->map(fn ($v) => (float) $v)->values(),
'z_bbu_scores' => $measurements->pluck('z_bbu')->map(fn ($v) => $v !== null ? (float) $v : null)->values(),
'z_scores' => $measurements->pluck('z_tbu')->map(fn ($v) => (float) $v)->values(),
'z_bbtb_scores' => $measurements->pluck('z_bbtb')->map(fn ($v) => $v !== null ? (float) $v : null)->values(),
'measurement_dates' => $measurements->map(fn (Measurement $measurement) => optional($measurement->tanggal_ukur)->toDateString())->values(),
'measurement_statuses' => $measurements->map(fn (Measurement $measurement) => (string) ($measurement->status_gabungan ?? ''))->values(),
'weight_delta' => $latest && $previous ? (float) $latest->berat - (float) $previous->berat : null,
'height_delta' => $latest && $previous ? (float) $latest->tinggi - (float) $previous->tinggi : null,
'risk_reasons' => $this->riskReasons($status, $latest, $previous),
'is_anomaly' => (bool) ($latest?->is_anomaly ?? false),
'validation_status' => (string) ($latest?->validation_status ?? 'valid'),
'validation_note' => (string) ($latest?->validation_note ?? ''),
'monitoring_status' => (string) ($latest?->monitoring_status ?? 'normal'),
'is_confirmed_by_parent' => (bool) ($latest?->is_confirmed_by_parent ?? false),
];
}
private function nutritionistDisplayPayload(?Nutritionist $nutritionist): array
{
if (! $nutritionist) {
return [
'name' => '',
'specialization' => '',
'asset_image' => '',
'online' => null,
];
}
$name = trim((string) ($nutritionist->user?->name ?? ''));
$title = trim((string) ($nutritionist->title ?? ''));
if ($name !== '' && $title !== '') {
$name = trim($name.', '.$title);
}
return [
'name' => $name,
'specialization' => (string) ($nutritionist->specialization ?? ''),
'asset_image' => $nutritionist->user?->avatar ? asset($nutritionist->user->avatar) : '',
'online' => (bool) ($nutritionist->user?->last_active_at?->gte(now()->subMinutes(5))),
];
}
private function messagePayload(ConsultationMessage $message): array
{
return [
'id' => $message->id,
'sender_type' => (string) $message->sender_type,
'message' => (string) $message->message,
'is_read' => (bool) $message->is_read,
'created_at' => optional($message->created_at)->toISOString(),
];
}
private function normalizeRoomStatus(string $status): string
{
return match (mb_strtolower(trim($status))) {
'aktif', 'menunggu', 'active' => 'active',
'selesai', 'closed' => 'closed',
default => 'pending',
};
}
private function focusTag(?string $specialization): string
{
$value = mb_strtolower((string) $specialization);
if (str_contains($value, 'stunting') || str_contains($value, 'tumbuh') || str_contains($value, 'tinggi')) {
return 'growth';
}
if (str_contains($value, 'berat') || str_contains($value, 'gizi kurang') || str_contains($value, 'underweight')) {
return 'weight';
}
if (str_contains($value, 'obes') || str_contains($value, 'lebih')) {
return 'obesity';
}
return 'general';
}
private function initialAnalysisMessage(?ConsultationRoom $room): ?string
{
$measurement = $room?->child?->measurements()
->orderByDesc('tanggal_ukur')
->first();
if (!$measurement) {
return null;
}
return implode("\n", [
'Hasil Analisis Anak',
'Berat Badan: '.number_format((float) $measurement->berat, 1).' kg',
'Tinggi Badan: '.number_format((float) $measurement->tinggi, 0).' cm',
'Status WHO: '.(string) ($measurement->status_gabungan ?? 'Belum Diukur'),
'Z-score BB/U: '.($measurement->z_bbu !== null ? number_format((float) $measurement->z_bbu, 1) : '-'),
'Z-score TB/U: '.($measurement->z_tbu !== null ? number_format((float) $measurement->z_tbu, 1) : '-'),
'Z-score BB/TB: '.($measurement->z_bbtb !== null ? number_format((float) $measurement->z_bbtb, 1) : '-'),
'Status Validasi: '.$this->validationLabel((string) ($measurement->validation_status ?? 'valid')),
'Status Pemantauan: '.$this->monitoringLabel((string) ($measurement->monitoring_status ?? 'normal')),
'Catatan: '.((string) ($measurement->validation_note ?? '') ?: '-'),
'Tanggal Pengukuran: '.optional($measurement->tanggal_ukur)->toDateString(),
]);
}
private function validationLabel(string $status): string
{
return match ($status) {
'perlu_ukur_ulang' => 'Perlu Ukur Ulang',
default => 'Valid',
};
}
private function monitoringLabel(string $status): string
{
return match ($status) {
'perlu_dipantau' => 'Perlu Dipantau',
default => 'Normal',
};
}
private function measurementPayload(Measurement $measurement): array
{
return [
'id' => $measurement->id,
'berat' => (float) $measurement->berat,
'tinggi' => (float) $measurement->tinggi,
'tanggal_ukur' => optional($measurement->tanggal_ukur)->toDateString(),
'status_gabungan' => (string) ($measurement->status_gabungan ?? ''),
'z_bbu' => $measurement->z_bbu !== null ? (float) $measurement->z_bbu : null,
'z_tbu' => $measurement->z_tbu !== null ? (float) $measurement->z_tbu : null,
'z_bbtb' => $measurement->z_bbtb !== null ? (float) $measurement->z_bbtb : null,
'is_anomaly' => (bool) $measurement->is_anomaly,
'validation_status' => (string) ($measurement->validation_status ?? 'valid'),
'validation_note' => (string) ($measurement->validation_note ?? ''),
'monitoring_status' => (string) ($measurement->monitoring_status ?? 'normal'),
'is_confirmed_by_parent' => (bool) ($measurement->is_confirmed_by_parent ?? false),
];
}
private function riskLevel(string $status, ?Measurement $measurement, ?Measurement $previous = null): string
{
$value = mb_strtolower($status);
if (str_contains($value, 'berat') || str_contains($value, 'severely') || str_contains($value, 'buruk')) {
return 'high';
}
if ($measurement && (($measurement->z_tbu ?? 0) <= -3 || ($measurement->z_bbu ?? 0) <= -3 || ($measurement->z_bbtb ?? 0) <= -3)) {
return 'high';
}
if (str_contains($value, 'stunting') || str_contains($value, 'kurang') || str_contains($value, 'under') || str_contains($value, 'rendah')) {
return 'warning';
}
if ($measurement && (($measurement->z_tbu ?? 0) <= -2 || ($measurement->z_bbu ?? 0) <= -2 || ($measurement->z_bbtb ?? 0) <= -2)) {
return 'warning';
}
if ($measurement && $previous) {
$weightDown = (float) $measurement->berat < (float) $previous->berat;
$heightStagnant = (float) $measurement->tinggi <= (float) $previous->tinggi;
$statusWorse = $this->statusSeverity($measurement->status_gabungan) > $this->statusSeverity($previous->status_gabungan);
if ($weightDown || $heightStagnant || $statusWorse) {
return 'warning';
}
}
return 'normal';
}
private function riskReasons(string $status, ?Measurement $measurement, ?Measurement $previous): array
{
$reasons = [];
$value = mb_strtolower($status);
if (str_contains($value, 'stunting')) {
$reasons[] = 'Stunting';
}
if (str_contains($value, 'kurang') || str_contains($value, 'under')) {
$reasons[] = 'Gizi kurang';
}
if ($measurement && (($measurement->z_tbu ?? 0) < -2 || ($measurement->z_bbu ?? 0) < -2 || ($measurement->z_bbtb ?? 0) < -2)) {
$reasons[] = 'Z-score < -2';
}
if ($measurement && $previous && (float) $measurement->berat < (float) $previous->berat) {
$reasons[] = 'BB turun';
}
if ($measurement && $previous && (float) $measurement->tinggi <= (float) $previous->tinggi) {
$reasons[] = 'TB stagnan';
}
if ($measurement && $previous && $this->statusSeverity($measurement->status_gabungan) > $this->statusSeverity($previous->status_gabungan)) {
$reasons[] = 'Status memburuk';
}
return array_values(array_unique($reasons));
}
private function statusSeverity(?string $status): int
{
$value = mb_strtolower((string) $status);
if (str_contains($value, 'buruk') || str_contains($value, 'severely')) {
return 3;
}
if (str_contains($value, 'stunting') || str_contains($value, 'kurang') || str_contains($value, 'under') || str_contains($value, 'rendah')) {
return 2;
}
if (str_contains($value, 'lebih') || str_contains($value, 'obes')) {
return 1;
}
return 0;
}
private function ageLabel($birthDate): string
{
$birth = \Carbon\Carbon::parse($birthDate);
$now = now();
$diff = $birth->diff($now);
$years = (int) $diff->y;
$months = (int) $diff->m;
$totalMonths = ($years * 12) + $months;
if ($years < 2) {
return max(0, $totalMonths).' Bulan';
}
if ($months <= 0) {
return $years.' Tahun';
}
return $years.' Tahun '.$months.' Bulan';
}
}