MIF_E31231313/Website/s_gizi/app/Http/Controllers/Admin/AdminPageController.php

943 lines
39 KiB
PHP

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Helpers\NutritionStatusHelper;
use App\Models\Child;
use App\Models\ConsultationMessage;
use App\Models\ConsultationRoom;
use App\Models\Nutritionist;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Password;
use Illuminate\View\View;
class AdminPageController extends Controller
{
public function monitoring(Request $request): View
{
$q = trim((string) $request->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;
}
}