This commit is contained in:
Endyfadlullah 2025-08-29 13:16:14 +07:00
parent 47e63ba9ed
commit 89c7fc14b8
36 changed files with 2171 additions and 1278 deletions

View File

@ -49,7 +49,7 @@ public function dashboard()
// Get recent antrian
$antrianTerbaru = Antrian::with(['user', 'poli'])
->orderBy('created_at', 'desc')
->limit(10)
->limit(3)
->get();
return view('admin.dashboard', compact(
@ -65,6 +65,56 @@ public function dashboard()
));
}
public function dashboardApi()
{
// Auto-batalkan antrian yang sudah dipanggil lebih dari 5 menit
$this->autoBatalkanAntrianLama();
// Get counts for each poli
$poliUmumCount = Antrian::whereHas('poli', function ($query) {
$query->where('nama_poli', 'umum');
})->where('status', 'menunggu')->count();
$poliGigiCount = Antrian::whereHas('poli', function ($query) {
$query->where('nama_poli', 'gigi');
})->where('status', 'menunggu')->count();
$poliJiwaCount = Antrian::whereHas('poli', function ($query) {
$query->where('nama_poli', 'kesehatan jiwa');
})->where('status', 'menunggu')->count();
$poliTradisionalCount = Antrian::whereHas('poli', function ($query) {
$query->where('nama_poli', 'kesehatan tradisional');
})->where('status', 'menunggu')->count();
// Get recent antrian
$antrianTerbaru = Antrian::with(['user', 'poli'])
->orderBy('created_at', 'desc')
->limit(3)
->get();
return response()->json([
'success' => true,
'data' => [
'poliUmumCount' => $poliUmumCount,
'poliGigiCount' => $poliGigiCount,
'poliJiwaCount' => $poliJiwaCount,
'poliTradisionalCount' => $poliTradisionalCount,
'antrianTerbaru' => $antrianTerbaru->map(function ($antrian) {
return [
'id' => $antrian->id,
'no_antrian' => $antrian->no_antrian,
'status' => $antrian->status,
'poli_name' => $antrian->poli->nama_poli,
'user_name' => $antrian->user->nama,
'created_at' => $antrian->created_at->diffForHumans(),
'updated_at' => $antrian->updated_at->diffForHumans(),
];
})
]
]);
}
public function manageUsers(Request $request)
{
// Auto-batalkan antrian yang sudah dipanggil lebih dari 5 menit
@ -91,10 +141,39 @@ public function manageUsers(Request $request)
public function showUser(User $user)
{
$user->load(['antrians.poli']);
// Load user with antrians ordered by created_at desc to get the latest first
$user->load([
'antrians' => function ($query) {
$query->with('poli')->orderBy('created_at', 'desc');
}
]);
return view('admin.users.show', compact('user'));
}
public function getUserAntrianTerbaru(User $user)
{
// Get latest 5 antrians for the user
$antrians = $user->antrians()
->with('poli')
->orderBy('created_at', 'desc')
->take(5)
->get();
return response()->json([
'success' => true,
'antrians' => $antrians->map(function ($antrian) {
return [
'id' => $antrian->id,
'no_antrian' => $antrian->no_antrian,
'poli_name' => $antrian->poli->nama_poli ?? 'N/A',
'status' => $antrian->status,
'created_at' => $antrian->created_at ? $antrian->created_at->format('d/m/Y H:i') : 'N/A'
];
})
]);
}
public function createUser()
{
return view('admin.users.create');
@ -213,7 +292,12 @@ public function laporan(Request $request)
// Filter berdasarkan status
if ($request->filled('status')) {
$query->where('status', $request->status);
$status = $request->status;
// Convert 'sedang' to 'sedang_diperiksa' for database compatibility
if ($status === 'sedang') {
$status = 'sedang_diperiksa';
}
$query->where('status', $status);
}
// Filter berdasarkan jenis kelamin
@ -223,17 +307,23 @@ public function laporan(Request $request)
});
}
$antrian = $query->orderBy('created_at', 'desc')->get();
// Clone query for statistics before pagination
$statisticQuery = clone $query;
$allData = $statisticQuery->get();
// Paginate the results
$antrian = $query->orderBy('created_at', 'desc')->paginate(10);
$polis = Poli::all();
// Statistik
$totalAntrian = $antrian->count();
$antrianSelesai = $antrian->where('status', 'selesai')->count();
$antrianMenunggu = $antrian->where('status', 'menunggu')->count();
$antrianDipanggil = $antrian->where('status', 'dipanggil')->count();
$antrianSedang = $antrian->where('status', 'sedang')->count();
// Statistik dari semua data (tidak hanya yang dipaginate)
$totalAntrian = $allData->count();
$antrianSelesai = $allData->where('status', 'selesai')->count();
$antrianMenunggu = $allData->where('status', 'menunggu')->count();
$antrianDipanggil = $allData->where('status', 'dipanggil')->count();
$antrianSedang = $allData->where('status', 'sedang_diperiksa')->count();
$antrianBatal = $allData->where('status', 'batal')->count();
return view('admin.laporan.index', compact('antrian', 'polis', 'totalAntrian', 'antrianSelesai', 'antrianMenunggu', 'antrianDipanggil', 'antrianSedang'));
return view('admin.laporan.index', compact('antrian', 'polis', 'totalAntrian', 'antrianSelesai', 'antrianMenunggu', 'antrianDipanggil', 'antrianSedang', 'antrianBatal'));
}
public function exportPDF(Request $request)
@ -256,7 +346,11 @@ public function exportPDF(Request $request)
// Filter berdasarkan status
if ($request->filled('status')) {
$query->where('status', $request->status);
$status = $request->status;
if ($status === 'sedang') {
$status = 'sedang_diperiksa';
}
$query->where('status', $status);
}
// Filter berdasarkan jenis kelamin
@ -273,7 +367,7 @@ public function exportPDF(Request $request)
$antrianSelesai = $antrian->where('status', 'selesai')->count();
$antrianMenunggu = $antrian->where('status', 'menunggu')->count();
$antrianDipanggil = $antrian->where('status', 'dipanggil')->count();
$antrianSedang = $antrian->where('status', 'sedang')->count();
$antrianSedang = $antrian->where('status', 'sedang_diperiksa')->count();
$pdf = Pdf::loadView('admin.laporan.pdf', compact('antrian', 'totalAntrian', 'antrianSelesai', 'antrianMenunggu', 'antrianDipanggil', 'antrianSedang'));
return $pdf->download('laporan-antrian-' . date('Y-m-d') . '.pdf');
@ -299,7 +393,11 @@ public function exportExcel(Request $request)
// Filter berdasarkan status
if ($request->filled('status')) {
$query->where('status', $request->status);
$status = $request->status;
if ($status === 'sedang') {
$status = 'sedang_diperiksa';
}
$query->where('status', $status);
}
// Filter berdasarkan jenis kelamin
@ -316,7 +414,7 @@ public function exportExcel(Request $request)
$antrianSelesai = $antrian->where('status', 'selesai')->count();
$antrianMenunggu = $antrian->where('status', 'menunggu')->count();
$antrianDipanggil = $antrian->where('status', 'dipanggil')->count();
$antrianSedang = $antrian->where('status', 'sedang')->count();
$antrianSedang = $antrian->where('status', 'sedang_diperiksa')->count();
$filename = 'laporan-antrian-' . date('Y-m-d') . '.csv';
@ -339,21 +437,21 @@ public function exportExcel(Request $request)
'Tanggal Daftar',
'Waktu Daftar',
'Waktu Panggil'
]);
], ';');
// Data CSV
foreach ($antrian as $item) {
fputcsv($file, [
$item->no_antrian,
$item->user->nama,
$item->user->no_ktp,
"'" . $item->user->no_ktp,
$item->user->jenis_kelamin,
$item->poli->nama_poli,
ucfirst($item->status),
$item->created_at ? $item->created_at->format('d/m/Y') : '-',
$item->created_at ? $item->created_at->format('H:i') : '-',
$item->waktu_panggil ? $item->waktu_panggil->format('H:i') : '-'
]);
], ';');
}
fclose($file);
@ -362,7 +460,7 @@ public function exportExcel(Request $request)
return response()->stream($callback, 200, $headers);
}
public function poliUmum()
public function poliUmum(Request $request)
{
// Auto-batalkan antrian yang sudah dipanggil lebih dari 5 menit
$this->autoBatalkanAntrianLama();
@ -372,13 +470,13 @@ public function poliUmum()
$query->where('nama_poli', 'umum');
})
->orderBy('created_at', 'asc')
->get();
->paginate(10);
$title = 'Poli Umum';
return view('admin.poli.index', compact('antrians', 'title'));
}
public function poliGigi()
public function poliGigi(Request $request)
{
// Auto-batalkan antrian yang sudah dipanggil lebih dari 5 menit
$this->autoBatalkanAntrianLama();
@ -388,13 +486,13 @@ public function poliGigi()
$query->where('nama_poli', 'gigi');
})
->orderBy('created_at', 'asc')
->get();
->paginate(10);
$title = 'Poli Gigi';
return view('admin.poli.index', compact('antrians', 'title'));
}
public function poliJiwa()
public function poliJiwa(Request $request)
{
// Auto-batalkan antrian yang sudah dipanggil lebih dari 5 menit
$this->autoBatalkanAntrianLama();
@ -404,13 +502,13 @@ public function poliJiwa()
$query->where('nama_poli', 'kesehatan jiwa');
})
->orderBy('created_at', 'asc')
->get();
->paginate(10);
$title = 'Poli Jiwa';
return view('admin.poli.index', compact('antrians', 'title'));
}
public function poliTradisional()
public function poliTradisional(Request $request)
{
// Auto-batalkan antrian yang sudah dipanggil lebih dari 5 menit
$this->autoBatalkanAntrianLama();
@ -420,7 +518,7 @@ public function poliTradisional()
$query->where('nama_poli', 'kesehatan tradisional');
})
->orderBy('created_at', 'asc')
->get();
->paginate(10);
$title = 'Poli Tradisional';
return view('admin.poli.index', compact('antrians', 'title'));
@ -513,10 +611,10 @@ public function selesaiAntrian(Request $request)
$antrian = Antrian::findOrFail($request->antrian_id);
if ($antrian->status !== 'dipanggil') {
if (!in_array($antrian->status, ['dipanggil', 'sedang_diperiksa'])) {
return response()->json([
'success' => false,
'message' => 'Antrian ini tidak dalam status dipanggil'
'message' => 'Antrian ini tidak dalam status dipanggil atau sedang diperiksa'
]);
}
@ -535,6 +633,71 @@ public function selesaiAntrian(Request $request)
}
}
public function konfirmasiKehadiran(Request $request)
{
try {
$request->validate([
'antrian_id' => 'required|exists:antrians,id'
]);
$antrian = Antrian::findOrFail($request->antrian_id);
if ($antrian->status !== 'dipanggil') {
return response()->json([
'success' => false,
'message' => 'Antrian ini tidak dalam status dipanggil'
]);
}
// Update waktu hadir
$antrian->update(['waktu_hadir' => now()]);
return response()->json([
'success' => true,
'message' => 'Kehadiran pasien ' . $antrian->no_antrian . ' telah dikonfirmasi'
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan: ' . $e->getMessage()
], 500);
}
}
public function mulaiPemeriksaan(Request $request)
{
try {
$request->validate([
'antrian_id' => 'required|exists:antrians,id'
]);
$antrian = Antrian::findOrFail($request->antrian_id);
if ($antrian->status !== 'dipanggil' || !$antrian->waktu_hadir) {
return response()->json([
'success' => false,
'message' => 'Pasien harus dikonfirmasi hadir terlebih dahulu'
]);
}
// Update status dan waktu mulai pemeriksaan
$antrian->update([
'status' => 'sedang_diperiksa',
'waktu_mulai_periksa' => now()
]);
return response()->json([
'success' => true,
'message' => 'Pemeriksaan pasien ' . $antrian->no_antrian . ' dimulai'
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan: ' . $e->getMessage()
], 500);
}
}
public function batalAntrian(Request $request)
{
try {
@ -607,13 +770,30 @@ public function storeAntrianAdmin(Request $request)
$existingQueue = Antrian::where('user_id', $request->user_id)
->where('poli_id', $request->poli_id)
->whereDate('created_at', today())
->whereIn('status', ['menunggu', 'dipanggil'])
->whereIn('status', ['menunggu', 'dipanggil', 'sedang_diperiksa'])
->first();
if ($existingQueue) {
$statusMessage = '';
switch ($existingQueue->status) {
case 'menunggu':
$statusMessage = 'User ini masih memiliki antrian yang menunggu di ' . $poli->nama_poli . '.';
break;
case 'dipanggil':
$statusMessage = 'User ini memiliki antrian yang sudah dipanggil di ' . $poli->nama_poli . '.';
break;
case 'sedang_diperiksa':
$statusMessage = 'User ini sedang dalam proses pemeriksaan di ' . $poli->nama_poli . '. Tidak dapat membuat antrian baru.';
break;
}
return response()->json([
'success' => false,
'message' => 'User ini sudah memiliki antrian aktif di ' . $poli->nama_poli . ' hari ini.'
'message' => $statusMessage,
'existing_queue' => [
'no_antrian' => $existingQueue->no_antrian,
'status' => $existingQueue->status
]
]);
}
@ -681,14 +861,15 @@ private function getPoliPrefix($namaPoli)
/**
* Auto-batalkan antrian yang sudah dipanggil lebih dari 5 menit
* tanpa dikonfirmasi selesai oleh admin
* tetapi belum dikonfirmasi kehadirannya (belum ada waktu_hadir)
*/
private function autoBatalkanAntrianLama()
{
try {
// Cari antrian yang sudah dipanggil lebih dari 5 menit
// Cari antrian yang sudah dipanggil lebih dari 5 menit tapi belum konfirmasi hadir
$antrianLama = Antrian::where('status', 'dipanggil')
->where('waktu_panggil', '<=', now()->subMinutes(5))
->whereNull('waktu_hadir') // Hanya yang belum konfirmasi hadir
->get();
$count = 0;
@ -698,16 +879,16 @@ private function autoBatalkanAntrianLama()
$count++;
// Log untuk tracking
\Log::info("Antrian {$antrian->no_antrian} otomatis dibatalkan karena lewat 5 menit sejak dipanggil");
Log::info("Antrian {$antrian->no_antrian} otomatis dibatalkan karena lewat 5 menit sejak dipanggil tanpa konfirmasi kehadiran");
}
// Jika ada antrian yang dibatalkan, log jumlahnya
if ($count > 0) {
\Log::info("Total {$count} antrian otomatis dibatalkan karena timeout");
Log::info("Total {$count} antrian otomatis dibatalkan karena timeout tanpa konfirmasi kehadiran");
}
} catch (\Exception $e) {
// Log error jika terjadi masalah
\Log::error("Error saat auto-batalkan antrian: " . $e->getMessage());
Log::error("Error saat auto-batalkan antrian: " . $e->getMessage());
}
}
@ -737,7 +918,7 @@ public function playQueueCallAudio(Request $request)
]);
$poliName = $request->input('poli_name');
// Get audio sequence from AudioService
$audioService = app(AudioService::class);
$result = $audioService->getQueueCallAudio($poliName);
@ -765,9 +946,9 @@ public function panggilSelanjutnya(Request $request)
$poliName = $request->input('poli_name');
// Cari antrian berikutnya yang status 'menunggu'
$antrianSelanjutnya = Antrian::whereHas('poli', function($query) use ($poliName) {
$query->where('nama_poli', $poliName);
})
$antrianSelanjutnya = Antrian::whereHas('poli', function ($query) use ($poliName) {
$query->where('nama_poli', $poliName);
})
->where('status', 'menunggu')
->whereDate('created_at', today())
->orderBy('created_at', 'asc')
@ -790,7 +971,7 @@ public function panggilSelanjutnya(Request $request)
\App\Models\RiwayatPanggilan::create([
'antrian_id' => $antrianSelanjutnya->id,
'waktu_panggilan' => now(),
'admin_id' => auth()->id()
'admin_id' => Auth::id()
]);
// Get audio sequence

View File

@ -43,20 +43,34 @@ public function addQueue(Request $request)
$poli = Poli::find($request->poli_id);
$poliName = $poli->nama_poli;
// Check if user already has a queue today for the same poli
// Check if user already has an active queue today for the same poli
$existingQueue = Antrian::where('user_id', $user->id)
->where('poli_id', $request->poli_id)
->whereDate('created_at', today())
->whereIn('status', ['menunggu', 'dipanggil'])
->whereIn('status', ['menunggu', 'dipanggil', 'sedang_diperiksa'])
->first();
if ($existingQueue) {
// Determine the appropriate message based on status
$statusMessage = '';
switch ($existingQueue->status) {
case 'menunggu':
$statusMessage = 'Anda masih memiliki antrian yang menunggu di ' . $poliName . '.';
break;
case 'dipanggil':
$statusMessage = 'Anda memiliki antrian yang sudah dipanggil di ' . $poliName . '. Silakan datang ke poli.';
break;
case 'sedang_diperiksa':
$statusMessage = 'Anda sedang dalam proses pemeriksaan di ' . $poliName . '. Tidak dapat mengambil antrian baru di poli yang sama.';
break;
}
// Check if request wants JSON response
if ($request->expectsJson()) {
return response()->json([
'success' => false,
'type' => 'existing_queue',
'message' => 'Anda sudah memiliki antrian di ' . $poliName . ' hari ini.',
'message' => $statusMessage,
'existing_queue' => [
'id' => $existingQueue->id,
'no_antrian' => $existingQueue->no_antrian,
@ -68,7 +82,7 @@ public function addQueue(Request $request)
}
// Fallback to redirect for non-AJAX requests
return redirect()->back()->with('error', 'Anda sudah memiliki antrian di ' . $poliName . ' hari ini.');
return redirect()->back()->with('error', $statusMessage);
}
// Note: User CAN have multiple queues in different polis
@ -91,14 +105,14 @@ public function addQueue(Request $request)
$lastNumber = (int) preg_replace('/^[A-Z]+/', '', $lastQueue->no_antrian);
// Debug log
\Log::info("Last queue: {$lastQueue->no_antrian}, Extracted number: {$lastNumber}");
Log::info("Last queue: {$lastQueue->no_antrian}, Extracted number: {$lastNumber}");
}
$nextNumber = $lastNumber + 1;
$nextQueueNumber = $prefix . $nextNumber;
// Debug log
\Log::info("Generated next queue number: {$nextQueueNumber} for poli: {$poliName}");
Log::info("Generated next queue number: {$nextQueueNumber} for poli: {$poliName}");
// Create new queue using current user's data
$antrian = Antrian::create([
@ -152,6 +166,7 @@ public function updateProfile(Request $request)
]);
try {
/** @var \App\Models\User $user */
$user = Auth::user();
// Check if KTP number is already used by another user
@ -205,14 +220,15 @@ private function getPoliPrefix($namaPoli)
/**
* Auto-batalkan antrian yang sudah dipanggil lebih dari 5 menit
* tanpa dikonfirmasi selesai oleh admin
* tetapi belum dikonfirmasi kehadirannya (belum ada waktu_hadir)
*/
private function autoBatalkanAntrianLama()
{
try {
// Cari antrian yang sudah dipanggil lebih dari 5 menit
// Cari antrian yang sudah dipanggil lebih dari 5 menit tapi belum konfirmasi hadir
$antrianLama = Antrian::where('status', 'dipanggil')
->where('waktu_panggil', '<=', now()->subMinutes(5))
->whereNull('waktu_hadir') // Hanya yang belum konfirmasi hadir
->get();
foreach ($antrianLama as $antrian) {
@ -220,16 +236,61 @@ private function autoBatalkanAntrianLama()
$antrian->update(['status' => 'batal']);
// Log untuk tracking (optional)
\Log::info("Antrian {$antrian->no_antrian} otomatis dibatalkan karena lewat 5 menit sejak dipanggil");
Log::info("Antrian {$antrian->no_antrian} otomatis dibatalkan karena lewat 5 menit sejak dipanggil tanpa konfirmasi kehadiran");
}
// Jika ada antrian yang dibatalkan, log jumlahnya
if ($antrianLama->count() > 0) {
\Log::info("Total {$antrianLama->count()} antrian otomatis dibatalkan karena timeout");
Log::info("Total {$antrianLama->count()} antrian otomatis dibatalkan karena timeout tanpa konfirmasi kehadiran");
}
} catch (\Exception $e) {
// Log error jika terjadi masalah
\Log::error("Error saat auto-batalkan antrian: " . $e->getMessage());
Log::error("Error saat auto-batalkan antrian: " . $e->getMessage());
}
}
/**
* Check if user has active queue in specific poli today
* @param int $userId
* @param int $poliId
* @return array|null
*/
private function checkActiveQueue($userId, $poliId)
{
$activeQueue = Antrian::where('user_id', $userId)
->where('poli_id', $poliId)
->whereDate('created_at', today())
->whereIn('status', ['menunggu', 'dipanggil', 'sedang_diperiksa'])
->first();
if ($activeQueue) {
return [
'exists' => true,
'queue' => $activeQueue,
'message' => $this->getStatusMessage($activeQueue->status, $activeQueue->poli->nama_poli)
];
}
return ['exists' => false];
}
/**
* Get appropriate message based on queue status
* @param string $status
* @param string $poliName
* @return string
*/
private function getStatusMessage($status, $poliName)
{
switch ($status) {
case 'menunggu':
return "Anda masih memiliki antrian yang menunggu di {$poliName}.";
case 'dipanggil':
return "Anda memiliki antrian yang sudah dipanggil di {$poliName}. Silakan datang ke poli.";
case 'sedang_diperiksa':
return "Anda sedang dalam proses pemeriksaan di {$poliName}. Tidak dapat mengambil antrian baru di poli yang sama.";
default:
return "Anda sudah memiliki antrian aktif di {$poliName}.";
}
}

View File

@ -12,71 +12,71 @@ public function index()
{
// Get all available polis
$polis = Poli::all();
// Current: sedang dipanggil per poli
$poliUmumCurrent = Antrian::whereHas('poli', function($query) {
$query->where('nama_poli', 'umum');
})
->where('status', 'dipanggil')
$poliUmumCurrent = Antrian::whereHas('poli', function ($query) {
$query->where('nama_poli', 'umum');
})
->whereIn('status', ['dipanggil', 'sedang_diperiksa'])
->whereDate('created_at', today())
->orderByDesc('updated_at')
->first();
$poliGigiCurrent = Antrian::whereHas('poli', function($query) {
$query->where('nama_poli', 'gigi');
})
->where('status', 'dipanggil')
$poliGigiCurrent = Antrian::whereHas('poli', function ($query) {
$query->where('nama_poli', 'gigi');
})
->whereIn('status', ['dipanggil', 'sedang_diperiksa'])
->whereDate('created_at', today())
->orderByDesc('updated_at')
->first();
$poliJiwaCurrent = Antrian::whereHas('poli', function($query) {
$query->where('nama_poli', 'kesehatan jiwa');
})
->where('status', 'dipanggil')
$poliJiwaCurrent = Antrian::whereHas('poli', function ($query) {
$query->where('nama_poli', 'kesehatan jiwa');
})
->whereIn('status', ['dipanggil', 'sedang_diperiksa'])
->whereDate('created_at', today())
->orderByDesc('updated_at')
->first();
$poliTradisionalCurrent = Antrian::whereHas('poli', function($query) {
$query->where('nama_poli', 'kesehatan tradisional');
})
->where('status', 'dipanggil')
$poliTradisionalCurrent = Antrian::whereHas('poli', function ($query) {
$query->where('nama_poli', 'kesehatan tradisional');
})
->whereIn('status', ['dipanggil', 'sedang_diperiksa'])
->whereDate('created_at', today())
->orderByDesc('updated_at')
->first();
// Next: menunggu per poli (maks 3)
$poliUmumNext = Antrian::whereHas('poli', function($query) {
$query->where('nama_poli', 'umum');
})
$poliUmumNext = Antrian::whereHas('poli', function ($query) {
$query->where('nama_poli', 'umum');
})
->where('status', 'menunggu')
->whereDate('created_at', today())
->orderBy('created_at', 'asc')
->take(3)
->get();
$poliGigiNext = Antrian::whereHas('poli', function($query) {
$query->where('nama_poli', 'gigi');
})
$poliGigiNext = Antrian::whereHas('poli', function ($query) {
$query->where('nama_poli', 'gigi');
})
->where('status', 'menunggu')
->whereDate('created_at', today())
->orderBy('created_at', 'asc')
->take(3)
->get();
$poliJiwaNext = Antrian::whereHas('poli', function($query) {
$query->where('nama_poli', 'kesehatan jiwa');
})
$poliJiwaNext = Antrian::whereHas('poli', function ($query) {
$query->where('nama_poli', 'kesehatan jiwa');
})
->where('status', 'menunggu')
->whereDate('created_at', today())
->orderBy('created_at', 'asc')
->take(3)
->get();
$poliTradisionalNext = Antrian::whereHas('poli', function($query) {
$query->where('nama_poli', 'kesehatan tradisional');
})
$poliTradisionalNext = Antrian::whereHas('poli', function ($query) {
$query->where('nama_poli', 'kesehatan tradisional');
})
->where('status', 'menunggu')
->whereDate('created_at', today())
->orderBy('created_at', 'asc')
@ -126,57 +126,57 @@ public function checkNewCalls(Request $request)
public function getDisplayData()
{
// Current: sedang dipanggil per poli
$poliUmumCurrent = Antrian::whereHas('poli', function($query) {
$query->where('nama_poli', 'umum');
})
->where('status', 'dipanggil')
$poliUmumCurrent = Antrian::whereHas('poli', function ($query) {
$query->where('nama_poli', 'umum');
})
->whereIn('status', ['dipanggil', 'sedang_diperiksa'])
->whereDate('created_at', today())
->orderByDesc('updated_at')
->first();
$poliGigiCurrent = Antrian::whereHas('poli', function($query) {
$query->where('nama_poli', 'gigi');
})
->where('status', 'dipanggil')
$poliGigiCurrent = Antrian::whereHas('poli', function ($query) {
$query->where('nama_poli', 'gigi');
})
->whereIn('status', ['dipanggil', 'sedang_diperiksa'])
->whereDate('created_at', today())
->orderByDesc('updated_at')
->first();
$poliJiwaCurrent = Antrian::whereHas('poli', function($query) {
$query->where('nama_poli', 'kesehatan jiwa');
})
->where('status', 'dipanggil')
$poliJiwaCurrent = Antrian::whereHas('poli', function ($query) {
$query->where('nama_poli', 'kesehatan jiwa');
})
->whereIn('status', ['dipanggil', 'sedang_diperiksa'])
->whereDate('created_at', today())
->orderByDesc('updated_at')
->first();
$poliTradisionalCurrent = Antrian::where('poli_id', 4)
->where('status', 'dipanggil')
->whereIn('status', ['dipanggil', 'sedang_diperiksa'])
->whereDate('created_at', today())
->orderByDesc('updated_at')
->first();
// Next: menunggu per poli (maks 3)
$poliUmumNext = Antrian::whereHas('poli', function($query) {
$query->where('nama_poli', 'umum');
})
$poliUmumNext = Antrian::whereHas('poli', function ($query) {
$query->where('nama_poli', 'umum');
})
->where('status', 'menunggu')
->whereDate('created_at', today())
->orderBy('created_at', 'asc')
->take(3)
->get();
$poliGigiNext = Antrian::whereHas('poli', function($query) {
$query->where('nama_poli', 'gigi');
})
$poliGigiNext = Antrian::whereHas('poli', function ($query) {
$query->where('nama_poli', 'gigi');
})
->where('status', 'menunggu')
->whereDate('created_at', today())
->orderBy('created_at', 'asc')
->get();
$poliJiwaNext = Antrian::whereHas('poli', function($query) {
$query->where('nama_poli', 'kesehatan jiwa');
})
$poliJiwaNext = Antrian::whereHas('poli', function ($query) {
$query->where('nama_poli', 'kesehatan jiwa');
})
->where('status', 'menunggu')
->whereDate('created_at', today())
->orderBy('created_at', 'asc')

View File

@ -18,12 +18,15 @@ class Antrian extends Model
'is_call',
'status',
'waktu_panggil',
'loket_id'
'waktu_hadir',
'waktu_mulai_periksa'
];
protected $casts = [
'tanggal_antrian' => 'date',
'waktu_panggil' => 'datetime',
'waktu_hadir' => 'datetime',
'waktu_mulai_periksa' => 'datetime',
'is_call' => 'boolean',
];
@ -37,11 +40,6 @@ public function poli()
return $this->belongsTo(Poli::class);
}
public function loket()
{
return $this->belongsTo(Loket::class);
}
public function riwayatPanggilan()
{
return $this->hasMany(RiwayatPanggilan::class);

View File

@ -1,19 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Loket extends Model
{
use HasFactory;
protected $table = 'lokets';
protected $fillable = ['nama_loket'];
public function antrians()
{
return $this->hasMany(Antrian::class);
}
}

View File

@ -19,7 +19,8 @@ public function register(): void
*/
public function boot(): void
{
// Set pagination to use Tailwind CSS styling
\Illuminate\Pagination\Paginator::useTailwind();
// Set pagination to use custom view
\Illuminate\Pagination\Paginator::defaultView('custom.pagination');
\Illuminate\Pagination\Paginator::defaultSimpleView('custom.pagination-simple');
}
}

View File

@ -9,7 +9,6 @@
"php": "^8.2",
"barryvdh/laravel-dompdf": "^3.1",
"laravel/framework": "^12.0",
"laravel/sanctum": "^4.2",
"laravel/tinker": "^2.10.1",
"pusher/pusher-php-server": "^7.2"
},

70
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6a7319662ab12a24a54850a3e8a7b468",
"content-hash": "74ecb2b28b89ed06697fc02c059657d8",
"packages": [
{
"name": "barryvdh/laravel-dompdf",
@ -1559,70 +1559,6 @@
},
"time": "2025-07-07T14:17:42+00:00"
},
{
"name": "laravel/sanctum",
"version": "v4.2.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/sanctum.git",
"reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677",
"reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677",
"shasum": ""
},
"require": {
"ext-json": "*",
"illuminate/console": "^11.0|^12.0",
"illuminate/contracts": "^11.0|^12.0",
"illuminate/database": "^11.0|^12.0",
"illuminate/support": "^11.0|^12.0",
"php": "^8.2",
"symfony/console": "^7.0"
},
"require-dev": {
"mockery/mockery": "^1.6",
"orchestra/testbench": "^9.0|^10.0",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^11.3"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Sanctum\\SanctumServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Sanctum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
"keywords": [
"auth",
"laravel",
"sanctum"
],
"support": {
"issues": "https://github.com/laravel/sanctum/issues",
"source": "https://github.com/laravel/sanctum"
},
"time": "2025-07-09T19:45:24+00:00"
},
{
"name": "laravel/serializable-closure",
"version": "v2.0.4",
@ -8747,12 +8683,12 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"stability-flags": [],
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "^8.2"
},
"platform-dev": {},
"platform-dev": [],
"plugin-api-version": "2.6.0"
}

View File

@ -93,14 +93,25 @@
|
*/
'passwords' => [
'users' => [
'provider' => 'users',
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
'expire' => 60,
'throttle' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Password Reset Settings (DISABLED)
|--------------------------------------------------------------------------
|
| Password reset settings are disabled because this application uses
| session-based password reset instead of Laravel's built-in token-based
| password reset functionality.
|
*/
// 'passwords' => [
// 'users' => [
// 'provider' => 'users',
// 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
// 'expire' => 60,
// 'throttle' => 60,
// ],
// ],
/*
|--------------------------------------------------------------------------

View File

@ -1,84 +0,0 @@
<?php
use Laravel\Sanctum\Sanctum;
return [
/*
|--------------------------------------------------------------------------
| Stateful Domains
|--------------------------------------------------------------------------
|
| Requests from the following domains / hosts will receive stateful API
| authentication cookies. Typically, these should include your local
| and production domains which access your API via a frontend SPA.
|
*/
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
Sanctum::currentApplicationUrlWithPort(),
// Sanctum::currentRequestHost(),
))),
/*
|--------------------------------------------------------------------------
| Sanctum Guards
|--------------------------------------------------------------------------
|
| This array contains the authentication guards that will be checked when
| Sanctum is trying to authenticate a request. If none of these guards
| are able to authenticate the request, Sanctum will use the bearer
| token that's present on an incoming request for authentication.
|
*/
'guard' => ['web'],
/*
|--------------------------------------------------------------------------
| Expiration Minutes
|--------------------------------------------------------------------------
|
| This value controls the number of minutes until an issued token will be
| considered expired. This will override any values set in the token's
| "expires_at" attribute, but first-party sessions are not affected.
|
*/
'expiration' => null,
/*
|--------------------------------------------------------------------------
| Token Prefix
|--------------------------------------------------------------------------
|
| Sanctum can prefix new tokens in order to take advantage of numerous
| security scanning initiatives maintained by open source platforms
| that notify developers if they commit tokens into repositories.
|
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
*/
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
/*
|--------------------------------------------------------------------------
| Sanctum Middleware
|--------------------------------------------------------------------------
|
| When authenticating your first-party SPA with Sanctum you may need to
| customize some of the middleware Sanctum uses while processing the
| request. You may change the middleware listed below as required.
|
*/
'middleware' => [
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
],
];

View File

@ -1,27 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('lokets', function (Blueprint $table) {
$table->id();
$table->string('nama_loket', 100);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('lokets');
}
};

View File

@ -1,32 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->text('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable()->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('personal_access_tokens');
}
};

View File

@ -1,32 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
}
};

View File

@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('antrians', function (Blueprint $table) {
// Ubah enum status untuk menambah 'sedang_diperiksa'
$table->enum('status', ['menunggu', 'dipanggil', 'sedang_diperiksa', 'selesai', 'batal'])
->default('menunggu')
->change();
// Tambah field waktu konfirmasi kehadiran
$table->timestamp('waktu_hadir')->nullable()->after('waktu_panggil');
// Tambah field waktu mulai pemeriksaan
$table->timestamp('waktu_mulai_periksa')->nullable()->after('waktu_hadir');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('antrians', function (Blueprint $table) {
// Kembalikan enum status ke yang lama
$table->enum('status', ['menunggu', 'dipanggil', 'selesai', 'batal'])
->default('menunggu')
->change();
// Hapus field yang ditambahkan
$table->dropColumn(['waktu_hadir', 'waktu_mulai_periksa']);
});
}
};

View File

@ -0,0 +1,92 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
// 1. Drop foreign key constraint for loket_id in antrians table first
Schema::table('antrians', function (Blueprint $table) {
$table->dropForeign(['loket_id']);
$table->dropColumn('loket_id');
});
// 2. Drop unused tables
Schema::dropIfExists('lokets');
Schema::dropIfExists('personal_access_tokens');
Schema::dropIfExists('jobs');
// 3. Drop failed_jobs table if exists (related to jobs)
Schema::dropIfExists('failed_jobs');
Schema::dropIfExists('job_batches');
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// Recreate jobs related tables
Schema::create('jobs', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
// Recreate personal_access_tokens table
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->text('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable()->index();
$table->timestamps();
});
// Recreate lokets table
Schema::create('lokets', function (Blueprint $table) {
$table->id();
$table->string('nama_loket', 100);
$table->timestamps();
});
// Add back loket_id to antrians table
Schema::table('antrians', function (Blueprint $table) {
$table->foreignId('loket_id')->nullable()->constrained('lokets')->onDelete('set null');
});
}
};

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
// Drop password_reset_tokens table since the app uses session-based password reset
// instead of token-based password reset
Schema::dropIfExists('password_reset_tokens');
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// Recreate password_reset_tokens table if rollback is needed
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
}
};

View File

@ -30,18 +30,6 @@ public function run(): void
);
}
// Seed lokets table with upsert
$lokets = [
['id' => 1, 'nama_loket' => 'Loket 1'],
['id' => 2, 'nama_loket' => 'Loket 2'],
];
foreach ($lokets as $loket) {
DB::table('lokets')->updateOrInsert(
['id' => $loket['id']],
['nama_loket' => $loket['nama_loket'], 'updated_at' => now()]
);
}
// Seed users table with upsert
DB::table('users')->updateOrInsert(
['id' => 1],
@ -57,7 +45,7 @@ public function run(): void
]
);
// Seed antrians table with upsert
// Seed antrians table with upsert (removed loket_id reference)
DB::table('antrians')->updateOrInsert(
['id' => 1],
[
@ -68,12 +56,13 @@ public function run(): void
'is_call' => 0,
'status' => 'menunggu',
'waktu_panggil' => null,
'loket_id' => 1,
'waktu_hadir' => null,
'waktu_mulai_periksa' => null,
'updated_at' => now(),
]
);
// Add more antrian data for testing
// Add more antrian data for testing - Poli Gigi
DB::table('antrians')->updateOrInsert(
['id' => 2],
[
@ -84,11 +73,13 @@ public function run(): void
'is_call' => 0,
'status' => 'menunggu',
'waktu_panggil' => null,
'loket_id' => 1,
'waktu_hadir' => null,
'waktu_mulai_periksa' => null,
'updated_at' => now(),
]
);
// Poli Jiwa - Status dipanggil untuk testing
DB::table('antrians')->updateOrInsert(
['id' => 3],
[
@ -96,14 +87,16 @@ public function run(): void
'poli_id' => 3,
'no_antrian' => 'J1',
'tanggal_antrian' => now()->toDateString(),
'is_call' => 0,
'is_call' => 1,
'status' => 'dipanggil',
'waktu_panggil' => now(),
'loket_id' => 1,
'waktu_hadir' => null,
'waktu_mulai_periksa' => null,
'updated_at' => now(),
]
);
// Poli Tradisional - Status sedang diperiksa untuk testing
DB::table('antrians')->updateOrInsert(
['id' => 4],
[
@ -111,10 +104,79 @@ public function run(): void
'poli_id' => 4,
'no_antrian' => 'T1',
'tanggal_antrian' => now()->toDateString(),
'is_call' => 1,
'status' => 'sedang_diperiksa',
'waktu_panggil' => now()->subMinutes(10),
'waktu_hadir' => now()->subMinutes(8),
'waktu_mulai_periksa' => now()->subMinutes(5),
'updated_at' => now(),
]
);
// Tambah satu lagi data sedang diperiksa untuk testing filter
DB::table('antrians')->updateOrInsert(
['id' => 5],
[
'user_id' => 1,
'poli_id' => 1,
'no_antrian' => 'U2',
'tanggal_antrian' => now()->toDateString(),
'is_call' => 1,
'status' => 'sedang_diperiksa',
'waktu_panggil' => now()->subMinutes(15),
'waktu_hadir' => now()->subMinutes(12),
'waktu_mulai_periksa' => now()->subMinutes(8),
'updated_at' => now(),
]
);
// Tambah data dengan status batal untuk testing filter
DB::table('antrians')->updateOrInsert(
['id' => 6],
[
'user_id' => 1,
'poli_id' => 2,
'no_antrian' => 'G2',
'tanggal_antrian' => now()->toDateString(),
'is_call' => 0,
'status' => 'menunggu',
'status' => 'batal',
'waktu_panggil' => null,
'loket_id' => 1,
'waktu_hadir' => null,
'waktu_mulai_periksa' => null,
'updated_at' => now(),
]
);
// Tambah satu lagi data batal untuk testing filter
DB::table('antrians')->updateOrInsert(
['id' => 7],
[
'user_id' => 1,
'poli_id' => 3,
'no_antrian' => 'J2',
'tanggal_antrian' => now()->toDateString(),
'is_call' => 0,
'status' => 'batal',
'waktu_panggil' => null,
'waktu_hadir' => null,
'waktu_mulai_periksa' => null,
'updated_at' => now(),
]
);
// Tambah data dengan status selesai untuk testing filter
DB::table('antrians')->updateOrInsert(
['id' => 8],
[
'user_id' => 1,
'poli_id' => 4,
'no_antrian' => 'T2',
'tanggal_antrian' => now()->toDateString(),
'is_call' => 1,
'status' => 'selesai',
'waktu_panggil' => now()->subMinutes(30),
'waktu_hadir' => now()->subMinutes(25),
'waktu_mulai_periksa' => now()->subMinutes(20),
'updated_at' => now(),
]
);

View File

@ -1,22 +0,0 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\Loket;
class LoketSeeder extends Seeder
{
public function run(): void
{
$lokets = [
['nama_loket' => 'Loket 1'],
['nama_loket' => 'Loket 2'],
['nama_loket' => 'Loket 3'],
];
foreach ($lokets as $loket) {
Loket::create($loket);
}
}
}

View File

@ -1,143 +1,151 @@
@extends('layouts.app')
@section('content')
<div class="min-h-screen bg-gray-50">
@include('admin.partials.top-nav')
<div class="min-h-screen bg-gray-50">
@include('admin.partials.top-nav')
<div class="flex">
@include('admin.partials.sidebar')
<div class="flex">
@include('admin.partials.sidebar')
<!-- Main Content -->
<div class="flex-1 lg:ml-0">
<div class="px-4 sm:px-6 lg:px-8 py-6 md:py-8">
<!-- Header -->
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900 mb-2">Tambah Antrian Manual</h1>
<p class="text-gray-600">Bantu pasien yang tidak bisa antri online dengan membuat antrian manual</p>
</div>
<!-- Main Content -->
<div class="flex-1 lg:ml-0">
<div class="px-4 sm:px-6 lg:px-8 py-6 md:py-8">
<!-- Header -->
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900 mb-2">Tambah Antrian Manual</h1>
<p class="text-gray-600">Bantu pasien yang tidak bisa antri online dengan membuat antrian manual</p>
</div>
<!-- Search User Section -->
<div class="bg-white rounded-lg shadow-sm border p-6 mb-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Cari Pasien</h2>
<div class="flex space-x-4 mb-4">
<div class="flex-1">
<input type="text" id="searchInput" placeholder="Cari berdasarkan nama, NIK, atau nomor HP..."
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
<!-- Search User Section -->
<div class="bg-white rounded-lg shadow-sm border p-6 mb-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Cari Pasien</h2>
<div class="flex space-x-4 mb-4">
<div class="flex-1">
<input type="text" id="searchInput"
placeholder="Cari berdasarkan nama, NIK, atau nomor HP..."
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
</div>
<button id="searchBtn"
class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:ring-2 focus:ring-blue-500">
Cari
</button>
</div>
<button id="searchBtn" class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:ring-2 focus:ring-blue-500">
Cari
<!-- Search Results -->
<div id="searchResults" class="hidden">
<h3 class="text-md font-medium text-gray-900 mb-3">Hasil Pencarian:</h3>
<div id="userList" class="space-y-2"></div>
</div>
<!-- No Results Message -->
<div id="noResults" class="hidden text-center py-4 text-gray-500">
Tidak ada hasil yang ditemukan
</div>
</div>
<!-- Selected User & Poli Selection -->
<div id="userSelection" class="hidden bg-white rounded-lg shadow-sm border p-6 mb-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Pilih Poli</h2>
<div class="mb-4">
<h3 class="text-md font-medium text-gray-700 mb-2">Pasien yang dipilih:</h3>
<div id="selectedUserInfo" class="bg-gray-50 rounded-lg p-3"></div>
</div>
<div class="mb-4">
<label for="poliSelect" class="block text-sm font-medium text-gray-700 mb-2">Pilih Poli:</label>
<select id="poliSelect"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
<option value="">Pilih Poli</option>
@foreach ($polis as $poli)
<option value="{{ $poli->id }}">{{ ucfirst($poli->nama_poli) }}</option>
@endforeach
</select>
</div>
<button id="createQueueBtn"
class="w-full px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 focus:ring-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed"
disabled>
Buat Antrian
</button>
</div>
<!-- Search Results -->
<div id="searchResults" class="hidden">
<h3 class="text-md font-medium text-gray-900 mb-3">Hasil Pencarian:</h3>
<div id="userList" class="space-y-2"></div>
</div>
<!-- No Results Message -->
<div id="noResults" class="hidden text-center py-4 text-gray-500">
Tidak ada hasil yang ditemukan
</div>
</div>
<!-- Selected User & Poli Selection -->
<div id="userSelection" class="hidden bg-white rounded-lg shadow-sm border p-6 mb-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Pilih Poli</h2>
<div class="mb-4">
<h3 class="text-md font-medium text-gray-700 mb-2">Pasien yang dipilih:</h3>
<div id="selectedUserInfo" class="bg-gray-50 rounded-lg p-3"></div>
</div>
<div class="mb-4">
<label for="poliSelect" class="block text-sm font-medium text-gray-700 mb-2">Pilih Poli:</label>
<select id="poliSelect" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
<option value="">Pilih Poli</option>
@foreach($polis as $poli)
<option value="{{ $poli->id }}">{{ ucfirst($poli->nama_poli) }}</option>
@endforeach
</select>
</div>
<button id="createQueueBtn" class="w-full px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 focus:ring-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed" disabled>
Buat Antrian
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Logout Form -->
<form id="logout-form" method="POST" action="{{ route('logout') }}" class="hidden">
@csrf
</form>
<!-- Logout Form -->
<form id="logout-form" method="POST" action="{{ route('logout') }}" class="hidden">
@csrf
</form>
<script>
let selectedUser = null;
<script>
let selectedUser = null;
// Search functionality
document.getElementById('searchBtn').addEventListener('click', function() {
const searchTerm = document.getElementById('searchInput').value.trim();
if (searchTerm.length < 3) {
Swal.fire({
icon: 'warning',
title: 'Peringatan',
text: 'Masukkan minimal 3 karakter untuk pencarian',
confirmButtonColor: '#3B82F6'
// Search functionality
document.getElementById('searchBtn').addEventListener('click', function() {
const searchTerm = document.getElementById('searchInput').value.trim();
if (searchTerm.length < 3) {
Swal.fire({
icon: 'warning',
title: 'Peringatan',
text: 'Masukkan minimal 3 karakter untuk pencarian',
confirmButtonColor: '#3B82F6'
});
return;
}
searchUsers(searchTerm);
});
return;
}
searchUsers(searchTerm);
});
document.getElementById('searchInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
document.getElementById('searchBtn').click();
}
});
document.getElementById('searchInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
document.getElementById('searchBtn').click();
}
});
function searchUsers(searchTerm) {
fetch('{{ route("admin.antrian.cari-user") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({ search: searchTerm })
})
.then(response => response.json())
.then(data => {
if (data.success && data.users.length > 0) {
displaySearchResults(data.users);
} else {
showNoResults();
function searchUsers(searchTerm) {
fetch('{{ route('admin.antrian.cari-user') }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({
search: searchTerm
})
})
.then(response => response.json())
.then(data => {
if (data.success && data.users.length > 0) {
displaySearchResults(data.users);
} else {
showNoResults();
}
})
.catch(error => {
console.error('Error:', error);
Swal.fire({
icon: 'error',
title: 'Error',
text: 'Terjadi kesalahan saat mencari user',
confirmButtonColor: '#EF4444'
});
});
}
})
.catch(error => {
console.error('Error:', error);
Swal.fire({
icon: 'error',
title: 'Error',
text: 'Terjadi kesalahan saat mencari user',
confirmButtonColor: '#EF4444'
});
});
}
function displaySearchResults(users) {
const userList = document.getElementById('userList');
const searchResults = document.getElementById('searchResults');
const noResults = document.getElementById('noResults');
userList.innerHTML = '';
users.forEach(user => {
const userDiv = document.createElement('div');
userDiv.className = 'flex items-center justify-between p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer';
userDiv.innerHTML = `
function displaySearchResults(users) {
const userList = document.getElementById('userList');
const searchResults = document.getElementById('searchResults');
const noResults = document.getElementById('noResults');
userList.innerHTML = '';
users.forEach(user => {
const userDiv = document.createElement('div');
userDiv.className =
'flex items-center justify-between p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer';
userDiv.innerHTML = `
<div>
<div class="font-medium text-gray-900">${user.nama}</div>
<div class="text-sm text-gray-600">NIK: ${user.no_ktp} | HP: ${user.no_hp}</div>
@ -147,117 +155,169 @@ function displaySearchResults(users) {
Pilih
</button>
`;
userList.appendChild(userDiv);
});
searchResults.classList.remove('hidden');
noResults.classList.add('hidden');
}
userList.appendChild(userDiv);
});
function showNoResults() {
document.getElementById('searchResults').classList.add('hidden');
document.getElementById('noResults').classList.remove('hidden');
}
searchResults.classList.remove('hidden');
noResults.classList.add('hidden');
}
function selectUser(user) {
selectedUser = user;
// Display selected user info
const selectedUserInfo = document.getElementById('selectedUserInfo');
selectedUserInfo.innerHTML = `
function showNoResults() {
document.getElementById('searchResults').classList.add('hidden');
document.getElementById('noResults').classList.remove('hidden');
}
function selectUser(user) {
selectedUser = user;
// Display selected user info
const selectedUserInfo = document.getElementById('selectedUserInfo');
selectedUserInfo.innerHTML = `
<div class="font-medium text-gray-900">${user.nama}</div>
<div class="text-sm text-gray-600">NIK: ${user.no_ktp} | HP: ${user.no_hp}</div>
<div class="text-sm text-gray-500">${user.jenis_kelamin} - ${user.alamat}</div>
`;
// Show user selection section
document.getElementById('userSelection').classList.remove('hidden');
// Reset poli selection
document.getElementById('poliSelect').value = '';
document.getElementById('createQueueBtn').disabled = true;
// Hide search results
document.getElementById('searchResults').classList.add('hidden');
document.getElementById('searchInput').value = '';
}
// Poli selection change
document.getElementById('poliSelect').addEventListener('change', function() {
const createQueueBtn = document.getElementById('createQueueBtn');
createQueueBtn.disabled = !this.value;
});
// Show user selection section
document.getElementById('userSelection').classList.remove('hidden');
// Create queue
document.getElementById('createQueueBtn').addEventListener('click', function() {
if (!selectedUser || !document.getElementById('poliSelect').value) {
Swal.fire({
icon: 'warning',
title: 'Peringatan',
text: 'Pilih user dan poli terlebih dahulu',
confirmButtonColor: '#3B82F6'
});
return;
}
createQueue();
});
// Reset poli selection
document.getElementById('poliSelect').value = '';
document.getElementById('createQueueBtn').disabled = true;
function createQueue() {
const poliId = document.getElementById('poliSelect').value;
fetch('{{ route("admin.antrian.store") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({
user_id: selectedUser.id,
poli_id: poliId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: 'success',
title: 'Berhasil!',
text: data.message,
confirmButtonColor: '#10B981'
}).then(() => {
resetForm();
});
} else {
Swal.fire({
icon: 'error',
title: 'Error',
text: data.message,
confirmButtonColor: '#EF4444'
});
// Hide search results
document.getElementById('searchResults').classList.add('hidden');
document.getElementById('searchInput').value = '';
}
})
.catch(error => {
console.error('Error:', error);
Swal.fire({
icon: 'error',
title: 'Error',
text: 'Terjadi kesalahan saat membuat antrian',
confirmButtonColor: '#EF4444'
// Poli selection change
document.getElementById('poliSelect').addEventListener('change', function() {
const createQueueBtn = document.getElementById('createQueueBtn');
createQueueBtn.disabled = !this.value;
});
});
}
function resetForm() {
selectedUser = null;
document.getElementById('userSelection').classList.add('hidden');
document.getElementById('searchResults').classList.add('hidden');
document.getElementById('noResults').classList.add('hidden');
document.getElementById('searchInput').value = '';
document.getElementById('poliSelect').value = '';
document.getElementById('createQueueBtn').disabled = true;
}
</script>
// Create queue
document.getElementById('createQueueBtn').addEventListener('click', function() {
if (!selectedUser || !document.getElementById('poliSelect').value) {
Swal.fire({
icon: 'warning',
title: 'Peringatan',
text: 'Pilih user dan poli terlebih dahulu',
confirmButtonColor: '#3B82F6'
});
return;
}
@include('admin.partials.sidebar-script')
createQueue();
});
function createQueue() {
const poliId = document.getElementById('poliSelect').value;
fetch('{{ route('admin.antrian.store') }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({
user_id: selectedUser.id,
poli_id: poliId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: 'success',
title: 'Berhasil!',
text: data.message,
confirmButtonColor: '#10B981'
}).then(() => {
resetForm();
});
} else {
// Check if it's an existing queue error with additional details
if (data.existing_queue) {
let statusText = '';
let alertIcon = 'warning';
let titleText = 'Antrian Sudah Ada!';
switch (data.existing_queue.status) {
case 'menunggu':
statusText = 'Menunggu';
break;
case 'dipanggil':
statusText = 'Dipanggil';
alertIcon = 'info';
titleText = 'Antrian Sedang Dipanggil!';
break;
case 'sedang_diperiksa':
statusText = 'Sedang Diperiksa';
alertIcon = 'error';
titleText = 'Sedang Dalam Pemeriksaan!';
break;
}
Swal.fire({
icon: alertIcon,
title: titleText,
html: `
<div class="text-left">
<p class="mb-3">${data.message}</p>
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-3 mb-3">
<div class="flex items-center">
<svg class="w-5 h-5 text-yellow-600 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
</svg>
<span class="font-medium text-yellow-800">Detail Antrian Aktif:</span>
</div>
<div class="mt-2 text-sm text-yellow-700">
<p><strong>No. Antrian:</strong> ${data.existing_queue.no_antrian}</p>
<p><strong>Status:</strong> ${statusText}</p>
</div>
</div>
${data.existing_queue.status === 'sedang_diperiksa'
? '<p class="text-sm text-red-600 font-medium">⚠️ User tidak dapat mengambil antrian baru di poli yang sama selama masih dalam proses pemeriksaan.</p>'
: '<p class="text-sm text-gray-600">Silakan tunggu hingga antrian selesai atau dibatalkan terlebih dahulu.</p>'
}
</div>
`,
confirmButtonColor: data.existing_queue.status === 'sedang_diperiksa' ?
'#EF4444' : '#F59E0B'
});
} else {
// Generic error
Swal.fire({
icon: 'error',
title: 'Error',
text: data.message,
confirmButtonColor: '#EF4444'
});
}
}
})
.catch(error => {
console.error('Error:', error);
Swal.fire({
icon: 'error',
title: 'Error',
text: 'Terjadi kesalahan saat membuat antrian',
confirmButtonColor: '#EF4444'
});
});
}
function resetForm() {
selectedUser = null;
document.getElementById('userSelection').classList.add('hidden');
document.getElementById('searchResults').classList.add('hidden');
document.getElementById('noResults').classList.add('hidden');
document.getElementById('searchInput').value = '';
document.getElementById('poliSelect').value = '';
document.getElementById('createQueueBtn').disabled = true;
}
</script>
@include('admin.partials.sidebar-script')
@endsection

View File

@ -19,68 +19,64 @@
</div>
<!-- Poli Summary -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="bg-white rounded-2xl shadow-xl p-6">
<div class="flex items-center">
<div class="p-3 rounded-full bg-blue-100 text-blue-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
</div>
<div class="ml-4">
<p class="text-sm text-gray-500">Poli Umum</p>
<p class="text-2xl font-bold text-gray-900">{{ $poliUmumCount ?? 0 }}</p>
<p class="text-xs text-gray-400">Antrian menunggu</p>
</div>
<div class="grid grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="bg-white rounded-2xl shadow-xl p-6 flex flex-col lg:flex-row items-center lg:items-start justify-center lg:justify-start text-center lg:text-left"
data-poli-count="umum">
<div class="p-3 rounded-full bg-blue-100 text-blue-600 mb-2 lg:mb-0 lg:mr-4">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
</div>
<div>
<p class="text-sm text-gray-500">Poli Umum</p>
<p class="text-2xl font-bold text-gray-900">{{ $poliUmumCount ?? 0 }}</p>
<p class="text-xs text-gray-400">Antrian menunggu</p>
</div>
</div>
<div class="bg-white rounded-2xl shadow-xl p-6">
<div class="flex items-center">
<div class="p-3 rounded-full bg-green-100 text-green-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
</div>
<div class="ml-4">
<p class="text-sm text-gray-500">Poli Gigi</p>
<p class="text-2xl font-bold text-gray-900">{{ $poliGigiCount ?? 0 }}</p>
<p class="text-xs text-gray-400">Antrian menunggu</p>
</div>
<div class="bg-white rounded-2xl shadow-xl p-6 flex flex-col lg:flex-row items-center lg:items-start justify-center lg:justify-start text-center lg:text-left"
data-poli-count="gigi">
<div class="p-3 rounded-full bg-green-100 text-green-600 mb-2 lg:mb-0 lg:mr-4">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
</div>
<div>
<p class="text-sm text-gray-500">Poli Gigi</p>
<p class="text-2xl font-bold text-gray-900">{{ $poliGigiCount ?? 0 }}</p>
<p class="text-xs text-gray-400">Antrian menunggu</p>
</div>
</div>
<div class="bg-white rounded-2xl shadow-xl p-6">
<div class="flex items-center">
<div class="p-3 rounded-full bg-purple-100 text-purple-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
</div>
<div class="ml-4">
<p class="text-sm text-gray-500">Poli Jiwa</p>
<p class="text-2xl font-bold text-gray-900">{{ $poliJiwaCount ?? 0 }}</p>
<p class="text-xs text-gray-400">Antrian menunggu</p>
</div>
<div class="bg-white rounded-2xl shadow-xl p-6 flex flex-col lg:flex-row items-center lg:items-start justify-center lg:justify-start text-center lg:text-left"
data-poli-count="jiwa">
<div class="p-3 rounded-full bg-purple-100 text-purple-600 mb-2 lg:mb-0 lg:mr-4">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
</div>
<div>
<p class="text-sm text-gray-500">Poli Jiwa</p>
<p class="text-2xl font-bold text-gray-900">{{ $poliJiwaCount ?? 0 }}</p>
<p class="text-xs text-gray-400">Antrian menunggu</p>
</div>
</div>
<div class="bg-white rounded-2xl shadow-xl p-6">
<div class="flex items-center">
<div class="p-3 rounded-full bg-yellow-100 text-yellow-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
</div>
<div class="ml-4">
<p class="text-sm text-gray-500">Poli Tradisional</p>
<p class="text-2xl font-bold text-gray-900">{{ $poliTradisionalCount ?? 0 }}</p>
<p class="text-xs text-gray-400">Antrian menunggu</p>
</div>
<div class="bg-white rounded-2xl shadow-xl p-6 flex flex-col lg:flex-row items-center lg:items-start justify-center lg:justify-start text-center lg:text-left"
data-poli-count="tradisional">
<div class="p-3 rounded-full bg-yellow-100 text-yellow-600 mb-2 lg:mb-0 lg:mr-4">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
</div>
<div>
<p class="text-sm text-gray-500">Poli Tradisional</p>
<p class="text-2xl font-bold text-gray-900">{{ $poliTradisionalCount ?? 0 }}</p>
<p class="text-xs text-gray-400">Antrian menunggu</p>
</div>
</div>
</div>
@ -102,8 +98,8 @@
class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition duration-200">
Tambah Antrian
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 5l7 7-7 7"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7">
</path>
</svg>
</a>
</div>
@ -156,43 +152,105 @@ class="inline-flex items-center px-4 py-2 bg-purple-600 text-white rounded-lg ho
<!-- Recent Activity -->
<div class="bg-white rounded-2xl shadow-xl p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Aktivitas Terbaru</h3>
<div class="space-y-4">
<div class="flex items-center p-4 bg-gray-50 rounded-lg">
<div class="p-2 rounded-full bg-blue-100 text-blue-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
<div class="space-y-4" data-recent-activity>
@if (isset($antrianTerbaru) && $antrianTerbaru->count() > 0)
@foreach ($antrianTerbaru as $antrian)
<div class="flex items-center p-4 bg-gray-50 rounded-lg">
@if ($antrian->status == 'menunggu')
<div class="p-2 rounded-full bg-blue-100 text-blue-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-900">Antrian baru ditambahkan</p>
<p class="text-xs text-gray-500">{{ $antrian->poli->nama_poli }} -
{{ $antrian->created_at->diffForHumans() }}</p>
</div>
@elseif($antrian->status == 'dipanggil')
<div class="p-2 rounded-full bg-yellow-100 text-yellow-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-900">Antrian dipanggil</p>
<p class="text-xs text-gray-500">{{ $antrian->poli->nama_poli }} -
{{ $antrian->updated_at->diffForHumans() }}</p>
</div>
@elseif($antrian->status == 'selesai')
<div class="p-2 rounded-full bg-green-100 text-green-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 13l4 4L19 7"></path>
</svg>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-900">Antrian selesai</p>
<p class="text-xs text-gray-500">{{ $antrian->poli->nama_poli }} -
{{ $antrian->updated_at->diffForHumans() }}</p>
</div>
@elseif($antrian->status == 'sedang_diperiksa')
<div class="p-2 rounded-full bg-purple-100 text-purple-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-900">Sedang diperiksa</p>
<p class="text-xs text-gray-500">{{ $antrian->poli->nama_poli }} -
{{ $antrian->updated_at->diffForHumans() }}</p>
</div>
@elseif($antrian->status == 'batal')
<div class="p-2 rounded-full bg-red-100 text-red-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
</svg>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-900">Antrian dibatalkan</p>
<p class="text-xs text-gray-500">{{ $antrian->poli->nama_poli }} -
{{ $antrian->updated_at->diffForHumans() }}</p>
</div>
@else
<div class="p-2 rounded-full bg-gray-100 text-gray-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z">
</path>
</svg>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-900">Status tidak dikenal</p>
<p class="text-xs text-gray-500">{{ $antrian->poli->nama_poli }} -
{{ $antrian->updated_at->diffForHumans() }}</p>
</div>
@endif
</div>
@endforeach
@else
<div class="flex items-center justify-center p-6 bg-gray-50 rounded-lg">
<div class="text-center">
<svg class="w-8 h-8 mx-auto text-gray-400 mb-2" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z">
</path>
</svg>
<p class="text-gray-500 text-sm">Belum ada aktivitas hari ini</p>
</div>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-900">Antrian baru ditambahkan</p>
<p class="text-xs text-gray-500">Poli Umum - 2 menit yang lalu</p>
</div>
</div>
<div class="flex items-center p-4 bg-gray-50 rounded-lg">
<div class="p-2 rounded-full bg-green-100 text-green-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 13l4 4L19 7"></path>
</svg>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-900">Antrian selesai</p>
<p class="text-xs text-gray-500">Poli Gigi - 5 menit yang lalu</p>
</div>
</div>
<div class="flex items-center p-4 bg-gray-50 rounded-lg">
<div class="p-2 rounded-full bg-yellow-100 text-yellow-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-900">Antrian dipanggil</p>
<p class="text-xs text-gray-500">Poli Jiwa - 8 menit yang lalu</p>
</div>
</div>
@endif
</div>
</div>
</div>
@ -206,4 +264,121 @@ class="inline-flex items-center px-4 py-2 bg-purple-600 text-white rounded-lg ho
</form>
@include('admin.partials.sidebar-script')
@push('scripts')
<script>
// Auto refresh dashboard data every 30 seconds
function refreshDashboard() {
fetch('{{ route('admin.dashboard.api') }}', {
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Update poli counts
updatePoliCount('umum', data.data.poliUmumCount);
updatePoliCount('gigi', data.data.poliGigiCount);
updatePoliCount('jiwa', data.data.poliJiwaCount);
updatePoliCount('tradisional', data.data.poliTradisionalCount);
// Update recent activities
updateRecentActivities(data.data.antrianTerbaru);
}
})
.catch(error => {
console.log('Dashboard refresh error:', error);
});
}
function updatePoliCount(poliType, count) {
const countElement = document.querySelector(`[data-poli-count="${poliType}"] .text-2xl`);
if (countElement) {
countElement.textContent = count;
}
}
function updateRecentActivities(activities) {
const container = document.querySelector('[data-recent-activity]');
if (!container) return;
if (activities && activities.length > 0) {
container.innerHTML = activities.map(antrian => {
let iconColor, iconPath, statusText;
switch (antrian.status) {
case 'menunggu':
iconColor = 'bg-blue-100 text-blue-600';
iconPath = 'M12 6v6m0 0v6m0-6h6m-6 0H6';
statusText = 'Antrian baru ditambahkan';
break;
case 'dipanggil':
iconColor = 'bg-yellow-100 text-yellow-600';
iconPath = 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z';
statusText = 'Antrian dipanggil';
break;
case 'sedang_diperiksa':
iconColor = 'bg-purple-100 text-purple-600';
iconPath = 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z';
statusText = 'Sedang diperiksa';
break;
case 'selesai':
iconColor = 'bg-green-100 text-green-600';
iconPath = 'M5 13l4 4L19 7';
statusText = 'Antrian selesai';
break;
case 'batal':
iconColor = 'bg-red-100 text-red-600';
iconPath = 'M6 18L18 6M6 6l12 12';
statusText = 'Antrian dibatalkan';
break;
default:
iconColor = 'bg-gray-100 text-gray-600';
iconPath =
'M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z';
statusText = 'Status tidak dikenal';
}
return `
<div class="flex items-center p-4 bg-gray-50 rounded-lg">
<div class="p-2 rounded-full ${iconColor}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${iconPath}"></path>
</svg>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-900">${statusText}</p>
<p class="text-xs text-gray-500">${antrian.poli_name} - ${antrian.created_at}</p>
</div>
</div>
`;
}).join('');
} else {
container.innerHTML = `
<div class="flex items-center justify-center p-6 bg-gray-50 rounded-lg">
<div class="text-center">
<svg class="w-8 h-8 mx-auto text-gray-400 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<p class="text-gray-500 text-sm">Belum ada aktivitas hari ini</p>
</div>
</div>
`;
}
}
// Start auto refresh
setInterval(refreshDashboard, 30000); // Refresh every 30 seconds
// Also refresh when page becomes visible (user switches back to tab)
document.addEventListener('visibilitychange', function() {
if (!document.hidden) {
refreshDashboard();
}
});
</script>
@endpush
@endsection

View File

@ -4,142 +4,10 @@
@section('content')
<div class="min-h-screen bg-gray-50">
<!-- Top Navigation -->
<nav class="bg-white shadow-lg sticky top-0 z-40">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
<button id="sidebar-toggle" class="lg:hidden text-gray-700 hover:text-primary mr-4">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
<div class="flex-shrink-0">
<h1 class="text-xl md:text-2xl font-bold text-primary">🏥 Admin Puskesmas</h1>
</div>
</div>
<div class="hidden md:flex items-center space-x-4">
<span class="text-gray-700">Selamat datang, Admin</span>
<button onclick="confirmLogout()"
class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md text-sm font-medium transition duration-200">
Logout
</button>
</div>
</div>
</div>
</nav>
@include('admin.partials.top-nav')
<div class="flex">
<!-- Sidebar -->
<aside id="sidebar"
class="fixed inset-y-0 left-0 z-50 w-64 bg-white shadow-xl transform -translate-x-full lg:translate-x-0 lg:static lg:inset-0 transition duration-200 ease-in-out">
<div class="flex items-center justify-between h-16 px-6 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-900">Menu Admin</h2>
<button id="sidebar-close" class="lg:hidden text-gray-500 hover:text-gray-700">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12">
</path>
</svg>
</button>
</div>
<nav class="px-6 py-6">
<div class="space-y-6">
<!-- Dashboard -->
<div>
<a href="{{ route('admin.dashboard') }}"
class="flex items-center px-4 py-3 rounded-xl {{ request()->routeIs('admin.dashboard') ? 'bg-blue-50 text-blue-700 border border-blue-200' : 'text-gray-700 hover:bg-gray-50' }} transition duration-200">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 5a2 2 0 012-2h4a2 2 0 012 2v6H8V5z"></path>
</svg>
<span class="font-medium">Dashboard</span>
</a>
</div>
<!-- Daftar Antrian -->
<div>
<div
class="flex items-center px-4 py-2 text-sm font-semibold text-gray-500 uppercase tracking-wider">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">
</path>
</svg>
Daftar Antrian
</div>
<div class="mt-3 space-y-1">
<a href="{{ route('admin.poli.umum') }}"
class="flex items-center px-4 py-2 rounded-lg {{ request()->routeIs('admin.poli.umum') ? 'bg-blue-50 text-blue-700' : 'text-gray-600 hover:bg-gray-50' }} transition duration-200">
<div class="w-2 h-2 bg-blue-500 rounded-full mr-3"></div>
<span class="text-sm">Poli Umum</span>
</a>
<a href="{{ route('admin.poli.gigi') }}"
class="flex items-center px-4 py-2 rounded-lg {{ request()->routeIs('admin.poli.gigi') ? 'bg-green-50 text-green-700' : 'text-gray-600 hover:bg-gray-50' }} transition duration-200">
<div class="w-2 h-2 bg-green-500 rounded-full mr-3"></div>
<span class="text-sm">Poli Gigi</span>
</a>
<a href="{{ route('admin.poli.jiwa') }}"
class="flex items-center px-4 py-2 rounded-lg {{ request()->routeIs('admin.poli.jiwa') ? 'bg-purple-50 text-purple-700' : 'text-gray-600 hover:bg-gray-50' }} transition duration-200">
<div class="w-2 h-2 bg-purple-500 rounded-full mr-3"></div>
<span class="text-sm">Poli Jiwa</span>
</a>
<a href="{{ route('admin.poli.tradisional') }}"
class="flex items-center px-4 py-2 rounded-lg {{ request()->routeIs('admin.poli.tradisional') ? 'bg-yellow-50 text-yellow-700' : 'text-gray-600 hover:bg-gray-50' }} transition duration-200">
<div class="w-2 h-2 bg-yellow-500 rounded-full mr-3"></div>
<span class="text-sm">Poli Tradisional</span>
</a>
</div>
</div>
<!-- Kelola User -->
<div>
<a href="{{ route('admin.users.index') }}"
class="flex items-center px-4 py-3 rounded-xl {{ request()->routeIs('admin.users.*') ? 'bg-blue-50 text-blue-700 border border-blue-200' : 'text-gray-700 hover:bg-gray-50' }} transition duration-200">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z">
</path>
</svg>
<span class="font-medium">Kelola User</span>
</a>
</div>
<!-- Laporan -->
<div>
<a href="{{ route('admin.laporan.index') }}"
class="flex items-center px-4 py-3 rounded-xl {{ request()->routeIs('admin.laporan.*') ? 'bg-blue-50 text-blue-700 border border-blue-200' : 'text-gray-700 hover:bg-gray-50' }} transition duration-200">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z">
</path>
</svg>
<span class="font-medium">Laporan</span>
</a>
</div>
<!-- Display -->
<div>
<a href="{{ route('display') }}"
class="flex items-center px-4 py-3 rounded-xl text-gray-700 hover:bg-gray-50 transition duration-200">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z">
</path>
</svg>
<span class="font-medium">Display</span>
</a>
</div>
</div>
</nav>
</aside>
<!-- Overlay for mobile -->
<div id="sidebar-overlay" class="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden hidden"></div>
@include('admin.partials.sidebar')
<!-- Main Content -->
<div class="flex-1 lg:ml-0">
@ -266,7 +134,7 @@ class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shad
</div>
<!-- Statistics Cards -->
<div class="grid grid-cols-1 md:grid-cols-5 gap-6 mb-8">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-6 mb-8">
<div class="bg-white rounded-2xl shadow-xl border p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
@ -358,6 +226,24 @@ class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shad
</div>
</div>
</div>
<div class="bg-white rounded-2xl shadow-xl border p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-red-100 rounded-lg flex items-center justify-center">
<svg class="w-5 h-5 text-red-600" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
</svg>
</div>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">Batal</p>
<p class="text-2xl font-bold text-gray-900">{{ $antrianBatal }}</p>
</div>
</div>
</div>
</div>
<!-- Data Table -->
@ -427,7 +313,7 @@ class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium b
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
Dipanggil
</span>
@elseif($item->status == 'sedang')
@elseif($item->status == 'sedang_diperiksa')
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
Sedang
@ -504,7 +390,7 @@ class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium b
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
Dipanggil
</span>
@elseif($item->status == 'sedang')
@elseif($item->status == 'sedang_diperiksa')
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
Sedang
@ -568,6 +454,11 @@ class="text-gray-600">{{ $item->waktu_panggil ? $item->waktu_panggil->format('H:
</div>
@endforelse
</div>
<!-- Pagination -->
<div class="mt-8">
{{ $antrian->withQueryString()->links() }}
</div>
</div>
</div>
</div>
@ -661,4 +552,6 @@ function toggleLaporanDetails(id) {
}
</script>
@endpush
@include('admin.partials.sidebar-script')
@endsection

View File

@ -116,7 +116,16 @@
<td>{{ $item->user->no_ktp }}</td>
<td>{{ $item->poli->nama_poli }}</td>
<td class="status-{{ $item->status }}">
{{ ucfirst($item->status) }}
@php
$statusMap = [
'menunggu' => 'Menunggu',
'dipanggil' => 'Dipanggil',
'sedang_diperiksa' => 'Sedang Diperiksa',
'selesai' => 'Selesai',
'batal' => 'Batal',
];
@endphp
{{ $statusMap[$item->status] ?? ucfirst(str_replace('_', ' ', $item->status)) }}
</td>
<td>{{ $item->created_at ? $item->created_at->format('d/m/Y') : '-' }}</td>
<td>{{ $item->created_at ? $item->created_at->format('H:i') : '-' }}</td>

View File

@ -1,49 +1,113 @@
<script>
// Sidebar functionality
document.addEventListener('DOMContentLoaded', function() {
const sidebar = document.getElementById('sidebar');
const sidebarToggle = document.getElementById('sidebar-toggle');
const sidebarClose = document.getElementById('sidebar-close');
const sidebarOverlay = document.getElementById('sidebar-overlay');
const mainContent = document.querySelector('.flex-1');
// Sidebar functionality
document.addEventListener('DOMContentLoaded', function() {
const sidebar = document.getElementById('sidebar');
const sidebarToggle = document.getElementById('sidebar-toggle');
const sidebarClose = document.getElementById('sidebar-close');
const sidebarOverlay = document.getElementById('sidebar-overlay');
// Toggle sidebar on mobile
if (sidebarToggle) {
sidebarToggle.addEventListener('click', function() {
sidebar.classList.remove('-translate-x-full');
sidebarOverlay.classList.remove('hidden');
// Toggle sidebar on mobile
if (sidebarToggle) {
sidebarToggle.addEventListener('click', function() {
sidebar.classList.remove('-translate-x-full');
sidebarOverlay.classList.remove('hidden');
});
}
// Close sidebar on mobile
if (sidebarClose) {
sidebarClose.addEventListener('click', function() {
sidebar.classList.add('-translate-x-full');
sidebarOverlay.classList.add('hidden');
});
}
// Close sidebar when clicking overlay
if (sidebarOverlay) {
sidebarOverlay.addEventListener('click', function() {
sidebar.classList.add('-translate-x-full');
sidebarOverlay.classList.add('hidden');
});
}
// Close sidebar on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
sidebar.classList.add('-translate-x-full');
sidebarOverlay.classList.add('hidden');
}
});
}
// Close sidebar on mobile
if (sidebarClose) {
sidebarClose.addEventListener('click', function() {
sidebar.classList.add('-translate-x-full');
sidebarOverlay.classList.add('hidden');
});
}
// Mobile menu dropdown functionality
const mobileMenuButton = document.getElementById('mobile-menu-button');
const mobileMenu = document.getElementById('mobile-menu');
// Close sidebar when clicking overlay
if (sidebarOverlay) {
sidebarOverlay.addEventListener('click', function() {
sidebar.classList.add('-translate-x-full');
sidebarOverlay.classList.add('hidden');
});
}
if (mobileMenuButton && mobileMenu) {
mobileMenuButton.addEventListener('click', function(e) {
e.stopPropagation();
mobileMenu.classList.toggle('hidden');
});
// Close sidebar on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
sidebar.classList.add('-translate-x-full');
sidebarOverlay.classList.add('hidden');
// Close mobile menu when clicking outside
document.addEventListener('click', function(e) {
if (!mobileMenuButton.contains(e.target) && !mobileMenu.contains(e.target)) {
mobileMenu.classList.add('hidden');
}
});
// Close mobile menu on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
mobileMenu.classList.add('hidden');
}
});
}
});
});
// Logout confirmation
function confirmLogout() {
if (confirm('Apakah Anda yakin ingin keluar?')) {
document.getElementById('logout-form').submit();
// Logout confirmation
function confirmLogout() {
Swal.fire({
title: 'Konfirmasi Logout',
text: 'Apakah Anda yakin ingin keluar dari sistem?',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Ya, Keluar',
cancelButtonText: 'Batal',
customClass: {
popup: 'rounded-lg',
confirmButton: 'btn btn-danger px-4 py-2 rounded-md',
cancelButton: 'btn btn-secondary px-4 py-2 rounded-md'
}
}).then((result) => {
if (result.isConfirmed) {
// Show loading
Swal.fire({
title: 'Logging out...',
text: 'Mohon tunggu sebentar',
icon: 'info',
allowOutsideClick: false,
showConfirmButton: false,
willOpen: () => {
Swal.showLoading();
}
});
// Create and submit logout form
const form = document.createElement('form');
form.method = 'POST';
form.action = '{{ route('logout') }}';
const csrfToken = document.createElement('input');
csrfToken.type = 'hidden';
csrfToken.name = '_token';
csrfToken.value = '{{ csrf_token() }}';
form.appendChild(csrfToken);
document.body.appendChild(form);
form.submit();
}
});
}
}
</script>

View File

@ -1,6 +1,6 @@
<!-- Sidebar -->
<aside id="sidebar"
class="fixed inset-y-0 left-0 z-50 w-64 bg-white shadow-xl transform -translate-x-full lg:translate-x-0 lg:static lg:inset-0 transition duration-200 ease-in-out">
class="fixed inset-y-0 left-0 z-40 w-64 bg-white shadow-xl transform -translate-x-full lg:translate-x-0 lg:static lg:inset-0 transition duration-200 ease-in-out">
<div class="flex items-center justify-between h-16 px-6 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-900">Menu Admin</h2>
<button id="sidebar-close" class="lg:hidden text-gray-500 hover:text-gray-700">
@ -99,11 +99,9 @@ class="flex items-center px-4 py-3 rounded-xl {{ request()->routeIs('admin.lapor
</a>
</div>
<!-- Audio Management -->
<!-- Audio Management - HIDDEN FOR NOW -->
<!-- Uncomment section below to re-enable Audio Management -->
<!--
<div>
<a href="{{ route('admin.audio.index') }}"
class="flex items-center px-4 py-3 rounded-xl {{ request()->routeIs('admin.audio.*') ? 'bg-blue-50 text-blue-700 border border-blue-200' : 'text-gray-700 hover:bg-gray-50' }} transition duration-200">
@ -115,6 +113,7 @@ class="flex items-center px-4 py-3 rounded-xl {{ request()->routeIs('admin.audio
<span class="font-medium">Audio Management</span>
</a>
</div>
-->
<!-- Display -->
<div>
@ -129,8 +128,21 @@ class="flex items-center px-4 py-3 rounded-xl text-gray-700 hover:bg-gray-50 tra
</a>
</div>
</div>
<!-- Logout Section (Mobile Only) -->
<div class="lg:hidden mt-6 pt-6 border-t border-gray-200">
<button onclick="confirmLogout()"
class="flex items-center w-full px-4 py-3 rounded-xl text-red-600 hover:bg-red-50 transition duration-200">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1">
</path>
</svg>
<span class="font-medium">Logout</span>
</button>
</div>
</nav>
</aside>
<!-- Overlay for mobile -->
<div id="sidebar-overlay" class="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden hidden"></div>
<div id="sidebar-overlay" class="fixed inset-0 bg-black bg-opacity-50 z-30 lg:hidden hidden"></div>

View File

@ -1,5 +1,5 @@
<!-- Top Navigation -->
<nav class="bg-white shadow-lg sticky top-0 z-40">
<nav class="bg-white shadow-lg sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
@ -13,14 +13,44 @@
<h1 class="text-xl md:text-2xl font-bold text-primary">🏥 Admin Puskesmas</h1>
</div>
</div>
<!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-4">
<span class="text-gray-700">Selamat datang, Admin</span>
<button onclick="confirmLogout()"
class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md text-sm font-medium transition duration-200">
Logout
</button>
</div>
<!-- Mobile Navigation -->
<div class="md:hidden flex items-center">
<div class="relative">
<button id="mobile-menu-button" class="text-gray-700 hover:text-primary p-2 rounded-md">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
</button>
<!-- Mobile Dropdown Menu -->
<div id="mobile-menu"
class="hidden absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-50">
<div class="px-4 py-2 text-sm text-gray-700 border-b">
<span class="font-medium">Admin</span>
</div>
<button onclick="confirmLogout()"
class="block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 transition duration-200">
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1">
</path>
</svg>
Logout
</button>
</div>
</div>
</div>
</div>
</div>
</nav>

View File

@ -2,144 +2,51 @@
@section('title', $title)
@push('styles')
<style>
/* Tooltip styles */
[title]:hover {
position: relative;
}
[title]:hover:after {
content: attr(title);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
z-index: 1000;
pointer-events: none;
max-width: 300px;
white-space: normal;
word-wrap: break-word;
}
[title]:hover:before {
content: '';
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%) translateY(100%);
border: 5px solid transparent;
border-top-color: rgba(0, 0, 0, 0.8);
z-index: 1000;
}
</style>
@endpush
@section('content')
<div class="min-h-screen bg-gray-50">
<!-- Top Navigation -->
<nav class="bg-white shadow-lg sticky top-0 z-40">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
<button id="sidebar-toggle" class="lg:hidden text-gray-700 hover:text-primary mr-4">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
<div class="flex-shrink-0">
<h1 class="text-xl md:text-2xl font-bold text-primary">🏥 Admin Puskesmas</h1>
</div>
</div>
<div class="hidden md:flex items-center space-x-4">
<span class="text-gray-700">Selamat datang, Admin</span>
<button onclick="confirmLogout()"
class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md text-sm font-medium transition duration-200">
Logout
</button>
</div>
</div>
</div>
</nav>
@include('admin.partials.top-nav')
<div class="flex">
<!-- Sidebar -->
<aside id="sidebar"
class="fixed inset-y-0 left-0 z-50 w-64 bg-white shadow-xl transform -translate-x-full lg:translate-x-0 lg:static lg:inset-0 transition duration-200 ease-in-out">
<div class="flex items-center justify-between h-16 px-6 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-900">Menu Admin</h2>
<button id="sidebar-close" class="lg:hidden text-gray-500 hover:text-gray-700">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12">
</path>
</svg>
</button>
</div>
<nav class="px-6 py-6">
<div class="space-y-6">
<!-- Dashboard -->
<div>
<a href="{{ route('admin.dashboard') }}"
class="flex items-center px-4 py-3 rounded-xl {{ request()->routeIs('admin.dashboard') ? 'bg-blue-50 text-blue-700 border border-blue-200' : 'text-gray-700 hover:bg-gray-50' }} transition duration-200">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 5a2 2 0 012-2h4a2 2 0 012 2v6H8V5z"></path>
</svg>
<span class="font-medium">Dashboard</span>
</a>
</div>
<!-- Daftar Antrian -->
<div>
<div
class="flex items-center px-4 py-2 text-sm font-semibold text-gray-500 uppercase tracking-wider">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">
</path>
</svg>
Daftar Antrian
</div>
<div class="mt-3 space-y-1">
<a href="{{ route('admin.poli.umum') }}"
class="flex items-center px-4 py-2 rounded-lg {{ request()->routeIs('admin.poli.umum') ? 'bg-blue-50 text-blue-700' : 'text-gray-600 hover:bg-gray-50' }} transition duration-200">
<div class="w-2 h-2 bg-blue-500 rounded-full mr-3"></div>
<span class="text-sm">Poli Umum</span>
</a>
<a href="{{ route('admin.poli.gigi') }}"
class="flex items-center px-4 py-2 rounded-lg {{ request()->routeIs('admin.poli.gigi') ? 'bg-green-50 text-green-700' : 'text-gray-600 hover:bg-gray-50' }} transition duration-200">
<div class="w-2 h-2 bg-green-500 rounded-full mr-3"></div>
<span class="text-sm">Poli Gigi</span>
</a>
<a href="{{ route('admin.poli.jiwa') }}"
class="flex items-center px-4 py-2 rounded-lg {{ request()->routeIs('admin.poli.jiwa') ? 'bg-purple-50 text-purple-700' : 'text-gray-600 hover:bg-gray-50' }} transition duration-200">
<div class="w-2 h-2 bg-purple-500 rounded-full mr-3"></div>
<span class="text-sm">Poli Jiwa</span>
</a>
<a href="{{ route('admin.poli.tradisional') }}"
class="flex items-center px-4 py-2 rounded-lg {{ request()->routeIs('admin.poli.tradisional') ? 'bg-yellow-50 text-yellow-700' : 'text-gray-600 hover:bg-gray-50' }} transition duration-200">
<div class="w-2 h-2 bg-yellow-500 rounded-full mr-3"></div>
<span class="text-sm">Poli Tradisional</span>
</a>
</div>
</div>
<!-- Kelola User -->
<div>
<a href="{{ route('admin.users.index') }}"
class="flex items-center px-4 py-3 rounded-xl {{ request()->routeIs('admin.users.*') ? 'bg-blue-50 text-blue-700 border border-blue-200' : 'text-gray-700 hover:bg-gray-50' }} transition duration-200">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z">
</path>
</svg>
<span class="font-medium">Kelola User</span>
</a>
</div>
<!-- Laporan -->
<div>
<a href="{{ route('admin.laporan.index') }}"
class="flex items-center px-4 py-3 rounded-xl {{ request()->routeIs('admin.laporan.*') ? 'bg-blue-50 text-blue-700 border border-blue-200' : 'text-gray-700 hover:bg-gray-50' }} transition duration-200">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z">
</path>
</svg>
<span class="font-medium">Laporan</span>
</a>
</div>
<!-- Display -->
<div>
<a href="{{ route('display') }}"
class="flex items-center px-4 py-3 rounded-xl text-gray-700 hover:bg-gray-50 transition duration-200">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z">
</path>
</svg>
<span class="font-medium">Display</span>
</a>
</div>
</div>
</nav>
</aside>
<!-- Overlay for mobile -->
<div id="sidebar-overlay" class="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden hidden"></div>
@include('admin.partials.sidebar')
<!-- Main Content -->
<div class="flex-1 lg:ml-0">
@ -158,28 +65,28 @@ class="flex items-center px-4 py-3 rounded-xl text-gray-700 hover:bg-gray-50 tra
<thead class="bg-gray-50">
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-20">
No Antrian</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Nama</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-32">
Alamat</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Jenis Kelamin</th>
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-24">
JK</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Nomor HP</th>
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-28">
No HP</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Nomor KTP</th>
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-32">
No KTP</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-24">
Status</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-32">
Aksi</th>
</tr>
</thead>
@ -190,9 +97,15 @@ class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-
{{ $antrian->no_antrian }}</td>
<td class="px-6 py-4 whitespace-nowrap text-gray-900">
{{ $antrian->user?->nama }}</td>
<td class="px-6 py-4 text-gray-700">{{ $antrian->user?->alamat }}</td>
<td class="px-6 py-4 whitespace-nowrap text-gray-700">
{{ $antrian->user?->jenis_kelamin }}</td>
<td class="px-6 py-4 text-gray-700 max-w-32"
title="{{ $antrian->user?->alamat }}">
<div class="truncate">
{{ Str::limit($antrian->user?->alamat, 30) }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-gray-700"
title="{{ $antrian->user?->jenis_kelamin }}">
{{ $antrian->user?->jenis_kelamin == 'laki-laki' ? 'L' : 'P' }}</td>
<td class="px-6 py-4 whitespace-nowrap text-gray-700">
{{ $antrian->user?->no_hp }}</td>
<td class="px-6 py-4 whitespace-nowrap text-gray-700">
@ -208,6 +121,11 @@ class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-yellow-100 te
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-blue-100 text-blue-800">
Dipanggil
</span>
@elseif($antrian->status == 'sedang_diperiksa')
<span
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-purple-100 text-purple-800">
Sedang Diperiksa
</span>
@elseif($antrian->status == 'selesai')
<span
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800">
@ -228,17 +146,38 @@ class="bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded-md text-xs fon
Panggil
</button>
@elseif($antrian->status == 'dipanggil')
<div class="flex flex-col space-y-1">
@if (!$antrian->waktu_hadir)
<button onclick="konfirmasiKehadiran({{ $antrian->id }})"
class="bg-orange-500 hover:bg-orange-600 text-white px-2 py-1 rounded-md text-xs font-medium transition duration-200">
Pasien Hadir
</button>
@else
<button onclick="mulaiPemeriksaan({{ $antrian->id }})"
class="bg-purple-500 hover:bg-purple-600 text-white px-2 py-1 rounded-md text-xs font-medium transition duration-200">
Mulai Periksa
</button>
@endif
<div class="flex space-x-1">
<button
onclick="selesai('{{ route('admin.selesai-antrian') }}', {{ $antrian->id }})"
class="bg-green-500 hover:bg-green-600 text-white px-2 py-1 rounded-md text-xs font-medium transition duration-200">
Selesai
</button>
<button
onclick="batal('{{ route('admin.batal-antrian') }}', {{ $antrian->id }})"
class="bg-red-500 hover:bg-red-600 text-white px-2 py-1 rounded-md text-xs font-medium transition duration-200">
Batal
</button>
</div>
</div>
@elseif($antrian->status == 'sedang_diperiksa')
<div class="flex space-x-2">
<button
onclick="selesai('{{ route('admin.selesai-antrian') }}', {{ $antrian->id }})"
class="bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded-md text-xs font-medium transition duration-200">
Selesai
</button>
<button
onclick="batal('{{ route('admin.batal-antrian') }}', {{ $antrian->id }})"
class="bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded-md text-xs font-medium transition duration-200">
Batal
</button>
</div>
@else
<span class="text-gray-400 text-xs">-</span>
@ -291,6 +230,11 @@ class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-yellow-100 te
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-blue-100 text-blue-800">
Dipanggil
</span>
@elseif($antrian->status == 'sedang_diperiksa')
<span
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-purple-100 text-purple-800">
Sedang Diperiksa
</span>
@elseif($antrian->status == 'selesai')
<span
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800">
@ -312,17 +256,38 @@ class="bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded-md text-xs fon
Panggil
</button>
@elseif($antrian->status == 'dipanggil')
<div class="flex flex-col space-y-1">
@if (!$antrian->waktu_hadir)
<button onclick="konfirmasiKehadiran({{ $antrian->id }})"
class="bg-orange-500 hover:bg-orange-600 text-white px-2 py-1 rounded-md text-xs font-medium transition duration-200">
Pasien Hadir
</button>
@else
<button onclick="mulaiPemeriksaan({{ $antrian->id }})"
class="bg-purple-500 hover:bg-purple-600 text-white px-2 py-1 rounded-md text-xs font-medium transition duration-200">
Mulai Periksa
</button>
@endif
<div class="flex space-x-1">
<button
onclick="selesai('{{ route('admin.selesai-antrian') }}', {{ $antrian->id }})"
class="bg-green-500 hover:bg-green-600 text-white px-2 py-1 rounded-md text-xs font-medium transition duration-200">
Selesai
</button>
<button
onclick="batal('{{ route('admin.batal-antrian') }}', {{ $antrian->id }})"
class="bg-red-500 hover:bg-red-600 text-white px-2 py-1 rounded-md text-xs font-medium transition duration-200">
Batal
</button>
</div>
</div>
@elseif($antrian->status == 'sedang_diperiksa')
<div class="flex space-x-2">
<button
onclick="selesai('{{ route('admin.selesai-antrian') }}', {{ $antrian->id }})"
class="bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded-md text-xs font-medium transition duration-200">
Selesai
</button>
<button
onclick="batal('{{ route('admin.batal-antrian') }}', {{ $antrian->id }})"
class="bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded-md text-xs font-medium transition duration-200">
Batal
</button>
</div>
@else
<span class="text-gray-400 text-xs">-</span>
@ -348,7 +313,10 @@ class="w-4 h-4 ml-1 transform transition-transform" fill="none"
<div class="grid grid-cols-1 gap-2 text-sm">
<div>
<span class="font-medium text-gray-700">Alamat:</span>
<span class="text-gray-600">{{ $antrian->user?->alamat }}</span>
<span class="text-gray-600 block mt-1"
title="{{ $antrian->user?->alamat }}">
{{ Str::limit($antrian->user?->alamat, 50) }}
</span>
</div>
<div>
<span class="font-medium text-gray-700">Jenis Kelamin:</span>
@ -380,6 +348,11 @@ class="text-gray-600">{{ $antrian->user?->jenis_kelamin }}</span>
</div>
@endforelse
</div>
<!-- Pagination -->
<div class="mt-8">
{{ $antrians->links() }}
</div>
</div>
</div>
</div>
@ -679,6 +652,80 @@ function batal(url, antrianId) {
});
}
function konfirmasiKehadiran(antrianId) {
Swal.fire({
title: 'Konfirmasi Kehadiran Pasien?',
text: 'Pasien sudah datang dan siap untuk diperiksa?',
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Ya, Pasien Hadir',
cancelButtonText: 'Batal'
}).then((res) => {
if (!res.isConfirmed) return;
fetch('{{ route('admin.konfirmasi-kehadiran') }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json'
},
body: JSON.stringify({
antrian_id: antrianId
})
})
.then(r => r.json())
.then(d => {
Swal.fire({
icon: d.success ? 'success' : 'warning',
title: d.success ? 'Berhasil' : 'Gagal',
text: d.message
})
.then(() => location.reload());
})
.catch(() => Swal.fire({
icon: 'error',
title: 'Error',
text: 'Terjadi kesalahan'
}));
});
}
function mulaiPemeriksaan(antrianId) {
Swal.fire({
title: 'Mulai Pemeriksaan?',
text: 'Pasien akan dipindah ke status sedang diperiksa',
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Ya, Mulai Periksa',
cancelButtonText: 'Batal'
}).then((res) => {
if (!res.isConfirmed) return;
fetch('{{ route('admin.mulai-pemeriksaan') }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json'
},
body: JSON.stringify({
antrian_id: antrianId
})
})
.then(r => r.json())
.then(d => {
Swal.fire({
icon: d.success ? 'success' : 'warning',
title: d.success ? 'Berhasil' : 'Gagal',
text: d.message
})
.then(() => location.reload());
})
.catch(() => Swal.fire({
icon: 'error',
title: 'Error',
text: 'Terjadi kesalahan'
}));
});
}
function confirmLogout() {
Swal.fire({
title: 'Konfirmasi Logout',
@ -724,4 +771,6 @@ function toggleDetails(antrianId) {
}
</script>
@endpush
@include('admin.partials.sidebar-script')
@endsection

View File

@ -5,7 +5,7 @@
@section('content')
<div class="min-h-screen bg-gray-50">
<!-- Top Navigation -->
<nav class="bg-white shadow-lg sticky top-0 z-40">
<nav class="bg-white shadow-lg sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
@ -20,7 +20,6 @@
</div>
</div>
<div class="hidden md:flex items-center space-x-4">
<span class="text-gray-700">Selamat datang, Admin</span>
<button onclick="confirmLogout()"
class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md text-sm font-medium transition duration-200">
@ -32,107 +31,7 @@ class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md text-sm font-
</nav>
<div class="flex">
<!-- Sidebar -->
<aside id="sidebar"
class="fixed inset-y-0 left-0 z-50 w-64 bg-white shadow-xl transform -translate-x-full lg:translate-x-0 lg:static lg:inset-0 transition duration-200 ease-in-out">
<div class="flex items-center justify-between h-16 px-6 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-900">Menu Admin</h2>
<button id="sidebar-close" class="lg:hidden text-gray-500 hover:text-gray-700">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12">
</path>
</svg>
</button>
</div>
<nav class="px-6 py-6">
<div class="space-y-6">
<!-- Dashboard -->
<div>
<a href="{{ route('admin.dashboard') }}"
class="flex items-center px-4 py-3 rounded-xl {{ request()->routeIs('admin.dashboard') ? 'bg-blue-50 text-blue-700 border border-blue-200' : 'text-gray-700 hover:bg-gray-50' }} transition duration-200">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 5a2 2 0 012-2h4a2 2 0 012 2v6H8V5z"></path>
</svg>
<span class="font-medium">Dashboard</span>
</a>
</div>
<!-- Daftar Antrian -->
<div>
<div
class="flex items-center px-4 py-2 text-sm font-semibold text-gray-500 uppercase tracking-wider">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">
</path>
</svg>
Daftar Antrian
</div>
<div class="mt-3 space-y-1">
<a href="{{ route('admin.poli.umum') }}"
class="flex items-center px-4 py-2 rounded-lg {{ request()->routeIs('admin.poli.umum') ? 'bg-blue-50 text-blue-700' : 'text-gray-600 hover:bg-gray-50' }} transition duration-200">
<div class="w-2 h-2 bg-blue-500 rounded-full mr-3"></div>
<span class="text-sm">Poli Umum</span>
</a>
<a href="{{ route('admin.poli.gigi') }}"
class="flex items-center px-4 py-2 rounded-lg {{ request()->routeIs('admin.poli.gigi') ? 'bg-green-50 text-green-700' : 'text-gray-600 hover:bg-gray-50' }} transition duration-200">
<div class="w-2 h-2 bg-green-500 rounded-full mr-3"></div>
<span class="text-sm">Poli Gigi</span>
</a>
<a href="{{ route('admin.poli.jiwa') }}"
class="flex items-center px-4 py-2 rounded-lg {{ request()->routeIs('admin.poli.jiwa') ? 'bg-purple-50 text-purple-700' : 'text-gray-600 hover:bg-gray-50' }} transition duration-200">
<div class="w-2 h-2 bg-purple-500 rounded-full mr-3"></div>
<span class="text-sm">Poli Jiwa</span>
</a>
<a href="{{ route('admin.poli.tradisional') }}"
class="flex items-center px-4 py-2 rounded-lg {{ request()->routeIs('admin.poli.tradisional') ? 'bg-yellow-50 text-yellow-700' : 'text-gray-600 hover:bg-gray-50' }} transition duration-200">
<div class="w-2 h-2 bg-yellow-500 rounded-full mr-3"></div>
<span class="text-sm">Poli Tradisional</span>
</a>
</div>
</div>
<!-- Kelola User -->
<div>
<div
class="flex items-center px-4 py-2 text-sm font-semibold text-gray-500 uppercase tracking-wider">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z">
</path>
</svg>
Kelola User
</div>
<div class="mt-3 space-y-1">
<a href="{{ route('admin.users.index') }}"
class="flex items-center px-4 py-2 rounded-lg {{ request()->routeIs('admin.users.*') ? 'bg-blue-50 text-blue-700' : 'text-gray-600 hover:bg-gray-50' }} transition duration-200">
<div class="w-2 h-2 bg-blue-500 rounded-full mr-3"></div>
<span class="text-sm">Daftar User</span>
</a>
</div>
</div>
<!-- Laporan -->
<div>
<a href="{{ route('admin.laporan.index') }}"
class="flex items-center px-4 py-3 rounded-xl {{ request()->routeIs('admin.laporan.*') ? 'bg-blue-50 text-blue-700 border border-blue-200' : 'text-gray-700 hover:bg-gray-50' }} transition duration-200">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z">
</path>
</svg>
<span class="font-medium">Laporan</span>
</a>
</div>
</div>
</nav>
</aside>
@include('admin.partials.sidebar')
<!-- Main Content -->
<div class="flex-1 lg:ml-64">
@ -179,8 +78,7 @@ class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shad
<div>
<label for="nama" class="block text-sm font-medium text-gray-700 mb-2">Nama
Lengkap <span class="text-red-500">*</span></label>
<input type="text" name="nama" id="nama" value="{{ old('nama') }}"
required
<input type="text" name="nama" id="nama" value="{{ old('nama') }}" required
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
@ -189,7 +87,8 @@ class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outlin
<label for="no_ktp" class="block text-sm font-medium text-gray-700 mb-2">Nomor KTP
<span class="text-red-500">*</span></label>
<input type="text" name="no_ktp" id="no_ktp" value="{{ old('no_ktp') }}"
maxlength="16" required
maxlength="16" required pattern="[0-9]*" inputmode="numeric"
oninput="this.value = this.value.replace(/[^0-9]/g, '')"
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="16 digit angka">
</div>
@ -212,8 +111,9 @@ class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outlin
<div>
<label for="no_hp" class="block text-sm font-medium text-gray-700 mb-2">Nomor HP
<span class="text-red-500">*</span></label>
<input type="text" name="no_hp" id="no_hp" value="{{ old('no_hp') }}"
required
<input type="text" name="no_hp" id="no_hp" value="{{ old('no_hp') }}" required
pattern="[0-9]*" inputmode="numeric"
oninput="this.value = this.value.replace(/[^0-9]/g, '')"
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
@ -221,8 +121,8 @@ class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outlin
<div>
<label for="pekerjaan" class="block text-sm font-medium text-gray-700 mb-2">Pekerjaan
<span class="text-red-500">*</span></label>
<input type="text" name="pekerjaan" id="pekerjaan"
value="{{ old('pekerjaan') }}" required
<input type="text" name="pekerjaan" id="pekerjaan" value="{{ old('pekerjaan') }}"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>

View File

@ -108,6 +108,8 @@ class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:rin
<label for="edit_no_hp" class="block text-sm font-medium text-gray-700 mb-2">Nomor
HP</label>
<input type="tel" name="no_hp" id="edit_no_hp" value="{{ $user->no_hp }}" required
pattern="[0-9]*" inputmode="numeric"
oninput="this.value = this.value.replace(/[^0-9]/g, '')"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-200">
</div>
@ -115,6 +117,8 @@ class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:rin
<label for="edit_no_ktp" class="block text-sm font-medium text-gray-700 mb-2">Nomor
KTP</label>
<input type="text" name="no_ktp" id="edit_no_ktp" value="{{ $user->no_ktp }}" required
pattern="[0-9]*" inputmode="numeric"
oninput="this.value = this.value.replace(/[^0-9]/g, '')"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-200">
</div>
@ -232,24 +236,38 @@ class="px-6 py-3 bg-red-600 hover:bg-red-700 text-white rounded-lg font-medium t
<h2 class="text-xl font-semibold text-gray-900">Statistik Antrian User</h2>
</div>
<div class="p-6">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
<div class="bg-gradient-to-br from-blue-50 to-blue-100 rounded-xl p-4 border border-blue-200">
<div class="text-2xl font-bold text-blue-600">
{{ $user->antrians->where('status', 'menunggu')->count() }}
</div>
<div class="text-sm text-blue-700 font-medium">Antrian Menunggu</div>
<div class="text-sm text-blue-700 font-medium">Menunggu</div>
</div>
<div
class="bg-gradient-to-br from-yellow-50 to-yellow-100 rounded-xl p-4 border border-yellow-200">
<div class="text-2xl font-bold text-yellow-600">
{{ $user->antrians->where('status', 'dipanggil')->count() }}
</div>
<div class="text-sm text-yellow-700 font-medium">Dipanggil</div>
</div>
<div
class="bg-gradient-to-br from-purple-50 to-purple-100 rounded-xl p-4 border border-purple-200">
<div class="text-2xl font-bold text-purple-600">
{{ $user->antrians->where('status', 'sedang_diperiksa')->count() }}
</div>
<div class="text-sm text-purple-700 font-medium">Sedang Periksa</div>
</div>
<div class="bg-gradient-to-br from-green-50 to-green-100 rounded-xl p-4 border border-green-200">
<div class="text-2xl font-bold text-green-600">
{{ $user->antrians->where('status', 'selesai')->count() }}
</div>
<div class="text-sm text-green-700 font-medium">Antrian Selesai</div>
<div class="text-sm text-green-700 font-medium">Selesai</div>
</div>
<div class="bg-gradient-to-br from-red-50 to-red-100 rounded-xl p-4 border border-red-200">
<div class="text-2xl font-bold text-red-600">
{{ $user->antrians->where('status', 'batal')->count() }}
</div>
<div class="text-sm text-red-700 font-medium">Antrian Batal</div>
<div class="text-sm text-red-700 font-medium">Batal</div>
</div>
<div class="bg-gradient-to-br from-gray-50 to-gray-100 rounded-xl p-4 border border-gray-200">
<div class="text-2xl font-bold text-gray-600">
@ -287,7 +305,7 @@ class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@foreach ($user->antrians->take(5) as $antrian)
@forelse ($user->antrians->take(5) as $antrian)
<tr class="hover:bg-gray-50 transition duration-200">
<td class="px-4 py-3 whitespace-nowrap">
<span
@ -310,6 +328,11 @@ class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-yellow-100 te
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-blue-100 text-blue-800">
Dipanggil
</span>
@elseif($antrian->status == 'sedang_diperiksa')
<span
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-purple-100 text-purple-800">
Sedang Diperiksa
</span>
@elseif($antrian->status == 'selesai')
<span
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800">
@ -326,7 +349,21 @@ class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-red-100 text-
{{ $antrian->created_at ? $antrian->created_at->format('d/m/Y H:i') : 'N/A' }}
</td>
</tr>
@endforeach
@empty
<tr>
<td colspan="4" class="px-4 py-8 text-center text-gray-500">
<svg class="w-12 h-12 mx-auto mb-4 text-gray-400" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z">
</path>
</svg>
<p class="text-lg font-medium">Belum ada antrian</p>
<p class="text-sm text-gray-400">User ini belum pernah mengambil antrian
</p>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@ -466,5 +503,100 @@ function togglePassword(inputId) {
});
});
});
// Auto refresh table riwayat antrian setiap 10 detik
setInterval(function() {
refreshRiwayatAntrian();
}, 10000);
function refreshRiwayatAntrian() {
fetch(`{{ route('admin.users.antrian-terbaru', $user->id) }}`, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
updateRiwayatAntrianTable(data.antrians);
}
})
.catch(error => {
console.log('Error refreshing riwayat antrian:', error);
});
}
function updateRiwayatAntrianTable(antrians) {
const tableBody = document.querySelector('tbody');
if (!tableBody) return;
if (antrians.length === 0) {
tableBody.innerHTML = `
<tr>
<td colspan="4" class="px-4 py-8 text-center text-gray-500">
<svg class="w-12 h-12 mx-auto mb-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<p class="text-lg font-medium">Belum ada antrian</p>
<p class="text-sm text-gray-400">User ini belum pernah mengambil antrian</p>
</td>
</tr>
`;
return;
}
tableBody.innerHTML = antrians.map(antrian => {
let statusBadge = '';
switch (antrian.status) {
case 'menunggu':
statusBadge =
'<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-yellow-100 text-yellow-800">Menunggu</span>';
break;
case 'dipanggil':
statusBadge =
'<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-blue-100 text-blue-800">Dipanggil</span>';
break;
case 'sedang_diperiksa':
statusBadge =
'<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-purple-100 text-purple-800">Sedang Diperiksa</span>';
break;
case 'selesai':
statusBadge =
'<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800">Selesai</span>';
break;
default:
statusBadge =
'<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-red-100 text-red-800">Batal</span>';
}
return `
<tr class="hover:bg-gray-50 transition duration-200">
<td class="px-4 py-3 whitespace-nowrap">
<span class="text-lg font-semibold text-blue-600">${antrian.no_antrian}</span>
</td>
<td class="px-4 py-3 whitespace-nowrap">
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-blue-100 text-blue-800">
${antrian.poli_name}
</span>
</td>
<td class="px-4 py-3 whitespace-nowrap">
${statusBadge}
</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
${antrian.created_at}
</td>
</tr>
`;
}).join('');
// Add subtle animation to indicate update
tableBody.style.transition = 'opacity 0.3s ease';
tableBody.style.opacity = '0.7';
setTimeout(() => {
tableBody.style.opacity = '1';
}, 300);
}
</script>
@endsection

View File

@ -52,8 +52,27 @@ class="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl placeholde
</svg>
</div>
<input id="password" name="password" type="password" required
class="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary transition duration-200 text-sm"
class="block w-full pl-10 pr-12 py-3 border border-gray-300 rounded-xl placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary transition duration-200 text-sm"
placeholder="Masukkan password Anda">
<button type="button" id="togglePassword"
class="absolute inset-y-0 right-0 pr-3 flex items-center cursor-pointer hover:text-gray-600 transition-colors duration-200">
<!-- Eye Icon (show) -->
<svg id="eyeOpen" class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z">
</path>
</svg>
<!-- Eye Slash Icon (hide) -->
<svg id="eyeClosed" class="h-5 w-5 text-gray-400 hidden" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21">
</path>
</svg>
</button>
</div>
</div>
@ -122,6 +141,23 @@ class="text-blue-100 hover:text-white text-sm transition duration-200 flex items
@push('scripts')
<script>
// Toggle password visibility
document.getElementById('togglePassword').addEventListener('click', function() {
const passwordField = document.getElementById('password');
const eyeOpen = document.getElementById('eyeOpen');
const eyeClosed = document.getElementById('eyeClosed');
if (passwordField.type === 'password') {
passwordField.type = 'text';
eyeOpen.classList.add('hidden');
eyeClosed.classList.remove('hidden');
} else {
passwordField.type = 'password';
eyeOpen.classList.remove('hidden');
eyeClosed.classList.add('hidden');
}
});
// Show SweetAlert2 for errors
@if ($errors->any())
Swal.fire({

View File

@ -92,7 +92,8 @@ class="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl text-gray-
</div>
<input id="no_hp" name="no_hp" type="tel" required
class="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary transition duration-200 text-sm"
placeholder="Masukkan nomor HP" value="{{ old('no_hp') }}">
placeholder="Masukkan nomor HP" value="{{ old('no_hp') }}" pattern="[0-9]*"
inputmode="numeric" oninput="this.value = this.value.replace(/[^0-9]/g, '')">
</div>
</div>

View File

@ -0,0 +1,54 @@
@if ($paginator->hasPages())
<nav role="navigation" aria-label="Pagination Navigation"
class="flex justify-between items-center bg-white px-4 py-3 border-t border-gray-200 rounded-lg shadow-sm">
@if ($paginator->onFirstPage())
<span
class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-400 bg-gray-100 border border-gray-300 cursor-default leading-5 rounded-lg">
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
clip-rule="evenodd" />
</svg>
Previous
</span>
@else
<a href="{{ $paginator->previousPageUrl() }}"
class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-lg hover:bg-blue-50 hover:text-blue-600 hover:border-blue-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200">
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
clip-rule="evenodd" />
</svg>
Previous
</a>
@endif
<div class="text-sm text-gray-600 bg-gray-50 px-3 py-2 rounded-lg">
<span class="font-semibold text-gray-900">{{ $paginator->currentPage() }}</span>
dari
<span class="font-semibold text-gray-900">{{ $paginator->lastPage() }}</span>
</div>
@if ($paginator->hasMorePages())
<a href="{{ $paginator->nextPageUrl() }}"
class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-lg hover:bg-blue-50 hover:text-blue-600 hover:border-blue-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200">
Next
<svg class="w-4 h-4 ml-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd" />
</svg>
</a>
@else
<span
class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-400 bg-gray-100 border border-gray-300 cursor-default leading-5 rounded-lg">
Next
<svg class="w-4 h-4 ml-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd" />
</svg>
</span>
@endif
</nav>
@endif

View File

@ -0,0 +1,147 @@
@if ($paginator->hasPages())
<nav role="navigation" aria-label="Pagination Navigation"
class="flex items-center justify-between bg-white px-4 py-3 border-t border-gray-200 sm:px-6 rounded-lg shadow-sm">
<div class="flex justify-between flex-1 sm:hidden">
@if ($paginator->onFirstPage())
<span
class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-400 bg-gray-100 border border-gray-300 cursor-default leading-5 rounded-lg">
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
clip-rule="evenodd" />
</svg>
Previous
</span>
@else
<a href="{{ $paginator->previousPageUrl() }}"
class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-lg hover:bg-blue-50 hover:text-blue-600 hover:border-blue-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200">
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
clip-rule="evenodd" />
</svg>
Previous
</a>
@endif
@if ($paginator->hasMorePages())
<a href="{{ $paginator->nextPageUrl() }}"
class="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-lg hover:bg-blue-50 hover:text-blue-600 hover:border-blue-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200">
Next
<svg class="w-4 h-4 ml-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd" />
</svg>
</a>
@else
<span
class="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-400 bg-gray-100 border border-gray-300 cursor-default leading-5 rounded-lg">
Next
<svg class="w-4 h-4 ml-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd" />
</svg>
</span>
@endif
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-600 leading-5 bg-gray-50 px-3 py-2 rounded-lg">
Menampilkan
<span class="font-semibold text-gray-900">{{ $paginator->firstItem() }}</span>
sampai
<span class="font-semibold text-gray-900">{{ $paginator->lastItem() }}</span>
dari
<span class="font-semibold text-gray-900">{{ $paginator->total() }}</span>
hasil
</p>
</div>
<div>
<span class="relative z-0 inline-flex shadow-lg rounded-lg overflow-hidden">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<span aria-disabled="true" aria-label="Previous">
<span
class="relative inline-flex items-center px-3 py-2 text-sm font-medium text-gray-400 bg-gray-100 border border-gray-300 cursor-default leading-5"
aria-hidden="true">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
clip-rule="evenodd" />
</svg>
</span>
</span>
@else
<a href="{{ $paginator->previousPageUrl() }}" rel="prev"
class="relative inline-flex items-center px-3 py-2 text-sm font-medium text-gray-600 bg-white border border-gray-300 leading-5 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-300 focus:z-10 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200"
aria-label="Previous">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
clip-rule="evenodd" />
</svg>
</a>
@endif
{{-- Pagination Elements --}}
@foreach ($elements as $element)
{{-- "Three Dots" Separator --}}
@if (is_string($element))
<span aria-disabled="true">
<span
class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 cursor-default leading-5">{{ $element }}</span>
</span>
@endif
{{-- Array Of Links --}}
@if (is_array($element))
@foreach ($element as $page => $url)
@if ($page == $paginator->currentPage())
<span aria-current="page">
<span
class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-semibold text-white bg-gradient-to-r from-blue-500 to-blue-600 border border-blue-600 cursor-default leading-5 shadow-md">{{ $page }}</span>
</span>
@else
<a href="{{ $url }}"
class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-300 focus:z-10 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200"
aria-label="Go to page {{ $page }}">
{{ $page }}
</a>
@endif
@endforeach
@endif
@endforeach
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<a href="{{ $paginator->nextPageUrl() }}" rel="next"
class="relative inline-flex items-center px-3 py-2 -ml-px text-sm font-medium text-gray-600 bg-white border border-gray-300 leading-5 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-300 focus:z-10 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200"
aria-label="Next">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd" />
</svg>
</a>
@else
<span aria-disabled="true" aria-label="Next">
<span
class="relative inline-flex items-center px-3 py-2 -ml-px text-sm font-medium text-gray-400 bg-gray-100 border border-gray-300 cursor-default leading-5"
aria-hidden="true">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd" />
</svg>
</span>
</span>
@endif
</span>
</div>
</div>
</nav>
@endif

View File

@ -224,6 +224,11 @@ class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-yellow-100 te
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-blue-100 text-blue-800">
Dipanggil
</span>
@elseif(($antrian->status ?? '') == 'sedang_diperiksa')
<span
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-purple-100 text-purple-800">
Sedang Diperiksa
</span>
@elseif(($antrian->status ?? '') == 'selesai')
<span
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800">
@ -333,6 +338,11 @@ class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-yellow-100 te
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-blue-100 text-blue-800">
Dipanggil
</span>
@elseif(($antrian->status ?? '') == 'sedang_diperiksa')
<span
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-purple-100 text-purple-800">
Sedang Diperiksa
</span>
@elseif(($antrian->status ?? '') == 'selesai')
<span
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800">
@ -445,7 +455,8 @@ class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:rin
<label for="edit_no_hp" class="block text-sm font-medium text-gray-700 mb-2">Nomor
HP</label>
<input type="tel" name="no_hp" id="edit_no_hp"
value="{{ auth()->user()->no_hp }}" required
value="{{ auth()->user()->no_hp }}" required pattern="[0-9]*" inputmode="numeric"
oninput="this.value = this.value.replace(/[^0-9]/g, '')"
class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-200">
</div>
@ -453,7 +464,8 @@ class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:rin
<label for="edit_no_ktp" class="block text-sm font-medium text-gray-700 mb-2">Nomor
KTP</label>
<input type="text" name="no_ktp" id="edit_no_ktp"
value="{{ auth()->user()->no_ktp }}" required
value="{{ auth()->user()->no_ktp }}" required pattern="[0-9]*" inputmode="numeric"
oninput="this.value = this.value.replace(/[^0-9]/g, '')"
class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-200">
</div>
@ -683,9 +695,29 @@ function closeEditModal() {
// Handle different error types
if (data.type === 'existing_queue') {
// User already has queue in same poli
let statusText = '';
let alertIcon = 'warning';
let titleText = 'Antrian Sudah Ada!';
switch (data.existing_queue.status) {
case 'menunggu':
statusText = 'Menunggu';
break;
case 'dipanggil':
statusText = 'Dipanggil';
alertIcon = 'info';
titleText = 'Antrian Sedang Dipanggil!';
break;
case 'sedang_diperiksa':
statusText = 'Sedang Diperiksa';
alertIcon = 'error';
titleText = 'Sedang Dalam Pemeriksaan!';
break;
}
Swal.fire({
icon: 'warning',
title: 'Antrian Sudah Ada!',
icon: alertIcon,
title: titleText,
html: `
<div class="text-left">
<p class="mb-3">${data.message}</p>
@ -694,20 +726,24 @@ function closeEditModal() {
<svg class="w-5 h-5 text-yellow-600 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
</svg>
<span class="font-medium text-yellow-800">Detail Antrian:</span>
<span class="font-medium text-yellow-800">Detail Antrian Aktif:</span>
</div>
<div class="mt-2 text-sm text-yellow-700">
<p><strong>No. Antrian:</strong> ${data.existing_queue.no_antrian}</p>
<p><strong>Poli:</strong> ${data.existing_queue.poli_name}</p>
<p><strong>Status:</strong> ${data.existing_queue.status === 'menunggu' ? 'Menunggu' : 'Dipanggil'}</p>
<p><strong>Status:</strong> ${statusText}</p>
<p><strong>Waktu Ambil:</strong> ${data.existing_queue.created_at}</p>
</div>
</div>
<p class="text-sm text-gray-600">Silakan tunggu hingga antrian Anda dipanggil atau batalkan antrian ini terlebih dahulu.</p>
${data.existing_queue.status === 'sedang_diperiksa'
? '<p class="text-sm text-red-600 font-medium">⚠️ Anda tidak dapat mengambil antrian baru di poli yang sama selama masih dalam proses pemeriksaan.</p>'
: '<p class="text-sm text-gray-600">Silakan tunggu hingga antrian Anda dipanggil atau batalkan antrian ini terlebih dahulu.</p>'
}
</div>
`,
confirmButtonText: 'OK',
confirmButtonColor: '#F59E0B'
confirmButtonColor: data.existing_queue.status === 'sedang_diperiksa' ?
'#EF4444' : '#F59E0B'
});
} else {
// Generic error

View File

@ -599,20 +599,49 @@ function updateDisplayData() {
fetch('/api/display-data')
.then(response => response.json())
.then(data => {
// Update current numbers
if (data.poliUmumCurrent) {
document.getElementById('poli-umum-current').textContent = data.poliUmumCurrent.no_antrian;
// Poli Umum
const umumCurrent = document.getElementById('poli-umum-current');
if (data.poliUmumCurrent && ['menunggu', 'dipanggil', 'sedang_diperiksa'].includes(data
.poliUmumCurrent.status)) {
umumCurrent.textContent = data.poliUmumCurrent.no_antrian;
} else {
umumCurrent.textContent = '---';
}
if (data.poliGigiCurrent) {
document.getElementById('poli-gigi-current').textContent = data.poliGigiCurrent.no_antrian;
umumCurrent.classList.remove('text-blue-200');
umumCurrent.classList.add('text-blue-600');
// Poli Gigi
const gigiCurrent = document.getElementById('poli-gigi-current');
if (data.poliGigiCurrent && ['menunggu', 'dipanggil', 'sedang_diperiksa'].includes(data
.poliGigiCurrent.status)) {
gigiCurrent.textContent = data.poliGigiCurrent.no_antrian;
} else {
gigiCurrent.textContent = '---';
}
if (data.poliJiwaCurrent) {
document.getElementById('poli-jiwa-current').textContent = data.poliJiwaCurrent.no_antrian;
gigiCurrent.classList.remove('text-green-200');
gigiCurrent.classList.add('text-green-600');
// Poli Jiwa
const jiwaCurrent = document.getElementById('poli-jiwa-current');
if (data.poliJiwaCurrent && ['menunggu', 'dipanggil', 'sedang_diperiksa'].includes(data
.poliJiwaCurrent.status)) {
jiwaCurrent.textContent = data.poliJiwaCurrent.no_antrian;
} else {
jiwaCurrent.textContent = '---';
}
if (data.poliTradisionalCurrent) {
document.getElementById('poli-tradisional-current').textContent = data.poliTradisionalCurrent
.no_antrian;
jiwaCurrent.classList.remove('text-pink-200');
jiwaCurrent.classList.add('text-pink-600');
// Poli Tradisional
const tradCurrent = document.getElementById('poli-tradisional-current');
if (data.poliTradisionalCurrent && ['menunggu', 'dipanggil', 'sedang_diperiksa'].includes(data
.poliTradisionalCurrent.status)) {
tradCurrent.textContent = data.poliTradisionalCurrent.no_antrian;
} else {
tradCurrent.textContent = '---';
}
tradCurrent.classList.remove('text-yellow-200');
tradCurrent.classList.add('text-yellow-600');
// Update next queues
updateNextQueue('poli-umum-next', data.poliUmumNext);

View File

@ -4,9 +4,16 @@
@section('content')
@if (session('success'))
<div id="success-message"
class="fixed top-4 right-4 bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded shadow-lg z-50">
{{ session('success') }}
<div id="success-alert"
class="fixed top-4 right-4 bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded shadow-lg z-50 transition-all duration-300 transform translate-x-0 opacity-100">
<div class="flex items-center">
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd"></path>
</svg>
{{ session('success') }}
</div>
</div>
@endif
@ -60,7 +67,7 @@ class="bg-primary hover:bg-secondary text-white block px-3 py-2 rounded-md text-
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 md:py-24">
<div class="text-center animate-fade-in">
<h1 class="text-4xl md:text-6xl lg:text-7xl font-bold mb-6 leading-tight">
Sistem Antrian Puskesmas
Puskesmas Mlandingan
</h1>
<p class="text-xl md:text-2xl mb-8 text-blue-100 max-w-4xl mx-auto">
Antrian digital yang memudahkan pelayanan kesehatan masyarakat
@ -87,27 +94,60 @@ class="border-2 border-white text-white hover:bg-white hover:text-primary px-8 p
<p class="text-lg md:text-xl text-gray-600 max-w-3xl mx-auto">Berbagai layanan kesehatan yang tersedia</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-12">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 md:gap-12">
<div
class="bg-white p-6 md:p-8 rounded-2xl shadow-xl border border-gray-200 hover:shadow-2xl transition duration-300 transform hover:-translate-y-2 animate-slide-up">
<div class="text-4xl md:text-5xl mb-4">👨‍⚕️</div>
<div class="text-4xl md:text-5xl mb-4 flex justify-center">
<svg class="w-12 h-12 md:w-16 md:h-16 text-red-600" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
</div>
<h3 class="text-xl md:text-2xl font-semibold mb-3">Poli Umum</h3>
<p class="text-gray-600 text-sm md:text-base">Layanan pemeriksaan kesehatan umum untuk semua usia</p>
</div>
<div class="bg-white p-6 md:p-8 rounded-2xl shadow-xl border border-gray-200 hover:shadow-2xl transition duration-300 transform hover:-translate-y-2 animate-slide-up"
style="animation-delay: 0.1s;">
<div class="text-4xl md:text-5xl mb-4">👶</div>
<h3 class="text-xl md:text-2xl font-semibold mb-3">Poli Anak</h3>
<p class="text-gray-600 text-sm md:text-base">Layanan kesehatan khusus untuk anak-anak</p>
<div class="text-4xl md:text-5xl mb-4 flex justify-center">
<svg class="w-12 h-12 md:w-16 md:h-16 text-blue-600" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z">
</path>
</svg>
</div>
<h3 class="text-xl md:text-2xl font-semibold mb-3">Poli Gigi</h3>
<p class="text-gray-600 text-sm md:text-base">Layanan perawatan dan pengobatan kesehatan gigi dan mulut
</p>
</div>
<div class="bg-white p-6 md:p-8 rounded-2xl shadow-xl border border-gray-200 hover:shadow-2xl transition duration-300 transform hover:-translate-y-2 animate-slide-up"
style="animation-delay: 0.2s;">
<div class="text-4xl md:text-5xl mb-4">🤰</div>
<h3 class="text-xl md:text-2xl font-semibold mb-3">Poli Ibu Hamil</h3>
<p class="text-gray-600 text-sm md:text-base">Layanan kesehatan untuk ibu hamil dan keluarga berencana
</p>
<div class="text-4xl md:text-5xl mb-4 flex justify-center">
<svg class="w-12 h-12 md:w-16 md:h-16 text-purple-600" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z">
</path>
</svg>
</div>
<h3 class="text-xl md:text-2xl font-semibold mb-3">Poli Kesehatan Jiwa</h3>
<p class="text-gray-600 text-sm md:text-base">Layanan konsultasi dan terapi kesehatan mental</p>
</div>
<div class="bg-white p-6 md:p-8 rounded-2xl shadow-xl border border-gray-200 hover:shadow-2xl transition duration-300 transform hover:-translate-y-2 animate-slide-up"
style="animation-delay: 0.3s;">
<div class="text-4xl md:text-5xl mb-4 flex justify-center">
<svg class="w-12 h-12 md:w-16 md:h-16 text-green-600" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M9 1v6m6-6v6"></path>
</svg>
</div>
<h3 class="text-xl md:text-2xl font-semibold mb-3">Poli Kesehatan Tradisional</h3>
<p class="text-gray-600 text-sm md:text-base">Layanan pengobatan tradisional dan herbal</p>
</div>
</div>
</div>
@ -212,29 +252,58 @@ class="bg-primary text-white rounded-full w-16 h-16 md:w-20 md:h-20 flex items-c
<h4 class="text-lg md:text-xl font-semibold mb-4">Layanan</h4>
<ul class="space-y-2 text-gray-400 text-sm md:text-base">
<li>Poli Umum</li>
<li>Poli Anak</li>
<li>Poli Ibu Hamil</li>
<li>Poli Gigi</li>
<li>Poli Kesehatan Jiwa</li>
<li>Poli Kesehatan Tradisional</li>
</ul>
</div>
<div>
<h4 class="text-lg md:text-xl font-semibold mb-4">Kontak</h4>
<ul class="space-y-2 text-gray-400 text-sm md:text-base">
<li>📞 (021) 1234-5678</li>
<li>📧 info@puskesmas.com</li>
<li>📍 Jl. Kesehatan No. 123</li>
</ul>
<div class="space-y-3 text-gray-400 text-sm md:text-base">
<div class="flex items-center space-x-3">
<svg class="w-5 h-5 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z">
</path>
</svg>
<span>(62) 812-4901-0290</span>
</div>
<div class="flex items-center space-x-3">
<svg class="w-5 h-5 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z">
</path>
</svg>
<span>pkmmlandinganeksotik@gmail.com</span>
</div>
<div class="flex items-start space-x-3">
<svg class="w-5 h-5 text-gray-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z">
</path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<span>Jalan Raya Mandingan No.437, Pesisir, Mlandingan Kulon, Kec. Mlandingan, Kabupaten
Situbondo, Jawa Timur 68353</span>
</div>
</div>
</div>
<div>
<h4 class="text-lg md:text-xl font-semibold mb-4">Jam Operasional</h4>
<ul class="space-y-2 text-gray-400 text-sm md:text-base">
<li>Senin - Jumat: 08:00 - 16:00</li>
<li>Sabtu: 08:00 - 12:00</li>
<li>Senin - Kamis: 08:00 - 12:00</li>
<li>Jumat: 08:00 - 10:00</li>
<li>Sabtu: 08:00 - 11:30</li>
<li>Minggu: Tutup</li>
</ul>
</div>
</div>
<div class="border-t border-gray-800 mt-8 md:mt-12 pt-8 text-center text-gray-400">
<p class="text-sm md:text-base">&copy; 2024 Sistem Antrian Puskesmas. All rights reserved.</p>
<p class="text-sm md:text-base">&copy; 2025 Sistem Antrian Puskesmas. All rights reserved.</p>
</div>
</div>
</footer>
@ -261,56 +330,51 @@ class="bg-primary text-white rounded-full w-16 h-16 md:w-20 md:h-20 flex items-c
});
});
// Show SweetAlert2 for success messages
// Auto-hide success alert after 3 seconds
@if (session('success'))
Swal.fire({
icon: 'success',
title: 'Berhasil!',
text: '{{ session('success') }}',
confirmButtonText: 'OK',
confirmButtonColor: '#10B981',
timer: 3000,
timerProgressBar: true
});
@endif
setTimeout(function() {
const successAlert = document.getElementById('success-alert');
if (successAlert) {
// Add fade out animation
successAlert.classList.add('translate-x-full', 'opacity-0');
// Show logout success message
@if (session('success') && str_contains(session('success'), 'logout'))
Swal.fire({
icon: 'success',
title: 'Logout Berhasil!',
text: '{{ session('success') }}',
confirmButtonText: 'OK',
confirmButtonColor: '#10B981',
timer: 3000,
timerProgressBar: true
});
// Remove element after animation completes
setTimeout(function() {
successAlert.remove();
}, 300);
}
}, 3000);
@endif
// Show SweetAlert2 for error messages
@if (session('error'))
Swal.fire({
icon: 'error',
title: 'Error!',
text: '{{ session('error') }}',
confirmButtonText: 'OK',
confirmButtonColor: '#EF4444',
timer: 4000,
timerProgressBar: true
});
@endif
// Show system error message
@if (session('error') && str_contains(session('error'), 'sistem'))
Swal.fire({
icon: 'error',
title: 'Error Sistem!',
text: '{{ session('error') }}',
confirmButtonText: 'OK',
confirmButtonColor: '#EF4444',
timer: 4000,
timerProgressBar: true
});
@if (str_contains(session('error'), 'sistem'))
Swal.fire({
icon: 'error',
title: 'Error Sistem!',
text: '{{ session('error') }}',
confirmButtonText: 'OK',
confirmButtonColor: '#EF4444',
timer: 5000,
timerProgressBar: true,
showCloseButton: true,
allowOutsideClick: true,
allowEscapeKey: true
});
@else
Swal.fire({
icon: 'error',
title: 'Error!',
text: '{{ session('error') }}',
confirmButtonText: 'OK',
confirmButtonColor: '#EF4444',
timer: 4000,
timerProgressBar: true,
showCloseButton: true,
allowOutsideClick: true,
allowEscapeKey: true
});
@endif
@endif
// Intersection Observer for animations

View File

@ -63,6 +63,8 @@
Route::get('/admin/poli/tradisional', [AdminController::class, 'poliTradisional'])->name('admin.poli.tradisional');
Route::post('/admin/panggil-antrian', [AdminController::class, 'panggilAntrian'])->name('admin.panggil-antrian');
Route::post('/admin/selesai-antrian', [AdminController::class, 'selesaiAntrian'])->name('admin.selesai-antrian');
Route::post('/admin/konfirmasi-kehadiran', [AdminController::class, 'konfirmasiKehadiran'])->name('admin.konfirmasi-kehadiran');
Route::post('/admin/mulai-pemeriksaan', [AdminController::class, 'mulaiPemeriksaan'])->name('admin.mulai-pemeriksaan');
Route::post('/admin/antrian/batal', [AdminController::class, 'batalAntrian'])->name('admin.batal-antrian');
Route::post('/admin/panggil-antrian/{antrian}', [AdminController::class, 'panggilAntrianById'])->name('admin.panggil-antrian-id');
Route::post('/admin/play-audio', [AdminController::class, 'playQueueCallAudio'])->name('admin.play-audio');
@ -74,6 +76,7 @@
Route::get('/admin/users/create', [AdminController::class, 'createUser'])->name('admin.users.create');
Route::post('/admin/users', [AdminController::class, 'storeUser'])->name('admin.users.store');
Route::get('/admin/users/{user}', [AdminController::class, 'showUser'])->name('admin.users.show');
Route::get('/admin/users/{user}/antrian-terbaru', [AdminController::class, 'getUserAntrianTerbaru'])->name('admin.users.antrian-terbaru');
Route::put('/admin/users/{user}', [AdminController::class, 'updateUser'])->name('admin.users.update');
Route::post('/admin/users/{user}/reset-password', [AdminController::class, 'resetUserPassword'])->name('admin.users.reset-password');
@ -88,6 +91,8 @@
Route::post('/admin/antrian/store', [AdminController::class, 'storeAntrianAdmin'])->name('admin.antrian.store');
Route::get('/admin/antrian/{antrian}/cetak', [AdminController::class, 'cetakAntrian'])->name('admin.antrian.cetak');
// Dashboard API for live updates
Route::get('/admin/dashboard/api', [AdminController::class, 'dashboardApi'])->name('admin.dashboard.api');
});