feature: keep sidebar and add service layer

This commit is contained in:
antv
2026-06-30 11:59:34 +07:00
parent 8a77324f89
commit c5016489ac
54 changed files with 1181 additions and 284 deletions
+3 -2
View File
@@ -57,6 +57,7 @@
@source '../../storage/framework/views/*.php';
@source '../**/*.blade.php';
@source '../**/*.js';
@source inline('bg-primary-light/35 ring-primary/20 border-primary ring-2 animate-pulse');
@theme {
--font-sans: 'SVN-Poppins', 'SVN-Poppins-SemiBold', 'SVN-Poppins-Regular', 'SVN-Poppins-Medium', 'Roboto', 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
@@ -100,7 +101,7 @@
}
.sidebar {
@apply w-60 bg-bg-sidebar border-r border-border-light flex flex-col justify-between shrink-0 shadow-sidebar transition-all duration-300;
@apply w-60 h-screen sticky top-0 bg-bg-sidebar border-r border-border-light flex flex-col justify-between shrink-0 shadow-sidebar transition-all duration-300 overflow-hidden;
}
.main-content {
@@ -185,7 +186,7 @@
}
.form-label {
@apply block text-[16px] font-[600] text-text-dark mb-2; /* 8px label gap */
@apply block text-[14px] font-[600] text-text-dark mb-2; /* 8px label gap */
}
/* Standard Input & Select */
+6 -1
View File
@@ -40,7 +40,7 @@
<div class="form-group mb-0">
<label class="flex items-center gap-3 cursor-pointer p-4 border border-[#d9dfe7] rounded-lg hover:bg-gray-50 hover:border-gray-400 transition-colors shadow-sm">
<input type="checkbox" name="flag_send" id="flag_send" value="1" class="form-input !w-5 !h-5 !min-h-0" {{ $user->flag_send ? 'checked' : '' }}>
<span class="text-[16px] font-[600] text-text-dark">Bật quyền cho phép User gửi thẻ</span>
<span class="text-[14px] font-[600] text-text-dark">Bật quyền cho phép User gửi thẻ</span>
</label>
</div>
</div>
@@ -110,6 +110,11 @@
</tbody>
</table>
</div>
@if($administrations->hasPages())
<div class="px-6 py-4 border-t border-border-light bg-gray-50/30">
{{ $administrations->links() }}
</div>
@endif
</div>
</div>
</div>
@@ -0,0 +1,32 @@
@props(['id', 'title' => 'Xác nhận', 'message' => '', 'confirmText' => 'Đồng ý', 'cancelText' => 'Hủy', 'type' => 'danger'])
<div id="{{ $id }}" class="fixed inset-0 z-[100] hidden flex-col items-center justify-center outline-none opacity-0 transition-opacity duration-300" aria-labelledby="modal-title-{{ $id }}" role="dialog" aria-modal="true">
<div class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm cursor-pointer" onclick="closeModal('{{ $id }}')"></div>
<div class="modal-panel bg-white rounded-2xl shadow-2xl w-full sm:max-w-md overflow-hidden z-50 relative transform scale-95 translate-y-4 transition-all duration-300 m-4 flex flex-col">
<div class="p-6 flex flex-col items-center text-center">
<!-- Icon based on type -->
<div class="w-14 h-14 rounded-full flex items-center justify-center mb-4 {{ $type === 'danger' ? 'bg-red-50 text-red-500' : 'bg-blue-50 text-blue-500' }}">
@if($type === 'danger')
<svg class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
@else
<svg class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" 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" />
</svg>
@endif
</div>
<h3 id="modal-title-{{ $id }}" class="text-xl font-bold text-slate-800 mb-2">{{ $title }}</h3>
<p class="text-slate-500 text-sm leading-relaxed">{{ $message }}</p>
</div>
<div class="px-6 py-4 bg-slate-50 flex gap-3 border-t border-slate-100 justify-end">
<button type="button" onclick="closeModal('{{ $id }}')" class="px-4 py-2 bg-white hover:bg-slate-100 border border-slate-200 text-slate-700 text-sm font-semibold rounded-lg transition-all duration-200 cursor-pointer">
{{ $cancelText }}
</button>
<button type="button" onclick="closeModal('{{ $id }}')" class="px-4 py-2 text-white text-sm font-semibold rounded-lg transition-all duration-200 cursor-pointer {{ $type === 'danger' ? 'bg-red-600 hover:bg-red-700' : 'bg-primary hover:bg-primary-hover' }}">
{{ $confirmText }}
</button>
</div>
</div>
</div>
@@ -0,0 +1,17 @@
@props(['title' => 'Không có dữ liệu', 'message' => 'Không tìm thấy dữ liệu nào phù hợp với yêu cầu.', 'actionText' => '', 'actionId' => ''])
<div class="flex flex-col items-center justify-center p-8 bg-white border border-slate-100 rounded-2xl shadow-sm text-center max-w-lg mx-auto my-6">
<div class="w-48 h-48 mb-6 flex items-center justify-center text-slate-300">
<!-- SVG illustration of empty search/mailbox -->
<svg class="w-32 h-32 text-slate-300 animate-pulse" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5m6 4.125l2.25 2.25m0 0l2.25 2.25M12 13.875l2.25-2.25M12 13.875l-2.25-2.25M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" />
</svg>
</div>
<h3 class="text-lg font-bold text-slate-800 mb-2">{{ $title }}</h3>
<p class="text-sm text-slate-500 max-w-sm mb-6 leading-relaxed">{{ $message }}</p>
@if($actionText)
<button type="button" @if($actionId) id="{{ $actionId }}" @endif class="btn-primary px-6 py-2 text-sm font-semibold rounded-lg shadow-sm cursor-pointer transition-all duration-200">
{{ $actionText }}
</button>
@endif
</div>
@@ -0,0 +1,22 @@
@props(['id', 'title' => 'Đã xảy ra lỗi', 'message' => '', 'buttonText' => 'Đóng'])
<div id="{{ $id }}" class="fixed inset-0 z-[100] hidden flex-col items-center justify-center outline-none opacity-0 transition-opacity duration-300" aria-labelledby="modal-title-{{ $id }}" role="dialog" aria-modal="true">
<div class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm cursor-pointer" onclick="closeModal('{{ $id }}')"></div>
<div class="modal-panel bg-white rounded-2xl shadow-2xl w-full sm:max-w-md overflow-hidden z-50 relative transform scale-95 translate-y-4 transition-all duration-300 m-4 flex flex-col">
<div class="p-6 flex flex-col items-center text-center">
<div class="w-14 h-14 rounded-full bg-red-50 text-red-500 flex items-center justify-center mb-4 animate-bounce">
<svg class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<h3 id="modal-title-{{ $id }}" class="text-xl font-bold text-slate-800 mb-2">{{ $title }}</h3>
<p class="text-slate-500 text-sm leading-relaxed">{{ $message }}</p>
</div>
<div class="px-6 py-4 bg-slate-50 flex justify-center border-t border-slate-100">
<button type="button" onclick="closeModal('{{ $id }}')" class="w-full sm:w-auto px-6 py-2 bg-red-600 hover:bg-red-700 text-white text-sm font-semibold rounded-lg shadow-sm transition-all duration-200 cursor-pointer text-center">
{{ $buttonText }}
</button>
</div>
</div>
</div>
@@ -0,0 +1,22 @@
@props(['id', 'title' => 'Thông tin', 'message' => '', 'buttonText' => 'Đóng'])
<div id="{{ $id }}" class="fixed inset-0 z-[100] hidden flex-col items-center justify-center outline-none opacity-0 transition-opacity duration-300" aria-labelledby="modal-title-{{ $id }}" role="dialog" aria-modal="true">
<div class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm cursor-pointer" onclick="closeModal('{{ $id }}')"></div>
<div class="modal-panel bg-white rounded-2xl shadow-2xl w-full sm:max-w-md overflow-hidden z-50 relative transform scale-95 translate-y-4 transition-all duration-300 m-4 flex flex-col">
<div class="p-6 flex flex-col items-center text-center">
<div class="w-14 h-14 rounded-full bg-blue-50 text-blue-500 flex items-center justify-center mb-4">
<svg class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h3 id="modal-title-{{ $id }}" class="text-xl font-bold text-slate-800 mb-2">{{ $title }}</h3>
<p class="text-slate-500 text-sm leading-relaxed">{{ $message }}</p>
</div>
<div class="px-6 py-4 bg-slate-50 flex justify-center border-t border-slate-100">
<button type="button" onclick="closeModal('{{ $id }}')" class="w-full sm:w-auto px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-semibold rounded-lg shadow-sm transition-all duration-200 cursor-pointer text-center">
{{ $buttonText }}
</button>
</div>
</div>
</div>
@@ -0,0 +1,22 @@
@props(['id', 'title' => 'Thành công', 'message' => '', 'buttonText' => 'Xác nhận'])
<div id="{{ $id }}" class="fixed inset-0 z-[100] hidden flex-col items-center justify-center outline-none opacity-0 transition-opacity duration-300" aria-labelledby="modal-title-{{ $id }}" role="dialog" aria-modal="true">
<div class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm cursor-pointer" onclick="closeModal('{{ $id }}')"></div>
<div class="modal-panel bg-white rounded-2xl shadow-2xl w-full sm:max-w-md overflow-hidden z-50 relative transform scale-95 translate-y-4 transition-all duration-300 m-4 flex flex-col">
<div class="p-6 flex flex-col items-center text-center">
<div class="w-14 h-14 rounded-full bg-emerald-50 text-emerald-500 flex items-center justify-center mb-4 scale-110">
<svg class="w-8 h-8 animate-pulse" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
</div>
<h3 id="modal-title-{{ $id }}" class="text-xl font-bold text-slate-800 mb-2">{{ $title }}</h3>
<p class="text-slate-500 text-sm leading-relaxed">{{ $message }}</p>
</div>
<div class="px-6 py-4 bg-slate-50 flex justify-center border-t border-slate-100">
<button type="button" onclick="closeModal('{{ $id }}')" class="w-full sm:w-auto px-6 py-2 bg-emerald-600 hover:bg-emerald-700 text-white text-sm font-semibold rounded-lg shadow-sm transition-all duration-200 cursor-pointer text-center">
{{ $buttonText }}
</button>
</div>
</div>
</div>
@@ -0,0 +1,20 @@
@props(['id' => 'toast', 'type' => 'success', 'message' => ''])
<div id="{{ $id }}" class="fixed top-4 right-4 z-[999] flex items-center w-full max-w-sm p-4 space-x-3 text-slate-500 bg-white rounded-xl shadow-xl border border-slate-100 hidden opacity-0 transform translate-y-[-10px] transition-all duration-300" role="alert">
<div class="inline-flex items-center justify-center shrink-0 w-9 h-9 rounded-lg {{ $type === 'success' ? 'bg-emerald-50 text-emerald-500' : ($type === 'error' ? 'bg-red-50 text-red-500' : ($type === 'warning' ? 'bg-amber-50 text-amber-500' : 'bg-blue-50 text-blue-500')) }}">
@if($type === 'success')
<svg class="w-5.5 h-5.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" /></svg>
@elseif($type === 'error')
<svg class="w-5.5 h-5.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
@elseif($type === 'warning')
<svg class="w-5.5 h-5.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
@else
<svg class="w-5.5 h-5.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
@endif
</div>
<div class="text-sm font-semibold text-slate-800 flex-1">{{ $message }}</div>
<button type="button" onclick="closeToast('{{ $id }}')" class="text-slate-400 hover:text-slate-600 rounded-lg p-1 hover:bg-slate-50 inline-flex h-8 w-8 justify-center items-center cursor-pointer transition-colors">
<span class="sr-only">Close</span>
<svg class="w-4 h-4" fill="none" viewBox="0 0 14 14" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/></svg>
</button>
</div>
@@ -0,0 +1,22 @@
@props(['id', 'title' => 'Cảnh báo', 'message' => '', 'buttonText' => 'Đã hiểu'])
<div id="{{ $id }}" class="fixed inset-0 z-[100] hidden flex-col items-center justify-center outline-none opacity-0 transition-opacity duration-300" aria-labelledby="modal-title-{{ $id }}" role="dialog" aria-modal="true">
<div class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm cursor-pointer" onclick="closeModal('{{ $id }}')"></div>
<div class="modal-panel bg-white rounded-2xl shadow-2xl w-full sm:max-w-md overflow-hidden z-50 relative transform scale-95 translate-y-4 transition-all duration-300 m-4 flex flex-col">
<div class="p-6 flex flex-col items-center text-center">
<div class="w-14 h-14 rounded-full bg-amber-50 text-amber-500 flex items-center justify-center mb-4">
<svg class="w-8 h-8 animate-bounce" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<h3 id="modal-title-{{ $id }}" class="text-xl font-bold text-slate-800 mb-2">{{ $title }}</h3>
<p class="text-slate-500 text-sm leading-relaxed">{{ $message }}</p>
</div>
<div class="px-6 py-4 bg-slate-50 flex justify-center border-t border-slate-100">
<button type="button" onclick="closeModal('{{ $id }}')" class="w-full sm:w-auto px-6 py-2 bg-amber-500 hover:bg-amber-600 text-white text-sm font-semibold rounded-lg shadow-sm transition-all duration-200 cursor-pointer text-center">
{{ $buttonText }}
</button>
</div>
</div>
</div>
+3 -9
View File
@@ -12,7 +12,7 @@
<div class="app-layout">
<aside class="sidebar">
<div class="flex flex-col h-full justify-between">
<div>
<div class="flex-1 flex flex-col min-h-0 overflow-hidden">
<div class="h-16 flex items-center justify-between px-6 border-b border-border-light">
<div class="flex items-center gap-2">
<img src="{{ asset('images/logo.png') }}" alt="GMO-Z.com RUNSYSTEM" class="h-8 w-auto object-contain">
@@ -24,7 +24,7 @@
</button>
</div>
<nav class="p-4 space-y-1">
<nav class="p-4 space-y-1 overflow-y-auto flex-1">
@if(Auth::check() && Auth::user()->role == \App\Models\User::ROLE_ADMIN)
<!-- ADMIN SIDEBAR -->
<a href="{{ route('admin.users.index') }}" class="sidebar-link {{ request()->routeIs('admin.users.index') ? 'sidebar-link-active' : '' }}">
@@ -64,13 +64,7 @@
</nav>
</div>
<div>
<div class="mx-4 my-2 p-4 rounded-lg bg-primary/5 border border-primary/10 flex flex-col items-center text-center">
<img src="https://illustrations.popsy.co/blue/document-delivery.svg" alt="Support" class="w-24 h-24 object-contain mb-2">
<h4 class="text-text-dark font-semibold text-xs mb-1">{{ __('layout.have_questions') }}</h4>
<a href="#" class="btn-primary py-1.5 px-4 text-xs w-full mt-2 inline-block">{{ __('layout.feedback') }}</a>
</div>
<div class="shrink-0">
<div class="px-6 py-4 border-t border-border-light text-center">
<span class="text-text-muted text-[10px]">&copy; 2026 GMO-Z.com RUNSYSTEM</span>
</div>
@@ -0,0 +1,443 @@
@extends('layouts.app')
@section('title', 'Notification Showcase')
@section('content')
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 h-[calc(100vh-8rem)]">
<!-- Left Panel: Case Selection (lg:col-span-4) -->
<div class="lg:col-span-4 bg-white rounded-2xl border border-slate-200 shadow-sm flex flex-col min-h-0 overflow-hidden">
<div class="p-5 border-b border-slate-100 bg-slate-50/50">
<h2 class="text-lg font-bold text-slate-800">Review Cases (15 cases)</h2>
<p class="text-xs text-slate-500 mt-1">Select a case to trigger its notification. Used for QA & BA approval.</p>
</div>
<div class="flex-1 overflow-y-auto p-4 space-y-2" id="cases-list">
<!-- Case 1 -->
<button onclick="triggerCase(1)" id="case-btn-1" class="case-btn w-full text-left p-3.5 rounded-xl border border-slate-100 hover:border-slate-300 hover:bg-slate-50 transition-all duration-200 flex flex-col gap-1 cursor-pointer">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-400">Case 01</span>
<span class="px-2 py-0.5 text-[10px] font-bold bg-amber-50 text-amber-600 rounded-full border border-amber-100 uppercase">Toast (Error)</span>
</div>
<span class="text-sm font-bold text-slate-700">Login failed</span>
</button>
<!-- Case 2 -->
<button onclick="triggerCase(2)" id="case-btn-2" class="case-btn w-full text-left p-3.5 rounded-xl border border-slate-100 hover:border-slate-300 hover:bg-slate-50 transition-all duration-200 flex flex-col gap-1 cursor-pointer">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-400">Case 02</span>
<span class="px-2 py-0.5 text-[10px] font-bold bg-red-50 text-red-600 rounded-full border border-red-100 uppercase">Error Dialog</span>
</div>
<span class="text-sm font-bold text-slate-700">Access denied</span>
</button>
<!-- Case 3 -->
<button onclick="triggerCase(3)" id="case-btn-3" class="case-btn w-full text-left p-3.5 rounded-xl border border-slate-100 hover:border-slate-300 hover:bg-slate-50 transition-all duration-200 flex flex-col gap-1 cursor-pointer">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-400">Case 03</span>
<span class="px-2 py-0.5 text-[10px] font-bold bg-blue-50 text-blue-600 rounded-full border border-blue-100 uppercase">Info Dialog</span>
</div>
<span class="text-sm font-bold text-slate-700">First login (change password)</span>
</button>
<!-- Case 4 -->
<button onclick="triggerCase(4)" id="case-btn-4" class="case-btn w-full text-left p-3.5 rounded-xl border border-slate-100 hover:border-slate-300 hover:bg-slate-50 transition-all duration-200 flex flex-col gap-1 cursor-pointer">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-400">Case 04</span>
<span class="px-2 py-0.5 text-[10px] font-bold bg-purple-50 text-purple-600 rounded-full border border-purple-100 uppercase">Confirm Dialog</span>
</div>
<span class="text-sm font-bold text-slate-700">Delete user confirmation</span>
</button>
<!-- Case 5 -->
<button onclick="triggerCase(5)" id="case-btn-5" class="case-btn w-full text-left p-3.5 rounded-xl border border-slate-100 hover:border-slate-300 hover:bg-slate-50 transition-all duration-200 flex flex-col gap-1 cursor-pointer">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-400">Case 05</span>
<span class="px-2 py-0.5 text-[10px] font-bold bg-emerald-50 text-emerald-600 rounded-full border border-emerald-100 uppercase">Success Dialog</span>
</div>
<span class="text-sm font-bold text-slate-700">Create user success</span>
</button>
<!-- Case 6 -->
<button onclick="triggerCase(6)" id="case-btn-6" class="case-btn w-full text-left p-3.5 rounded-xl border border-slate-100 hover:border-slate-300 hover:bg-slate-50 transition-all duration-200 flex flex-col gap-1 cursor-pointer">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-400">Case 06</span>
<span class="px-2 py-0.5 text-[10px] font-bold bg-red-50 text-red-600 rounded-full border border-red-100 uppercase">Error Dialog</span>
</div>
<span class="text-sm font-bold text-slate-700">Email already exists</span>
</button>
<!-- Case 7 -->
<button onclick="triggerCase(7)" id="case-btn-7" class="case-btn w-full text-left p-3.5 rounded-xl border border-slate-100 hover:border-slate-300 hover:bg-slate-50 transition-all duration-200 flex flex-col gap-1 cursor-pointer">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-400">Case 07</span>
<span class="px-2 py-0.5 text-[10px] font-bold bg-red-50 text-red-600 rounded-full border border-red-100 uppercase">Error Dialog</span>
</div>
<span class="text-sm font-bold text-slate-700">Employee ID already exists</span>
</button>
<!-- Case 8 -->
<button onclick="triggerCase(8)" id="case-btn-8" class="case-btn w-full text-left p-3.5 rounded-xl border border-slate-100 hover:border-slate-300 hover:bg-slate-50 transition-all duration-200 flex flex-col gap-1 cursor-pointer">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-400">Case 08</span>
<span class="px-2 py-0.5 text-[10px] font-bold bg-slate-50 text-slate-600 rounded-full border border-slate-200 uppercase">Empty State</span>
</div>
<span class="text-sm font-bold text-slate-700">No data found</span>
</button>
<!-- Case 9 -->
<button onclick="triggerCase(9)" id="case-btn-9" class="case-btn w-full text-left p-3.5 rounded-xl border border-slate-100 hover:border-slate-300 hover:bg-slate-50 transition-all duration-200 flex flex-col gap-1 cursor-pointer">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-400">Case 09</span>
<span class="px-2 py-0.5 text-[10px] font-bold bg-amber-50 text-amber-600 rounded-full border border-amber-100 uppercase">Warning Dialog</span>
</div>
<span class="text-sm font-bold text-slate-700">Not enough thankcards</span>
</button>
<!-- Case 10 -->
<button onclick="triggerCase(10)" id="case-btn-10" class="case-btn w-full text-left p-3.5 rounded-xl border border-slate-100 hover:border-slate-300 hover:bg-slate-50 transition-all duration-200 flex flex-col gap-1 cursor-pointer">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-400">Case 10</span>
<span class="px-2 py-0.5 text-[10px] font-bold bg-amber-50 text-amber-600 rounded-full border border-amber-100 uppercase">Warning Dialog</span>
</div>
<span class="text-sm font-bold text-slate-700">Max 5 thankcards/month</span>
</button>
<!-- Case 11 -->
<button onclick="triggerCase(11)" id="case-btn-11" class="case-btn w-full text-left p-3.5 rounded-xl border border-slate-100 hover:border-slate-300 hover:bg-slate-50 transition-all duration-200 flex flex-col gap-1 cursor-pointer">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-400">Case 11</span>
<span class="px-2 py-0.5 text-[10px] font-bold bg-purple-50 text-purple-600 rounded-full border border-purple-100 uppercase">Confirm Dialog</span>
</div>
<span class="text-sm font-bold text-slate-700">Send thankcards confirmation</span>
</button>
<!-- Case 12 -->
<button onclick="triggerCase(12)" id="case-btn-12" class="case-btn w-full text-left p-3.5 rounded-xl border border-slate-100 hover:border-slate-300 hover:bg-slate-50 transition-all duration-200 flex flex-col gap-1 cursor-pointer">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-400">Case 12</span>
<span class="px-2 py-0.5 text-[10px] font-bold bg-emerald-50 text-emerald-600 rounded-full border border-emerald-100 uppercase">Success Dialog</span>
</div>
<span class="text-sm font-bold text-slate-700">Send thankcards success</span>
</button>
<!-- Case 13 -->
<button onclick="triggerCase(13)" id="case-btn-13" class="case-btn w-full text-left p-3.5 rounded-xl border border-slate-100 hover:border-slate-300 hover:bg-slate-50 transition-all duration-200 flex flex-col gap-1 cursor-pointer">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-400">Case 13</span>
<span class="px-2 py-0.5 text-[10px] font-bold bg-red-50 text-red-600 rounded-full border border-red-100 uppercase">Error Dialog</span>
</div>
<span class="text-sm font-bold text-slate-700">Password confirmation mismatch</span>
</button>
<!-- Case 14 -->
<button onclick="triggerCase(14)" id="case-btn-14" class="case-btn w-full text-left p-3.5 rounded-xl border border-slate-100 hover:border-slate-300 hover:bg-slate-50 transition-all duration-200 flex flex-col gap-1 cursor-pointer">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-400">Case 14</span>
<span class="px-2 py-0.5 text-[10px] font-bold bg-emerald-50 text-emerald-600 rounded-full border border-emerald-100 uppercase">Success Dialog</span>
</div>
<span class="text-sm font-bold text-slate-700">Change password success</span>
</button>
<!-- Case 15 -->
<button onclick="triggerCase(15)" id="case-btn-15" class="case-btn w-full text-left p-3.5 rounded-xl border border-slate-100 hover:border-slate-300 hover:bg-slate-50 transition-all duration-200 flex flex-col gap-1 cursor-pointer">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-slate-400">Case 15</span>
<span class="px-2 py-0.5 text-[10px] font-bold bg-red-50 text-red-600 rounded-full border border-red-100 uppercase">Error Dialog</span>
</div>
<span class="text-sm font-bold text-slate-700">Generic system error</span>
</button>
</div>
</div>
<!-- Right Panel: Live Browser Preview (lg:col-span-8) -->
<div class="lg:col-span-8 bg-slate-100 rounded-2xl border border-slate-200 overflow-hidden flex flex-col relative" id="preview-container">
<!-- Mock Top Bar -->
<div class="bg-white border-b border-slate-200 px-6 py-4 flex items-center justify-between z-10">
<div class="flex items-center gap-2">
<div class="flex gap-1.5">
<span class="w-3.5 h-3.5 rounded-full bg-red-400 block"></span>
<span class="w-3.5 h-3.5 rounded-full bg-amber-400 block"></span>
<span class="w-3.5 h-3.5 rounded-full bg-emerald-400 block"></span>
</div>
<span class="text-slate-400 text-xs font-medium ml-4">Browser Mockup - Preview Viewport</span>
</div>
<div class="flex items-center gap-4 text-xs font-bold text-slate-700" id="current-case-label">
Active Case: None selected
</div>
</div>
<!-- Mock Workspace Page (this acts as the realistic background when modals appear) -->
<div class="flex-1 p-6 relative overflow-y-auto flex flex-col" id="mock-workspace">
<!-- Standard User Table Mockup -->
<div id="mock-table-view" class="bg-white rounded-xl shadow-sm border border-slate-200 flex-1 flex flex-col overflow-hidden">
<div class="p-5 border-b border-slate-100 flex items-center justify-between">
<div>
<h3 class="font-bold text-slate-800 text-base">Danh sách nhân viên (Mock User Table)</h3>
<p class="text-xs text-slate-500 mt-0.5">Hiển thị danh sách nhân viên trong hệ thống Thankcard.</p>
</div>
<button class="btn-primary py-1.5 px-4 text-xs font-semibold rounded-lg shadow-sm">Thêm nhân viên</button>
</div>
<div class="overflow-x-auto flex-1">
<table class="w-full text-left text-sm text-slate-600 border-collapse">
<thead>
<tr class="bg-slate-50 text-slate-500 font-semibold border-b border-slate-100">
<th class="p-4">MSNV</th>
<th class="p-4">Họ tên</th>
<th class="p-4">Email</th>
<th class="p-4">Trạng thái</th>
<th class="p-4 text-right">Thao tác</th>
</tr>
</thead>
<tbody>
<tr class="border-b border-slate-100 hover:bg-slate-50/50">
<td class="p-4 font-bold text-slate-700">NV0001</td>
<td class="p-4 font-semibold text-slate-800">Nguyễn Văn A</td>
<td class="p-4">nguyenvana@runsystem.net</td>
<td class="p-4">
<span class="inline-flex px-2 py-0.5 text-xs font-semibold rounded-full bg-emerald-50 text-emerald-600 border border-emerald-100">Hoạt động</span>
</td>
<td class="p-4 text-right flex justify-end gap-2">
<button class="p-1 text-slate-400 hover:text-primary transition-colors cursor-pointer">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/></svg>
</button>
<button class="p-1 text-slate-400 hover:text-red-500 transition-colors cursor-pointer">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
</button>
</td>
</tr>
<tr class="border-b border-slate-100 hover:bg-slate-50/50">
<td class="p-4 font-bold text-slate-700">NV0002</td>
<td class="p-4 font-semibold text-slate-800">Trần Thị B</td>
<td class="p-4">tranthib@runsystem.net</td>
<td class="p-4">
<span class="inline-flex px-2 py-0.5 text-xs font-semibold rounded-full bg-emerald-50 text-emerald-600 border border-emerald-100">Hoạt động</span>
</td>
<td class="p-4 text-right flex justify-end gap-2">
<button class="p-1 text-slate-400 hover:text-primary transition-colors cursor-pointer">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/></svg>
</button>
<button class="p-1 text-slate-400 hover:text-red-500 transition-colors cursor-pointer">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
</button>
</td>
</tr>
<tr class="hover:bg-slate-50/50">
<td class="p-4 font-bold text-slate-700">NV0003</td>
<td class="p-4 font-semibold text-slate-800"> Văn C</td>
<td class="p-4">levanc@runsystem.net</td>
<td class="p-4">
<span class="inline-flex px-2 py-0.5 text-xs font-semibold rounded-full bg-slate-100 text-slate-600 border border-slate-200">Khóa</span>
</td>
<td class="p-4 text-right flex justify-end gap-2">
<button class="p-1 text-slate-400 hover:text-primary transition-colors cursor-pointer">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/></svg>
</button>
<button class="p-1 text-slate-400 hover:text-red-500 transition-colors cursor-pointer">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Empty State Case container (visible only when case 8 is active) -->
<div id="mock-empty-state-view" class="hidden flex-1 items-center justify-center">
<x-empty-state
title="Không tìm thấy dữ liệu"
message="Không tìm thấy người dùng hoặc giao dịch nào phù hợp với bộ lọc tìm kiếm hiện tại của bạn. Vui lòng thử lại với từ khóa khác."
actionText="Tải lại dữ liệu"
actionId="reload-table-btn"
/>
</div>
</div>
</div>
</div>
<!-- All Case Modals/Toasts rendered as Reusable components -->
<!-- Case 1: Login failed -->
<x-toast id="toast-case-1" type="error" message="Đăng nhập thất bại. Vui lòng kiểm tra lại Email hoặc Mật khẩu." />
<!-- Case 2: Access denied -->
<x-error-dialog id="modal-case-2" title="Truy cập bị từ chối" message="Tài khoản của bạn không có quyền truy cập vào trang quản trị này. Vui lòng liên hệ với quản trị viên nếu đây là một sự nhầm lẫn." buttonText="Đồng ý" />
<!-- Case 3: First login (change password required) -->
<x-info-dialog id="modal-case-3" title="Yêu cầu đổi mật khẩu" message="Chào mừng bạn đến với hệ thống Thankcard! Đây là lần đầu tiên bạn đăng nhập, vui lòng đổi mật khẩu mới để tiếp tục sử dụng hệ thống." buttonText="Đổi mật khẩu ngay" />
<!-- Case 4: Delete user confirmation -->
<x-confirm-dialog id="modal-case-4" title="Xóa người dùng" message="Bạn có chắc chắn muốn xóa tài khoản của nhân viên Nguyễn Văn A (MSNV: NV0001)? Hành động này không thể hoàn tác và tất cả dữ liệu liên quan sẽ bị xóa." confirmText="Xóa vĩnh viễn" cancelText="Hủy bỏ" type="danger" />
<!-- Case 5: Create user success -->
<x-success-dialog id="modal-case-5" title="Tạo tài khoản thành công" message="Tài khoản nhân viên mới đã được tạo thành công trong hệ thống. Email kích hoạt đã được gửi tới người dùng." buttonText="Hoàn tất" />
<!-- Case 6: Email already exists -->
<x-error-dialog id="modal-case-6" title="Email đã tồn tại" message="Địa chỉ email 'nguyenvana@runsystem.net' đã được đăng ký bởi một nhân viên khác. Vui lòng sử dụng địa chỉ email khác." buttonText="Nhập lại" />
<!-- Case 7: Employee ID already exists -->
<x-error-dialog id="modal-case-7" title="Mã nhân viên đã tồn tại" message="Mã nhân viên (MSNV) 'NV0001' đã được gán cho một tài khoản khác trong hệ thống. Vui lòng kiểm tra lại." buttonText="Nhập lại" />
<!-- Case 9: Not enough thankcards -->
<x-warning-dialog id="modal-case-9" title="Không đủ lượt gửi" message="Số dư Thankcard hiện tại của bạn là 0. Bạn không thể gửi thêm thẻ cho đến khi số dư được đặt lại vào đầu tháng tới." buttonText="Đóng" />
<!-- Case 10: Maximum 5 thankcards per user/month -->
<x-warning-dialog id="modal-case-10" title="Giới hạn gửi thẻ" message="Bạn đã đạt đến hạn mức gửi tối đa 5 Thankcard trong tháng này. Vui lòng đợi đến tháng sau để tiếp tục gửi lời cảm ơn." buttonText="Đã hiểu" />
<!-- Case 11: Send thankcards confirmation -->
<x-confirm-dialog id="modal-case-11" title="Xác nhận gửi Thankcard" message="Bạn đang thực hiện gửi 1 Thankcard tới Trần Thị B với lời nhắn: 'Cảm ơn bạn đã hỗ trợ dự án'. Bạn có chắc chắn muốn gửi?" confirmText="Gửi ngay" cancelText="Xem lại" type="primary" />
<!-- Case 12: Send thankcards success -->
<x-success-dialog id="modal-case-12" title="Gửi thẻ thành công" message="Thankcard của bạn đã được gửi thành công đến Trần Thị B. Lượt gửi còn lại trong tháng của bạn: 4/5." buttonText="Tuyệt vời" />
<!-- Case 13: Password confirmation mismatch -->
<x-error-dialog id="modal-case-13" title="Mật khẩu không khớp" message="Mật khẩu xác nhận nhập lại không khớp với mật khẩu mới. Vui lòng nhập lại chính xác cả hai trường." buttonText="Thử lại" />
<!-- Case 14: Change password success -->
<x-success-dialog id="modal-case-14" title="Đổi mật khẩu thành công" message="Mật khẩu của bạn đã được cập nhật thành công. Vui lòng sử dụng mật khẩu mới cho các lần đăng nhập tiếp theo." buttonText="Đăng nhập lại" />
<!-- Case 15: Generic system error -->
<x-error-dialog id="modal-case-15" title="Lỗi hệ thống" message="Đã xảy ra sự cố kết nối với máy chủ (Mã lỗi: 500 - Internal Server Error). Vui lòng thử lại sau hoặc báo cáo cho đội ngũ kỹ thuật." buttonText="Đóng" />
@endsection
@push('scripts')
<script>
const casesMetadata = {
1: { name: "Case 1: Login failed", type: "toast", targetId: "toast-case-1" },
2: { name: "Case 2: Access denied", type: "modal", targetId: "modal-case-2" },
3: { name: "Case 3: First login (change password)", type: "modal", targetId: "modal-case-3" },
4: { name: "Case 4: Delete user confirmation", type: "modal", targetId: "modal-case-4" },
5: { name: "Case 5: Create user success", type: "modal", targetId: "modal-case-5" },
6: { name: "Case 6: Email already exists", type: "modal", targetId: "modal-case-6" },
7: { name: "Case 7: Employee ID already exists", type: "modal", targetId: "modal-case-7" },
8: { name: "Case 8: No data found", type: "empty-state", targetId: "mock-empty-state-view" },
9: { name: "Case 9: Not enough thankcards", type: "modal", targetId: "modal-case-9" },
10: { name: "Case 10: Maximum 5 thankcards per user/month", type: "modal", targetId: "modal-case-10" },
11: { name: "Case 11: Send thankcards confirmation", type: "modal", targetId: "modal-case-11" },
12: { name: "Case 12: Send thankcards success", type: "modal", targetId: "modal-case-12" },
13: { name: "Case 13: Password confirmation mismatch", type: "modal", targetId: "modal-case-13" },
14: { name: "Case 14: Change password success", type: "modal", targetId: "modal-case-14" },
15: { name: "Case 15: Generic system error", type: "modal", targetId: "modal-case-15" }
};
let activeCaseId = null;
window.showToast = function(id) {
document.querySelectorAll('[role="alert"]').forEach(el => {
el.classList.add('hidden');
el.classList.remove('flex', 'opacity-100', 'translate-y-0');
el.classList.add('opacity-0', 'translate-y-[-10px]');
});
const toast = document.getElementById(id);
if (!toast) return;
toast.classList.remove('hidden');
toast.classList.add('flex');
requestAnimationFrame(() => {
toast.classList.remove('opacity-0', 'translate-y-[-10px]');
toast.classList.add('opacity-100', 'translate-y-0');
});
};
window.closeToast = function(id) {
const toast = document.getElementById(id);
if (!toast) return;
toast.classList.remove('opacity-100', 'translate-y-0');
toast.classList.add('opacity-0', 'translate-y-[-10px]');
setTimeout(() => {
toast.classList.add('hidden');
toast.classList.remove('flex');
}, 300);
};
let _isTransitioning = false;
function resetAllViews() {
let hadOpenItem = false;
Object.keys(casesMetadata).forEach(id => {
const meta = casesMetadata[id];
const el = document.getElementById(meta.targetId);
if (el && !el.classList.contains('hidden')) {
hadOpenItem = true;
if (meta.type === 'modal') {
closeModal(meta.targetId);
} else if (meta.type === 'toast') {
closeToast(meta.targetId);
}
}
});
document.getElementById('mock-table-view').classList.remove('hidden');
document.getElementById('mock-empty-state-view').classList.add('hidden');
document.getElementById('mock-empty-state-view').classList.remove('flex');
document.querySelectorAll('.case-btn').forEach(btn => {
btn.classList.remove('border-primary', 'bg-primary-light/35', 'ring-2', 'ring-primary/20');
btn.classList.add('border-slate-100');
});
return new Promise(resolve => setTimeout(resolve, hadOpenItem ? 310 : 0));
}
window.triggerCase = async function(caseId) {
if (_isTransitioning) return;
_isTransitioning = true;
await resetAllViews();
const meta = casesMetadata[caseId];
if (!meta) { _isTransitioning = false; return; }
activeCaseId = caseId;
const btn = document.getElementById(`case-btn-${caseId}`);
if (btn) {
btn.classList.remove('border-slate-100');
btn.classList.add('border-primary', 'bg-primary-light/35', 'ring-2', 'ring-primary/20');
btn.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
document.getElementById('current-case-label').innerText = `Active Case: ${meta.name}`;
if (meta.type === 'modal') {
openModal(meta.targetId);
} else if (meta.type === 'toast') {
showToast(meta.targetId);
} else if (meta.type === 'empty-state') {
document.getElementById('mock-table-view').classList.add('hidden');
document.getElementById('mock-empty-state-view').classList.remove('hidden');
document.getElementById('mock-empty-state-view').classList.add('flex');
}
setTimeout(() => { _isTransitioning = false; }, 350);
};
document.addEventListener('DOMContentLoaded', () => {
const firstBtn = document.getElementById('case-btn-1');
if (firstBtn) {
firstBtn.classList.remove('border-slate-100');
firstBtn.classList.add('border-primary', 'bg-primary-light/35', 'ring-2', 'ring-primary/20');
}
document.getElementById('current-case-label').innerText =
`Active Case: ${casesMetadata[1].name}`;
setTimeout(() => { triggerCase(1); }, 400);
const reloadBtn = document.getElementById('reload-table-btn');
if (reloadBtn) {
reloadBtn.addEventListener('click', async () => {
await resetAllViews();
document.getElementById('current-case-label').innerText = `Active Case: Mock Table Reloaded`;
});
}
});
</script>
@endpush
+5
View File
@@ -73,5 +73,10 @@
</tbody>
</table>
</div>
@if($administrations->hasPages())
<div class="px-6 py-4 border-t border-border-light bg-gray-50/30">
{{ $administrations->links() }}
</div>
@endif
</div>
@endsection