Compare commits
241 Commits
Sprint-4/M
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
f0bed712e6 | |
|
|
f1de8cb627 | |
|
|
b50dc96ce2 | |
|
|
744c206743 | |
|
|
a23f2155dc | |
|
|
25918fcb62 | |
|
|
607882343a | |
|
|
8cd93c578e | |
|
|
755297e5b0 | |
|
|
a3ec46db0a | |
|
|
ab7ccbe194 | |
|
|
4eb3dda0e1 | |
|
|
1cd6b3a108 | |
|
|
935e88bb72 | |
|
|
2675a60ffc | |
|
|
95e4e833b5 | |
|
|
8ad0ba9c25 | |
|
|
574e92bd23 | |
|
|
925823c525 | |
|
|
a594ba5ba2 | |
|
|
683f85efd4 | |
|
|
6a1e19ec7f | |
|
|
8e54ca0ceb | |
|
|
0f2d5db8a3 | |
|
|
980676fe16 | |
|
|
9d536955e4 | |
|
|
a5fa0cbcda | |
|
|
0a4332729d | |
|
|
2f1e26d3f6 | |
|
|
3d1b43e200 | |
|
|
8d86fc2182 | |
|
|
54b8b9737b | |
|
|
42c5791da8 | |
|
|
8d5199342d | |
|
|
beccf5796c | |
|
|
baa3216c69 | |
|
|
de351f0877 | |
|
|
2c94e91668 | |
|
|
b22c9b9cc5 | |
|
|
f494367683 | |
|
|
61124cd9bb | |
|
|
78066f624e | |
|
|
d7b7a61511 | |
|
|
8f30081e55 | |
|
|
a0bfb19a34 | |
|
|
7e369785d5 | |
|
|
54046ac676 | |
|
|
9f921750fc | |
|
|
bd0d4fa13b | |
|
|
f5362e54ef | |
|
|
17dc0c3374 | |
|
|
29dc787c65 | |
|
|
6ccf5e9e8a | |
|
|
5736166da3 | |
|
|
7be6a12edc | |
|
|
70436f737f | |
|
|
25162be83d | |
|
|
06027e8f23 | |
|
|
8c7a24d31c | |
|
|
1f08e2b503 | |
|
|
9d77dfc5d6 | |
|
|
e7aa80f14b | |
|
|
24bd605b7c | |
|
|
2a5f7b0b25 | |
|
|
9183540976 | |
|
|
cdce726ff1 | |
|
|
790cab255d | |
|
|
647fc455d5 | |
|
|
b5f5fa1748 | |
|
|
5a564e3703 | |
|
|
0af2357bc2 | |
|
|
a623659b52 | |
|
|
b35d4d7ca5 | |
|
|
522034ab76 | |
|
|
487f4227d8 | |
|
|
56d2889297 | |
|
|
7c36add579 | |
|
|
6c61819f2e | |
|
|
f649b2a6ac | |
|
|
d335803cf8 | |
|
|
da0bb9ad0d | |
|
|
d3d9acb1ce | |
|
|
a4751e9d38 | |
|
|
b52537cfe4 | |
|
|
efaf906f0a | |
|
|
3e865e5575 | |
|
|
4f6f3dd803 | |
|
|
ee281986b8 | |
|
|
6e1e452bf1 | |
|
|
6528b7bc59 | |
|
|
fd1be27414 | |
|
|
d064b242e6 | |
|
|
8f6977be72 | |
|
|
af67644416 | |
|
|
705e8f9216 | |
|
|
db4feedde3 | |
|
|
27b59ae939 | |
|
|
034b9eee2c | |
|
|
543b1af58c | |
|
|
d09fa6d432 | |
|
|
40f80579dc | |
|
|
b1e7aaa1ea | |
|
|
a883662f0b | |
|
|
a11f67f252 | |
|
|
fb6c58f1c0 | |
|
|
c2c9322e7d | |
|
|
3263bce784 | |
|
|
500d6ec34d | |
|
|
1b35f8481b | |
|
|
172af363de | |
|
|
cd9c01cdee | |
|
|
2c8bb92775 | |
|
|
5b65415d76 | |
|
|
655d9cd4c6 | |
|
|
e05e92b96b | |
|
|
1328320713 | |
|
|
7217a83e33 | |
|
|
8474bd97a1 | |
|
|
f8c2af2ec8 | |
|
|
5f8b03f558 | |
|
|
fa47d32b07 | |
|
|
160596a7a3 | |
|
|
177296a324 | |
|
|
d5936087ff | |
|
|
951c3be613 | |
|
|
b5a7740d9f | |
|
|
2011797dd6 | |
|
|
7dc31bf75b | |
|
|
ca766fc293 | |
|
|
32b197969c | |
|
|
462d896e57 | |
|
|
44fa6b55f7 | |
|
|
ddcb78ef98 | |
|
|
d61fd879b7 | |
|
|
d3c1d9bf60 | |
|
|
65c4dbcf88 | |
|
|
af5721682b | |
|
|
308f5a3a2e | |
|
|
05cb3f722a | |
|
|
1f9ab744ae | |
|
|
f807a635bb | |
|
|
d2c0e0b95d | |
|
|
4da48df8d7 | |
|
|
5cd7071459 | |
|
|
6e2a8c2578 | |
|
|
c6a9fc28a3 | |
|
|
db5c9b09a8 | |
|
|
9a9a94e5d4 | |
|
|
d803d69b72 | |
|
|
d1f889c996 | |
|
|
e174146678 | |
|
|
cd2570bc07 | |
|
|
230beb4ed7 | |
|
|
60925a83af | |
|
|
b6476feae5 | |
|
|
8b9e18bdcb | |
|
|
4896461838 | |
|
|
7ef2897469 | |
|
|
88870e075c | |
|
|
008c335b79 | |
|
|
650cfe1b13 | |
|
|
20745381db | |
|
|
8ce0d957b1 | |
|
|
a5ece4d65d | |
|
|
d8cbb7e01b | |
|
|
86f2bb12fc | |
|
|
78bdca4e23 | |
|
|
e026b70e66 | |
|
|
0c5f56d30c | |
|
|
2561d39b4a | |
|
|
12a1447931 | |
|
|
35ecaa802d | |
|
|
1018172b58 | |
|
|
0ebe4c772e | |
|
|
281c8ad98a | |
|
|
a2e36501bb | |
|
|
04ee5e4081 | |
|
|
15880f8bd1 | |
|
|
6c5b2d8412 | |
|
|
f5e4d7b405 | |
|
|
5cfd78d2c0 | |
|
|
2bda35d8d3 | |
|
|
be6d7be79e | |
|
|
f60be27969 | |
|
|
73edecf341 | |
|
|
13a477e883 | |
|
|
471cc0eb2a | |
|
|
1887f2805f | |
|
|
bb12c55ef6 | |
|
|
49b681566d | |
|
|
9b472544b6 | |
|
|
5cdfaafe03 | |
|
|
8107978a42 | |
|
|
846df81d44 | |
|
|
5165ded25e | |
|
|
7ce7251097 | |
|
|
f05334ea34 | |
|
|
4d94e26bb9 | |
|
|
fc6587bcff | |
|
|
174c8889ce | |
|
|
4fd18b988e | |
|
|
9532845bc3 | |
|
|
38780c56a8 | |
|
|
b908185658 | |
|
|
07d28e24cd | |
|
|
d80e7c0957 | |
|
|
eb5c6e655f | |
|
|
af52952946 | |
|
|
1443d01685 | |
|
|
a6b5d83535 | |
|
|
7fdf6f3e25 | |
|
|
824b2fd87c | |
|
|
cfba5175a6 | |
|
|
93030b73c7 | |
|
|
fcaa2926fd | |
|
|
0e875d7cf9 | |
|
|
14e9e17273 | |
|
|
f96f445c7f | |
|
|
1d9e428587 | |
|
|
a830791648 | |
|
|
8d8985a6be | |
|
|
d53b786daf | |
|
|
79b3cbe916 | |
|
|
2eed0f006b | |
|
|
e226ad41d7 | |
|
|
cc0a62f746 | |
|
|
4334f3e73d | |
|
|
1c16b4b164 | |
|
|
2c6a6a81dd | |
|
|
34b6698696 | |
|
|
7c8267d6e3 | |
|
|
ceb925f571 | |
|
|
bf5dc3d8c1 | |
|
|
46d4ffa339 | |
|
|
d0c1b6894b | |
|
|
7fa50478cc | |
|
|
0650211f67 | |
|
|
0b823a226c | |
|
|
2c2d067e4c | |
|
|
84d6ee2630 | |
|
|
6aff6093a6 |
|
|
@ -3,6 +3,7 @@
|
|||
namespace Modules\Admin\app\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\Admin\app\Models\Category;
|
||||
|
||||
|
|
@ -14,7 +15,7 @@ class CategoryController extends Controller
|
|||
* @param Request $request The HTTP request object.
|
||||
* @return \Illuminate\Http\JsonResponse The JSON response containing the list of master data.
|
||||
*/
|
||||
public function getListMaster(Request $request)
|
||||
public static function getListMaster(Request $request)
|
||||
{
|
||||
$data = Category::where('c_type', '=', $request->type)->where('c_active', '=', 1)->select('id', 'c_code', 'c_name', 'c_value', 'c_type')->get();
|
||||
return AbstractController::ResultSuccess($data);
|
||||
|
|
@ -24,4 +25,50 @@ class CategoryController extends Controller
|
|||
$data = Category::where('c_type', '=', $type)->where('c_code', '=', $code)->where('c_active', '=', 1)->select('id', 'c_code', 'c_name', 'c_value', 'c_type')->first();
|
||||
return $data;
|
||||
}
|
||||
public static function getListMasterByType($type)
|
||||
{
|
||||
$data = Category::where('c_type', '=', $type)->where('c_active', '=', 1)->select('id', 'c_code', 'c_name', 'c_value', 'c_type')->get();
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function workDays()
|
||||
{
|
||||
$saturday_work_schedules = Category::where('c_type', 'SATURDAY_WORK_SCHEDULE')
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'data' => $saturday_work_schedules,
|
||||
'status' => true
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateWorkDays(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'c_code' => 'required|date_format:d-m-Y',
|
||||
]);
|
||||
|
||||
$schedule = Category::where('c_type', 'SATURDAY_WORK_SCHEDULE')->first();
|
||||
|
||||
if (!$schedule) {
|
||||
$schedule = Category::create([
|
||||
'c_type' => 'SATURDAY_WORK_SCHEDULE',
|
||||
'c_name' => "Ngày bắt đầu làm việc thứ 7 trong năm",
|
||||
'c_code' => $request->c_code,
|
||||
'c_value' => Carbon::now()->year,
|
||||
'c_active' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
} else {
|
||||
$schedule->update([
|
||||
'c_code' => $request->c_code,
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => true,
|
||||
'message' => 'Saturday work schedule updated successfully'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,161 @@
|
|||
<?php
|
||||
|
||||
namespace Modules\Admin\app\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Traits\HasFilterRequest;
|
||||
use App\Traits\HasOrderByRequest;
|
||||
use App\Traits\HasSearchRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Admin\app\Models\Document;
|
||||
|
||||
class DocumentController extends Controller
|
||||
{
|
||||
use HasOrderByRequest;
|
||||
use HasFilterRequest;
|
||||
use HasSearchRequest;
|
||||
|
||||
public function all(Request $request)
|
||||
{
|
||||
$documents = new Document;
|
||||
|
||||
// Order by
|
||||
$this->orderByRequest($documents, $request);
|
||||
$documents->orderBy('title', 'asc');
|
||||
// Filter
|
||||
$this->filterRequest(
|
||||
builder: $documents,
|
||||
request: $request,
|
||||
filterKeys: [
|
||||
'title' => self::F_TEXT,
|
||||
]
|
||||
);
|
||||
$this->searchRequest(
|
||||
builder: $documents,
|
||||
value: $request->get('search'),
|
||||
fields: [
|
||||
'title',
|
||||
]
|
||||
);
|
||||
|
||||
$responseData = $documents->get();
|
||||
return AbstractController::ResultSuccess($responseData);
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'type' => 'required|in:file,link',
|
||||
'files' => 'nullable|array',
|
||||
'files.*.title' => 'required|string|max:255',
|
||||
'files.*.file' => 'required|file|mimes:doc,docx,xls,xlsx,pdf|max:20480',
|
||||
'links' => 'nullable|array',
|
||||
'links.*.title' => 'required|string|max:255',
|
||||
'links.*.uri' => 'required|string|url',
|
||||
'is_active' => 'required|boolean',
|
||||
]);
|
||||
|
||||
$documents = [];
|
||||
|
||||
if ($request->type === "file") {
|
||||
foreach ($request->file('files') as $index => $file) {
|
||||
$path = $file['file']->store('uploads', 'public');
|
||||
$documents[] = [
|
||||
'title' => $request->input("files.$index.title"),
|
||||
'type' => 'file',
|
||||
'uri' => "storage/{$path}",
|
||||
'is_active' => $request->is_active,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
} else {
|
||||
foreach ($request->links as $link) {
|
||||
$documents[] = [
|
||||
'title' => $link['title'],
|
||||
'type' => 'link',
|
||||
'uri' => $link['uri'],
|
||||
'is_active' => $request->is_active,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($documents)) {
|
||||
Document::insert($documents);
|
||||
}
|
||||
|
||||
return AbstractController::ResultSuccess($documents, "Documents created successfully!");
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'required|exists:documents,id',
|
||||
'title' => 'required|string|max:255',
|
||||
'type' => 'required|in:file,link',
|
||||
'uri' => 'nullable|url',
|
||||
'file' => 'nullable|file|mimes:doc,docx,xls,xlsx,pdf|max:20480',
|
||||
'existing_file' => 'nullable|string',
|
||||
'is_active' => 'required|boolean',
|
||||
]);
|
||||
|
||||
$document = Document::find($request->input('id'));
|
||||
if (!$document) {
|
||||
return AbstractController::ResultError("Document not found.");
|
||||
}
|
||||
|
||||
if ($request->type === "file") {
|
||||
$uri = $request->existing_file;
|
||||
|
||||
if ($request->hasFile('file')) {
|
||||
$filePath = str_replace('storage/', 'public/', $request->existing_file);
|
||||
if (Storage::exists($filePath)) {
|
||||
Storage::delete($filePath);
|
||||
}
|
||||
|
||||
$path = $request->file('file')->store('uploads', 'public');
|
||||
$uri = "storage/{$path}";
|
||||
}
|
||||
|
||||
$document->update([
|
||||
'title' => $request->title,
|
||||
'type' => $request->type,
|
||||
'uri' => $uri,
|
||||
'is_active' => $request->is_active,
|
||||
]);
|
||||
|
||||
return AbstractController::ResultSuccess($document, "Document updated successfully!");
|
||||
}
|
||||
|
||||
$document->update([
|
||||
'title' => $request->title,
|
||||
'type' => $request->type,
|
||||
'uri' => $request->uri,
|
||||
'is_active' => $request->is_active,
|
||||
]);
|
||||
|
||||
return AbstractController::ResultSuccess($document, "Document updated successfully!");
|
||||
}
|
||||
|
||||
public function delete(Request $request)
|
||||
{
|
||||
$id = $request->input('id');
|
||||
$document = Document::find($id);
|
||||
if (!$document) {
|
||||
return AbstractController::ResultError("Document not found");
|
||||
}
|
||||
|
||||
if ($document->type === "file") {
|
||||
$filePath = str_replace('storage/', 'public/', $document->uri);
|
||||
if (Storage::exists($filePath)) {
|
||||
Storage::delete($filePath);
|
||||
}
|
||||
}
|
||||
|
||||
$document->delete();
|
||||
return AbstractController::ResultSuccess("Document deleted successfully!");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,359 @@
|
|||
<?php
|
||||
|
||||
namespace Modules\Admin\app\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Traits\HasFilterRequest;
|
||||
use App\Traits\HasOrderByRequest;
|
||||
use App\Traits\HasSearchRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\Admin\app\Models\ElectricityBill;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ElectricityBillController extends Controller
|
||||
{
|
||||
use HasOrderByRequest;
|
||||
use HasFilterRequest;
|
||||
use HasSearchRequest;
|
||||
|
||||
/**
|
||||
* Get all electricity bills with pagination
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
try {
|
||||
$bills = new ElectricityBill;
|
||||
|
||||
// Order by
|
||||
$this->orderByRequest($bills, $request);
|
||||
|
||||
// Filter
|
||||
$this->filterRequest(
|
||||
builder: $bills,
|
||||
request: $request,
|
||||
filterKeys: [
|
||||
'billing_date' => self::F_TEXT,
|
||||
]
|
||||
);
|
||||
|
||||
// Search
|
||||
$this->searchRequest(
|
||||
builder: $bills,
|
||||
value: $request->get('search'),
|
||||
fields: ['billing_date', 'notes']
|
||||
);
|
||||
|
||||
$responseData = $bills
|
||||
->leftJoin('users as creator', 'electricity_bills.created_by', '=', 'creator.id')
|
||||
->leftJoin('users as updater', 'electricity_bills.updated_by', '=', 'updater.id')
|
||||
->orderBy('electricity_bills.billing_date', 'desc')
|
||||
->select(
|
||||
'electricity_bills.*',
|
||||
'creator.name as creator_name',
|
||||
'updater.name as updater_name'
|
||||
)
|
||||
->paginate($request->get('per_page', 15));
|
||||
|
||||
return AbstractController::ResultSuccess($responseData);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error fetching electricity bills: ' . $e->getMessage());
|
||||
return AbstractController::ResultError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new electricity bill
|
||||
*/
|
||||
public function create(Request $request)
|
||||
{
|
||||
try {
|
||||
$validated = $request->validate([
|
||||
'billing_date' => 'required|string',
|
||||
'previous_reading' => 'required|numeric|min:0',
|
||||
'current_reading' => 'required|numeric|min:0',
|
||||
'unit_price' => 'required|numeric|min:0',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
|
||||
// Check if billing_date already exists
|
||||
$existingBill = ElectricityBill::where('billing_date', $validated['billing_date'])->first();
|
||||
if ($existingBill) {
|
||||
return AbstractController::ResultError('Bill for this month already exists', 422);
|
||||
}
|
||||
|
||||
// Calculate total amount
|
||||
$consumption = $validated['current_reading'] - $validated['previous_reading'];
|
||||
$totalAmount = $consumption * $validated['unit_price'];
|
||||
|
||||
$bill = ElectricityBill::create([
|
||||
'billing_date' => $validated['billing_date'],
|
||||
'previous_reading' => $validated['previous_reading'],
|
||||
'current_reading' => $validated['current_reading'],
|
||||
'unit_price' => $validated['unit_price'],
|
||||
'total_amount' => $totalAmount,
|
||||
'notes' => $validated['notes'] ?? null,
|
||||
'created_by' => auth('admins')->user()->id ?? null,
|
||||
]);
|
||||
|
||||
return AbstractController::ResultSuccess($bill, 'Electricity bill created successfully');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error creating electricity bill: ' . $e->getMessage());
|
||||
return AbstractController::ResultError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update electricity bill
|
||||
*/
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
try {
|
||||
$validated = $request->validate([
|
||||
'billing_date' => 'sometimes|string',
|
||||
'previous_reading' => 'sometimes|numeric|min:0',
|
||||
'current_reading' => 'sometimes|numeric|min:0',
|
||||
'unit_price' => 'sometimes|numeric|min:0',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$bill = ElectricityBill::findOrFail($id);
|
||||
|
||||
// Check if billing_date already exists (excluding current record)
|
||||
if (isset($validated['billing_date'])) {
|
||||
$existingBill = ElectricityBill::where('billing_date', $validated['billing_date'])
|
||||
->where('id', '!=', $id)
|
||||
->first();
|
||||
if ($existingBill) {
|
||||
return AbstractController::ResultError('Bill for this month already exists', 422);
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculate total if any reading or price changed
|
||||
$previousReading = $validated['previous_reading'] ?? $bill->previous_reading;
|
||||
$currentReading = $validated['current_reading'] ?? $bill->current_reading;
|
||||
$unitPrice = $validated['unit_price'] ?? $bill->unit_price;
|
||||
$consumption = $currentReading - $previousReading;
|
||||
$totalAmount = $consumption * $unitPrice;
|
||||
|
||||
$bill->update(array_merge($validated, [
|
||||
'total_amount' => $totalAmount,
|
||||
'updated_by' => auth('admins')->user()->id ?? null,
|
||||
]));
|
||||
|
||||
return AbstractController::ResultSuccess($bill, 'Electricity bill updated successfully');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error updating electricity bill: ' . $e->getMessage());
|
||||
return AbstractController::ResultError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete electricity bill
|
||||
*/
|
||||
public function delete(Request $request, $id)
|
||||
{
|
||||
try {
|
||||
$bill = ElectricityBill::findOrFail($id);
|
||||
$bill->delete();
|
||||
|
||||
return AbstractController::ResultSuccess(null, 'Electricity bill deleted successfully');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error deleting electricity bill: ' . $e->getMessage());
|
||||
return AbstractController::ResultError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export electricity bill to PDF
|
||||
*/
|
||||
public function exportPdf(Request $request, $id)
|
||||
{
|
||||
try {
|
||||
$bill = ElectricityBill::findOrFail($id);
|
||||
|
||||
// Get month name from billing_date
|
||||
$consumption = $bill->current_reading - $bill->previous_reading;
|
||||
$totalText = $this->numberToVietnamese($bill->total_amount);
|
||||
$date = Carbon::parse($bill->billing_date);
|
||||
|
||||
$dateNow = 'Ngày ' . $date->day .
|
||||
' tháng ' . $date->month .
|
||||
' năm ' . $date->year;
|
||||
|
||||
// Generate PDF
|
||||
$pdf = Pdf::loadView('admin::admin.electricity_bills.pdf', [
|
||||
'bill' => $bill,
|
||||
'consumption' => $consumption,
|
||||
'dateNow' => $dateNow,
|
||||
'totalText' => $totalText
|
||||
]);
|
||||
|
||||
$fileName = 'electricity_bill_' . $bill->billing_date . '.pdf';
|
||||
$filePath = 'electricity_bills/' . $fileName;
|
||||
|
||||
// đảm bảo folder tồn tại
|
||||
if (!Storage::disk('public')->exists('electricity_bills')) {
|
||||
Storage::disk('public')->makeDirectory('electricity_bills');
|
||||
}
|
||||
|
||||
// 👇 render 1 lần
|
||||
$pdfContent = $pdf->output();
|
||||
|
||||
// 👇 lưu file
|
||||
Storage::disk('public')->put($filePath, $pdfContent);
|
||||
|
||||
// update DB
|
||||
$bill->update(['file_path' => $filePath]);
|
||||
|
||||
// 👇 trả về đúng file đã tạo
|
||||
return response($pdfContent)
|
||||
->header('Content-Type', 'application/pdf')
|
||||
->header('Content-Disposition', 'attachment; filename="' . $fileName . '"');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error exporting electricity bill to PDF: ' . $e->getMessage());
|
||||
return AbstractController::ResultError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get electricity bill by ID
|
||||
*/
|
||||
public function show($id)
|
||||
{
|
||||
try {
|
||||
$bill = ElectricityBill::with(['creator', 'updater'])->findOrFail($id);
|
||||
return AbstractController::ResultSuccess($bill);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error fetching electricity bill: ' . $e->getMessage());
|
||||
return AbstractController::ResultError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload an attachment (PDF or image) for an electricity bill
|
||||
*/
|
||||
public function uploadAttachment(Request $request, $id)
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'file' => 'required|file|mimes:pdf,jpg,jpeg,png,gif,webp|max:10240',
|
||||
]);
|
||||
|
||||
$bill = ElectricityBill::findOrFail($id);
|
||||
|
||||
$file = $request->file('file');
|
||||
$extension = $file->getClientOriginalExtension();
|
||||
$filename = 'bill_' . $bill->id . '_' . time() . '.' . $extension;
|
||||
$directory = 'electricity_bills/attachments';
|
||||
|
||||
if (!Storage::disk('public')->exists($directory)) {
|
||||
Storage::disk('public')->makeDirectory($directory);
|
||||
}
|
||||
|
||||
// Delete old attachment if exists
|
||||
if ($bill->attachment_path && Storage::disk('public')->exists($bill->attachment_path)) {
|
||||
Storage::disk('public')->delete($bill->attachment_path);
|
||||
}
|
||||
|
||||
$path = $file->storeAs($directory, $filename, 'public');
|
||||
|
||||
$bill->update([
|
||||
'attachment_path' => $path,
|
||||
'updated_by' => auth('admins')->user()->id ?? null,
|
||||
]);
|
||||
|
||||
return AbstractController::ResultSuccess($bill, 'Attachment uploaded successfully');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error uploading electricity bill attachment: ' . $e->getMessage());
|
||||
return AbstractController::ResultError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the attachment of an electricity bill
|
||||
*/
|
||||
public function deleteAttachment($id)
|
||||
{
|
||||
try {
|
||||
$bill = ElectricityBill::findOrFail($id);
|
||||
|
||||
if ($bill->attachment_path && Storage::disk('public')->exists($bill->attachment_path)) {
|
||||
Storage::disk('public')->delete($bill->attachment_path);
|
||||
}
|
||||
|
||||
$bill->update([
|
||||
'attachment_path' => null,
|
||||
'updated_by' => auth('admins')->user()->id ?? null,
|
||||
]);
|
||||
|
||||
return AbstractController::ResultSuccess($bill, 'Attachment deleted successfully');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error deleting electricity bill attachment: ' . $e->getMessage());
|
||||
return AbstractController::ResultError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
function numberToVietnamese($number)
|
||||
{
|
||||
$units = ["", "một", "hai", "ba", "bốn", "năm", "sáu", "bảy", "tám", "chín"];
|
||||
$levels = ["", "nghìn", "triệu", "tỷ"];
|
||||
|
||||
if ($number == 0) return "không đồng";
|
||||
|
||||
$number = (int)$number;
|
||||
$result = "";
|
||||
$level = 0;
|
||||
|
||||
while ($number > 0) {
|
||||
$threeDigits = $number % 1000;
|
||||
if ($threeDigits != 0) {
|
||||
$result = $this->readThreeDigits($threeDigits, $units) . " " . $levels[$level] . " " . $result;
|
||||
}
|
||||
$number = floor($number / 1000);
|
||||
$level++;
|
||||
}
|
||||
|
||||
return ucfirst(trim(preg_replace('/\s+/', ' ', $result))) . " đồng";
|
||||
}
|
||||
|
||||
function readThreeDigits($number, $units)
|
||||
{
|
||||
$hundreds = floor($number / 100);
|
||||
$tens = floor(($number % 100) / 10);
|
||||
$ones = $number % 10;
|
||||
|
||||
$result = "";
|
||||
|
||||
if ($hundreds > 0) {
|
||||
$result .= $units[$hundreds] . " trăm";
|
||||
if ($tens == 0 && $ones > 0) {
|
||||
$result .= " lẻ";
|
||||
}
|
||||
}
|
||||
|
||||
if ($tens > 1) {
|
||||
$result .= " " . $units[$tens] . " mươi";
|
||||
if ($ones == 1) {
|
||||
$result .= " mốt";
|
||||
} elseif ($ones == 5) {
|
||||
$result .= " lăm";
|
||||
} elseif ($ones > 0) {
|
||||
$result .= " " . $units[$ones];
|
||||
}
|
||||
} elseif ($tens == 1) {
|
||||
$result .= " mười";
|
||||
if ($ones == 5) {
|
||||
$result .= " lăm";
|
||||
} elseif ($ones > 0) {
|
||||
$result .= " " . $units[$ones];
|
||||
}
|
||||
} elseif ($ones > 0) {
|
||||
$result .= " " . $units[$ones];
|
||||
}
|
||||
|
||||
return trim($result);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,517 @@
|
|||
<?php
|
||||
|
||||
namespace Modules\Admin\app\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Traits\HasFilterRequest;
|
||||
use App\Traits\HasOrderByRequest;
|
||||
use App\Traits\HasSearchRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Admin\app\Models\Admin;
|
||||
use Modules\Admin\app\Models\Equipment;
|
||||
use Modules\Admin\app\Models\EquipmentAssignment;
|
||||
|
||||
class EquipmentController extends Controller
|
||||
{
|
||||
use HasOrderByRequest;
|
||||
use HasFilterRequest;
|
||||
use HasSearchRequest;
|
||||
|
||||
/**
|
||||
* Get all equipments with pagination + filter + search.
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
try {
|
||||
$equipments = new Equipment;
|
||||
|
||||
$this->orderByRequest($equipments, $request);
|
||||
|
||||
$this->filterRequest(
|
||||
builder: $equipments,
|
||||
request: $request,
|
||||
filterKeys: [
|
||||
'code' => self::F_TEXT,
|
||||
'name' => self::F_TEXT,
|
||||
'note' => self::F_TEXT,
|
||||
]
|
||||
);
|
||||
|
||||
$this->searchRequest(
|
||||
builder: $equipments,
|
||||
value: $request->get('search'),
|
||||
fields: ['equipments.code', 'equipments.name', 'equipments.note']
|
||||
);
|
||||
|
||||
// Optional filter: assigned (in use) vs warehouse
|
||||
$assignedFilter = $request->get('assigned');
|
||||
if ($assignedFilter === 'in_use') {
|
||||
$equipments->whereNotNull('equipments.current_user_id');
|
||||
} elseif ($assignedFilter === 'warehouse') {
|
||||
$equipments->whereNull('equipments.current_user_id');
|
||||
}
|
||||
|
||||
$responseData = $equipments
|
||||
->leftJoin('users as holder', 'equipments.current_user_id', '=', 'holder.id')
|
||||
->orderBy('equipments.updated_at', 'desc')
|
||||
->select(
|
||||
'equipments.*',
|
||||
'holder.name as current_user_name'
|
||||
)
|
||||
->paginate($request->get('per_page', 15));
|
||||
|
||||
return AbstractController::ResultSuccess($responseData);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error fetching equipments: ' . $e->getMessage());
|
||||
return AbstractController::ResultError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single equipment by code (for barcode scan).
|
||||
*/
|
||||
public function showByCode(Request $request)
|
||||
{
|
||||
try {
|
||||
$code = trim((string) $request->get('code'));
|
||||
if ($code === '') {
|
||||
return AbstractController::ResultError('Code is required', 422);
|
||||
}
|
||||
|
||||
$equipment = Equipment::leftJoin('users as holder', 'equipments.current_user_id', '=', 'holder.id')
|
||||
->where('equipments.code', $code)
|
||||
->select('equipments.*', 'holder.name as current_user_name')
|
||||
->first();
|
||||
|
||||
if (!$equipment) {
|
||||
return AbstractController::ResultSuccess(null, 'Equipment not found');
|
||||
}
|
||||
|
||||
$history = EquipmentAssignment::leftJoin('users', 'equipment_assignments.user_id', '=', 'users.id')
|
||||
->where('equipment_assignments.equipment_id', $equipment->id)
|
||||
->orderBy('equipment_assignments.assigned_at', 'desc')
|
||||
->select(
|
||||
'equipment_assignments.*',
|
||||
'users.name as user_name'
|
||||
)
|
||||
->get();
|
||||
|
||||
$equipment->history = $history;
|
||||
|
||||
return AbstractController::ResultSuccess($equipment);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error fetching equipment by code: ' . $e->getMessage());
|
||||
return AbstractController::ResultError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get assignment history for an equipment.
|
||||
*/
|
||||
public function history($id)
|
||||
{
|
||||
try {
|
||||
$history = EquipmentAssignment::leftJoin('users', 'equipment_assignments.user_id', '=', 'users.id')
|
||||
->where('equipment_assignments.equipment_id', $id)
|
||||
->orderBy('equipment_assignments.assigned_at', 'desc')
|
||||
->select(
|
||||
'equipment_assignments.*',
|
||||
'users.name as user_name'
|
||||
)
|
||||
->get();
|
||||
|
||||
return AbstractController::ResultSuccess($history);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error fetching equipment history: ' . $e->getMessage());
|
||||
return AbstractController::ResultError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all users for assignment selectbox.
|
||||
*/
|
||||
public function userOptions()
|
||||
{
|
||||
try {
|
||||
$users = Admin::orderBy('name', 'asc')
|
||||
->select('id', 'name', 'email')
|
||||
->get();
|
||||
return AbstractController::ResultSuccess($users);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error fetching user options: ' . $e->getMessage());
|
||||
return AbstractController::ResultError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new equipment. Accepts multipart with optional image file.
|
||||
*/
|
||||
public function create(Request $request)
|
||||
{
|
||||
try {
|
||||
$validated = $request->validate([
|
||||
'code' => 'required|string|max:255|unique:equipments,code',
|
||||
'name' => 'required|string|max:255',
|
||||
'note' => 'nullable|string',
|
||||
'current_user_id' => 'nullable|integer|exists:users,id',
|
||||
'image' => 'nullable|file|mimes:jpg,jpeg,png,gif,webp|max:10240',
|
||||
]);
|
||||
|
||||
$authId = auth('admins')->user()->id ?? null;
|
||||
|
||||
$imagePath = null;
|
||||
if ($request->hasFile('image')) {
|
||||
$directory = 'equipments/images';
|
||||
if (!Storage::disk('public')->exists($directory)) {
|
||||
Storage::disk('public')->makeDirectory($directory);
|
||||
}
|
||||
$imagePath = $request->file('image')->store($directory, 'public');
|
||||
}
|
||||
|
||||
$equipment = DB::transaction(function () use ($validated, $authId, $imagePath) {
|
||||
$now = Carbon::now();
|
||||
$currentUserId = $validated['current_user_id'] ?? null;
|
||||
|
||||
$equipment = Equipment::create([
|
||||
'code' => $validated['code'],
|
||||
'name' => $validated['name'],
|
||||
'note' => $validated['note'] ?? null,
|
||||
'image_path' => $imagePath,
|
||||
'current_user_id' => $currentUserId,
|
||||
'assigned_at' => $now,
|
||||
'created_by' => $authId,
|
||||
]);
|
||||
|
||||
// Always create an initial assignment record so the history
|
||||
// shows the very first state (warehouse or user).
|
||||
EquipmentAssignment::create([
|
||||
'equipment_id' => $equipment->id,
|
||||
'user_id' => $currentUserId, // null = at warehouse
|
||||
'assigned_at' => $now,
|
||||
'note' => $currentUserId ? 'Initial assignment' : 'Initial — at warehouse',
|
||||
'created_by' => $authId,
|
||||
]);
|
||||
|
||||
return $equipment;
|
||||
});
|
||||
|
||||
return AbstractController::ResultSuccess($equipment, 'Equipment created successfully');
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
return AbstractController::ResultError($e->getMessage(), 422);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error creating equipment: ' . $e->getMessage());
|
||||
return AbstractController::ResultError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload / replace the equipment image (optional, post-create or edit).
|
||||
*/
|
||||
public function uploadImage(Request $request, $id)
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'image' => 'required|file|mimes:jpg,jpeg,png,gif,webp|max:10240',
|
||||
]);
|
||||
|
||||
$equipment = Equipment::findOrFail($id);
|
||||
|
||||
// Delete old image if present
|
||||
if ($equipment->image_path && Storage::disk('public')->exists($equipment->image_path)) {
|
||||
Storage::disk('public')->delete($equipment->image_path);
|
||||
}
|
||||
|
||||
$directory = 'equipments/images';
|
||||
if (!Storage::disk('public')->exists($directory)) {
|
||||
Storage::disk('public')->makeDirectory($directory);
|
||||
}
|
||||
$path = $request->file('image')->store($directory, 'public');
|
||||
|
||||
$equipment->update([
|
||||
'image_path' => $path,
|
||||
'updated_by' => auth('admins')->user()->id ?? null,
|
||||
]);
|
||||
|
||||
return AbstractController::ResultSuccess($equipment, 'Image uploaded');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error uploading equipment image: ' . $e->getMessage());
|
||||
return AbstractController::ResultError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the equipment image.
|
||||
*/
|
||||
public function deleteImage($id)
|
||||
{
|
||||
try {
|
||||
$equipment = Equipment::findOrFail($id);
|
||||
if ($equipment->image_path && Storage::disk('public')->exists($equipment->image_path)) {
|
||||
Storage::disk('public')->delete($equipment->image_path);
|
||||
}
|
||||
$equipment->update([
|
||||
'image_path' => null,
|
||||
'updated_by' => auth('admins')->user()->id ?? null,
|
||||
]);
|
||||
return AbstractController::ResultSuccess($equipment, 'Image deleted');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error deleting equipment image: ' . $e->getMessage());
|
||||
return AbstractController::ResultError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update equipment basic info (code, name, note).
|
||||
*/
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
try {
|
||||
$equipment = Equipment::findOrFail($id);
|
||||
|
||||
$validated = $request->validate([
|
||||
'code' => 'sometimes|string|max:255|unique:equipments,code,' . $id,
|
||||
'name' => 'sometimes|string|max:255',
|
||||
'note' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$equipment->update(array_merge($validated, [
|
||||
'updated_by' => auth('admins')->user()->id ?? null,
|
||||
]));
|
||||
|
||||
return AbstractController::ResultSuccess($equipment, 'Equipment updated successfully');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error updating equipment: ' . $e->getMessage());
|
||||
return AbstractController::ResultError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign equipment to a user, or return to warehouse (user_id = null).
|
||||
* Every state change is recorded: closes the previous open record AND opens
|
||||
* a new one for the new holder (user or warehouse).
|
||||
*/
|
||||
public function assign(Request $request, $id)
|
||||
{
|
||||
try {
|
||||
$validated = $request->validate([
|
||||
'user_id' => 'nullable|integer|exists:users,id',
|
||||
'note' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$authId = auth('admins')->user()->id ?? null;
|
||||
|
||||
$equipment = DB::transaction(function () use ($id, $validated, $authId) {
|
||||
$equipment = Equipment::lockForUpdate()->findOrFail($id);
|
||||
$now = Carbon::now();
|
||||
$newUserId = $validated['user_id'] ?? null;
|
||||
|
||||
// No-op guard: if assigning to the same holder, skip to avoid duplicate history rows
|
||||
if ($equipment->current_user_id == $newUserId) {
|
||||
return $equipment;
|
||||
}
|
||||
|
||||
// Close any currently open assignment (records who/Kho was holding it until now)
|
||||
EquipmentAssignment::where('equipment_id', $equipment->id)
|
||||
->whereNull('returned_at')
|
||||
->update([
|
||||
'returned_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
// Always open a new assignment row — for a user OR for the warehouse.
|
||||
// user_id = null means "currently at warehouse".
|
||||
EquipmentAssignment::create([
|
||||
'equipment_id' => $equipment->id,
|
||||
'user_id' => $newUserId,
|
||||
'assigned_at' => $now,
|
||||
'note' => $validated['note'] ?? ($newUserId === null ? 'Returned to warehouse' : null),
|
||||
'created_by' => $authId,
|
||||
]);
|
||||
|
||||
$equipment->update([
|
||||
'current_user_id' => $newUserId,
|
||||
'assigned_at' => $now,
|
||||
'updated_by' => $authId,
|
||||
]);
|
||||
|
||||
return $equipment->fresh();
|
||||
});
|
||||
|
||||
return AbstractController::ResultSuccess($equipment, 'Equipment assignment updated');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error assigning equipment: ' . $e->getMessage());
|
||||
return AbstractController::ResultError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistics: list users grouped with the equipments they are currently holding.
|
||||
*/
|
||||
public function userStatistics(Request $request)
|
||||
{
|
||||
try {
|
||||
$search = trim((string) $request->get('search', ''));
|
||||
|
||||
$usersQuery = DB::table('equipments')
|
||||
->join('users', 'equipments.current_user_id', '=', 'users.id')
|
||||
->whereNotNull('equipments.current_user_id');
|
||||
|
||||
if ($search !== '') {
|
||||
$usersQuery->where(function ($q) use ($search) {
|
||||
$q->where('users.name', 'like', "%{$search}%")
|
||||
->orWhere('users.email', 'like', "%{$search}%")
|
||||
->orWhere('equipments.code', 'like', "%{$search}%")
|
||||
->orWhere('equipments.name', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
$userTotals = (clone $usersQuery)
|
||||
->select(
|
||||
'users.id as user_id',
|
||||
'users.name as user_name',
|
||||
'users.email as user_email',
|
||||
DB::raw('COUNT(equipments.id) as equipment_count')
|
||||
)
|
||||
->groupBy('users.id', 'users.name', 'users.email')
|
||||
->orderByDesc('equipment_count')
|
||||
->orderBy('users.name')
|
||||
->get();
|
||||
|
||||
$userIds = $userTotals->pluck('user_id')->all();
|
||||
|
||||
$devices = Equipment::leftJoin('users as holder', 'equipments.current_user_id', '=', 'holder.id')
|
||||
->whereIn('equipments.current_user_id', $userIds)
|
||||
->select(
|
||||
'equipments.id',
|
||||
'equipments.code',
|
||||
'equipments.name',
|
||||
'equipments.note',
|
||||
'equipments.image_path',
|
||||
'equipments.current_user_id',
|
||||
'equipments.assigned_at'
|
||||
)
|
||||
->orderBy('equipments.assigned_at', 'desc')
|
||||
->get()
|
||||
->groupBy('current_user_id');
|
||||
|
||||
$result = $userTotals->map(function ($u) use ($devices) {
|
||||
return [
|
||||
'user_id' => $u->user_id,
|
||||
'user_name' => $u->user_name,
|
||||
'user_email' => $u->user_email,
|
||||
'equipment_count' => (int) $u->equipment_count,
|
||||
'equipments' => ($devices->get($u->user_id) ?? collect())->values(),
|
||||
];
|
||||
});
|
||||
|
||||
// Also include warehouse summary
|
||||
$warehouseCount = Equipment::whereNull('current_user_id')->count();
|
||||
|
||||
return AbstractController::ResultSuccess([
|
||||
'users' => $result,
|
||||
'warehouse_count' => $warehouseCount,
|
||||
'total_equipment' => Equipment::count(),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error fetching equipment user statistics: ' . $e->getMessage());
|
||||
return AbstractController::ResultError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete equipment (and its history via cascade).
|
||||
*/
|
||||
public function delete($id)
|
||||
{
|
||||
try {
|
||||
$equipment = Equipment::findOrFail($id);
|
||||
$equipment->delete();
|
||||
return AbstractController::ResultSuccess(null, 'Equipment deleted successfully');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error deleting equipment: ' . $e->getMessage());
|
||||
return AbstractController::ResultError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the (filtered) equipment list as CSV.
|
||||
*/
|
||||
public function export(Request $request)
|
||||
{
|
||||
try {
|
||||
$equipments = new Equipment;
|
||||
|
||||
$this->filterRequest(
|
||||
builder: $equipments,
|
||||
request: $request,
|
||||
filterKeys: [
|
||||
'code' => self::F_TEXT,
|
||||
'name' => self::F_TEXT,
|
||||
'note' => self::F_TEXT,
|
||||
]
|
||||
);
|
||||
|
||||
$this->searchRequest(
|
||||
builder: $equipments,
|
||||
value: $request->get('search'),
|
||||
fields: ['equipments.code', 'equipments.name', 'equipments.note']
|
||||
);
|
||||
|
||||
$assignedFilter = $request->get('assigned');
|
||||
if ($assignedFilter === 'in_use') {
|
||||
$equipments->whereNotNull('equipments.current_user_id');
|
||||
} elseif ($assignedFilter === 'warehouse') {
|
||||
$equipments->whereNull('equipments.current_user_id');
|
||||
}
|
||||
|
||||
$rows = $equipments
|
||||
->leftJoin('users as holder', 'equipments.current_user_id', '=', 'holder.id')
|
||||
->orderBy('equipments.updated_at', 'desc')
|
||||
->select(
|
||||
'equipments.id',
|
||||
'equipments.code',
|
||||
'equipments.name',
|
||||
'equipments.note',
|
||||
'equipments.assigned_at',
|
||||
'equipments.created_at',
|
||||
'equipments.updated_at',
|
||||
'holder.name as current_user_name'
|
||||
)
|
||||
->get();
|
||||
|
||||
$filename = 'equipments_' . date('Y-m-d_His') . '.csv';
|
||||
|
||||
$callback = function () use ($rows) {
|
||||
$out = fopen('php://output', 'w');
|
||||
// BOM for Excel UTF-8
|
||||
fwrite($out, "\xEF\xBB\xBF");
|
||||
fputcsv($out, ['ID', 'Code', 'Name', 'Note', 'Current Holder', 'Assigned At', 'Created At', 'Updated At']);
|
||||
foreach ($rows as $r) {
|
||||
fputcsv($out, [
|
||||
$r->id,
|
||||
$r->code,
|
||||
$r->name,
|
||||
$r->note,
|
||||
$r->current_user_name ?: 'Kho',
|
||||
$r->assigned_at,
|
||||
$r->created_at,
|
||||
$r->updated_at,
|
||||
]);
|
||||
}
|
||||
fclose($out);
|
||||
};
|
||||
|
||||
return response()->stream($callback, 200, [
|
||||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error exporting equipments: ' . $e->getMessage());
|
||||
return AbstractController::ResultError($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,9 +8,11 @@ use App\Services\JiraService;
|
|||
use Carbon\Carbon;
|
||||
use Modules\Admin\app\Models\TechnicalUser;
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\Admin\app\Models\ProjectReview;
|
||||
use Modules\Admin\app\Models\UserCriteria;
|
||||
use PhpOffice\PhpWord\IOFactory;
|
||||
use PhpOffice\PhpWord\PhpWord;
|
||||
use PhpOffice\PhpWord\SimpleType\Jc;
|
||||
|
||||
class EvaluationController extends Controller
|
||||
{
|
||||
|
|
@ -222,6 +224,50 @@ class EvaluationController extends Controller
|
|||
'spaceAfter' => 600,
|
||||
]);
|
||||
}
|
||||
|
||||
// **ProjectReview Section**
|
||||
// Fetch Project Reviews
|
||||
$projectReviews = ProjectReview::where('user_id', $user->id);
|
||||
if ($startDate && $endDate) {
|
||||
$projectReviews->whereBetween('updated_at', [$startDate, $endDate . ' 23:59:59']);
|
||||
} elseif ($startDate) {
|
||||
$projectReviews->where('updated_at', '>=', $startDate);
|
||||
} elseif ($endDate) {
|
||||
$projectReviews->where('updated_at', '<=', $endDate . ' 23:59:59');
|
||||
}
|
||||
|
||||
if ($projectReviews->get()->count() > 0) {
|
||||
$section->addText("Project Reviews", ['bold' => true, 'size' => 14, 'color' => '000080'], ['alignment' => Jc::CENTER]);
|
||||
|
||||
$table = $section->addTable([
|
||||
'borderColor' => '000000',
|
||||
'borderSize' => 6,
|
||||
'cellMargin' => 80,
|
||||
]);
|
||||
|
||||
// Table Header
|
||||
$table->addRow();
|
||||
$table->addCell(3500)->addText('Project Name', ['bold' => true]);
|
||||
$table->addCell(2500)->addText('Role', ['bold' => true]);
|
||||
$table->addCell(5000)->addText('Note', ['bold' => true]);
|
||||
$table->addCell(2500)->addText('Created At', ['bold' => true]);
|
||||
$table->addCell(2500)->addText('Updated At', ['bold' => true]);
|
||||
|
||||
|
||||
|
||||
foreach ($projectReviews->get() as $review) {
|
||||
$table->addRow();
|
||||
$table->addCell(3500)->addText($review->name);
|
||||
$table->addCell(2500)->addText($review->role);
|
||||
$table->addCell(5000)->addText($review->note);
|
||||
$table->addCell(2500)->addText(Carbon::parse($review->created_at)->format('d/m/Y H:i:s'));
|
||||
$table->addCell(2500)->addText(Carbon::parse($review->updated_at)->format('d/m/Y H:i:s'));
|
||||
}
|
||||
|
||||
$section->addText(' ', [], [
|
||||
'spaceAfter' => 600,
|
||||
]);
|
||||
}
|
||||
if ($technicalData)
|
||||
$section->addPageBreak();
|
||||
//Technical
|
||||
|
|
@ -260,4 +306,156 @@ class EvaluationController extends Controller
|
|||
|
||||
return response()->download($tempFile, "$user->name.docx")->deleteFileAfterSend(true);
|
||||
}
|
||||
|
||||
public function reportAllUsers(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'fromDate' => 'nullable|date',
|
||||
'toDate' => 'nullable|date',
|
||||
]);
|
||||
|
||||
$startDate = $request->input('fromDate');
|
||||
$endDate = $request->input('toDate');
|
||||
|
||||
$users = User::all();
|
||||
|
||||
$phpWord = new PhpWord();
|
||||
$phpWord->setDefaultFontName('Times New Roman');
|
||||
$phpWord->setDefaultFontSize(12);
|
||||
$section = $phpWord->addSection();
|
||||
|
||||
foreach ($users as $index => $user) {
|
||||
$userEmail = $user->email;
|
||||
|
||||
|
||||
// Add user heading
|
||||
$section->addText("Staff Evaluation", ['bold' => true, 'size' => 20, 'color' => '000000'], ['align' => 'center']);
|
||||
|
||||
if ($startDate) {
|
||||
$fromDate = Carbon::parse($startDate)->format('d-m-Y');
|
||||
$section->addText("From: " . $fromDate, ['size' => 12], ['align' => 'end']);
|
||||
}
|
||||
if ($endDate) {
|
||||
$toDate = Carbon::parse($endDate)->format('d-m-Y');
|
||||
$section->addText("To: " . $toDate, ['size' => 12], ['align' => 'end']);
|
||||
}
|
||||
|
||||
$section->addText("{$user->name}", ['bold' => true, 'size' => 14, 'color' => '000000'], ['spaceAfter' => 400]);
|
||||
|
||||
// **Projects Data**
|
||||
$projectsData = self::getProjectReviewByParams($startDate, $endDate, $userEmail);
|
||||
if (!empty($projectsData)) {
|
||||
foreach ($projectsData as $project) {
|
||||
$section->addText("Project: {$project['name']}", ['bold' => true, 'size' => 14, 'color' => '000080']);
|
||||
|
||||
foreach ($project['sprints'] as $sprint) {
|
||||
$section->addText("Sprint: {$sprint['name']}", ['bold' => true, 'italic' => true, 'size' => 12]);
|
||||
|
||||
$table = $section->addTable(['borderSize' => 6, 'cellMargin' => 80]);
|
||||
$table->addRow();
|
||||
$table->addCell(3000)->addText('Criteria', ['bold' => true]);
|
||||
$table->addCell(3000)->addText('Note', ['bold' => true]);
|
||||
$table->addCell(2500)->addText('Created By', ['bold' => true]);
|
||||
$table->addCell(1500)->addText('Point', ['bold' => true]);
|
||||
|
||||
foreach ($sprint['criterias'] as $criteria) {
|
||||
$table->addRow();
|
||||
$table->addCell(3000)->addText($criteria['criteria']);
|
||||
$table->addCell(3000)->addText($criteria['note']);
|
||||
$table->addCell(2500)->addText($criteria['createdBy']);
|
||||
$table->addCell(1500)->addText($criteria['point'] > 0 ? $criteria['point'] : '');
|
||||
}
|
||||
}
|
||||
$section->addText(' ', [], [
|
||||
'spaceAfter' => 600,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// **ProjectReview Section**
|
||||
// Fetch Project Reviews
|
||||
$projectReviews = ProjectReview::where('user_id', $user->id);
|
||||
if ($startDate && $endDate) {
|
||||
$projectReviews->whereBetween('updated_at', [$startDate, $endDate . ' 23:59:59']);
|
||||
} elseif ($startDate) {
|
||||
$projectReviews->where('updated_at', '>=', $startDate);
|
||||
} elseif ($endDate) {
|
||||
$projectReviews->where('updated_at', '<=', $endDate . ' 23:59:59');
|
||||
}
|
||||
if ($projectReviews->get()->count() > 0) {
|
||||
$section->addText("Project Reviews", ['bold' => true, 'size' => 14, 'color' => '000080'], ['alignment' => Jc::CENTER]);
|
||||
|
||||
$table = $section->addTable([
|
||||
'borderColor' => '000000',
|
||||
'borderSize' => 6,
|
||||
'cellMargin' => 80,
|
||||
]);
|
||||
|
||||
// Table Header
|
||||
$table->addRow();
|
||||
$table->addCell(3500)->addText('Project Name', ['bold' => true]);
|
||||
$table->addCell(2500)->addText('Role', ['bold' => true]);
|
||||
$table->addCell(5000)->addText('Note', ['bold' => true]);
|
||||
$table->addCell(2500)->addText('Created At', ['bold' => true]);
|
||||
$table->addCell(2500)->addText('Updated At', ['bold' => true]);
|
||||
|
||||
|
||||
foreach ($projectReviews->get() as $review) {
|
||||
$table->addRow();
|
||||
$table->addCell(3500)->addText($review->name);
|
||||
$table->addCell(2500)->addText($review->role);
|
||||
$table->addCell(5000)->addText($review->note);
|
||||
$table->addCell(2500)->addText(Carbon::parse($review->created_at)->format('d/m/Y H:i:s'));
|
||||
$table->addCell(2500)->addText(Carbon::parse($review->updated_at)->format('d/m/Y H:i:s'));
|
||||
}
|
||||
|
||||
$section->addText(' ', [], [
|
||||
'spaceAfter' => 600,
|
||||
]);
|
||||
}
|
||||
|
||||
// **Technical Section**
|
||||
$section->addText("Technicals", ['bold' => true, 'size' => 14, 'color' => '000080'], ['alignment' => Jc::CENTER]);
|
||||
|
||||
$table = $section->addTable([
|
||||
'borderColor' => '000000',
|
||||
'borderSize' => 6,
|
||||
'cellMargin' => 80,
|
||||
]);
|
||||
|
||||
$table->addRow();
|
||||
$table->addCell(1500)->addText('Level', ['bold' => true]);
|
||||
$table->addCell(3500)->addText('Name', ['bold' => true]);
|
||||
$table->addCell(2500)->addText('Point', ['bold' => true]);
|
||||
$table->addCell(2500)->addText('Last Update', ['bold' => true]);
|
||||
|
||||
// Fetch Technical Data
|
||||
$technicalData = TechnicalController::getDataTechnicalsByUserId($user->id);
|
||||
|
||||
foreach ($technicalData as $technical) {
|
||||
$updated_at = $technical['updated_at'] ? Carbon::parse($technical['updated_at'])->format('d/m/Y H:i:s') : null;
|
||||
$table->addRow();
|
||||
$table->addCell(1500)->addText($technical['level']);
|
||||
$table->addCell(3500)->addText($technical['name']);
|
||||
$table->addCell(2500)->addText($technical['point']);
|
||||
$table->addCell(2500)->addText($updated_at);
|
||||
}
|
||||
|
||||
// Add page break between users (except last one)
|
||||
if ($index < count($users) - 1) {
|
||||
$section->addPageBreak();
|
||||
}
|
||||
}
|
||||
|
||||
// Save & Download Word File
|
||||
$tempFile = tempnam(sys_get_temp_dir(), 'word');
|
||||
$objWriter = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'Word2007');
|
||||
$objWriter->save($tempFile);
|
||||
|
||||
return response()->download($tempFile, "All_Users_Report.docx")->deleteFileAfterSend(true);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
namespace Modules\Admin\app\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Mail\WarningLongTask;
|
||||
use App\Mail\WorklogReport;
|
||||
use App\Models\User;
|
||||
use App\Traits\HasFilterRequest;
|
||||
use App\Traits\HasOrderByRequest;
|
||||
use App\Traits\HasSearchRequest;
|
||||
|
|
@ -176,6 +178,27 @@ class JiraController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
public function getWeeklyReport()
|
||||
{
|
||||
try {
|
||||
$startOfWeek = Carbon::now()->startOfWeek()->format('Y-m-d'); // Mặc định là Thứ Hai
|
||||
$endOfWeek = Carbon::now()->endOfWeek()->format('Y-m-d'); // Mặc định là Chủ Nhật
|
||||
// dd($startOfWeek);
|
||||
$results = [];
|
||||
$workLogs = $this->jiraService->getAllUserWorkLogs($startOfWeek, $endOfWeek);
|
||||
foreach($workLogs as $data){
|
||||
$results[$data['username']] = $data['information']['issues'];
|
||||
}
|
||||
|
||||
|
||||
return response()->json([
|
||||
$results
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function sendReport()
|
||||
{
|
||||
$dateFormatted = Carbon::yesterday()->setTimezone(env('TIME_ZONE'))->format('Y-m-d');
|
||||
|
|
@ -183,8 +206,8 @@ class JiraController extends Controller
|
|||
$workLogs = $this->jiraService->getAllUserWorkLogs($dateFormatted, $dateFormatted);
|
||||
|
||||
$tasksByUser = $this->formatWorkLogsByUser($workLogs);
|
||||
Mail::to(['luanlt632000@gmail.com'])->send(new WorklogReport($tasksByUser));
|
||||
// Mail::to(['luanlt632000@gmail.com', 'admin@apactech.io'])->send(new WorklogReport($tasksByUser));
|
||||
// Mail::to(['luanlt632000@gmail.com'])->send(new WorklogReport($tasksByUser));
|
||||
Mail::to(['joseph@apactech.io', 'admin@apactech.io'])->send(new WorklogReport($tasksByUser));
|
||||
|
||||
// return "Email sent successfully!";
|
||||
return response()->json([
|
||||
|
|
@ -248,7 +271,6 @@ class JiraController extends Controller
|
|||
return $tasksByUser;
|
||||
}
|
||||
|
||||
|
||||
public function getAllUserDoing(Request $request)
|
||||
{
|
||||
try {
|
||||
|
|
@ -303,6 +325,7 @@ class JiraController extends Controller
|
|||
'status' => true
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function getAllIssueByIdSprint(Request $request)
|
||||
{
|
||||
$id = $request->input('id');
|
||||
|
|
@ -313,4 +336,42 @@ class JiraController extends Controller
|
|||
'status' => true
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function sendWarningMailByAllowcation()
|
||||
{
|
||||
$data = $this->jiraService->getAllUserDoing();
|
||||
$user_info = [];
|
||||
foreach ($data['projects'] as $project) {
|
||||
foreach ($project['users'] as $user) {
|
||||
foreach ($user['issues'] as $issue) {
|
||||
$targetDate = Carbon::parse($issue['changelog']['histories'][0]['created']); // Target date
|
||||
$daysRemaining = Carbon::now()->setTimezone(env('TIME_ZONE'))->diffInDays($targetDate);
|
||||
if ($daysRemaining > 10) {
|
||||
$issue['daysRemaining'] = $daysRemaining;
|
||||
$user_info[$user['user']['emailAddress']][] = $issue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($user_info as $email => $user) {
|
||||
Mail::to([$email])->cc(['admin@apactech.io', 'joseph@apactech.io'])->send(new WarningLongTask($user_info[$email]));
|
||||
}
|
||||
}
|
||||
|
||||
public function getUserProjectParticipating(Request $request)
|
||||
{
|
||||
$userID = $request->input('userID');
|
||||
$startDate = $request->input('fromDate');
|
||||
$endDate = $request->input('toDate');
|
||||
$user = User::find($userID);
|
||||
$userJira = $this->jiraService->getUserByEmail($user->email);
|
||||
$projects = $this->jiraService->getUserWorkLogs($userJira[0]['accountId'], $startDate, $endDate);
|
||||
|
||||
return response()->json([
|
||||
'data' => $projects,
|
||||
"accountId" => $userJira[0]['accountId'],
|
||||
'status' => true
|
||||
], 200);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,16 +2,23 @@
|
|||
|
||||
namespace Modules\Admin\app\Http\Controllers;
|
||||
|
||||
use App\Exports\LeaveManagementExport;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\InitializeLeaveDays;
|
||||
use App\Models\LeaveDays;
|
||||
use App\Models\Notes;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Modules\Admin\app\Models\Ticket;
|
||||
use App\Traits\AnalyzeData;
|
||||
|
||||
class LeaveManagementController extends Controller
|
||||
{
|
||||
use AnalyzeData;
|
||||
|
||||
public function get(Request $request)
|
||||
{
|
||||
$yearNow = $request->query('year', now()->year);
|
||||
|
|
@ -33,31 +40,34 @@ class LeaveManagementController extends Controller
|
|||
})
|
||||
->leftJoin("categories as reason", function ($join) {
|
||||
$join->on('n_reason', '=', 'reason.c_code');
|
||||
$join->on('reason.c_type', DB::raw("CONCAT('REASON')"));
|
||||
$join->on('reason.c_type', DB::raw("CONCAT('REASON_NOTES')"));
|
||||
})
|
||||
->select(
|
||||
DB::raw('notes.id as id'),
|
||||
DB::raw('notes.n_user_id as n_user_id'),
|
||||
DB::raw('notes.n_time_type as time_type'),
|
||||
DB::raw('notes.n_year as year'),
|
||||
DB::raw('notes.n_month as month'),
|
||||
DB::raw('categories.c_value as leave_days'),
|
||||
DB::raw('notes.n_day as day'),
|
||||
DB::raw('notes.n_reason as reason_code'),
|
||||
'reason.c_name as reason_name',
|
||||
'categories.c_name as time_type_name',
|
||||
// DB::raw('SUM(categories.c_value) as leave_days')
|
||||
)
|
||||
// ->where('notes.n_user_id', "1")
|
||||
->where('notes.n_year', $year)
|
||||
->where('notes.n_reason', 'ONLEAVE')
|
||||
->whereIn('notes.n_reason', ['ONLEAVE', 'LEAVE_WITHOUT_PAY', 'TEMPORARY_ONLEAVE'])
|
||||
// ->groupBy("notes.n_user_id")
|
||||
->orderBy('notes.n_month')
|
||||
->orderBy('notes.n_day')
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
return [
|
||||
"id" => $item->id,
|
||||
"day" => $item->day,
|
||||
"n_user_id" => $item->n_user_id,
|
||||
// "time_type" => $item->time_type,
|
||||
"reason_code" => $item->reason_code,
|
||||
"reason_name" => $item->reason_name,
|
||||
"time_type_name" => $item->time_type_name,
|
||||
"month" => $item->month,
|
||||
|
|
@ -73,6 +83,7 @@ class LeaveManagementController extends Controller
|
|||
'users.name as user_name',
|
||||
'users.email',
|
||||
'users.avatar',
|
||||
'users.is_separated',
|
||||
'users.created_at as user_created_at',
|
||||
'users.permission',
|
||||
'users.updated_at as user_updated_at',
|
||||
|
|
@ -94,6 +105,7 @@ class LeaveManagementController extends Controller
|
|||
'name' => $item->user_name,
|
||||
'email' => $item->email,
|
||||
'avatar' => $item->avatar,
|
||||
'is_separated' => $item->is_separated,
|
||||
'created_at' => $item->user_created_at,
|
||||
'permission' => $item->permission,
|
||||
'updated_at' => $item->user_updated_at,
|
||||
|
|
@ -103,9 +115,10 @@ class LeaveManagementController extends Controller
|
|||
'leaveDay' => [
|
||||
'id' => $item->id,
|
||||
'ld_user_id' => $item->ld_user_id,
|
||||
'ld_day' => $item->ld_day,
|
||||
'ld_day_total' => $item->ld_day_total,
|
||||
'ld_year' => $item->ld_year,
|
||||
'ld_date_additional' => $item->ld_date_additional,
|
||||
'ld_additional_day' => $item->ld_additional_day,
|
||||
'ld_special_leave_day' => $item->ld_special_leave_day,
|
||||
'ld_note' => $item->ld_note,
|
||||
'created_at' => $item->created_at,
|
||||
'updated_at' => $item->updated_at,
|
||||
|
|
@ -131,12 +144,66 @@ class LeaveManagementController extends Controller
|
|||
$validatedData = $request->all();
|
||||
$leaveDays = LeaveDays::find($validatedData['id']);
|
||||
|
||||
$leaveDays->ld_day = $validatedData['totalLeave'];
|
||||
$leaveDays->ld_date_additional = $validatedData['dayAdditional']; // Assuming you have this field to store additional days
|
||||
$leaveDays->ld_day_total = $validatedData['totalLeave'];
|
||||
$leaveDays->ld_additional_day = $validatedData['dayAdditional'];
|
||||
$leaveDays->ld_special_leave_day = $validatedData['specialLeave'];
|
||||
$leaveDays->ld_note = $validatedData['note'];
|
||||
|
||||
$leaveDays->save();
|
||||
|
||||
return response()->json(['status' => true, 'message' => 'Updated successfully']);
|
||||
}
|
||||
|
||||
public function updateNoteStatus(Request $request)
|
||||
{
|
||||
$rules = [
|
||||
'id' => 'required',
|
||||
'n_reason' => 'required|in:ONLEAVE,LEAVE_WITHOUT_PAY'
|
||||
];
|
||||
|
||||
// Validate the request
|
||||
$request->validate($rules);
|
||||
$id = $request->input('id');
|
||||
$reason = $request->input('n_reason');
|
||||
|
||||
$note = Notes::find($id);
|
||||
if (!$note) {
|
||||
return response()->json(['message' => 'Note not found', 'status' => false]);
|
||||
}
|
||||
|
||||
$note->n_reason = $reason;
|
||||
$note->save();
|
||||
|
||||
// Clear Timekeeping cache
|
||||
$ticket = Ticket::find($note->ticket_id);
|
||||
if ($ticket) {
|
||||
$this->createOrUpdateRecordForCurrentMonth(Carbon::parse($ticket->start_date)->month, Carbon::parse($ticket->start_date)->year);
|
||||
$this->createOrUpdateRecordForCurrentMonth(Carbon::parse($ticket->end_date)->month, Carbon::parse($ticket->end_date)->year);
|
||||
} else {
|
||||
$this->createOrUpdateRecordForCurrentMonth(Carbon::parse($note->created_at)->month, Carbon::parse($note->created_at)->year);
|
||||
}
|
||||
|
||||
return response()->json(data: ['message' => 'Update success', 'status' => true]);
|
||||
}
|
||||
|
||||
public function export(Request $request)
|
||||
{
|
||||
$year = $request->query('year', now()->year);
|
||||
$leaveDays = $this->getDataByYear($year);
|
||||
|
||||
if ($leaveDays->isEmpty()) {
|
||||
return response()->json(['status' => false, 'message' => 'No data found']);
|
||||
}
|
||||
|
||||
// Lọc chỉ lấy user có permission bao gồm staff
|
||||
$staffData = $leaveDays->filter(function ($user) {
|
||||
return isset($user['user']['permission']) && strpos($user['user']['permission'], 'staff') !== false;
|
||||
});
|
||||
|
||||
$currentDate = date('d_His');
|
||||
return Excel::download(
|
||||
new LeaveManagementExport($staffData),
|
||||
"LeaveManagement_{$year}_{$currentDate}.xlsx"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,14 @@ use App\Traits\HasFilterRequest;
|
|||
use App\Traits\HasOrderByRequest;
|
||||
use App\Traits\HasSearchRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Modules\Admin\app\Models\Admin;
|
||||
use Modules\Admin\app\Models\Sprint;
|
||||
use Modules\Admin\app\Models\UserCriteria;
|
||||
use App\Models\Files;
|
||||
use App\DataTransferObjects\FileData;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
|
|
@ -28,6 +33,7 @@ class ProfileController extends Controller
|
|||
$this->jiraService = $jiraService;
|
||||
}
|
||||
|
||||
|
||||
public function getProfilesData(Request $request)
|
||||
{
|
||||
$user = auth('admins')->user();
|
||||
|
|
@ -115,4 +121,353 @@ class ProfileController extends Controller
|
|||
$user->save();
|
||||
return AbstractController::ResultSuccess($path);
|
||||
}
|
||||
|
||||
public function listFiles(Request $request)
|
||||
{
|
||||
// Get the root folder from the input URL
|
||||
$rootFolder = $request->input('root_folder');
|
||||
|
||||
// Ensure the root folder is correctly formatted
|
||||
$rootFolder = rtrim($rootFolder, '/') . '/';
|
||||
|
||||
// Get all files and directories in the specified root folder
|
||||
$fileList = $this->getDirectoryTree(public_path($rootFolder), env('APP_ENV') === 'local' ? $rootFolder : 'image' . $rootFolder);
|
||||
|
||||
return response()->json(['data' => $fileList, 'status' => true]);
|
||||
}
|
||||
|
||||
private function getDirectoryTree($dir, $urlRoot)
|
||||
{
|
||||
$results = [];
|
||||
|
||||
// Scan the directory for files and folders
|
||||
$files = scandir($dir);
|
||||
|
||||
foreach ($files as $file) {
|
||||
if ($file !== '.' && $file !== '..') {
|
||||
$filePath = $dir . DIRECTORY_SEPARATOR . $file;
|
||||
$fileUrl = url($urlRoot . $file);
|
||||
|
||||
if (is_dir($filePath)) {
|
||||
// If it's a directory, recurse into it
|
||||
$results[] = [
|
||||
'label' => $file,
|
||||
'type' => 'directory',
|
||||
'value' => $fileUrl,
|
||||
'children' => $this->getDirectoryTree($filePath, $urlRoot . $file . '/')
|
||||
];
|
||||
} else {
|
||||
// If it's a file, add it to the list
|
||||
$results[] = [
|
||||
'label' => $file,
|
||||
'type' => 'file',
|
||||
'value' => $fileUrl
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function updateProfile(Request $request)
|
||||
{
|
||||
$name = $request->input('name') ?? auth('admins')->user()->name;
|
||||
// Validate the incoming files
|
||||
$request->validate([
|
||||
'files.*' => 'required|file|mimes:jpg,png,jpeg,pdf,doc,docx,xlsx,xls,csv|max:5120', // Adjust file types and size limit as needed
|
||||
]);
|
||||
|
||||
$uploadedFiles = [];
|
||||
$baseDirectory = 'profiles/' . $name;
|
||||
$othersDirectory = $baseDirectory . '/others';
|
||||
|
||||
// Check if the base directory exists, if not create it
|
||||
if (!Storage::disk('public')->exists($baseDirectory)) {
|
||||
Storage::disk('public')->makeDirectory($baseDirectory);
|
||||
}
|
||||
|
||||
// Check if the "others" directory exists, if not create it
|
||||
if (!Storage::disk('public')->exists($othersDirectory)) {
|
||||
Storage::disk('public')->makeDirectory($othersDirectory);
|
||||
}
|
||||
|
||||
$adminEmails = Admin::where('permission', 'like', '%admin%')->pluck('email')->toArray();
|
||||
$currentUser = auth('admins')->user();
|
||||
|
||||
if ($request->hasFile('files')) {
|
||||
foreach ($request->file('files') as $file) {
|
||||
// Store the file and get its path
|
||||
$originalFilename = $file->getClientOriginalName();
|
||||
if (strpos($originalFilename, '__') === 0) {
|
||||
// Store the file in the "others" directory
|
||||
$path = $file->storeAs($othersDirectory, $originalFilename, 'public');
|
||||
} else {
|
||||
// Store the file in the base directory
|
||||
$path = $file->storeAs($baseDirectory, $originalFilename, 'public');
|
||||
}
|
||||
$uploadedFiles[] = $path;
|
||||
|
||||
// Tạo URL đầy đủ cho file
|
||||
$fileUrl = (env('APP_ENV') === 'prod' || env('APP_ENV') === 'production')
|
||||
? env('APP_URL') . '/image/' . str_replace('/storage/', '', Storage::url($path))
|
||||
: env('APP_URL') . str_replace('/storage/', '', Storage::url($path));
|
||||
|
||||
// // Gửi email thông báo cho admin
|
||||
// foreach ($adminEmails as $adminEmail) {
|
||||
// $admin = Admin::where('email', $adminEmail)->first();
|
||||
// if ($admin) {
|
||||
// $this->sendFileUploadNotification(
|
||||
// $admin,
|
||||
// "File {$originalFilename} đã được tải lên bởi {$currentUser->name}",
|
||||
// $fileUrl,
|
||||
// "[APAC Tech] {$currentUser->name} - Đã tải lên file mới"
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Gửi email xác nhận cho người tải lên
|
||||
// $this->sendFileUploadNotification(
|
||||
// $currentUser,
|
||||
// "Bạn đã tải lên file {$originalFilename} thành công",
|
||||
// $fileUrl,
|
||||
// "[APAC Tech] {$currentUser->name} - Tải file thành công"
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => true,
|
||||
'message' => 'Files uploaded successfully',
|
||||
'files' => $uploadedFiles,
|
||||
]);
|
||||
}
|
||||
|
||||
public function removeFile(Request $request)
|
||||
{
|
||||
// Validate that the file URL is provided in the request
|
||||
$request->validate([
|
||||
'file_url' => 'required|string',
|
||||
]);
|
||||
|
||||
// Get the full file URL from the request
|
||||
$fileUrl = $request->input('file_url');
|
||||
|
||||
// Parse the file path from the URL (remove the base URL part)
|
||||
$storagePath = parse_url($fileUrl, PHP_URL_PATH); // Extract the path part of the URL
|
||||
$filePath = str_replace(env('APP_ENV') === 'local' ? '/storage/' : '/image/storage/', '', $storagePath); // Remove "/storage/" to get the actual file path
|
||||
|
||||
// Check if the file exists before attempting to delete it
|
||||
if (Storage::disk('public')->exists($filePath)) {
|
||||
// Delete the file
|
||||
Storage::disk('public')->delete($filePath);
|
||||
|
||||
return response()->json([
|
||||
'status' => true,
|
||||
'message' => 'File deleted successfully',
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => false,
|
||||
'message' => 'File not found',
|
||||
], 404);
|
||||
}
|
||||
|
||||
public function sendFileUploadNotification($user, $description, $url, $subject, $note)
|
||||
{
|
||||
try {
|
||||
// Gửi email bất đồng bộ không cần job
|
||||
dispatch(function() use ($user, $description, $url, $subject, $note) {
|
||||
Mail::send('emails.file_upload_notification', [
|
||||
'user' => $user,
|
||||
'description' => $description,
|
||||
'url' => $url,
|
||||
'note' => $note
|
||||
], function ($message) use ($user, $subject) {
|
||||
$message->to($user->email)
|
||||
->subject($subject);
|
||||
});
|
||||
})->afterResponse();
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error dispatching file upload notification email: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function uploadFiles(Request $request)
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'file' => 'required|file|mimes:jpg,jpeg,png,pdf,doc,docx,xls,xlsx,csv|max:5120',
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'user_name' => 'required|string|max:255'
|
||||
]);
|
||||
|
||||
$file = $request->file('file');
|
||||
$user = auth('admins')->user();
|
||||
|
||||
// Tạo thư mục cho user nếu chưa tồn tại
|
||||
$userFolder = 'files/' . $request->user_name;
|
||||
if (!Storage::disk('public')->exists($userFolder)) {
|
||||
Storage::disk('public')->makeDirectory($userFolder);
|
||||
}
|
||||
|
||||
$path = $file->store($userFolder, 'public');
|
||||
|
||||
$fileRecord = Files::create([
|
||||
'name' => $request->name,
|
||||
'url' => $path,
|
||||
'type' => $this->getFileType($file->getClientOriginalName()),
|
||||
'description' => $request->description,
|
||||
'user_id' => Admin::where('name', $request->user_name)->first()->id
|
||||
]);
|
||||
|
||||
$currentUser = Admin::where('name', $request->user_name)->first();
|
||||
// Gửi email thông báo cho người upload
|
||||
$fileUrl = (env('APP_ENV') === 'prod' || env('APP_ENV') === 'production')
|
||||
? env('APP_URL') . '/image' . Storage::url($path)
|
||||
: env('APP_URL') . Storage::url($path);
|
||||
$this->sendFileUploadNotification(
|
||||
$user,
|
||||
'Bạn đã tải lên file "' . $request->name . '" thành công',
|
||||
$fileUrl,
|
||||
"[APAC Tech] {$currentUser->name} - Đã tải lên file mới",
|
||||
$request->description ?? 'No description'
|
||||
);
|
||||
|
||||
// Gửi email thông báo cho tất cả admin khác
|
||||
$otherAdmins = Admin::where('permission', 'like', '%admin%')->get();
|
||||
foreach ($otherAdmins as $admin) {
|
||||
$this->sendFileUploadNotification(
|
||||
$admin,
|
||||
'File "' . $request->name . '" đã được tải lên bởi ' . $user->name,
|
||||
$fileUrl,
|
||||
"[APAC Tech] {$currentUser->name} - Đã tải lên file mới",
|
||||
$request->description ?? 'No description'
|
||||
);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => true,
|
||||
'message' => 'File uploaded successfully',
|
||||
'data' => [
|
||||
'id' => $fileRecord->id,
|
||||
'name' => $fileRecord->name,
|
||||
'url' => Storage::url($path),
|
||||
'type' => $fileRecord->type,
|
||||
'description' => $fileRecord->description
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => false,
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function getFiles()
|
||||
{
|
||||
try {
|
||||
// Lấy tất cả users
|
||||
$users = Admin::all();
|
||||
|
||||
// Lấy files và map theo cấu trúc
|
||||
$files = Files::with('user')->get()
|
||||
->map(function($file) {
|
||||
return [
|
||||
'id' => $file->id,
|
||||
'name' => $file->name,
|
||||
'url' => Storage::url($file->url),
|
||||
'type' => $file->type,
|
||||
'description' => $file->description,
|
||||
'created_at' => $file->created_at,
|
||||
'user_id' => $file->user_id,
|
||||
'user_name' => $file->user->name
|
||||
];
|
||||
});
|
||||
|
||||
// Tạo mảng kết quả với tất cả users, không có file thì mảng rỗng
|
||||
$result = $users->pluck('name')->mapWithKeys(function($userName) use ($files) {
|
||||
$userFiles = $files->where('user_name', $userName)
|
||||
->map(function($file) {
|
||||
return (object)[
|
||||
'id' => $file['id'],
|
||||
'name' => $file['name'],
|
||||
'url' => $file['url'],
|
||||
'type' => $file['type'],
|
||||
'description' => $file['description'],
|
||||
'created_at' => $file['created_at'],
|
||||
'user_id' => $file['user_id']
|
||||
];
|
||||
})->values();
|
||||
|
||||
return [$userName => $userFiles];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'status' => true,
|
||||
'data' => $result
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => false,
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteFile($id)
|
||||
{
|
||||
try {
|
||||
$file = Files::findOrFail($id);
|
||||
$user = auth('admins')->user();
|
||||
|
||||
if ($file->user_id !== $user->id) {
|
||||
return response()->json([
|
||||
'status' => false,
|
||||
'message' => 'Unauthorized'
|
||||
], 403);
|
||||
}
|
||||
|
||||
Storage::disk('public')->delete($file->url);
|
||||
$file->delete();
|
||||
|
||||
return response()->json([
|
||||
'status' => true,
|
||||
'message' => 'File deleted successfully'
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => false,
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
private function getFileType($filename)
|
||||
{
|
||||
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
|
||||
$typeMap = [
|
||||
'pdf' => 'document',
|
||||
'doc' => 'document',
|
||||
'docx' => 'document',
|
||||
'jpg' => 'image',
|
||||
'jpeg' => 'image',
|
||||
'png' => 'image',
|
||||
'xls' => 'spreadsheet',
|
||||
'xlsx' => 'spreadsheet',
|
||||
'csv' => 'spreadsheet'
|
||||
];
|
||||
|
||||
return $typeMap[$extension] ?? 'other';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
namespace Modules\Admin\app\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Modules\Admin\app\Http\Controllers\AbstractController;
|
||||
use Modules\Admin\app\Models\ProjectReview;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use DateTime;
|
||||
|
||||
class ProjectReviewController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function getListReviews(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'userID' => 'required|exists:users,id',
|
||||
'fromDate' => 'nullable|date',
|
||||
'toDate' => 'nullable|date',
|
||||
]);
|
||||
$userID = $request->input('userID');
|
||||
$startDate = $request->input('fromDate');
|
||||
$endDate = $request->input('toDate');
|
||||
$projectsData = ProjectReview::where('user_id', $userID);
|
||||
|
||||
if ($startDate && $endDate) {
|
||||
$projectsData->whereBetween('updated_at', [$startDate, $endDate . ' 23:59:59']);
|
||||
} elseif ($startDate) {
|
||||
$projectsData->where('updated_at', '>=', $startDate);
|
||||
} elseif ($endDate) {
|
||||
$projectsData->where('updated_at', '<=', $endDate . ' 23:59:59');
|
||||
}
|
||||
return AbstractController::ResultSuccess($projectsData->get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function create(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string',
|
||||
'role' => 'required|string',
|
||||
'note' => 'required|string',
|
||||
'user_id' => 'required|exists:users,id',
|
||||
]);
|
||||
|
||||
$review = ProjectReview::create($request->all());
|
||||
return response()->json($review, 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'sometimes|required|string',
|
||||
'role' => 'sometimes|required|string',
|
||||
'note' => 'sometimes|required|string',
|
||||
'user_id' => 'sometimes|required|exists:users,id',
|
||||
]);
|
||||
$id = $request->get('id');
|
||||
$projectReview = ProjectReview::find($id);
|
||||
$payload = $request->all();
|
||||
// if ($request->has('created_at')) {
|
||||
// $created_at = Carbon::create($request->get('created_at'))->setTimezone(env('TIME_ZONE'));
|
||||
// $payload['created_at'] = $created_at;
|
||||
// }
|
||||
if ($projectReview) {
|
||||
$projectReview->update($payload);
|
||||
}
|
||||
return response()->json([
|
||||
'data' => $projectReview,
|
||||
'status' => true
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Request $request)
|
||||
{
|
||||
$id = $request->get('id');
|
||||
ProjectReview::destroy($id);
|
||||
return response()->json(['message' => 'Deleted successfully', 'status' => true]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace Modules\Admin\app\Http\Controllers;
|
||||
use App\Traits\AnalyzeData;
|
||||
class DataAnalyzer
|
||||
{
|
||||
use AnalyzeData;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -11,10 +11,14 @@ use App\Traits\HasOrderByRequest;
|
|||
use App\Traits\HasSearchRequest;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use App\Mail\TicketMail;
|
||||
use Modules\Admin\app\Models\Admin;
|
||||
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
|
||||
{
|
||||
|
|
@ -74,14 +78,14 @@ class TimekeepingController extends Controller
|
|||
'user_id' => $user->id,
|
||||
'status' => 'check in',
|
||||
'time_string' => $start->format('Y-m-d H:i:s'),
|
||||
'created_at' => $start->setTimezone('UTC')
|
||||
'created_at' => $start->setTimezone(env('TIME_ZONE'))
|
||||
],
|
||||
[
|
||||
'name' => $user->name,
|
||||
'user_id' => $user->id,
|
||||
'status' => 'check out',
|
||||
'time_string' => $end->format('Y-m-d H:i:s'),
|
||||
'created_at' => $end->setTimezone('UTC')
|
||||
'created_at' => $end->setTimezone(env('TIME_ZONE'))
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
|
@ -150,8 +154,6 @@ class TimekeepingController extends Controller
|
|||
return response()->json(['status' => true, 'message' => 'Add successfully']);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function updateCacheMonth(Request $request)
|
||||
{
|
||||
$month = $request->month;
|
||||
|
|
@ -170,19 +172,97 @@ class TimekeepingController extends Controller
|
|||
|
||||
// Validate the request
|
||||
$request->validate($rules);
|
||||
|
||||
$id = $request->input('id');
|
||||
|
||||
$month = $request->month;
|
||||
$year = $request->year;
|
||||
|
||||
$note = Notes::find($id);
|
||||
if ($note) {
|
||||
$note->delete();
|
||||
$this->createOrUpdateRecordForCurrentMonth($month, $year);
|
||||
if (!$note) {
|
||||
return response()->json(['message' => 'Note not found', 'status' => false]);
|
||||
}
|
||||
|
||||
if ($note->ticket_id != null) {
|
||||
$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();
|
||||
|
||||
// Handle send mail
|
||||
$dataMasterStartPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $ticket->start_period);
|
||||
$dataMasterEndPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $ticket->end_period);
|
||||
$dataMasterType = CategoryController::getListMasterByCodeAndType("REASON", $ticket->type);
|
||||
$formattedStartDate = Carbon::createFromFormat('Y-m-d', $ticket->start_date)->format('d/m/Y');
|
||||
$formattedEndDate = Carbon::createFromFormat('Y-m-d', $ticket->end_date)->format('d/m/Y');
|
||||
|
||||
$user = Admin::find($ticket->user_id);
|
||||
|
||||
$data = array(
|
||||
"email_template" => "email.notification_tickets_user",
|
||||
"user_name" => $user->name,
|
||||
"email" => $user->email,
|
||||
"name" => $admin->name, //name admin duyệt
|
||||
"date" => $dataMasterStartPeriod->c_name . " (" . $formattedStartDate . ") - " . $dataMasterEndPeriod->c_name . " (" . $formattedEndDate . ")",
|
||||
"type" => $dataMasterType->c_name,
|
||||
"note" => $ticket->reason,
|
||||
"admin_note" => $ticket->admin_note,
|
||||
"link" => "/tickets", //link đến page admin
|
||||
"status" => "refused",
|
||||
"subject" => "[Ticket response] Ticket From " . $admin->name
|
||||
);
|
||||
Mail::to($user->email)->send(new TicketMail($data));
|
||||
|
||||
// Update
|
||||
$ticket->updated_by = $admin->name;
|
||||
$ticket->status = "REFUSED";
|
||||
$ticket->save();
|
||||
Notes::where('ticket_id', $ticket->id)->delete();
|
||||
|
||||
// Clear Timekeeping cache
|
||||
$this->createOrUpdateRecordForCurrentMonth(Carbon::parse($ticket->start_date)->month, Carbon::parse($ticket->start_date)->year);
|
||||
$this->createOrUpdateRecordForCurrentMonth(Carbon::parse($ticket->end_date)->month, Carbon::parse($ticket->end_date)->year);
|
||||
|
||||
return response()->json(['message' => 'Delete success', 'status' => true]);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Delete fail', 'status' => false]);
|
||||
$note->delete();
|
||||
$this->createOrUpdateRecordForCurrentMonth($month, $year);
|
||||
|
||||
return response()->json(['message' => 'Delete success', 'status' => true]);
|
||||
}
|
||||
|
||||
public function export(Request $request)
|
||||
{
|
||||
// Validate request
|
||||
$request->validate([
|
||||
'month' => 'required|numeric|between:1,12',
|
||||
'year' => 'required|numeric|min:2000',
|
||||
'working_days' => 'required|numeric|between:1,31'
|
||||
]);
|
||||
|
||||
// Reuse get() function to fetch data
|
||||
$response = $this->get($request);
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
|
||||
if (!$responseData['status']) {
|
||||
return response()->json(['status' => false, 'message' => 'No data found']);
|
||||
}
|
||||
|
||||
// Lọc chỉ lấy user có permission bao gồm staff
|
||||
$staffData = array_filter($responseData['data'], function ($user) {
|
||||
return isset($user['user']['permission']) && strpos($user['user']['permission'], 'staff') !== false;
|
||||
});
|
||||
|
||||
$currentDate = date('d_His');
|
||||
return Excel::download(
|
||||
new TimekeepingExport(
|
||||
array_values($staffData), // Convert to indexed array after filtering
|
||||
$request->month,
|
||||
$request->year,
|
||||
$request->working_days
|
||||
),
|
||||
"Timekeeping_{$request->month}_{$request->year}_{$currentDate}.xlsx"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
namespace Modules\Admin\app\Http\Controllers;
|
||||
|
||||
use App\Helper\Cache\CurrentMonthTimekeeping;
|
||||
use App\Events\WarningChekinLate;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Mail\CheckinLateMail;
|
||||
use App\Traits\AnalyzeData;
|
||||
use App\Traits\HasFilterRequest;
|
||||
use App\Traits\HasOrderByRequest;
|
||||
|
|
@ -11,10 +12,12 @@ use App\Traits\HasSearchRequest;
|
|||
use Carbon\Carbon;
|
||||
use DateTime;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Admin\app\Models\Admin;
|
||||
use Modules\Admin\app\Models\MonthlyTimekeeping;
|
||||
use Modules\Admin\app\Models\Tracking;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TrackingController extends Controller
|
||||
{
|
||||
|
|
@ -163,14 +166,9 @@ class TrackingController extends Controller
|
|||
$id = $request->get('id');
|
||||
|
||||
$tracking = Tracking::find($id);
|
||||
// $id = $request->get('id');
|
||||
|
||||
// $tracking = Tracking::find($id);
|
||||
// $payload = $request->all();
|
||||
|
||||
// if ($tracking) {
|
||||
// $tracking->update($payload);
|
||||
// }
|
||||
$user = Admin::find($tracking->user_id);
|
||||
$this->sendCheckinLateMail($user, $tracking->created_at, $tracking->status);
|
||||
if ($request->hasFile('image')) {
|
||||
$file = $request->file('image');
|
||||
$filename = $request->file_name;
|
||||
|
|
@ -203,4 +201,163 @@ class TrackingController extends Controller
|
|||
'status' => true
|
||||
]);
|
||||
}
|
||||
|
||||
// public function sendCheckinLateMail()
|
||||
private function sendCheckinLateMail($user, $time, $status)
|
||||
{
|
||||
// $status = $this->CHECK_IN;
|
||||
// $lastCheck =Tracking::find(1)->created_at;
|
||||
// $user = Admin::where('name', 'LE TAN LUAN')->first();
|
||||
if ($status === $this->CHECK_IN) {
|
||||
$morning_time = Carbon::createFromTimeString('07:30AM')->setTimezone(env('TIME_ZONE'));
|
||||
$morning_condition_time = Carbon::createFromTimeString('07:40AM')->setTimezone(env('TIME_ZONE'));
|
||||
$afternoon_time = Carbon::createFromTimeString('11:30AM')->setTimezone(env('TIME_ZONE'));
|
||||
$afternoon_condition_time = Carbon::createFromTimeString('01:10PM')->setTimezone(env('TIME_ZONE'));
|
||||
$admin_mails = Admin::where('permission', 'like', '%admin%')->pluck('email');
|
||||
|
||||
if ($time->greaterThan($morning_condition_time) && $time->lessThan($afternoon_time)) {
|
||||
$period = 'morning';
|
||||
$minutes_late = $morning_time->diffInMinutes($time);
|
||||
$data = array(
|
||||
"email_template" => "email.checkin_late_notification",
|
||||
"email" => $user->email,
|
||||
"name" => $user->name,
|
||||
"admin_mails" => $admin_mails,
|
||||
"message1" => "Your " . $period . " starts " . $minutes_late . " minutes late",
|
||||
"message2" => "You checked in at [" . $time . "]",
|
||||
"url" => env('ADMIN_URL') . "/tracking?search=&per_page=10&page=1&timezone=Asia%2FSaigon&name=" . $user->name . "&time_string=" . $time->format("Y-m-d H:i") . "&status=check+in",
|
||||
"subject" => "[Management System] Late warning - " . $user->name
|
||||
);
|
||||
Mail::to($user->email)->cc($admin_mails)->send(new CheckinLateMail($data));
|
||||
}
|
||||
|
||||
// if ($time->greaterThan($afternoon_condition_time))
|
||||
// {
|
||||
// $period = 'afternoon';
|
||||
// $minutes_late = $afternoon_time->diffInMinutes($time);
|
||||
// $data = array(
|
||||
// "email_template" => "email.checkin_late_notification",
|
||||
// "email" => $user->email,
|
||||
// "name" => $user->name,
|
||||
// "message1" => "Your ". $period ." starts <b>". $minutes_late ."</b> minutes late",
|
||||
// "message2" => "You checked in at <b>[" . $time ."]</b>",
|
||||
// "url" => env('ADMIN_URL')."/tracking?search=&per_page=10&page=1&timezone=Asia%2FSaigon&name=".$user->name."&time_string=".$time->format("Y-m-d H:i")."&status=check+in",
|
||||
// "subject" => "[Management System] Late warning"
|
||||
// );
|
||||
// Mail::to($user->email)->cc($admin_mails)->send(new CheckinLateMail($data));
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public function getSummaryTracking(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'userID' => 'required|exists:users,id',
|
||||
'fromDate' => 'nullable|date',
|
||||
'toDate' => 'nullable|date',
|
||||
]);
|
||||
|
||||
$userID = $request->input('userID');
|
||||
$startDate = $request->input('fromDate');
|
||||
$endDate = $request->input('toDate');
|
||||
|
||||
$trackingData = Tracking::where('user_id', $userID);
|
||||
|
||||
if ($startDate && $endDate) {
|
||||
$trackingData->whereBetween(
|
||||
DB::raw("STR_TO_DATE(time_string, '%Y-%m-%d %H:%i:%s')"),
|
||||
[$startDate, $endDate . ' 23:59:59']
|
||||
);
|
||||
} elseif ($startDate) {
|
||||
$trackingData->where(
|
||||
DB::raw("STR_TO_DATE(time_string, '%Y-%m-%d %H:%i:%s')"),
|
||||
'>=',
|
||||
$startDate
|
||||
);
|
||||
} elseif ($endDate) {
|
||||
$trackingData->where(
|
||||
DB::raw("STR_TO_DATE(time_string, '%Y-%m-%d %H:%i:%s')"),
|
||||
'<=',
|
||||
$endDate . ' 23:59:59'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
$onTimeMorning = 0;
|
||||
$lateMorning = 0;
|
||||
$onTimeAfternoon = 0;
|
||||
$lateAfternoon = 0;
|
||||
$returnTracking = $trackingData->get();
|
||||
$listLate = [];
|
||||
|
||||
$returnTracking->groupBy(function ($record) {
|
||||
return Carbon::parse($record->time_string)->toDateString();
|
||||
})->each(function ($records, $date) use ($userID, &$listLate, &$onTimeMorning, &$lateMorning, &$onTimeAfternoon, &$lateAfternoon, &$datesChecked) {
|
||||
$morningCheck = $records->filter(function ($record) {
|
||||
return Carbon::parse($record->time_string)->hour < 12;
|
||||
})->sortBy('time_string')->first();
|
||||
|
||||
$afternoonCheck = $records->filter(function ($record) {
|
||||
$time = Carbon::parse($record->time_string)->hour;
|
||||
return $time >= 12 && $time <= 14;
|
||||
})->sortBy('time_string')->first();
|
||||
|
||||
$morningTime = Carbon::parse($date)->setTime(7, 40, 0);
|
||||
$afternoonTime = Carbon::parse($date)->setTime(13, 10, 0);
|
||||
$checkOutAfternoonTime = Carbon::parse($date)->setTime(15, 00, 0);
|
||||
|
||||
|
||||
if ($morningCheck) {
|
||||
$checkInTime = Carbon::parse($morningCheck->time_string);
|
||||
if ($checkInTime->lessThanOrEqualTo($morningTime)) {
|
||||
$onTimeMorning++;
|
||||
} else {
|
||||
array_push($listLate, $morningCheck->id);
|
||||
$lateMorning++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($afternoonCheck) {
|
||||
$checkInTime = Carbon::parse($afternoonCheck->time_string);
|
||||
if ($checkInTime->lessThanOrEqualTo($afternoonTime)) {
|
||||
$onTimeAfternoon++;
|
||||
} else {
|
||||
array_push($listLate, $afternoonCheck->id);
|
||||
$lateAfternoon++;
|
||||
}
|
||||
} else {
|
||||
// print ($date . "\n");
|
||||
$checkOutAfternoon = Tracking::where('user_id', $userID)->whereBetween(
|
||||
DB::raw("STR_TO_DATE(time_string, '%Y-%m-%d %H:%i:%s')"),
|
||||
[$date, $date . ' 23:59:59'],
|
||||
)->where("status", "check out")->first();
|
||||
// check if not check-in afternoon but has check out
|
||||
if ($checkOutAfternoon) {
|
||||
$timeCheck = Carbon::parse($checkOutAfternoon->time_string);
|
||||
// print ($timeCheck . "\n");
|
||||
if ($checkOutAfternoon && $timeCheck->greaterThanOrEqualTo($checkOutAfternoonTime)) {
|
||||
$onTimeAfternoon++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// ** Add `isLate` flag to each record **
|
||||
foreach ($returnTracking as $record) {
|
||||
if (in_array($record->id, $listLate)) {
|
||||
$record->isLate = true;
|
||||
}
|
||||
}
|
||||
|
||||
return AbstractController::ResultSuccess([
|
||||
'on_time_morning' => $onTimeMorning,
|
||||
'late_morning' => $lateMorning,
|
||||
'on_time_afternoon' => $onTimeAfternoon,
|
||||
'late_afternoon' => $lateAfternoon,
|
||||
'value' => $returnTracking
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,4 +8,12 @@ use Illuminate\Database\Eloquent\Model;
|
|||
class Category extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'c_type',
|
||||
'c_name',
|
||||
'c_code',
|
||||
'c_value',
|
||||
'c_active',
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace Modules\Admin\app\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Document extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['title', 'uri', 'type', 'is_active'];
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace Modules\Admin\app\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Traits\HasCacheModel;
|
||||
|
||||
class ElectricityBill extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use HasCacheModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->table = 'electricity_bills';
|
||||
$this->guarded = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate total amount based on reading difference and unit price
|
||||
*/
|
||||
public function calculateTotal(): float
|
||||
{
|
||||
$consumption = $this->current_reading - $this->previous_reading;
|
||||
return round($consumption * $this->unit_price, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get consumption in kWh
|
||||
*/
|
||||
public function getConsumption(): float
|
||||
{
|
||||
return $this->current_reading - $this->previous_reading;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user who created this record
|
||||
*/
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user who updated this record
|
||||
*/
|
||||
public function updater()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\User::class, 'updated_by');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace Modules\Admin\app\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Equipment extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->table = 'equipments';
|
||||
$this->guarded = [];
|
||||
}
|
||||
|
||||
public function currentUser()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\User::class, 'current_user_id');
|
||||
}
|
||||
|
||||
public function assignments()
|
||||
{
|
||||
return $this->hasMany(EquipmentAssignment::class, 'equipment_id')->orderBy('assigned_at', 'desc');
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function updater()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\User::class, 'updated_by');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace Modules\Admin\app\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class EquipmentAssignment extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->table = 'equipment_assignments';
|
||||
$this->guarded = [];
|
||||
}
|
||||
|
||||
public function equipment()
|
||||
{
|
||||
return $this->belongsTo(Equipment::class, 'equipment_id');
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\User::class, 'user_id');
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\User::class, 'created_by');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace Modules\Admin\app\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ProjectReview extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['name', 'role', 'note', 'user_id'];
|
||||
|
||||
// Relationship: A review belongs to a user
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Bảng kê thanh toán tiền điện</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: DejaVu Sans, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.mt-20 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 15px;
|
||||
}
|
||||
table, th, td {
|
||||
border: 1px solid #000;
|
||||
}
|
||||
th, td {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.no-border {
|
||||
border: none;
|
||||
}
|
||||
.signature {
|
||||
width: 100%;
|
||||
margin-top: 50px;
|
||||
}
|
||||
.signature td {
|
||||
border: none;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h3 class="text-center">BẢNG KÊ THANH TOÁN TIỀN ĐIỆN</h3>
|
||||
<p class="text-center">({{ $dateNow ?? '' }})</p>
|
||||
|
||||
<div class="mt-20">
|
||||
<p>- Tên doanh nghiệp: Công ty TNHH Kỹ Thuật Công Nghệ APAC</p>
|
||||
<p>- Mã số thuế: 0110038408</p>
|
||||
<p>- Địa chỉ: Số 219/26/3 đường Lĩnh Nam, Phường Vĩnh Hưng, thành phố Hà Nội, Việt Nam</p>
|
||||
<p>- Tên chủ sở hữu cho thuê địa điểm sản xuất kinh doanh: Lâm Văn Mười</p>
|
||||
<p>- Địa chỉ thuê: 50B31 tại Khu dân cư 91B giai đoạn 3, phường Tân An, thành phố Cần Thơ</p>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Số điện đầu kỳ</th>
|
||||
<th>Số điện cuối kỳ</th>
|
||||
<th>Số điện tiêu thụ</th>
|
||||
<th>Đơn giá</th>
|
||||
<th>Thành tiền</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ number_format($bill->previous_reading) ?? 0 }}</td>
|
||||
<td>{{ number_format($bill->current_reading) ?? 0 }}</td>
|
||||
<td>{{ $consumption ?? 0 }}</td>
|
||||
<td>{{ number_format($bill->unit_price) ?? '0' }}</td>
|
||||
<td>{{ number_format($bill->total_amount) ?? '0' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p class="mt-20">
|
||||
- Tổng tiền thanh toán: <strong>{{ number_format($bill->total_amount) ?? '0' }} VND</strong>
|
||||
({{ $totalText ?? '' }})
|
||||
</p>
|
||||
|
||||
<table class="signature no-border">
|
||||
<tr>
|
||||
<td>
|
||||
Người lập bảng kê<br>
|
||||
(Ký, ghi rõ họ tên)
|
||||
</td>
|
||||
<td>
|
||||
Đại diện doanh nghiệp<br>
|
||||
(Ký, ghi rõ họ tên)
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use App\Http\Middleware\CheckPermission;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Admin\app\Http\Controllers\AdminController;
|
||||
use Modules\Admin\app\Http\Controllers\BannerController;
|
||||
use Modules\Admin\app\Http\Controllers\CategoryController;
|
||||
|
|
@ -9,6 +10,7 @@ use Modules\Admin\app\Http\Controllers\ClientController;
|
|||
use Modules\Admin\app\Http\Controllers\CountryController;
|
||||
use Modules\Admin\app\Http\Controllers\CustomThemeController;
|
||||
use Modules\Admin\app\Http\Controllers\DashboardController;
|
||||
use Modules\Admin\app\Http\Controllers\DocumentController;
|
||||
use Modules\Admin\app\Http\Controllers\JiraController;
|
||||
use Modules\Admin\app\Http\Controllers\LeaveManagementController;
|
||||
use Modules\Admin\app\Http\Controllers\SettingController;
|
||||
|
|
@ -17,9 +19,12 @@ use Modules\Admin\app\Http\Controllers\TimekeepingController;
|
|||
use Modules\Admin\app\Http\Controllers\TrackingController;
|
||||
use Modules\Admin\app\Http\Controllers\CriteriasController;
|
||||
use Modules\Admin\app\Http\Controllers\EvaluationController;
|
||||
use Modules\Admin\app\Http\Controllers\ProjectReviewController;
|
||||
use Modules\Admin\app\Http\Controllers\ProfileController;
|
||||
use Modules\Admin\app\Http\Controllers\TechnicalController;
|
||||
use Modules\Admin\app\Http\Controllers\TestCaseForSprintController;
|
||||
use Modules\Admin\app\Http\Controllers\ElectricityBillController;
|
||||
use Modules\Admin\app\Http\Controllers\EquipmentController;
|
||||
use Modules\Admin\app\Http\Middleware\AdminMiddleware;
|
||||
|
||||
/*
|
||||
|
|
@ -41,6 +46,7 @@ Route::middleware('api')
|
|||
Route::post('login', [AdminController::class, 'login']);
|
||||
Route::post('reset-password', [AdminController::class, 'resetPassword']);
|
||||
Route::get('forgot-password', [AdminController::class, 'forgotPassword']);
|
||||
Route::get('/email-handle-ticket', [TicketController::class, 'handleTicketEmail'])->name('email.ticket.handle');
|
||||
});
|
||||
|
||||
// NOTE after login
|
||||
|
|
@ -111,21 +117,25 @@ Route::middleware('api')
|
|||
Route::get('/get-all-sprint-by-id-board', [JiraController::class, 'getAllSprintByIdBoard'])->middleware('check.permission:admin.tester');
|
||||
Route::get('/get-all-issue-by-id-sprint', [JiraController::class, 'getAllIssueByIdSprint']);
|
||||
|
||||
Route::get('/export-weekly-report', [JiraController::class, 'getWeeklyReport']);
|
||||
|
||||
Route::get('/all-issue-by-project', [JiraController::class, 'fetchIssuesByProject']);
|
||||
Route::get('/worklogs', [JiraController::class, 'getAllUserWorkLogs'])->middleware('check.permission:admin.staff');
|
||||
Route::get('/allocation', [JiraController::class, 'getAllUserDoing'])->middleware('check.permission:admin.staff');
|
||||
Route::get('/issue/detail', [JiraController::class, 'getDetailIssueById'])->middleware('check.permission:admin.staff');
|
||||
Route::get('/project-participating', [JiraController::class, 'getUserProjectParticipating'])->middleware('check.permission:admin');
|
||||
});
|
||||
|
||||
Route::group([
|
||||
'prefix' => 'timekeeping',
|
||||
], function () {
|
||||
Route::get('/', [TimekeepingController::class, 'get'])->middleware('check.permission:admin.hr.staff');
|
||||
Route::get('/', [TimekeepingController::class, 'get'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::post('/addMutilple', [TimekeepingController::class, 'addWorkingTimeForMultipleUser'])->middleware('check.permission:admin.hr');
|
||||
Route::post('/addNote', [TimekeepingController::class, 'addNoteForUser'])->middleware('check.permission:admin.hr');
|
||||
Route::get('/delete', [TimekeepingController::class, 'deleteNote'])->middleware('check.permission:admin.hr');
|
||||
Route::post('/update-cache-month', [TimekeepingController::class, 'updateCacheMonth'])->middleware('check.permission:admin');
|
||||
Route::post('/update-working-days', [TimekeepingController::class, 'saveWorkingDays'])->middleware('check.permission:admin.hr');
|
||||
Route::get('/export', [TimekeepingController::class, 'export'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
});
|
||||
|
||||
Route::group([
|
||||
|
|
@ -134,19 +144,24 @@ Route::middleware('api')
|
|||
Route::post('/create', [TrackingController::class, 'create'])->middleware('check.permission:admin.hr');
|
||||
Route::post('/update', [TrackingController::class, 'update'])->middleware('check.permission:admin.hr');
|
||||
Route::get('/delete', [TrackingController::class, 'delete'])->middleware('check.permission:admin.hr');
|
||||
Route::get('/summary', [TrackingController::class, 'getSummaryTracking'])->middleware('check.permission:admin');
|
||||
});
|
||||
|
||||
Route::group([
|
||||
'prefix' => 'category',
|
||||
], function () {
|
||||
Route::get('/get-list-master', [CategoryController::class, 'getListMaster']);
|
||||
Route::get('/work-days', [CategoryController::class, 'workDays'])->middleware('check.permission:admin.hr');
|
||||
Route::put('/update-work-days', [CategoryController::class, 'updateWorkDays'])->middleware('check.permission:admin.hr');
|
||||
});
|
||||
|
||||
Route::group([
|
||||
'prefix' => 'leave-management',
|
||||
], function () {
|
||||
Route::get('/', [LeaveManagementController::class, 'get'])->middleware('check.permission:admin.hr.staff');
|
||||
Route::get('/', [LeaveManagementController::class, 'get'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::get('/export', [LeaveManagementController::class, 'export'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::post('/saveNoteLeave', [LeaveManagementController::class, 'saveNoteLeave'])->middleware('check.permission:admin.hr');
|
||||
Route::post('/updateNoteStatus', [LeaveManagementController::class, 'updateNoteStatus'])->middleware('check.permission:admin.hr');
|
||||
});
|
||||
|
||||
Route::group([
|
||||
|
|
@ -154,11 +169,55 @@ 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');
|
||||
});
|
||||
|
||||
// Electricity Bills
|
||||
Route::group([
|
||||
'prefix' => 'electricity-bill',
|
||||
], function () {
|
||||
Route::get('/', [ElectricityBillController::class, 'index'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::get('/{id}', [ElectricityBillController::class, 'show'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::post('/create', [ElectricityBillController::class, 'create'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::put('/{id}', [ElectricityBillController::class, 'update'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::get('/delete/{id}', [ElectricityBillController::class, 'delete'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::get('/export-pdf/{id}', [ElectricityBillController::class, 'exportPdf'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::post('/upload-attachment/{id}', [ElectricityBillController::class, 'uploadAttachment'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::delete('/delete-attachment/{id}', [ElectricityBillController::class, 'deleteAttachment'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
});
|
||||
|
||||
// Equipments
|
||||
Route::group([
|
||||
'prefix' => 'equipment',
|
||||
], function () {
|
||||
Route::get('/', [EquipmentController::class, 'index'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::get('/users', [EquipmentController::class, 'userOptions'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::get('/user-statistics', [EquipmentController::class, 'userStatistics'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::get('/export', [EquipmentController::class, 'export'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::get('/by-code', [EquipmentController::class, 'showByCode'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::get('/history/{id}', [EquipmentController::class, 'history'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::post('/create', [EquipmentController::class, 'create'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::put('/{id}', [EquipmentController::class, 'update'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::post('/{id}/assign', [EquipmentController::class, 'assign'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::post('/{id}/upload-image', [EquipmentController::class, 'uploadImage'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::delete('/{id}/image', [EquipmentController::class, 'deleteImage'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::get('/delete/{id}', [EquipmentController::class, 'delete'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
});
|
||||
|
||||
Route::group([
|
||||
'prefix' => 'profile',
|
||||
], function () {
|
||||
Route::get('/all-files', [ProfileController::class, 'listFiles'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::post('/update-profile', [ProfileController::class, 'updateProfile'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::get('/delete-profile-file', [ProfileController::class, 'removeFile'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::get('/files', [ProfileController::class, 'getFiles'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::post('/upload-files', [ProfileController::class, 'uploadFiles'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::delete('/files/{id}', [ProfileController::class, 'deleteFile'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
});
|
||||
|
||||
Route::group([
|
||||
'prefix' => 'criterias',
|
||||
], function () {
|
||||
|
|
@ -182,6 +241,11 @@ Route::middleware('api')
|
|||
Route::get('/sprint-review', [EvaluationController::class, 'sprintReview'])->middleware('check.permission:admin');
|
||||
Route::get('/technical', [EvaluationController::class, 'technical'])->middleware('check.permission:admin');
|
||||
Route::get('/report', [EvaluationController::class, 'report'])->middleware('check.permission:admin');
|
||||
Route::get('/report-all-users', [EvaluationController::class, 'reportAllUsers'])->middleware('check.permission:admin');
|
||||
Route::get('/project-review', [ProjectReviewController::class, 'getListReviews'])->middleware('check.permission:admin');
|
||||
Route::post('/project-review/create', [ProjectReviewController::class, 'create'])->middleware('check.permission:admin');
|
||||
Route::post('/project-review/update', [ProjectReviewController::class, 'update'])->middleware('check.permission:admin');
|
||||
Route::get('/project-review/delete', [ProjectReviewController::class, 'destroy'])->middleware('check.permission:admin');
|
||||
});
|
||||
|
||||
Route::group([
|
||||
|
|
@ -196,6 +260,32 @@ Route::middleware('api')
|
|||
Route::get('/get-list-user-by-tech-id/{technicalId}', [TechnicalController::class, 'getListUserByTechnicalId'])->middleware('check.permission:admin');
|
||||
Route::post('/technicals-user/update', [TechnicalController::class, 'updateTechnicalsUser']);
|
||||
});
|
||||
|
||||
Route::group([
|
||||
'prefix' => 'document',
|
||||
], function () {
|
||||
Route::get('/all', [DocumentController::class, 'all'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::post('/create', [DocumentController::class, 'create'])->middleware('check.permission:admin');
|
||||
Route::post('/update', [DocumentController::class, 'update'])->middleware('check.permission:admin');
|
||||
Route::get('/delete', [DocumentController::class, 'delete'])->middleware('check.permission:admin');
|
||||
});
|
||||
|
||||
Route::get('/download-file/{filename}', function ($filename) {
|
||||
$path = "uploads/{$filename}";
|
||||
|
||||
if (!Storage::disk('public')->exists($path)) {
|
||||
return response()->json(['error' => 'File not found'], 404);
|
||||
}
|
||||
|
||||
$headers = [
|
||||
'Access-Control-Allow-Origin' => '*',
|
||||
'Access-Control-Allow-Methods' => 'GET',
|
||||
'Access-Control-Allow-Headers' => 'Content-Type',
|
||||
'Content-Disposition' => 'inline; filename="' . $filename . '"',
|
||||
];
|
||||
|
||||
return response()->file(storage_path("app/public/{$path}"), $headers);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -212,4 +302,5 @@ Route::group([
|
|||
'prefix' => 'v1/admin/jira',
|
||||
], function () {
|
||||
Route::get('/send-worklog-report', [JiraController::class, 'sendReport']);
|
||||
Route::get('/send-warning-mail', [JiraController::class, 'sendWarningMailByAllowcation']);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,18 +3,23 @@
|
|||
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;
|
||||
use App\Traits\AnalyzeData;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
use IsAPI;
|
||||
use AnalyzeData;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('jwt.auth');
|
||||
|
|
@ -34,9 +39,41 @@ class UserController extends Controller
|
|||
]);
|
||||
|
||||
if ($request->has('id')) {
|
||||
$payload = $request->only(['name', 'email', 'permission']);
|
||||
$payload = $request->only(['name', 'email', 'permission', 'is_permanent', 'is_separated']);
|
||||
$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.']);
|
||||
}
|
||||
|
||||
if (!$request->is_separated && $user->is_separated) {
|
||||
return response()->json(['status' => false, 'message' => 'You cannot change status of separated employee.']);
|
||||
}
|
||||
|
||||
// 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,9 +81,23 @@ class UserController extends Controller
|
|||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => bcrypt('Work@1234'),
|
||||
'permission' => $request->permission
|
||||
'permission' => $request->permission,
|
||||
'is_permanent' => false,
|
||||
'is_separated' => 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(),
|
||||
]);
|
||||
$this->createOrUpdateRecordForCurrentMonth(Carbon::now()->month, Carbon::now()->year);
|
||||
|
||||
$user_res = [
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
|
|
@ -98,8 +149,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)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,10 @@ class User extends Authenticatable implements JWTSubject
|
|||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'permission'
|
||||
'permission',
|
||||
'is_permanent',
|
||||
'is_separated',
|
||||
'permanent_date'
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Jobs\AddMonthlyLeaveDays;
|
||||
|
||||
class AddMonthlyLeaveDaysCommand extends Command
|
||||
{
|
||||
protected $signature = 'add:monthly-leavedays {month?} {year?}';
|
||||
protected $description = 'Cộng 1 ngày phép hàng tháng cho tất cả người dùng';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$month = $this->argument('month');
|
||||
$year = $this->argument('year');
|
||||
AddMonthlyLeaveDays::dispatch($month, $year);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\CheckUserAttendanceJob;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CheckUserAttendanceCommand extends Command
|
||||
{
|
||||
// Định nghĩa command signature
|
||||
protected $signature = 'attendance:check {period?}';
|
||||
protected $description = 'Kiểm tra check in và check out của người dùng và tạo ticket nếu thiếu';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
// Lấy argument period (Sáng "S" hoặc Chiều "C")
|
||||
$period = $this->argument('period');
|
||||
|
||||
// Dispatch job để kiểm tra check in và check out
|
||||
CheckUserAttendanceJob::dispatch($period);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ use App\Jobs\InitializeLeaveDays;
|
|||
class InitializeLeaveDaysCommand extends Command
|
||||
{
|
||||
protected $signature = 'initialize:leavedays {year?}';
|
||||
protected $description = 'Initialize leave days for users';
|
||||
protected $description = 'Cấp phép năm cho tất cả người dùng';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
|
@ -17,7 +17,6 @@ class InitializeLeaveDaysCommand extends Command
|
|||
|
||||
public function handle()
|
||||
{
|
||||
$year = $this->argument('year');
|
||||
InitializeLeaveDays::dispatch($year);
|
||||
InitializeLeaveDays::dispatch();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Jobs\UpdateTemporaryLeaveDays;
|
||||
|
||||
class UpdateTemporaryLeaveDaysCommand extends Command
|
||||
{
|
||||
protected $signature = 'update:temporary-leavedays {month?} {year?}';
|
||||
protected $description = 'Tính lại ngày phép cho các note tạm.';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$month = $this->argument('month');
|
||||
$year = $this->argument('year');
|
||||
UpdateTemporaryLeaveDays::dispatch($month, $year);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Console;
|
||||
|
||||
use App\Jobs\DeductLeaveDays;
|
||||
use App\Jobs\AddMonthlyLeaveDays;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
|
|
@ -23,9 +24,21 @@ class Kernel extends ConsoleKernel
|
|||
// $schedule->command('daily:api-call')
|
||||
// ->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('leave:deduct')->yearlyOn(3, 31, '23:59:59');
|
||||
// Chạy command vào ngày 01/01 lúc 00:00 mỗi năm
|
||||
$schedule->command('initialize:leavedays')->yearlyOn(1, 1, '00:00');
|
||||
|
||||
// Chạy command vào ngày 01/04 lúc 00:00 mỗi năm
|
||||
$schedule->command('leave:deduct')->yearlyOn(4, 1, '00:00');
|
||||
|
||||
// Chạy buổi sáng lúc 12:00
|
||||
$schedule->command('attendance:check S')->dailyAt('12:00');
|
||||
|
||||
// Chạy buổi chiều lúc 17:30
|
||||
$schedule->command('attendance:check C')->dailyAt('17:30');
|
||||
|
||||
// Chạy vào ngày đầu tiên của mỗi tháng
|
||||
$schedule->command('add:monthly-leavedays')->monthlyOn(1, '00:01');
|
||||
$schedule->command('update:temporary-leavedays')->monthlyOn(1, '00:05');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,194 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Maatwebsite\Excel\Concerns\FromArray;
|
||||
use Maatwebsite\Excel\Concerns\WithEvents;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use Maatwebsite\Excel\Concerns\WithStyles;
|
||||
use Maatwebsite\Excel\Events\AfterSheet;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
|
||||
class LeaveManagementExport implements FromArray, WithHeadings, WithStyles, WithEvents
|
||||
{
|
||||
protected $data;
|
||||
protected $year;
|
||||
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
$this->year = Carbon::now()->year;
|
||||
}
|
||||
|
||||
public function headings(): array
|
||||
{
|
||||
$months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
return array_merge(
|
||||
['No.', 'User'],
|
||||
$months,
|
||||
['Total', 'Off', 'Remaining', 'Notes']
|
||||
);
|
||||
}
|
||||
|
||||
public function array(): array
|
||||
{
|
||||
$headers = $this->headings(); // Lấy tiêu đề
|
||||
|
||||
$rows = [];
|
||||
$stt = 0;
|
||||
foreach ($this->data as $index => $user) {
|
||||
$totalDayOff = 0;
|
||||
$totalDayLeave = $user['leaveDay']['ld_day_total'] + $user['leaveDay']['ld_additional_day'];
|
||||
|
||||
// Tính tổng ngày nghỉ theo tháng
|
||||
$monthlyLeaves = array_fill(1, 12, 0);
|
||||
foreach ($user['monthlyLeaveDays'] as $leaveDay) {
|
||||
$monthlyLeaves[$leaveDay['month']] += $leaveDay['leave_days'];
|
||||
$totalDayOff += $leaveDay['leave_days'];
|
||||
}
|
||||
|
||||
// Tạo dòng dữ liệu
|
||||
$row = [
|
||||
$stt + 1,
|
||||
$user['user']['name']
|
||||
];
|
||||
|
||||
// Thêm dữ liệu các tháng
|
||||
for ($month = 1; $month <= 12; $month++) {
|
||||
$row[] = $monthlyLeaves[$month] ?: '';
|
||||
}
|
||||
|
||||
// Thêm tổng số ngày
|
||||
$row[] = $totalDayLeave;
|
||||
$row[] = $totalDayOff;
|
||||
$row[] = $totalDayLeave - $totalDayOff;
|
||||
$row[] = $user['leaveDay']['ld_note'] ?? '';
|
||||
|
||||
$rows[] = $row;
|
||||
$stt++;
|
||||
}
|
||||
|
||||
return array_merge([$headers], $rows); // Thêm tiêu đề vào đầu mảng
|
||||
}
|
||||
|
||||
public function registerEvents(): array
|
||||
{
|
||||
return [
|
||||
AfterSheet::class => function(AfterSheet $event) {
|
||||
$sheet = $event->sheet->getDelegate();
|
||||
$lastRow = count($this->data) + 2;
|
||||
$noteColumn = 'R'; // Cột Notes
|
||||
|
||||
// Xử lý đặc biệt cho cột Notes
|
||||
$sheet->getStyle("{$noteColumn}3:{$noteColumn}{$lastRow}")
|
||||
->getAlignment()
|
||||
->setWrapText(true)
|
||||
->setVertical(Alignment::VERTICAL_TOP)
|
||||
->setHorizontal(Alignment::HORIZONTAL_LEFT);
|
||||
|
||||
// Tắt auto-size cho cột Notes và set độ rộng cố định
|
||||
$sheet->getColumnDimension($noteColumn)
|
||||
->setAutoSize(false)
|
||||
->setWidth(60);
|
||||
|
||||
// Tự động điều chỉnh chiều cao cho từng dòng có nội dung
|
||||
for ($row = 3; $row <= $lastRow; $row++) {
|
||||
$cellValue = $sheet->getCell($noteColumn . $row)->getValue();
|
||||
if (!empty($cellValue)) {
|
||||
// Tính toán chiều cao dựa trên nội dung
|
||||
$sheet->getRowDimension($row)->setRowHeight(-1);
|
||||
|
||||
// Tính toán lại chiều cao dựa trên số dòng trong nội dung
|
||||
$lineCount = substr_count($cellValue, "\n") + 1;
|
||||
$minHeight = max(30, $lineCount * 15); // 15 pixels cho mỗi dòng
|
||||
|
||||
// Lấy chiều cao hiện tại sau khi auto-size
|
||||
$currentHeight = $sheet->getRowDimension($row)->getRowHeight();
|
||||
|
||||
// Nếu chiều cao tự động nhỏ hơn chiều cao tối thiểu, sử dụng chiều cao tối thiểu
|
||||
if ($currentHeight < $minHeight) {
|
||||
$sheet->getRowDimension($row)->setRowHeight($minHeight);
|
||||
}
|
||||
} else {
|
||||
$sheet->getRowDimension($row)->setRowHeight(30);
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh các tính toán của Excel
|
||||
$sheet->calculateColumnWidths();
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public function styles(Worksheet $sheet)
|
||||
{
|
||||
$lastRow = count($this->data) + 2;
|
||||
$lastColumn = 'R';
|
||||
|
||||
// Thêm và style title
|
||||
$sheet->mergeCells("A1:{$lastColumn}1");
|
||||
$sheet->setCellValue('A1', "DANH SÁCH NGÀY NGHỈ NĂM {$this->year}");
|
||||
$sheet->getStyle("A1:{$lastColumn}1")->applyFromArray([
|
||||
'font' => [
|
||||
'bold' => true,
|
||||
'size' => 14
|
||||
],
|
||||
'alignment' => [
|
||||
'horizontal' => Alignment::HORIZONTAL_CENTER,
|
||||
'vertical' => Alignment::VERTICAL_CENTER
|
||||
]
|
||||
]);
|
||||
|
||||
// Style cho header (dời xuống row 2)
|
||||
$sheet->getStyle("A2:{$lastColumn}2")->applyFromArray([
|
||||
'font' => ['bold' => true],
|
||||
'alignment' => [
|
||||
'horizontal' => Alignment::HORIZONTAL_CENTER,
|
||||
'vertical' => Alignment::VERTICAL_CENTER
|
||||
]
|
||||
]);
|
||||
|
||||
// Style cho toàn bộ bảng (bắt đầu từ row 1)
|
||||
$sheet->getStyle("A1:{$lastColumn}{$lastRow}")->applyFromArray([
|
||||
'borders' => [
|
||||
'allBorders' => [
|
||||
'borderStyle' => Border::BORDER_THIN
|
||||
]
|
||||
],
|
||||
'alignment' => [
|
||||
'vertical' => Alignment::VERTICAL_CENTER
|
||||
]
|
||||
]);
|
||||
|
||||
// Căn giữa cho các cột số liệu (điều chỉnh range bắt đầu từ row 3)
|
||||
for ($col = 'C'; $col <= 'P'; $col++) {
|
||||
$sheet->getStyle("{$col}3:{$col}{$lastRow}")
|
||||
->getAlignment()
|
||||
->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
}
|
||||
|
||||
// Set độ rộng cột
|
||||
$sheet->getColumnDimension('A')->setWidth(5); // No.
|
||||
$sheet->getColumnDimension('B')->setWidth(30); // User
|
||||
// Các tháng
|
||||
for ($i = 'C'; $i <= 'N'; $i++) {
|
||||
$sheet->getColumnDimension($i)->setWidth(8);
|
||||
}
|
||||
$sheet->getColumnDimension('O')->setWidth(8); // Total
|
||||
$sheet->getColumnDimension('P')->setWidth(8); // Off
|
||||
$sheet->getColumnDimension('Q')->setWidth(12); // Remaining
|
||||
$sheet->getColumnDimension('R')->setWidth(30); // Notes
|
||||
|
||||
// Điều chỉnh style cho cột Notes
|
||||
$sheet->getStyle("R3:R{$lastRow}")
|
||||
->getAlignment()
|
||||
->setHorizontal(Alignment::HORIZONTAL_LEFT)
|
||||
->setVertical(Alignment::VERTICAL_TOP)
|
||||
->setWrapText(true);
|
||||
|
||||
return $sheet;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Maatwebsite\Excel\Concerns\FromArray;
|
||||
use Maatwebsite\Excel\Concerns\WithEvents;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use Maatwebsite\Excel\Concerns\WithStyles;
|
||||
use Maatwebsite\Excel\Events\AfterSheet;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
|
||||
class TimekeepingExport implements FromArray, WithHeadings, WithStyles, WithEvents
|
||||
{
|
||||
protected $data;
|
||||
protected $month;
|
||||
protected $year;
|
||||
protected $workingDays;
|
||||
protected $daysInMonth;
|
||||
|
||||
public function __construct($data, $month, $year, $workingDays)
|
||||
{
|
||||
$this->data = $data;
|
||||
$this->month = $month;
|
||||
$this->year = $year;
|
||||
$this->workingDays = $workingDays;
|
||||
$this->daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
|
||||
}
|
||||
|
||||
public function registerEvents(): array
|
||||
{
|
||||
return [
|
||||
AfterSheet::class => function(AfterSheet $event) {
|
||||
$sheet = $event->sheet->getDelegate();
|
||||
$lastRow = count($this->data) + 4;
|
||||
$noteColumn = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($this->daysInMonth + 4);
|
||||
|
||||
// Xử lý đặc biệt cho cột Notes
|
||||
$sheet->getStyle("{$noteColumn}5:{$noteColumn}{$lastRow}")
|
||||
->getAlignment()
|
||||
->setWrapText(true)
|
||||
->setVertical(Alignment::VERTICAL_TOP)
|
||||
->setHorizontal(Alignment::HORIZONTAL_LEFT);
|
||||
|
||||
// Tắt auto-size cho cột Notes và set độ rộng cố định
|
||||
$sheet->getColumnDimension($noteColumn)
|
||||
->setAutoSize(false)
|
||||
->setWidth(60);
|
||||
|
||||
// Tự động điều chỉnh chiều cao cho từng dòng có nội dung
|
||||
for ($row = 5; $row <= $lastRow; $row++) {
|
||||
$cellValue = $sheet->getCell($noteColumn . $row)->getValue();
|
||||
if (!empty($cellValue)) {
|
||||
// Tính toán chiều cao dựa trên nội dung
|
||||
$sheet->getRowDimension($row)->setRowHeight(-1);
|
||||
|
||||
// Tính toán lại chiều cao dựa trên số dòng trong nội dung
|
||||
$lineCount = substr_count($cellValue, "\n") + 1;
|
||||
$minHeight = max(30, $lineCount * 15); // 15 pixels cho mỗi dòng
|
||||
|
||||
// Lấy chiều cao hiện tại sau khi auto-size
|
||||
$currentHeight = $sheet->getRowDimension($row)->getRowHeight();
|
||||
|
||||
// Nếu chiều cao tự động nhỏ hơn chiều cao tối thiểu, sử dụng chiều cao tối thiểu
|
||||
if ($currentHeight < $minHeight) {
|
||||
$sheet->getRowDimension($row)->setRowHeight($minHeight);
|
||||
}
|
||||
} else {
|
||||
$sheet->getRowDimension($row)->setRowHeight(30);
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh các tính toán của Excel
|
||||
$sheet->calculateColumnWidths();
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public function array(): array
|
||||
{
|
||||
// Lấy headers
|
||||
$headers = $this->headings();
|
||||
|
||||
// Lấy dữ liệu người dùng
|
||||
$userRows = [];
|
||||
foreach ($this->data as $user) {
|
||||
$row = [
|
||||
$user['user']['name'] ?? 'Unknown',
|
||||
0, // Total days
|
||||
$this->workingDays, // Off days (initialize with working days)
|
||||
];
|
||||
|
||||
$totalDays = 0;
|
||||
// Add data for each day in month
|
||||
for ($day = 1; $day <= $this->daysInMonth; $day++) {
|
||||
$dayData = '';
|
||||
if (isset($user['history'])) {
|
||||
foreach ($user['history'] as $history) {
|
||||
if ($history['day'] === $day) {
|
||||
$total = $history['total'] ?? 0;
|
||||
if ($total >= 7 * 3600) {
|
||||
$dayData = '1';
|
||||
$totalDays += 1;
|
||||
} else if ($total >= 3.5 * 3600) {
|
||||
$dayData = '0.5';
|
||||
$totalDays += 0.5;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$row[] = $dayData;
|
||||
}
|
||||
|
||||
// Update total and off days
|
||||
$row[1] = $totalDays;
|
||||
$row[2] = $this->workingDays - $totalDays;
|
||||
|
||||
// Add Notes column with formatted content
|
||||
$notes = [];
|
||||
if (isset($user['history'])) {
|
||||
foreach ($user['history'] as $history) {
|
||||
if (!empty($history['notes'])) {
|
||||
$dayNotes = [];
|
||||
foreach ($history['notes'] as $note) {
|
||||
$dayNotes[] = "- {$note['reasonName']} ({$note['timeTypeName']}): {$note['note']}";
|
||||
}
|
||||
if (!empty($dayNotes)) {
|
||||
$notes[] = "Day {$history['day']}:\n" . implode("\n", $dayNotes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$row[] = !empty($notes) ? implode("\n\n", $notes) : '';
|
||||
|
||||
$userRows[] = $row;
|
||||
}
|
||||
|
||||
return array_merge($headers, $userRows);
|
||||
}
|
||||
|
||||
public function headings(): array
|
||||
{
|
||||
|
||||
$firstRow = ['Day', '', ''];
|
||||
// Second row: Day of week
|
||||
$secondRow = ['', '', ''];
|
||||
|
||||
$date = Carbon::create($this->year, $this->month, 1);
|
||||
|
||||
for ($day = 1; $day <= $this->daysInMonth; $day++) {
|
||||
$firstRow[] = $day;
|
||||
$secondRow[] = $date->format('D');
|
||||
$date->addDay();
|
||||
}
|
||||
|
||||
// Add Notes column
|
||||
$firstRow[] = 'Notes';
|
||||
$secondRow[] = '';
|
||||
|
||||
return [$firstRow, $secondRow];
|
||||
}
|
||||
|
||||
public function styles(Worksheet $sheet)
|
||||
{
|
||||
$lastColumn = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($this->daysInMonth + 4);
|
||||
$lastRow = count($this->data) + 4;
|
||||
$noteColumn = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($this->daysInMonth + 4);
|
||||
|
||||
// Title and working days
|
||||
$sheet->mergeCells("A1:{$lastColumn}1");
|
||||
$sheet->setCellValue('A1', "DANH SÁCH CHẤM CÔNG THÁNG {$this->month} NĂM {$this->year}");
|
||||
$sheet->mergeCells("A2:{$lastColumn}2");
|
||||
$sheet->setCellValue('A2', "Số ngày làm việc: {$this->workingDays}");
|
||||
|
||||
// Merge cells for "Day" title and set value
|
||||
$sheet->mergeCells("A3:C3");
|
||||
$sheet->setCellValue('A3', 'Day');
|
||||
|
||||
// Set values for A4, B4, C4
|
||||
$sheet->setCellValue('A4', 'User');
|
||||
$sheet->setCellValue('B4', 'Total');
|
||||
$sheet->setCellValue('C4', 'Off');
|
||||
|
||||
// Styling
|
||||
$sheet->getStyle("A1:{$lastColumn}1")->getFont()->setBold(true)->setSize(14);
|
||||
$sheet->getStyle("A2:{$lastColumn}2")->getFont()->setBold(true);
|
||||
$sheet->getStyle("A3:{$lastColumn}4")->getFont()->setBold(true);
|
||||
|
||||
// Border style
|
||||
$borderStyle = [
|
||||
'borders' => [
|
||||
'allBorders' => [
|
||||
'borderStyle' => Border::BORDER_THIN,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Apply borders to the data area
|
||||
$sheet->getStyle("A1:{$lastColumn}{$lastRow}")->applyFromArray($borderStyle);
|
||||
|
||||
// Center align all cells except Notes column
|
||||
$sheet->getStyle("A1:{$lastColumn}{$lastRow}")
|
||||
->getAlignment()
|
||||
->setHorizontal(Alignment::HORIZONTAL_CENTER)
|
||||
->setVertical(Alignment::VERTICAL_CENTER);
|
||||
|
||||
// Left align Notes column và bật wrap text
|
||||
$sheet->getStyle("{$noteColumn}5:{$noteColumn}{$lastRow}")
|
||||
->getAlignment()
|
||||
->setHorizontal(Alignment::HORIZONTAL_LEFT)
|
||||
->setVertical(Alignment::VERTICAL_TOP)
|
||||
->setWrapText(true);
|
||||
|
||||
// Set width for Note column - tăng độ rộng để hiển thị tốt hơn
|
||||
$sheet->getColumnDimension($noteColumn)->setWidth(60);
|
||||
|
||||
// Tự động điều chỉnh chiều cao cho các dòng có nội dung Notes
|
||||
for ($row = 5; $row <= $lastRow; $row++) {
|
||||
$cellValue = $sheet->getCell($noteColumn . $row)->getValue();
|
||||
if (!empty($cellValue)) {
|
||||
// Đết chiều cao tự động
|
||||
$sheet->getRowDimension($row)->setRowHeight(-1);
|
||||
|
||||
// Đảm bảo chiều cao tối thiểu
|
||||
$currentHeight = $sheet->getRowDimension($row)->getRowHeight();
|
||||
if ($currentHeight < 30) {
|
||||
$sheet->getRowDimension($row)->setRowHeight(30);
|
||||
}
|
||||
} else {
|
||||
// Chiều cao mặc định cho các dòng không có note
|
||||
$sheet->getRowDimension($row)->setRowHeight(30);
|
||||
}
|
||||
}
|
||||
|
||||
// Set column widths
|
||||
$sheet->getColumnDimension('A')->setWidth(30);
|
||||
$sheet->getColumnDimension('B')->setWidth(10);
|
||||
$sheet->getColumnDimension('C')->setWidth(10);
|
||||
for ($i = 4; $i <= $this->daysInMonth + 3; $i++) {
|
||||
$sheet->getColumnDimension(\PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($i))->setWidth(5);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\LeaveDays;
|
||||
use App\Models\Notes;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Modules\Admin\app\Models\Category;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AddMonthlyLeaveDays implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
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;
|
||||
$this->year = $year ?? Carbon::now()->year;
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$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;
|
||||
}
|
||||
|
||||
// Nếu là nhân viên nghỉ việc, ko cộng phép
|
||||
if ($user->is_separated) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$leaveDay = LeaveDays::where('ld_user_id', $user->id)
|
||||
->where('ld_year', $this->year)
|
||||
->first();
|
||||
|
||||
if (!$leaveDay && $this->month > 1) {
|
||||
// Nếu chưa có dữ liệu năm hiện tại, tạo mới
|
||||
$previousYearData = LeaveDays::where('ld_user_id', $user->id)
|
||||
->where('ld_year', $this->year - 1)
|
||||
->first();
|
||||
$ld_additional_day = 0;
|
||||
$ld_note = '';
|
||||
|
||||
if ($previousYearData) {
|
||||
$ld_additional_day = $previousYearData->ld_day_total + $previousYearData->ld_additional_day + $previousYearData->ld_special_leave_day;
|
||||
$totalLeaveDaysByMonth = 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 - 1)
|
||||
->where('notes.n_user_id', $user->id)
|
||||
->where('notes.n_reason', 'ONLEAVE')
|
||||
->groupBy(DB::raw('notes.n_year'))
|
||||
->first();
|
||||
if ($totalLeaveDaysByMonth) {
|
||||
$ld_additional_day = $ld_additional_day - $totalLeaveDaysByMonth->leave_days;
|
||||
if ($ld_additional_day < 0) {
|
||||
$ld_additional_day = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if ($ld_additional_day > 0) {
|
||||
$ld_note = "Cộng " . $ld_additional_day . " ngày phép tồn năm trước. \n";
|
||||
}
|
||||
}
|
||||
|
||||
$leaveDay = new LeaveDays([
|
||||
'ld_user_id' => $user->id,
|
||||
'ld_day_total' => $this->month, // Số ngày phép bằng tháng hiện tại
|
||||
'ld_year' => $this->year,
|
||||
'ld_additional_day' => $ld_additional_day,
|
||||
'ld_note' => $ld_note,
|
||||
'ld_special_leave_day' => 0,
|
||||
]);
|
||||
$leaveDay->save();
|
||||
} else {
|
||||
// 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;
|
||||
$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ộng mỗi tháng 1 ngày phép cho nhân viên
|
||||
$leaveDay->ld_day_total += self::ONLEAVE_PER_MONTH;
|
||||
$leaveDay->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Mail\TicketMail;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Modules\Admin\app\Http\Controllers\CategoryController;
|
||||
use Modules\Admin\app\Models\Admin;
|
||||
use Modules\Admin\app\Models\Ticket;
|
||||
use Modules\Admin\app\Models\Tracking;
|
||||
|
||||
class CheckUserAttendanceJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $period;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
* @param string|null $period
|
||||
*/
|
||||
public function __construct($period = null)
|
||||
{
|
||||
$this->period = $period;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
// Lấy tất cả người dùng
|
||||
$users = User::where('permission', 'not like', '%admin%')
|
||||
->where('permission', 'not like', '%accountant%')->get();
|
||||
|
||||
foreach ($users as $key => $user) {
|
||||
// Check nhân viên nghỉ việc
|
||||
if ($user->is_separated) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Kiểm tra dựa trên period (Sáng 'S' hoặc Chiều 'C')
|
||||
if ($this->period === 'S') {
|
||||
$this->checkMorning($user);
|
||||
} elseif ($this->period === 'C') {
|
||||
$this->checkAfternoon($user);
|
||||
} else {
|
||||
if ($this->period == null) {
|
||||
// Nếu không có period, kiểm tra cả sáng và chiều
|
||||
$this->checkMorning($user);
|
||||
$this->checkAfternoon($user);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function checkAttendance($user, $periodCode, $startTime, $endTime)
|
||||
{
|
||||
$today = Carbon::today();
|
||||
$userId = $user->id;
|
||||
// Lấy tất cả tracking của user trong khoảng thời gian được chỉ định
|
||||
$records = Tracking::where('user_id', $userId)
|
||||
->whereBetween('time_string', [$startTime, $endTime])
|
||||
->get();
|
||||
|
||||
$hasCheckIn = $records->where('status', 'check in')->isNotEmpty();
|
||||
$hasCheckOut = $records->where('status', 'check out')->isNotEmpty();
|
||||
|
||||
// Kiểm tra nếu đã có ticket chờ xử lý hoặc đã được xác nhận, cho cả sáng và chiều
|
||||
$existingTicket = Ticket::where('user_id', $userId)
|
||||
->where(function ($query) use ($today, $periodCode) {
|
||||
// Check for tickets that exactly cover the current day and period
|
||||
$query->where(function ($subQuery) use ($today, $periodCode) {
|
||||
$subQuery->where('start_date', $today->format('Y-m-d'))
|
||||
->where('end_date', $today->format('Y-m-d'))
|
||||
->where(function ($periodQuery) use ($periodCode) {
|
||||
// Check if the ticket covers the current period
|
||||
$periodQuery->where('start_period', $periodCode)
|
||||
->orWhere(function ($query) {
|
||||
// Check for a full-day ticket (S -> C)
|
||||
$query->where('start_period', 'S')
|
||||
->where('end_period', 'C');
|
||||
});
|
||||
});
|
||||
})
|
||||
// Check for tickets that span multiple days
|
||||
->orWhere(function ($subQuery) use ($today, $periodCode) {
|
||||
$subQuery->where('start_date', '<=', $today->format('Y-m-d'))
|
||||
->where('end_date', '>=', $today->format('Y-m-d'))
|
||||
->where(function ($periodQuery) use ($periodCode, $today) {
|
||||
$periodQuery->where(function ($query) use ($today) {
|
||||
$query->where('start_date', '<', $today->format('Y-m-d'))
|
||||
->where('end_date', '>', $today->format('Y-m-d'));
|
||||
})
|
||||
->orWhere(function ($query) use ($today, $periodCode) {
|
||||
$query->where(function ($queryDateStart) use ($today, $periodCode) {
|
||||
$queryDateStart->where('start_date', '=', $today->format('Y-m-d'))
|
||||
->where('end_date', '>', $today->format('Y-m-d'))
|
||||
->where(function ($queryDateStartChild) use ($periodCode) {
|
||||
$queryDateStartChild->where('start_period', $periodCode)
|
||||
->orWhere(function ($query) {
|
||||
$query->where('start_period', 'S');
|
||||
});
|
||||
});
|
||||
})
|
||||
->orWhere(function ($queryDateEnd) use ($today, $periodCode) {
|
||||
$queryDateEnd->where('end_date', '=', $today->format('Y-m-d'))
|
||||
->where('start_date', '<', $today->format('Y-m-d'))
|
||||
->where(function ($queryDateStartChild) use ($periodCode) {
|
||||
$queryDateStartChild->where('end_period', $periodCode)
|
||||
->orWhere(function ($query) {
|
||||
$query->where('end_period', 'C');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
->whereIn('status', ['WAITING', 'CONFIRMED'])
|
||||
->first();
|
||||
|
||||
$type = 'ONLEAVE';
|
||||
$reason = 'KHONG PHEP';
|
||||
|
||||
//Check ngày hợp lệ nếu có check và check out của user khác
|
||||
$dateNow = Tracking::whereBetween('time_string', [$startTime, $endTime])->exists();
|
||||
|
||||
// Nếu không có check in/out, không gửi ticket và có check in/out của người khác => tạo ticket không phép
|
||||
if ($hasCheckIn || $hasCheckOut) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($existingTicket) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($dateNow) {
|
||||
$ticket = Ticket::create([
|
||||
'user_id' => $userId,
|
||||
'start_date' => $today->format('Y-m-d'),
|
||||
'start_period' => $periodCode,
|
||||
'end_date' => $today->format('Y-m-d'),
|
||||
'end_period' => $periodCode,
|
||||
'type' => $type,
|
||||
'reason' => $reason,
|
||||
'status' => 'WAITING',
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now()
|
||||
]);
|
||||
|
||||
//Send mail
|
||||
$dataMasterStartPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $periodCode);
|
||||
$dataMasterEndPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $periodCode);
|
||||
$dataMasterType = CategoryController::getListMasterByCodeAndType("REASON", $type);
|
||||
|
||||
$formattedStartDate = $today->format('d/m/Y');
|
||||
$formattedEndDate = $today->format('d/m/Y');
|
||||
|
||||
$admins = Admin::where('permission', 'like', '%admin%')->get();
|
||||
foreach ($admins as $key => $value) {
|
||||
$data = array(
|
||||
"ticket_id" => $ticket->id,
|
||||
"email_template" => "email.notification_tickets",
|
||||
"email" => $user->email,
|
||||
"admin_email" => $value->email,
|
||||
"name" => $user->name,
|
||||
"date" => $dataMasterStartPeriod->c_name . " (" . $formattedStartDate . ") - " . $dataMasterEndPeriod->c_name . " (" . $formattedEndDate . ")",
|
||||
"type" => $dataMasterType->c_name,
|
||||
"note" => $reason,
|
||||
"link" => "/tickets-management", //link đến page admin
|
||||
"subject" => "[Ticket request] Ticket From " . $user->name
|
||||
);
|
||||
Mail::to($value->email)->send(new TicketMail($data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kiểm tra check-in/check-out buổi sáng và tạo ticket nếu thiếu.
|
||||
* @param int $userId
|
||||
*/
|
||||
public function checkMorning($user)
|
||||
{
|
||||
$startTime = Carbon::today()->setTime(6, 0); // Thời gian bắt đầu buổi sáng
|
||||
$endTime = Carbon::createFromTime(12, 0); // Thời gian kết thúc buổi sáng
|
||||
$this->checkAttendance($user, 'S', $startTime, $endTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kiểm tra check-in/check-out buổi chiều và tạo ticket nếu thiếu.
|
||||
* @param int $userId
|
||||
*/
|
||||
public function checkAfternoon($user)
|
||||
{
|
||||
$startTime = Carbon::today()->setTime(12, 0); // Thời gian bắt đầu buổi chiều
|
||||
$endTime = Carbon::createFromTime(17, 30); // Thời gian kết thúc buổi chiều
|
||||
$this->checkAttendance($user, 'C', $startTime, $endTime);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -36,44 +35,36 @@ class DeductLeaveDays implements ShouldQueue
|
|||
foreach ($users as $user) {
|
||||
$existingData = LeaveDays::where('ld_user_id', $user->id)
|
||||
->where('ld_year', $this->year)
|
||||
->where('ld_date_additional', ">", 0)
|
||||
->where('ld_additional_day', ">", 0)
|
||||
->first();
|
||||
if (!$existingData) {
|
||||
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();
|
||||
->where('n_user_id', $user->id)
|
||||
->where('n_year', $this->year)
|
||||
->where('n_month', "<=", 3)
|
||||
->where('n_reason', 'ONLEAVE')
|
||||
->sum('categories.c_value');
|
||||
|
||||
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_date_additional > $totalLeaveDaysByMonth->leave_days) {
|
||||
LeaveDays::where('ld_year', $this->year)
|
||||
->where('ld_user_id', $user->id)
|
||||
->update([
|
||||
'ld_date_additional' => $totalLeaveDaysByMonth->leave_days,
|
||||
]);
|
||||
if ($usedOnleaveDaysTotal) {
|
||||
if ($existingData->ld_additional_day > $usedOnleaveDaysTotal) {
|
||||
$ld_note = "Trừ " . $existingData->ld_additional_day - $usedOnleaveDaysTotal . " ngày phép tồn năm trước. \n";
|
||||
$existingData->ld_note = $existingData->ld_note . $ld_note;
|
||||
$existingData->ld_additional_day = $usedOnleaveDaysTotal;
|
||||
}
|
||||
} 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_date_additional' => "0",
|
||||
]);
|
||||
$ld_note = "Trừ " . $existingData->ld_additional_day . " ngày phép tồn năm trước. \n";
|
||||
$existingData->ld_note = $existingData->ld_note . $ld_note;
|
||||
$existingData->ld_additional_day = 0;
|
||||
}
|
||||
|
||||
$existingData->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,15 +34,11 @@ class InitializeLeaveDays implements ShouldQueue
|
|||
public function handle(): void
|
||||
{
|
||||
$users = User::get();
|
||||
$ld_day = 12;
|
||||
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)
|
||||
->where('ld_year', $this->year)
|
||||
->first();
|
||||
$ld_day_total = Carbon::now()->month; // Khởi tạo phép hiện có bằng tháng hiện tại
|
||||
|
||||
if ($existingData) {
|
||||
// Nếu dữ liệu đã tồn tại, bỏ qua user này
|
||||
foreach ($users as $user) {
|
||||
// Check nhân viên nghỉ việc
|
||||
if ($user->is_separated) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -51,11 +47,11 @@ class InitializeLeaveDays implements ShouldQueue
|
|||
->where('ld_year', $this->year - 1)
|
||||
->first();
|
||||
|
||||
$ld_date_additional = 0;
|
||||
$ld_additional_day = 0;
|
||||
$ld_note = '';
|
||||
|
||||
if ($previousYearData) {
|
||||
$ld_date_additional = $previousYearData->ld_day + $previousYearData->ld_date_additional;
|
||||
$ld_additional_day = $previousYearData->ld_day_total + $previousYearData->ld_additional_day + $previousYearData->ld_special_leave_day;
|
||||
$totalLeaveDaysByMonth = Notes::join('categories', function ($join) {
|
||||
$join->on('notes.n_time_type', '=', 'categories.c_code')
|
||||
->where('categories.c_type', 'TIME_TYPE');
|
||||
|
|
@ -71,20 +67,36 @@ class InitializeLeaveDays implements ShouldQueue
|
|||
->groupBy(DB::raw('notes.n_year'))
|
||||
->first();
|
||||
if ($totalLeaveDaysByMonth) {
|
||||
$ld_date_additional = $ld_date_additional - $totalLeaveDaysByMonth->leave_days;
|
||||
if ($ld_date_additional < 0) {
|
||||
$ld_date_additional = 0;
|
||||
$ld_additional_day = $ld_additional_day - $totalLeaveDaysByMonth->leave_days;
|
||||
if ($ld_additional_day < 0) {
|
||||
$ld_additional_day = 0;
|
||||
}
|
||||
}
|
||||
$ld_note = 'Cộng dồn ngày phép năm cũ';
|
||||
|
||||
if ($ld_additional_day > 0) {
|
||||
$ld_note = "Cộng " . $ld_additional_day . " ngày phép tồn năm trước. \n";
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
->where('ld_year', $this->year)
|
||||
->first();
|
||||
|
||||
if ($existingData) {
|
||||
// Nếu dữ liệu đã tồn tại, update lại phép tồn
|
||||
$existingData->ld_note = $ld_note;
|
||||
$existingData->ld_additional_day = $ld_additional_day;
|
||||
$existingData->save();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Tạo dữ liệu cho năm hiện tại
|
||||
LeaveDays::insert([
|
||||
'ld_user_id' => $user->id,
|
||||
'ld_day' => $ld_day,
|
||||
'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_date_additional' => $ld_date_additional,
|
||||
'ld_additional_day' => $ld_additional_day,
|
||||
'ld_note' => $ld_note,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,220 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\LeaveDays;
|
||||
use App\Models\Notes;
|
||||
use App\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Modules\Admin\app\Models\Category;
|
||||
|
||||
class UpdateTemporaryLeaveDays implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
protected $month;
|
||||
protected $year;
|
||||
|
||||
public function __construct($month = null, $year = null)
|
||||
{
|
||||
$this->month = $month ?? Carbon::now()->month;
|
||||
$this->year = $year ?? Carbon::now()->year;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$users = User::get();
|
||||
|
||||
foreach ($users as $user) {
|
||||
$leaveDay = LeaveDays::where('ld_user_id', $user->id)
|
||||
->where('ld_year', $this->year)
|
||||
->first();
|
||||
|
||||
$notes = Notes::where('n_reason', 'TEMPORARY_ONLEAVE')
|
||||
->where('n_user_id', $user->id)
|
||||
->where('n_year', $this->year)
|
||||
->where('n_month', $this->month)
|
||||
->whereExists(function ($query) use ($user) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('tickets')
|
||||
->where('tickets.user_id', $user->id)
|
||||
->where('tickets.status', 'CONFIRMED')
|
||||
->where('tickets.type', 'ONLEAVE');
|
||||
})
|
||||
->get();
|
||||
|
||||
$maxDaysPerMonth = $this->getMaxLeaveDaysPerMonth();
|
||||
|
||||
// Tổng ngày nghỉ sẽ dùng trong tháng
|
||||
$willUsedDaysInMonth = 0;
|
||||
foreach ($notes as $note) {
|
||||
$willUsedDaysInMonth += $note->n_time_type == 'ALL' ? 1.0 : 0.5;
|
||||
}
|
||||
|
||||
// Tổng phép đang có
|
||||
$onleaveDaysTotal = $leaveDay->ld_day_total + $leaveDay->ld_additional_day + $leaveDay->ld_special_leave_day;
|
||||
// Phép đã sử dụng tới tháng hiện tại
|
||||
$usedOnleaveDaysTotal = Notes::join('categories', function ($join) {
|
||||
$join->on('notes.n_time_type', '=', 'categories.c_code')
|
||||
->where('categories.c_type', 'TIME_TYPE');
|
||||
})
|
||||
->where('n_user_id', $user->id)
|
||||
->where('n_year', $this->year)
|
||||
->where('n_month', "<=", $this->month)
|
||||
->where('n_reason', 'ONLEAVE')
|
||||
->sum('categories.c_value');
|
||||
// Phép còn lại
|
||||
$remainingOnleaveDays = $onleaveDaysTotal - $usedOnleaveDaysTotal;
|
||||
|
||||
// Log::debug("User {$user->name}\n");
|
||||
// Log::debug(
|
||||
// "📊 Thống kê ngày phép:\n" .
|
||||
// " - Tháng: {$this->month}\n" .
|
||||
// " - Tổng ngày nghỉ sẽ dùng trong tháng: $willUsedDaysInMonth\n" .
|
||||
// " - Tổng ngày phép: $onleaveDaysTotal\n" .
|
||||
// " - Tổng ngày phép đã nghỉ: $usedOnleaveDaysTotal\n" .
|
||||
// " - Tổng ngày phép còn lại: $remainingOnleaveDays\n"
|
||||
// );
|
||||
|
||||
$onleave_days_will_use = 0; // Ngày phép sẽ dùng
|
||||
$nopay_days_will_use = 0; // Ngày ko phép sẽ dùng
|
||||
|
||||
// Ngày phép còn lại <= 0 (Hết phép)
|
||||
if ($remainingOnleaveDays <= 0) {
|
||||
$onleave_days_will_use = 0;
|
||||
$nopay_days_will_use = $willUsedDaysInMonth;
|
||||
|
||||
Log::debug("--- Hết phép trong tháng ---");
|
||||
}
|
||||
|
||||
// Ngày phép còn lại < ngày yêu cầu (Không đủ phép)
|
||||
else if ($remainingOnleaveDays < $willUsedDaysInMonth) {
|
||||
// Vượt limit
|
||||
if ($willUsedDaysInMonth > $maxDaysPerMonth) {
|
||||
// Phép còn lại > limit
|
||||
if ($remainingOnleaveDays > $maxDaysPerMonth) {
|
||||
$onleave_days_will_use = $maxDaysPerMonth;
|
||||
$nopay_days_will_use = $willUsedDaysInMonth - $maxDaysPerMonth;
|
||||
}
|
||||
// Phép còn lại < limit
|
||||
else {
|
||||
$onleave_days_will_use = $remainingOnleaveDays;
|
||||
$nopay_days_will_use = $willUsedDaysInMonth - $remainingOnleaveDays;
|
||||
}
|
||||
|
||||
Log::debug("--- Không đủ phép trong tháng, vượt quá limit ---",);
|
||||
}
|
||||
// Không vượt limit
|
||||
else {
|
||||
$onleave_days_will_use = $remainingOnleaveDays;
|
||||
$nopay_days_will_use = $willUsedDaysInMonth - $remainingOnleaveDays;
|
||||
|
||||
Log::debug("--- Không đủ phép trong tháng, ko vượt limit ---");
|
||||
}
|
||||
}
|
||||
|
||||
// Ngày phép còn lại >= ngày yêu cầu (Đủ phép)
|
||||
else {
|
||||
// Vượt limit
|
||||
if ($willUsedDaysInMonth > $maxDaysPerMonth) {
|
||||
$onleave_days_will_use = $maxDaysPerMonth;
|
||||
$nopay_days_will_use = $willUsedDaysInMonth - $maxDaysPerMonth;
|
||||
|
||||
Log::debug("--- Đủ phép, vượt limit ---");
|
||||
}
|
||||
// Không vượt limit
|
||||
else {
|
||||
$onleave_days_will_use = $willUsedDaysInMonth;
|
||||
$nopay_days_will_use = 0;
|
||||
|
||||
Log::debug("--- Đủ phép ---");
|
||||
}
|
||||
}
|
||||
|
||||
Log::debug("", [
|
||||
"Phep" => $onleave_days_will_use,
|
||||
"Khong Phep" => $nopay_days_will_use
|
||||
]);
|
||||
|
||||
// Có nghỉ không phép
|
||||
if ($nopay_days_will_use > 0) {
|
||||
foreach ($notes as $note) {
|
||||
$value = ($note->n_time_type === 'ALL') ? 1.0 : 0.5;
|
||||
|
||||
if ($note->n_time_type === 'ALL' && $onleave_days_will_use == 0.5) {
|
||||
// Chỉ còn 0.5 phép, chia thành 2 bản ghi: 1 phép, 1 không phép
|
||||
// Ưu tiên phép cho buổi sáng (S), không phép cho buổi chiều (C)
|
||||
|
||||
Notes::create([
|
||||
'n_user_id' => $user->id,
|
||||
'n_day' => $note->n_day,
|
||||
'n_month' => $note->n_month,
|
||||
'n_year' => $note->n_year,
|
||||
'n_time_type' => 'S',
|
||||
'n_reason' => 'ONLEAVE',
|
||||
'n_note' => $note->n_note,
|
||||
'ticket_id' => $note->ticket_id
|
||||
]);
|
||||
Notes::create([
|
||||
'n_user_id' => $user->id,
|
||||
'n_day' => $note->n_day,
|
||||
'n_month' => $note->n_month,
|
||||
'n_year' => $note->n_year,
|
||||
'n_time_type' => 'C',
|
||||
'n_reason' => 'LEAVE_WITHOUT_PAY',
|
||||
'n_note' => $note->n_note,
|
||||
'ticket_id' => $note->ticket_id
|
||||
]);
|
||||
|
||||
$note->delete();
|
||||
|
||||
$onleave_days_will_use = 0;
|
||||
$nopay_days_will_use -= 0.5;
|
||||
} elseif ($onleave_days_will_use > 0) {
|
||||
// Dùng ngày phép trước
|
||||
$use = min($onleave_days_will_use, $value);
|
||||
$note->update([
|
||||
'n_reason' => "ONLEAVE"
|
||||
]);
|
||||
$onleave_days_will_use -= $use;
|
||||
} elseif ($nopay_days_will_use > 0) {
|
||||
// Hết phép, chuyển sang không phép
|
||||
$use = min($nopay_days_will_use, $value);
|
||||
$note->update([
|
||||
'n_reason' => "LEAVE_WITHOUT_PAY"
|
||||
]);
|
||||
$nopay_days_will_use -= $use;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Đủ phép
|
||||
else {
|
||||
foreach ($notes as $note) {
|
||||
$note->update([
|
||||
'n_reason' => "ONLEAVE"
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getMaxLeaveDaysPerMonth(): int
|
||||
{
|
||||
$limitLeaveMonth = Category::where('c_type', 'LIMIT_LEAVE_MONTH')->where('c_code', "LIMIT")->first();
|
||||
if ($limitLeaveMonth) {
|
||||
$maxDaysPerMonth = (int)$limitLeaveMonth->c_value;
|
||||
} else {
|
||||
$maxDaysPerMonth = 3; // default nếu k có setting
|
||||
}
|
||||
return $maxDaysPerMonth;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Mail\Mailables\Address;
|
||||
|
||||
class CheckinLateMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
public $data;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
// replyTo: [
|
||||
// new Address($this->data["email"], $this->data["name"]),
|
||||
// ],
|
||||
subject: $this->data["subject"],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: ($this->data["email_template"])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class WarningLongTask extends Mailable
|
||||
{
|
||||
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $data;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: '[SYSTEM] - Task are forgotten - '.$this->data[0]['fields']['assignee']['displayName'],
|
||||
);
|
||||
}
|
||||
|
||||
public function build(): self
|
||||
{
|
||||
return $this->view('email.warningLongTask', ['data'=> $this->data]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Files extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'url',
|
||||
'type',
|
||||
'description',
|
||||
'user_id'
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ class LeaveDays extends Model
|
|||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'id', 'ld_user_id', 'ld_day', 'ld_year', 'ld_date_additional', 'ld_note'
|
||||
'id', 'ld_user_id', 'ld_day_total', 'ld_year', 'ld_additional_day', 'ld_note'
|
||||
];
|
||||
|
||||
protected $table = 'leave_days';
|
||||
|
|
|
|||
|
|
@ -11,7 +11,14 @@ class Notes extends Model
|
|||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'n_user_id', 'n_day', 'n_month', 'n_year', 'n_time_type', 'n_reason', 'n_note',
|
||||
'n_user_id',
|
||||
'n_day',
|
||||
'n_month',
|
||||
'n_year',
|
||||
'n_time_type',
|
||||
'n_reason',
|
||||
'n_note',
|
||||
'ticket_id'
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -25,7 +32,7 @@ class Notes extends Model
|
|||
{
|
||||
return self::leftJoin("categories as reason", function ($join) {
|
||||
$join->on('n_reason', '=', 'reason.c_code');
|
||||
$join->on('reason.c_type', DB::raw("CONCAT('REASON')"));
|
||||
$join->on('reason.c_type', DB::raw("CONCAT('REASON_NOTES')"));
|
||||
})
|
||||
->leftJoin("categories as timeTypes", function ($join) {
|
||||
$join->on('n_time_type', '=', 'timeTypes.c_code');
|
||||
|
|
@ -47,4 +54,18 @@ class Notes extends Model
|
|||
)
|
||||
->get();
|
||||
}
|
||||
|
||||
public static function getNotesByMonthAndYearAndUserId($month, $year, $userId, $idNote)
|
||||
{
|
||||
return self::where('n_reason', 'ONLEAVE')->where('n_month', $month)->where('n_year', $year)
|
||||
->where('n_user_id', $userId)
|
||||
->where('id', '!=', $idNote)->get();
|
||||
}
|
||||
|
||||
public static function getNotesByMonthAndYearAndUserIdAndReason($month, $year, $userId, $reason)
|
||||
{
|
||||
return self::where('n_reason', $reason)->where('n_month', $month)->where('n_year', $year)
|
||||
->where('n_user_id', $userId)
|
||||
->orderBy('n_day', 'asc')->orderBy('n_time_type', 'desc')->get();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ class JiraService
|
|||
'Authorization' => $this->authHeader,
|
||||
'Accept' => 'application/json',
|
||||
'Content-Type' => 'application/json'
|
||||
]
|
||||
],
|
||||
'timeout' => 60, // Tăng thời gian timeout lên 60 giây
|
||||
'connect_timeout' => 30 // Tăng thời gian chờ kết nối lên 30 giây
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -230,13 +232,14 @@ class JiraService
|
|||
$groupedIssues = [];
|
||||
$users_data = [];
|
||||
$user_warning = [];
|
||||
$ignore_projects = ['PJ_tracking'];
|
||||
foreach ($users as $user) {
|
||||
$user = (array)$user[0];
|
||||
$user = (array) $user[0];
|
||||
$users_data[$user['displayName']]['user'] = $user;
|
||||
$users_data[$user['displayName']]['total_spent'] = 0;
|
||||
$users_data[$user['displayName']]['total_est'] = 0;
|
||||
$body = [
|
||||
'expand' => ['names', 'schema','changelog'],
|
||||
'expand' => ['names', 'schema', 'changelog'],
|
||||
'fields' => ['summary', 'status', 'timeoriginalestimate', 'timespent', 'assignee', 'project', 'updated'],
|
||||
'jql' => sprintf(
|
||||
"assignee = '%s' AND status IN ('to do', 'todo', 'in progress') ORDER BY updated DESC",
|
||||
|
|
@ -252,14 +255,22 @@ class JiraService
|
|||
|
||||
$issues = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
// Lọc các issue không thuộc các project bị ignore
|
||||
$filtered_issues = array_filter($issues['issues'], function ($issue) use ($ignore_projects) {
|
||||
return !in_array($issue['fields']['project']['name'], $ignore_projects);
|
||||
});
|
||||
|
||||
$issues['issues'] = $filtered_issues;
|
||||
|
||||
if (count($issues['issues']) == 0) {
|
||||
$user_warning[] = $user;
|
||||
}
|
||||
|
||||
foreach ($issues['issues'] as $issue) {
|
||||
$projectName = $issue['fields']['project']['name'];
|
||||
if (!in_array($projectName, $ignore_projects)) {
|
||||
$username = $issue['fields']['assignee']['displayName'];
|
||||
|
||||
$issue['fields']['assignee']['emailAddress'] = $user['emailAddress'];
|
||||
if (!isset($groupedIssues[$projectName])) {
|
||||
$groupedIssues[$projectName] = [];
|
||||
$groupedIssues[$projectName]['project'] = $issue['fields']['project'];
|
||||
|
|
@ -280,6 +291,7 @@ class JiraService
|
|||
$users_data[$user['displayName']]['total_est'] = $users_data[$user['displayName']]['total_est'] + ($issue['fields']['timeoriginalestimate'] ?? 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -314,4 +326,18 @@ class JiraService
|
|||
$response = $this->client->get('/rest/agile/1.0/sprint/' . $id . '/issue');
|
||||
return json_decode($response->getBody()->getContents(), true);
|
||||
}
|
||||
|
||||
public function getWeeklyReport()
|
||||
{
|
||||
$body = [
|
||||
'fields' => ['summary', 'status', 'timeoriginalestimate', 'timespent', 'assignee', 'project', 'worklog'],
|
||||
'jql' => 'worklogDate >= startOfWeek() AND worklogDate < startOfWeek(1) order by created DESC',
|
||||
'maxResults' => 1000
|
||||
];
|
||||
|
||||
$response = $this->client->post('/rest/api/3/search', [
|
||||
'body' => json_encode($body)
|
||||
]);
|
||||
return json_decode($response->getBody()->getContents(), true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,16 @@
|
|||
"name": "laravel/laravel",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": ["laravel", "framework"],
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"framework"
|
||||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"barryvdh/laravel-debugbar": "^3.9",
|
||||
"barryvdh/laravel-dompdf": "^2.0",
|
||||
"doctrine/dbal": "^3.10",
|
||||
"drnxloc/laravel-simple-html-dom": "^1.9",
|
||||
"guzzlehttp/guzzle": "^7.8",
|
||||
"laravel/framework": "^10.10",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -59,6 +59,8 @@ return [
|
|||
|
||||
'asset_url' => env('ASSET_URL'),
|
||||
|
||||
'client_url' => env('ADMIN_URL', 'http://localhost'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'paths' => ['api/*', 'sanctum/csrf-cookie'],
|
||||
'paths' => ['api/*', 'sanctum/csrf-cookie', 'storage/*'],
|
||||
|
||||
'allowed_methods' => ['*'],
|
||||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'ttl' => env('JWT_TTL', 60*24),
|
||||
'ttl' => env('JWT_TTL', 60*24*365),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('project_reviews', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('role');
|
||||
$table->longText('note');
|
||||
$table->foreignId('user_id')->constrained('users')->onDelete('cascade'); // Khóa ngoại tới bảng users
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('project_reviews');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?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::create('documents', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('title');
|
||||
$table->text('uri');
|
||||
$table->enum('type', ['file', 'link'])->default("file");
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('documents');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class RenameLdDayToLdDayTotalInLeaveDaysTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('leave_days', function (Blueprint $table) {
|
||||
$table->renameColumn('ld_day', 'ld_day_total');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('leave_days', function (Blueprint $table) {
|
||||
$table->renameColumn('ld_day_total', 'ld_day');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddLdSpecialLeaveDayToLeaveDaysTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('leave_days', function (Blueprint $table) {
|
||||
$table->float('ld_special_leave_day')->default(0); // Adding the new field
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('leave_days', function (Blueprint $table) {
|
||||
$table->dropColumn('ld_special_leave_day'); // Dropping the field if needed
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class RenameLdDateAdditionalToLdAdditionalDayInLeaveDaysTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('leave_days', function (Blueprint $table) {
|
||||
$table->renameColumn('ld_date_additional', 'ld_additional_day');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('leave_days', function (Blueprint $table) {
|
||||
$table->renameColumn('ld_date_additional', 'ld_additional_day');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('categories')->insert([
|
||||
[
|
||||
'c_code' => 'LEAVE_WITHOUT_PAY',
|
||||
'c_name' => 'Nghỉ không hưởng lương',
|
||||
'c_type' => 'REASON',
|
||||
'c_value' => "",
|
||||
'c_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('categories')
|
||||
->where('c_name', 'Nghỉ phép')
|
||||
->update(['c_name' => 'Nghỉ phép năm']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('categories')
|
||||
->where('c_name', 'Nghỉ phép năm')
|
||||
->update(['c_name' => 'Nghỉ phép']);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('files', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('url');
|
||||
$table->string('type');
|
||||
$table->text('description')->nullable();
|
||||
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('files');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UpdateLeaveCategories extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
// Xóa item với type REASON và code LEAVE_WITHOUT_PAY
|
||||
DB::table('categories')
|
||||
->where('c_type', 'REASON')
|
||||
->where('c_code', 'LEAVE_WITHOUT_PAY')
|
||||
->delete();
|
||||
|
||||
// Cập nhật tên "Nghỉ phép năm" thành "Nghỉ phép"
|
||||
DB::table('categories')
|
||||
->where('c_name', 'Nghỉ phép năm')
|
||||
->update(['c_name' => 'Nghỉ phép']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
// Khôi phục item đã xóa
|
||||
DB::table('categories')->insert([
|
||||
'c_code' => 'LEAVE_WITHOUT_PAY',
|
||||
'c_name' => 'Không phép',
|
||||
'c_type' => 'REASON',
|
||||
'c_value' => "",
|
||||
'c_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
// Khôi phục tên cũ
|
||||
DB::table('categories')
|
||||
->where('c_name', 'Nghỉ phép')
|
||||
->update(['c_name' => 'Nghỉ phép năm']);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AddLimitLeaveMonthCategory extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
DB::table('categories')->insert([
|
||||
'c_code' => 'LIMIT',
|
||||
'c_name' => 'Giới hạn số ngày nghỉ có phép/tháng',
|
||||
'c_type' => 'LIMIT_LEAVE_MONTH',
|
||||
'c_value' => '3',
|
||||
'c_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
DB::table('categories')
|
||||
->where('c_code', 'LIMIT')
|
||||
->where('c_type', 'LIMIT_LEAVE_MONTH')
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AddSaturdayWorkScheduleCategory extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
DB::table('categories')->insert([
|
||||
'c_code' => '10-05-2025',
|
||||
'c_name' => 'Ngày bắt đầu làm việc thứ 7 trong năm',
|
||||
'c_type' => 'SATURDAY_WORK_SCHEDULE',
|
||||
'c_value' => '2025',
|
||||
'c_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
DB::table('categories')
|
||||
->where('c_code', '10-05-2025')
|
||||
->where('c_type', 'SATURDAY_WORK_SCHEDULE')
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AddDayWorkSpecialCategory extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
DB::table('categories')->insert([
|
||||
'c_code' => '17-05-2025',
|
||||
'c_name' => 'Ngày làm việc đặc biệt',
|
||||
'c_type' => 'DAY_WORK_SPECIAL',
|
||||
'c_value' => '2025',
|
||||
'c_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
DB::table('categories')
|
||||
->where('c_code', '17-05-2025')
|
||||
->where('c_type', 'DAY_WORK_SPECIAL')
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AddLeaveCategories extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
DB::table('categories')->insert([
|
||||
[
|
||||
'c_code' => 'LEAVE_WITHOUT_PAY',
|
||||
'c_name' => 'Không phép',
|
||||
'c_type' => 'REASON_NOTES',
|
||||
'c_value' => "",
|
||||
'c_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'c_code' => 'WFH',
|
||||
'c_name' => 'Work From Home',
|
||||
'c_type' => 'REASON_NOTES',
|
||||
'c_value' => "",
|
||||
'c_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'c_code' => 'ONLEAVE',
|
||||
'c_name' => 'Nghỉ phép',
|
||||
'c_type' => 'REASON_NOTES',
|
||||
'c_value' => "",
|
||||
'c_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
DB::table('categories')
|
||||
->whereIn('c_code', ['LEAVE_WITHOUT_PAY', 'WFH', 'ONLEAVE'])
|
||||
->where('c_type', 'REASON_NOTES')
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<?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' => 1,
|
||||
'c_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'c_code' => 'TEMPORARY_ONLEAVE',
|
||||
'c_name' => 'Nghỉ dự kiến',
|
||||
'c_type' => 'REASON_NOTES',
|
||||
'c_value' => "",
|
||||
'c_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('categories')->where('c_code', 'PERMANENT')->where('c_type', 'PERMANENT_ONLEAVE')->delete();
|
||||
DB::table('categories')->where('c_code', 'TEMPORARY_ONLEAVE')->where('c_type', 'REASON_NOTES')->delete();
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?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_separated')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('is_separated');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('electricity_bills', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->date('billing_date')->comment('Ngày lập hóa đơn');
|
||||
$table->decimal('previous_reading', 12, 2)->comment('Số điện kỳ trước');
|
||||
$table->decimal('current_reading', 12, 2)->comment('Số điện kỳ này');
|
||||
$table->decimal('unit_price', 12, 2)->comment('Đơn giá điện');
|
||||
$table->decimal('total_amount', 12, 2)->comment('Tổng tiền điện');
|
||||
$table->string('notes')->nullable()->comment('Ghi chú');
|
||||
$table->string('file_path')->nullable()->comment('Đường dẫn file PDF');
|
||||
$table->unsignedBigInteger('created_by')->nullable();
|
||||
$table->unsignedBigInteger('updated_by')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('created_by')->references('id')->on('users')->onDelete('set null');
|
||||
$table->foreign('updated_by')->references('id')->on('users')->onDelete('set null');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('electricity_bills');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<?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('electricity_bills', function (Blueprint $table) {
|
||||
$table->string('attachment_path')->nullable()->after('file_path')
|
||||
->comment('User-uploaded attachment (PDF or image)');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('electricity_bills', function (Blueprint $table) {
|
||||
$table->dropColumn('attachment_path');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<?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::create('equipments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('code')->unique()->comment('Mã thiết bị (dùng để scan barcode)');
|
||||
$table->string('name')->comment('Tên thiết bị');
|
||||
$table->text('note')->nullable()->comment('Ghi chú');
|
||||
$table->unsignedBigInteger('current_user_id')->nullable()->comment('Người đang sử dụng, null = đang ở kho');
|
||||
$table->timestamp('assigned_at')->nullable()->comment('Thời điểm giao thiết bị hiện tại');
|
||||
$table->unsignedBigInteger('created_by')->nullable();
|
||||
$table->unsignedBigInteger('updated_by')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('current_user_id')->references('id')->on('users')->onDelete('set null');
|
||||
$table->foreign('created_by')->references('id')->on('users')->onDelete('set null');
|
||||
$table->foreign('updated_by')->references('id')->on('users')->onDelete('set null');
|
||||
});
|
||||
|
||||
Schema::create('equipment_assignments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('equipment_id');
|
||||
$table->unsignedBigInteger('user_id')->nullable()->comment('null = trả về kho');
|
||||
$table->timestamp('assigned_at')->comment('Bắt đầu sử dụng');
|
||||
$table->timestamp('returned_at')->nullable()->comment('Trả thiết bị');
|
||||
$table->text('note')->nullable();
|
||||
$table->unsignedBigInteger('created_by')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('equipment_id')->references('id')->on('equipments')->onDelete('cascade');
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('set null');
|
||||
$table->foreign('created_by')->references('id')->on('users')->onDelete('set null');
|
||||
|
||||
$table->index(['equipment_id', 'returned_at']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('equipment_assignments');
|
||||
Schema::dropIfExists('equipments');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<?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('equipments', function (Blueprint $table) {
|
||||
$table->string('image_path')->nullable()->after('note')
|
||||
->comment('Ảnh thiết bị (tùy chọn)');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('equipments', function (Blueprint $table) {
|
||||
$table->dropColumn('image_path');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -18,5 +18,10 @@ class DatabaseSeeder extends Seeder
|
|||
// 'name' => 'Test User',
|
||||
// 'email' => 'test@example.com',
|
||||
// ]);
|
||||
|
||||
$this->call([
|
||||
UserSeeder::class,
|
||||
FileSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Arial", sans-serif;
|
||||
|
||||
}
|
||||
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f7fafc;
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: #fffefe;
|
||||
padding: 2rem 0;
|
||||
border-radius: 0.5rem;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.my-0 {
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
<title>{{ $data['subject'] }}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<body style="
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
background-color: #edf2f7;
|
||||
">
|
||||
<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="
|
||||
margin: 0 auto;
|
||||
background-color: #ffffff;
|
||||
width: 768px;
|
||||
padding: 24px;
|
||||
">
|
||||
<tr>
|
||||
<td>
|
||||
<h3 style="color: #222222; margin: 5px 0 0 0; font-weight: bold">
|
||||
Hi {{ $data['name'] }},
|
||||
</h3>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<p style=" white-space:pre-line; margin: 0; margin-bottom: 5px">
|
||||
{{ $data['message1'] }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<p style=" white-space:pre-line; margin: 0; margin-bottom: 5px">
|
||||
{{ $data['message2'] }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<p style="margin:0 0 16px;padding:5px;margin: 5px;text-align: center;">
|
||||
<a href="{{ $data['url'] }}"
|
||||
style="
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
background-color: rgba(68,115,196);
|
||||
background-image: linear-gradient(to top left,rgba(0,0,0,.2),rgba(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>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table
|
||||
style="
|
||||
margin: 0 auto;
|
||||
|
||||
width: 768px;
|
||||
">
|
||||
<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>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
|
|
@ -37,9 +36,7 @@
|
|||
<title>{{ $data['subject'] }}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<body style="
|
||||
<body style="
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
background-color: #edf2f7;
|
||||
">
|
||||
|
|
@ -83,7 +80,7 @@
|
|||
<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:
|
||||
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>
|
||||
|
|
@ -98,15 +95,34 @@
|
|||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<p style="margin:0 0 16px;padding:5px;margin: 5px;text-align: center;">
|
||||
<a href='{{ config('app.url') . $data['link'] }}'
|
||||
<p style="text-align: center">
|
||||
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;
|
||||
">
|
||||
<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: rgba(68,115,196);
|
||||
background-image: linear-gradient(to top left,rgba(0,0,0,.2),rgba(0,0,0,.2) 30%,rgba(0,0,0,0));
|
||||
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;
|
||||
|
|
@ -116,11 +132,43 @@
|
|||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
">
|
||||
Check now</a>
|
||||
</p>
|
||||
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>
|
||||
</div>
|
||||
</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>
|
||||
|
|
@ -149,7 +197,6 @@
|
|||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Arial", sans-serif;
|
||||
|
||||
}
|
||||
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f7fafc;
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: #fffefe;
|
||||
padding: 2rem 0;
|
||||
border-radius: 0.5rem;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.my-0 {
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
<title>[SYSTEM] - Task are forgotten</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<body style="
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
background-color: #edf2f7;
|
||||
">
|
||||
<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="
|
||||
margin: 0 auto;
|
||||
background-color: #ffffff;
|
||||
width: 768px;
|
||||
padding: 24px;
|
||||
">
|
||||
<tr>
|
||||
<td>
|
||||
<h3 style="color: #222222; margin: 5px 0 0 0; font-weight: bold">
|
||||
Hi {{ $data[0]['fields']['assignee']['displayName'] }},
|
||||
</h3>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<p style=" white-space:pre-line; margin: 0; margin-bottom: 5px">
|
||||
You currently have some tasks that have not been updated for a long time. Please check and update them!
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
@foreach($data as $iss)
|
||||
<a href="https://apactechvn.atlassian.net/browse/{{$iss['key']}}">{{$iss['fields']['summary']}} - <b>haven't updated in {{$iss['daysRemaining']}} days</b></a><br>
|
||||
@endforeach
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table
|
||||
style="
|
||||
margin: 0 auto;
|
||||
|
||||
width: 768px;
|
||||
">
|
||||
<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>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Thông báo tải lên file mới</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
background-color: #1a73e8;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.content {
|
||||
padding: 20px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
background-color: #1a73e8;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Thông báo tải lên file mới</h1>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<p>Xin chào {{ $user->name }},</p>
|
||||
|
||||
<p>{{ $description }} ở hệ thống APAC Tech.</p>
|
||||
<p>Note: {{ $note }}</p>
|
||||
<p>Vui lòng kiểm tra ngay thông tin bằng cách nhấn nút bên dưới:</p>
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="{{ $url }}" class="button">Kiểm tra ngay</a>
|
||||
</div>
|
||||
|
||||
<p>Trân trọng,<br>Đội ngũ APAC Tech</p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>Email này được gửi tự động từ hệ thống APAC Tech. Vui lòng không trả lời email này.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
require_once __DIR__ . "/../vendor/autoload.php";
|
||||
$app = include_once __DIR__ . '/../bootstrap/app.php';
|
||||
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
||||
$kernel->bootstrap();
|
||||
|
||||
// Cập nhật lại data cho tới tháng hiện tại
|
||||
$currentMonth = Carbon::now()->month;
|
||||
$tmpClass = $app->make('Modules\Admin\app\Http\Controllers\TicketController');
|
||||
$tmpClass->updateOldData($currentMonth, 2025); // Params: month, year
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
dataset
|
||||
test
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import os
|
||||
import shutil
|
||||
|
||||
def organize_files_by_username(folder_path, dest_folder_path):
|
||||
# Lấy danh sách các tệp trong thư mục
|
||||
files = [f for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f))]
|
||||
|
||||
for file in files:
|
||||
# Kiểm tra định dạng tên tệp: <username>_checkin_date.png
|
||||
if "_" in file and file.endswith(".png"):
|
||||
username = file.split("_")[0] # Lấy phần username từ tên tệp
|
||||
|
||||
# Tạo đường dẫn thư mục con
|
||||
subfolder_path = os.path.join(folder_path, username)
|
||||
|
||||
# Tạo thư mục con nếu chưa tồn tại
|
||||
if not os.path.exists(subfolder_path):
|
||||
os.makedirs(subfolder_path)
|
||||
|
||||
# Di chuyển tệp vào thư mục con
|
||||
shutil.move(os.path.join(folder_path, file), os.path.join(subfolder_path, file))
|
||||
|
||||
print("Hoàn thành sắp xếp tệp theo username.")
|
||||
|
||||
# Đường dẫn tới thư mục chứa các tệp
|
||||
folder_path = "/home/joseph/screenshot"
|
||||
dest_folder_path = "/home/joseph/DetectFace/dataset"
|
||||
organize_files_by_username(folder_path, dest_folder_path)
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import cv2
|
||||
import face_recognition
|
||||
import os
|
||||
import numpy as np
|
||||
import pickle
|
||||
datasetPath = "dataset"
|
||||
images = []
|
||||
classNames = []
|
||||
lisFileTrain = os.listdir(datasetPath)
|
||||
|
||||
for file in lisFileTrain:
|
||||
currentImg = cv2.imread(f"{datasetPath}/{file}")
|
||||
images.append(currentImg)
|
||||
classNames.append(os.path.splitext(file)[0].split('_')[0])
|
||||
|
||||
print(len(images))
|
||||
|
||||
def encodeImgs(images, save_path="encodings.pkl"):
|
||||
if os.path.exists(save_path):
|
||||
print(f"Loading encodings from {save_path}...")
|
||||
with open(save_path, "rb") as f:
|
||||
return pickle.load(f)
|
||||
|
||||
encodeList = []
|
||||
for i, img in enumerate(images):
|
||||
print(i+1)
|
||||
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
||||
encode = face_recognition.face_encodings(img)
|
||||
|
||||
if encode: # Check if encodings list is not empty
|
||||
encodeList.append(encode[0])
|
||||
else:
|
||||
print("No face detected in an image. Skipping...")
|
||||
os.remove(f"{datasetPath}/{lisFileTrain[i]}")
|
||||
# Lưu encodeList vào file
|
||||
print(f"Saving encodings to {save_path}...")
|
||||
with open(save_path, "wb") as f:
|
||||
pickle.dump(encodeList, f)
|
||||
|
||||
return encodeList
|
||||
|
||||
encodeListKnow = encodeImgs(images)
|
||||
print("Load data success")
|
||||
print(len(encodeListKnow))
|
||||
|
||||
cap = cv2.VideoCapture(0)
|
||||
|
||||
while True:
|
||||
ret, frame = cap.read()
|
||||
frameS = cv2.resize(frame, (0,0), None, fx=1, fy=1)
|
||||
frameS = cv2.cvtColor(frameS, cv2.COLOR_BGR2RGB)
|
||||
|
||||
faceCurFrame = face_recognition.face_locations(frameS)
|
||||
encodeCurFrame = face_recognition.face_encodings(frameS)
|
||||
|
||||
for encodeFace, faceLoc in zip(encodeCurFrame, faceCurFrame):
|
||||
matches = face_recognition.compare_faces(encodeListKnow, encodeFace)
|
||||
faceDis = face_recognition.face_distance(encodeListKnow, encodeFace)
|
||||
print(faceDis)
|
||||
matchIndex = np.argmin(faceDis)
|
||||
|
||||
if faceDis[matchIndex] < 0.3:
|
||||
name = classNames[matchIndex].upper()
|
||||
else:
|
||||
name = "Unknow"
|
||||
|
||||
y1, x2, y2, x1 = faceLoc
|
||||
y1, x2, y2, x1 = y1, x2, y2, x1
|
||||
cv2.rectangle(frame, (x1,y1), (x2,y2), (0,255,0), 2)
|
||||
cv2.putText(frame, name + f"({(1 - round(faceDis[matchIndex], 2))*100}%)", (x2, y2), cv2.FONT_HERSHEY_COMPLEX, 1, (255,255,255), 2)
|
||||
cv2.imshow('Face decting', frame)
|
||||
|
||||
if cv2.waitKey(1) == ord("q"):
|
||||
break
|
||||
|
||||
cap.release()
|
||||
cv2.destroyAllWindows()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 390 KiB |
|
|
@ -1 +1,3 @@
|
|||
VITE_BACKEND_URL=http://localhost:8000/
|
||||
|
||||
VITE_URL_DRAWIO="https://viewer.diagrams.net/?tags=%7B%7D&lightbox=1&highlight=0000ff&edit=_blank&layers=1&nav=1&title=Test%20Draw.drawio&dark=auto#Uhttps%3A%2F%2Fdrive.google.com%2Fuc%3Fid%3D1LmB9wCac9DonQPFU-53g1nhI9SfvWuWK%26export%3Ddownload"
|
||||
|
|
@ -0,0 +1,425 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
{ # this ensures the entire script is downloaded #
|
||||
|
||||
nvm_has() {
|
||||
type "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
nvm_default_install_dir() {
|
||||
[ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm"
|
||||
}
|
||||
|
||||
nvm_install_dir() {
|
||||
if [ -n "$NVM_DIR" ]; then
|
||||
printf %s "${NVM_DIR}"
|
||||
else
|
||||
nvm_default_install_dir
|
||||
fi
|
||||
}
|
||||
|
||||
nvm_latest_version() {
|
||||
echo "v0.35.0"
|
||||
}
|
||||
|
||||
nvm_profile_is_bash_or_zsh() {
|
||||
local TEST_PROFILE
|
||||
TEST_PROFILE="${1-}"
|
||||
case "${TEST_PROFILE-}" in
|
||||
*"/.bashrc" | *"/.bash_profile" | *"/.zshrc")
|
||||
return
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
#
|
||||
# Outputs the location to NVM depending on:
|
||||
# * The availability of $NVM_SOURCE
|
||||
# * The method used ("script" or "git" in the script, defaults to "git")
|
||||
# NVM_SOURCE always takes precedence unless the method is "script-nvm-exec"
|
||||
#
|
||||
nvm_source() {
|
||||
local NVM_METHOD
|
||||
NVM_METHOD="$1"
|
||||
local NVM_SOURCE_URL
|
||||
NVM_SOURCE_URL="$NVM_SOURCE"
|
||||
if [ "_$NVM_METHOD" = "_script-nvm-exec" ]; then
|
||||
NVM_SOURCE_URL="https://raw.githubusercontent.com/nvm-sh/nvm/$(nvm_latest_version)/nvm-exec"
|
||||
elif [ "_$NVM_METHOD" = "_script-nvm-bash-completion" ]; then
|
||||
NVM_SOURCE_URL="https://raw.githubusercontent.com/nvm-sh/nvm/$(nvm_latest_version)/bash_completion"
|
||||
elif [ -z "$NVM_SOURCE_URL" ]; then
|
||||
if [ "_$NVM_METHOD" = "_script" ]; then
|
||||
NVM_SOURCE_URL="https://raw.githubusercontent.com/nvm-sh/nvm/$(nvm_latest_version)/nvm.sh"
|
||||
elif [ "_$NVM_METHOD" = "_git" ] || [ -z "$NVM_METHOD" ]; then
|
||||
NVM_SOURCE_URL="https://github.com/nvm-sh/nvm.git"
|
||||
else
|
||||
echo >&2 "Unexpected value \"$NVM_METHOD\" for \$NVM_METHOD"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
echo "$NVM_SOURCE_URL"
|
||||
}
|
||||
|
||||
#
|
||||
# Node.js version to install
|
||||
#
|
||||
nvm_node_version() {
|
||||
echo "$NODE_VERSION"
|
||||
}
|
||||
|
||||
nvm_download() {
|
||||
if nvm_has "curl"; then
|
||||
curl --compressed -q "$@"
|
||||
elif nvm_has "wget"; then
|
||||
# Emulate curl with wget
|
||||
ARGS=$(echo "$*" | command sed -e 's/--progress-bar /--progress=bar /' \
|
||||
-e 's/-L //' \
|
||||
-e 's/--compressed //' \
|
||||
-e 's/-I /--server-response /' \
|
||||
-e 's/-s /-q /' \
|
||||
-e 's/-o /-O /' \
|
||||
-e 's/-C - /-c /')
|
||||
# shellcheck disable=SC2086
|
||||
eval wget $ARGS
|
||||
fi
|
||||
}
|
||||
|
||||
install_nvm_from_git() {
|
||||
local INSTALL_DIR
|
||||
INSTALL_DIR="$(nvm_install_dir)"
|
||||
|
||||
if [ -d "$INSTALL_DIR/.git" ]; then
|
||||
echo "=> nvm is already installed in $INSTALL_DIR, trying to update using git"
|
||||
command printf '\r=> '
|
||||
command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" fetch origin tag "$(nvm_latest_version)" --depth=1 2> /dev/null || {
|
||||
echo >&2 "Failed to update nvm, run 'git fetch' in $INSTALL_DIR yourself."
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
# Cloning to $INSTALL_DIR
|
||||
echo "=> Downloading nvm from git to '$INSTALL_DIR'"
|
||||
command printf '\r=> '
|
||||
mkdir -p "${INSTALL_DIR}"
|
||||
if [ "$(ls -A "${INSTALL_DIR}")" ]; then
|
||||
command git init "${INSTALL_DIR}" || {
|
||||
echo >&2 'Failed to initialize nvm repo. Please report this!'
|
||||
exit 2
|
||||
}
|
||||
command git --git-dir="${INSTALL_DIR}/.git" remote add origin "$(nvm_source)" 2> /dev/null \
|
||||
|| command git --git-dir="${INSTALL_DIR}/.git" remote set-url origin "$(nvm_source)" || {
|
||||
echo >&2 'Failed to add remote "origin" (or set the URL). Please report this!'
|
||||
exit 2
|
||||
}
|
||||
command git --git-dir="${INSTALL_DIR}/.git" fetch origin tag "$(nvm_latest_version)" --depth=1 || {
|
||||
echo >&2 'Failed to fetch origin with tags. Please report this!'
|
||||
exit 2
|
||||
}
|
||||
else
|
||||
command git -c advice.detachedHead=false clone "$(nvm_source)" -b "$(nvm_latest_version)" --depth=1 "${INSTALL_DIR}" || {
|
||||
echo >&2 'Failed to clone nvm repo. Please report this!'
|
||||
exit 2
|
||||
}
|
||||
fi
|
||||
fi
|
||||
command git -c advice.detachedHead=false --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" checkout -f --quiet "$(nvm_latest_version)"
|
||||
if [ -n "$(command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" show-ref refs/heads/master)" ]; then
|
||||
if command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" branch --quiet 2>/dev/null; then
|
||||
command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" branch --quiet -D master >/dev/null 2>&1
|
||||
else
|
||||
echo >&2 "Your version of git is out of date. Please update it!"
|
||||
command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" branch -D master >/dev/null 2>&1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "=> Compressing and cleaning up git repository"
|
||||
if ! command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" reflog expire --expire=now --all; then
|
||||
echo >&2 "Your version of git is out of date. Please update it!"
|
||||
fi
|
||||
if ! command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" gc --auto --aggressive --prune=now ; then
|
||||
echo >&2 "Your version of git is out of date. Please update it!"
|
||||
fi
|
||||
return
|
||||
}
|
||||
|
||||
#
|
||||
# Automatically install Node.js
|
||||
#
|
||||
nvm_install_node() {
|
||||
local NODE_VERSION_LOCAL
|
||||
NODE_VERSION_LOCAL="$(nvm_node_version)"
|
||||
|
||||
if [ -z "$NODE_VERSION_LOCAL" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "=> Installing Node.js version $NODE_VERSION_LOCAL"
|
||||
nvm install "$NODE_VERSION_LOCAL"
|
||||
local CURRENT_NVM_NODE
|
||||
|
||||
CURRENT_NVM_NODE="$(nvm_version current)"
|
||||
if [ "$(nvm_version "$NODE_VERSION_LOCAL")" == "$CURRENT_NVM_NODE" ]; then
|
||||
echo "=> Node.js version $NODE_VERSION_LOCAL has been successfully installed"
|
||||
else
|
||||
echo >&2 "Failed to install Node.js $NODE_VERSION_LOCAL"
|
||||
fi
|
||||
}
|
||||
|
||||
install_nvm_as_script() {
|
||||
local INSTALL_DIR
|
||||
INSTALL_DIR="$(nvm_install_dir)"
|
||||
local NVM_SOURCE_LOCAL
|
||||
NVM_SOURCE_LOCAL="$(nvm_source script)"
|
||||
local NVM_EXEC_SOURCE
|
||||
NVM_EXEC_SOURCE="$(nvm_source script-nvm-exec)"
|
||||
local NVM_BASH_COMPLETION_SOURCE
|
||||
NVM_BASH_COMPLETION_SOURCE="$(nvm_source script-nvm-bash-completion)"
|
||||
|
||||
# Downloading to $INSTALL_DIR
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
if [ -f "$INSTALL_DIR/nvm.sh" ]; then
|
||||
echo "=> nvm is already installed in $INSTALL_DIR, trying to update the script"
|
||||
else
|
||||
echo "=> Downloading nvm as script to '$INSTALL_DIR'"
|
||||
fi
|
||||
nvm_download -s "$NVM_SOURCE_LOCAL" -o "$INSTALL_DIR/nvm.sh" || {
|
||||
echo >&2 "Failed to download '$NVM_SOURCE_LOCAL'"
|
||||
return 1
|
||||
} &
|
||||
nvm_download -s "$NVM_EXEC_SOURCE" -o "$INSTALL_DIR/nvm-exec" || {
|
||||
echo >&2 "Failed to download '$NVM_EXEC_SOURCE'"
|
||||
return 2
|
||||
} &
|
||||
nvm_download -s "$NVM_BASH_COMPLETION_SOURCE" -o "$INSTALL_DIR/bash_completion" || {
|
||||
echo >&2 "Failed to download '$NVM_BASH_COMPLETION_SOURCE'"
|
||||
return 2
|
||||
} &
|
||||
for job in $(jobs -p | command sort)
|
||||
do
|
||||
wait "$job" || return $?
|
||||
done
|
||||
chmod a+x "$INSTALL_DIR/nvm-exec" || {
|
||||
echo >&2 "Failed to mark '$INSTALL_DIR/nvm-exec' as executable"
|
||||
return 3
|
||||
}
|
||||
}
|
||||
|
||||
nvm_try_profile() {
|
||||
if [ -z "${1-}" ] || [ ! -f "${1}" ]; then
|
||||
return 1
|
||||
fi
|
||||
echo "${1}"
|
||||
}
|
||||
|
||||
#
|
||||
# Detect profile file if not specified as environment variable
|
||||
# (eg: PROFILE=~/.myprofile)
|
||||
# The echo'ed path is guaranteed to be an existing file
|
||||
# Otherwise, an empty string is returned
|
||||
#
|
||||
nvm_detect_profile() {
|
||||
if [ "${PROFILE-}" = '/dev/null' ]; then
|
||||
# the user has specifically requested NOT to have nvm touch their profile
|
||||
return
|
||||
fi
|
||||
|
||||
if [ -n "${PROFILE}" ] && [ -f "${PROFILE}" ]; then
|
||||
echo "${PROFILE}"
|
||||
return
|
||||
fi
|
||||
|
||||
local DETECTED_PROFILE
|
||||
DETECTED_PROFILE=''
|
||||
|
||||
if [ -n "${BASH_VERSION-}" ]; then
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
DETECTED_PROFILE="$HOME/.bashrc"
|
||||
elif [ -f "$HOME/.bash_profile" ]; then
|
||||
DETECTED_PROFILE="$HOME/.bash_profile"
|
||||
fi
|
||||
elif [ -n "${ZSH_VERSION-}" ]; then
|
||||
DETECTED_PROFILE="$HOME/.zshrc"
|
||||
fi
|
||||
|
||||
if [ -z "$DETECTED_PROFILE" ]; then
|
||||
for EACH_PROFILE in ".profile" ".bashrc" ".bash_profile" ".zshrc"
|
||||
do
|
||||
if DETECTED_PROFILE="$(nvm_try_profile "${HOME}/${EACH_PROFILE}")"; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -n "$DETECTED_PROFILE" ]; then
|
||||
echo "$DETECTED_PROFILE"
|
||||
fi
|
||||
}
|
||||
|
||||
#
|
||||
# Check whether the user has any globally-installed npm modules in their system
|
||||
# Node, and warn them if so.
|
||||
#
|
||||
nvm_check_global_modules() {
|
||||
command -v npm >/dev/null 2>&1 || return 0
|
||||
|
||||
local NPM_VERSION
|
||||
NPM_VERSION="$(npm --version)"
|
||||
NPM_VERSION="${NPM_VERSION:--1}"
|
||||
[ "${NPM_VERSION%%[!-0-9]*}" -gt 0 ] || return 0
|
||||
|
||||
local NPM_GLOBAL_MODULES
|
||||
NPM_GLOBAL_MODULES="$(
|
||||
npm list -g --depth=0 |
|
||||
command sed -e '/ npm@/d' -e '/ (empty)$/d'
|
||||
)"
|
||||
|
||||
local MODULE_COUNT
|
||||
MODULE_COUNT="$(
|
||||
command printf %s\\n "$NPM_GLOBAL_MODULES" |
|
||||
command sed -ne '1!p' | # Remove the first line
|
||||
wc -l | command tr -d ' ' # Count entries
|
||||
)"
|
||||
|
||||
if [ "${MODULE_COUNT}" != '0' ]; then
|
||||
# shellcheck disable=SC2016
|
||||
echo '=> You currently have modules installed globally with `npm`. These will no'
|
||||
# shellcheck disable=SC2016
|
||||
echo '=> longer be linked to the active version of Node when you install a new node'
|
||||
# shellcheck disable=SC2016
|
||||
echo '=> with `nvm`; and they may (depending on how you construct your `$PATH`)'
|
||||
# shellcheck disable=SC2016
|
||||
echo '=> override the binaries of modules installed with `nvm`:'
|
||||
echo
|
||||
|
||||
command printf %s\\n "$NPM_GLOBAL_MODULES"
|
||||
echo '=> If you wish to uninstall them at a later point (or re-install them under your'
|
||||
# shellcheck disable=SC2016
|
||||
echo '=> `nvm` Nodes), you can remove them from the system Node as follows:'
|
||||
echo
|
||||
echo ' $ nvm use system'
|
||||
echo ' $ npm uninstall -g a_module'
|
||||
echo
|
||||
fi
|
||||
}
|
||||
|
||||
nvm_do_install() {
|
||||
if [ -n "${NVM_DIR-}" ] && ! [ -d "${NVM_DIR}" ]; then
|
||||
if [ -e "${NVM_DIR}" ]; then
|
||||
echo >&2 "File \"${NVM_DIR}\" has the same name as installation directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${NVM_DIR}" = "$(nvm_default_install_dir)" ]; then
|
||||
mkdir "${NVM_DIR}"
|
||||
else
|
||||
echo >&2 "You have \$NVM_DIR set to \"${NVM_DIR}\", but that directory does not exist. Check your profile files and environment."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if [ -z "${METHOD}" ]; then
|
||||
# Autodetect install method
|
||||
if nvm_has git; then
|
||||
install_nvm_from_git
|
||||
elif nvm_has nvm_download; then
|
||||
install_nvm_as_script
|
||||
else
|
||||
echo >&2 'You need git, curl, or wget to install nvm'
|
||||
exit 1
|
||||
fi
|
||||
elif [ "${METHOD}" = 'git' ]; then
|
||||
if ! nvm_has git; then
|
||||
echo >&2 "You need git to install nvm"
|
||||
exit 1
|
||||
fi
|
||||
install_nvm_from_git
|
||||
elif [ "${METHOD}" = 'script' ]; then
|
||||
if ! nvm_has nvm_download; then
|
||||
echo >&2 "You need curl or wget to install nvm"
|
||||
exit 1
|
||||
fi
|
||||
install_nvm_as_script
|
||||
else
|
||||
echo >&2 "The environment variable \$METHOD is set to \"${METHOD}\", which is not recognized as a valid installation method."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
|
||||
local NVM_PROFILE
|
||||
NVM_PROFILE="$(nvm_detect_profile)"
|
||||
local PROFILE_INSTALL_DIR
|
||||
PROFILE_INSTALL_DIR="$(nvm_install_dir | command sed "s:^$HOME:\$HOME:")"
|
||||
|
||||
SOURCE_STR="\\nexport NVM_DIR=\"${PROFILE_INSTALL_DIR}\"\\n[ -s \"\$NVM_DIR/nvm.sh\" ] && \\. \"\$NVM_DIR/nvm.sh\" # This loads nvm\\n"
|
||||
|
||||
# shellcheck disable=SC2016
|
||||
COMPLETION_STR='[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion\n'
|
||||
BASH_OR_ZSH=false
|
||||
|
||||
if [ -z "${NVM_PROFILE-}" ] ; then
|
||||
local TRIED_PROFILE
|
||||
if [ -n "${PROFILE}" ]; then
|
||||
TRIED_PROFILE="${NVM_PROFILE} (as defined in \$PROFILE), "
|
||||
fi
|
||||
echo "=> Profile not found. Tried ${TRIED_PROFILE-}~/.bashrc, ~/.bash_profile, ~/.zshrc, and ~/.profile."
|
||||
echo "=> Create one of them and run this script again"
|
||||
echo " OR"
|
||||
echo "=> Append the following lines to the correct file yourself:"
|
||||
command printf "${SOURCE_STR}"
|
||||
echo
|
||||
else
|
||||
if nvm_profile_is_bash_or_zsh "${NVM_PROFILE-}"; then
|
||||
BASH_OR_ZSH=true
|
||||
fi
|
||||
if ! command grep -qc '/nvm.sh' "$NVM_PROFILE"; then
|
||||
echo "=> Appending nvm source string to $NVM_PROFILE"
|
||||
command printf "${SOURCE_STR}" >> "$NVM_PROFILE"
|
||||
else
|
||||
echo "=> nvm source string already in ${NVM_PROFILE}"
|
||||
fi
|
||||
# shellcheck disable=SC2016
|
||||
if ${BASH_OR_ZSH} && ! command grep -qc '$NVM_DIR/bash_completion' "$NVM_PROFILE"; then
|
||||
echo "=> Appending bash_completion source string to $NVM_PROFILE"
|
||||
command printf "$COMPLETION_STR" >> "$NVM_PROFILE"
|
||||
else
|
||||
echo "=> bash_completion source string already in ${NVM_PROFILE}"
|
||||
fi
|
||||
fi
|
||||
if ${BASH_OR_ZSH} && [ -z "${NVM_PROFILE-}" ] ; then
|
||||
echo "=> Please also append the following lines to the if you are using bash/zsh shell:"
|
||||
command printf "${COMPLETION_STR}"
|
||||
fi
|
||||
|
||||
# Source nvm
|
||||
# shellcheck source=/dev/null
|
||||
\. "$(nvm_install_dir)/nvm.sh"
|
||||
|
||||
nvm_check_global_modules
|
||||
|
||||
nvm_install_node
|
||||
|
||||
nvm_reset
|
||||
|
||||
echo "=> Close and reopen your terminal to start using nvm or run the following to use it now:"
|
||||
command printf "${SOURCE_STR}"
|
||||
if ${BASH_OR_ZSH} ; then
|
||||
command printf "${COMPLETION_STR}"
|
||||
fi
|
||||
}
|
||||
|
||||
#
|
||||
# Unsets the various functions defined
|
||||
# during the execution of the install script
|
||||
#
|
||||
nvm_reset() {
|
||||
unset -f nvm_has nvm_install_dir nvm_latest_version nvm_profile_is_bash_or_zsh \
|
||||
nvm_source nvm_node_version nvm_download install_nvm_from_git nvm_install_node \
|
||||
install_nvm_as_script nvm_try_profile nvm_detect_profile nvm_check_global_modules \
|
||||
nvm_do_install nvm_reset nvm_default_install_dir
|
||||
}
|
||||
|
||||
[ "_$NVM_ENV" = "_testing" ] || nvm_do_install
|
||||
|
||||
} # this ensures the entire script is downloaded #
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -11,19 +11,20 @@
|
|||
"test": "npm run jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-javascript": "^6.2.1",
|
||||
"@mantine/charts": "^7.4.1",
|
||||
"@mantine/core": "7.2.1",
|
||||
"@mantine/dates": "^7.3.2",
|
||||
"@mantine/ds": "^7.2.2",
|
||||
"@mantine/form": "^7.2.2",
|
||||
"@mantine/hooks": "7.2.1",
|
||||
"@mantine/notifications": "^7.2.2",
|
||||
"@mantine/vanilla-extract": "7.2.1",
|
||||
"@reduxjs/toolkit": "^1.9.7",
|
||||
"@tabler/icons-react": "^2.40.0",
|
||||
"@uiw/react-codemirror": "^4.21.21",
|
||||
"@vanilla-extract/css": "^1.13.0",
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@mantine/charts": "^7.16.3",
|
||||
"@mantine/core": "^7.13.2",
|
||||
"@mantine/dates": "^7.13.2",
|
||||
"@mantine/form": "^7.13.2",
|
||||
"@mantine/hooks": "^7.13.2",
|
||||
"@mantine/notifications": "^7.13.2",
|
||||
"@mantine/vanilla-extract": "^7.13.2",
|
||||
"@reduxjs/toolkit": "^2.3.0",
|
||||
"@tabler/icons-react": "^3.19.0",
|
||||
"@uiw/react-codemirror": "^4.23.5",
|
||||
"@vanilla-extract/css": "^1.16.0",
|
||||
"@zxing/browser": "^0.2.0",
|
||||
"@zxing/library": "^0.23.0",
|
||||
"axios": "^1.6.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.10",
|
||||
|
|
@ -33,11 +34,13 @@
|
|||
"jwt-decode": "^4.0.0",
|
||||
"moment": "^2.30.1",
|
||||
"react": "^18.2.0",
|
||||
"react-doc-viewer": "^0.1.14",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-file-viewer": "^1.2.1",
|
||||
"react-redux": "^8.1.3",
|
||||
"react-router-dom": "^6.19.0",
|
||||
"reactstrap": "^9.2.2",
|
||||
"recharts": "^2.11.0",
|
||||
"recharts": "^2.15.1",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tests": "^0.4.2"
|
||||
},
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
|
|
@ -11,6 +11,7 @@ import { BrowserRouter, useRoutes } from 'react-router-dom'
|
|||
import mainRoutes from '@/routes/main'
|
||||
import classes from '@/App.module.css'
|
||||
import '@mantine/dates/styles.css'
|
||||
import '@mantine/charts/styles.css'
|
||||
|
||||
export const App = () => {
|
||||
const element = useRoutes(mainRoutes)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export const getListTracking = API_URL + 'v1/admin/tracking'
|
|||
export const addTracking = API_URL + 'v1/admin/tracking/create'
|
||||
export const updateTracking = API_URL + 'v1/admin/tracking/update'
|
||||
export const deleteTracking = API_URL + 'v1/admin/tracking/delete'
|
||||
export const getListTrackingSummary = API_URL + 'v1/admin/tracking/summary'
|
||||
|
||||
// Worklogs
|
||||
export const fetchAllIssues = API_URL + 'v1/admin/jira/fetch-issues'
|
||||
|
|
@ -17,9 +18,12 @@ export const getAllIssuesByProject =
|
|||
export const getAllUserWorklogs = API_URL + 'v1/admin/jira/worklogs'
|
||||
export const getAllUserDoing = API_URL + 'v1/admin/jira/allocation'
|
||||
export const getDetailIssByKey = API_URL + 'v1/admin/jira/issue/detail'
|
||||
export const getPJParticipating =
|
||||
API_URL + 'v1/admin/jira/project-participating'
|
||||
|
||||
//Timekeeping
|
||||
export const getTheTimesheet = API_URL + 'v1/admin/timekeeping'
|
||||
export const exportTimekeeping = API_URL + 'v1/admin/timekeeping/export'
|
||||
export const updateMultipleUserWorkingTime =
|
||||
API_URL + 'v1/admin/timekeeping/addMutilple'
|
||||
export const updateNote = API_URL + 'v1/admin/timekeeping/addNote'
|
||||
|
|
@ -31,17 +35,24 @@ export const updateWorkingDays =
|
|||
|
||||
//Category
|
||||
export const getListMaster = API_URL + 'v1/admin/category/get-list-master'
|
||||
export const getWorkDay = API_URL + 'v1/admin/category/work-days'
|
||||
export const updateWorkDay = API_URL + 'v1/admin/category/update-work-days'
|
||||
|
||||
//LeaveManagement
|
||||
export const getLeaveManagement = API_URL + 'v1/admin/leave-management'
|
||||
export const updateNoteLeave =
|
||||
API_URL + 'v1/admin/leave-management/saveNoteLeave'
|
||||
export const updateNoteStatus =
|
||||
API_URL + 'v1/admin/leave-management/updateNoteStatus'
|
||||
export const exportLeaveManagement =
|
||||
API_URL + 'v1/admin/leave-management/export'
|
||||
|
||||
//Tickets
|
||||
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
|
||||
|
|
@ -77,16 +88,82 @@ export const updateProfilesData =
|
|||
API_URL + 'v1/admin/criterias/profiles-data/update'
|
||||
|
||||
export const listUserTechnical = API_URL + 'v1/admin/technical/get-tech-of-user'
|
||||
export const updateUserTechnical = API_URL + 'v1/admin/technical/technicals-user/update'
|
||||
export const updateUserTechnical =
|
||||
API_URL + 'v1/admin/technical/technicals-user/update'
|
||||
|
||||
export const getAllUser = API_URL + 'v1/admin/technical/get-all-user'
|
||||
export const getAllTechByUserId =
|
||||
API_URL + 'v1/admin/technical/get-tech-by-user-id'
|
||||
|
||||
export const evaluation = API_URL + 'v1/admin/evaluation/report'
|
||||
export const evaluationReportAllUsers =
|
||||
API_URL + 'v1/admin/evaluation/report-all-users'
|
||||
export const sprintReview = API_URL + 'v1/admin/evaluation/sprint-review'
|
||||
export const projectReview = API_URL + 'v1/admin/evaluation/project-review'
|
||||
export const projectReviewAdd =
|
||||
API_URL + 'v1/admin/evaluation/project-review/create'
|
||||
export const projectReviewUpdate =
|
||||
API_URL + 'v1/admin/evaluation/project-review/update'
|
||||
export const projectReviewDelete =
|
||||
API_URL + 'v1/admin/evaluation/project-review/delete'
|
||||
|
||||
export const getAllFilesInProfiles = API_URL + 'v1/admin/profile/all-files'
|
||||
export const updateProfileFolder = API_URL + 'v1/admin/profile/update-profile'
|
||||
export const deleteFile = API_URL + 'v1/admin/profile/delete-profile-file'
|
||||
|
||||
//Technical
|
||||
export const listTechnical = API_URL + 'v1/admin/technical/get-all'
|
||||
export const createTechnical = API_URL + 'v1/admin/technical/create'
|
||||
export const deleteTechnical = API_URL + 'v1/admin/technical/delete'
|
||||
|
||||
// Document
|
||||
export const listDocument = API_URL + 'v1/admin/document/all'
|
||||
export const createDocument = API_URL + 'v1/admin/document/create'
|
||||
export const updateDocument = API_URL + 'v1/admin/document/update'
|
||||
export const deleteDocument = API_URL + 'v1/admin/document/delete'
|
||||
|
||||
// Download File
|
||||
export const downloadFile = API_URL + 'v1/admin/download-file'
|
||||
|
||||
// Electricity Bills
|
||||
export const getElectricityBills = API_URL + 'v1/admin/electricity-bill'
|
||||
export const getElectricityBillById = (id: number) =>
|
||||
API_URL + `v1/admin/electricity-bill/${id}`
|
||||
export const createElectricityBill =
|
||||
API_URL + 'v1/admin/electricity-bill/create'
|
||||
export const updateElectricityBill = (id: number) =>
|
||||
API_URL + `v1/admin/electricity-bill/${id}`
|
||||
export const deleteElectricityBill = (id: number) =>
|
||||
API_URL + `v1/admin/electricity-bill/delete/${id}`
|
||||
export const exportElectricityBillPdf = (id: number) =>
|
||||
API_URL + `v1/admin/electricity-bill/export-pdf/${id}`
|
||||
export const uploadElectricityBillAttachment = (id: number) =>
|
||||
API_URL + `v1/admin/electricity-bill/upload-attachment/${id}`
|
||||
export const deleteElectricityBillAttachment = (id: number) =>
|
||||
API_URL + `v1/admin/electricity-bill/delete-attachment/${id}`
|
||||
|
||||
// Equipments
|
||||
export const getEquipments = API_URL + 'v1/admin/equipment'
|
||||
export const getEquipmentUserOptions = API_URL + 'v1/admin/equipment/users'
|
||||
export const getEquipmentUserStatistics =
|
||||
API_URL + 'v1/admin/equipment/user-statistics'
|
||||
export const exportEquipments = API_URL + 'v1/admin/equipment/export'
|
||||
export const getEquipmentByCode = API_URL + 'v1/admin/equipment/by-code'
|
||||
export const getEquipmentHistory = (id: number) =>
|
||||
API_URL + `v1/admin/equipment/history/${id}`
|
||||
export const createEquipment = API_URL + 'v1/admin/equipment/create'
|
||||
export const updateEquipment = (id: number) =>
|
||||
API_URL + `v1/admin/equipment/${id}`
|
||||
export const assignEquipment = (id: number) =>
|
||||
API_URL + `v1/admin/equipment/${id}/assign`
|
||||
export const uploadEquipmentImage = (id: number) =>
|
||||
API_URL + `v1/admin/equipment/${id}/upload-image`
|
||||
export const deleteEquipmentImage = (id: number) =>
|
||||
API_URL + `v1/admin/equipment/${id}/image`
|
||||
export const deleteEquipment = (id: number) =>
|
||||
API_URL + `v1/admin/equipment/delete/${id}`
|
||||
|
||||
// Files APIs
|
||||
export const getFiles = API_URL + 'v1/admin/profile/files'
|
||||
export const uploadFiles = API_URL + 'v1/admin/profile/upload-files'
|
||||
export const deleteFileById = API_URL + 'v1/admin/profile/files'
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
MultiSelect,
|
||||
Pagination,
|
||||
RadioGroup,
|
||||
ScrollArea,
|
||||
Select,
|
||||
Skeleton,
|
||||
Table,
|
||||
|
|
@ -84,6 +85,9 @@ export const DataTableAll = ({
|
|||
checkBox,
|
||||
size,
|
||||
infoTotal,
|
||||
componentRight,
|
||||
height = 600,
|
||||
keyHighlight = '',
|
||||
}: {
|
||||
data: any[]
|
||||
columns: Column[]
|
||||
|
|
@ -92,6 +96,9 @@ export const DataTableAll = ({
|
|||
checkBox?: boolean
|
||||
size: string
|
||||
infoTotal?: React.ReactNode // Set the type to ReactNode to allow JSX elements
|
||||
componentRight?: React.ReactNode
|
||||
height?: number
|
||||
keyHighlight?: string
|
||||
}) => {
|
||||
const [Tdata, setTData] = useState<any[]>(data)
|
||||
// const [tempData, setTempData] = useState<any[]>([])
|
||||
|
|
@ -204,6 +211,9 @@ export const DataTableAll = ({
|
|||
? 'var(--mantine-color-blue-light)'
|
||||
: undefined
|
||||
}
|
||||
style={{
|
||||
backgroundColor: element[keyHighlight] ? '#ff70704d' : 'transparent',
|
||||
}}
|
||||
>
|
||||
<Table.Td display={checkBox ? 'block' : 'none'}>
|
||||
<Checkbox
|
||||
|
|
@ -240,10 +250,12 @@ export const DataTableAll = ({
|
|||
if (query !== '') {
|
||||
setTData(
|
||||
data.filter((obj) =>
|
||||
Object.values(obj).some(
|
||||
(value: any) =>
|
||||
value !== null &&
|
||||
value.toString().toLowerCase().includes(query.toLowerCase()),
|
||||
Object.values(obj)?.find((c: any) =>
|
||||
c
|
||||
.toString()
|
||||
.normalize('NFC')
|
||||
.toLowerCase()
|
||||
.includes(query.normalize('NFC').toLowerCase()),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -325,10 +337,7 @@ export const DataTableAll = ({
|
|||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
className={classes.totalBox}
|
||||
display={infoTotal ? 'flex' : 'none'}
|
||||
>
|
||||
<Box className={classes.totalBox} display={infoTotal ? 'flex' : 'none'}>
|
||||
<Text fz={'sm'} ta={'right'}>
|
||||
{infoTotal}
|
||||
</Text>
|
||||
|
|
@ -368,8 +377,10 @@ export const DataTableAll = ({
|
|||
}}
|
||||
/>
|
||||
</Box>
|
||||
{componentRight}
|
||||
</Box>
|
||||
<Box className={classes.box}>
|
||||
<Box className={classes.box} style={{ height: height + 50 }}>
|
||||
<ScrollArea h={height}>
|
||||
<Table
|
||||
stickyHeader
|
||||
stickyHeaderOffset={-1}
|
||||
|
|
@ -406,6 +417,7 @@ export const DataTableAll = ({
|
|||
</Table.Thead>
|
||||
<Table.Tbody>{rows}</Table.Tbody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
|
|
@ -451,7 +463,7 @@ export const DataTablePagination = ({
|
|||
})
|
||||
const [selectedRows, setSelectedRows] = useState<any[]>([])
|
||||
const navigate = useNavigate()
|
||||
const urlParams = new URLSearchParams(location.search)
|
||||
let urlParams = new URLSearchParams(location.search)
|
||||
|
||||
// Render headers
|
||||
const headers = columns.map((col) => (
|
||||
|
|
@ -591,7 +603,7 @@ export const DataTablePagination = ({
|
|||
|
||||
// Remove specific parameters
|
||||
params.delete(name)
|
||||
|
||||
urlParams.delete(name)
|
||||
// Update the URL without reloading the page
|
||||
window.history.replaceState({}, document.title, url.toString())
|
||||
}
|
||||
|
|
@ -655,9 +667,8 @@ export const DataTablePagination = ({
|
|||
date_used_to: date_used,
|
||||
})
|
||||
}
|
||||
|
||||
// Add all attributes in 'params' to URL params
|
||||
Object.entries(params).forEach((param) => urlParams.set(...param))
|
||||
urlParams = new URLSearchParams(Object.entries(params))
|
||||
Object.entries(dataFilter).forEach(([key, value]) => {
|
||||
const typeFilter = filterInfo.find((o) => o.key === key).type
|
||||
const hasType = {
|
||||
|
|
@ -674,17 +685,16 @@ export const DataTablePagination = ({
|
|||
if (hasType.Date) {
|
||||
value = value ? Date.parse(String(value)) / 1000 : '' // to unix timestamp
|
||||
}
|
||||
|
||||
console.log(String(value))
|
||||
String(value).length
|
||||
? urlParams.set(key, String(value))
|
||||
: urlParams.delete(key)
|
||||
})
|
||||
|
||||
// Request to get data API
|
||||
const res = await get(url, Object.fromEntries(urlParams.entries()))
|
||||
if (res.status) {
|
||||
setBaseData(res)
|
||||
setTData(res.data)
|
||||
setBaseData(res.data?.links ? res?.data : res)
|
||||
setTData(res.data?.data ? res.data?.data : res.data)
|
||||
setSkeletion(false)
|
||||
navigate({
|
||||
pathname: location.pathname,
|
||||
|
|
@ -760,7 +770,7 @@ export const DataTablePagination = ({
|
|||
|
||||
if (order_by_) {
|
||||
const sortParam = {
|
||||
name: order_by_.split('=')[0].split('_')[2],
|
||||
name: order_by_.split('=')[0].split('_').slice(2).join('_'),
|
||||
status: order_by_.split('=')[1],
|
||||
}
|
||||
if (JSON.stringify(sortParam) !== JSON.stringify(statusSort)) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import DocViewer, { DocViewerRenderers } from "react-doc-viewer";
|
||||
|
||||
export default function FilePreview() {
|
||||
const docs = [
|
||||
// {
|
||||
// uri:
|
||||
// "http://localhost:9000/uploads/ULRYB3ATJ56B/Screenshot%202021-04-28%20at%2014.04.23.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minio%2F20210507%2F%2Fs3%2Faws4_request&X-Amz-Date=20210507T142426Z&X-Amz-Expires=432000&X-Amz-SignedHeaders=host&X-Amz-Signature=761187860be22801088ab8c212733f7f52af8f62d638f1341ee2ae4c18944251"
|
||||
// // "http://localhost:9000/uploads/6QK5HJ84MAEM/RAS-118_CompanyCodes__SalesOffices.xlsx?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minio%2F20210507%2F%2Fs3%2Faws4_request&X-Amz-Date=20210507T110429Z&X-Amz-Expires=432000&X-Amz-SignedHeaders=host&X-Amz-Signature=c20f9b77ffdc1a15910cea5acd3420b6583a1d4d38ce5716da30f1d0ea4315d5"
|
||||
// // "https://res.cloudinary.com/cloudinaryforme/image/upload/v1618339571/workplace-1245776_1920_i9ayae.jpg"
|
||||
// },
|
||||
|
||||
// {
|
||||
// uri:
|
||||
// "https://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf"
|
||||
// },
|
||||
{ uri: "https://s28.q4cdn.com/392171258/files/doc_downloads/test.pdf" },
|
||||
];
|
||||
return (
|
||||
<div className="App">
|
||||
<h1>Hello CodeSandbox</h1>
|
||||
|
||||
<DocViewer pluginRenderers={DocViewerRenderers} documents={docs} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
// import { render } from 'tests'
|
||||
// import Footer from './Footer'
|
||||
|
||||
// describe('Footer component', () => {
|
||||
// it('renders "Footer"', () => {
|
||||
// render(<Footer />)
|
||||
// // expect(getByRole('divider', { name: 'Login' })).toBeInTheDocument()
|
||||
// })
|
||||
// })
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
import { Text, Container, ActionIcon, Group, rem } from '@mantine/core'
|
||||
import {
|
||||
IconBrandTwitter,
|
||||
IconBrandYoutube,
|
||||
IconBrandInstagram,
|
||||
} from '@tabler/icons-react'
|
||||
import { MantineLogo } from '@mantine/ds'
|
||||
import * as classes from './FooterLinks.module.css.ts'
|
||||
|
||||
const data = [
|
||||
{
|
||||
title: 'About',
|
||||
links: [
|
||||
{ label: 'Features', link: '#' },
|
||||
{ label: 'Pricing', link: '#' },
|
||||
{ label: 'Support', link: '#' },
|
||||
{ label: 'Forums', link: '#' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Project',
|
||||
links: [
|
||||
{ label: 'Contribute', link: '#' },
|
||||
{ label: 'Media assets', link: '#' },
|
||||
{ label: 'Changelog', link: '#' },
|
||||
{ label: 'Releases', link: '#' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Community',
|
||||
links: [
|
||||
{ label: 'Join Discord', link: '#' },
|
||||
{ label: 'Follow on Twitter', link: '#' },
|
||||
{ label: 'Email newsletter', link: '#' },
|
||||
{ label: 'GitHub discussions', link: '#' },
|
||||
],
|
||||
},
|
||||
]
|
||||
const Footer = () => {
|
||||
const groups = data.map((group) => {
|
||||
const links = group.links.map((link, index) => (
|
||||
<Text<'a'>
|
||||
key={index}
|
||||
className={classes.link}
|
||||
component="a"
|
||||
href={link.link}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
>
|
||||
{link.label}
|
||||
</Text>
|
||||
))
|
||||
|
||||
return (
|
||||
<div className={classes.wrapper} key={group.title}>
|
||||
<Text className={classes.title}>{group.title}</Text>
|
||||
{links}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<footer className={classes.footer}>
|
||||
<Container className={classes.inner}>
|
||||
<div className={classes.logo}>
|
||||
<MantineLogo size={30} />
|
||||
<Text size="xs" c="dimmed" className={classes.description}>
|
||||
Build fully functional accessible web applications faster than ever
|
||||
</Text>
|
||||
</div>
|
||||
<div className={classes.groups}>{groups}</div>
|
||||
</Container>
|
||||
<Container className={classes.afterFooter}>
|
||||
<Text c="dimmed" size="sm">
|
||||
© 2023 APACTECH.dev. All rights reserved.
|
||||
</Text>
|
||||
|
||||
<Group
|
||||
gap={0}
|
||||
className={classes.social}
|
||||
justify="flex-end"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<ActionIcon size="lg" color="gray" variant="subtle">
|
||||
<IconBrandTwitter
|
||||
style={{ width: rem(18), height: rem(18) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</ActionIcon>
|
||||
<ActionIcon size="lg" color="gray" variant="subtle">
|
||||
<IconBrandYoutube
|
||||
style={{ width: rem(18), height: rem(18) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</ActionIcon>
|
||||
<ActionIcon size="lg" color="gray" variant="subtle">
|
||||
<IconBrandInstagram
|
||||
style={{ width: rem(18), height: rem(18) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Container>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
export default Footer
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
import { rem } from '@mantine/core'
|
||||
import { vars } from '@/theme'
|
||||
import { style } from '@vanilla-extract/css'
|
||||
|
||||
export const footer = style({
|
||||
paddingTop: rem(vars.spacing.xl),
|
||||
paddingBottom: rem(vars.spacing.xl),
|
||||
backgroundColor: `light-dark(${vars.colors.gray[0]}, ${vars.colors.dark[6]})`,
|
||||
borderTop: `${rem(1)} solid light-dark(${vars.colors.gray[2]}, transparent)`,
|
||||
})
|
||||
|
||||
export const logo = style({
|
||||
maxWidth: rem(200),
|
||||
|
||||
'@media': {
|
||||
[`(max-width: ${vars.breakpoints.sm})`]: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const description = style({
|
||||
marginTop: rem(5),
|
||||
|
||||
'@media': {
|
||||
[`(max-width: ${vars.breakpoints.sm})`]: {
|
||||
marginTop: rem(vars.spacing.xs),
|
||||
textAlign: 'center',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const inner = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
|
||||
'@media': {
|
||||
[`(max-width: ${vars.breakpoints.sm})`]: {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const groups = style({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
|
||||
'@media': {
|
||||
[`(max-width: ${vars.breakpoints.sm})`]: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const wrapper = style({
|
||||
width: rem(160),
|
||||
})
|
||||
|
||||
export const link = style({
|
||||
display: 'block',
|
||||
color: `light-dark(${vars.colors.gray[6]}, ${vars.colors.dark[1]})`,
|
||||
fontSize: vars.fontSizes.sm,
|
||||
paddingTop: rem(3),
|
||||
paddingBottom: rem(3),
|
||||
|
||||
':hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
})
|
||||
|
||||
export const title = style({
|
||||
fontSize: vars.fontSizes.lg,
|
||||
fontWeight: 700,
|
||||
fontFamily: `Greycliff CF, ${vars.fontFamily}`,
|
||||
marginBottom: Number(vars.spacing.xs) / 2,
|
||||
color: `light-dark(${vars.colors.black}, ${vars.colors.white})`,
|
||||
})
|
||||
|
||||
export const afterFooter = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: vars.spacing.xl,
|
||||
paddingTop: vars.spacing.xl,
|
||||
paddingBottom: vars.spacing.xl,
|
||||
borderTop: `rem(1) solid light-dark(${vars.colors.gray[2]}, ${vars.colors.dark[4]})`,
|
||||
|
||||
'@media': {
|
||||
[`(max-width: ${vars.breakpoints.sm})`]: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const social = style({
|
||||
'@media': {
|
||||
[`(max-width: ${vars.breakpoints.sm})`]: {
|
||||
marginTop: vars.spacing.xs,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
import { style, styleVariants } from '@vanilla-extract/css'
|
||||
import { vars } from '../../theme'
|
||||
import { rem } from '@mantine/core'
|
||||
|
||||
export const header = style({
|
||||
paddingTop: rem(vars.spacing.sm),
|
||||
backgroundColor: `light-dark(${vars.colors.gray[0]}, ${vars.colors.dark[6]})`,
|
||||
borderBottom: `${rem(1)} solid light-dark(${
|
||||
vars.colors.gray[2]
|
||||
}, transparent)`,
|
||||
marginBottom: rem(120),
|
||||
})
|
||||
|
||||
export const mainSection = style({
|
||||
paddingBottom: rem(vars.spacing.sm),
|
||||
})
|
||||
|
||||
export const user = style({
|
||||
color: `light-dark(${vars.colors.black}, ${vars.colors.dark[0]})`,
|
||||
padding: `${rem(vars.spacing.xs)} ${rem(vars.spacing.sm)}`,
|
||||
borderRadius: rem(vars.radius.sm),
|
||||
transition: 'background-color 100ms ease',
|
||||
|
||||
':hover': {
|
||||
backgroundColor: `light-dark(${vars.colors.white}, ${vars.colors.dark[8]})`,
|
||||
},
|
||||
|
||||
'@media': {
|
||||
[`(max-width: ${vars.breakpoints.xs})`]: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const userActive = style({
|
||||
backgroundColor: `light-dark(${vars.colors.white}, ${vars.colors.dark[8]})`,
|
||||
})
|
||||
|
||||
export const tabsList = style({
|
||||
'::before': {
|
||||
display: 'none',
|
||||
},
|
||||
})
|
||||
|
||||
export const tab = styleVariants({
|
||||
default: {
|
||||
fontWeight: 500,
|
||||
height: rem(38),
|
||||
backgroundColor: 'transparent',
|
||||
position: 'relative',
|
||||
bottom: '-1px',
|
||||
|
||||
':hover': {
|
||||
backgroundColor: `light-dark(${vars.colors.gray[1]}, ${vars.colors.dark[5]})`,
|
||||
},
|
||||
},
|
||||
active: {
|
||||
backgroundColor: `light-dark(${vars.colors.white}, ${vars.colors.dark[7]})`,
|
||||
borderColor: `light-dark(${vars.colors.gray[2]}, ${vars.colors.dark[7]})`,
|
||||
borderBottomColor: 'transparent',
|
||||
},
|
||||
})
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
// import { render } from '@tests'
|
||||
// import Header from './Header'
|
||||
|
||||
// describe('Header component', () => {
|
||||
// it('renders "Header"', () => {
|
||||
// render(<Header header={''} setHeader={function(): void {
|
||||
// throw new Error('Function not implemented.')
|
||||
// } } />)
|
||||
// // expect(getByRole('divider', { name: 'Login' })).toBeInTheDocument()
|
||||
// })
|
||||
// })
|
||||
|
|
@ -1,210 +0,0 @@
|
|||
import { logout } from '@/rtk/dispatches/auth'
|
||||
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
|
||||
import {
|
||||
Avatar,
|
||||
Burger,
|
||||
Container,
|
||||
Group,
|
||||
Menu,
|
||||
Tabs,
|
||||
Text,
|
||||
UnstyledButton,
|
||||
rem,
|
||||
} from '@mantine/core'
|
||||
import { MantineLogo } from '@mantine/ds'
|
||||
import { useDisclosure } from '@mantine/hooks'
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconLogout,
|
||||
IconSettings,
|
||||
IconSwitchHorizontal,
|
||||
} from '@tabler/icons-react'
|
||||
import cx from 'clsx'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { ColorSchemeToggle } from '../ColorSchemeToggle/ColorSchemeToggle'
|
||||
import LanguagePicker from '../LanguagePicker/LanguagePicker'
|
||||
import * as classes from './DoubleHeader.module.css.ts'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
// const user = {
|
||||
// name: 'Jane Spoonfighter',
|
||||
// email: 'janspoon@fighter.dev',
|
||||
// image: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-5.png',
|
||||
// };
|
||||
|
||||
const tabs = ['Home', 'Page 1', 'Page 2', 'Page 3', 'Page 4', 'Page 5']
|
||||
|
||||
const Header = ({
|
||||
header,
|
||||
setHeader,
|
||||
}: {
|
||||
header: string
|
||||
setHeader: (newHeader: string) => void
|
||||
}) => {
|
||||
const [opened, { toggle }] = useDisclosure(false)
|
||||
const [userMenuOpened, setUserMenuOpened] = useState(false)
|
||||
const dispatch = useAppDispatch()
|
||||
const navigate = useNavigate()
|
||||
const user = useAppSelector((state) => state.authentication.user.user)
|
||||
|
||||
const items = tabs.map((tab) => (
|
||||
<Tabs.Tab value={tab} key={tab} onClick={() => setHeader(tab)}>
|
||||
{tab}
|
||||
</Tabs.Tab>
|
||||
))
|
||||
const handleLogout = useCallback(() => {
|
||||
dispatch(logout(navigate))
|
||||
}, [dispatch, navigate])
|
||||
return (
|
||||
<div className={classes.header}>
|
||||
<Container className={classes.mainSection} size="xl">
|
||||
<Group justify="space-between">
|
||||
<MantineLogo size={28} />
|
||||
|
||||
<Burger opened={opened} onClick={toggle} hiddenFrom="xs" size="sm" />
|
||||
|
||||
<Group>
|
||||
<LanguagePicker />
|
||||
|
||||
<ColorSchemeToggle />
|
||||
<Menu
|
||||
width={260}
|
||||
position="bottom-end"
|
||||
transitionProps={{ transition: 'pop-top-right' }}
|
||||
onClose={() => setUserMenuOpened(false)}
|
||||
onOpen={() => setUserMenuOpened(true)}
|
||||
withinPortal
|
||||
>
|
||||
<Menu.Target>
|
||||
<UnstyledButton
|
||||
className={cx(classes.user, {
|
||||
[classes.userActive]: userMenuOpened,
|
||||
})}
|
||||
>
|
||||
<Group gap={7}>
|
||||
<Avatar
|
||||
src={
|
||||
'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-5.png'
|
||||
}
|
||||
alt={user.name}
|
||||
radius="xl"
|
||||
size={25}
|
||||
/>
|
||||
<Text fw={500} size="sm" lh={1} mr={3}>
|
||||
{user.name}
|
||||
</Text>
|
||||
<IconChevronDown
|
||||
style={{ width: rem(12), height: rem(12) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
{/* <Menu.Item
|
||||
leftSection={
|
||||
<IconHeart
|
||||
style={{ width: rem(16), height: rem(16) }}
|
||||
color={theme.colors.red[6]}
|
||||
stroke={1.5}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Liked posts
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={
|
||||
<IconStar
|
||||
style={{ width: rem(16), height: rem(16) }}
|
||||
color={theme.colors.yellow[6]}
|
||||
stroke={1.5}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Saved posts
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={
|
||||
<IconMessage
|
||||
style={{ width: rem(16), height: rem(16) }}
|
||||
color={theme.colors.blue[6]}
|
||||
stroke={1.5}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Your comments
|
||||
</Menu.Item> */}
|
||||
|
||||
<Menu.Label>Settings</Menu.Label>
|
||||
<Menu.Item
|
||||
leftSection={
|
||||
<IconSettings
|
||||
style={{ width: rem(16), height: rem(16) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Account settings
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={
|
||||
<IconSwitchHorizontal
|
||||
style={{ width: rem(16), height: rem(16) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Change account
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={
|
||||
<IconLogout
|
||||
style={{ width: rem(16), height: rem(16) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
}
|
||||
onClick={handleLogout}
|
||||
>
|
||||
Logout
|
||||
</Menu.Item>
|
||||
|
||||
{/* <Menu.Divider />
|
||||
|
||||
<Menu.Label>Danger zone</Menu.Label>
|
||||
<Menu.Item
|
||||
leftSection={
|
||||
<IconPlayerPause style={{ width: rem(16), height: rem(16) }} stroke={1.5} />
|
||||
}
|
||||
>
|
||||
Pause subscription
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
color="red"
|
||||
leftSection={<IconTrash style={{ width: rem(16), height: rem(16) }} stroke={1.5} />}
|
||||
>
|
||||
Delete account
|
||||
</Menu.Item> */}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Group>
|
||||
</Group>
|
||||
</Container>
|
||||
<Container size="md">
|
||||
<Tabs
|
||||
defaultValue={header}
|
||||
variant="outline"
|
||||
visibleFrom="sm"
|
||||
classNames={{
|
||||
// root: classes.tab.active,
|
||||
list: classes.tabsList,
|
||||
tab: classes.tab.default,
|
||||
}}
|
||||
>
|
||||
<Tabs.List justify="center">{items}</Tabs.List>
|
||||
</Tabs>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
|
|
@ -4,6 +4,7 @@ import { logout } from '@/rtk/dispatches/auth'
|
|||
import { get, post } from '@/rtk/helpers/apiService'
|
||||
import { requirementsPassword } from '@/rtk/helpers/variables'
|
||||
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
|
||||
import { checkPermissions } from '@/utils/checkRoles'
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
|
|
@ -26,6 +27,8 @@ import {
|
|||
IconCalendarClock,
|
||||
IconChartDots2,
|
||||
IconDevices,
|
||||
IconFileInvoice,
|
||||
IconFolders,
|
||||
IconLayoutSidebarLeftExpand,
|
||||
IconLayoutSidebarRightExpand,
|
||||
IconListCheck,
|
||||
|
|
@ -36,6 +39,7 @@ import {
|
|||
IconReport,
|
||||
IconScan,
|
||||
IconSettings,
|
||||
IconShredder,
|
||||
IconSun,
|
||||
IconTicket,
|
||||
IconUsersGroup,
|
||||
|
|
@ -45,7 +49,6 @@ import { useCallback, useEffect, useState } from 'react'
|
|||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import PasswordRequirementInput from '../PasswordRequirementInput/PasswordRequirementInput'
|
||||
import classes from './NavbarSimpleColored.module.css'
|
||||
import { checkPermissions } from '@/utils/checkRoles'
|
||||
|
||||
const data = [
|
||||
// { link: '/dashboard', label: 'Dashboard', icon: IconHome },
|
||||
|
|
@ -53,15 +56,15 @@ const data = [
|
|||
link: '/timekeeping',
|
||||
label: 'Timekeeping',
|
||||
icon: IconCalendar,
|
||||
permissions: 'admin,hr,staff,tester',
|
||||
permissions: 'admin,hr,staff,tester,accountant',
|
||||
group: 'staff',
|
||||
},
|
||||
{
|
||||
link: '/tracking',
|
||||
label: 'Check in/out',
|
||||
icon: IconScan,
|
||||
permissions: 'hr,admin',
|
||||
group: 'other',
|
||||
permissions: 'staff,hr,admin',
|
||||
group: 'staff',
|
||||
},
|
||||
{
|
||||
link: '/worklogs',
|
||||
|
|
@ -70,11 +73,18 @@ const data = [
|
|||
permissions: 'admin,hr,staff,tester',
|
||||
group: 'staff',
|
||||
},
|
||||
{
|
||||
link: '/documents',
|
||||
label: 'Documents',
|
||||
icon: IconFileInvoice,
|
||||
permissions: 'admin,hr,staff,tester,accountant',
|
||||
group: 'staff',
|
||||
},
|
||||
{
|
||||
link: '/leave-management',
|
||||
label: 'Leave Management',
|
||||
icon: IconCalendarClock,
|
||||
permissions: 'admin,hr,staff,tester',
|
||||
permissions: 'admin,hr,staff,tester,accountant',
|
||||
group: 'staff',
|
||||
},
|
||||
{
|
||||
|
|
@ -137,8 +147,22 @@ const data = [
|
|||
link: '/organization-settings',
|
||||
label: 'Organization Settings',
|
||||
icon: IconSettings,
|
||||
group: 'other',
|
||||
permissions: 'admin,hr',
|
||||
},
|
||||
{
|
||||
link: '/profiles',
|
||||
label: 'Profiles',
|
||||
icon: IconFolders,
|
||||
group: 'admin',
|
||||
permissions: 'admin',
|
||||
permissions: 'admin,accountant',
|
||||
},
|
||||
{
|
||||
link: '/office-support',
|
||||
label: 'Office Support',
|
||||
icon: IconShredder,
|
||||
group: 'other',
|
||||
permissions: 'admin,hr,accountant',
|
||||
},
|
||||
// { link: '/jira', label: 'Jira', icon: IconSubtask },
|
||||
// { link: '/custom-theme', label: 'Custom Theme', icon: IconBrush },
|
||||
|
|
@ -183,7 +207,7 @@ const Navbar = ({
|
|||
confirm_password: '',
|
||||
})
|
||||
const [countSpam, setCountSpam] = useState(0)
|
||||
const [avatar, setAvatar] = useState(user.user.avatar)
|
||||
const [avatar, setAvatar] = useState(user?.user?.avatar)
|
||||
|
||||
const navigate = useNavigate()
|
||||
const dispatch = useAppDispatch()
|
||||
|
|
@ -231,7 +255,11 @@ const Navbar = ({
|
|||
// })
|
||||
|
||||
const group = [
|
||||
{ name: 'staff', label: 'General', permissions: 'admin,hr,staff,tester' },
|
||||
{
|
||||
name: 'staff',
|
||||
label: 'General',
|
||||
permissions: 'admin,hr,staff,tester,accountant',
|
||||
},
|
||||
{ name: 'admin', label: 'Admin', permissions: 'admin' },
|
||||
{ name: 'other', label: 'Other', permissions: 'admin,hr' },
|
||||
{ name: 'test', label: 'Test', permissions: 'admin,tester' },
|
||||
|
|
@ -301,7 +329,7 @@ const Navbar = ({
|
|||
const res = await post(
|
||||
changePassword,
|
||||
{
|
||||
email: user.user.email,
|
||||
email: user?.user?.email,
|
||||
password: dataChange.password,
|
||||
new_password: dataChange.new_password,
|
||||
confirm_password: dataChange.confirm_password,
|
||||
|
|
@ -417,7 +445,7 @@ const Navbar = ({
|
|||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{user.user.name}
|
||||
{user?.user?.name}
|
||||
</Code>
|
||||
</Box>
|
||||
<Box
|
||||
|
|
@ -569,7 +597,7 @@ const Navbar = ({
|
|||
<Box p="sm">
|
||||
<TextInput
|
||||
label="E-mail"
|
||||
value={user.user.email}
|
||||
value={user?.user?.email}
|
||||
disabled
|
||||
mb={'md'}
|
||||
></TextInput>
|
||||
|
|
|
|||
|
|
@ -3,3 +3,8 @@ declare module '*.css'
|
|||
declare module '@codemirror/lang-javascript'
|
||||
|
||||
declare const __VITE_BACKEND_URL__: string
|
||||
|
||||
declare module 'react-file-viewer' {
|
||||
const FileViewer: any
|
||||
export default FileViewer
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
.root {
|
||||
/* font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif; */
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.label[data-hovered] {
|
||||
background-color: var(--mantine-color-gray-0);
|
||||
}
|
||||
/* Add the light and dark logic using classes, for example */
|
||||
.label[data-hovered].light {
|
||||
background-color: var(--mantine-color-gray-0);
|
||||
}
|
||||
.label[data-hovered].dark {
|
||||
background-color: var(--mantine-color-dark-6);
|
||||
color: var(--mantine-color-white);
|
||||
}
|
||||
|
||||
.label[data-selected] {
|
||||
font-weight: 700;
|
||||
}
|
||||
.label[data-selected].light {
|
||||
background-color: var(--mantine-color-blue-0);
|
||||
color: var(--mantine-color-black);
|
||||
}
|
||||
.label[data-selected].dark {
|
||||
background-color: rgba(var(--mantine-color-blue-8), 0.35);
|
||||
color: var(--mantine-color-blue-0);
|
||||
}
|
||||
|
|
@ -0,0 +1,335 @@
|
|||
import { getFiles, uploadFiles } from '@/api/Admin'
|
||||
import { get } from '@/rtk/helpers/apiService'
|
||||
import { getAccessToken } from '@/rtk/localStorage'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Collapse,
|
||||
Group,
|
||||
Modal,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core'
|
||||
import { notifications } from '@mantine/notifications'
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconDownload,
|
||||
IconFileTypeDocx,
|
||||
IconFileTypePdf,
|
||||
IconFolder,
|
||||
IconListCheck,
|
||||
IconPhoto,
|
||||
IconSearch,
|
||||
IconTrash,
|
||||
} from '@tabler/icons-react'
|
||||
import axios from 'axios'
|
||||
import { useEffect, useState } from 'react'
|
||||
import FileUploadForm from '../Profile/components/FileUploadForm'
|
||||
import classes from './AllProfiles.module.css'
|
||||
|
||||
interface FileData {
|
||||
id: number
|
||||
name: string
|
||||
url: string
|
||||
type: string
|
||||
description?: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface GroupedFiles {
|
||||
[key: string]: FileData[]
|
||||
}
|
||||
|
||||
const AllProfiles = () => {
|
||||
const [groupedFiles, setGroupedFiles] = useState<GroupedFiles>({})
|
||||
const [currentUser, setCurrentUser] = useState<string>('')
|
||||
const [openedProfile, setOpenedProfile] = useState(false)
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [expandedFolders, setExpandedFolders] = useState<{
|
||||
[key: string]: boolean
|
||||
}>({})
|
||||
const [searchTerms, setSearchTerms] = useState<{ [key: string]: string }>({})
|
||||
|
||||
const toggleFolder = (userName: string) => {
|
||||
setExpandedFolders((prev) => ({
|
||||
...prev,
|
||||
[userName]: !prev[userName],
|
||||
}))
|
||||
}
|
||||
|
||||
const getFileIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'document':
|
||||
return <IconFileTypeDocx size={16} />
|
||||
case 'image':
|
||||
return <IconPhoto size={16} />
|
||||
default:
|
||||
return <IconFileTypePdf size={16} />
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (
|
||||
e: React.FormEvent,
|
||||
fileName: string,
|
||||
description: string,
|
||||
currentUser: string
|
||||
) => {
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
const formData = new FormData()
|
||||
if (selectedFile) {
|
||||
formData.append('file', selectedFile)
|
||||
formData.append('name', fileName)
|
||||
formData.append('description', description)
|
||||
formData.append('user_name', currentUser)
|
||||
|
||||
try {
|
||||
const token = await getAccessToken()
|
||||
const response = await axios.post(uploadFiles, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (response.status === 200) {
|
||||
setSelectedFile(null)
|
||||
await getAllFiles()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error('Error uploading file:', error)
|
||||
throw error
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const getAllFiles = async () => {
|
||||
try {
|
||||
const res = await get(getFiles)
|
||||
if (res.status === true) {
|
||||
setGroupedFiles(res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
const removeFile = async (id: number) => {
|
||||
try {
|
||||
const token = await getAccessToken();
|
||||
const response = await axios.delete(`${import.meta.env.VITE_BACKEND_URL}api/v1/admin/profile/files/${id}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
notifications.show({
|
||||
title: 'Thành công',
|
||||
message: 'Xóa file thành công',
|
||||
color: 'green',
|
||||
});
|
||||
await getAllFiles();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
notifications.show({
|
||||
title: 'Lỗi',
|
||||
message: 'Không thể xóa file',
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getAllFiles()
|
||||
}, [])
|
||||
|
||||
const filterFiles = (files: FileData[], searchTerm: string) => {
|
||||
return files.filter(
|
||||
(file) =>
|
||||
file.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(file.description &&
|
||||
file.description.toLowerCase().includes(searchTerm.toLowerCase())),
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={classes.title}>
|
||||
<h3>
|
||||
<Text size="sm">Admin/</Text>
|
||||
Files Management
|
||||
</h3>
|
||||
</div>
|
||||
<Box ml={'md'}>
|
||||
<Stack gap="xs">
|
||||
{Object.entries(groupedFiles).map(([userName, files]) => (
|
||||
<Card key={userName} shadow="xs" radius="sm" withBorder p="xs">
|
||||
<Group
|
||||
justify="space-between"
|
||||
mb="xs"
|
||||
gap="xs"
|
||||
onClick={() => toggleFolder(userName)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Group gap="xs">
|
||||
<IconFolder size={18} color="var(--mantine-color-yellow-9)" />
|
||||
<Title order={5}>{userName}</Title>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={() => toggleFolder(userName)}
|
||||
leftSection={
|
||||
<IconChevronDown
|
||||
size={14}
|
||||
style={{
|
||||
transform: expandedFolders[userName]
|
||||
? 'rotate(180deg)'
|
||||
: 'none',
|
||||
transition: 'transform 0.2s',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{expandedFolders[userName] ? 'Collapse' : 'Expand'}
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() => {
|
||||
setCurrentUser(userName)
|
||||
setOpenedProfile(true)
|
||||
}}
|
||||
>
|
||||
<Group gap={2}>
|
||||
<IconListCheck size={14} />
|
||||
<Text size="xs">Upload Files</Text>
|
||||
</Group>
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
<Collapse in={expandedFolders[userName]}>
|
||||
<Stack gap="xs">
|
||||
<TextInput
|
||||
placeholder="Search files by name or description..."
|
||||
size="xs"
|
||||
leftSection={<IconSearch size={14} />}
|
||||
value={searchTerms[userName] || ''}
|
||||
onChange={(e) =>
|
||||
setSearchTerms((prev) => ({
|
||||
...prev,
|
||||
[userName]: e.target.value,
|
||||
}))
|
||||
}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
{filterFiles(files, searchTerms[userName] || '')
|
||||
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
|
||||
.map((file: FileData) => (
|
||||
<Card
|
||||
key={file.id}
|
||||
shadow="xs"
|
||||
padding="xs"
|
||||
radius="sm"
|
||||
withBorder
|
||||
>
|
||||
<Group justify="space-between" gap="xs">
|
||||
<Group gap="xs">
|
||||
{getFileIcon(file.type)}
|
||||
<Box>
|
||||
<Text size="xs" fw={500}>
|
||||
{file.name}
|
||||
</Text>
|
||||
{file.description && (
|
||||
<Text size="xs" c="dimmed">
|
||||
{file.description}
|
||||
</Text>
|
||||
)}
|
||||
<Text size="xs" c="dimmed">
|
||||
Uploaded:{' '}
|
||||
{new Date(file.created_at).toLocaleDateString()}
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
color="blue"
|
||||
component='a'
|
||||
href={`${import.meta.env.VITE_BACKEND_URL}${
|
||||
import.meta.env.VITE_BACKEND_URL?.includes(
|
||||
'localhost',
|
||||
)
|
||||
? ''
|
||||
: 'image/'
|
||||
}${file.url.slice(1)}`}
|
||||
target="_blank"
|
||||
>
|
||||
<Group gap={2}>
|
||||
<IconDownload size={12} />
|
||||
<Text size="xs">Download</Text>
|
||||
</Group>
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
color="red"
|
||||
onClick={() => removeFile(file.id)}
|
||||
>
|
||||
<Group gap={2}>
|
||||
<IconTrash size={12} />
|
||||
<Text size="xs">Delete</Text>
|
||||
</Group>
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
),
|
||||
)}
|
||||
</Stack>
|
||||
</Collapse>
|
||||
</Card>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
<Modal
|
||||
size="lg"
|
||||
opened={openedProfile}
|
||||
onClose={() => {
|
||||
setOpenedProfile(false)
|
||||
setCurrentUser('')
|
||||
setSelectedFile(null)
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<FileUploadForm
|
||||
data={groupedFiles[currentUser] || []}
|
||||
handleSubmit={handleSubmit}
|
||||
handleFileChange={(file) => file && setSelectedFile(file)}
|
||||
removeFile={removeFile}
|
||||
isLoading={isLoading}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
</Box>
|
||||
</Modal>
|
||||
</Box>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AllProfiles
|
||||
|
|
@ -10,6 +10,7 @@ import {
|
|||
Popover,
|
||||
Text,
|
||||
Tooltip,
|
||||
Switch,
|
||||
} from '@mantine/core'
|
||||
import { IconInnerShadowTopRightFilled } from '@tabler/icons-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
|
@ -80,6 +81,31 @@ const Allocation = () => {
|
|||
const [opened, setOpened] = useState(false)
|
||||
const [issDetail, setIssDetail] = useState('')
|
||||
const [data, setData] = useState<any>({})
|
||||
const [showDrawio, setShowDrawio] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
// Check if iframe already exists
|
||||
const existingIframe = document.querySelector('#drawio iframe')
|
||||
if (existingIframe) return
|
||||
|
||||
// Add iframe only if it doesn't exist
|
||||
const iframe = document.createElement('iframe')
|
||||
iframe.src = import.meta.env.VITE_URL_DRAWIO ?? ''
|
||||
iframe.style.width = '100%'
|
||||
iframe.style.height = '500px'
|
||||
const drawioDiv = document.getElementById('drawio')
|
||||
if (drawioDiv) {
|
||||
drawioDiv.appendChild(iframe)
|
||||
}
|
||||
|
||||
return () => {
|
||||
const iframe = document.querySelector('#drawio iframe')
|
||||
if (iframe) {
|
||||
iframe.remove()
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const getAll = async () => {
|
||||
try {
|
||||
const res = await get(getAllUserDoing)
|
||||
|
|
@ -117,6 +143,14 @@ const Allocation = () => {
|
|||
}, [])
|
||||
return (
|
||||
<div>
|
||||
<Box style={{ display: 'flex', alignItems: 'center', margin: '10px' }}>
|
||||
<Switch
|
||||
label="Show Diagram"
|
||||
checked={showDrawio}
|
||||
onChange={(event) => setShowDrawio(event.currentTarget.checked)}
|
||||
/>
|
||||
</Box>
|
||||
<div id="drawio" style={{ display: showDrawio ? 'block' : 'none' }}></div>
|
||||
<div>
|
||||
<Box
|
||||
style={{
|
||||
|
|
@ -138,12 +172,12 @@ const Allocation = () => {
|
|||
}}
|
||||
>
|
||||
<Box>
|
||||
<Text fw={600} fz={'md'}>
|
||||
{/* <Text fw={600} fz={'md'}>
|
||||
Admin/
|
||||
</Text>
|
||||
<Text fw={700} fz={'lg'}>
|
||||
Personnel Allocation
|
||||
</Text>
|
||||
</Text> */}
|
||||
<Text fw={600} fz={'sm'} c={'gray'} fs={'italic'} ml={'md'}>
|
||||
"P:" is the timspent/timeestimate number within the project itself
|
||||
</Text>
|
||||
|
|
@ -224,23 +258,34 @@ const Allocation = () => {
|
|||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Box w='10%'>
|
||||
<Box w="10%">
|
||||
<IconInnerShadowTopRightFilled
|
||||
style={{ color: 'orange' }}
|
||||
height={20}
|
||||
width={20}
|
||||
display={userData.issues?.filter(
|
||||
display={
|
||||
userData.issues?.filter(
|
||||
(iss: Issue) =>
|
||||
iss.fields.status.name === 'In Progress' &&
|
||||
((Date.now() - (new Date(iss.changelog?.histories[0]?.created)).getTime()) > 172800000)
|
||||
).length > 0 ? 'block' :'none'}
|
||||
iss.fields.status.name ===
|
||||
'In Progress' &&
|
||||
Date.now() -
|
||||
new Date(
|
||||
iss.changelog?.histories[0]?.created,
|
||||
).getTime() >
|
||||
172800000,
|
||||
).length > 0
|
||||
? 'block'
|
||||
: 'none'
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Box display={'flex'}>
|
||||
<Avatar
|
||||
size={'sm'}
|
||||
ml={'5px'}
|
||||
src={userData.user.avatarUrls['48x48']}
|
||||
src={
|
||||
userData.user.avatarUrls['48x48']
|
||||
}
|
||||
/>
|
||||
<Text ml={'md'} fw={600}>
|
||||
{user}
|
||||
|
|
@ -260,9 +305,12 @@ const Allocation = () => {
|
|||
}}
|
||||
ml={'md'}
|
||||
p="0 20px"
|
||||
|
||||
>
|
||||
<Text ml={'md'} fw={700} fz={'sm'}>{`P: `}</Text>
|
||||
<Text
|
||||
ml={'md'}
|
||||
fw={700}
|
||||
fz={'sm'}
|
||||
>{`P: `}</Text>
|
||||
<Text fw={700} c="green" fz={'sm'}>{`${
|
||||
userData.p_total_spent / 60 / 60
|
||||
}h/`}</Text>
|
||||
|
|
@ -316,7 +364,7 @@ const Allocation = () => {
|
|||
<Box
|
||||
className={
|
||||
Date.now() - date.getTime() >
|
||||
172800000 * 5
|
||||
172800000 * 5 // 10 days
|
||||
? classes['blinking-background']
|
||||
: ''
|
||||
}
|
||||
|
|
@ -326,7 +374,7 @@ const Allocation = () => {
|
|||
borderRadius: '10px',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: issStatus
|
||||
? issLastHistory < 172800000
|
||||
? issLastHistory < 172800000 // 2 days
|
||||
? '#d1f3d1'
|
||||
: '#ffff8a'
|
||||
: '',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
.title {
|
||||
background-color: light-dark(var(white), var(--mantine-color-dark-7));
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 var(--mantine-spacing-sm) var(--mantine-spacing-lg)
|
||||
var(--mantine-spacing-sm);
|
||||
border-bottom: solid rgba(201, 201, 201, 0.377) 1px;
|
||||
}
|
||||
|
||||
.optionIcon {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.deleteIcon {
|
||||
color: red;
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
border-radius: 25%;
|
||||
}
|
||||
|
||||
.editIcon {
|
||||
color: rgb(9, 132, 132);
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
border-radius: 25%;
|
||||
}
|
||||
|
||||
.editIcon:hover {
|
||||
background-color: rgba(203, 203, 203, 0.809);
|
||||
}
|
||||
|
||||
.deleteIcon:hover {
|
||||
background-color: rgba(203, 203, 203, 0.809);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue