feature: send thankscard page

This commit is contained in:
antv
2026-07-14 16:23:04 +07:00
parent 3aa9e04bd7
commit 05605ed1eb
28 changed files with 536 additions and 297 deletions
+3
View File
@@ -63,3 +63,6 @@ AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
CHATOPS_API_URL=http://10.1.12.160/aiapi/sent-message-user
CHATOPS_API_TOKEN=
+30 -1
View File
@@ -97,8 +97,26 @@ class AdminController extends Controller
public function update(UpdateUserRequest $request, string $msnv)
{
$user = $this->adminService->getUserByMsnv($msnv);
$validatedData = $request->validated();
$this->adminService->updateUser($user, $request->validated());
$this->adminService->updateUser($user, $validatedData);
$currentUser = auth()->user();
if ($currentUser && $user->id === $currentUser->id && isset($validatedData['status']) && $validatedData['status'] == config('constants.STATUS_INACTIVE')) {
auth()->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
if ($request->expectsJson() || $request->ajax() || $request->wantsJson()) {
return response()->json([
'success' => true,
'message' => 'Tài khoản của bạn đã chuyển sang trạng thái nghỉ việc. Bạn đã được đăng xuất.',
'redirect' => route('login'),
]);
}
return redirect()->route('login')->with('success', 'Tài khoản của bạn đã chuyển sang trạng thái nghỉ việc. Bạn đã được đăng xuất.');
}
if ($request->expectsJson()) {
return response()->json([
@@ -116,8 +134,19 @@ class AdminController extends Controller
*/
public function destroy(string $msnv): RedirectResponse
{
$user = $this->adminService->getUserByMsnv($msnv);
$this->adminService->deactivateUser($msnv);
$currentUser = auth()->user();
if ($currentUser && $user->id === $currentUser->id) {
auth()->logout();
request()->session()->invalidate();
request()->session()->regenerateToken();
return redirect()->route('login')->with('success', 'Tài khoản của bạn đã chuyển sang trạng thái nghỉ việc. Bạn đã được đăng xuất.');
}
return redirect()->route('admin.users.index')->with('success', __('messages.user_deactivate_success'));
}
+7 -4
View File
@@ -61,9 +61,6 @@ class UserController extends Controller
return view('user.send', compact('users'));
}
/**
* Store and execute sending thank cards to another user.
*/
public function storeThankcards(SendThankCardRequest $request): JsonResponse
{
try {
@@ -76,7 +73,13 @@ class UserController extends Controller
return response()->json(['success' => false, 'message' => $e->getMessage()], 422);
}
return response()->json(['success' => true, 'message' => __('messages.thank_card_send_success')]);
$receiverUser = \App\Models\User::where('msnv', $request->input('receiver'))->first();
return response()->json([
'success' => true,
'message' => __('messages.thank_card_send_success'),
'receiver_email' => $receiverUser ? $receiverUser->mail : '',
]);
}
/**
+15
View File
@@ -88,5 +88,20 @@ class User extends Authenticatable
return asset($value);
}
/**
* Get the user's department name.
*
* @return string|null
*/
public function getDepartmentNameAttribute()
{
$deptVal = $this->departments;
if (is_numeric($deptVal)) {
$idx = ((int)$deptVal) - 1;
return config('constants.DEPARTMENTS')[$idx] ?? $deptVal;
}
return $deptVal;
}
}
+4 -1
View File
@@ -29,5 +29,8 @@ return [
'DL-SSD - Smart Solutions Delivery Division / DL-SSD - Ho Chi Minh',
'FC-HRD - Human Resources Division / FC-HRD - Ho Chi Minh',
'FC-RMD - Risk Management Division / FC-RMD - Ho Chi Minh'
]
],
'CHATOPS_API_URL' => env('CHATOPS_API_URL', 'http://10.1.12.160/aiapi/sent-message-user'),
'CHATOPS_API_TOKEN' => env('CHATOPS_API_TOKEN', 'ifOgk13pXIa6p2q6vvJB'),
];
+6 -5
View File
@@ -26,6 +26,7 @@ class MockDataSeeder extends Seeder
// The default password is now set to 1234@Dcba according to the design requirements
$password = md5('1234@Dcba');
$depts = config('constants.DEPARTMENTS');
// 1. CREATE USERS
// Admins
@@ -34,7 +35,7 @@ class MockDataSeeder extends Seeder
'name' => 'System Admin',
'mail' => 'admin@runsystem.net',
'pass' => $password,
'departments' => 1,
'departments' => $depts[0],
'role' => config('constants.ROLE_ADMIN'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 0,
@@ -47,7 +48,7 @@ class MockDataSeeder extends Seeder
'name' => 'Second Admin',
'mail' => 'admin2@runsystem.net',
'pass' => $password,
'departments' => 1,
'departments' => $depts[0],
'role' => config('constants.ROLE_ADMIN'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 0,
@@ -81,13 +82,13 @@ class MockDataSeeder extends Seeder
];
}
foreach ($users as $u) {
foreach ($users as $idx => $u) {
User::create([
'msnv' => $u['msnv'],
'name' => $u['name'],
'mail' => $u['mail'],
'pass' => $password,
'departments' => rand(1, 5),
'departments' => $depts[$idx % count($depts)],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => $u['card'],
@@ -114,7 +115,7 @@ class MockDataSeeder extends Seeder
'name' => 'Nghi Duy',
'mail' => 'nghiduy@runsystem.net',
'pass' => $password,
'departments' => 2,
'departments' => $depts[1],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_INACTIVE'),
'card' => 0,
+2 -2
View File
@@ -9,8 +9,8 @@ return [
'thank_card_send_success' => 'Đã gửi Thank card thành công!',
'error' => [
'no_send_permission' => 'Bạn hiện không có quyền gửi Thank Card.',
'not_enough_cards' => 'Số lượng card của bạn không đủ. Vui lòng liên hệ Admin.',
'no_send_permission' => "Bạn không thể gửi Thank Card.\nCó thể bạn đã sử dụng hết số lượng Thank Card hoặc chưa được cấp quyền gửi. Vui lòng liên hệ Admin để được hỗ trợ.",
'not_enough_cards' => "Bạn không thể gửi Thank Card.\nCó thể bạn đã sử dụng hết số lượng Thank Card hoặc chưa được cấp quyền gửi. Vui lòng liên hệ Admin để được hỗ trợ.",
'max_send_limit_template' => 'Một tháng bạn chỉ được gửi tối đa :max card cho một người. Bạn đã gửi :sent card cho nhân viên này.',
'receiver_invalid' => 'Người nhận không tồn tại hoặc đã nghỉ việc.',
]
+4
View File
File diff suppressed because one or more lines are too long
+2
View File
@@ -1,3 +1,5 @@
@import "./fonts.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
+1 -10
View File
@@ -2,16 +2,7 @@
@section('title', $mode === 'create' ? 'Thêm User Mới' : 'Chi tiết User')
@section('content')
<div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 pl-1">
<div>
<h1 class="text-[28px] font-extrabold text-[#1a2b49] flex items-center gap-2 mb-1.5 tracking-tight">
{{ $mode === 'create' ? 'Thêm User Mới' : 'Chi tiết User' }}
</h1>
<p class="text-gray-500 font-medium text-[14px]">
{{ $mode === 'create' ? 'Tạo tài khoản nhân viên mới và thiết lập các thông tin cơ bản.' : 'Quản lý thông tin thẻ và theo dõi lịch sử nhận gửi của nhân viên.' }}
</p>
</div>
</div>
<x-user-page :mode="$mode" :user="$user ?? null" :administrations="$administrations ?? null" :selectedMonth="$selectedMonth ?? null" :addCards="$addCards ?? null" :admins="$admins ?? null" />
@endsection
@@ -38,13 +38,7 @@
</td>
<td class="table-body-cell">
@php
$deptVal = $u->departments;
if (is_numeric($deptVal)) {
$idx = ((int)$deptVal) - 1;
$deptName = config('constants.DEPARTMENTS')[$idx] ?? 'Team';
} else {
$deptName = $deptVal ?? 'Team';
}
$deptName = $u->department_name ?? 'Team';
@endphp
<div class="max-w-[200px] truncate font-medium text-gray-600" title="{{ $deptName }}">
{{ $deptName }}
+1 -7
View File
@@ -23,13 +23,7 @@
<div class="w-10 h-10 rounded-full overflow-hidden shrink-0 border border-gray-200">
@php
$avatarUrl = Auth::user()->avatar ? Auth::user()->avatar : asset('images/avatar.png');
$deptVal = Auth::user()->departments;
if (is_numeric($deptVal)) {
$idx = ((int)$deptVal) - 1;
$deptName = config('constants.DEPARTMENTS')[$idx] ?? 'Team';
} else {
$deptName = $deptVal ?? 'Team';
}
$deptName = Auth::user()->department_name ?? 'Team';
@endphp
<img src="{{ $avatarUrl }}" alt="Avatar" class="w-full h-full object-cover" onerror="this.onerror=null; this.src='{{ asset('images/avatar.png') }}';">
</div>
@@ -61,6 +61,9 @@
<!-- Department -->
<div class="relative">
<label class="block text-[14px] font-bold text-[#1a2b49] mb-2" for="departments">Phòng ban <span class="text-red-500">*</span></label>
@php
$deptVal = old('departments', $user?->department_name);
@endphp
@if($mode === 'self')
<div class="relative flex items-center w-full">
<span class="absolute left-4 text-gray-400 pointer-events-none w-5 h-5 flex items-center justify-center">
@@ -72,8 +75,8 @@
class="form-input form-input-with-icon !rounded-xl !bg-gray-50 !text-gray-500 cursor-not-allowed w-full"
style="padding-left: 48px;"
readonly
title="{{ $user?->departments }}"
value="{{ $user?->departments }}">
title="{{ $deptVal }}"
value="{{ $deptVal }}">
</div>
@else
<div class="relative flex items-center w-full">
@@ -85,7 +88,7 @@
<select name="departments" id="departments" class="form-input form-input-with-icon !rounded-xl !focus:border-[#3462f7] !focus:ring-[#3462f7]/20 w-full" style="padding-left: 48px;" required>
<option value="">-- Chọn phòng ban --</option>
@foreach(config('constants.DEPARTMENTS') as $dept)
<option value="{{ $dept }}" {{ old('departments', $user?->departments) == $dept ? 'selected' : '' }}>{{ $dept }}</option>
<option value="{{ $dept }}" {{ $deptVal == $dept ? 'selected' : '' }}>{{ $dept }}</option>
@endforeach
</select>
</div>
@@ -11,7 +11,7 @@
<form id="filterForm" action="{{ $action }}" method="GET" class="flex flex-col md:flex-row md:flex-wrap items-stretch md:items-center gap-3.5 w-full mb-6 bg-white p-5 rounded-[24px] border border-white shadow-[0_10px_30px_rgba(15,23,42,0.05)]">
<div class="search-wrapper flex-1 relative flex items-center w-full">
<span class="absolute left-4 text-gray-400 pointer-events-none w-5 h-5 flex items-center justify-center z-10">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</span>
@@ -19,7 +19,7 @@
class="form-input form-input-with-icon !rounded-xl !focus:border-[#3462f7] !focus:ring-[#3462f7]/20 w-full"
style="padding-left: 44px; padding-right: 40px;" />
<button type="button" id="btnClearSearch" class="{{ empty($search) ? 'hidden' : '' }} absolute right-3.5 text-gray-400 hover:text-gray-600 focus:outline-none">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
@@ -84,7 +84,7 @@
field: "text",
direction: "asc"
},
placeholder: "🔍 Tìm phòng ban...",
placeholder: "Tìm phòng ban...",
render: {
item: function(data, escape) {
return '<div class="item" title="' + escape(data.text) + '">' + escape(data.text) + '</div>';
+12 -15
View File
@@ -26,9 +26,9 @@
<div class="bg-gradient-to-r from-blue-50/70 via-indigo-50/50 to-purple-50/30 px-6 py-6 sm:px-8 border-b border-gray-100">
<h3 class="text-[17px] font-extrabold text-[#1a2b49] leading-tight flex items-center gap-2">
<span>{{ match($mode) {
'create' => '👥 Thêm User Mới',
'delete' => '⚠️ Xác nhận nghỉ việc: ' . ($user?->msnv),
'self' => '👤 Thông tin cá nhân: ' . ($user?->msnv),
'create' => 'Thêm User Mới',
'delete' => 'Xác nhận nghỉ việc: ' . ($user?->name),
'self' => 'Thông tin cá nhân',
} }}</span>
</h3>
</div>
@@ -40,23 +40,20 @@
<div class="w-12 h-12 rounded-full bg-blue-50 text-[#3462f7] flex items-center justify-center font-extrabold text-lg shadow-inner flex-shrink-0">
{{ strtoupper(substr($user->name ?? $user->msnv, 0, 2)) }}
</div>
<div class="flex-1 min-w-0">
<h4 class="font-extrabold text-[#1a2b49] text-base truncate">{{ $user->name }}</h4>
<p class="text-xs text-gray-500 font-medium truncate mb-1">{{ $user->mail }}</p>
<div class="flex items-center gap-2">
<span class="px-2 py-0.5 text-[10px] font-bold rounded-full bg-blue-50 text-blue-700 border border-blue-100">
{{ $user->role == config('constants.ROLE_ADMIN') ? 'Admin' : 'Member' }}
</span>
<span class="text-xs text-gray-300 font-semibold"></span>
<span class="text-xs font-bold text-gray-600">Thẻ: <span class="text-orange-500 font-extrabold text-sm">{{ $user->card }}</span></span>
</div>
<div class="flex-1">
<h4 class="text-base font-extrabold text-[#1a2b49]">{{ $user->name }}</h4>
<p class="text-xs text-gray-500 font-semibold mt-1 flex items-center gap-2">
<span>MSNV: {{ $user->msnv }}</span>
<span class="w-1 h-1 bg-gray-300 rounded-full"></span>
<span>{{ $user->mail }}</span>
</p>
</div>
</div>
</div>
@endif
@if($actionUrl)
<form action="{{ $actionUrl }}" method="POST" enctype="multipart/form-data" class="flex flex-col flex-1 justify-between">
<form action="{{ $actionUrl }}" method="POST" id="userForm" enctype="multipart/form-data" class="flex-1 flex flex-col justify-between">
@csrf
@if($method !== 'POST')
@method($method)
@@ -66,7 +63,7 @@
<div class="p-6 sm:p-8 {{ !$isCreate && !$isEdit && !$isSelf ? 'pt-0' : '' }} space-y-5">
@if($isDelete && $user)
<p class="text-sm text-red-600 leading-relaxed font-semibold">
Bạn chắc chắn muốn cho nhân viên {{ $user->msnv }} nghỉ việc không? Tài khoản này sẽ không thể đăng nhập hoặc thực hiện các giao dịch thẻ sau khi xác nhận.
Bạn chắc chắn muốn cho nhân viên {{ $user->name }} (MSNV: {{ $user->msnv }}) nghỉ việc không? Tài khoản này sẽ không thể đăng nhập hoặc thực hiện các giao dịch thẻ sau khi xác nhận.
</p>
@else
<!-- 2. User Basic Information Section -->
@@ -344,11 +344,15 @@
await reloadListsSection();
} else {
alert(result.message || 'Đã xảy ra lỗi. Vui lòng thử lại.');
if (window.showToast) {
window.showToast(result.message || 'Đã xảy ra lỗi. Vui lòng thử lại.', 'error');
}
}
} catch (err) {
console.error(err);
alert('Đã xảy ra lỗi kết nối.');
if (window.showToast) {
window.showToast('Đã xảy ra lỗi kết nối.', 'error');
}
} finally {
if (submitBtn) submitBtn.disabled = false;
}
@@ -9,9 +9,10 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0V10.5m-2.812 10.5h14.625c.621 0 1.125-.504 1.125-1.125V11.25c0-.621-.504-1.125-1.125-1.125H3.75c-.621 0-1.125.504-1.125 1.125v7.875c0 .621.504 1.125 1.125 1.125z" />
</svg>
</span>
<input type="text" name="password" id="password" class="form-input form-input-with-icon !rounded-xl !bg-gray-50 !text-gray-500 cursor-not-allowed" style="padding-left: 48px;" readonly required value="1234@Dcba">
<input type="text" name="password" id="password" class="form-input form-input-with-icon !rounded-xl !focus:border-[#3462f7] !focus:ring-[#3462f7]/20" style="padding-left: 48px;" required value="{{ old('password', '1234@Dcba') }}">
</div>
<span class="text-[13px] text-gray-400 mt-2 block">Mật khẩu mặc định cho tất cả user mới tạo.</span>
@error('password')<span class="text-red-500 text-[13px] font-semibold mt-1.5 block">{{ $message }}</span>@enderror
<span class="text-[13px] text-gray-400 mt-2 block">Thiết lập mật khẩu khởi tạo cho user (mặc định 1234@Dcba).</span>
</div>
@elseif($mode === 'edit')
<div class="relative">
@@ -28,7 +29,7 @@
@elseif($mode === 'self')
<div class="border-t border-gray-100 pt-5 mt-5">
<h4 class="text-sm font-bold text-[#1a2b49] mb-4 flex items-center gap-2">
🔑 Bảo mật & Đổi mật khẩu
Bảo mật & Đổi mật khẩu
</h4>
<div class="space-y-4">
<!-- Current Password -->
+34 -1
View File
@@ -141,15 +141,48 @@
closeBtn.addEventListener('click', closeSidebar);
overlay.addEventListener('click', closeSidebar);
}
// Client-side flash message support
const flashSuccess = localStorage.getItem('flash_success');
if (flashSuccess) {
window.showToast(flashSuccess, 'success');
localStorage.removeItem('flash_success');
}
const flashError = localStorage.getItem('flash_error');
if (flashError) {
window.showToast(flashError, 'error');
localStorage.removeItem('flash_error');
}
});
window.showToast = function(message) {
window.showToast = function(message, type = 'success') {
const toast = document.getElementById('global-toast');
if (!toast) return;
const msgEl = toast.querySelector('.text-sm');
if (msgEl) msgEl.textContent = message;
// Dynamically set theme styling and icon
const iconContainer = toast.querySelector('.shrink-0');
if (iconContainer) {
iconContainer.className = 'inline-flex items-center justify-center shrink-0 w-9 h-9 rounded-lg';
let iconHtml = '';
if (type === 'success') {
iconContainer.classList.add('bg-emerald-50', 'text-emerald-500');
iconHtml = '<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>';
} else if (type === 'error') {
iconContainer.classList.add('bg-red-50', 'text-red-500');
iconHtml = '<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>';
} else if (type === 'warning') {
iconContainer.classList.add('bg-amber-50', 'text-amber-500');
iconHtml = '<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 {
iconContainer.classList.add('bg-blue-50', 'text-blue-500');
iconHtml = '<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>';
}
iconContainer.innerHTML = iconHtml;
}
toast.classList.remove('hidden');
toast.classList.add('flex');
-1
View File
@@ -5,7 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>GMO-Z.com RUNSYSTEM - {{ __('auth.login') }}</title>
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@700&display=swap" rel="stylesheet">
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="min-h-screen flex items-center justify-center font-sans antialiased overflow-hidden relative bg-[#f0f4f9] p-4 sm:p-6 lg:p-10">
@@ -3,13 +3,7 @@
<div class="flex flex-col">
@foreach($rankers as $item)
@php
$deptVal = $item->departments;
if (is_numeric($deptVal)) {
$idx = ((int)$deptVal) - 1;
$teamName = config('constants.DEPARTMENTS')[$idx] ?? 'Team';
} else {
$teamName = $deptVal ?? 'Team';
}
$teamName = $item->department_name ?? 'Team';
@endphp
<x-ranking-item
rank="{{ $item->rank }}"
@@ -33,13 +27,7 @@
<div class="mt-auto">
<div class="my-3 border-t border-dashed border-gray-100 mx-2"></div>
@php
$currDeptVal = $currentUserRankItem->departments;
if (is_numeric($currDeptVal)) {
$idx = ((int)$currDeptVal) - 1;
$currTeamName = config('constants.DEPARTMENTS')[$idx] ?? 'Team';
} else {
$currTeamName = $currDeptVal ?? 'Team';
}
$currTeamName = $currentUserRankItem->department_name ?? 'Team';
@endphp
<x-ranking-item
rank="{{ $currentUserRankItem->rank > 0 ? $currentUserRankItem->rank : '-' }}"
+248 -120
View File
@@ -2,185 +2,313 @@
@section('title', 'Gửi Thank Card')
@section('content')
<div class="max-w-[640px] mx-auto w-full pb-10">
<div class="bg-white rounded-[24px] shadow-[0_10px_30px_rgba(15,23,42,0.05)] border border-white overflow-hidden">
@php
$tpls = [
1=>['icon'=>'💌','name'=>'Thư cảm ơn', 'thumb'=>'from-[#FFF9EC] to-[#FFF5DD]','thumbAccent'=>'#FFD066'],
2=>['icon'=>'🌸','name'=>'Sakura', 'thumb'=>'from-[#FFF5F9] to-[#FFECF4]','thumbAccent'=>'#F9A8D4'],
3=>['icon'=>'⭐','name'=>'Appreciation','thumb'=>'from-[#EFF8FF] to-[#DBEAFE]','thumbAccent'=>'#93C5FD'],
4=>['icon'=>'🎉','name'=>'Celebration', 'thumb'=>'from-[#F5F0FF] to-[#EDE9FE]','thumbAccent'=>'#C4B5FD'],
];
@endphp
<div class="bg-gradient-to-r from-[#eff4ff] to-[#faeffa] px-8 py-8 flex flex-col relative overflow-hidden">
<div class="absolute -top-10 -right-10 w-32 h-32 bg-white/30 rounded-full blur-xl"></div>
<div class="absolute -bottom-10 -left-10 w-24 h-24 bg-white/20 rounded-full blur-lg"></div>
<div class="max-w-[760px] mx-auto w-full pb-12 px-4 lg:px-0">
<div class="relative z-10 flex items-center gap-3">
<h3 class="text-[22px] font-extrabold text-[#1a2b49] tracking-tight">Gửi Thank Card</h3>
<span class="text-xl animate-pulse">💌</span>
{{-- Header --}}
<div class="relative bg-gradient-to-r from-[#eff4ff] to-[#faeffa] rounded-[24px] p-7 md:p-10 mb-7 overflow-hidden border border-white shadow-[0_10px_30px_rgba(15,23,42,0.05)]">
<div class="absolute -top-10 -right-10 w-44 h-44 bg-white/25 rounded-full blur-2xl"></div>
<div class="absolute -bottom-8 -left-8 w-32 h-32 bg-white/20 rounded-full blur-xl"></div>
<div class="relative z-10 flex items-center gap-4">
<div class="w-14 h-14 rounded-2xl bg-white/80 shadow-sm flex items-center justify-center text-3xl shrink-0">💌</div>
<div>
<h1 class="text-[26px] md:text-[30px] font-extrabold text-[#1a2b49] tracking-tight leading-tight">Gửi Thank Card</h1>
<p class="text-[14px] text-gray-500 font-medium mt-1 max-w-[380px] leading-relaxed">Gửi lời cảm ơn chân thành đến đồng nghiệp lan tỏa văn hóa ghi nhận trong tổ chức.</p>
</div>
</div>
<p class="text-[13px] text-gray-500 font-medium mt-1.5 relative z-10">
Gửi gắm lời cảm ơn chân thành đến người đồng nghiệp tuyệt vời của bạn!
</p>
</div>
<form id="sendCardForm">
@csrf
<input type="hidden" name="template_id" id="template_id" value="1">
<div class="p-8 space-y-6">
<div class="flex flex-col gap-2">
<label class="text-[14px] font-bold text-text-dark" for="receiver">
Chọn người nhận <span class="text-red-500">*</span>
</label>
<select name="receiver" id="receiver" class="form-input rounded-xl border-[#d9dfe7]" required>
<option value="" disabled selected>-- Chọn đồng nghiệp --</option>
{{-- Template Gallery --}}
<div class="bg-white rounded-[20px] shadow-[0_4px_20px_rgba(15,23,42,0.06)] border border-[#f0f4fa] p-6 mb-5">
<p class="text-[13px] font-bold text-gray-400 uppercase tracking-widest mb-4">Chọn mẫu thiệp <span class="text-[#3462f7]">*</span></p>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
@foreach($tpls as $id => $t)
<button type="button" onclick="selectTpl(this,{{$id}})"
data-tpl="{{$id}}"
class="tpl-btn relative group flex flex-col items-center gap-2 p-4 rounded-2xl border-2 transition-all duration-200 bg-gradient-to-br {{$t['thumb']}} cursor-pointer
{{$loop->first ? 'border-[#3462f7] ring-2 ring-[#3462f7]/20 shadow-md' : 'border-[#e8edf4] hover:border-[#3462f7]/40 hover:shadow-md'}}">
<span class="tpl-check absolute top-2 right-2 w-5 h-5 rounded-full bg-[#3462f7] flex items-center justify-center {{$loop->first ? 'opacity-100':'opacity-0'}} transition-opacity duration-200">
<svg class="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
</span>
<span class="text-3xl group-hover:scale-110 transition-transform duration-200 select-none">{{$t['icon']}}</span>
<span class="text-[12px] font-bold text-[#1a2b49] text-center leading-tight">{{$t['name']}}</span>
</button>
@endforeach
</div>
</div>
{{-- e-Card (form inside the card) --}}
<div id="eCard" class="relative rounded-[24px] overflow-hidden shadow-[0_20px_60px_rgba(15,23,42,0.12)] border-2 mb-5 transition-all duration-500"
style="background:linear-gradient(145deg,#FFF9EC,#FFFDF7);border-color:#FFE4A0;">
{{-- Decorative SVG top --}}
<div id="cardDeco" class="relative overflow-hidden" style="height:140px;background:linear-gradient(135deg,#FFF5CC,#FFE8A0);">
{{-- blob 1 --}}
<div class="absolute -top-8 -left-8 w-36 h-36 rounded-full opacity-40 blur-2xl" id="blob1" style="background:#FFD066;"></div>
<div class="absolute -bottom-6 -right-6 w-28 h-28 rounded-full opacity-30 blur-xl" id="blob2" style="background:#FFAD00;"></div>
{{-- SVG decoration --}}
<div id="decoSvg" class="absolute inset-0 flex items-center justify-center select-none pointer-events-none">
{{-- Template 1 default: envelope --}}
<div class="flex flex-col items-center gap-1 z-10">
<span id="cardBigIcon" class="text-[72px] leading-none drop-shadow-lg" style="filter:drop-shadow(0 4px 16px rgba(0,0,0,0.10))">💌</span>
<span id="cardLabel" class="text-[13px] font-bold uppercase tracking-[0.2em] mt-1" style="color:#B8860B;">Thank Card</span>
</div>
</div>
{{-- wavy bottom --}}
<svg class="absolute bottom-0 left-0 w-full" viewBox="0 0 760 30" preserveAspectRatio="none" style="height:30px;">
<path id="wavePath" d="M0,20 Q190,0 380,20 Q570,40 760,20 L760,30 L0,30 Z" fill="white" opacity="0.8"/>
</svg>
</div>
{{-- Card body: form fields ON the card --}}
<div class="px-8 py-7 bg-white/0">
{{-- To: Người nhận --}}
<div class="mb-6">
<label class="block text-[11px] font-bold uppercase tracking-widest mb-2" id="labelTo" style="color:#B8860B;"> Gửi đến</label>
<select name="receiver" id="receiver" class="form-input rounded-xl border-[#e8edf4] bg-white/90" required>
<option value="" disabled selected></option>
@foreach($users as $u)
<option value="{{ $u->msnv }}">{{ $u->name }} - {{ $u->msnv }} ({{ $u->mail }})</option>
@php $dept=$u->department_name??'Team'; @endphp
<option value="{{$u->msnv}}">{{$u->name}} - {{$u->msnv}} ({{$u->mail}}) - {{$dept}}</option>
@endforeach
</select>
</div>
<div class="flex flex-col gap-2">
<label class="text-[14px] font-bold text-text-dark" for="amount">
Số lượng thẻ <span class="text-red-500">*</span>
</label>
<select name="amount" id="amount" class="form-input rounded-xl border-[#d9dfe7]" required>
{{-- Số lượng --}}
<div class="mb-6">
<label class="block text-[11px] font-bold uppercase tracking-widest mb-2" id="labelQty" style="color:#B8860B;">🎴 Số lượng thẻ</label>
<div class="flex items-center gap-3">
<select name="amount" id="amount" class="form-input rounded-xl border-[#e8edf4] bg-white/90 !w-auto !px-5" required>
<option value="1">1 Thẻ</option>
<option value="2">2 Thẻ</option>
<option value="3">3 Thẻ</option>
<option value="4">4 Thẻ</option>
<option value="{{ \App\Models\Administration::MAX_SEND_CARD_PER_MONTH }}">{{ \App\Models\Administration::MAX_SEND_CARD_PER_MONTH }} Thẻ (Tối đa)</option>
<option value="{{App\Models\Administration::MAX_SEND_CARD_PER_MONTH}}">{{App\Models\Administration::MAX_SEND_CARD_PER_MONTH}} Thẻ (Tối đa)</option>
</select>
<div class="mt-1 flex items-start gap-2 bg-[#eff4ff] p-3.5 rounded-xl border border-blue-50">
<svg class="w-5 h-5 text-[#3462f7] shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<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>
<p class="text-[12px] text-gray-600 font-medium leading-relaxed">
Lưu ý: Chỉ được gửi tối đa {{ \App\Models\Administration::MAX_SEND_CARD_PER_MONTH }} thẻ cho cùng 1 người trong 1 tháng để đảm bảo tính công bằng.
</p>
<span class="text-[12px] text-gray-400 font-medium">/ tháng</span>
</div>
</div>
<div class="flex flex-col gap-2 pt-2">
<div class="flex items-center gap-3 bg-gray-50/50 p-4 rounded-xl border border-[#d9dfe7]/60 hover:bg-gray-50 transition-colors">
<input type="checkbox" name="confirm_written" id="confirm_written" value="1" class="w-5 h-5 text-[#3462f7] border-[#d9dfe7] rounded-md focus:ring-[#3462f7]/20 transition-all cursor-pointer" required>
<label for="confirm_written" class="text-[13px] font-medium text-gray-700 cursor-pointer select-none">
Xác nhận đã viết card <span class="text-red-500">*</span>
{{-- Confirm checkbox --}}
<label class="flex items-start gap-3 p-4 rounded-2xl border border-[#e8edf4] bg-white/70 hover:bg-white/90 transition-colors cursor-pointer mb-2">
<input type="checkbox" name="confirm_written" id="confirm_written" value="1"
class="w-4 h-4 mt-0.5 text-[#3462f7] border-[#d9dfe7] rounded focus:ring-[#3462f7]/20 cursor-pointer shrink-0" required>
<span class="text-[13px] font-medium text-gray-600 leading-relaxed select-none">
Tôi xác nhận đã viết nội dung trên ThankCard sẵn sàng gửi đến người nhận. <span class="text-[#3462f7] font-bold">*</span>
</span>
</label>
{{-- Card footer line --}}
<div class="mt-6 pt-4 border-t border-dashed" id="cardFooterLine" style="border-color:#FFE4A0;">
<div class="flex items-center justify-between">
<span class="text-[11px] text-gray-400 font-medium">Từ: <span class="font-bold text-gray-500">{{ Auth::user()->name ?? 'Bạn' }}</span></span>
<span id="cardFooterBadge" class="text-[10px] font-bold px-2.5 py-1 rounded-full" style="background:rgba(255,176,0,0.15);color:#B8860B;">Thư cảm ơn</span>
</div>
</div>
</div>
</div>
<div class="px-8 py-5 border-t border-[#d9dfe7]/50 bg-gray-50/50 flex flex-col sm:flex-row justify-end gap-3">
<a href="{{ route('user.dashboard') }}" class="btn-secondary rounded-xl font-bold">Hủy bỏ</a>
<button type="button" id="btnSubmitSend" class="btn-primary rounded-xl font-bold !bg-[#3462f7] hover:!bg-blue-700 shadow-md shadow-blue-500/10">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor" class="w-4 h-4 mr-2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
</svg>
Gửi Card
{{-- Info note --}}
<div class="flex items-start gap-3 bg-gradient-to-r from-[#eff4ff] to-[#f5f0ff] p-4 rounded-2xl border border-[#dce7ff] mb-5">
<div class="w-8 h-8 rounded-xl bg-white flex items-center justify-center shrink-0 shadow-sm">
<svg class="w-4 h-4 text-[#3462f7]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><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>
<div>
<p class="text-[12px] font-bold text-[#3462f7] mb-0.5">Lưu ý</p>
<p class="text-[13px] text-gray-600 font-medium leading-relaxed">Chỉ được gửi tối đa <strong>{{ App\Models\Administration::MAX_SEND_CARD_PER_MONTH }} thẻ</strong> cho cùng một người trong tháng để đảm bảo tính công bằng.</p>
</div>
</div>
{{-- Actions --}}
<div class="flex flex-col sm:flex-row justify-end gap-3">
<a href="{{ route('user.dashboard') }}" class="btn-secondary rounded-xl font-bold flex items-center justify-center gap-2 order-2 sm:order-1">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18"/></svg>Hủy bỏ
</a>
<button type="button" id="btnSubmitSend" class="btn-primary rounded-xl font-bold !bg-[#3462f7] hover:!bg-blue-700 shadow-md shadow-blue-500/15 flex items-center justify-center gap-2 order-1 sm:order-2">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>Gửi Card
</button>
</div>
</form>
</div>
</div>
<x-modal id="customModal" title="Thông báo" maxWidth="md">
<p id="modalMessage" class="text-sm text-gray-600 leading-relaxed"></p>
<x-slot name="footer">
<div id="modalActions" class="w-full flex flex-col sm:flex-row-reverse gap-3">
</div>
<div id="modalActions" class="w-full flex flex-col sm:flex-row-reverse gap-3"></div>
</x-slot>
</x-modal>
@endsection
@push('scripts')
<script src="{{ asset('js/jquery.min.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
new TomSelect("#receiver", {
create: false,
sortField: {
field: "text",
direction: "asc"
},
placeholder: "🔍 Nhập tên, mã NV hoặc email...",
render: {
no_results: function(data, escape) {
return '<div class="no-results p-3 text-gray-500">Không tìm thấy nhân viên nào</div>';
// ── Theme data ───────────────────────────────────────────────────
const T = {
1:{icon:'💌',name:'Thư cảm ơn', label:'THANK CARD',
deco:'linear-gradient(135deg,#FFF5CC,#FFE8A0)',
card:'linear-gradient(145deg,#FFF9EC,#FFFDF7)',
border:'#FFE4A0', blob1:'#FFD066', blob2:'#FFAD00',
accent:'#B8860B', badgeBg:'rgba(255,176,0,0.15)', wave:'white',
footerBorder:'#FFE4A0'},
2:{icon:'🌸',name:'Sakura', label:'SAKURA CARD',
deco:'linear-gradient(135deg,#FFE8F4,#FFD6EC)',
card:'linear-gradient(145deg,#FFF8FB,#FFF3F8)',
border:'#FFBCD9', blob1:'#F9A8D4', blob2:'#F472B6',
accent:'#be185d', badgeBg:'rgba(244,114,182,0.12)', wave:'white',
footerBorder:'#FFBCD9'},
3:{icon:'⭐',name:'Appreciation',label:'APPRECIATION',
deco:'linear-gradient(135deg,#DBEAFE,#BFDBFE)',
card:'linear-gradient(145deg,#F0F8FF,#EAF3FF)',
border:'#BFDBFE', blob1:'#93C5FD', blob2:'#60A5FA',
accent:'#1d4ed8', badgeBg:'rgba(96,165,250,0.12)', wave:'white',
footerBorder:'#BFDBFE'},
4:{icon:'🎉',name:'Celebration', label:'CELEBRATION',
deco:'linear-gradient(135deg,#EDE9FE,#DDD6FE)',
card:'linear-gradient(145deg,#F8F5FF,#F3EFFF)',
border:'#DDD6FE', blob1:'#C4B5FD', blob2:'#A78BFA',
accent:'#6d28d9', badgeBg:'rgba(167,139,250,0.12)', wave:'white',
footerBorder:'#DDD6FE'},
};
function applyTheme(id) {
const t = T[id]; if (!t) return;
const $card = document.getElementById('eCard');
const $deco = document.getElementById('cardDeco');
const $b1 = document.getElementById('blob1');
const $b2 = document.getElementById('blob2');
const $bigIcon = document.getElementById('cardBigIcon');
const $label = document.getElementById('cardLabel');
const $wave = document.getElementById('wavePath');
const $lTo = document.getElementById('labelTo');
const $lQty = document.getElementById('labelQty');
const $badge = document.getElementById('cardFooterBadge');
const $footerL = document.getElementById('cardFooterLine');
$card.style.background = t.card;
$card.style.borderColor = t.border;
$deco.style.background = t.deco;
$b1.style.background = t.blob1;
$b2.style.background = t.blob2;
$bigIcon.textContent = t.icon;
$label.textContent = t.label;
$label.style.color = t.accent;
$wave.setAttribute('fill', t.wave);
$lTo.style.color = t.accent;
$lQty.style.color = t.accent;
$badge.textContent = t.name;
$badge.style.background = t.badgeBg;
$badge.style.color = t.accent;
$footerL.style.borderColor = t.footerBorder;
}
// ── Template picker ──────────────────────────────────────────────
window.selectTpl = function(el, id) {
document.querySelectorAll('.tpl-btn').forEach(b => {
b.classList.remove('border-[#3462f7]','ring-2','ring-[#3462f7]/20','shadow-md');
b.classList.add('border-[#e8edf4]');
b.querySelector('.tpl-check').classList.replace('opacity-100','opacity-0');
});
el.classList.remove('border-[#e8edf4]');
el.classList.add('border-[#3462f7]','ring-2','ring-[#3462f7]/20','shadow-md');
el.querySelector('.tpl-check').classList.replace('opacity-0','opacity-100');
document.getElementById('template_id').value = id;
applyTheme(id);
};
applyTheme(1);
// ── TomSelect ────────────────────────────────────────────────────
new TomSelect('#receiver', {
create:false, searchField:['text','value'], maxOptions:10,
sortField:{field:'text',direction:'asc'},
placeholder:'Nhập tên hoặc mã nhân viên...',
render:{
option:function(d,e){
const p=d.text.split(' - '),n=p[0]||'',i=p.slice(1).join(' · ')||'';
return`<div class="px-3 py-2.5 flex flex-col gap-0.5 hover:bg-[#eff4ff] cursor-pointer transition-colors">
<span class="font-bold text-[14px] text-[#1a2b49]">${e(n)}</span>
<span class="text-[11px] text-gray-400">${e(i)}</span></div>`;
},
item:function(d,e){
const n=d.text.split(' - ')[0]||'';
return`<div class="font-semibold text-[14px] text-[#1a2b49]">${e(n)} <span class="text-gray-400 font-normal text-[12px]">(${e(d.value)})</span></div>`;
},
no_results:()=>'<div class="p-3 text-[13px] text-gray-400 text-center">Không tìm thấy nhân viên nào</div>'
}
});
const btnSubmit = document.getElementById('btnSubmitSend');
const form = document.getElementById('sendCardForm');
// ── Modal ────────────────────────────────────────────────────────
const $mTitle = document.getElementById('modal-title-customModal');
const $mMsg = document.getElementById('modalMessage');
const $mAct = document.getElementById('modalActions');
const modalTitle = document.getElementById('modal-title-customModal');
const modalMessage = document.getElementById('modalMessage');
const modalActions = document.getElementById('modalActions');
const showModal = (title, message, isConfirm, confirmCallback) => {
modalTitle.textContent = title;
modalMessage.innerHTML = message;
if(isConfirm) {
modalTitle.className = "text-lg font-bold text-primary";
modalActions.innerHTML = `
<button id="btnConfirmOk" class="w-full sm:w-auto btn-primary !px-6 shadow-sm rounded-xl font-bold">Xác nhận gửi</button>
<button id="btnCancel" class="w-full sm:w-auto btn-secondary !px-6 shadow-sm rounded-xl font-bold">Hủy</button>
`;
document.getElementById('btnCancel').addEventListener('click', () => window.closeModal('customModal'));
document.getElementById('btnConfirmOk').addEventListener('click', function() {
const showModal = (title, msg, confirm, cb) => {
$mTitle.textContent = title;
$mMsg.innerHTML = msg.replace(/\n/g,'<br>');
if (confirm) {
$mTitle.className = 'text-lg font-bold text-primary';
$mAct.innerHTML = `<button id="btnOk" class="w-full sm:w-auto btn-primary !px-6 rounded-xl font-bold">Xác nhận gửi</button>
<button id="btnCancel" class="w-full sm:w-auto btn-secondary !px-6 rounded-xl font-bold">Hủy</button>`;
document.getElementById('btnCancel').onclick = () => window.closeModal('customModal');
document.getElementById('btnOk').addEventListener('click', function(){
this.disabled=true;
this.innerHTML='<span class="flex items-center gap-2"><svg class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> Đang xử lý...</span>';
confirmCallback();
cb();
});
} else {
modalTitle.className = "text-lg font-bold text-red-600";
modalActions.innerHTML = `
<button id="btnOkOnly" class="w-full sm:w-auto btn-secondary !px-6 shadow-sm rounded-xl font-bold">Đóng</button>
`;
document.getElementById('btnOkOnly').addEventListener('click', () => window.closeModal('customModal'));
$mTitle.className = 'text-lg font-bold text-[#1a2b49]';
$mAct.innerHTML = `<button id="btnClose" class="w-full sm:w-auto btn-secondary !px-6 rounded-xl font-bold">Đóng</button>`;
document.getElementById('btnClose').onclick = () => window.closeModal('customModal');
}
window.openModal('customModal');
};
btnSubmit.addEventListener('click', function() {
if(!form.checkValidity()) {
form.reportValidity();
return;
}
const receiver = document.getElementById('receiver').value;
// ── Submit ───────────────────────────────────────────────────────
document.getElementById('btnSubmitSend').addEventListener('click', function(){
const form = document.getElementById('sendCardForm');
if (!form.checkValidity()) { form.reportValidity(); return; }
const msnv = document.getElementById('receiver').value;
const amount = document.getElementById('amount').value;
showModal(
'Xác nhận gửi Thank Card',
`Bạn có chắc chắn muốn gửi <b>${amount} thẻ</b> cho nhân viên <span class="font-bold text-primary">${receiver}</span> không? <br><br><span class="text-xs text-text-light">Hành động này không thể hoàn tác, tin nhắn CO sẽ được gửi đi!</span>`,
true,
() => {
fetch('{{ route('user.store_send') }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
body: JSON.stringify({
receiver: receiver,
amount: amount
})
})
.then(response => response.json().then(data => ({status: response.status, body: data})))
.then(res => {
if (res.status === 200 && res.body.success) {
alert(res.body.message);
window.location.href = '{{ route('user.dashboard') }}';
showModal('Xác nhận gửi Thank Card',
'Đừng quên hoàn thành Thank Card của bạn nhé! Chúng mình sẽ gửi thông báo đến người nhận để họ đến khu vực nhận thư.',
true, () => {
$.ajax({
url:'{{ route("user.store_send") }}', method:'POST', contentType:'application/json',
headers:{'X-CSRF-TOKEN':$('meta[name="csrf-token"]').attr('content')},
data:JSON.stringify({receiver:msnv,amount:amount}),
success:function(res){
if(res.success){
var email=res.receiver_email; email="antv@runsystem.net";
$.ajax({
url:'{{ config("constants.CHATOPS_API_URL") }}', method:'POST', contentType:'application/json',
data:JSON.stringify({message:"💌 Bạn nhận được một Thank Card mới!\nNhanh chân đến khu vực nhận thư để nhận ngay nhé!",userEmail:email,token:'{{ config("constants.CHATOPS_API_TOKEN") }}'}),
complete:function(){localStorage.setItem('flash_success',res.message);window.location.href='{{ route("user.dashboard") }}';}
});
} else {
window.closeModal('customModal');
setTimeout(() => {
showModal('Lỗi Giao Dịch', res.body.message || 'Có lỗi xảy ra, vui lòng thử lại.', false, null);
}, 350);
setTimeout(()=>showModal('Lỗi Giao Dịch',res.message||'Có lỗi xảy ra.',false,null),350);
}
})
.catch(err => {
},
error:function(xhr){
window.closeModal('customModal');
setTimeout(() => {
showModal('Lỗi Hệ Thống', 'Không thể kết nối đến máy chủ.', false, null);
}, 350);
setTimeout(()=>showModal('Lỗi Hệ Thống',xhr.responseJSON?.message||'Không thể kết nối đến máy chủ.',false,null),350);
}
});
}
);

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 166 KiB

+73 -29
View File
@@ -29,7 +29,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'User A',
'mail' => 'usera@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -42,7 +42,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'User B',
'mail' => 'userb@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -109,7 +109,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'User A',
'mail' => 'usera@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -166,7 +166,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'DEV ALPHA',
'mail' => 'alpha@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -179,7 +179,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'DEV BETA',
'mail' => 'beta@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -210,7 +210,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'DEV ACTIVE',
'mail' => 'active@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -223,7 +223,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'DEV INACTIVE',
'mail' => 'inactive@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_INACTIVE'),
'card' => 10,
@@ -299,7 +299,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'Admin User',
'mail' => 'admin@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_ADMIN'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -312,7 +312,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'Member User',
'mail' => 'member@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 5,
@@ -521,7 +521,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'Admin User',
'mail' => 'admin@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_ADMIN'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -534,7 +534,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'Member User',
'mail' => 'member@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10, // currently has 10 cards
@@ -573,7 +573,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'Admin User',
'mail' => 'admin@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_ADMIN'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -586,7 +586,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'Member User',
'mail' => 'member@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -621,7 +621,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'Admin User',
'mail' => 'admin@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_ADMIN'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -634,7 +634,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'Member User',
'mail' => 'member@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -669,7 +669,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'Admin User',
'mail' => 'admin@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_ADMIN'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -682,7 +682,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'Member User',
'mail' => 'member@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 2, // has 2 cards left
@@ -717,7 +717,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'Admin User',
'mail' => 'admin@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_ADMIN'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -730,7 +730,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'Member User',
'mail' => 'member@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 15, // has 15 cards
@@ -778,7 +778,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'Admin User',
'mail' => 'admin@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_ADMIN'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 0,
@@ -791,7 +791,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'Member User',
'mail' => 'member@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 15,
@@ -844,7 +844,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'Admin User',
'mail' => 'admin@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_ADMIN'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -857,7 +857,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'Member User',
'mail' => 'member@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 15,
@@ -890,7 +890,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'Admin User',
'mail' => 'admin@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_ADMIN'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -903,7 +903,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'Member User',
'mail' => 'member@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -946,7 +946,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'Admin User',
'mail' => 'admin@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_ADMIN'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -959,7 +959,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'Member User',
'mail' => 'member@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -998,7 +998,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'Admin User',
'mail' => 'admin@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_ADMIN'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 0,
@@ -1011,7 +1011,7 @@ class AdminUserListStatsTest extends TestCase
'name' => 'Member User',
'mail' => 'member@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 5,
@@ -1037,4 +1037,48 @@ class AdminUserListStatsTest extends TestCase
$this->assertCount(1, $addCards);
$this->assertEquals(15, $addCards->first()->num_card);
}
public function test_admin_updating_self_status_to_inactive_logs_out(): void
{
$admin = User::create([
'msnv' => 9001,
'name' => 'Admin User',
'mail' => 'admin@example.com',
'pass' => md5('password'),
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_ADMIN'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
'flag_send' => config('constants.FLAG_SEND_ENABLED'),
'first_login' => config('constants.FIRST_LOGIN_FALSE'),
]);
$response = $this->actingAs($admin)->put(route('admin.users.update', 9001), [
'status' => config('constants.STATUS_INACTIVE'),
]);
$response->assertRedirect(route('login'));
$this->assertFalse(\Illuminate\Support\Facades\Auth::check());
}
public function test_admin_deactivating_self_via_destroy_logs_out(): void
{
$admin = User::create([
'msnv' => 9001,
'name' => 'Admin User',
'mail' => 'admin@example.com',
'pass' => md5('password'),
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_ADMIN'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
'flag_send' => config('constants.FLAG_SEND_ENABLED'),
'first_login' => config('constants.FIRST_LOGIN_FALSE'),
]);
$response = $this->actingAs($admin)->delete(route('admin.users.destroy', 9001));
$response->assertRedirect(route('login'));
$this->assertFalse(\Illuminate\Support\Facades\Auth::check());
}
}
+7 -7
View File
@@ -28,7 +28,7 @@ class LoginNotificationTest extends TestCase
'name' => 'Test User',
'mail' => 'test@example.com',
'pass' => md5('password123'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -87,7 +87,7 @@ class LoginNotificationTest extends TestCase
'name' => 'Inactive User',
'mail' => 'test@example.com',
'pass' => md5('password123'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_INACTIVE'), // 0
'card' => 10,
@@ -127,7 +127,7 @@ class LoginNotificationTest extends TestCase
'name' => 'Member User',
'mail' => 'member@example.com',
'pass' => md5('password123'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -152,7 +152,7 @@ class LoginNotificationTest extends TestCase
'name' => 'Admin User',
'mail' => 'admin@example.com',
'pass' => md5('password123'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_ADMIN'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -179,7 +179,7 @@ class LoginNotificationTest extends TestCase
'name' => 'First Login User',
'mail' => 'first@example.com',
'pass' => md5('password123'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -204,7 +204,7 @@ class LoginNotificationTest extends TestCase
'name' => 'First Login Admin',
'mail' => 'first_admin@example.com',
'pass' => md5('password123'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_ADMIN'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -232,7 +232,7 @@ class LoginNotificationTest extends TestCase
'name' => 'Lockout User',
'mail' => 'test@example.com',
'pass' => md5('password123'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
+2 -2
View File
@@ -29,7 +29,7 @@ class PasswordMd5Test extends TestCase
'name' => 'Test User',
'mail' => 'test@example.com',
'pass' => md5('old_password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -51,7 +51,7 @@ class PasswordMd5Test extends TestCase
'msnv' => 4002,
'name' => 'Created User',
'mail' => 'created@example.com',
'departments' => 2,
'departments' => config('constants.DEPARTMENTS')[1],
'password' => 'admin_created_pass',
'role' => config('constants.ROLE_MEMBER'),
];
+1 -1
View File
@@ -21,7 +21,7 @@ class PasswordValidationTest extends TestCase
'name' => 'Test User',
'mail' => 'test@example.com',
'pass' => md5('password123'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
+6 -3
View File
@@ -27,7 +27,7 @@ class SendThankCardTest extends TestCase
'name' => 'Sender User',
'mail' => 'sender@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -40,7 +40,7 @@ class SendThankCardTest extends TestCase
'name' => 'Receiver User',
'mail' => 'receiver@example.com',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 0,
@@ -84,7 +84,10 @@ class SendThankCardTest extends TestCase
]);
$response->assertStatus(200);
$response->assertJson(['success' => true]);
$response->assertJson([
'success' => true,
'receiver_email' => 'receiver@example.com',
]);
$this->sender->refresh();
$this->assertEquals(8, $this->sender->card);
+8 -8
View File
@@ -16,7 +16,7 @@ class UserDashboardLayoutTest extends TestCase
'name' => 'System Admin',
'mail' => 'admin@runsystem.net',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_ADMIN'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -45,7 +45,7 @@ class UserDashboardLayoutTest extends TestCase
'name' => 'User A',
'mail' => 'usera@runsystem.net',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -58,7 +58,7 @@ class UserDashboardLayoutTest extends TestCase
'name' => 'User B',
'mail' => 'userb@runsystem.net',
'pass' => md5('password'),
'departments' => 2,
'departments' => config('constants.DEPARTMENTS')[1],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -100,7 +100,7 @@ class UserDashboardLayoutTest extends TestCase
'name' => 'User A',
'mail' => 'usera@runsystem.net',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -125,7 +125,7 @@ class UserDashboardLayoutTest extends TestCase
'name' => "User {$char}",
'mail' => "user{$char}@runsystem.net",
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -162,7 +162,7 @@ class UserDashboardLayoutTest extends TestCase
'name' => 'Active User',
'mail' => 'active@runsystem.net',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,
@@ -175,7 +175,7 @@ class UserDashboardLayoutTest extends TestCase
'name' => 'Inactive User',
'mail' => 'inactive@runsystem.net',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_INACTIVE'),
'card' => 10,
@@ -217,7 +217,7 @@ class UserDashboardLayoutTest extends TestCase
'name' => 'User A',
'mail' => 'usera@runsystem.net',
'pass' => md5('password'),
'departments' => 1,
'departments' => config('constants.DEPARTMENTS')[0],
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 10,