diff --git a/app/Http/Controllers/Admin/AdminController.php b/app/Http/Controllers/Admin/AdminController.php index d6d93f9..3b87a3b 100644 --- a/app/Http/Controllers/Admin/AdminController.php +++ b/app/Http/Controllers/Admin/AdminController.php @@ -146,6 +146,31 @@ class AdminController extends Controller } } + /** + * Delete card allocation history record. + */ + public function destroyAddCard(Request $request, int $id) + { + try { + $this->adminService->destroyAddCard($id); + if ($request->expectsJson()) { + return response()->json([ + 'success' => true, + 'message' => 'Xóa lịch sử cấp phát thẻ thành công.', + ]); + } + return redirect()->back()->with('success', 'Xóa lịch sử cấp phát thẻ thành công.'); + } catch (\Exception $e) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 422); + } + return redirect()->back()->withErrors(['error' => $e->getMessage()]); + } + } + /** * Reset all cards for active users. */ diff --git a/app/Http/Controllers/User/UserController.php b/app/Http/Controllers/User/UserController.php index 36dfd57..cc452f8 100644 --- a/app/Http/Controllers/User/UserController.php +++ b/app/Http/Controllers/User/UserController.php @@ -116,8 +116,8 @@ class UserController extends Controller $this->userService->updateProfile( $user, - $request->file('avatar'), - $request->filled('new_password') ? $request->input('new_password') : null + $request->validated(), + $request->file('avatar') ); return redirect()->route('user.edit')->with('success', 'Cập nhật hồ sơ thành công'); diff --git a/app/Http/Requests/Admin/UpdateUserRequest.php b/app/Http/Requests/Admin/UpdateUserRequest.php index 9e977a3..b46356c 100644 --- a/app/Http/Requests/Admin/UpdateUserRequest.php +++ b/app/Http/Requests/Admin/UpdateUserRequest.php @@ -29,6 +29,8 @@ class UpdateUserRequest extends FormRequest */ public function rules(): array { + $msnv = $this->route('msnv'); + return [ 'card' => 'nullable|integer|min:0', 'num_card' => 'nullable|integer|min:1', @@ -36,6 +38,12 @@ class UpdateUserRequest extends FormRequest 'status' => 'nullable|in:' . config('constants.STATUS_INACTIVE') . ',' . config('constants.STATUS_ACTIVE'), 'flag_send' => 'nullable|in:0,1', 'first_login' => 'nullable|in:' . config('constants.FIRST_LOGIN_FALSE') . ',' . config('constants.FIRST_LOGIN_TRUE'), + + // Editable basic info + 'name' => 'sometimes|required|string|max:255', + 'mail' => 'sometimes|required|email|unique:user,mail,' . $msnv . ',msnv', + 'departments' => 'sometimes|required|string|in:' . implode(',', config('constants.DEPARTMENTS')), + 'avatar' => 'nullable|image|max:2048', ]; } } diff --git a/app/Http/Requests/User/UpdateProfileRequest.php b/app/Http/Requests/User/UpdateProfileRequest.php index 23e9cce..141be50 100644 --- a/app/Http/Requests/User/UpdateProfileRequest.php +++ b/app/Http/Requests/User/UpdateProfileRequest.php @@ -20,8 +20,35 @@ class UpdateProfileRequest extends FormRequest public function rules(): array { return [ - 'avatar' => 'nullable|image|max:2048', // 2MB max - 'new_password' => 'nullable|min:6|confirmed', + 'name' => 'required|string|max:255', + 'avatar' => 'nullable|image|max:2048', // 2MB max + 'current_password' => [ + 'nullable', + 'required_with:new_password', + function ($attribute, $value, $fail) { + if ($value && md5($value) !== auth()->user()->pass) { + $fail('Mật khẩu hiện tại không chính xác.'); + } + }, + ], + 'new_password' => 'nullable|min:6|confirmed|different:current_password', + ]; + } + + /** + * Get the error messages for the defined validation rules. + */ + public function messages(): array + { + return [ + 'name.required' => 'Họ và tên không được để trống.', + 'name.string' => 'Họ và tên phải là chuỗi ký tự.', + 'name.max' => 'Họ và tên không được vượt quá 255 ký tự.', + 'avatar.image' => 'Ảnh đại diện phải là tệp ảnh.', + 'avatar.max' => 'Ảnh đại diện không được vượt quá 2MB.', + 'new_password.min' => 'Mật khẩu mới phải có ít nhất 6 ký tự.', + 'new_password.confirmed' => 'Mật khẩu xác nhận không trùng khớp.', + 'new_password.different' => 'Mật khẩu mới phải khác mật khẩu hiện tại.', ]; } } diff --git a/app/Services/Admin/AdminService.php b/app/Services/Admin/AdminService.php index c4715a4..c510739 100644 --- a/app/Services/Admin/AdminService.php +++ b/app/Services/Admin/AdminService.php @@ -16,6 +16,9 @@ use App\DTOs\UserFilterDto; class AdminService implements AdminServiceInterface { + public function __construct( + private readonly \App\Services\User\Contracts\UserServiceInterface $userService + ) {} public function getUserListWithStats( string|UserFilterDto $monthOrFilters, ?string $search = null, @@ -136,6 +139,19 @@ class AdminService implements AdminServiceInterface : User::where('msnv', $userOrMsnv)->firstOrFail(); DB::transaction(function () use ($data, $user) { + // Update profile fields (name & avatar) via UserService + $this->userService->updateProfile($user, [ + 'name' => $data['name'] ?? null, + ], request()->file('avatar')); + + if (isset($data['mail'])) { + $user->mail = $data['mail']; + } + + if (isset($data['departments'])) { + $user->departments = $data['departments']; + } + $newCard = $user->card; if (isset($data['num_card'])) { $newCard = $user->card + intval($data['num_card']); @@ -147,12 +163,21 @@ class AdminService implements AdminServiceInterface if ($newCard > $oldCard) { $diff = $newCard - $oldCard; - AddCard::create([ - 'buyer' => $user->msnv, - 'num_card' => $diff, - 'seller' => Auth::user()->msnv, - 'date' => Carbon::today(), - ]); + $todayRecord = AddCard::where('buyer', $user->msnv) + ->where('seller', Auth::user()->msnv) + ->whereDate('date', Carbon::today()) + ->first(); + if ($todayRecord) { + $todayRecord->num_card += $diff; + $todayRecord->save(); + } else { + AddCard::create([ + 'buyer' => $user->msnv, + 'num_card' => $diff, + 'seller' => Auth::user()->msnv, + 'date' => Carbon::today(), + ]); + } } $user->card = $newCard; @@ -173,14 +198,16 @@ class AdminService implements AdminServiceInterface public function resetAllCards(): void { - User::where('status', config('constants.STATUS_ACTIVE'))->update(['card' => 0]); + User::where('status', config('constants.STATUS_ACTIVE'))->update([ + 'card' => 0, + 'flag_send' => config('constants.FLAG_SEND_DISABLED'), + ]); } public function updateAddCard(int $id, array $data): void { $addCard = AddCard::findOrFail($id); - $currentMonth = Carbon::now()->format('Y-m'); $recordMonth = Carbon::parse($addCard->date)->format('Y-m'); @@ -195,15 +222,25 @@ class AdminService implements AdminServiceInterface $user = User::where('msnv', $addCard->buyer)->firstOrFail(); - DB::transaction(function () use ($addCard, $user, $data) { + $startOfMonth = Carbon::now()->startOfMonth(); + $endOfMonth = Carbon::now()->endOfMonth(); + $totalSentThisMonth = Administration::where('msnv', $user->msnv) + ->whereBetween('date', [$startOfMonth, $endOfMonth]) + ->sum('sent'); + + DB::transaction(function () use ($addCard, $user, $data, $totalSentThisMonth) { $oldNumCard = intval($addCard->num_card); $newNumCard = intval($data['num_card']); $diff = $newNumCard - $oldNumCard; - - if ($user->card + $diff < 0) { + $newCardBalance = $user->card + $diff; + if ($newCardBalance < 0) { throw new \Exception('Số lượng card cập nhật không hợp lệ vì tổng số card của user không được nhỏ hơn 0.'); } + if ($newCardBalance < $totalSentThisMonth) { + throw new \Exception('Số lượng card cập nhật không hợp lệ vì tổng số card của user không được nhỏ hơn số card đã gửi trong tháng (' . $totalSentThisMonth . ' card).'); + } + $user->card += $diff; $user->save(); @@ -214,6 +251,43 @@ class AdminService implements AdminServiceInterface }); } + public function destroyAddCard(int $id): void + { + $addCard = AddCard::findOrFail($id); + + $currentMonth = Carbon::now()->format('Y-m'); + $recordMonth = Carbon::parse($addCard->date)->format('Y-m'); + + if ($recordMonth !== $currentMonth) { + throw new \Exception('Không cho phép xóa dữ liệu card của các tháng trước.'); + } + + $user = User::where('msnv', $addCard->buyer)->firstOrFail(); + + $startOfMonth = Carbon::now()->startOfMonth(); + $endOfMonth = Carbon::now()->endOfMonth(); + $totalSentThisMonth = Administration::where('msnv', $user->msnv) + ->whereBetween('date', [$startOfMonth, $endOfMonth]) + ->sum('sent'); + + DB::transaction(function () use ($addCard, $user, $totalSentThisMonth) { + $newCardBalance = $user->card - intval($addCard->num_card); + + if ($newCardBalance < 0) { + throw new \Exception('Không thể xóa lịch sử cấp phát thẻ này vì tổng số card của user không được nhỏ hơn 0.'); + } + + if ($newCardBalance < $totalSentThisMonth) { + throw new \Exception('Không thể xóa lịch sử cấp phát thẻ này vì tổng số card của user không được nhỏ hơn số card đã gửi trong tháng (' . $totalSentThisMonth . ' card).'); + } + + $user->card = $newCardBalance; + $user->save(); + + $addCard->delete(); + }); + } + public function getUserByMsnv(string $msnv): User { return User::where('msnv', $msnv)->firstOrFail(); diff --git a/app/Services/Admin/Contracts/AdminServiceInterface.php b/app/Services/Admin/Contracts/AdminServiceInterface.php index 898bc86..bf7caff 100644 --- a/app/Services/Admin/Contracts/AdminServiceInterface.php +++ b/app/Services/Admin/Contracts/AdminServiceInterface.php @@ -27,6 +27,8 @@ interface AdminServiceInterface public function updateAddCard(int $id, array $data): void; + public function destroyAddCard(int $id): void; + public function getUserByMsnv(string $msnv): \App\Models\User; public function getAddCardsHistory(string $buyerMsnv, ?string $selectedMonth = null): \Illuminate\Database\Eloquent\Collection; diff --git a/app/Services/User/Contracts/UserServiceInterface.php b/app/Services/User/Contracts/UserServiceInterface.php index 499c91d..18c20ce 100644 --- a/app/Services/User/Contracts/UserServiceInterface.php +++ b/app/Services/User/Contracts/UserServiceInterface.php @@ -19,5 +19,5 @@ interface UserServiceInterface public function getRankingList(string $type, string $selectedMonth, User $currentUser): array; - public function updateProfile(User $user, ?\Illuminate\Http\UploadedFile $avatarFile, ?string $newPassword): void; + public function updateProfile(User $user, array $data, ?\Illuminate\Http\UploadedFile $avatarFile = null): void; } \ No newline at end of file diff --git a/app/Services/User/UserService.php b/app/Services/User/UserService.php index 8f43648..928e171 100644 --- a/app/Services/User/UserService.php +++ b/app/Services/User/UserService.php @@ -214,15 +214,19 @@ class UserService implements UserServiceInterface ]; } - public function updateProfile(User $user, ?\Illuminate\Http\UploadedFile $avatarFile, ?string $newPassword): void + public function updateProfile(User $user, array $data, ?\Illuminate\Http\UploadedFile $avatarFile = null): void { + if (isset($data['name'])) { + $user->name = $data['name']; + } + if ($avatarFile) { $path = $avatarFile->store('avatars', 'public'); $user->avatar = 'storage/' . $path; } - if ($newPassword) { - $user->pass = md5($newPassword); + if (!empty($data['new_password'])) { + $user->pass = md5($data['new_password']); $user->first_login = config('constants.FIRST_LOGIN_FALSE'); } diff --git a/docs/crob-job.md b/docs/crob-job.md new file mode 100644 index 0000000..4770c4f --- /dev/null +++ b/docs/crob-job.md @@ -0,0 +1,28 @@ +# Hướng dẫn thiết lập Cron Job cho Laravel trên Ubuntu Server + +Để hệ thống tự động chạy các tác vụ định kỳ (bao gồm tác vụ tự động reset thẻ vào cuối tháng qua lệnh `cards:reset`), bạn cần cấu hình Cron Job trên hệ điều hành Ubuntu Server trỏ đến bộ lập lịch (Task Scheduler) của Laravel. + +## Các bước thiết lập + +1. **Mở cấu hình Crontab:** + Truy cập vào terminal của máy chủ Ubuntu dưới quyền của user chạy web (thường là `www-data` hoặc user deploy của bạn) và chạy lệnh: + ```bash + crontab -e + ``` + +2. **Thêm cấu hình chạy Scheduler:** + Thêm dòng dưới đây vào cuối file cấu hình crontab để gọi Laravel scheduler kiểm tra tác vụ mỗi phút: + ```bash + * * * * * cd /đường-dẫn-đến-thư-mục-dự-án && php artisan schedule:run >> /dev/null 2>&1 + ``` + *Lưu ý:* Thay thế `/đường-dẫn-đến-thư-mục-dự-án` bằng đường dẫn tuyệt đối đến thư mục chứa dự án trên server (Ví dụ: `/var/www/thankcard-system`). + +3. **Lưu và Thoát:** + - Nếu bạn dùng trình soạn thảo `nano`: Nhấn `Ctrl + O` -> nhấn `Enter` để lưu, sau đó bấm `Ctrl + X` để thoát. + - Hệ thống sẽ hiển thị thông báo `crontab: installing new crontab`. + +4. **Kiểm tra trạng thái:** + Chạy lệnh sau để hiển thị danh sách các cron job đang hoạt động trên user đó: + ```bash + crontab -l + ``` diff --git a/resources/css/app.css b/resources/css/app.css index e7ba0f3..b3c85e3 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -51,6 +51,110 @@ @import 'tailwindcss'; @import 'tom-select/dist/css/tom-select.css'; +/* --- TOM SELECT CUSTOM DESIGN SYSTEM --- */ +.ts-wrapper.form-input { + padding: 0 !important; + border: none !important; + background: transparent !important; + height: 48px !important; + box-shadow: none !important; +} +.ts-wrapper.form-input .ts-control { + border: 1px solid #d9dfe7 !important; + border-radius: 0.75rem !important; /* 12px / rounded-xl */ + height: 48px !important; + padding: 0 1rem !important; + display: flex !important; + align-items: center !important; + font-size: 0.875rem !important; + color: #1e293b !important; + background-color: #ffffff !important; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05) !important; + transition: border-color 0.2s, box-shadow 0.2s !important; + position: relative !important; +} +.ts-wrapper.form-input:hover .ts-control { + border-color: #9ca3af !important; +} +.ts-wrapper.form-input.focus .ts-control { + border-color: #3462f7 !important; + box-shadow: 0 0 0 4px rgba(52, 98, 247, 0.2) !important; + outline: none !important; +} +.ts-wrapper.form-input.focus:hover .ts-control { + border-color: #3462f7 !important; +} +.ts-wrapper.form-input-with-icon .ts-control { + padding-left: 48px !important; +} +/* Selected value item styling */ +.ts-wrapper.form-input .ts-control .item { + white-space: nowrap !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + max-width: calc(100% - 24px) !important; + color: #1e293b !important; + font-size: 0.875rem !important; + padding: 0 !important; + margin: 0 !important; + line-height: normal !important; +} +/* Search input field styling */ +.ts-wrapper.form-input .ts-control input { + font-size: 0.875rem !important; + padding: 0 !important; + margin: 0 !important; + height: auto !important; + line-height: normal !important; + background: transparent !important; + border: none !important; + box-shadow: none !important; +} +/* Dropdown popup list styling */ +.ts-wrapper.form-input .ts-dropdown { + border: 1px solid #d9dfe7 !important; + border-radius: 0.75rem !important; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1) !important; + margin-top: 4px !important; + z-index: 100 !important; + background-color: #ffffff !important; + overflow: hidden !important; +} +.ts-wrapper.form-input .ts-dropdown .option { + padding: 0.65rem 1rem !important; + font-size: 0.875rem !important; + color: #475569 !important; + white-space: normal !important; + word-break: break-word !important; +} +.ts-wrapper.form-input .ts-dropdown .option.active { + background-color: #e8f0fe !important; + color: #3462f7 !important; +} +/* Select dropdown arrow icon */ +.ts-wrapper.form-input.single .ts-control::after { + right: 1.25rem !important; + border-color: #6b7280 transparent transparent transparent !important; + border-width: 5px 5px 0 5px !important; + top: 50% !important; + margin-top: -2.5px !important; +} +.ts-wrapper.form-input.single.dropdown-active .ts-control::after { + border-color: transparent transparent #6b7280 transparent !important; + border-width: 0 5px 5px 5px !important; + margin-top: -2.5px !important; +} +/* Specific height override for the search filter form */ +#filterForm .ts-wrapper.form-input { + height: 38px !important; +} +#filterForm .ts-wrapper.form-input .ts-control { + height: 38px !important; + min-height: 38px !important; + padding: 0.375rem 1rem !important; +} + + @source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; @source '../../storage/framework/views/*.php'; @source '../**/*.blade.php'; diff --git a/resources/views/components/action-panel.blade.php b/resources/views/components/action-panel.blade.php index a42dab8..bd3ac20 100644 --- a/resources/views/components/action-panel.blade.php +++ b/resources/views/components/action-panel.blade.php @@ -15,5 +15,6 @@ @elseif($mode === 'self') Quay lại + @endif diff --git a/resources/views/components/user-admin-section.blade.php b/resources/views/components/user-admin-section.blade.php new file mode 100644 index 0000000..9972d27 --- /dev/null +++ b/resources/views/components/user-admin-section.blade.php @@ -0,0 +1,55 @@ +@props(['mode', 'user' => null]) + +@if($mode === 'create' || $mode === 'edit') +
| Hành động | Ngày cấp | Người cấp (Admin) | Số lượng thẻ | -Hành động |
|---|---|---|---|---|
|
+ @if($isCurrentMonth)
+
+
+
+
+ @else
+ Tháng trước
+ @endif
+ |
{{ Carbon\Carbon::parse($addCard->date)->format('d/m/Y') }} | @if($addCard->sellerUser) @@ -106,13 +116,6 @@ @endif | +{{ $addCard->num_card }} | -- @if($isCurrentMonth) - - @else - Tháng trước - @endif - |
| Hành động | Ngày cấp | Người cấp (Admin) | Số lượng thẻ | -Hành động |
|---|---|---|---|---|
|
+ @if($isCurrentMonth)
+
+
+ @else
+ Tháng trước
+ @endif
+ |
{{ Carbon\Carbon::parse($addCard->date)->format('d/m/Y') }} | @if($addCard->sellerUser) @@ -195,13 +211,6 @@ @endif | +{{ $addCard->num_card }} | -
- @if($isCurrentMonth)
- |