query('q', '')); $statusFilter = (string) $request->query('status', 'Semua'); $timeFilter = (string) $request->query('time', 'Semua'); $sort = (string) $request->query('sort', 'Terbaru diukur'); $children = Child::query() ->with(['user', 'measurements' => fn ($query) => $query->latest('tanggal_ukur')->latest('id')]) ->when($q !== '', function ($query) use ($q) { $query->where(function ($nested) use ($q) { $nested->where('nama', 'like', "%{$q}%") ->orWhereHas('user', fn ($user) => $user->where('name', 'like', "%{$q}%")); }); }) ->get(); $rows = $children->map(function (Child $child) { $latest = $child->measurements->first(); $previous = $child->measurements->skip(1)->first(); $status = NutritionStatusHelper::getStatus($latest); return [ 'child' => $child, 'latest' => $latest, 'previous' => $previous, 'status' => $status, 'risk' => NutritionStatusHelper::riskLabel($status), 'risk_score' => NutritionStatusHelper::riskScore($status), ]; }); $totalChildren = $rows->count(); $highRiskCount = $rows->where('risk', 'Risiko Tinggi')->count(); $unmeasuredCount = $rows->filter(fn ($row) => $row['latest'] === null)->count(); $todayMeasurements = $rows->filter(fn ($row) => $row['latest']?->tanggal_ukur?->isToday())->count(); $rows = $rows ->when($statusFilter !== 'Semua', fn ($collection) => $collection->filter(fn ($row) => $row['status'] === $statusFilter)) ->when($timeFilter !== 'Semua', function ($collection) use ($timeFilter) { return $collection->filter(function ($row) use ($timeFilter) { $date = $row['latest']?->tanggal_ukur; if (!$date) { return false; } return match ($timeFilter) { 'Hari Ini' => $date->isToday(), '7 Hari' => $date->gte(now()->subDays(7)), '30 Hari' => $date->gte(now()->subDays(30)), default => true, }; }); }); $rows = (match ($sort) { 'Terbaru diukur' => $rows->sortByDesc(fn ($row) => $row['latest']?->tanggal_ukur?->timestamp ?? 0), 'Belum diukur' => $rows->sortBy(fn ($row) => $row['latest'] ? 1 : 0), 'Nama anak' => $rows->sortBy(fn ($row) => $row['child']->nama), default => $rows->sortByDesc('risk_score'), })->values(); $perPage = 10; $page = LengthAwarePaginator::resolveCurrentPage(); $paginatedRows = new LengthAwarePaginator( $rows->forPage($page, $perPage)->values(), $rows->count(), $perPage, $page, [ 'path' => $request->url(), 'query' => $request->query(), ], ); return view('admin.monitoring', [ 'rows' => $paginatedRows, 'q' => $q, 'statusFilter' => $statusFilter, 'timeFilter' => $timeFilter, 'sort' => $sort, 'summary' => [ 'totalChildren' => $totalChildren, 'highRiskCount' => $highRiskCount, 'unmeasuredCount' => $unmeasuredCount, 'todayMeasurements' => $todayMeasurements, ], ]); } public function consultations(Request $request): View { $q = trim((string) $request->query('q', '')); $filter = (string) $request->query('filter', 'Semua'); $selectedRoomId = $request->integer('room'); $expertId = trim((string) $request->query('expert_id', '')); $expertName = trim((string) $request->query('expert_name', '')); $roomsQuery = ConsultationRoom::query() ->with([ 'user', 'child.user', 'child.measurements' => fn ($query) => $query->latest('tanggal_ukur')->latest('id')->take(8), ]) ->when($expertId !== '', fn ($query) => $query->where('expert_id', $expertId)) ->when($q !== '', function ($query) use ($q) { $query->where(function ($nested) use ($q) { $nested->where('last_message', 'like', "%{$q}%") ->orWhereHas('user', fn ($user) => $user->where('name', 'like', "%{$q}%")) ->orWhereHas('child', fn ($child) => $child->where('nama', 'like', "%{$q}%")); }); }) ->latest('last_message_at'); $rooms = $roomsQuery->get()->map(function (ConsultationRoom $room) { $latest = $room->child?->measurements?->first(); $status = NutritionStatusHelper::getStatus($latest); $room->monitoring_status = $status; $room->monitoring_risk = NutritionStatusHelper::riskLabel($status); $room->monitoring_risk_score = NutritionStatusHelper::riskScore($status); return $room; }); $rooms = match ($filter) { 'Belum Dibalas' => $rooms->filter(fn ($room) => (int) $room->unread_count > 0), 'Aktif' => $rooms->filter(fn ($room) => in_array($room->status, ['aktif', 'active', 'open', 'waiting'], true)), 'Risiko Tinggi' => $rooms->filter(fn ($room) => $room->monitoring_risk === 'Risiko Tinggi'), 'Hari Ini' => $rooms->filter(fn ($room) => $room->last_message_at?->isToday()), 'Selesai' => $rooms->filter(fn ($room) => in_array($room->status, ['selesai', 'closed', 'resolved'], true)), default => $rooms, }; $rooms = $rooms->values(); $selectedRoom = $selectedRoomId ? $rooms->firstWhere('id', $selectedRoomId) : $rooms->first(); if ($selectedRoom) { $selectedRoom->load([ 'messages' => fn ($query) => $query->oldest()->take(40), 'child.user', 'child.measurements' => fn ($query) => $query->latest('tanggal_ukur')->latest('id')->take(8), ]); } return view('admin.consultations', [ 'rooms' => $rooms, 'selectedRoom' => $selectedRoom, 'q' => $q, 'filter' => $filter, 'expertId' => $expertId, 'expertName' => $expertName, ]); } public function parents(): View { return $this->parentsIndex(request()); } public function parentsIndex(Request $request): View { $q = trim((string) $request->query('q', '')); $filter = (string) $request->query('filter', 'Semua'); $sort = (string) $request->query('sort', 'Terbaru daftar'); $parents = User::query() ->with([ 'children.measurements' => fn ($query) => $query->latest('tanggal_ukur')->latest('id'), ]) ->withCount([ 'children', 'children as measurement_count' => fn ($query) => $query->whereHas('measurements'), ]) ->where('role', 'orang_tua') ->when($q !== '', function ($query) use ($q) { $query->where(function ($nested) use ($q) { $nested->where('name', 'like', "%{$q}%") ->orWhere('email', 'like', "%{$q}%") ->orWhere('phone', 'like', "%{$q}%") ->orWhereHas('children', fn ($child) => $child->where('nama', 'like', "%{$q}%")); }); }) ->get(); $consultationCounts = ConsultationRoom::query() ->selectRaw('user_id, count(*) as total') ->groupBy('user_id') ->pluck('total', 'user_id'); $activeConsultationCounts = ConsultationRoom::query() ->selectRaw('user_id, count(*) as total') ->whereIn('status', ['aktif', 'active', 'open', 'waiting']) ->groupBy('user_id') ->pluck('total', 'user_id'); $rows = $parents->map(function (User $parent) use ($consultationCounts, $activeConsultationCounts) { $childRows = $parent->children->map(function (Child $child) { $latest = $child->measurements->first(); $status = NutritionStatusHelper::getStatus($latest); return [ 'child' => $child, 'latest' => $latest, 'status' => $status, 'risk' => NutritionStatusHelper::riskLabel($status), 'risk_score' => NutritionStatusHelper::riskScore($status), ]; }); $riskScore = (int) $childRows->max('risk_score'); $familyStatus = match ($riskScore) { 3 => 'Risiko Tinggi', 2 => 'Perlu Pantau', 1 => 'Stabil', default => 'Belum Ukur', }; $lastMeasurement = $childRows ->pluck('latest') ->filter() ->sortByDesc(fn ($measurement) => $measurement->tanggal_ukur?->timestamp ?? 0) ->first(); $lastConsultationAt = ConsultationRoom::query() ->where('user_id', $parent->id) ->latest('last_message_at') ->value('last_message_at'); $lastConsultationAt = $lastConsultationAt ? Carbon::parse($lastConsultationAt) : null; return [ 'parent' => $parent, 'children' => $childRows, 'family_status' => $familyStatus, 'risk_score' => $riskScore, 'account_status' => $parent->account_status ?: 'Aktif', 'consultations_count' => (int) ($consultationCounts[$parent->id] ?? 0), 'active_consultations_count' => (int) ($activeConsultationCounts[$parent->id] ?? 0), 'last_active' => $parent->last_active_at ?? $lastConsultationAt ?? $lastMeasurement?->created_at ?? $parent->updated_at, 'has_linked_data' => $parent->children_count > 0 || (int) ($consultationCounts[$parent->id] ?? 0) > 0 || (int) $parent->measurement_count > 0, ]; }); $summary = [ 'totalParents' => $rows->count(), 'activeConsultations' => $rows->sum('active_consultations_count'), 'highRiskFamilies' => $rows->where('family_status', 'Risiko Tinggi')->count(), 'unmeasuredFamilies' => $rows->where('family_status', 'Belum Ukur')->count(), ]; $rows = $rows ->when($filter !== 'Semua', function ($collection) use ($filter) { return $collection->filter(function ($row) use ($filter) { return match ($filter) { 'Aktif' => $row['account_status'] === 'Aktif', 'Nonaktif' => $row['account_status'] === 'Nonaktif', 'Risiko Tinggi' => $row['family_status'] === 'Risiko Tinggi', 'Belum Ukur' => $row['family_status'] === 'Belum Ukur', 'Konsultasi Aktif' => $row['active_consultations_count'] > 0, default => true, }; }); }); $rows = (match ($sort) { 'Terbaru daftar' => $rows->sortByDesc(fn ($row) => $row['parent']->created_at?->timestamp ?? 0), 'Jumlah anak terbanyak' => $rows->sortByDesc(fn ($row) => $row['parent']->children_count), 'Risiko tertinggi' => $rows->sortByDesc('risk_score'), default => $rows->sortBy(fn ($row) => $row['parent']->name), })->values(); $perPage = 10; $page = LengthAwarePaginator::resolveCurrentPage(); $paginatedRows = new LengthAwarePaginator( $rows->forPage($page, $perPage)->values(), $rows->count(), $perPage, $page, ['path' => $request->url(), 'query' => $request->query()], ); return view('admin.parents', [ 'rows' => $paginatedRows, 'q' => $q, 'filter' => $filter, 'sort' => $sort, 'summary' => $summary, ]); } public function parentDetail(User $parent): View { abort_unless((string) $parent->role === 'orang_tua', 404); $parent->load([ 'children.measurements' => fn ($query) => $query->latest('tanggal_ukur')->latest('id')->take(8), ]); $consultationsCount = ConsultationRoom::query()->where('user_id', $parent->id)->count(); $lastMeasurement = $parent->children ->flatMap(fn ($child) => $child->measurements) ->sortByDesc(fn ($measurement) => $measurement->tanggal_ukur?->timestamp ?? 0) ->first(); $lastConsultationAt = ConsultationRoom::query() ->where('user_id', $parent->id) ->latest('last_message_at') ->value('last_message_at'); $lastConsultationAt = $lastConsultationAt ? Carbon::parse($lastConsultationAt) : null; return view('admin.parents.show', [ 'parent' => $parent, 'consultationsCount' => $consultationsCount, 'lastMeasurement' => $lastMeasurement, 'lastActive' => $parent->last_active_at ?? $lastConsultationAt ?? $lastMeasurement?->created_at ?? $parent->updated_at, ]); } public function createParent(): View { return view('admin.parents.create'); } public function storeParent(Request $request): RedirectResponse { $request->merge(['phone' => $this->normalizeAdminPhone((string) $request->input('phone', ''))]); $validated = $request->validate([ 'name' => ['required', 'string', 'max:255'], 'phone' => ['required', 'string', 'max:32', Rule::unique('users', 'phone')], 'email' => ['nullable', 'email', 'max:255', Rule::unique('users', 'email')], 'parent_gender' => ['required', Rule::in(['ayah', 'bunda'])], 'password' => ['required', 'confirmed', Password::min(8)], 'account_status' => ['required', Rule::in(['Aktif', 'Nonaktif'])], ]); $parent = User::query()->create([ 'name' => $validated['name'], 'phone' => $validated['phone'], 'email' => $validated['email'] ?: null, 'role' => 'orang_tua', 'parent_gender' => $validated['parent_gender'], 'password' => Hash::make($validated['password']), 'account_status' => $validated['account_status'], 'status' => $validated['account_status'] === 'Aktif' ? 'aktif' : 'nonaktif', ]); return redirect() ->route('admin.parents.show', $parent) ->with('success', 'Akun orang tua berhasil ditambahkan.'); } public function updateParent(Request $request, User $parent): RedirectResponse { abort_unless((string) $parent->role === 'orang_tua', 404); $isSuperAdmin = $request->user()?->role === 'super_admin'; $rules = [ 'name' => ['required', 'string', 'max:255'], 'phone' => ['required', 'string', 'max:32', Rule::unique('users', 'phone')->ignore($parent->id)], 'email' => ['nullable', 'email', 'max:255', Rule::unique('users', 'email')->ignore($parent->id)], 'account_status' => ['required', Rule::in(['Aktif', 'Nonaktif', 'Diblokir'])], ]; if ($isSuperAdmin) { $rules['password'] = ['nullable', 'confirmed', Password::min(8)]; } else { $rules['password'] = ['prohibited']; $rules['password_confirmation'] = ['prohibited']; } $validated = $request->validate($rules); $payload = collect($validated) ->except(['password_confirmation']) ->filter(fn ($value, $key) => $key !== 'password' || filled($value)) ->all(); if (! empty($payload['password'])) { $payload['password'] = Hash::make($payload['password']); } $parent->update($payload); return back()->with('success', ! empty($payload['password']) ? 'Data orang tua dan password berhasil diperbarui.' : 'Data orang tua berhasil diperbarui.'); } public function deactivateParent(User $parent): RedirectResponse { abort_unless(auth()->user()?->role === 'super_admin', 403); abort_unless((string) $parent->role === 'orang_tua', 404); $parent->update(['account_status' => 'Nonaktif']); return back()->with('success', 'Akun orang tua berhasil dinonaktifkan.'); } public function destroyParent(User $parent): RedirectResponse { abort_unless(auth()->user()?->role === 'super_admin', 403); abort_unless((string) $parent->role === 'orang_tua', 404); $parent->delete(); return redirect()->route('admin.parents')->with('success', 'Akun orang tua berhasil dihapus.'); } public function openChildConsultation(Child $child): RedirectResponse { $room = ConsultationRoom::query() ->where('child_id', $child->id) ->latest('last_message_at') ->latest('updated_at') ->first(); if (!$room) { return back()->with('warning', 'Anak dengan nama '.$child->nama.' belum pernah melakukan konsultasi.'); } return redirect()->route('admin.consultations', [ 'room' => $room->id, 'q' => $child->nama, ]); } public function openParentConsultation(User $parent): RedirectResponse { abort_unless((string) $parent->role === 'orang_tua', 404); $room = ConsultationRoom::query() ->where('user_id', $parent->id) ->latest('last_message_at') ->latest('updated_at') ->first(); if (!$room) { $childName = $parent->children()->value('nama') ?: $parent->name; return back()->with('warning', 'Anak dengan nama '.$childName.' belum pernah melakukan konsultasi.'); } return redirect()->route('admin.consultations', [ 'room' => $room->id, 'q' => $parent->name, ]); } public function openNutritionistConsultations(Nutritionist $nutritionist): RedirectResponse { $expertId = trim((string) ($nutritionist->expert_id ?: 'expert-'.$nutritionist->user_id)); $room = ConsultationRoom::query() ->where('expert_id', $expertId) ->latest('last_message_at') ->latest('updated_at') ->first(); if (!$room) { return back()->with('warning', 'Ahli gizi ini belum memiliki riwayat konsultasi.'); } return redirect()->route('admin.consultations', [ 'room' => $room->id, 'expert_id' => $expertId, 'expert_name' => $nutritionist->user?->name, ]); } public function nutritionists(Request $request): View { $q = trim((string) $request->query('q', '')); $filter = (string) $request->query('filter', 'Semua'); $nutritionists = Nutritionist::query() ->with('user') ->when($q !== '', function ($query) use ($q) { $query->where('specialization', 'like', "%{$q}%") ->orWhereHas('user', fn ($user) => $user->where('name', 'like', "%{$q}%")); }) ->latest() ->get(); $rows = $nutritionists->map(fn (Nutritionist $nutritionist) => $this->nutritionistMonitoringRow($nutritionist)); $summary = [ 'totalNutritionists' => $rows->count(), 'onlineToday' => $rows->filter(fn ($row) => $row['is_online'])->count(), 'activeConsultations' => $rows->sum('active_consultations'), 'availableNutritionists' => $rows->filter(fn ($row) => $row['capacity'] !== 'Penuh' && $row['account_status'] === 'Aktif' && $row['is_available'])->count(), ]; $rows = $rows ->when($filter !== 'Semua', function ($collection) use ($filter) { return $collection->filter(function ($row) use ($filter) { return match ($filter) { 'Online' => $row['is_online'], 'Offline' => ! $row['is_online'], 'Aktif' => $row['account_status'] === 'Aktif', 'Nonaktif' => $row['account_status'] === 'Nonaktif', default => true, }; }); }) ->values(); $perPage = 9; $page = LengthAwarePaginator::resolveCurrentPage(); $paginatedRows = new LengthAwarePaginator( $rows->forPage($page, $perPage)->values(), $rows->count(), $perPage, $page, ['path' => $request->url(), 'query' => $request->query()], ); return view('admin.nutritionists', [ 'rows' => $paginatedRows, 'q' => $q, 'filter' => $filter, 'summary' => $summary, ]); } public function nutritionistDetail(Nutritionist $nutritionist): View { $nutritionist->load('user'); return view('admin.nutritionists.show', [ 'row' => $this->nutritionistMonitoringRow($nutritionist, true), ]); } public function createNutritionist(): View { return view('admin.nutritionists.form', [ 'nutritionist' => new Nutritionist(), 'mode' => 'create', ]); } public function storeNutritionist(Request $request): RedirectResponse { $request->merge(['phone' => $this->normalizeAdminPhone((string) $request->input('phone', ''))]); $validated = $this->validateNutritionistPayload($request); $avatar = $this->storeNutritionistAvatar($request); $user = User::query()->create([ 'name' => $validated['name'], 'email' => $validated['email'] ?: null, 'phone' => $validated['phone'], 'role' => 'ahli_gizi', 'account_status' => $validated['account_status'], 'status' => $validated['account_status'] === 'Aktif' ? 'aktif' : 'nonaktif', 'avatar' => $avatar, 'parent_gender' => $validated['gender'], 'password' => $validated['password'], ]); $user->nutritionist()->create([ 'expert_id' => $validated['expert_id'], 'title' => $validated['title'], 'specialization' => $validated['specialization'], 'experience' => $validated['experience_years'].' tahun pengalaman', 'experience_years' => $validated['experience_years'], 'bio' => $validated['bio'], 'str_sip' => $validated['str_sip'], 'is_online' => false, 'is_available' => $validated['account_status'] === 'Aktif', 'max_consultation' => $validated['max_consultation'], ]); return redirect()->route('admin.nutritionists')->with('success', 'Ahli gizi berhasil ditambahkan.'); } public function editNutritionist(Nutritionist $nutritionist): View { $nutritionist->load('user'); return view('admin.nutritionists.form', [ 'nutritionist' => $nutritionist, 'mode' => 'edit', ]); } public function updateNutritionist(Request $request, Nutritionist $nutritionist): RedirectResponse { $nutritionist->load('user'); $request->merge(['phone' => $this->normalizeAdminPhone((string) $request->input('phone', ''))]); $validated = $this->validateNutritionistPayload($request, $nutritionist); $avatar = $this->storeNutritionistAvatar($request); $userPayload = [ 'name' => $validated['name'], 'email' => $validated['email'] ?: null, 'phone' => $validated['phone'], 'account_status' => $validated['account_status'], 'status' => $validated['account_status'] === 'Aktif' ? 'aktif' : 'nonaktif', 'parent_gender' => $validated['gender'], ]; if ($avatar) { $userPayload['avatar'] = $avatar; } if (!empty($validated['password'])) { $userPayload['password'] = $validated['password']; } $nutritionist->user?->update($userPayload); $nutritionist->update([ 'expert_id' => $validated['expert_id'], 'title' => $validated['title'], 'specialization' => $validated['specialization'], 'experience' => $validated['experience_years'].' tahun pengalaman', 'experience_years' => $validated['experience_years'], 'bio' => $validated['bio'], 'str_sip' => $validated['str_sip'], 'is_available' => $validated['account_status'] === 'Aktif', 'max_consultation' => $validated['max_consultation'], ]); return redirect()->route('admin.nutritionists')->with('success', 'Data ahli gizi berhasil diperbarui.'); } public function deactivateNutritionist(Nutritionist $nutritionist): RedirectResponse { $nutritionist->user?->update(['account_status' => 'Nonaktif', 'status' => 'nonaktif']); $nutritionist->update(['is_online' => false, 'is_available' => false]); return back()->with('success', 'Akun ahli gizi berhasil dinonaktifkan.'); } public function activateNutritionist(Nutritionist $nutritionist): RedirectResponse { $nutritionist->user?->update(['account_status' => 'Aktif', 'status' => 'aktif']); $nutritionist->update(['is_available' => true]); return back()->with('success', 'Akun ahli gizi berhasil diaktifkan.'); } public function settings(): View { $admin = auth()->user(); $admins = User::query() ->whereIn('role', ['super_admin', 'admin_operasional']) ->latest('last_login_at') ->latest() ->get(); $activities = $admins ->map(fn (User $row) => [ 'admin' => $row->name ?: 'Admin S-Gizi', 'action' => $row->last_login_at ? 'login ke sistem admin' : 'akun admin dibuat', 'time' => $row->last_login_at ?? $row->created_at, ]) ->filter(fn ($activity) => $activity['time'] !== null) ->sortByDesc(fn ($activity) => $activity['time']->timestamp) ->values(); return view('admin.settings', [ 'admin' => $admin, 'admins' => $admins, 'activities' => $activities, 'isSuperAdmin' => $admin?->role === 'super_admin', ]); } public function storeAdmin(Request $request): RedirectResponse { $request->merge(['phone' => $this->normalizeAdminPhone((string) $request->input('phone', ''))]); $validated = $request->validate([ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'max:255', Rule::unique('users', 'email')], 'phone' => ['required', 'string', 'max:32', Rule::unique('users', 'phone')], 'password' => ['required', 'confirmed', Password::min(8)], 'role' => ['required', Rule::in(['super_admin', 'admin_operasional'])], 'account_status' => ['required', Rule::in(['Aktif', 'Nonaktif'])], ]); User::query()->create([ 'name' => $validated['name'], 'email' => $validated['email'], 'phone' => $this->normalizeAdminPhone($validated['phone']), 'password' => Hash::make($validated['password']), 'role' => $validated['role'], 'account_status' => $validated['account_status'], 'status' => $validated['account_status'] === 'Aktif' ? 'aktif' : 'nonaktif', ]); return back()->with('success', 'Admin berhasil ditambahkan.'); } public function updateProfile(Request $request): RedirectResponse { /** @var User $admin */ $admin = auth()->user(); $request->merge(['phone' => $this->normalizeAdminPhone((string) $request->input('phone', ''))]); $validated = $request->validate([ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'max:255', Rule::unique('users', 'email')->ignore($admin->id)], 'phone' => ['required', 'string', 'max:32', Rule::unique('users', 'phone')->ignore($admin->id)], ]); $admin->update($validated); return back()->with('success', 'Profil admin berhasil diperbarui.'); } public function updatePassword(Request $request): RedirectResponse { /** @var User $admin */ $admin = auth()->user(); $validated = $request->validate([ 'current_password' => ['required', 'string'], 'password' => ['required', 'confirmed', Password::min(8)], ]); if (! $admin->password || ! Hash::check($validated['current_password'], $admin->password)) { return back()->withErrors(['current_password' => 'Password lama tidak sesuai.']); } $admin->update([ 'password' => Hash::make($validated['password']), ]); return back()->with('success', 'Password admin berhasil diperbarui.'); } public function updateAdmin(Request $request, User $admin): RedirectResponse { abort_unless(in_array($admin->role, ['super_admin', 'admin_operasional'], true), 404); $request->merge(['phone' => $this->normalizeAdminPhone((string) $request->input('phone', ''))]); $validated = $request->validate([ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'max:255', Rule::unique('users', 'email')->ignore($admin->id)], 'phone' => ['required', 'string', 'max:32', Rule::unique('users', 'phone')->ignore($admin->id)], 'password' => ['nullable', 'confirmed', Password::min(8)], 'role' => ['required', Rule::in(['super_admin', 'admin_operasional'])], 'account_status' => ['required', Rule::in(['Aktif', 'Nonaktif'])], ]); $payload = [ 'name' => $validated['name'], 'email' => $validated['email'], 'phone' => $validated['phone'], 'role' => $validated['role'], 'account_status' => $validated['account_status'], 'status' => $validated['account_status'] === 'Aktif' ? 'aktif' : 'nonaktif', ]; if (! empty($validated['password'])) { $payload['password'] = Hash::make($validated['password']); } $admin->update($payload); return back()->with('success', 'Data admin berhasil diperbarui.'); } public function deactivateAdmin(User $admin): RedirectResponse { abort_if($admin->id === auth()->id(), 422, 'Admin tidak dapat menonaktifkan akunnya sendiri.'); abort_unless(in_array($admin->role, ['super_admin', 'admin_operasional'], true), 404); $admin->update([ 'account_status' => 'Nonaktif', 'status' => 'nonaktif', ]); return back()->with('success', 'Admin berhasil dinonaktifkan.'); } private function nutritionistMonitoringRow(Nutritionist $nutritionist, bool $withRooms = false): array { $nutritionist->loadMissing('user'); $user = $nutritionist->user; $expertId = trim((string) ($nutritionist->expert_id ?: 'expert-'.$nutritionist->user_id)); $rooms = ConsultationRoom::query() ->with(['child.measurements' => fn ($query) => $query->latest('tanggal_ukur')->latest('id')->take(1), 'user']) ->where('expert_id', $expertId) ->get(); $activeRooms = $rooms->filter(fn ($room) => in_array($room->status, ['aktif', 'active', 'open', 'waiting'], true)); $completedRooms = $rooms->filter(fn ($room) => in_array($room->status, ['selesai', 'closed', 'resolved'], true)); $highRiskChildren = $rooms ->pluck('child') ->filter() ->unique('id') ->filter(function ($child) { $status = NutritionStatusHelper::getStatus($child->measurements->first()); return NutritionStatusHelper::riskLabel($status) === 'Risiko Tinggi'; }) ->count(); $rawUserLastActiveAt = $user?->getRawOriginal('last_active_at'); $rawNutritionistLastActiveAt = $nutritionist->getRawOriginal('last_active_at'); $lastActiveAt = collect([$rawUserLastActiveAt, $rawNutritionistLastActiveAt]) ->filter() ->map(fn ($timestamp) => Carbon::parse($timestamp)) ->sortDesc() ->first(); $isAccountActive = ! (($user?->status ?? 'aktif') === 'nonaktif' || ($user?->account_status ?? 'Aktif') === 'Nonaktif'); $isOnline = $isAccountActive && (bool) $nutritionist->is_online && (bool) $lastActiveAt?->gte(now()->subMinutes(5)); $presence = $isOnline ? 'Online' : 'Offline'; $activeCount = $activeRooms->count(); $capacity = match (true) { $activeCount >= (int) ($nutritionist->max_consultation ?: 25) => 'Penuh', $activeCount > 10 => 'Sedang', default => 'Ringan', }; $roomIds = $rooms->pluck('id'); $expertReplies = $roomIds->isEmpty() ? 0 : ConsultationMessage::query()->whereIn('room_id', $roomIds)->where('sender_type', 'expert')->count(); $parentMessages = $roomIds->isEmpty() ? 0 : ConsultationMessage::query()->whereIn('room_id', $roomIds)->where('sender_type', 'parent')->count(); $responseRate = $parentMessages > 0 ? min(100, (int) round(($expertReplies / $parentMessages) * 100)) : 0; return [ 'nutritionist' => $nutritionist, 'user' => $user, 'expert_id' => $expertId, 'name' => trim(($user?->name ?? 'Ahli Gizi').' '.($nutritionist->title ? ', '.$nutritionist->title : '')), 'account_status' => (($user?->status ?? 'aktif') === 'nonaktif' || ($user?->account_status ?? 'Aktif') === 'Nonaktif') ? 'Nonaktif' : 'Aktif', 'is_available' => (bool) $nutritionist->is_available, 'last_active_at' => $lastActiveAt, 'last_online_label' => $isOnline ? 'Sedang online' : ($lastActiveAt?->locale('id')->translatedFormat('d M Y H:i') ?? 'Belum pernah online'), 'is_online' => $isOnline, 'presence' => $presence, 'specializations' => $this->nutritionistSpecializationChips((string) $nutritionist->specialization), 'experience' => ($nutritionist->experience_years ?: 0).' tahun pengalaman', 'bio' => $nutritionist->bio, 'avatar' => $user?->avatar, 'active_consultations' => $activeCount, 'completed_consultations' => $completedRooms->count(), 'monitored_children' => $rooms->pluck('child_id')->filter()->unique()->count(), 'monitored_families' => $rooms->pluck('user_id')->filter()->unique()->count(), 'high_risk_children' => $highRiskChildren, 'capacity' => $capacity, 'response_rate' => $responseRate, 'avg_response' => $expertReplies > 0 ? '+/- 15 menit' : '-', 'rooms' => $withRooms ? $rooms->sortByDesc(fn ($room) => $room->last_message_at?->timestamp ?? 0)->values() : collect(), ]; } private function nutritionistSpecializationChips(string $specialization): array { $known = ['Stunting', 'MPASI', 'Gizi Kurang', 'Obesitas Anak', 'Tumbuh Kembang']; $lower = strtolower($specialization); $chips = collect($known)->filter(fn ($item) => str_contains($lower, strtolower(str_replace(' Anak', '', $item))))->values()->all(); return $chips ?: [$specialization ?: 'Gizi Anak']; } private function validateNutritionistPayload(Request $request, ?Nutritionist $nutritionist = null): array { $userId = $nutritionist?->user_id; $isCreate = $nutritionist === null || ! $nutritionist->exists; return $request->validate([ 'name' => ['required', 'string', 'max:255'], 'title' => ['nullable', 'string', 'max:80'], 'email' => ['nullable', 'email', 'max:255', Rule::unique('users', 'email')->ignore($userId)], 'phone' => ['required', 'string', 'max:32', Rule::unique('users', 'phone')->ignore($userId)], 'account_status' => ['required', Rule::in(['Aktif', 'Nonaktif'])], 'gender' => ['nullable', Rule::in(['laki-laki', 'perempuan', 'ayah', 'bunda'])], 'password' => [$isCreate ? 'required' : 'nullable', 'confirmed', Password::min(8)], 'avatar' => ['nullable', 'image', 'max:2048'], 'expert_id' => ['required', 'string', 'max:80', Rule::unique('nutritionists', 'expert_id')->ignore($nutritionist?->id)], 'specialization' => ['required', Rule::in(['Stunting', 'Gizi Kurang', 'MPASI', 'Obesitas Anak', 'Tumbuh Kembang', 'Konsultasi Umum'])], 'experience_years' => ['required', 'integer', 'min:0', 'max:60'], 'bio' => ['nullable', 'string', 'max:1000'], 'max_consultation' => ['required', 'integer', 'min:1', 'max:100'], 'str_sip' => ['nullable', 'string', 'max:120'], ]); } private function normalizeAdminPhone(string $phone): string { $digits = preg_replace('/\D+/', '', $phone) ?? ''; if (str_starts_with($digits, '0')) { $digits = '62'.substr($digits, 1); } if (! str_starts_with($digits, '62')) { $digits = '62'.$digits; } return '+'.$digits; } private function storeNutritionistAvatar(Request $request): ?string { if (! $request->hasFile('avatar')) { return null; } $file = $request->file('avatar'); $directory = public_path('assets/nutritionists'); if (! is_dir($directory)) { mkdir($directory, 0755, true); } $filename = 'ahli-gizi-'.now()->format('YmdHis').'-'.uniqid().'.'.$file->getClientOriginalExtension(); $file->move($directory, $filename); return 'assets/nutritionists/'.$filename; } }