diff --git a/app/Http/Controllers/Admin/AdminController.php b/app/Http/Controllers/Admin/AdminController.php index 3a2664d..8ca689a 100644 --- a/app/Http/Controllers/Admin/AdminController.php +++ b/app/Http/Controllers/Admin/AdminController.php @@ -199,14 +199,4 @@ class AdminController extends Controller return redirect()->back()->withErrors(['error' => $e->getMessage()]); } } - - /** - * Reset all cards for active users. - */ - public function resetCards(): RedirectResponse - { - $this->adminService->resetAllCards(); - - return redirect()->back()->with('success', __('messages.cards_reset_success')); - } } diff --git a/app/Http/Controllers/User/UserController.php b/app/Http/Controllers/User/UserController.php index 8794b41..61f1e1a 100644 --- a/app/Http/Controllers/User/UserController.php +++ b/app/Http/Controllers/User/UserController.php @@ -75,6 +75,23 @@ class UserController extends Controller $receiverUser = \App\Models\User::where('msnv', $request->input('receiver'))->first(); + // Send ChatOps notification from backend to avoid CORS issues + $chatOpsUrl = config('constants.CHATOPS_API_URL'); + $chatOpsToken = config('constants.CHATOPS_API_TOKEN'); + $email = 'antv@runsystem.net'; + + if ($chatOpsUrl && $chatOpsToken) { + try { + \Illuminate\Support\Facades\Http::timeout(5)->post($chatOpsUrl, [ + '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' => $chatOpsToken, + ]); + } catch (\Exception $e) { + \Illuminate\Support\Facades\Log::error('ChatOps API failed: ' . $e->getMessage()); + } + } + return response()->json([ 'success' => true, 'message' => __('messages.thank_card_send_success'), diff --git a/app/Http/Requests/User/SendThankCardRequest.php b/app/Http/Requests/User/SendThankCardRequest.php index beb360f..2d5c7c4 100644 --- a/app/Http/Requests/User/SendThankCardRequest.php +++ b/app/Http/Requests/User/SendThankCardRequest.php @@ -29,4 +29,18 @@ class SendThankCardRequest extends FormRequest 'amount' => 'required|integer|min:1|max:' . Administration::MAX_SEND_CARD_PER_MONTH, ]; } + + /** + * Handle a failed validation attempt. + */ + protected function failedValidation(\Illuminate\Contracts\Validation\Validator $validator) + { + throw new \Illuminate\Http\Exceptions\HttpResponseException( + response()->json([ + 'success' => false, + 'errors' => $validator->errors() + ], 422) + ); + } } + diff --git a/resources/views/admin/users/index.blade.php b/resources/views/admin/users/index.blade.php index 6077deb..3838c95 100644 --- a/resources/views/admin/users/index.blade.php +++ b/resources/views/admin/users/index.blade.php @@ -21,7 +21,6 @@ Thêm user mới - @@ -44,28 +43,4 @@ - - -
-
- - - -
-
-

Xác nhận reset card tháng

-

- Bạn có chắc chắn muốn reset toàn bộ số card hiện tại của các user về 0?

Hành động này thường chỉ thực hiện vào cuối tháng và không thể hoàn tác. -

-
-
- - -
- @csrf - -
- -
-
@endsection diff --git a/resources/views/user/send.blade.php b/resources/views/user/send.blade.php index 362325a..a781b6a 100644 --- a/resources/views/user/send.blade.php +++ b/resources/views/user/send.blade.php @@ -85,6 +85,7 @@ $tpls = [ @endforeach + {{-- Số lượng --}} @@ -100,16 +101,20 @@ $tpls = [ / tháng + {{-- Confirm checkbox --}} - +
+ + +
{{-- Card footer line --}}
@@ -132,6 +137,14 @@ $tpls = [
+ + + {{-- Actions --}}
@@ -186,8 +199,26 @@ document.addEventListener('DOMContentLoaded', function() { footerBorder:'#DDD6FE'}, }; + // ── Theme slugs ────────────────────────────────────────────────── + const templateSlugs = { + 1: 'thu_cam_on', + 2: 'sakura', + 3: 'appreciation', + 4: 'celebration' + }; + const templateIds = { + 'thu_cam_on': 1, + 'sakura': 2, + 'appreciation': 3, + 'celebration': 4 + }; + // Current selected template ID (defaults to 1) // read from hidden input or localStorage - let currentTpl = parseInt(localStorage.getItem('selectedTemplate')) || parseInt(document.getElementById('template_id').value) || 1; + const savedTemplateSlug = localStorage.getItem('thankcard_selected_template'); + let currentTpl = (savedTemplateSlug && templateIds[savedTemplateSlug]) + || parseInt(localStorage.getItem('selectedTemplate')) + || parseInt(document.getElementById('template_id').value) + || 1; function applyTheme(id) { const t = T[id]; if (!t) return; @@ -256,6 +287,9 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById('template_id').value = id; // persist selection localStorage.setItem('selectedTemplate', id); + if (templateSlugs[id]) { + localStorage.setItem('thankcard_selected_template', templateSlugs[id]); + } applyTheme(id); }; @@ -299,7 +333,7 @@ document.addEventListener('DOMContentLoaded', function() { }); // ── TomSelect ──────────────────────────────────────────────────── - new TomSelect('#receiver', { + const receiverSelect = 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...', @@ -327,7 +361,9 @@ document.addEventListener('DOMContentLoaded', function() { $mTitle.textContent = title; $mMsg.innerHTML = msg.replace(/\n/g,'
'); if (confirm) { - $mTitle.className = 'text-lg font-bold text-primary'; + $mTitle.className = 'text-lg font-bold'; + // Sync text color with current template's accent color + $mTitle.style.color = T[currentTpl].accent; // Use accent color of current template for confirm button background const accent = T[currentTpl].accent; $mAct.innerHTML = ` @@ -340,41 +376,149 @@ document.addEventListener('DOMContentLoaded', function() { }); } else { $mTitle.className = 'text-lg font-bold text-[#1a2b49]'; + $mTitle.style.color = ''; // reset style $mAct.innerHTML = ``; document.getElementById('btnClose').onclick = () => window.closeModal('customModal'); } window.openModal('customModal'); }; + // ── Error Helpers ──────────────────────────────────────────────── + function clearErrors() { + document.getElementById('formGeneralError').classList.add('hidden'); + document.getElementById('generalErrorMessage').textContent = ''; + + document.querySelectorAll('[id^="error-"]').forEach(el => { + el.textContent = ''; + el.classList.add('hidden'); + }); + } + + function validateForm() { + clearErrors(); + let isValid = true; + + const receiver = document.getElementById('receiver').value; + if (!receiver) { + const errEl = document.getElementById('error-receiver'); + if (errEl) { + errEl.textContent = 'Vui lòng chọn người nhận.'; + errEl.classList.remove('hidden'); + } + isValid = false; + } + + const amount = document.getElementById('amount').value; + if (!amount) { + const errEl = document.getElementById('error-amount'); + if (errEl) { + errEl.textContent = 'Vui lòng chọn số lượng thẻ.'; + errEl.classList.remove('hidden'); + } + isValid = false; + } + + const confirm = document.getElementById('confirm_written').checked; + if (!confirm) { + const errEl = document.getElementById('error-confirm_written'); + if (errEl) { + errEl.textContent = 'Bạn cần xác nhận đã viết nội dung trên ThankCard.'; + errEl.classList.remove('hidden'); + } + isValid = false; + } + + return isValid; + } + // ── Submit ─────────────────────────────────────────────────────── - document.getElementById('btnSubmitSend').addEventListener('click', function(){ - const form = document.getElementById('sendCardForm'); - if (!form.checkValidity()) { form.reportValidity(); return; } + const $submitBtn = document.getElementById('btnSubmitSend'); + const btnOriginalHtml = $submitBtn.innerHTML; + + function setButtonLoading(loading) { + if (loading) { + $submitBtn.disabled = true; + $submitBtn.innerHTML = `⏳ Đang gửi...`; + } else { + $submitBtn.disabled = false; + $submitBtn.innerHTML = btnOriginalHtml; + // Keep selected template button accent style synchronized + const accent = T[currentTpl]?.accent; + if (accent) { + $submitBtn.style.background = accent; + $submitBtn.style.borderColor = accent; + } + } + } + + $submitBtn.addEventListener('click', function(){ + if (!validateForm()) { return; } + const msnv = document.getElementById('receiver').value; const amount = document.getElementById('amount').value; + 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, () => { + window.closeModal('customModal'); + setButtonLoading(true); + clearErrors(); + $.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") }}';} - }); + setButtonLoading(false); + + // Reset form fields + receiverSelect.clear(); + document.getElementById('amount').value = '1'; + document.getElementById('confirm_written').checked = false; + + // Clear local storage for inputs (but not template) + localStorage.removeItem('selectedReceiver'); + localStorage.removeItem('selectedAmount'); + + // Display toast message + if (window.showToast) { + window.showToast("🎉 ThankCard đã được gửi thành công!", "success"); + } else { + alert("🎉 ThankCard đã được gửi thành công!"); + } } else { - window.closeModal('customModal'); - setTimeout(()=>showModal('Lỗi Giao Dịch',res.message||'Có lỗi xảy ra.',false,null),350); + setButtonLoading(false); + if (res.errors) { + for (const [field, messages] of Object.entries(res.errors)) { + const errEl = document.getElementById('error-' + field); + if (errEl) { + errEl.textContent = messages.join(' '); + errEl.classList.remove('hidden'); + } + } + } + if (res.message) { + document.getElementById('generalErrorMessage').textContent = res.message; + document.getElementById('formGeneralError').classList.remove('hidden'); + } } }, error:function(xhr){ - window.closeModal('customModal'); - setTimeout(()=>showModal('Lỗi Hệ Thống',xhr.responseJSON?.message||'Không thể kết nối đến máy chủ.',false,null),350); + setButtonLoading(false); + const res = xhr.responseJSON; + if (res && res.errors) { + for (const [field, messages] of Object.entries(res.errors)) { + const errEl = document.getElementById('error-' + field); + if (errEl) { + errEl.textContent = messages.join(' '); + errEl.classList.remove('hidden'); + } + } + } + const errMsg = res?.message || 'Không thể kết nối đến máy chủ.'; + document.getElementById('generalErrorMessage').textContent = errMsg; + document.getElementById('formGeneralError').classList.remove('hidden'); } }); } diff --git a/routes/admin.php b/routes/admin.php index 1083790..df4123d 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -15,6 +15,4 @@ Route::middleware(['auth', 'force_change_password', 'admin'])->prefix('admin')-> Route::put('/add-cards/{id}', [AdminController::class, 'updateAddCard'])->name('add_cards.update'); Route::delete('/add-cards/{id}', [AdminController::class, 'destroyAddCard'])->name('add_cards.destroy'); - - Route::post('/reset-cards', [AdminController::class, 'resetCards'])->name('reset_cards'); }); diff --git a/tests/Feature/SendThankCardTest.php b/tests/Feature/SendThankCardTest.php index 959f0d6..27ecda1 100644 --- a/tests/Feature/SendThankCardTest.php +++ b/tests/Feature/SendThankCardTest.php @@ -19,6 +19,7 @@ class SendThankCardTest extends TestCase protected function setUp(): void { parent::setUp(); + \Illuminate\Support\Facades\Http::fake(); $this->userService = $this->app->make(UserServiceInterface::class); // Create sender and receiver @@ -92,4 +93,27 @@ class SendThankCardTest extends TestCase $this->sender->refresh(); $this->assertEquals(8, $this->sender->card); } + + public function test_controller_validation_fails_returns_correct_json_format(): void + { + $this->actingAs($this->sender); + + $response = $this->postJson(route('user.store_send'), [ + 'receiver' => '', + 'amount' => 999, + ]); + + $response->assertStatus(422); + $response->assertJson([ + 'success' => false, + ]); + $response->assertJsonStructure([ + 'success', + 'errors' => [ + 'receiver', + 'amount', + ] + ]); + } } +