select box
This commit is contained in:
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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';
|
||||
|
||||
@@ -15,5 +15,6 @@
|
||||
<button type="submit" class="inline-flex items-center justify-center bg-red-600 hover:bg-red-700 text-white font-bold py-2.5 px-6 rounded-xl text-sm transition-colors shadow-md shadow-red-500/10 min-w-[120px] h-[45px]">Nghỉ việc</button>
|
||||
@elseif($mode === 'self')
|
||||
<a href="{{ route('user.dashboard') }}" class="inline-flex items-center justify-center bg-white border border-[#d9dfe7] hover:bg-gray-50 text-[#1a2b49] font-bold py-2.5 px-6 rounded-xl text-sm transition-colors shadow-sm min-w-[120px] h-[45px]">Quay lại</a>
|
||||
<button type="submit" class="inline-flex items-center justify-center bg-[#3462f7] hover:bg-blue-700 text-white font-bold py-2.5 px-6 rounded-xl text-sm transition-colors shadow-md shadow-blue-500/10 min-w-[120px] h-[45px]">Lưu Thay Đổi</button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
@props(['mode', 'user' => null])
|
||||
|
||||
@if($mode === 'create' || $mode === 'edit')
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Quyền hạn -->
|
||||
<div class="relative">
|
||||
<label class="block text-[14px] font-bold text-[#1a2b49] mb-2">Quyền hạn <span class="text-red-500">*</span></label>
|
||||
<div class="grid grid-cols-2 gap-2 mt-2 bg-gray-100/80 p-1 rounded-xl">
|
||||
<label class="flex items-center justify-center gap-2 cursor-pointer py-2 rounded-lg transition-all text-center has-[:checked]:bg-white has-[:checked]:text-[#3462f7] has-[:checked]:shadow-sm text-gray-500 font-bold text-sm hover:bg-white/50">
|
||||
<input type="radio" name="role" value="{{ config('constants.ROLE_MEMBER') }}" class="sr-only" {{ old('role', $user?->role) == config('constants.ROLE_MEMBER') || (is_null($user) && old('role') != config('constants.ROLE_ADMIN')) ? 'checked' : '' }}>
|
||||
<span>Member</span>
|
||||
</label>
|
||||
<label class="flex items-center justify-center gap-2 cursor-pointer py-2 rounded-lg transition-all text-center has-[:checked]:bg-white has-[:checked]:text-[#3462f7] has-[:checked]:shadow-sm text-gray-500 font-bold text-sm hover:bg-white/50">
|
||||
<input type="radio" name="role" value="{{ config('constants.ROLE_ADMIN') }}" class="sr-only" {{ old('role', $user?->role) == config('constants.ROLE_ADMIN') ? 'checked' : '' }}>
|
||||
<span>Admin</span>
|
||||
</label>
|
||||
</div>
|
||||
@error('role')<span class="text-red-500 text-[13px] font-semibold mt-1.5 block">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
|
||||
@if($mode === 'edit')
|
||||
<!-- Trạng thái -->
|
||||
<div class="relative">
|
||||
<label class="block text-[14px] font-bold text-[#1a2b49] mb-2">Trạng thái <span class="text-red-500">*</span></label>
|
||||
<div class="grid grid-cols-2 gap-2 mt-2 bg-gray-100/80 p-1 rounded-xl">
|
||||
<label class="flex items-center justify-center gap-2 cursor-pointer py-2 rounded-lg transition-all text-center has-[:checked]:bg-white has-[:checked]:text-[#3462f7] has-[:checked]:shadow-sm text-gray-500 font-bold text-sm hover:bg-white/50">
|
||||
<input type="radio" name="status" value="{{ config('constants.STATUS_ACTIVE') }}" class="sr-only" {{ old('status', $user?->status) == config('constants.STATUS_ACTIVE') ? 'checked' : '' }}>
|
||||
<span>Đang làm việc</span>
|
||||
</label>
|
||||
<label class="flex items-center justify-center gap-2 cursor-pointer py-2 rounded-lg transition-all text-center has-[:checked]:bg-white has-[:checked]:text-[#3462f7] has-[:checked]:shadow-sm text-gray-500 font-bold text-sm hover:bg-white/50">
|
||||
<input type="radio" name="status" value="{{ config('constants.STATUS_INACTIVE') }}" class="sr-only" {{ old('status', $user?->status) == config('constants.STATUS_INACTIVE') ? 'checked' : '' }}>
|
||||
<span>Đã nghỉ việc</span>
|
||||
</label>
|
||||
</div>
|
||||
@error('status')<span class="text-red-500 text-[13px] font-semibold mt-1.5 block">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
|
||||
<!-- Quyền gửi thẻ -->
|
||||
<div class="relative col-span-1">
|
||||
<label class="block text-[14px] font-bold text-[#1a2b49] mb-2">Quyền gửi thẻ <span class="text-red-500">*</span></label>
|
||||
<div class="grid grid-cols-2 gap-2 mt-2 bg-gray-100/80 p-1 rounded-xl">
|
||||
<label class="flex items-center justify-center gap-2 cursor-pointer py-2 rounded-lg transition-all text-center has-[:checked]:bg-white has-[:checked]:text-[#3462f7] has-[:checked]:shadow-sm text-gray-500 font-bold text-sm hover:bg-white/50">
|
||||
<input type="radio" name="flag_send" value="{{ config('constants.FLAG_SEND_ENABLED') }}" class="sr-only" {{ old('flag_send', $user?->flag_send) == config('constants.FLAG_SEND_ENABLED') ? 'checked' : '' }}>
|
||||
<span>Được phép gửi</span>
|
||||
</label>
|
||||
<label class="flex items-center justify-center gap-2 cursor-pointer py-2 rounded-lg transition-all text-center has-[:checked]:bg-white has-[:checked]:text-[#3462f7] has-[:checked]:shadow-sm text-gray-500 font-bold text-sm hover:bg-white/50">
|
||||
<input type="radio" name="flag_send" value="{{ config('constants.FLAG_SEND_DISABLED') }}" class="sr-only" {{ old('flag_send', $user?->flag_send) == config('constants.FLAG_SEND_DISABLED') ? 'checked' : '' }}>
|
||||
<span>Không cho phép</span>
|
||||
</label>
|
||||
</div>
|
||||
@error('flag_send')<span class="text-red-500 text-[13px] font-semibold mt-1.5 block">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,29 @@
|
||||
@props(['mode', 'user' => null])
|
||||
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="w-16 h-16 rounded-full overflow-hidden border border-gray-200 shrink-0">
|
||||
<img src="{{ ($user && $user->avatar) ? asset($user->avatar) : asset('images/avatar.png') }}" alt="Avatar" class="w-full h-full object-cover" id="avatar-preview" onerror="this.onerror=null; this.src='{{ asset('images/avatar.png') }}';">
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<label class="block text-[13px] font-bold text-gray-700 mb-1" for="avatar">Thay đổi ảnh đại diện</label>
|
||||
<input type="file" name="avatar" id="avatar" accept="image/*" class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-3 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-[#3462f7] file:text-white hover:file:bg-[#254edb]" onchange="previewAvatar(event)">
|
||||
@error('avatar')<span class="text-red-500 text-[13px] font-semibold mt-1.5 block">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script>
|
||||
function previewAvatar(event) {
|
||||
const input = event.target;
|
||||
if (input.files && input.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
document.getElementById('avatar-preview').src = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
@@ -0,0 +1,95 @@
|
||||
@props(['mode', 'user' => null])
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<!-- MSNV -->
|
||||
<div class="relative">
|
||||
<label class="block text-[14px] font-bold text-[#1a2b49] mb-2" for="msnv">Mã nhân viên (MSNV) <span class="text-red-500">*</span></label>
|
||||
<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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.8" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 9h3.75M15 12h3.75M15 15h3.75M4.5 19.5h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5zm6-10.125a1.875 1.875 0 11-3.75 0 1.875 1.875 0 013.75 0zm1.294 6.336a6.721 6.721 0 01-3.17.789 6.721 6.721 0 01-3.168-.789 3.376 3.376 0 016.338 0z" />
|
||||
</svg>
|
||||
</span>
|
||||
<input type="number" name="msnv" id="msnv"
|
||||
class="form-input form-input-with-icon !rounded-xl !focus:border-[#3462f7] !focus:ring-[#3462f7]/20 {{ $mode !== 'create' ? '!bg-gray-50 !text-gray-500 cursor-not-allowed' : '' }}"
|
||||
style="padding-left: 48px;"
|
||||
{{ $mode !== 'create' ? 'readonly' : 'required' }}
|
||||
value="{{ old('msnv', $user?->msnv) }}"
|
||||
placeholder="VD: 1001">
|
||||
</div>
|
||||
@error('msnv')<span class="text-red-500 text-[13px] font-semibold mt-1.5 block">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
|
||||
<!-- Name -->
|
||||
<div class="relative">
|
||||
<label class="block text-[14px] font-bold text-[#1a2b49] mb-2" for="name">Họ và tên <span class="text-red-500">*</span></label>
|
||||
<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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.8" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" name="name" id="name"
|
||||
class="form-input form-input-with-icon !rounded-xl !focus:border-[#3462f7] !focus:ring-[#3462f7]/20"
|
||||
style="padding-left: 48px;"
|
||||
required
|
||||
value="{{ old('name', $user?->name) }}"
|
||||
placeholder="VD: Nguyễn Văn A">
|
||||
</div>
|
||||
@error('name')<span class="text-red-500 text-[13px] font-semibold mt-1.5 block">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="relative">
|
||||
<label class="block text-[14px] font-bold text-[#1a2b49] mb-2" for="mail">Email <span class="text-red-500">*</span></label>
|
||||
<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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.8" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
|
||||
</svg>
|
||||
</span>
|
||||
<input type="email" name="mail" id="mail"
|
||||
class="form-input form-input-with-icon !rounded-xl !focus:border-[#3462f7] !focus:ring-[#3462f7]/20 {{ $mode === 'self' ? '!bg-gray-50 !text-gray-500 cursor-not-allowed' : '' }}"
|
||||
style="padding-left: 48px;"
|
||||
{{ $mode === 'self' ? 'readonly' : 'required' }}
|
||||
value="{{ old('mail', $user?->mail) }}"
|
||||
placeholder="VD: nguyenvana@runsystem.net">
|
||||
</div>
|
||||
@error('mail')<span class="text-red-500 text-[13px] font-semibold mt-1.5 block">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
@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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.8" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 14.15v4.25c0 .621-.504 1.125-1.125 1.125H4.875c-.621 0-1.125-.504-1.125-1.125v-4.25m16.5 0a2.18 2.18 0 0 0 .75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 0 0-3.413-.387m4.5 8.006c-.194.165-.453.254-.718.254H4.875a1.03 1.03 0 0 1-.718-.254m16.5 0c-.417-.38-.953-.59-1.517-.59H4.875A2.25 2.25 0 0 0 2.25 14.15v-4.25c0-1.081.768-2.015 1.837-2.175a48.067 48.067 0 0 1 16.076 0c1.069.16 1.837 1.094 1.837 2.175v4.25Z" />
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text"
|
||||
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 }}">
|
||||
</div>
|
||||
@else
|
||||
<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 z-10">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.8" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 14.15v4.25c0 .621-.504 1.125-1.125 1.125H4.875c-.621 0-1.125-.504-1.125-1.125v-4.25m16.5 0a2.18 2.18 0 0 0 .75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 0 0-3.413-.387m4.5 8.006c-.194.165-.453.254-.718.254H4.875a1.03 1.03 0 0 1-.718-.254m16.5 0c-.417-.38-.953-.59-1.517-.59H4.875A2.25 2.25 0 0 0 2.25 14.15v-4.25c0-1.081.768-2.015 1.837-2.175a48.067 48.067 0 0 1 16.076 0c1.069.16 1.837 1.094 1.837 2.175v4.25Z" />
|
||||
</svg>
|
||||
</span>
|
||||
<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>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@endif
|
||||
@error('departments')<span class="text-red-500 text-[13px] font-semibold mt-1.5 block">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,8 +68,6 @@
|
||||
width: 220px !important;
|
||||
}
|
||||
}
|
||||
.ts-control { border-radius: 0.75rem; border-color: #d9dfe7; padding: 0.45rem 1rem; }
|
||||
.ts-wrapper.form-input { padding: 0; border: none; }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@@ -84,6 +82,9 @@
|
||||
},
|
||||
placeholder: "🔍 Tìm phòng ban...",
|
||||
render: {
|
||||
item: function(data, escape) {
|
||||
return '<div class="item" title="' + escape(data.text) + '">' + escape(data.text) + '</div>';
|
||||
},
|
||||
no_results: function(data, escape) {
|
||||
return '<div class="no-results p-3 text-gray-500">Không tìm thấy phòng ban nào</div>';
|
||||
}
|
||||
|
||||
@@ -22,18 +22,19 @@
|
||||
@endphp
|
||||
|
||||
<div class="bg-white rounded-[24px] shadow-[0_10px_30px_rgba(15,23,42,0.05)] border border-white overflow-hidden flex flex-col justify-between">
|
||||
<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',
|
||||
'edit' => '⚙️ Quản Lý Thẻ: ' . ($user?->msnv),
|
||||
'delete' => '⚠️ Xác nhận nghỉ việc: ' . ($user?->msnv),
|
||||
'self' => '👤 Thông tin cá nhân: ' . ($user?->msnv),
|
||||
} }}</span>
|
||||
</h3>
|
||||
</div>
|
||||
@if($mode !== 'edit')
|
||||
<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),
|
||||
} }}</span>
|
||||
</h3>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(!$isCreate && $user && !$isEdit)
|
||||
@if(!$isCreate && $user && !$isEdit && !$isSelf)
|
||||
<div class="p-6 sm:p-8 pb-0">
|
||||
<div class="mb-2 p-5 rounded-2xl bg-gradient-to-br from-slate-50 to-gray-50 border border-gray-100 flex items-center gap-4 shadow-sm">
|
||||
<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">
|
||||
@@ -55,168 +56,32 @@
|
||||
@endif
|
||||
|
||||
@if($actionUrl)
|
||||
<form action="{{ $actionUrl }}" method="POST" class="flex flex-col flex-1 justify-between">
|
||||
<form action="{{ $actionUrl }}" method="POST" enctype="multipart/form-data" class="flex flex-col flex-1 justify-between">
|
||||
@csrf
|
||||
@if($method !== 'POST')
|
||||
@method($method)
|
||||
@endif
|
||||
@endif
|
||||
|
||||
<div class="p-6 sm:p-8 {{ !$isCreate && !$isEdit ? 'pt-0' : '' }} space-y-5">
|
||||
@if($isCreate)
|
||||
<div class="relative">
|
||||
<label class="block text-[14px] font-bold text-[#1a2b49] mb-2" for="msnv">Mã nhân viên (MSNV) <span class="text-red-500">*</span></label>
|
||||
<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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.8" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 9h3.75M15 12h3.75M15 15h3.75M4.5 19.5h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5zm6-10.125a1.875 1.875 0 11-3.75 0 1.875 1.875 0 013.75 0zm1.294 6.336a6.721 6.721 0 01-3.17.789 6.721 6.721 0 01-3.168-.789 3.376 3.376 0 016.338 0z" />
|
||||
</svg>
|
||||
</span>
|
||||
<input type="number" name="msnv" id="msnv" class="form-input form-input-with-icon !rounded-xl !focus:border-[#3462f7] !focus:ring-[#3462f7]/20" style="padding-left: 48px;" required value="{{ old('msnv') }}" placeholder="VD: 1001">
|
||||
</div>
|
||||
@error('msnv')<span class="text-red-500 text-[13px] font-semibold mt-1.5 block">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<label class="block text-[14px] font-bold text-[#1a2b49] mb-2" for="name">Họ và tên <span class="text-red-500">*</span></label>
|
||||
<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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.8" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" name="name" id="name" class="form-input form-input-with-icon !rounded-xl !focus:border-[#3462f7] !focus:ring-[#3462f7]/20" style="padding-left: 48px;" required value="{{ old('name') }}" placeholder="VD: Nguyễn Văn A">
|
||||
</div>
|
||||
@error('name')<span class="text-red-500 text-[13px] font-semibold mt-1.5 block">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<label class="block text-[14px] font-bold text-[#1a2b49] mb-2" for="mail">Email <span class="text-red-500">*</span></label>
|
||||
<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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.8" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
|
||||
</svg>
|
||||
</span>
|
||||
<input type="email" name="mail" id="mail" class="form-input form-input-with-icon !rounded-xl !focus:border-[#3462f7] !focus:ring-[#3462f7]/20" style="padding-left: 48px;" required value="{{ old('mail') }}" placeholder="VD: nguyenvana@runsystem.net">
|
||||
</div>
|
||||
@error('mail')<span class="text-red-500 text-[13px] font-semibold mt-1.5 block">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<select name="departments" id="departments" class="form-input !rounded-xl !focus:border-[#3462f7] !focus:ring-[#3462f7]/20" required>
|
||||
<option value="">-- Chọn phòng ban --</option>
|
||||
@foreach(config('constants.DEPARTMENTS') as $dept)
|
||||
<option value="{{ $dept }}" {{ old('departments') == $dept ? 'selected' : '' }}>{{ $dept }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('departments')<span class="text-red-500 text-[13px] font-semibold mt-1.5 block">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<label class="block text-[14px] font-bold text-[#1a2b49] mb-2" for="password">Mật khẩu khởi tạo <span class="text-red-500">*</span></label>
|
||||
<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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.8" stroke="currentColor" class="w-5 h-5">
|
||||
<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">
|
||||
</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
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<label class="block text-[14px] font-bold text-[#1a2b49] mb-2" for="role">Quyền hạn <span class="text-red-500">*</span></label>
|
||||
<select name="role" id="role" class="form-input !rounded-xl !focus:border-[#3462f7] !focus:ring-[#3462f7]/20" required>
|
||||
<option value="{{ config('constants.ROLE_MEMBER') }}" {{ old('role') == config('constants.ROLE_MEMBER') ? 'selected' : '' }}>Member</option>
|
||||
<option value="{{ config('constants.ROLE_ADMIN') }}" {{ old('role') == config('constants.ROLE_ADMIN') ? 'selected' : '' }}>Admin</option>
|
||||
</select>
|
||||
@error('role')<span class="text-red-500 text-[13px] font-semibold mt-1.5 block">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($isEdit && $user)
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="relative">
|
||||
<label class="block text-[14px] font-bold text-[#1a2b49] mb-2" for="mail">Email (Chỉ xem)</label>
|
||||
<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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.8" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
|
||||
</svg>
|
||||
</span>
|
||||
<input type="email" name="mail" id="mail" 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 value="{{ $user->mail }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<label class="block text-[14px] font-bold text-[#1a2b49] mb-2" for="password">Mật khẩu (Chỉ xem)</label>
|
||||
<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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.8" stroke="currentColor" class="w-5 h-5">
|
||||
<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="password" name="password" id="password" 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 value="********">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<label class="block text-[14px] font-bold text-[#1a2b49] mb-2">Quyền hạn</label>
|
||||
<div class="grid grid-cols-2 gap-2 mt-2 bg-gray-100/80 p-1 rounded-xl">
|
||||
<label class="flex items-center justify-center gap-2 cursor-pointer py-2 rounded-lg transition-all text-center has-[:checked]:bg-white has-[:checked]:text-[#3462f7] has-[:checked]:shadow-sm text-gray-500 font-bold text-sm hover:bg-white/50">
|
||||
<input type="radio" name="role" value="{{ config('constants.ROLE_MEMBER') }}" class="sr-only" {{ old('role', $user->role) == config('constants.ROLE_MEMBER') ? 'checked' : '' }}>
|
||||
<span>Member</span>
|
||||
</label>
|
||||
<label class="flex items-center justify-center gap-2 cursor-pointer py-2 rounded-lg transition-all text-center has-[:checked]:bg-white has-[:checked]:text-[#3462f7] has-[:checked]:shadow-sm text-gray-500 font-bold text-sm hover:bg-white/50">
|
||||
<input type="radio" name="role" value="{{ config('constants.ROLE_ADMIN') }}" class="sr-only" {{ old('role', $user->role) == config('constants.ROLE_ADMIN') ? 'checked' : '' }}>
|
||||
<span>Admin</span>
|
||||
</label>
|
||||
</div>
|
||||
@error('role')<span class="text-red-500 text-[13px] font-semibold mt-1.5 block">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<label class="block text-[14px] font-bold text-[#1a2b49] mb-2">Trạng thái</label>
|
||||
<div class="grid grid-cols-2 gap-2 mt-2 bg-gray-100/80 p-1 rounded-xl">
|
||||
<label class="flex items-center justify-center gap-2 cursor-pointer py-2 rounded-lg transition-all text-center has-[:checked]:bg-white has-[:checked]:text-[#3462f7] has-[:checked]:shadow-sm text-gray-500 font-bold text-sm hover:bg-white/50">
|
||||
<input type="radio" name="status" value="{{ config('constants.STATUS_ACTIVE') }}" class="sr-only" {{ old('status', $user->status) == config('constants.STATUS_ACTIVE') ? 'checked' : '' }}>
|
||||
<span>Đang làm việc</span>
|
||||
</label>
|
||||
<label class="flex items-center justify-center gap-2 cursor-pointer py-2 rounded-lg transition-all text-center has-[:checked]:bg-white has-[:checked]:text-[#3462f7] has-[:checked]:shadow-sm text-gray-500 font-bold text-sm hover:bg-white/50">
|
||||
<input type="radio" name="status" value="{{ config('constants.STATUS_INACTIVE') }}" class="sr-only" {{ old('status', $user->status) == config('constants.STATUS_INACTIVE') ? 'checked' : '' }}>
|
||||
<span>Đã nghỉ việc</span>
|
||||
</label>
|
||||
</div>
|
||||
@error('status')<span class="text-red-500 text-[13px] font-semibold mt-1.5 block">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<label class="block text-[14px] font-bold text-[#1a2b49] mb-2">Quyền gửi thẻ</label>
|
||||
<div class="grid grid-cols-2 gap-2 mt-2 bg-gray-100/80 p-1 rounded-xl">
|
||||
<label class="flex items-center justify-center gap-2 cursor-pointer py-2 rounded-lg transition-all text-center has-[:checked]:bg-white has-[:checked]:text-[#3462f7] has-[:checked]:shadow-sm text-gray-500 font-bold text-sm hover:bg-white/50">
|
||||
<input type="radio" name="flag_send" value="{{ config('constants.FLAG_SEND_ENABLED') }}" class="sr-only" {{ old('flag_send', $user->flag_send) == config('constants.FLAG_SEND_ENABLED') ? 'checked' : '' }}>
|
||||
<span>Được phép gửi</span>
|
||||
</label>
|
||||
<label class="flex items-center justify-center gap-2 cursor-pointer py-2 rounded-lg transition-all text-center has-[:checked]:bg-white has-[:checked]:text-[#3462f7] has-[:checked]:shadow-sm text-gray-500 font-bold text-sm hover:bg-white/50">
|
||||
<input type="radio" name="flag_send" value="{{ config('constants.FLAG_SEND_DISABLED') }}" class="sr-only" {{ old('flag_send', $user->flag_send) == config('constants.FLAG_SEND_DISABLED') ? 'checked' : '' }}>
|
||||
<span>Không cho phép</span>
|
||||
</label>
|
||||
</div>
|
||||
@error('flag_send')<span class="text-red-500 text-[13px] font-semibold mt-1.5 block">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
|
||||
<div class="hidden md:block"></div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<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 có 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.
|
||||
</p>
|
||||
@else
|
||||
<!-- 1. User Avatar Section -->
|
||||
@if($isSelf || $isEdit)
|
||||
<x-user-avatar-section :mode="$mode" :user="$user" />
|
||||
@endif
|
||||
|
||||
<!-- 2. User Basic Information Section -->
|
||||
<x-user-basic-info :mode="$mode" :user="$user" />
|
||||
|
||||
<!-- 3. User Security Section -->
|
||||
<x-user-security-section :mode="$mode" :user="$user" />
|
||||
|
||||
<!-- 4. User Administration Fields Section -->
|
||||
<x-user-admin-section :mode="$mode" :user="$user" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -227,30 +92,28 @@
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($isCreate)
|
||||
@push('styles')
|
||||
<style>
|
||||
.ts-control { border-radius: 0.75rem; border-color: #d9dfe7; padding: 0.65rem 1rem; }
|
||||
.ts-wrapper.form-input { padding: 0; border: none; }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@if($isCreate || $isEdit)
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
new TomSelect("#departments", {
|
||||
create: false,
|
||||
sortField: {
|
||||
field: "text",
|
||||
direction: "asc"
|
||||
},
|
||||
placeholder: "🔍 Tìm phòng ban...",
|
||||
render: {
|
||||
no_results: function(data, escape) {
|
||||
return '<div class="no-results p-3 text-gray-500">Không tìm thấy phòng ban nào</div>';
|
||||
if (document.getElementById('departments') && typeof TomSelect === 'function') {
|
||||
new TomSelect("#departments", {
|
||||
create: false,
|
||||
sortField: {
|
||||
field: "text",
|
||||
direction: "asc"
|
||||
},
|
||||
placeholder: "Tìm phòng ban...",
|
||||
render: {
|
||||
item: function(data, escape) {
|
||||
return '<div class="item" title="' + escape(data.text) + '">' + escape(data.text) + '</div>';
|
||||
},
|
||||
no_results: function(data, escape) {
|
||||
return '<div class="no-results p-3 text-gray-500">Không tìm thấy phòng ban nào</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@@ -44,11 +44,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 bg-slate-50 border border-slate-100 rounded-2xl p-4 relative z-10 min-w-[200px] shadow-sm">
|
||||
<div class="flex items-center gap-4 bg-slate-50 border border-slate-100 rounded-2xl p-4 relative z-10 min-w-[220px] shadow-sm">
|
||||
<div class="w-11 h-11 bg-white rounded-xl shadow-sm flex items-center justify-center text-xl">
|
||||
🎫
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex-1">
|
||||
<span class="text-[10px] font-bold text-gray-500 uppercase tracking-wider block">Số thẻ hiện có</span>
|
||||
<span id="user-card-balance" class="text-xl font-black text-[#1a2b49]">{{ $user->card }}</span>
|
||||
</div>
|
||||
@@ -82,10 +82,10 @@
|
||||
<table class="min-w-full divide-y divide-gray-100 text-sm">
|
||||
<thead class="bg-gray-50/50 sticky top-0 z-10">
|
||||
<tr>
|
||||
<th class="px-6 py-4 text-center text-xs font-bold text-text-light uppercase tracking-wider w-24">Hành động</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-bold text-text-light uppercase tracking-wider">Ngày cấp</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-bold text-text-light uppercase tracking-wider">Người cấp (Admin)</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-bold text-text-light uppercase tracking-wider">Số lượng thẻ</th>
|
||||
<th class="px-6 py-4 text-center text-xs font-bold text-text-light uppercase tracking-wider w-24">Hành động</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-100">
|
||||
@@ -94,6 +94,16 @@
|
||||
$isCurrentMonth = Carbon\Carbon::parse($addCard->date)->format('Y-m') === Carbon\Carbon::now()->format('Y-m');
|
||||
@endphp
|
||||
<tr class="hover:bg-slate-50/60 transition-colors">
|
||||
<td class="px-6 py-3.5 whitespace-nowrap text-center">
|
||||
@if($isCurrentMonth)
|
||||
<div class="flex items-center justify-center gap-3">
|
||||
<button type="button" onclick="openEditAddCardModal({{ json_encode($addCard) }})" class="text-blue-600 hover:text-blue-900 font-bold transition-colors">Sửa</button>
|
||||
<button type="button" onclick="confirmDeleteAddCard({{ $addCard->id }})" class="text-red-600 hover:text-red-900 font-bold transition-colors">Xóa</button>
|
||||
</div>
|
||||
@else
|
||||
<span class="text-gray-400 font-semibold text-xs cursor-not-allowed" title="Không thể chỉnh sửa giao dịch tháng trước">Tháng trước</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-3.5 whitespace-nowrap text-text-medium font-medium">{{ Carbon\Carbon::parse($addCard->date)->format('d/m/Y') }}</td>
|
||||
<td class="px-6 py-3.5 whitespace-nowrap font-bold text-[#1a2b49]">
|
||||
@if($addCard->sellerUser)
|
||||
@@ -106,13 +116,6 @@
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-3.5 whitespace-nowrap font-extrabold text-base text-green-600">+{{ $addCard->num_card }}</td>
|
||||
<td class="px-6 py-3.5 whitespace-nowrap text-center">
|
||||
@if($isCurrentMonth)
|
||||
<button type="button" onclick="openEditAddCardModal({{ json_encode($addCard) }})" class="text-blue-600 hover:text-blue-900 font-bold transition-colors">Sửa</button>
|
||||
@else
|
||||
<span class="text-gray-400 font-semibold text-xs cursor-not-allowed" title="Không thể chỉnh sửa giao dịch tháng trước">Tháng trước</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
@@ -124,9 +127,12 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@php
|
||||
$isCurrentMonthFilter = $selectedMonth === Carbon\Carbon::now()->format('Y-m');
|
||||
@endphp
|
||||
<div class="px-6 py-4 bg-slate-50 border-t border-gray-100 flex justify-end">
|
||||
<button type="button" onclick="openAddCardModal()" class="inline-flex items-center gap-2 bg-[#3462f7] hover:bg-blue-700 text-white font-bold text-sm px-5 py-2.5 rounded-xl shadow-md shadow-blue-500/10 transition-all">
|
||||
➕ Thêm thẻ
|
||||
<button type="button" onclick="openAddCardModal()" {{ !$isCurrentMonthFilter ? 'disabled' : '' }} class="inline-flex items-center justify-center bg-[#3462f7] hover:bg-blue-700 disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed disabled:shadow-none text-white font-bold text-sm px-5 py-2.5 rounded-xl shadow-md shadow-blue-500/10 transition-all">
|
||||
Thêm thẻ
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -171,10 +177,10 @@
|
||||
<table class="min-w-full divide-y divide-gray-100 text-sm">
|
||||
<thead class="bg-gray-50/50 sticky top-0">
|
||||
<tr>
|
||||
<th class="px-6 py-4 text-center text-xs font-bold text-text-light uppercase tracking-wider w-24">Hành động</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-bold text-text-light uppercase tracking-wider">Ngày cấp</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-bold text-text-light uppercase tracking-wider">Người cấp (Admin)</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-bold text-text-light uppercase tracking-wider">Số lượng thẻ</th>
|
||||
<th class="px-6 py-4 text-center text-xs font-bold text-text-light uppercase tracking-wider w-24">Hành động</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-100">
|
||||
@@ -183,6 +189,16 @@
|
||||
$isCurrentMonth = Carbon\Carbon::parse($addCard->date)->format('Y-m') === Carbon\Carbon::now()->format('Y-m');
|
||||
@endphp
|
||||
<tr class="hover:bg-slate-50/60 transition-colors">
|
||||
<td class="px-6 py-3.5 whitespace-nowrap text-center">
|
||||
@if($isCurrentMonth)
|
||||
<div class="flex items-center justify-center gap-3">
|
||||
<button type="button" onclick="openEditAddCardModal({{ json_encode($addCard) }})" class="text-blue-600 hover:text-blue-900 font-bold transition-colors">Sửa</button>
|
||||
<button type="button" onclick="confirmDeleteAddCard({{ $addCard->id }})" class="text-red-600 hover:text-red-900 font-bold transition-colors">Xóa</button>
|
||||
</div>
|
||||
@else
|
||||
<span class="text-gray-400 font-semibold text-xs cursor-not-allowed" title="Không thể chỉnh sửa giao dịch tháng trước">Tháng trước</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-3.5 whitespace-nowrap text-text-medium font-medium">{{ Carbon\Carbon::parse($addCard->date)->format('d/m/Y') }}</td>
|
||||
<td class="px-6 py-3.5 whitespace-nowrap font-bold text-[#1a2b49]">
|
||||
@if($addCard->sellerUser)
|
||||
@@ -195,13 +211,6 @@
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-3.5 whitespace-nowrap font-extrabold text-base text-green-600">+{{ $addCard->num_card }}</td>
|
||||
<td class="px-6 py-3.5 whitespace-nowrap text-center">
|
||||
@if($isCurrentMonth)
|
||||
<button type="button" onclick="openEditAddCardModal({{ json_encode($addCard) }})" class="text-blue-600 hover:text-blue-900 font-bold transition-colors">Sửa</button>
|
||||
@else
|
||||
<span class="text-gray-400 font-semibold text-xs cursor-not-allowed" title="Không thể chỉnh sửa giao dịch tháng trước">Tháng trước</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
@@ -213,9 +222,12 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@php
|
||||
$isCurrentMonthFilter = $selectedMonth === Carbon\Carbon::now()->format('Y-m');
|
||||
@endphp
|
||||
<div class="px-6 py-4 bg-slate-50 border-t border-gray-100 flex justify-end">
|
||||
<button type="button" onclick="openAddCardModal()" class="inline-flex items-center gap-2 bg-[#3462f7] hover:bg-blue-700 text-white font-bold text-sm px-5 py-2.5 rounded-xl shadow-md shadow-blue-500/10 transition-all">
|
||||
➕ Thêm thẻ
|
||||
<button type="button" onclick="openAddCardModal()" {{ !$isCurrentMonthFilter ? 'disabled' : '' }} class="inline-flex items-center justify-center bg-[#3462f7] hover:bg-blue-700 disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed disabled:shadow-none text-white font-bold text-sm px-5 py-2.5 rounded-xl shadow-md shadow-blue-500/10 transition-all">
|
||||
Thêm thẻ
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -275,6 +287,8 @@
|
||||
</form>
|
||||
</x-modal>
|
||||
|
||||
<x-confirm-modal id="deleteAddCardModal" title="Xác nhận xóa giao dịch" confirmText="Xác nhận xóa" />
|
||||
|
||||
<script>
|
||||
function openEditAddCardModal(record) {
|
||||
const form = document.getElementById('editAddCardForm');
|
||||
@@ -340,6 +354,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDeleteAddCard(id) {
|
||||
const messageEl = document.getElementById('deleteAddCardModal-message');
|
||||
if (messageEl) {
|
||||
messageEl.innerHTML = 'Bạn có chắc chắn muốn xóa giao dịch cấp phát thẻ này không? Thao tác này sẽ trừ số thẻ tương ứng của user.';
|
||||
}
|
||||
|
||||
const formEl = document.getElementById('deleteAddCardModal-form');
|
||||
if (formEl) {
|
||||
formEl.action = `/admin/add-cards/${id}`;
|
||||
}
|
||||
|
||||
openModal('deleteAddCardModal');
|
||||
}
|
||||
|
||||
async function reloadListsSection() {
|
||||
try {
|
||||
const response = await fetch(window.location.href, {
|
||||
@@ -379,6 +407,11 @@
|
||||
if (editForm) {
|
||||
editForm.addEventListener('submit', (e) => submitModalForm(e, 'editAddCardForm', 'editAddCardModal'));
|
||||
}
|
||||
|
||||
const deleteForm = document.getElementById('deleteAddCardModal-form');
|
||||
if (deleteForm) {
|
||||
deleteForm.addEventListener('submit', (e) => submitModalForm(e, 'deleteAddCardModal-form', 'deleteAddCardModal'));
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState !== 'loading') {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
@props(['mode', 'user' => null])
|
||||
|
||||
@if($mode === 'create')
|
||||
<div class="relative">
|
||||
<label class="block text-[14px] font-bold text-[#1a2b49] mb-2" for="password">Mật khẩu khởi tạo <span class="text-red-500">*</span></label>
|
||||
<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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.8" stroke="currentColor" class="w-5 h-5">
|
||||
<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">
|
||||
</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>
|
||||
</div>
|
||||
@elseif($mode === 'edit')
|
||||
<div class="relative">
|
||||
<label class="block text-[14px] font-bold text-[#1a2b49] mb-2" for="password">Mật khẩu (Chỉ xem)</label>
|
||||
<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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.8" stroke="currentColor" class="w-5 h-5">
|
||||
<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="password" name="password" id="password" 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 value="********">
|
||||
</div>
|
||||
</div>
|
||||
@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
|
||||
</h4>
|
||||
<div class="space-y-4">
|
||||
<!-- Current Password -->
|
||||
<div class="flex flex-col">
|
||||
<label class="text-[13px] font-bold text-gray-700 mb-1.5" for="current_password">Mật khẩu hiện tại</label>
|
||||
<div class="relative">
|
||||
<span class="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
|
||||
<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="password" name="current_password" id="current_password"
|
||||
class="w-full h-[46px] pl-[42px] pr-12 bg-white border @error('current_password') border-red-500 focus:border-red-500 focus:ring-red-500/20 @else border-gray-200 focus:border-[#3462f7] focus:ring-[#3462f7]/20 @enderror rounded-xl text-sm text-gray-800 outline-none transition-all shadow-sm placeholder:text-gray-400"
|
||||
placeholder="Nhập mật khẩu hiện tại (chỉ cần khi đổi mật khẩu)">
|
||||
<button type="button" onclick="togglePasswordVisibility('current_password', this)" class="absolute right-3.5 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 focus:outline-none cursor-pointer" aria-label="Show password">
|
||||
<svg class="w-5 h-5 eye-icon-closed" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12c1.292 4.338 5.31 7.5 10.066 7.5.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"/></svg>
|
||||
<svg class="w-5 h-5 eye-icon-open hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@error('current_password')
|
||||
<span class="text-red-500 text-[12px] font-semibold mt-1.5 block">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- New Password -->
|
||||
<div class="flex flex-col">
|
||||
<label class="text-[13px] font-bold text-gray-700 mb-1.5" for="new_password">Mật khẩu mới</label>
|
||||
<div class="relative">
|
||||
<span class="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
|
||||
<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="password" name="new_password" id="new_password"
|
||||
class="w-full h-[46px] pl-[42px] pr-12 bg-white border @error('new_password') border-red-500 focus:border-red-500 focus:ring-red-500/20 @else border-gray-200 focus:border-[#3462f7] focus:ring-[#3462f7]/20 @enderror rounded-xl text-sm text-gray-800 outline-none transition-all shadow-sm placeholder:text-gray-400"
|
||||
placeholder="Nhập mật khẩu mới">
|
||||
<button type="button" onclick="togglePasswordVisibility('new_password', this)" class="absolute right-3.5 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 focus:outline-none cursor-pointer" aria-label="Show password">
|
||||
<svg class="w-5 h-5 eye-icon-closed" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12c1.292 4.338 5.31 7.5 10.066 7.5.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"/></svg>
|
||||
<svg class="w-5 h-5 eye-icon-open hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@error('new_password')
|
||||
<span class="text-red-500 text-[12px] font-semibold mt-1.5 block">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<div class="flex flex-col">
|
||||
<label class="text-[13px] font-bold text-gray-700 mb-1.5" for="new_password_confirmation">Xác nhận mật khẩu mới</label>
|
||||
<div class="relative">
|
||||
<span class="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<input type="password" name="new_password_confirmation" id="new_password_confirmation"
|
||||
class="w-full h-[46px] pl-[42px] pr-12 bg-white border @error('new_password_confirmation') border-red-500 focus:border-red-500 focus:ring-red-500/20 @else border-gray-200 focus:border-[#3462f7] focus:ring-[#3462f7]/20 @enderror rounded-xl text-sm text-gray-800 outline-none transition-all shadow-sm placeholder:text-gray-400"
|
||||
placeholder="Nhập lại mật khẩu mới">
|
||||
<button type="button" onclick="togglePasswordVisibility('new_password_confirmation', this)" class="absolute right-3.5 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 focus:outline-none cursor-pointer" aria-label="Show password">
|
||||
<svg class="w-5 h-5 eye-icon-closed" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12c1.292 4.338 5.31 7.5 10.066 7.5.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"/></svg>
|
||||
<svg class="w-5 h-5 eye-icon-open hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@error('new_password_confirmation')
|
||||
<span class="text-red-500 text-[12px] font-semibold mt-1.5 block">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@@ -3,76 +3,14 @@
|
||||
@section('title', 'Cài đặt tài khoản')
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-[480px] mx-auto bg-white rounded-[24px] shadow-[0_10px_30px_rgba(15,23,42,0.05)] p-8 mt-6">
|
||||
<h3 class="text-[20px] font-extrabold text-[#1a2b49] mb-4 text-center">Cài đặt tài khoản</h3>
|
||||
|
||||
<div class="max-w-2xl mx-auto w-full mt-6">
|
||||
@if(session('success'))
|
||||
<div class="mb-4 p-3 bg-green-100 text-green-800 rounded-xl">{{ session('success') }}</div>
|
||||
<div class="mb-4 p-4 bg-green-50 border border-green-200 text-green-800 rounded-2xl font-semibold flex items-center gap-2 shadow-sm">
|
||||
<span>✅</span>
|
||||
<span>{{ session('success') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('user.update_profile') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<!-- Avatar preview and upload -->
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="w-16 h-16 rounded-full overflow-hidden border border-gray-200">
|
||||
<img src="{{ Auth::user()->avatar ?? asset('images/avatar.png') }}" alt="Avatar" class="w-full h-full object-cover" id="avatar-preview" onerror="this.onerror=null; this.src='{{ asset('images/avatar.png') }}';">
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<label class="block text-[13px] font-bold text-gray-700 mb-1" for="avatar">Thay đổi ảnh đại diện</label>
|
||||
<input type="file" name="avatar" id="avatar" accept="image/*" class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-3 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-[#3462f7] file:text-white hover:file:bg-[#254edb]" onchange="previewAvatar(event)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New password -->
|
||||
<div class="space-y-6 mb-4">
|
||||
<div class="flex flex-col">
|
||||
<label class="text-[13px] font-bold text-gray-700 mb-1.5" for="new_password">Mật khẩu mới <span class="text-red-500">*</span></label>
|
||||
<div class="relative">
|
||||
<span class="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-400"><svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" class=\"w-5 h-5\"><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="password" name="new_password" id="new_password" class="w-full h-[46px] pl-[42px] pr-12 bg-white border @error('new_password') border-red-500 focus:border-red-500 focus:ring-red-500/20 @else border-gray-200 focus:border-[#2563eb] focus:ring-[#2563eb]/20 @enderror rounded-lg text-sm text-gray-800 outline-none transition-all shadow-sm placeholder:text-gray-400" placeholder="Nhập mật khẩu mới">
|
||||
<button type="button" onclick="togglePasswordVisibility('new_password', this)" class="absolute right-3.5 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 focus:outline-none cursor-pointer" aria-label="Show password">
|
||||
<svg class="w-5 h-5 eye-icon-closed" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12c1.292 4.338 5.31 7.5 10.066 7.5.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"/></svg>
|
||||
<svg class="w-5 h-5 eye-icon-open hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@error('new_password')
|
||||
<span class="text-red-500 text-[12px] font-semibold mt-1.5 block">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<label class="text-[13px] font-bold text-gray-700 mb-1.5" for="new_password_confirmation">Xác nhận mật khẩu mới <span class="text-red-500">*</span></label>
|
||||
<div class="relative">
|
||||
<span class="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-400"><svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" class=\"w-5 h-5\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"/></svg></span>
|
||||
<input type="password" name="new_password_confirmation" id="new_password_confirmation" class="w-full h-[46px] pl-[42px] pr-12 bg-white border @error('new_password_confirmation') border-red-500 focus:border-red-500 focus:ring-red-500/20 @else border-gray-200 focus:border-[#2563eb] focus:ring-[#2563eb]/20 @enderror rounded-lg text-sm text-gray-800 outline-none transition-all shadow-sm placeholder:text-gray-400" placeholder="Nhập lại mật khẩu mới">
|
||||
<button type="button" onclick="togglePasswordVisibility('new_password_confirmation', this)" class="absolute right-3.5 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 focus:outline-none cursor-pointer" aria-label="Show password">
|
||||
<svg class="w-5 h-5 eye-icon-closed" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12c1.292 4.338 5.31 7.5 10.066 7.5.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"/></svg>
|
||||
<svg class="w-5 h-5 eye-icon-open hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@error('new_password_confirmation')
|
||||
<span class="text-red-500 text-[12px] font-semibold mt-1.5 block">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 pt-4">
|
||||
<a href="{{ route('user.dashboard') }}" class="flex-1 text-center py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-xl font-medium">Hủy bỏ</a>
|
||||
<button type="submit" class="flex-1 py-2 bg-[#3462f7] hover:bg-[#254edb] text-white rounded-xl font-bold">Lưu thay đổi</button>
|
||||
</div>
|
||||
</form>
|
||||
<x-user-form mode="self" :user="Auth::user()" />
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function previewAvatar(event) {
|
||||
const input = event.target;
|
||||
if (input.files && input.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
document.getElementById('avatar-preview').src = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Gửi Thank Card')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.ts-control { border-radius: 0.75rem; border-color: #d9dfe7; padding: 0.65rem 1rem; }
|
||||
.ts-wrapper.form-input { padding: 0; border: none; }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@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">
|
||||
|
||||
@@ -14,6 +14,7 @@ Route::middleware(['auth', 'force_change_password', 'admin'])->prefix('admin')->
|
||||
Route::delete('/users/{msnv}', [AdminController::class, 'destroy'])->name('users.destroy');
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
+3
-1
@@ -8,4 +8,6 @@ Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->purpose('Display an inspiring quote');
|
||||
|
||||
Schedule::command('cards:reset')->monthlyOn(1, '00:00');
|
||||
Schedule::command('cards:reset')->dailyAt('23:59')->when(function () {
|
||||
return \Carbon\Carbon::now()->isLastOfMonth();
|
||||
});
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
@@ -770,4 +770,205 @@ class AdminUserListStatsTest extends TestCase
|
||||
$member->refresh();
|
||||
$this->assertEquals(16, $member->card);
|
||||
}
|
||||
|
||||
public function test_delete_add_card_successfully_updates_balance(): void
|
||||
{
|
||||
$admin = User::create([
|
||||
'msnv' => 9001,
|
||||
'name' => 'Admin User',
|
||||
'mail' => 'admin@example.com',
|
||||
'pass' => md5('password'),
|
||||
'departments' => 1,
|
||||
'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'),
|
||||
]);
|
||||
|
||||
$member = User::create([
|
||||
'msnv' => 9002,
|
||||
'name' => 'Member User',
|
||||
'mail' => 'member@example.com',
|
||||
'pass' => md5('password'),
|
||||
'departments' => 1,
|
||||
'role' => config('constants.ROLE_MEMBER'),
|
||||
'status' => config('constants.STATUS_ACTIVE'),
|
||||
'card' => 15,
|
||||
'flag_send' => config('constants.FLAG_SEND_ENABLED'),
|
||||
'first_login' => config('constants.FIRST_LOGIN_FALSE'),
|
||||
]);
|
||||
|
||||
$addCard = \App\Models\AddCard::create([
|
||||
'buyer' => 9002,
|
||||
'num_card' => 5,
|
||||
'seller' => 9001,
|
||||
'date' => Carbon::now()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
// Action: delete the allocation
|
||||
$response = $this->actingAs($admin)->delete(route('admin.add_cards.destroy', $addCard->id));
|
||||
|
||||
$response->assertRedirect();
|
||||
|
||||
$this->assertDatabaseMissing('add_card', ['id' => $addCard->id]);
|
||||
|
||||
$member->refresh();
|
||||
$this->assertEquals(10, $member->card);
|
||||
}
|
||||
|
||||
public function test_update_add_card_fails_if_balance_drops_below_sent_cards(): void
|
||||
{
|
||||
$admin = User::create([
|
||||
'msnv' => 9001,
|
||||
'name' => 'Admin User',
|
||||
'mail' => 'admin@example.com',
|
||||
'pass' => md5('password'),
|
||||
'departments' => 1,
|
||||
'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'),
|
||||
]);
|
||||
|
||||
$member = User::create([
|
||||
'msnv' => 9002,
|
||||
'name' => 'Member User',
|
||||
'mail' => 'member@example.com',
|
||||
'pass' => md5('password'),
|
||||
'departments' => 1,
|
||||
'role' => config('constants.ROLE_MEMBER'),
|
||||
'status' => config('constants.STATUS_ACTIVE'),
|
||||
'card' => 10,
|
||||
'flag_send' => config('constants.FLAG_SEND_ENABLED'),
|
||||
'first_login' => config('constants.FIRST_LOGIN_FALSE'),
|
||||
]);
|
||||
|
||||
$addCard = \App\Models\AddCard::create([
|
||||
'buyer' => 9002,
|
||||
'num_card' => 10,
|
||||
'seller' => 9001,
|
||||
'date' => Carbon::now()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
// Member sent 6 cards this month
|
||||
Administration::create([
|
||||
'msnv' => 9002,
|
||||
'received' => 0,
|
||||
'sent' => 6,
|
||||
'date' => Carbon::now()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
// Try to update num_card from 10 to 4 (decrease of 6, new user balance would be 10 - 6 = 4).
|
||||
// Since member sent 6 cards, 4 is less than 6, so this should fail!
|
||||
$response = $this->actingAs($admin)->put(route('admin.add_cards.update', $addCard->id), [
|
||||
'num_card' => 4,
|
||||
'seller' => 9001,
|
||||
'date' => Carbon::now()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHasErrors('error');
|
||||
$this->assertStringContainsString('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', session('errors')->first('error'));
|
||||
}
|
||||
|
||||
public function test_delete_add_card_fails_if_balance_drops_below_sent_cards(): void
|
||||
{
|
||||
$admin = User::create([
|
||||
'msnv' => 9001,
|
||||
'name' => 'Admin User',
|
||||
'mail' => 'admin@example.com',
|
||||
'pass' => md5('password'),
|
||||
'departments' => 1,
|
||||
'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'),
|
||||
]);
|
||||
|
||||
$member = User::create([
|
||||
'msnv' => 9002,
|
||||
'name' => 'Member User',
|
||||
'mail' => 'member@example.com',
|
||||
'pass' => md5('password'),
|
||||
'departments' => 1,
|
||||
'role' => config('constants.ROLE_MEMBER'),
|
||||
'status' => config('constants.STATUS_ACTIVE'),
|
||||
'card' => 10,
|
||||
'flag_send' => config('constants.FLAG_SEND_ENABLED'),
|
||||
'first_login' => config('constants.FIRST_LOGIN_FALSE'),
|
||||
]);
|
||||
|
||||
$addCard = \App\Models\AddCard::create([
|
||||
'buyer' => 9002,
|
||||
'num_card' => 10,
|
||||
'seller' => 9001,
|
||||
'date' => Carbon::now()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
// Member sent 6 cards this month
|
||||
Administration::create([
|
||||
'msnv' => 9002,
|
||||
'received' => 0,
|
||||
'sent' => 6,
|
||||
'date' => Carbon::now()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
// Try to delete the 10 cards allocation (user balance would be 10 - 10 = 0).
|
||||
// Since member sent 6 cards, 0 is less than 6, so this should fail!
|
||||
$response = $this->actingAs($admin)->delete(route('admin.add_cards.destroy', $addCard->id));
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHasErrors('error');
|
||||
$this->assertStringContainsString('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', session('errors')->first('error'));
|
||||
}
|
||||
|
||||
public function test_card_additions_on_same_day_are_accumulated(): void
|
||||
{
|
||||
$admin = User::create([
|
||||
'msnv' => 9001,
|
||||
'name' => 'Admin User',
|
||||
'mail' => 'admin@example.com',
|
||||
'pass' => md5('password'),
|
||||
'departments' => 1,
|
||||
'role' => config('constants.ROLE_ADMIN'),
|
||||
'status' => config('constants.STATUS_ACTIVE'),
|
||||
'card' => 0,
|
||||
'flag_send' => config('constants.FLAG_SEND_DISABLED'),
|
||||
'first_login' => config('constants.FIRST_LOGIN_FALSE'),
|
||||
]);
|
||||
|
||||
$member = User::create([
|
||||
'msnv' => 9002,
|
||||
'name' => 'Member User',
|
||||
'mail' => 'member@example.com',
|
||||
'pass' => md5('password'),
|
||||
'departments' => 1,
|
||||
'role' => config('constants.ROLE_MEMBER'),
|
||||
'status' => config('constants.STATUS_ACTIVE'),
|
||||
'card' => 5,
|
||||
'flag_send' => config('constants.FLAG_SEND_ENABLED'),
|
||||
'first_login' => config('constants.FIRST_LOGIN_FALSE'),
|
||||
]);
|
||||
|
||||
// First allocation: add 10 cards
|
||||
$this->actingAs($admin)->put(route('admin.users.update', 9002), [
|
||||
'card' => 15,
|
||||
]);
|
||||
|
||||
// Second allocation: add 5 cards
|
||||
$this->actingAs($admin)->put(route('admin.users.update', 9002), [
|
||||
'card' => 20,
|
||||
]);
|
||||
|
||||
$member->refresh();
|
||||
$this->assertEquals(20, $member->card);
|
||||
|
||||
// There should be only 1 AddCard record with 15 cards (10 + 5)
|
||||
$addCards = \App\Models\AddCard::where('buyer', 9002)->where('seller', 9001)->get();
|
||||
$this->assertCount(1, $addCards);
|
||||
$this->assertEquals(15, $addCards->first()->num_card);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user