Merge pull request 'refactor create, handle ticket' (#125) from vi.ticket-email into dev

Reviewed-on: #125
This commit is contained in:
joseph 2025-06-24 18:45:44 +10:00
commit a11f67f252
23 changed files with 1328 additions and 1049 deletions

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,7 @@ use Modules\Admin\app\Models\MonthlyTimekeeping;
use Modules\Admin\app\Models\Tracking;
use Maatwebsite\Excel\Facades\Excel;
use App\Exports\TimekeepingExport;
use Modules\Admin\app\Models\Ticket;
class TimekeepingController extends Controller
{
@ -177,78 +178,92 @@ class TimekeepingController extends Controller
$year = $request->year;
$note = Notes::find($id);
if ($note) {
$n_month = $note->n_month;
$n_year = $note->n_year;
if ($note->n_reason == "ONLEAVE") {
// Get note reason ONLEAVE by $n_month, $n_year not include $note->id & include $note->n_user_id
// $onleave = Notes::getNotesByMonthAndYearAndUserId($n_month, $n_year, $note->n_user_id, $note->id);
// Get note reason LEAVE_WITHOUT_PAY by $n_month, $n_year & include $note->n_user_id
$leaveWithoutPay = Notes::getNotesByMonthAndYearAndUserIdAndReason($n_month, $n_year, $note->n_user_id, 'LEAVE_WITHOUT_PAY');
if (count($leaveWithoutPay) > 0) {
$deletedValue = ($note->n_time_type === 'ALL') ? 1.0 : 0.5;
$needUpdate = $deletedValue;
// dd($needUpdate, $leaveWithoutPay);
foreach ($leaveWithoutPay as $lwNote) {
if ($needUpdate <= 0) break;
if ($lwNote->n_time_type === 'ALL') {
if ($needUpdate == 1.0) {
// Chuyển cả note ALL thành phép
$lwNote->update(['n_reason' => 'ONLEAVE']);
$needUpdate = 0;
break;
} else { // $needUpdate == 0.5
// Tách ALL thành 2 note S và C, chuyển S thành phép, C giữ không phép
Notes::create([
'n_user_id' => $lwNote->n_user_id,
'n_day' => $lwNote->n_day,
'n_month' => $lwNote->n_month,
'n_year' => $lwNote->n_year,
'n_time_type' => 'S',
'n_reason' => 'ONLEAVE',
'n_note' => $lwNote->n_note
]);
Notes::create([
'n_user_id' => $lwNote->n_user_id,
'n_day' => $lwNote->n_day,
'n_month' => $lwNote->n_month,
'n_year' => $lwNote->n_year,
'n_time_type' => 'C',
'n_reason' => 'LEAVE_WITHOUT_PAY',
'n_note' => $lwNote->n_note
]);
$lwNote->delete();
$needUpdate = 0;
break;
}
} else {
// Nếu $lwNote->n_time_type == 'S' hoặc 'C' => 0.5
if ($needUpdate == 1.0) {
// Chuyển cả note ALL thành phép
$lwNote->update(['n_reason' => 'ONLEAVE']);
$needUpdate -= 0.5;
} else { // $needUpdate == 0.5
// S hoặc C, chỉ cần chuyển đúng 0.5 ngày
$lwNote->update(['n_reason' => 'ONLEAVE']);
$needUpdate = 0;
break;
}
}
}
} else {
// Khi note phép và k tồn tại nghỉ không phép => phép + dồn cho tháng sau
}
}
$note->delete();
$this->createOrUpdateRecordForCurrentMonth($month, $year);
return response()->json(['message' => 'Delete success', 'status' => true]);
if (!$note) {
return response()->json(['message' => 'Note not found', 'status' => false]);
}
return response()->json(['message' => 'Delete fail', 'status' => false]);
$ticket = Ticket::find($note->ticket_id);
if (!$ticket) {
return response()->json(['message' => 'Ticket not found, can not delete note', 'status' => false]);
}
$admin = auth('admins')->user();
$ticket->updated_by = $admin->name;
$ticket->status = "REFUSED";
$ticket->save();
Notes::where('ticket_id', $ticket->id)->delete();
$this->createOrUpdateRecordForCurrentMonth($month, $year);
return response()->json(['message' => 'Delete success', 'status' => true]);
// $n_month = $note->n_month;
// $n_year = $note->n_year;
// if ($note->n_reason == "ONLEAVE") {
// // Get note reason ONLEAVE by $n_month, $n_year not include $note->id & include $note->n_user_id
// // $onleave = Notes::getNotesByMonthAndYearAndUserId($n_month, $n_year, $note->n_user_id, $note->id);
// // Get note reason LEAVE_WITHOUT_PAY by $n_month, $n_year & include $note->n_user_id
// $leaveWithoutPay = Notes::getNotesByMonthAndYearAndUserIdAndReason($n_month, $n_year, $note->n_user_id, 'LEAVE_WITHOUT_PAY');
// if (count($leaveWithoutPay) > 0) {
// $deletedValue = ($note->n_time_type === 'ALL') ? 1.0 : 0.5;
// $needUpdate = $deletedValue;
// // dd($needUpdate, $leaveWithoutPay);
// foreach ($leaveWithoutPay as $lwNote) {
// if ($needUpdate <= 0) break;
// if ($lwNote->n_time_type === 'ALL') {
// if ($needUpdate == 1.0) {
// // Chuyển cả note ALL thành phép
// $lwNote->update(['n_reason' => 'ONLEAVE']);
// $needUpdate = 0;
// break;
// } else { // $needUpdate == 0.5
// // Tách ALL thành 2 note S và C, chuyển S thành phép, C giữ không phép
// Notes::create([
// 'n_user_id' => $lwNote->n_user_id,
// 'n_day' => $lwNote->n_day,
// 'n_month' => $lwNote->n_month,
// 'n_year' => $lwNote->n_year,
// 'n_time_type' => 'S',
// 'n_reason' => 'ONLEAVE',
// 'n_note' => $lwNote->n_note
// ]);
// Notes::create([
// 'n_user_id' => $lwNote->n_user_id,
// 'n_day' => $lwNote->n_day,
// 'n_month' => $lwNote->n_month,
// 'n_year' => $lwNote->n_year,
// 'n_time_type' => 'C',
// 'n_reason' => 'LEAVE_WITHOUT_PAY',
// 'n_note' => $lwNote->n_note
// ]);
// $lwNote->delete();
// $needUpdate = 0;
// break;
// }
// } else {
// // Nếu $lwNote->n_time_type == 'S' hoặc 'C' => 0.5
// if ($needUpdate == 1.0) {
// // Chuyển cả note ALL thành phép
// $lwNote->update(['n_reason' => 'ONLEAVE']);
// $needUpdate -= 0.5;
// } else { // $needUpdate == 0.5
// // S hoặc C, chỉ cần chuyển đúng 0.5 ngày
// $lwNote->update(['n_reason' => 'ONLEAVE']);
// $needUpdate = 0;
// break;
// }
// }
// }
// } else {
// // Khi note phép và k tồn tại nghỉ không phép => phép + dồn cho tháng sau
// }
// }
// $note->delete();
// $this->createOrUpdateRecordForCurrentMonth($month, $year);
// return response()->json(['message' => 'Delete success', 'status' => true]);
}
public function export(Request $request)

View File

@ -164,6 +164,7 @@ Route::middleware('api')
], function () {
Route::get('/all', [TicketController::class, 'getAll'])->middleware('check.permission:admin.hr');
Route::get('/getByUserId', [TicketController::class, 'getByUserId'])->middleware('check.permission:admin.hr.staff');
Route::post('/update', [TicketController::class, 'updateTicket'])->middleware('check.permission:admin.hr');
Route::post('/create', [TicketController::class, 'createTicket'])->middleware('check.permission:admin.hr.staff');
Route::get('/delete', [TicketController::class, 'deleteTicket'])->middleware('check.permission:admin.hr.staff');
Route::post('/handle-ticket', [TicketController::class, 'handleTicket'])->middleware('check.permission:admin');

View File

@ -3,13 +3,15 @@
namespace Modules\Auth\app\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\LeaveDays;
use App\Traits\IsAPI;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Modules\Auth\app\Models\User;
use Illuminate\Support\Str;
use Modules\Admin\app\Models\Category;
use SimpleSoftwareIO\QrCode\Facades\QrCode;
class UserController extends Controller
@ -34,9 +36,36 @@ class UserController extends Controller
]);
if ($request->has('id')) {
$payload = $request->only(['name', 'email', 'permission']);
$payload = $request->only(['name', 'email', 'permission', 'is_permanent']);
$user = User::find($request->id);
// Không cho chuyển từ chính thức thành lại thử việc
if (!$request->is_permanent && $user->is_permanent) {
return response()->json(['status' => false, 'message' => 'You cannot change an employee from permanent to probationary.']);
}
// Thêm ngày phép khi thành nhân viên chính thức
if ($request->is_permanent && !$user->is_permanent) {
$userLeaveDay = LeaveDays::where('ld_user_id', $user->id)
->where('ld_year', Carbon::now()->year)
->first();
if ($userLeaveDay) {
$permanentCategory = Category::where('c_type', 'PERMANENT_ONLEAVE')->where('c_code', "PERMANENT")->first();
$permanentDefault = (int) $permanentCategory->c_value; // Ngày phép khi thành nv chính thức
$userLeaveDay->ld_day_total = $permanentDefault;
$newNote = "Cộng ngày phép cho nhân viên chính thức"; // Thêm ghi chú
if (!empty($userLeaveDay->ld_note)) {
$userLeaveDay->ld_note = $userLeaveDay->ld_note . "\n" . $newNote;
} else {
$userLeaveDay->ld_note = $newNote;
}
$userLeaveDay->save();
}
}
$payload['permanent_date'] = Carbon::now()->toDateString();
$user->update($payload);
return response()->json(['data' => $user, 'status' => true, 'message' => 'Update successful']);
} else {
@ -44,7 +73,19 @@ class UserController extends Controller
'name' => $request->name,
'email' => $request->email,
'password' => bcrypt('Work@1234'),
'permission' => $request->permission
'permission' => $request->permission,
'is_permanent' => false
]);
// Khởi tạo LeaveDays cho nhân viên mới
LeaveDays::insert([
'ld_user_id' => $user->id,
'ld_day_total' => 0,
'ld_year' => Carbon::now()->year,
'ld_additional_day' => 0,
'ld_note' => '',
'created_at' => now(),
'updated_at' => now(),
]);
$user_res = [
@ -98,8 +139,6 @@ class UserController extends Controller
return response()->json(['data' => ['user' => $user_res, 'gitea' => "dev", 'zulip' => "dev"], 'status' => true, 'message' => 'Create successful']);
}
}
return response()->json(['status' => false, 'message' => 'Process fail']);
}
public function delete(Request $request)

View File

@ -25,7 +25,9 @@ class User extends Authenticatable implements JWTSubject
'name',
'email',
'password',
'permission'
'permission',
'is_permanent',
'permanent_date'
];
/**

View File

@ -25,7 +25,7 @@ class Kernel extends ConsoleKernel
// ->dailyAt('18:00');
// Chạy command vào ngày 31/12 lúc 23:59:59 mỗi năm
// $schedule->command('initialize:leavedays')->yearlyOn(12, 31, '23:59:59');
$schedule->command('initialize:leavedays')->yearlyOn(12, 31, '23:59:59');
$schedule->command('leave:deduct')->yearlyOn(3, 31, '23:59:59');
// Chạy buổi sáng lúc 12:00

View File

@ -10,6 +10,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Modules\Admin\app\Models\Category;
class AddMonthlyLeaveDays implements ShouldQueue
{
@ -18,6 +19,8 @@ class AddMonthlyLeaveDays implements ShouldQueue
protected $month;
protected $year;
private const ONLEAVE_PER_MONTH = 1; // Ngày phép cộng mỗi tháng
public function __construct($month = null, $year = null)
{
$this->month = $month ?? Carbon::now()->month;
@ -29,6 +32,11 @@ class AddMonthlyLeaveDays implements ShouldQueue
$users = User::get();
foreach ($users as $user) {
// Nếu là nhân viên chưa chính thức, ko cộng phép
if (!$user->is_permanent) {
continue;
}
$leaveDay = LeaveDays::where('ld_user_id', $user->id)
->where('ld_year', $this->year)
->first();
@ -46,11 +54,36 @@ class AddMonthlyLeaveDays implements ShouldQueue
]);
$leaveDay->save();
} else {
// Kiểm tra nếu số ngày phép hiện tại nhỏ hơn tháng hiện tại
// Check có phải là nhân viên chính thức trong năm nay (Nhân viên mới)
if ($user->permanent_date && $user->permanent_date !== '0000-00-00') {
$permenantYear = Carbon::parse($user->permanent_date)->year;
if ($permenantYear === $this->year) {
$permanentCategory = Category::where('c_type', 'PERMANENT_ONLEAVE')->where('c_code', "PERMANENT")->first();
$permanentDefault = (int) $permanentCategory->c_value; // Ngày phép khi thành nv chính thức
$permanentMonth = Carbon::parse($user->permanent_date)->month;
if ($this->month > $leaveDay->ld_day_total - ($permanentDefault - $permanentMonth)) {
$leaveDay->ld_day_total += self::ONLEAVE_PER_MONTH;
// Xử lý ghi chú
$newNote = "Cập nhật ngày phép đến tháng " . $this->month;
if (!empty($leaveDay->ld_note)) {
// Nếu đã có ghi chú, thêm ghi chú mới vào và xuống dòng
$leaveDay->ld_note = $leaveDay->ld_note . "\n" . $newNote;
} else {
// Nếu chưa có ghi chú, gán ghi chú mới
$leaveDay->ld_note = $newNote;
}
$leaveDay->save();
}
}
}
// Kiểm tra nếu số ngày phép hiện tại nhỏ hơn tháng hiện tại (Nhân viên cũ)
if ($leaveDay->ld_day_total < $this->month) {
// Cập nhật số ngày phép bằng với tháng hiện tại
$oldDays = $leaveDay->ld_day_total;
$leaveDay->ld_day_total = $this->month;
// Cộng mỗi tháng 1 ngày phép cho nhân viên
$leaveDay->ld_day_total += self::ONLEAVE_PER_MONTH;
// Xử lý ghi chú
$newNote = "Cập nhật ngày phép đến tháng " . $this->month;

View File

@ -10,7 +10,6 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class DeductLeaveDays implements ShouldQueue
@ -42,38 +41,19 @@ class DeductLeaveDays implements ShouldQueue
continue;
}
$totalLeaveDaysByMonth = Notes::join('categories', function ($join) {
// Lấy tổng ngày nghỉ phép 3 tháng đầu trong năm
$usedOnleaveDaysTotal = Notes::join('categories', function ($join) {
$join->on('notes.n_time_type', '=', 'categories.c_code')
->where('categories.c_type', 'TIME_TYPE');
})
->select(
DB::raw('notes.n_user_id as n_user_id'),
DB::raw('notes.n_year as year'),
DB::raw('SUM(categories.c_value) as leave_days')
)
->where('notes.n_year', $this->year)
->where('notes.n_user_id', $user->id)
->where('notes.n_reason', 'ONLEAVE')
->groupBy(DB::raw('notes.n_year'))
->first();
if ($totalLeaveDaysByMonth) {
//Nếu ngày phép thừa năm trước chưa sử dụng hết => cập nhật lại ngày đó (Ngày tồn đọng - ngày sử dụng)
if ($existingData->ld_additional_day > $totalLeaveDaysByMonth->leave_days) {
LeaveDays::where('ld_year', $this->year)
->where('ld_user_id', $user->id)
->update([
'ld_additional_day' => $totalLeaveDaysByMonth->leave_days,
]);
}
} else {
//Nếu không sử dụng ngày nghỉ còn lại ở năm rồi thì xóa => theo luật ld
LeaveDays::where('ld_year', $this->year)
->where('ld_user_id', $user->id)
->update([
'ld_additional_day' => "0",
]);
}
->where('n_user_id', $user->id)
->where('n_year', $this->year)
->where('n_month', "<=", 3)
->where('n_reason', 'ONLEAVE')
->sum('categories.c_value');
$existingData->ld_additional_day = $usedOnleaveDaysTotal ?? 0;
$existingData->save();
}
}
}

View File

@ -34,7 +34,8 @@ class InitializeLeaveDays implements ShouldQueue
public function handle(): void
{
$users = User::get();
$ld_day_total = 12;
$ld_day_total = Carbon::now()->month; // Khởi tạo phép hiện có bằng tháng hiện tại
foreach ($users as $user) {
// Kiểm tra xem dữ liệu của user này đã tồn tại cho năm hiện tại chưa
$existingData = LeaveDays::where('ld_user_id', $user->id)
@ -82,7 +83,7 @@ class InitializeLeaveDays implements ShouldQueue
// Tạo dữ liệu cho năm hiện tại
LeaveDays::insert([
'ld_user_id' => $user->id,
'ld_day_total' => $ld_day_total,
'ld_day_total' => $user->is_permanent ? $ld_day_total : 0, // Nếu là nhân viên mới, ko cấp phép
'ld_year' => $this->year,
'ld_additional_day' => $ld_additional_day,
'ld_note' => $ld_note,

View File

@ -18,6 +18,7 @@ class Notes extends Model
'n_time_type',
'n_reason',
'n_note',
'ticket_id'
];
/**

View File

@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('notes', function (Blueprint $table) {
$table->foreignId('ticket_id')
->nullable()
->constrained('tickets')
->onDelete('cascade');
});
}
public function down(): void
{
Schema::table('notes', function (Blueprint $table) {
$table->dropForeign(['ticket_id']);
$table->dropColumn('ticket_id');
});
}
};

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_permanent')->default(true);
$table->date('permanent_date');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_permanent');
$table->dropColumn('permanent_date');
});
}
};

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
DB::table('categories')->insert([
[
'c_code' => 'PERMANENT',
'c_name' => 'Phép cộng nhân viên chính thức',
'c_type' => 'PERMANENT_ONLEAVE',
'c_value' => 3,
'c_active' => 1,
'created_at' => now(),
'updated_at' => now(),
],
]);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
DB::table('categories')->where('c_code', 'PERMANENT')->delete();
}
};

View File

@ -1,4 +1,3 @@
<!DOCTYPE html>
<html lang="en">
@ -37,199 +36,167 @@
<title>{{ $data['subject'] }}</title>
</head>
<body>
<body style="
<body style="
font-family: Arial, Helvetica, sans-serif;
background-color: #edf2f7;
">
<table style="margin: 0 auto">
<tr>
<td>
<table
style="
<table style="margin: 0 auto">
<tr>
<td>
<table
style="
margin: 0 auto;
width: 768px;
">
<tr>
<td align="center" valign="top" style="padding: 36px 24px;">
<a href="{{ config('app.url') }}" target="_blank" style="display: inline-block;">
<img src="https://apactech.io/wp-content/uploads/2022/12/APAC-TECH_side-e1670975093601-190x78.png"
alt="Logo" border="0" width="100"
style="display: block; width: 100px; max-width: 100px; min-width: 48px;">
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table
style="
<tr>
<td align="center" valign="top" style="padding: 36px 24px;">
<a href="{{ config('app.url') }}" target="_blank" style="display: inline-block;">
<img src="https://apactech.io/wp-content/uploads/2022/12/APAC-TECH_side-e1670975093601-190x78.png"
alt="Logo" border="0" width="100"
style="display: block; width: 100px; max-width: 100px; min-width: 48px;">
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table
style="
margin: 0 auto;
background-color: #ffffff;
width: 768px;
padding: 24px;
">
<tr>
<td>
<h3 style="color: #222222; margin: 5px 0 0 0; font-weight: bold">
Dear Admin,
</h3>
</td>
</tr>
<tr>
<td>
<p style=" white-space:pre-line; margin: 0; margin-bottom: 5px">
Employee <span style="color: #222222;font-weight: bold;">{{ $data['name'] }}</span> has sent a request ticket, the specific content is as follows:
</p>
</td>
</tr>
<tr>
<td>
<div style="padding-left: 10px;color: #696969; margin-bottom: 15px">
<p style="padding: 3px;">Name: <span style="color: #222222;font-weight: bold;">{{ $data['name'] }}</span></p>
<p style="padding: 3px;">Date: <span style="color: #222222;font-weight: bold;">{{ $data['date'] }}</span></p>
<p style="padding: 3px;">Type: <span style="color: #222222;font-weight: bold;">{{ $data['type'] }}</span></p>
<p style="padding: 3px;">Note: <span style="color: #222222;font-weight: bold;">{{ $data['note'] }}</span></p>
</div>
</td>
</tr>
<tr>
<td>
<h3 style="color: #222222; margin: 5px 0 0 0; font-weight: bold">
Dear Admin,
</h3>
</td>
</tr>
<tr>
<td>
<p
style="
margin: 0 0 16px;
padding: 5px;
margin: 5px;
text-align: center;
"
>
<a
href="{{ config('app.client_url') . $data['link'] }}"
style="
color: #fff;
border-radius: 10px;
background-color: rgba(68, 115, 196);
background-image: linear-gradient(
to top left,
rgba(0, 0, 0, 0.2),
rgba(0, 0, 0, 0.2) 30%,
rgba(0, 0, 0, 0)
);
text-decoration: none;
display: inline-block;
font-weight: 600;
font-size: 16px;
line-height: 150%;
text-align: center;
margin: 0;
padding: 10px 12px;
"
>
Check now</a
>
<tr>
<td>
<p style=" white-space:pre-line; margin: 0; margin-bottom: 5px">
Employee <span style="color: #222222;font-weight: bold;">{{ $data['name'] }}</span> has sent a <a href="{{ config('app.client_url') . $data['link'] }}"> request ticket</a>, the specific content is as follows:
</p>
</td>
</tr>
<tr>
<td>
<div style="padding-left: 10px;color: #696969; margin-bottom: 15px">
<p style="padding: 3px;">Name: <span style="color: #222222;font-weight: bold;">{{ $data['name'] }}</span></p>
<p style="padding: 3px;">Date: <span style="color: #222222;font-weight: bold;">{{ $data['date'] }}</span></p>
<p style="padding: 3px;">Type: <span style="color: #222222;font-weight: bold;">{{ $data['type'] }}</span></p>
<p style="padding: 3px;">Note: <span style="color: #222222;font-weight: bold;">{{ $data['note'] }}</span></p>
</div>
</td>
</tr>
<tr>
<td>
<p style="text-align: center">
Or you can quick
You can quick
<span style="font-weight: bold">Confirm</span> or
<span style="font-weight: bold">Refuse</span> here:
</p>
<div
style="
display: flex;
justify-content: center;
gap: 10px;
margin-top: 10px;
"
>
display: flex;
justify-content: center;
gap: 10px;
margin-top: 10px;
">
<a
href="{{ route('email.ticket.handle', ['ticket_id' => $data['ticket_id'], 'action' => 'confirm', 'admin_email' => $data['admin_email']]) }}"
style="
color: #fff;
border-radius: 10px;
background-color: #12b886;
background-image: linear-gradient(
to top left,
rgba(0, 0, 0, 0.2),
rgba(0, 0, 0, 0.2) 30%,
rgba(0, 0, 0, 0)
);
text-decoration: none;
display: inline-block;
font-weight: 600;
font-size: 16px;
line-height: 150%;
text-align: center;
margin: 0;
padding: 10px 12px;
"
>
Confirm</a
>
color: #fff;
border-radius: 10px;
background-color: #12b886;
background-image: linear-gradient(
to top left,
rgba(0, 0, 0, 0.2),
rgba(0, 0, 0, 0.2) 30%,
rgba(0, 0, 0, 0)
);
text-decoration: none;
display: inline-block;
font-weight: 600;
font-size: 16px;
line-height: 150%;
text-align: center;
margin: 0;
padding: 10px 12px;
">
Confirm</a>
<a
href="{{ route('email.ticket.handle', ['ticket_id' => $data['ticket_id'], 'action' => 'refuse', 'admin_email' => $data['admin_email']]) }}"
style="
color: #fff;
border-radius: 10px;
background-color: #f03e3e;
background-image: linear-gradient(
to top left,
rgba(0, 0, 0, 0.2),
rgba(0, 0, 0, 0.2) 30%,
rgba(0, 0, 0, 0)
);
text-decoration: none;
display: inline-block;
font-weight: 600;
font-size: 16px;
line-height: 150%;
text-align: center;
margin: 0;
padding: 10px 12px;
"
>
Refuse</a
>
color: #fff;
border-radius: 10px;
background-color: #f03e3e;
background-image: linear-gradient(
to top left,
rgba(0, 0, 0, 0.2),
rgba(0, 0, 0, 0.2) 30%,
rgba(0, 0, 0, 0)
);
text-decoration: none;
display: inline-block;
font-weight: 600;
font-size: 16px;
line-height: 150%;
text-align: center;
margin: 0;
padding: 10px 12px;
">
Refuse</a>
</div>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table
style="
</td>
</tr>
<tr>
<td style="color: #222222;">
<div style="margin-top: 3rem">
<p><span style="font-weight: bold">Note</span>: If you are redirected to a <span style="font-weight: bold">404 page</span>, it means:</p>
<p>1. The ticket has already been approved by another admin.</p>
<p>2. The ticket has been deleted.</p>
</div>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table
style="
margin: 0 auto;
width: 768px;
">
<tr>
<td>
<h5
style="
<tr>
<td>
<h5
style="
color: #222222;
text-align: center;
padding: 10px 36px;
margin: 0;
">
<p>© 2024 APAC Tech.</p>
</h5>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
<p>© 2024 APAC Tech.</p>
</h5>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View File

@ -48,6 +48,7 @@ export const getTickets = API_URL + 'v1/admin/ticket/all'
export const getTicketsOfUser = API_URL + 'v1/admin/ticket/getByUserId'
export const deleteTicket = API_URL + 'v1/admin/ticket/delete'
export const addTicket = API_URL + 'v1/admin/ticket/create'
export const updateTicket = API_URL + 'v1/admin/ticket/update'
export const handleTicket = API_URL + 'v1/admin/ticket/handle-ticket'
//Users

View File

@ -1,5 +1,42 @@
export const PageNotFound = () => {
return <>{'Not found!'}</>
import {
Box,
Button,
Container,
Image,
SimpleGrid,
Text,
Title,
} from '@mantine/core'
import image404 from '../../../public/404Image.jpg'
import { useNavigate } from 'react-router-dom'
const PageNotFound = () => {
const navigate = useNavigate()
return (
<Container mt="lg">
<SimpleGrid spacing={{ base: 40, sm: 80 }} cols={{ base: 1, sm: 2 }}>
<Box py="3rem">
<Title> Something is not right...</Title>
<Text c="dimmed" size="lg">
Page you are trying to open does not exist. You may have mistyped
the address, or the page has been moved to another URL. If you think
this is an error contact support.
</Text>
<Button
variant="outline"
size="md"
mt="xl"
onClick={() => navigate('/')}
>
Get back to home page
</Button>
</Box>
<Image src={image404} />
</SimpleGrid>
</Container>
)
}
export default PageNotFound

View File

@ -45,4 +45,38 @@
.dialogText {
color: light-dark(#2d353c, white);
}
}
/* Thêm styles cho Modal xác nhận xóa */
.deleteModal {
background-color: light-dark(white, #2d353c);
text-align: center;
border: solid 1px #ff4646;
}
.deleteModalTitle {
color: #ff4646;
font-weight: 600;
font-size: 1.2rem;
margin-bottom: 1rem;
}
.deleteModalContent {
color: light-dark(#2d353c, white);
margin-bottom: 1.5rem;
}
.deleteModalFooter {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 1rem;
}
.deleteButton {
background-color: #ff4646;
}
.deleteButton:hover {
background-color: #ff6b6b;
}

View File

@ -1,6 +1,11 @@
import { getListMaster, getTickets, handleTicket } from '@/api/Admin'
import {
getListMaster,
getTickets,
handleTicket,
updateTicket,
} from '@/api/Admin'
import { DataTablePagination } from '@/components/DataTable/DataTable'
import { create } from '@/rtk/helpers/CRUD'
import { create, update } from '@/rtk/helpers/CRUD'
import { get } from '@/rtk/helpers/apiService'
import {
Badge,
@ -9,12 +14,13 @@ import {
HoverCard,
Modal,
Select,
Switch,
Text,
Textarea,
} from '@mantine/core'
import { useForm } from '@mantine/form'
import { notifications } from '@mantine/notifications'
import { IconCheckbox, IconSquareXFilled } from '@tabler/icons-react'
import { IconCheckbox, IconEdit, IconSquareXFilled } from '@tabler/icons-react'
import moment from 'moment'
import { useEffect, useState } from 'react'
import classes from './TicketsManagement.module.css'
@ -24,6 +30,7 @@ type TTickets = {
ticket_id: number
admin_note: string
action: string
status: string
}
type TListTickets = {
@ -78,6 +85,7 @@ const TicketsManagement = () => {
end_period: '',
reason: '',
type: '',
status: '',
})
const [disableBtn, setDisableBtn] = useState(false)
const [filter, setFilter] = useState({
@ -86,6 +94,7 @@ const TicketsManagement = () => {
})
const [dataTimeType, setDataTimeType] = useState<DataTimeType[]>([])
const [dataReason, setDataReason] = useState<DataReason[]>([])
const [isRefuseConfirmOpen, setIsRefuseConfirmOpen] = useState<boolean>(false)
const getListMasterByType = async (type: string) => {
try {
@ -156,7 +165,7 @@ const TicketsManagement = () => {
},
{
name: 'reason',
size: '15%',
size: '10%',
header: 'Notes',
render: (row: any) => {
return (
@ -198,7 +207,7 @@ const TicketsManagement = () => {
},
{
name: 'admin_note',
size: '15%',
size: '10%',
header: 'Admin Notes',
render: (row: any) => {
return (
@ -226,6 +235,18 @@ const TicketsManagement = () => {
size: '10%',
header: 'Updated By',
},
{
name: 'updated_at',
size: '10%',
header: 'Updated At',
render: (row: any) => {
if (row?.updated_at) {
return (
<Box>{moment(row?.updated_at).format('HH:mm:ss DD/MM/YYYY')}</Box>
)
}
},
},
{
name: '#',
size: '5%',
@ -254,7 +275,23 @@ const TicketsManagement = () => {
height={20}
/>
</Box>
) : null
) : (
<Box className={classes.optionIcon}>
<IconEdit
className={classes.editIcon}
onClick={() => {
setAction('update')
setItem(row)
form.reset()
console.log(row)
form.setFieldValue('status', row.status)
form.setFieldValue('admin_note', row.admin_note)
}}
width={20}
height={20}
/>
</Box>
)
},
},
]
@ -347,6 +384,27 @@ const TicketsManagement = () => {
}
}
const handleUpdate = async (values: TTickets) => {
try {
const res = await update(
updateTicket + `?ticket_id=${item?.id}`,
{
ticket_id: item.id,
admin_note: values.admin_note,
status: values.status,
},
getAllTickets,
)
if (res === true) {
setAction('')
setIsRefuseConfirmOpen(false)
form.reset()
}
} catch (error) {
console.log(error)
}
}
useEffect(() => {
getAllTickets()
}, [filter])
@ -356,6 +414,7 @@ const TicketsManagement = () => {
ticket_id: 0,
action: '',
admin_note: '',
status: '',
},
})
@ -416,21 +475,38 @@ const TicketsManagement = () => {
</Box>
<Modal
opened={action === 'confirm' || action === 'refuse'}
opened={
action === 'confirm' || action === 'refuse' || action === 'update'
}
onClose={() => {
setAction('')
setIsRefuseConfirmOpen(false)
form.reset()
}}
title={
<Text pl={'sm'} fw={700} fz={'lg'}>
{action === 'confirm' ? 'Confirm Ticket' : 'Refuse Ticket'}
{action === 'confirm'
? 'Confirm Ticket'
: action === 'refuse'
? 'Refuse Ticket'
: 'Update Ticket'}
</Text>
}
>
<form
onSubmit={form.onSubmit(async (values) => {
setDisableBtn(true)
await handleSave(values)
if (action === 'update') {
if (values.status === 'REFUSED') {
setIsRefuseConfirmOpen(true)
} else {
await handleUpdate(values)
}
} else {
await handleSave(values)
}
setDisableBtn(false)
})}
>
@ -509,14 +585,39 @@ const TicketsManagement = () => {
<Textarea
label="Admin Notes"
// required
value={form.values.admin_note}
onChange={(e) => form.setFieldValue('admin_note', e.target.value)}
/>
{action === 'update' && item.status !== 'REFUSED' ? (
<Switch
mt="md"
color="red"
label={
<Text size="sm">
Change status to{' '}
<Text span color="red" fw="bold">
Refuse
</Text>
</Text>
}
style={{ width: 'fit-content' }}
checked={form.values.status === 'REFUSED'}
onChange={(event) =>
form.setFieldValue(
'status',
event.currentTarget.checked ? 'REFUSED' : 'CONFIRMED',
)
}
/>
) : (
''
)}
<Box ta={'center'}>
<Button
mt={'lg'}
bg={'green'}
bg={action === 'update' ? 'blue' : 'green'}
type="submit"
disabled={disableBtn}
>
@ -526,6 +627,50 @@ const TicketsManagement = () => {
</Box>
</form>
</Modal>
<Modal
opened={isRefuseConfirmOpen}
onClose={() => {
setIsRefuseConfirmOpen(false)
}}
centered
size="sm"
classNames={{
content: classes.deleteModal,
}}
>
<Text className={classes.deleteModalTitle}>Confirm Update</Text>
<Text className={classes.deleteModalContent}>
Changing ticket status to <strong>Refused</strong> will also delete
all related notes.
</Text>
<Text className={classes.deleteModalContent}>
Are you sure you want to proceed?
</Text>
<Box className={classes.deleteModalFooter}>
<Button
variant="outline"
onClick={() => {
setIsRefuseConfirmOpen(false)
}}
disabled={disableBtn}
>
Cancel
</Button>
<Button
className={classes.deleteButton}
onClick={async () => {
setDisableBtn(true)
await handleUpdate(form.values)
setDisableBtn(false)
}}
disabled={disableBtn}
>
Confirm
</Button>
</Box>
</Modal>
</div>
)
}

View File

@ -493,7 +493,11 @@ const Timekeeping = () => {
>
<Text className={classes.deleteModalTitle}>Confirm Delete</Text>
<Text className={classes.deleteModalContent}>
Are you sure you want to delete this note?
This action will change the ticket status to <strong>Refused</strong>{' '}
and delete all related notes.
</Text>
<Text className={classes.deleteModalContent}>
Are you sure you want to proceed?
</Text>
<Box className={classes.deleteModalFooter}>
<Button

View File

@ -45,3 +45,37 @@
.dialogText {
color: light-dark(#2d353c, white);
}
/* Thêm styles cho Modal xác nhận xóa */
.deleteModal {
background-color: light-dark(white, #2d353c);
text-align: center;
border: solid 1px rgb(9, 132, 132);
}
.deleteModalTitle {
color: rgb(9, 132, 132);
font-weight: 600;
font-size: 1.2rem;
margin-bottom: 1rem;
}
.deleteModalContent {
color: light-dark(#2d353c, white);
margin-bottom: 1.5rem;
}
.deleteModalFooter {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 1rem;
}
.deleteButton {
background-color: rgb(9, 132, 132);
}
.deleteButton:hover {
background-color: rgb(9, 132, 132);
}

View File

@ -13,6 +13,7 @@ import {
Group,
Modal,
MultiSelect,
Switch,
Text,
TextInput,
} from '@mantine/core'
@ -24,28 +25,31 @@ const UsersManagement = () => {
const [users, setUsers] = useState<TUser[]>([])
const [action, setAction] = useState('')
const [activeBtn, setActiveBtn] = useState(false)
const [item, setItem] = useState({ id: 0 })
const [item, setItem] = useState({ id: 0, is_permanent: false })
const [disableBtn, setDisableBtn] = useState(false)
const [info, setInfo] = useState('')
const [isPermanentConfirmOpen, setIsPermanentConfirmOpen] =
useState<boolean>(false)
const columns = [
{
name: 'id',
size: '3%',
size: '5%',
header: 'ID',
},
{
name: 'name',
size: '17%',
size: '20%',
header: 'Name',
},
{
name: 'email',
size: '26%',
size: '25%',
header: 'Email',
},
{
name: 'permission',
size: '10%',
size: '20%',
header: 'Permission',
render: (row: TUser) => {
if (row.permission.includes(',')) {
@ -57,9 +61,21 @@ const UsersManagement = () => {
}
},
},
{
name: 'is_permanent',
size: '20%',
header: 'Employment Type',
render: (row: TUser) => {
return row.is_permanent ? (
<Badge color="teal">Permanent</Badge>
) : (
<Badge color="violet">Probation</Badge>
)
},
},
{
name: '#',
size: '5%',
size: '10%',
header: 'Action',
render: (row: TUser) => {
return (
@ -68,6 +84,8 @@ const UsersManagement = () => {
className={classes.editIcon}
onClick={() => {
setAction('edit')
setItem(row)
form.reset()
form.setValues(row)
}}
width={20}
@ -94,6 +112,7 @@ const UsersManagement = () => {
name: '',
email: '',
permission: '',
is_permanent: false,
},
})
@ -128,6 +147,7 @@ const UsersManagement = () => {
const res = await update(createOrUpdateUser, values, getAll)
if (res === true) {
setAction('')
setIsPermanentConfirmOpen(false)
form.reset()
}
} catch (error) {
@ -172,6 +192,7 @@ const UsersManagement = () => {
opened={action === 'add' || action === 'edit'}
onClose={() => {
setAction('')
setIsPermanentConfirmOpen(false)
form.reset()
}}
title={
@ -183,9 +204,15 @@ const UsersManagement = () => {
<form
onSubmit={form.onSubmit(async (values) => {
setDisableBtn(true)
action === 'edit'
? await handleUpdate(values)
: await handleCreate(values)
if (action === 'edit') {
if (values.is_permanent && !item.is_permanent) {
setIsPermanentConfirmOpen(true)
} else {
await handleUpdate(values)
}
} else {
await handleCreate(values)
}
setDisableBtn(false)
})}
>
@ -196,6 +223,7 @@ const UsersManagement = () => {
value={form.values.name}
error={form.errors.name}
onChange={(e) => form.setFieldValue('name', e.target.value)}
required
/>
<TextInput
@ -204,6 +232,7 @@ const UsersManagement = () => {
value={form.values.email}
error={form.errors.email}
onChange={(e) => form.setFieldValue('email', e.target.value)}
required
/>
<MultiSelect
@ -224,7 +253,25 @@ const UsersManagement = () => {
e!.filter((p) => p.trim() !== '').join(','),
)
}
mb={'md'}
/>
{action === 'edit' && !item.is_permanent ? (
<Switch
label="Permanent employee"
style={{ width: 'fit-content' }}
checked={form.values.is_permanent}
onChange={(event) =>
form.setFieldValue(
'is_permanent',
event.currentTarget.checked,
)
}
/>
) : (
''
)}
<Box ta={'center'}>
{action === 'add' ? (
<Button
@ -273,6 +320,50 @@ const UsersManagement = () => {
{info}
</Code>
</Modal>
{/* Confirm change to permanent employee */}
<Modal
opened={isPermanentConfirmOpen}
onClose={() => setIsPermanentConfirmOpen(false)}
centered
size="sm"
classNames={{
content: classes.deleteModal,
}}
>
<Text className={classes.deleteModalTitle}>Confirm Update</Text>
<Text className={classes.deleteModalContent}>
This action will change the employment type from{' '}
<strong>Probation</strong> to <strong>Permanent</strong>.
</Text>
<Text className={classes.deleteModalContent}>
Are you sure you want to proceed?
</Text>
<Box className={classes.deleteModalFooter}>
<Button
variant="outline"
onClick={() => {
setIsPermanentConfirmOpen(false)
}}
disabled={disableBtn}
>
Cancel
</Button>
<Button
className={classes.deleteButton}
onClick={async () => {
setDisableBtn(true)
await handleUpdate(form.values)
setDisableBtn(false)
}}
disabled={disableBtn}
>
Confirm
</Button>
</Box>
</Modal>
<Dialog
className={classes.dialog}
opened={action === 'delete'}

View File

@ -76,6 +76,7 @@ export type TUser = {
email: string
name: string
permission: string
is_permanent: boolean
}
export type DataReason = {