m
This commit is contained in:
@@ -42,13 +42,15 @@ class AdminController extends Controller
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'msnv' => 'required|unique:user,msnv',
|
||||
'msnv' => 'required|integer|unique:user,msnv',
|
||||
'name' => 'required|string|max:255',
|
||||
'mail' => 'required|email|unique:user,mail',
|
||||
'password' => 'required|min:6',
|
||||
'departments' => 'required|integer|min:1|max:5',
|
||||
'password' => 'required|string',
|
||||
'role' => 'required|in:' . User::ROLE_MEMBER . ',' . User::ROLE_ADMIN,
|
||||
]);
|
||||
|
||||
$this->adminService->createUser($request->only('msnv', 'mail', 'password', 'role'));
|
||||
$this->adminService->createUser($request->only('msnv', 'name', 'mail', 'departments', 'password', 'role'));
|
||||
|
||||
return redirect()->route('admin.users.index')->with('success', __('messages.user_create_success'));
|
||||
}
|
||||
|
||||
@@ -34,7 +34,10 @@ class UserController extends Controller
|
||||
public function storeThankcards(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'receiver' => 'required|exists:user,msnv',
|
||||
'receiver' => [
|
||||
'required',
|
||||
\Illuminate\Validation\Rule::exists('user', 'msnv')->where('status', \App\Models\User::STATUS_ACTIVE)
|
||||
],
|
||||
'amount' => 'required|integer|min:1|max:' . Administration::MAX_SEND_CARD_PER_MONTH,
|
||||
]);
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@ class AddCard extends Model
|
||||
|
||||
protected $table = 'add_card';
|
||||
public $timestamps = false;
|
||||
protected $primaryKey = null;
|
||||
public $incrementing = false;
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $fillable = [
|
||||
'buyer',
|
||||
|
||||
+4
-3
@@ -25,14 +25,15 @@ class User extends Authenticatable
|
||||
|
||||
protected $table = 'user';
|
||||
public $timestamps = false;
|
||||
protected $primaryKey = 'msnv';
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'string';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $fillable = [
|
||||
'msnv',
|
||||
'name',
|
||||
'mail',
|
||||
'pass',
|
||||
'departments',
|
||||
'avatar',
|
||||
'role',
|
||||
'status',
|
||||
'card',
|
||||
|
||||
@@ -66,8 +66,10 @@ class AdminService implements AdminServiceInterface
|
||||
{
|
||||
User::create([
|
||||
'msnv' => $data['msnv'],
|
||||
'name' => $data['name'],
|
||||
'mail' => $data['mail'],
|
||||
'pass' => Hash::make($data['password']),
|
||||
'departments' => $data['departments'],
|
||||
'role' => $data['role'],
|
||||
'status' => User::STATUS_ACTIVE,
|
||||
'card' => 0,
|
||||
@@ -83,9 +85,9 @@ class AdminService implements AdminServiceInterface
|
||||
DB::transaction(function () use ($data, $user) {
|
||||
if (!empty($data['num_card'])) {
|
||||
AddCard::create([
|
||||
'buyer' => abs(crc32($user->msnv)) % 1000000,
|
||||
'buyer' => $user->msnv,
|
||||
'num_card' => $data['num_card'],
|
||||
'seller' => abs(crc32(Auth::user()->msnv)) % 1000000,
|
||||
'seller' => Auth::user()->msnv,
|
||||
'date' => Carbon::today(),
|
||||
]);
|
||||
$user->card += $data['num_card'];
|
||||
|
||||
@@ -11,14 +11,15 @@ class AuthService implements AuthServiceInterface
|
||||
{
|
||||
public function attemptLogin(array $credentials, Request $request): array
|
||||
{
|
||||
if (!Auth::attempt(['mail' => $credentials['mail'], 'password' => $credentials['password']])) {
|
||||
// Manual MD5 password check
|
||||
$user = User::where('mail', $credentials['mail'])->first();
|
||||
if (!$user || $user->pass !== md5($credentials['password'])) {
|
||||
return ['success' => false, 'error_key' => 'mail', 'error_msg' => __('auth.failed')];
|
||||
}
|
||||
|
||||
// Log the user in manually
|
||||
Auth::login($user);
|
||||
$request->session()->regenerate();
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
if ($user->status != User::STATUS_ACTIVE) {
|
||||
Auth::logout();
|
||||
return ['success' => false, 'error_key' => 'mail', 'error_msg' => __('auth.inactive')];
|
||||
|
||||
@@ -38,6 +38,11 @@ class UserService implements UserServiceInterface
|
||||
throw new \RuntimeException(__('messages.error.no_send_permission'));
|
||||
}
|
||||
|
||||
$receiver = User::where('msnv', $receiverMsnv)->first();
|
||||
if (!$receiver || $receiver->status != User::STATUS_ACTIVE) {
|
||||
throw new \RuntimeException(__('messages.error.receiver_invalid'));
|
||||
}
|
||||
|
||||
$startOfMonth = Carbon::now()->startOfMonth();
|
||||
$endOfMonth = Carbon::now()->endOfMonth();
|
||||
|
||||
|
||||
@@ -12,14 +12,18 @@ return new class extends Migration
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user', function (Blueprint $table) {
|
||||
$table->string('msnv')->primary();
|
||||
$table->increments('id');
|
||||
$table->integer('msnv')->unique();
|
||||
$table->string('name');
|
||||
$table->string('mail');
|
||||
$table->string('pass');
|
||||
$table->tinyInteger('role')->default(0)->comment('0: member / 1: admin');
|
||||
$table->tinyInteger('status')->default(1)->comment('0: đã nghỉ / 1: đang làm');
|
||||
$table->integer('card')->nullable();
|
||||
$table->tinyInteger('flag_send')->default(0)->comment('0: không được gửi / 1: được gửi');
|
||||
$table->tinyInteger('first_login')->default(1)->comment('0: không là lần đầu / 1: lần đầu');
|
||||
$table->integer('departments')->default(1);
|
||||
$table->string('avatar')->nullable();
|
||||
$table->tinyInteger('role')->default(0)->comment('0: member, default / 1: admin');
|
||||
$table->tinyInteger('status')->default(1)->comment('0: đã nghỉ / 1: đang làm, default');
|
||||
$table->integer('card')->default(0);
|
||||
$table->tinyInteger('flag_send')->default(0)->comment('0: không được gửi card / 1: được gửi card');
|
||||
$table->tinyInteger('first_login')->default(1)->comment('0: không là lần đầu / 1: lần đầu, default');
|
||||
});
|
||||
|
||||
// Only user table remains
|
||||
|
||||
@@ -12,10 +12,11 @@ return new class extends Migration
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('add_card', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->integer('buyer');
|
||||
$table->integer('num_card');
|
||||
$table->integer('seller');
|
||||
$table->date('date')->nullable();
|
||||
$table->date('date');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,12 +12,12 @@ return new class extends Migration
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('administration', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('msnv');
|
||||
$table->increments('id');
|
||||
$table->integer('msnv');
|
||||
$table->integer('received')->nullable();
|
||||
$table->string('sender')->nullable();
|
||||
$table->integer('sender')->nullable();
|
||||
$table->integer('sent')->nullable();
|
||||
$table->string('receiver')->nullable();
|
||||
$table->integer('receiver')->nullable();
|
||||
$table->date('date')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,14 +24,17 @@ class MockDataSeeder extends Seeder
|
||||
AddCard::truncate();
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
|
||||
|
||||
$password = Hash::make('123456');
|
||||
// The default password is now set to 1234@Dcba according to the design requirements
|
||||
$password = md5('1234@Dcba');
|
||||
|
||||
// 1. CREATE USERS
|
||||
// Admins
|
||||
User::create([
|
||||
'msnv' => 'ADMIN',
|
||||
'msnv' => 9999,
|
||||
'name' => 'System Admin',
|
||||
'mail' => 'admin@runsystem.net',
|
||||
'pass' => $password,
|
||||
'departments' => 1,
|
||||
'role' => User::ROLE_ADMIN,
|
||||
'status' => User::STATUS_ACTIVE,
|
||||
'card' => 0,
|
||||
@@ -40,9 +43,11 @@ class MockDataSeeder extends Seeder
|
||||
]);
|
||||
|
||||
User::create([
|
||||
'msnv' => 'ADMIN2',
|
||||
'msnv' => 9998,
|
||||
'name' => 'Second Admin',
|
||||
'mail' => 'admin2@runsystem.net',
|
||||
'pass' => $password,
|
||||
'departments' => 1,
|
||||
'role' => User::ROLE_ADMIN,
|
||||
'status' => User::STATUS_ACTIVE,
|
||||
'card' => 0,
|
||||
@@ -52,19 +57,24 @@ class MockDataSeeder extends Seeder
|
||||
|
||||
// Active Users (Initialize with specific users needed for logs/transactions)
|
||||
$users = [
|
||||
['msnv' => 'DEV001', 'mail' => 'nguyenvana@runsystem.net', 'card' => 10, 'flag' => User::FLAG_SEND_ENABLED, 'first_login' => User::FIRST_LOGIN_FALSE],
|
||||
['msnv' => 'DEV002', 'mail' => 'tranthingoc@runsystem.net', 'card' => 3, 'flag' => User::FLAG_SEND_ENABLED, 'first_login' => User::FIRST_LOGIN_FALSE],
|
||||
['msnv' => 'TEST01', 'mail' => 'lekiemthu@runsystem.net', 'card' => 6, 'flag' => User::FLAG_SEND_ENABLED, 'first_login' => User::FIRST_LOGIN_TRUE], // First-time login
|
||||
['msnv' => 'HR001', 'mail' => 'phongnhansu@runsystem.net', 'card' => 0, 'flag' => User::FLAG_SEND_DISABLED, 'first_login' => User::FIRST_LOGIN_FALSE], // Disabled sending permission
|
||||
['msnv' => 'SALE01', 'mail' => 'bangiang@runsystem.net', 'card' => 36, 'flag' => User::FLAG_SEND_ENABLED, 'first_login' => User::FIRST_LOGIN_FALSE],
|
||||
['msnv' => 1001, 'name' => 'Nguyễn Văn A', 'mail' => 'nguyenvana@runsystem.net', 'card' => 10, 'flag' => User::FLAG_SEND_ENABLED, 'first_login' => User::FIRST_LOGIN_FALSE],
|
||||
['msnv' => 1002, 'name' => 'Trần Thị Ngọc', 'mail' => 'tranthingoc@runsystem.net', 'card' => 3, 'flag' => User::FLAG_SEND_ENABLED, 'first_login' => User::FIRST_LOGIN_FALSE],
|
||||
['msnv' => 1003, 'name' => 'Lê Kim Thư', 'mail' => 'lekiemthu@runsystem.net', 'card' => 6, 'flag' => User::FLAG_SEND_ENABLED, 'first_login' => User::FIRST_LOGIN_TRUE], // First-time login
|
||||
['msnv' => 1004, 'name' => 'Phòng Nhân Sự', 'mail' => 'phongnhansu@runsystem.net', 'card' => 0, 'flag' => User::FLAG_SEND_DISABLED, 'first_login' => User::FIRST_LOGIN_FALSE], // Disabled sending permission
|
||||
['msnv' => 1005, 'name' => 'Bản Giang', 'mail' => 'bangiang@runsystem.net', 'card' => 36, 'flag' => User::FLAG_SEND_ENABLED, 'first_login' => User::FIRST_LOGIN_FALSE],
|
||||
];
|
||||
|
||||
// Generate additional users to reach exactly 100 member users
|
||||
for ($i = 3; $i <= 97; $i++) {
|
||||
$numStr = str_pad($i, 3, '0', STR_PAD_LEFT);
|
||||
$firstNames = ['Nguyễn', 'Trần', 'Lê', 'Phạm', 'Hoàng', 'Huỳnh', 'Phan', 'Vũ', 'Võ', 'Đặng'];
|
||||
$middleNames = ['Văn', 'Thị', 'Minh', 'Đức', 'Quốc', 'Thanh', 'Hữu', 'Ngọc', 'Quỳnh', 'Thu'];
|
||||
$lastNames = ['Anh', 'Bình', 'Chi', 'Dương', 'Em', 'Giang', 'Huy', 'Khánh', 'Lâm', 'Nam', 'Oanh', 'Phúc', 'Quang', 'Sơn', 'Trang', 'Vy'];
|
||||
|
||||
for ($i = 6; $i <= 100; $i++) {
|
||||
$randomName = $firstNames[array_rand($firstNames)] . ' ' . $middleNames[array_rand($middleNames)] . ' ' . $lastNames[array_rand($lastNames)];
|
||||
$users[] = [
|
||||
'msnv' => 'DEV' . $numStr,
|
||||
'mail' => 'dev' . $numStr . '@runsystem.net',
|
||||
'msnv' => 1000 + $i,
|
||||
'name' => $randomName,
|
||||
'mail' => 'dev' . str_pad($i, 3, '0', STR_PAD_LEFT) . '@runsystem.net',
|
||||
'card' => rand(0, 30),
|
||||
'flag' => rand(0, 5) > 0 ? User::FLAG_SEND_ENABLED : User::FLAG_SEND_DISABLED,
|
||||
'first_login' => rand(0, 4) == 0 ? User::FIRST_LOGIN_TRUE : User::FIRST_LOGIN_FALSE
|
||||
@@ -74,8 +84,10 @@ class MockDataSeeder extends Seeder
|
||||
foreach ($users as $u) {
|
||||
User::create([
|
||||
'msnv' => $u['msnv'],
|
||||
'name' => $u['name'],
|
||||
'mail' => $u['mail'],
|
||||
'pass' => $password,
|
||||
'departments' => rand(1, 5),
|
||||
'role' => User::ROLE_MEMBER,
|
||||
'status' => User::STATUS_ACTIVE,
|
||||
'card' => $u['card'],
|
||||
@@ -84,12 +96,12 @@ class MockDataSeeder extends Seeder
|
||||
]);
|
||||
|
||||
// For other users, if they have cards, create a matching purchase log in June 2026
|
||||
if (!in_array($u['msnv'], ['DEV001', 'DEV002', 'TEST01', 'SALE01'])) {
|
||||
if (!in_array($u['msnv'], [1001, 1002, 1003, 1005])) {
|
||||
if ($u['card'] > 0) {
|
||||
AddCard::create([
|
||||
'buyer' => abs(crc32($u['msnv'])) % 1000000,
|
||||
'buyer' => $u['msnv'],
|
||||
'num_card' => $u['card'],
|
||||
'seller' => abs(crc32('ADMIN')) % 1000000,
|
||||
'seller' => 9999,
|
||||
'date' => '2026-06-01'
|
||||
]);
|
||||
}
|
||||
@@ -98,9 +110,11 @@ class MockDataSeeder extends Seeder
|
||||
|
||||
// Inactive User
|
||||
User::create([
|
||||
'msnv' => 'OLD001',
|
||||
'msnv' => 9997,
|
||||
'name' => 'Nghi Duy',
|
||||
'mail' => 'nghiduy@runsystem.net',
|
||||
'pass' => $password,
|
||||
'departments' => 2,
|
||||
'role' => User::ROLE_MEMBER,
|
||||
'status' => User::STATUS_INACTIVE,
|
||||
'card' => 0,
|
||||
@@ -111,28 +125,28 @@ class MockDataSeeder extends Seeder
|
||||
// 2. CREATE CARD ADDITION LOGS (add_card table)
|
||||
// Manual purchase history for specific users:
|
||||
$addCardLogs = [
|
||||
// DEV001: total 30 cards bought
|
||||
['buyer' => 'DEV001', 'num_card' => 15, 'date' => '2026-06-01'],
|
||||
['buyer' => 'DEV001', 'num_card' => 10, 'date' => '2026-07-01'],
|
||||
['buyer' => 'DEV001', 'num_card' => 5, 'date' => '2026-08-01'],
|
||||
// 1001: total 30 cards bought
|
||||
['buyer' => 1001, 'num_card' => 15, 'date' => '2026-06-01'],
|
||||
['buyer' => 1001, 'num_card' => 10, 'date' => '2026-07-01'],
|
||||
['buyer' => 1001, 'num_card' => 5, 'date' => '2026-08-01'],
|
||||
|
||||
// DEV002: total 15 cards bought
|
||||
['buyer' => 'DEV002', 'num_card' => 10, 'date' => '2026-06-02'],
|
||||
['buyer' => 'DEV002', 'num_card' => 5, 'date' => '2026-07-01'],
|
||||
// 1002: total 15 cards bought
|
||||
['buyer' => 1002, 'num_card' => 10, 'date' => '2026-06-02'],
|
||||
['buyer' => 1002, 'num_card' => 5, 'date' => '2026-07-01'],
|
||||
|
||||
// TEST01: total 15 cards bought
|
||||
['buyer' => 'TEST01', 'num_card' => 10, 'date' => '2026-06-03'],
|
||||
['buyer' => 'TEST01', 'num_card' => 5, 'date' => '2026-08-02'],
|
||||
// 1003: total 15 cards bought
|
||||
['buyer' => 1003, 'num_card' => 10, 'date' => '2026-06-03'],
|
||||
['buyer' => 1003, 'num_card' => 5, 'date' => '2026-08-02'],
|
||||
|
||||
// SALE01: total 50 cards bought
|
||||
['buyer' => 'SALE01', 'num_card' => 50, 'date' => '2026-06-04'],
|
||||
// 1005: total 50 cards bought
|
||||
['buyer' => 1005, 'num_card' => 50, 'date' => '2026-06-04'],
|
||||
];
|
||||
|
||||
foreach ($addCardLogs as $log) {
|
||||
AddCard::create([
|
||||
'buyer' => abs(crc32($log['buyer'])) % 1000000,
|
||||
'buyer' => $log['buyer'],
|
||||
'num_card' => $log['num_card'],
|
||||
'seller' => abs(crc32('ADMIN')) % 1000000,
|
||||
'seller' => 9999,
|
||||
'date' => $log['date']
|
||||
]);
|
||||
}
|
||||
@@ -140,24 +154,24 @@ class MockDataSeeder extends Seeder
|
||||
// 3. CREATE CARD SENDING HISTORY (administration table)
|
||||
$transactions = [
|
||||
// June 2026 transactions
|
||||
['sender' => 'DEV001', 'receiver' => 'SALE01', 'amount' => 5, 'date' => '2026-06-05'],
|
||||
['sender' => 'DEV001', 'receiver' => 'DEV002', 'amount' => 3, 'date' => '2026-06-12'],
|
||||
['sender' => 'DEV002', 'receiver' => 'HR001', 'amount' => 4, 'date' => '2026-06-10'],
|
||||
['sender' => 'TEST01', 'receiver' => 'DEV001', 'amount' => 2, 'date' => '2026-06-15'],
|
||||
['sender' => 'SALE01', 'receiver' => 'DEV002', 'amount' => 5, 'date' => '2026-06-20'],
|
||||
['sender' => 1001, 'receiver' => 1005, 'amount' => 5, 'date' => '2026-06-05'],
|
||||
['sender' => 1001, 'receiver' => 1002, 'amount' => 3, 'date' => '2026-06-12'],
|
||||
['sender' => 1002, 'receiver' => 1004, 'amount' => 4, 'date' => '2026-06-10'],
|
||||
['sender' => 1003, 'receiver' => 1001, 'amount' => 2, 'date' => '2026-06-15'],
|
||||
['sender' => 1005, 'receiver' => 1002, 'amount' => 5, 'date' => '2026-06-20'],
|
||||
|
||||
// July 2026 transactions
|
||||
['sender' => 'DEV001', 'receiver' => 'DEV002', 'amount' => 5, 'date' => '2026-07-02'],
|
||||
['sender' => 'DEV001', 'receiver' => 'TEST01', 'amount' => 3, 'date' => '2026-07-10'],
|
||||
['sender' => 'DEV002', 'receiver' => 'DEV001', 'amount' => 5, 'date' => '2026-07-15'],
|
||||
['sender' => 'TEST01', 'receiver' => 'HR001', 'amount' => 4, 'date' => '2026-07-08'],
|
||||
['sender' => 'SALE01', 'receiver' => 'DEV002', 'amount' => 4, 'date' => '2026-07-12'],
|
||||
['sender' => 1001, 'receiver' => 1002, 'amount' => 5, 'date' => '2026-07-02'],
|
||||
['sender' => 1001, 'receiver' => 1003, 'amount' => 3, 'date' => '2026-07-10'],
|
||||
['sender' => 1002, 'receiver' => 1001, 'amount' => 5, 'date' => '2026-07-15'],
|
||||
['sender' => 1003, 'receiver' => 1004, 'amount' => 4, 'date' => '2026-07-08'],
|
||||
['sender' => 1005, 'receiver' => 1002, 'amount' => 4, 'date' => '2026-07-12'],
|
||||
|
||||
// August 2026 transactions
|
||||
['sender' => 'DEV001', 'receiver' => 'SALE01', 'amount' => 4, 'date' => '2026-08-05'],
|
||||
['sender' => 'DEV002', 'receiver' => 'TEST01', 'amount' => 3, 'date' => '2026-08-12'],
|
||||
['sender' => 'TEST01', 'receiver' => 'DEV001', 'amount' => 3, 'date' => '2026-08-10'],
|
||||
['sender' => 'SALE01', 'receiver' => 'HR001', 'amount' => 5, 'date' => '2026-08-15'],
|
||||
['sender' => 1001, 'receiver' => 1005, 'amount' => 4, 'date' => '2026-08-05'],
|
||||
['sender' => 1002, 'receiver' => 1003, 'amount' => 3, 'date' => '2026-08-12'],
|
||||
['sender' => 1003, 'receiver' => 1001, 'amount' => 3, 'date' => '2026-08-10'],
|
||||
['sender' => 1005, 'receiver' => 1004, 'amount' => 5, 'date' => '2026-08-15'],
|
||||
];
|
||||
|
||||
foreach ($transactions as $t) {
|
||||
|
||||
+20
-9
@@ -67,16 +67,27 @@ docker compose -f docker/docker-compose.yml exec app composer install
|
||||
|
||||
### 4.2. Khi cập nhật giao diện (CSS / JS / Tailwind classes mới)
|
||||
|
||||
Do Docker container không cài Node.js, bạn cần chạy các lệnh compile frontend trực tiếp **ở máy thật (host)**:
|
||||
Cập nhật giao diện cần biên dịch lại tài nguyên frontend. Bạn có 2 cách:
|
||||
|
||||
- **Chạy chế độ phát triển (Hot Reload):**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
- **Build production tĩnh:**
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
**1️⃣ Chạy trên máy host (đề xuất)**
|
||||
```bash
|
||||
npm install # một lần, nếu chưa có các package
|
||||
npm run dev # Vite dev server, hot‑reload
|
||||
```
|
||||
|
||||
**2️⃣ Chạy trong Docker (sử dụng container `node`)**
|
||||
```bash
|
||||
# Cài đặt các package (chỉ chạy 1 lần)
|
||||
docker compose -f docker/docker-compose.yml exec node npm install
|
||||
|
||||
# Chế độ phát triển (hot‑reload)
|
||||
docker compose -f docker/docker-compose.yml exec node npm run dev
|
||||
|
||||
# Hoặc biên dịch bản production
|
||||
docker compose -f docker/docker-compose.yml exec node npm run build
|
||||
```
|
||||
|
||||
Container `node` đã expose port **5173**, cho phép truy cập Vite dev server tại `http://localhost:5173`. Khi `npm run dev` đang chạy, mọi thay đổi CSS/JS sẽ tự động cập nhật trong container `app` nhờ volume mount.
|
||||
|
||||
### 4.3. Khi cập nhật Database (Migrations)
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ services:
|
||||
volumes:
|
||||
- ..:/var/www/html
|
||||
command: sh -c "npm install && npm run build"
|
||||
ports:
|
||||
- "5173:5173"
|
||||
networks:
|
||||
- thankcard-system-network
|
||||
|
||||
|
||||
@@ -12,5 +12,6 @@ return [
|
||||
'no_send_permission' => 'Bạn hiện không có quyền gửi Thank Card.',
|
||||
'not_enough_cards' => 'Số lượng card của bạn không đủ. Vui lòng liên hệ Admin.',
|
||||
'max_send_limit_template' => 'Một tháng bạn chỉ được gửi tối đa :max card cho một người. Bạn đã gửi :sent card cho nhân viên này.',
|
||||
'receiver_invalid' => 'Người nhận không tồn tại hoặc đã nghỉ việc.',
|
||||
]
|
||||
];
|
||||
|
||||
@@ -69,11 +69,24 @@
|
||||
<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="text" name="msnv" id="msnv" class="form-input form-input-with-icon bg-white" required value="{{ old('msnv') }}" placeholder="VD: RUN-0123">
|
||||
<input type="number" name="msnv" id="msnv" class="form-input form-input-with-icon bg-white" required value="{{ old('msnv') }}" placeholder="VD: 1001">
|
||||
</div>
|
||||
@error('msnv')<span class="error-text">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="name">Họ và tên <span class="text-red-500">*</span></label>
|
||||
<div class="form-input-group">
|
||||
<span class="form-input-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" 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 bg-white" required value="{{ old('name') }}" placeholder="VD: Nguyễn Văn A">
|
||||
</div>
|
||||
@error('name')<span class="error-text">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="mail">Email <span class="text-red-500">*</span></label>
|
||||
<div class="form-input-group">
|
||||
@@ -82,11 +95,23 @@
|
||||
<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 bg-white" required value="{{ old('mail') }}" placeholder="VD: nguyenva@runsystem.net">
|
||||
<input type="email" name="mail" id="mail" class="form-input form-input-with-icon bg-white" required value="{{ old('mail') }}" placeholder="VD: nguyenvana@runsystem.net">
|
||||
</div>
|
||||
@error('mail')<span class="error-text">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="departments">Phòng ban <span class="text-red-500">*</span></label>
|
||||
<select name="departments" id="departments" class="form-input bg-white" required>
|
||||
<option value="1" {{ old('departments') == '1' ? 'selected' : '' }}>Phòng Phát Triển (Dev)</option>
|
||||
<option value="2" {{ old('departments') == '2' ? 'selected' : '' }}>Phòng Nhân Sự (HR)</option>
|
||||
<option value="3" {{ old('departments') == '3' ? 'selected' : '' }}>Phòng Kinh Doanh (Sale)</option>
|
||||
<option value="4" {{ old('departments') == '4' ? 'selected' : '' }}>Ban Giám Đốc (Board)</option>
|
||||
<option value="5" {{ old('departments') == '5' ? 'selected' : '' }}>Phòng Marketing</option>
|
||||
</select>
|
||||
@error('departments')<span class="error-text">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="password">Mật khẩu khởi tạo <span class="text-red-500">*</span></label>
|
||||
<div class="form-input-group">
|
||||
@@ -95,9 +120,9 @@
|
||||
<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 bg-white" required placeholder="Nhập mật khẩu cho user...">
|
||||
<input type="text" name="password" id="password" class="form-input form-input-with-icon bg-gray-50" readonly required value="1234@Dcba">
|
||||
</div>
|
||||
<span class="helper-text">Lưu ý: User sẽ bị buộc phải đổi mật khẩu này ở lần đăng nhập đầu tiên.</span>
|
||||
<span class="helper-text">Mật khẩu mặc định cho tất cả user mới tạo.</span>
|
||||
@error('password')<span class="error-text">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
|
||||
|
||||
@@ -53,9 +53,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Row 3: Thanks Grid & Ranking -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 items-start">
|
||||
<!-- Recent Thanks (Left 2/3) -->
|
||||
<div class="col-span-1 lg:col-span-2 flex flex-col">
|
||||
<div class="col-span-1 lg:col-span-2 flex flex-col flex-1">
|
||||
<div class="flex items-center justify-between mb-4 pl-1 pr-1">
|
||||
<h3 class="text-[17px] font-extrabold text-[#1a2b49]">Thanks gần đây</h3>
|
||||
<a href="{{ route('user.received') }}" class="text-[13px] font-bold text-[#3462f7] hover:text-blue-800">Xem tất cả</a>
|
||||
@@ -96,22 +96,22 @@
|
||||
</div>
|
||||
|
||||
<!-- Ranking and Tip (Right 1/3) -->
|
||||
<div class="col-span-1 flex flex-col">
|
||||
<div class="col-span-1 flex flex-col w-full max-w-[420px] min-w-[380px] flex-1">
|
||||
<div class="flex items-center justify-between mb-3 pl-1 pr-1">
|
||||
<h3 class="text-[17px] font-extrabold text-[#1a2b49]">Bảng xếp hạng</h3>
|
||||
<a href="{{ route('user.ranking') }}" class="text-[13px] font-bold text-[#3462f7] hover:text-blue-800">Xem tất cả</a>
|
||||
</div>
|
||||
|
||||
<!-- Tabs Toggle -->
|
||||
<div class="inline-flex bg-[#F5F7FB] rounded-xl p-1 mb-4 w-fit">
|
||||
<button id="tab-received" onclick="switchRankingTab('received')" class="flex items-center justify-center gap-2 w-[135px] py-2 rounded-[10px] transition-all bg-white shadow-[0_2px_4px_rgba(0,0,0,0.04)] text-[#3462f7] font-bold text-[14px]">
|
||||
<div class="inline-flex bg-[#F5F7FB] rounded-xl p-1 mb-4 w-fit space-x-2">
|
||||
<button id="tab-received" onclick="switchRankingTab('received')" class="flex items-center justify-center gap-2 w-[120px] py-2 rounded-[10px] transition-all bg-white text-[#3462f7] font-bold text-[14px]">
|
||||
<svg class="w-[18px] h-[18px]" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M17.5 4h-11c-1.38 0-2.5 1.12-2.5 2.5V8c0 2.21 1.79 4 4 4h.34c1.17 2.19 3.52 3.56 6.16 3.56s4.99-1.37 6.16-3.56h.34c2.21 0 4-1.79 4-4v-1.5c0-1.38-1.12-2.5-2.5-2.5zm-11 6c-.55 0-1-.45-1-1V6.5c0-.28.22-.5.5-.5h2v4H6.5zm11 0h-2v-4h2c.28 0 .5.22.5.5V9c0 .55-.45 1-1 1zM11 16h2v4h-2v-4zm-2 4h6v2H9v-2z"/>
|
||||
<path fill="#ffffff" d="M12 11.5L10 7h4z"/>
|
||||
</svg>
|
||||
TOP Nhận
|
||||
</button>
|
||||
<button id="tab-sent" onclick="switchRankingTab('sent')" class="flex items-center justify-center gap-2 w-[135px] py-2 rounded-[10px] transition-all text-[#64748b] hover:text-gray-800 font-bold text-[14px]">
|
||||
<button id="tab-sent" onclick="switchRankingTab('sent')" class="flex items-center justify-center gap-2 w-[120px] py-2 rounded-[10px] transition-all text-[#64748b] hover:text-gray-800 font-bold text-[14px]">
|
||||
<svg class="w-[18px] h-[18px]" fill="none" stroke="currentColor" stroke-width="2.2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 2L11 13"></path>
|
||||
<path d="M22 2L15 22L11 13L2 9L22 2Z"></path>
|
||||
|
||||
@@ -25,7 +25,8 @@ class AdminUserListStatsTest extends TestCase
|
||||
{
|
||||
// 1. Create mock users
|
||||
$userA = User::create([
|
||||
'msnv' => 'USER_A',
|
||||
'msnv' => 1001,
|
||||
'name' => 'User A',
|
||||
'mail' => 'usera@example.com',
|
||||
'pass' => bcrypt('password'),
|
||||
'role' => User::ROLE_MEMBER,
|
||||
@@ -36,7 +37,8 @@ class AdminUserListStatsTest extends TestCase
|
||||
]);
|
||||
|
||||
$userB = User::create([
|
||||
'msnv' => 'USER_B',
|
||||
'msnv' => 1002,
|
||||
'name' => 'User B',
|
||||
'mail' => 'userb@example.com',
|
||||
'pass' => bcrypt('password'),
|
||||
'role' => User::ROLE_MEMBER,
|
||||
@@ -52,18 +54,18 @@ class AdminUserListStatsTest extends TestCase
|
||||
// 2. Create mock transaction logs in administration table
|
||||
// A sends 3 cards to B
|
||||
Administration::create([
|
||||
'msnv' => 'USER_A',
|
||||
'msnv' => 1001,
|
||||
'received' => 0,
|
||||
'sender' => null,
|
||||
'sent' => 3,
|
||||
'receiver' => 'USER_B',
|
||||
'receiver' => 1002,
|
||||
'date' => $testDate,
|
||||
]);
|
||||
|
||||
Administration::create([
|
||||
'msnv' => 'USER_B',
|
||||
'msnv' => 1002,
|
||||
'received' => 3,
|
||||
'sender' => 'USER_A',
|
||||
'sender' => 1001,
|
||||
'sent' => 0,
|
||||
'receiver' => null,
|
||||
'date' => $testDate,
|
||||
@@ -77,8 +79,8 @@ class AdminUserListStatsTest extends TestCase
|
||||
$topSent = $stats['topSentUser'];
|
||||
|
||||
// Find users in the paginated collection
|
||||
$userAResult = collect($users->items())->firstWhere('msnv', 'USER_A');
|
||||
$userBResult = collect($users->items())->firstWhere('msnv', 'USER_B');
|
||||
$userAResult = collect($users->items())->firstWhere('msnv', 1001);
|
||||
$userBResult = collect($users->items())->firstWhere('msnv', 1002);
|
||||
|
||||
$this->assertNotNull($userAResult);
|
||||
$this->assertNotNull($userBResult);
|
||||
@@ -91,17 +93,18 @@ class AdminUserListStatsTest extends TestCase
|
||||
$this->assertEquals(3, $userBResult->total_received);
|
||||
|
||||
// Assert top users
|
||||
$this->assertEquals('USER_B', $topReceived->msnv);
|
||||
$this->assertEquals(1002, $topReceived->msnv);
|
||||
$this->assertEquals(3, $topReceived->total_received);
|
||||
|
||||
$this->assertEquals('USER_A', $topSent->msnv);
|
||||
$this->assertEquals(1001, $topSent->msnv);
|
||||
$this->assertEquals(3, $topSent->total_sent);
|
||||
}
|
||||
|
||||
public function test_user_transactions_history_displays_correct_records(): void
|
||||
{
|
||||
$userA = User::create([
|
||||
'msnv' => 'USER_A',
|
||||
'msnv' => 1001,
|
||||
'name' => 'User A',
|
||||
'mail' => 'usera@example.com',
|
||||
'pass' => bcrypt('password'),
|
||||
'role' => User::ROLE_MEMBER,
|
||||
@@ -116,25 +119,25 @@ class AdminUserListStatsTest extends TestCase
|
||||
|
||||
// 1. Create a send record
|
||||
Administration::create([
|
||||
'msnv' => 'USER_A',
|
||||
'msnv' => 1001,
|
||||
'received' => 0,
|
||||
'sender' => null,
|
||||
'sent' => 3,
|
||||
'receiver' => 'USER_B',
|
||||
'receiver' => 1002,
|
||||
'date' => $testDate,
|
||||
]);
|
||||
|
||||
// 2. Create a receive record
|
||||
Administration::create([
|
||||
'msnv' => 'USER_A',
|
||||
'msnv' => 1001,
|
||||
'received' => 5,
|
||||
'sender' => 'USER_C',
|
||||
'sender' => 1003,
|
||||
'sent' => 0,
|
||||
'receiver' => null,
|
||||
'date' => $testDate,
|
||||
]);
|
||||
|
||||
$transactions = $this->adminService->getUserTransactions('USER_A', $selectedMonth);
|
||||
$transactions = $this->adminService->getUserTransactions(1001, $selectedMonth);
|
||||
|
||||
$this->assertCount(2, $transactions->items());
|
||||
|
||||
@@ -145,10 +148,10 @@ class AdminUserListStatsTest extends TestCase
|
||||
$this->assertNotNull($sendTx);
|
||||
$this->assertNotNull($receiveTx);
|
||||
|
||||
$this->assertEquals('USER_B', $sendTx->receiver);
|
||||
$this->assertEquals(1002, $sendTx->receiver);
|
||||
$this->assertNull($sendTx->sender);
|
||||
|
||||
$this->assertEquals('USER_C', $receiveTx->sender);
|
||||
$this->assertEquals(1003, $receiveTx->sender);
|
||||
$this->assertNull($receiveTx->receiver);
|
||||
}
|
||||
|
||||
@@ -156,7 +159,8 @@ class AdminUserListStatsTest extends TestCase
|
||||
{
|
||||
// 1. Create mock users
|
||||
User::create([
|
||||
'msnv' => 'DEV_ALPHA',
|
||||
'msnv' => 2001,
|
||||
'name' => 'DEV ALPHA',
|
||||
'mail' => 'alpha@example.com',
|
||||
'pass' => bcrypt('password'),
|
||||
'role' => User::ROLE_MEMBER,
|
||||
@@ -167,7 +171,8 @@ class AdminUserListStatsTest extends TestCase
|
||||
]);
|
||||
|
||||
User::create([
|
||||
'msnv' => 'DEV_BETA',
|
||||
'msnv' => 2002,
|
||||
'name' => 'DEV BETA',
|
||||
'mail' => 'beta@example.com',
|
||||
'pass' => bcrypt('password'),
|
||||
'role' => User::ROLE_MEMBER,
|
||||
@@ -177,26 +182,27 @@ class AdminUserListStatsTest extends TestCase
|
||||
'first_login' => User::FIRST_LOGIN_FALSE,
|
||||
]);
|
||||
|
||||
// 2. Fetch statistics via AdminService with search for 'ALPHA'
|
||||
$stats = $this->adminService->getUserListWithStats('2026-07', 'ALPHA');
|
||||
// 2. Fetch statistics via AdminService with search for '2001' (msnv)
|
||||
$stats = $this->adminService->getUserListWithStats('2026-07', '2001');
|
||||
$users = $stats['users'];
|
||||
|
||||
$this->assertCount(1, $users->items());
|
||||
$this->assertEquals('DEV_ALPHA', $users->items()[0]->msnv);
|
||||
$this->assertEquals(2001, $users->items()[0]->msnv);
|
||||
|
||||
// 3. Fetch statistics via AdminService with search for 'beta@example.com'
|
||||
// 3. Fetch statistics via AdminService with search for 'beta@example.com' (mail)
|
||||
$stats2 = $this->adminService->getUserListWithStats('2026-07', 'beta@example.com');
|
||||
$users2 = $stats2['users'];
|
||||
|
||||
$this->assertCount(1, $users2->items());
|
||||
$this->assertEquals('DEV_BETA', $users2->items()[0]->msnv);
|
||||
$this->assertEquals(2002, $users2->items()[0]->msnv);
|
||||
}
|
||||
|
||||
public function test_user_list_stats_filters_by_flag_send(): void
|
||||
{
|
||||
// 1. Create mock users
|
||||
User::create([
|
||||
'msnv' => 'DEV_ENABLED',
|
||||
'msnv' => 3001,
|
||||
'name' => 'DEV ENABLED',
|
||||
'mail' => 'enabled@example.com',
|
||||
'pass' => bcrypt('password'),
|
||||
'role' => User::ROLE_MEMBER,
|
||||
@@ -207,7 +213,8 @@ class AdminUserListStatsTest extends TestCase
|
||||
]);
|
||||
|
||||
User::create([
|
||||
'msnv' => 'DEV_DISABLED',
|
||||
'msnv' => 3002,
|
||||
'name' => 'DEV DISABLED',
|
||||
'mail' => 'disabled@example.com',
|
||||
'pass' => bcrypt('password'),
|
||||
'role' => User::ROLE_MEMBER,
|
||||
@@ -220,13 +227,13 @@ class AdminUserListStatsTest extends TestCase
|
||||
// 2. Fetch statistics via AdminService with flag_send = '1'
|
||||
$stats = $this->adminService->getUserListWithStats('2026-07', null, '1');
|
||||
$users = collect($stats['users']->items());
|
||||
$this->assertTrue($users->contains('msnv', 'DEV_ENABLED'));
|
||||
$this->assertFalse($users->contains('msnv', 'DEV_DISABLED'));
|
||||
$this->assertTrue($users->contains('msnv', 3001));
|
||||
$this->assertFalse($users->contains('msnv', 3002));
|
||||
|
||||
// 3. Fetch statistics via AdminService with flag_send = '0'
|
||||
$stats2 = $this->adminService->getUserListWithStats('2026-07', null, '0');
|
||||
$users2 = collect($stats2['users']->items());
|
||||
$this->assertTrue($users2->contains('msnv', 'DEV_DISABLED'));
|
||||
$this->assertFalse($users2->contains('msnv', 'DEV_ENABLED'));
|
||||
$this->assertTrue($users2->contains('msnv', 3002));
|
||||
$this->assertFalse($users2->contains('msnv', 3001));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user