Bổ sung note cho Timekepping
This commit is contained in:
parent
b203e8d82c
commit
ae5737c3bf
|
|
@ -4,6 +4,7 @@ namespace Modules\Admin\app\Http\Controllers;
|
|||
|
||||
use App\Helper\Cache\CurrentMonthTimekeeping;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Notes;
|
||||
use App\Traits\AnalyzeData;
|
||||
use App\Traits\HasFilterRequest;
|
||||
use App\Traits\HasOrderByRequest;
|
||||
|
|
@ -23,7 +24,7 @@ class TimekeepingController extends Controller
|
|||
use HasFilterRequest;
|
||||
use HasSearchRequest;
|
||||
use AnalyzeData;
|
||||
|
||||
|
||||
public function get(Request $request)
|
||||
{
|
||||
$currentDate = Carbon::now();
|
||||
|
|
@ -32,11 +33,9 @@ class TimekeepingController extends Controller
|
|||
$data = MonthlyTimekeeping::where('month', '=', $request->month)->where('year', '=', $request->year)->first();
|
||||
if ($currentMonth == (int)$request->month && $currentYear == (int)$request->year) {
|
||||
$cacheData = CurrentMonthTimekeeping::getCacheCurrentMonthTimekeeping();
|
||||
// $result = $this->analyzeCurrentMonthTimeKeepingData($currentMonth, $currentYear);
|
||||
// dd($result);die;
|
||||
if ($cacheData) {
|
||||
$cacheData->data = json_decode($cacheData->data, true);
|
||||
return response()->json(['status' => true, 'data' => $cacheData->data, 'working_days'=> $cacheData->working_days, 'message' => 'Get from cache']);
|
||||
return response()->json(['status' => true, 'data' => $cacheData->data, 'working_days' => $cacheData->working_days, 'message' => 'Get from cache']);
|
||||
} else {
|
||||
$result = $this->analyzeCurrentMonthTimeKeepingData($currentMonth, $currentYear);
|
||||
if ($data) {
|
||||
|
|
@ -49,7 +48,7 @@ class TimekeepingController extends Controller
|
|||
} else {
|
||||
if ($data) {
|
||||
$data['data'] = json_decode($data['data']);
|
||||
return response()->json(['status' => true, 'data' => $data['data'], 'working_days'=> $data['working_days'], 'message' => 'Get from DB']);
|
||||
return response()->json(['status' => true, 'data' => $data['data'], 'working_days' => $data['working_days'], 'message' => 'Get from DB']);
|
||||
} else {
|
||||
$result = $this->analyzeCurrentMonthTimeKeepingData($request->month, $request->year);
|
||||
MonthlyTimekeeping::create(['month' => $request->month, 'year' => $request->year, 'working_days' => Carbon::create((int)$request->year, (int)$request->month)->daysInMonth, 'data' => json_encode($result)]);
|
||||
|
|
@ -95,12 +94,42 @@ class TimekeepingController extends Controller
|
|||
public function saveWorkingDays(Request $request)
|
||||
{
|
||||
$data = MonthlyTimekeeping::where('month', '=', $request->month)->where('year', '=', $request->year)->first();
|
||||
if($data){
|
||||
$data->update(['working_days'=>$request->working_days]);
|
||||
if ($data) {
|
||||
$data->update(['working_days' => $request->working_days]);
|
||||
$this->createOrUpdateRecordForCurrentMonth($request->month, $request->year);
|
||||
return response()->json(['status' => true, 'message' => 'Update successful']);
|
||||
}
|
||||
|
||||
return response()->json(['status' => false, 'message' => 'Update failed']);
|
||||
}
|
||||
|
||||
public function addNoteForUser(Request $request)
|
||||
{
|
||||
$user_id = $request->users["id"] ?? "";
|
||||
$month = $request->month;
|
||||
$year = $request->year;
|
||||
$day = $request->day;
|
||||
$time_type = $request->type;
|
||||
$reason = $request->reason;
|
||||
$note = $request->note;
|
||||
|
||||
if ($user_id == "") {
|
||||
return response()->json(['status' => false, 'message' => 'User not found!']);
|
||||
}
|
||||
Notes::insert([
|
||||
[
|
||||
"n_user_id" => $user_id,
|
||||
"n_day" => $day,
|
||||
"n_month" => $month,
|
||||
"n_year" => $year,
|
||||
"n_time_type" => $time_type,
|
||||
"n_reason" => $reason,
|
||||
"n_note" => $note
|
||||
]
|
||||
]);
|
||||
|
||||
$this->createOrUpdateRecordForCurrentMonth($month, $year);
|
||||
|
||||
return response()->json(['status' => true, 'message' => 'Add successfully']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ Route::middleware('api')
|
|||
], function () {
|
||||
Route::get('/', [TimekeepingController::class, 'get'])->middleware('check.permission:admin.hr.staff');
|
||||
Route::post('/addMutilple', [TimekeepingController::class, 'addWorkingTimeForMultipleUser'])->middleware('check.permission:admin.hr');
|
||||
Route::post('/addNote', [TimekeepingController::class, 'addNoteForUser'])->middleware('check.permission:admin.hr');
|
||||
Route::post('/update-working-days', [TimekeepingController::class, 'saveWorkingDays'])->middleware('check.permission:admin.hr');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class Notes extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'n_user_id', 'n_day', 'n_month', 'n_year', 'n_time_type', 'n_reason', 'n_note',
|
||||
];
|
||||
|
||||
/**
|
||||
* Lấy thông tin bảng notes dựa vào tháng và năm.
|
||||
*
|
||||
* @param int $month
|
||||
* @param int $year
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public static function getNotesByMonthAndYear($month, $year)
|
||||
{
|
||||
return self::leftJoin("categories as reason", function ($join) {
|
||||
$join->on('n_reason', '=', 'reason.c_code');
|
||||
$join->on('reason.c_type', DB::raw("CONCAT('REASON')"));
|
||||
})
|
||||
->leftJoin("categories as timeTypes", function ($join) {
|
||||
$join->on('n_time_type', '=', 'timeTypes.c_code');
|
||||
$join->on('timeTypes.c_type', DB::raw("CONCAT('TIME_TYPE')"));
|
||||
})
|
||||
->where('n_month', $month)
|
||||
->where('n_year', $year)
|
||||
->select(
|
||||
'n_user_id',
|
||||
'n_day',
|
||||
'n_month',
|
||||
'n_year',
|
||||
'n_time_type',
|
||||
'n_reason',
|
||||
'n_note',
|
||||
'reason.c_name as reason_name',
|
||||
'timeTypes.c_name as time_type_name'
|
||||
)
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Traits;
|
||||
|
||||
use App\Helper\Cache\CurrentMonthTimekeeping;
|
||||
use App\Models\Notes;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
|
@ -32,44 +33,72 @@ trait AnalyzeData
|
|||
$endOfMonth = $now->endOfMonth()->endOfDay()->toDateTimeString();
|
||||
$admins = Admin::all();
|
||||
$history = DB::table('tracking')->select('*')->whereBetween('tracking.created_at', [$startOfMonth, $endOfMonth])->orderBy('tracking.created_at', 'asc')->get();
|
||||
$dataNotes = Notes::getNotesByMonthAndYear((int)$month, (int)$year);
|
||||
|
||||
$history = collect($history);
|
||||
$result = [];
|
||||
foreach ($admins as $key => $admin) {
|
||||
if ($key == 0) {
|
||||
$user_data = [];
|
||||
for ($i = 1; $i <= $daysInMonth; $i++) {
|
||||
// Tạo ngày cụ thể trong tháng
|
||||
$date = Carbon::create($now->year, $now->month, $i)->setTimezone(env('TIME_ZONE'))->format('Y-m-d');
|
||||
// Kiểm tra xem có mục nào trong $history có created_at trùng với $date
|
||||
$hasEntry = $history->filter(function ($entry) use ($date, $admin) {
|
||||
// echo($hasEntry);
|
||||
return Carbon::parse($entry->created_at)->setTimezone(env('TIME_ZONE'))->format('Y-m-d') === $date && $entry->user_id == $admin->id;
|
||||
});
|
||||
$user_data = [];
|
||||
for ($i = 1; $i <= $daysInMonth; $i++) {
|
||||
// Tạo ngày cụ thể trong tháng
|
||||
$date = Carbon::create($now->year, $now->month, $i)->setTimezone(env('TIME_ZONE'))->format('Y-m-d');
|
||||
// Kiểm tra xem có mục nào trong $history có created_at trùng với $date
|
||||
$hasEntry = $history->filter(function ($entry) use ($date, $admin) {
|
||||
// echo($hasEntry);
|
||||
// dd($date,$admin,$history,$daysInMonth);
|
||||
if (count($hasEntry) > 0) {
|
||||
$values = array_values($hasEntry->toArray());
|
||||
$last_checkin = null;
|
||||
$total = 0;
|
||||
foreach ($values as $value) {
|
||||
$createdAt = Carbon::parse($value->created_at)->setTimezone(env('TIME_ZONE'));
|
||||
if ($value->status == 'check out' && $last_checkin != null) {
|
||||
$lastCheckInTime = Carbon::parse($last_checkin)->setTimezone(env('TIME_ZONE'));
|
||||
// Tính thời gian làm việc bằng hiệu của thời gian check out và check in
|
||||
$workingTime = $createdAt->diffInSeconds($lastCheckInTime);
|
||||
$total += $workingTime;
|
||||
}
|
||||
|
||||
if ($value->status == 'check in') {
|
||||
$last_checkin = $createdAt;
|
||||
}
|
||||
return Carbon::parse($entry->created_at)->setTimezone(env('TIME_ZONE'))->format('Y-m-d') === $date && $entry->user_id == $admin->id;
|
||||
});
|
||||
$hasNotes = $dataNotes->filter(function ($entry) use ($i, $admin) {
|
||||
return $entry->n_user_id == $admin->id && $entry->n_day == $i;
|
||||
});
|
||||
// dd($date,$admin,$history,$daysInMonth);
|
||||
if (count($hasEntry) > 0) {
|
||||
$values = array_values($hasEntry->toArray());
|
||||
$last_checkin = null;
|
||||
$total = 0;
|
||||
foreach ($values as $value) {
|
||||
$createdAt = Carbon::parse($value->created_at)->setTimezone(env('TIME_ZONE'));
|
||||
if ($value->status == 'check out' && $last_checkin != null) {
|
||||
$lastCheckInTime = Carbon::parse($last_checkin)->setTimezone(env('TIME_ZONE'));
|
||||
// Tính thời gian làm việc bằng hiệu của thời gian check out và check in
|
||||
$workingTime = $createdAt->diffInSeconds($lastCheckInTime);
|
||||
$total += $workingTime;
|
||||
}
|
||||
$user_data[] = ['values' => array_values($hasEntry->toArray()), 'total' => $total, 'day' => $i];
|
||||
|
||||
if ($value->status == 'check in') {
|
||||
$last_checkin = $createdAt;
|
||||
}
|
||||
}
|
||||
$notes = [];
|
||||
if (count($hasNotes) > 0) {
|
||||
foreach ($hasNotes as $k_Note => $value_Note) {
|
||||
$notes[$k_Note] = [
|
||||
'timeType' => $value_Note->n_time_type,
|
||||
'timeTypeName' => $value_Note->time_type_name,
|
||||
'reason' => $value_Note->n_reason,
|
||||
'reasonName' => $value_Note->reason_name,
|
||||
'note' => $value_Note->n_note
|
||||
];
|
||||
}
|
||||
}
|
||||
$user_data[] = ['values' => array_values($hasEntry->toArray()), 'notes' => array_values($notes), 'total' => $total, 'day' => $i];
|
||||
} else {
|
||||
if (count($hasNotes) > 0) {
|
||||
$notes = [];
|
||||
foreach ($hasNotes as $k_Note => $value_Note) {
|
||||
$notes[$k_Note] = [
|
||||
'timeType' => $value_Note->n_time_type,
|
||||
'timeTypeName' => $value_Note->time_type_name,
|
||||
'reason' => $value_Note->n_reason,
|
||||
'reasonName' => $value_Note->reason_name,
|
||||
'note' => $value_Note->n_note
|
||||
];
|
||||
}
|
||||
$user_data[] = ['values' => [], 'notes' => array_values($notes), 'total' => 0, 'day' => $i];
|
||||
}
|
||||
}
|
||||
|
||||
$result[] = ['user' => $admin, 'history' => $user_data];
|
||||
}
|
||||
|
||||
$result[] = ['user' => $admin, 'history' => $user_data];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
<?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('notes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('n_user_id');
|
||||
$table->integer('n_day');
|
||||
$table->integer('n_month');
|
||||
$table->integer('n_year');
|
||||
$table->string('n_time_type');
|
||||
$table->string('n_reason');
|
||||
$table->text('n_note');
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('notes');
|
||||
}
|
||||
};
|
||||
|
|
@ -67,4 +67,8 @@ export const getAllUserWorklogs = API_URL + 'v1/admin/jira/worklogs'
|
|||
//Timekeeping
|
||||
export const getTheTimesheet = API_URL + 'v1/admin/timekeeping'
|
||||
export const updateMultipleUserWorkingTime = API_URL + 'v1/admin/timekeeping/addMutilple'
|
||||
export const updateWorkingDays = API_URL + 'v1/admin/timekeeping/update-working-days'
|
||||
export const updateNote = API_URL + 'v1/admin/timekeeping/addNote'
|
||||
export const updateWorkingDays = API_URL + 'v1/admin/timekeeping/update-working-days'
|
||||
|
||||
//Category
|
||||
export const getListMaster = API_URL + 'v1/admin/category/get-list-master'
|
||||
|
|
@ -55,3 +55,8 @@
|
|||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.historyRow td {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import {
|
||||
getListMaster,
|
||||
getTheTimesheet,
|
||||
updateMultipleUserWorkingTime,
|
||||
updateNote,
|
||||
updateWorkingDays,
|
||||
} from '@/api/Admin'
|
||||
import { update } from '@/rtk/helpers/CRUD'
|
||||
|
|
@ -15,6 +17,7 @@ import {
|
|||
Select,
|
||||
Table,
|
||||
Text,
|
||||
Textarea,
|
||||
TextInput,
|
||||
Tooltip,
|
||||
} from '@mantine/core'
|
||||
|
|
@ -39,6 +42,18 @@ interface User {
|
|||
updated_at: string | null
|
||||
}
|
||||
|
||||
interface DataReason {
|
||||
id: number
|
||||
c_code: string
|
||||
c_name: string
|
||||
}
|
||||
|
||||
interface DataTimeType {
|
||||
id: number
|
||||
c_code: string
|
||||
c_name: string
|
||||
}
|
||||
|
||||
interface HistoryValue {
|
||||
id: number
|
||||
name: string
|
||||
|
|
@ -50,10 +65,19 @@ interface HistoryValue {
|
|||
updated_at: string
|
||||
}
|
||||
|
||||
interface NoteValue {
|
||||
note: string
|
||||
reason: string
|
||||
timeType: string
|
||||
reasonName: string
|
||||
timeTypeName: string
|
||||
}
|
||||
|
||||
interface History {
|
||||
values: HistoryValue[]
|
||||
total: number
|
||||
day: number
|
||||
notes: NoteValue[]
|
||||
}
|
||||
|
||||
interface UserData {
|
||||
|
|
@ -62,7 +86,8 @@ interface UserData {
|
|||
}
|
||||
|
||||
const Timekeeping = () => {
|
||||
const [opened, { open, close }] = useDisclosure(false)
|
||||
const [opened1, { open: open1, close: close1 }] = useDisclosure(false)
|
||||
const [opened2, { open: open2, close: close2 }] = useDisclosure(false)
|
||||
const [disableBtn, setDisableBtn] = useState(false)
|
||||
const [daysInMonth, setDaysInMonth] = useState(
|
||||
Array.from({ length: 31 }, (_, index) => index + 1),
|
||||
|
|
@ -76,31 +101,67 @@ const Timekeeping = () => {
|
|||
type: '',
|
||||
day: 0,
|
||||
})
|
||||
const [workingDays, setWorkingDays] = useState(30)
|
||||
const [isOpen, setIsOpen] = useState('')
|
||||
const popoverRef = useRef<HTMLElement>(null)
|
||||
|
||||
const [selectedOption1, setSelectedOption1] = useState('')
|
||||
const [selectedOption2, setSelectedOption2] = useState('')
|
||||
const [textValue, setTextValue] = useState('')
|
||||
|
||||
const handleSave = () => {
|
||||
// Send data to API
|
||||
const data = {
|
||||
option1: selectedOption1,
|
||||
option2: selectedOption2,
|
||||
text: textValue,
|
||||
const [customAddNotes, setCustomAddNotes] = useState<{
|
||||
user: {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
console.log(data) // Replace with actual API call
|
||||
setIsOpen(false)
|
||||
}
|
||||
type: string
|
||||
reason: string
|
||||
note: string
|
||||
day: number
|
||||
}>({
|
||||
user: {
|
||||
id: 0,
|
||||
name: '',
|
||||
},
|
||||
type: '',
|
||||
reason: '',
|
||||
note: '',
|
||||
day: 0,
|
||||
})
|
||||
|
||||
const [workingDays, setWorkingDays] = useState(30)
|
||||
const [data, setData] = useState<UserData[]>([])
|
||||
const [date, setDate] = useState({
|
||||
month: (new Date().getMonth() + 1).toString(),
|
||||
year: new Date().getFullYear().toString(),
|
||||
})
|
||||
|
||||
const [dataTimeType, setDataTimeType] = useState<DataTimeType[]>([])
|
||||
const [dataReason, setDataReason] = useState<DataReason[]>([])
|
||||
|
||||
const getListMasterByType = async (type: string) => {
|
||||
try {
|
||||
const params = {
|
||||
type: type,
|
||||
}
|
||||
const res = await get(getListMaster, params)
|
||||
if (res.status) {
|
||||
return res.data
|
||||
}
|
||||
} catch (error: any) {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: error.message ?? error,
|
||||
color: 'red',
|
||||
})
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const resultTimeType = await getListMasterByType('TIME_TYPE')
|
||||
setDataTimeType(resultTimeType)
|
||||
|
||||
const resultReason = await getListMasterByType('REASON')
|
||||
setDataReason(resultReason)
|
||||
}
|
||||
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
const getTimeSheet = async () => {
|
||||
try {
|
||||
const res = await get(getTheTimesheet, {
|
||||
|
|
@ -173,6 +234,37 @@ const Timekeeping = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const updateInfoNote = async (
|
||||
users: {
|
||||
id: number
|
||||
name: string
|
||||
},
|
||||
day: number,
|
||||
type: string,
|
||||
reason: string,
|
||||
note: string,
|
||||
) => {
|
||||
try {
|
||||
await update(
|
||||
updateNote,
|
||||
{
|
||||
month: date.month,
|
||||
year: date.year,
|
||||
users: users,
|
||||
type: type,
|
||||
reason: reason,
|
||||
note: note,
|
||||
day: day,
|
||||
},
|
||||
getTimeSheet,
|
||||
)
|
||||
|
||||
setDisableBtn(false)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateWorkingDays = async () => {
|
||||
try {
|
||||
await update(
|
||||
|
|
@ -193,49 +285,87 @@ const Timekeeping = () => {
|
|||
getTimeSheet()
|
||||
}, [date])
|
||||
|
||||
// useEffect(() => {
|
||||
// const handleClickOutside = (event: MouseEvent | React.MouseEvent) => {
|
||||
// if (
|
||||
// popoverRef.current &&
|
||||
// !(popoverRef.current as HTMLElement).contains(event.target as Node)
|
||||
// ) {
|
||||
// setIsOpen('')
|
||||
// }
|
||||
// }
|
||||
const showTooltipNote = (user: UserData, d: number) => {
|
||||
return user.history
|
||||
.find((h) => h.day === d && h.notes && h.notes.length > 0)
|
||||
?.notes.map((v, index) => {
|
||||
return (
|
||||
<p key={index}>
|
||||
- {v.reasonName} ({v.timeTypeName}): {v.note}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
}
|
||||
const showTooltipAllNote = (user: UserData) => {
|
||||
return (
|
||||
<table style={{ borderCollapse: 'collapse', width: '100%' }}>
|
||||
<tbody>
|
||||
{user.history
|
||||
.filter((h) => h.notes && h.notes.length > 0)
|
||||
.map((h, index) => (
|
||||
<tr key={index} className={classes.historyRow}>
|
||||
<td
|
||||
style={{
|
||||
border: '0',
|
||||
verticalAlign: 'top',
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
Day {h.day}:
|
||||
</td>
|
||||
<td style={{ border: '0', paddingLeft: '10px' }}>
|
||||
{h.notes.map((v, noteIndex) => (
|
||||
<p key={noteIndex} style={{ margin: '0' }}>
|
||||
- {v.reasonName} ({v.timeTypeName}): {v.note}
|
||||
</p>
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
// document.addEventListener('click', handleClickOutside)
|
||||
|
||||
// return () => {
|
||||
// document.removeEventListener('click', handleClickOutside)
|
||||
// }
|
||||
// }, [popoverRef])
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: React.MouseEvent) => {
|
||||
if (
|
||||
popoverRef.current &&
|
||||
!(popoverRef.current as HTMLElement).contains(event.target as Node)
|
||||
) {
|
||||
setIsOpen('')
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
document.addEventListener('scroll', handleClickOutside)
|
||||
document
|
||||
.getElementById('my-child-div')
|
||||
?.addEventListener('scroll', handleClickOutside)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
document.removeEventListener('scroll', handleClickOutside)
|
||||
document
|
||||
.getElementById('my-child-div')
|
||||
?.removeEventListener('scroll', handleClickOutside)
|
||||
}
|
||||
}, [popoverRef])
|
||||
|
||||
// console.log(daysInMonth, 'daysInMonth')
|
||||
const showTooltip = (user: UserData, total: number, d: number) => {
|
||||
return (
|
||||
<div key={d}>
|
||||
{`Total: ${(total / 60 / 60).toFixed(1)}h`}
|
||||
{user.history
|
||||
.find((h) => h.day === d)
|
||||
?.values.map((v) => {
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
key={v.id}
|
||||
>
|
||||
<p>{v.status + ': ' + v.time_string}</p>{' '}
|
||||
{v.image && (
|
||||
<Image
|
||||
w={100}
|
||||
h={100}
|
||||
src={
|
||||
import.meta.env.VITE_BACKEND_URL.includes('local')
|
||||
? import.meta.env.VITE_BACKEND_URL +
|
||||
'storage/' +
|
||||
v.image
|
||||
: import.meta.env.VITE_BACKEND_URL +
|
||||
'image/storage/' +
|
||||
v.image
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
{showTooltipNote(user, d)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div className={classes.title}>
|
||||
|
|
@ -245,8 +375,8 @@ const Timekeeping = () => {
|
|||
</h3>
|
||||
</div>
|
||||
<Drawer
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
opened={opened1}
|
||||
onClose={close1}
|
||||
position="right"
|
||||
title={<strong>Add custom worklog</strong>}
|
||||
>
|
||||
|
|
@ -299,6 +429,85 @@ const Timekeeping = () => {
|
|||
Submit
|
||||
</Button>
|
||||
</Drawer>
|
||||
|
||||
{/* Form Save Note */}
|
||||
<Drawer
|
||||
opened={opened2}
|
||||
onClose={close2}
|
||||
position="right"
|
||||
title={<strong>Save Note</strong>}
|
||||
>
|
||||
<p>
|
||||
<span style={{ fontWeight: 'bold' }}>User</span>:{' '}
|
||||
{customAddNotes.user.name}
|
||||
<span style={{ paddingLeft: '10px', paddingRight: '10px' }}>|</span>
|
||||
<span style={{ fontWeight: 'bold' }}>Day</span>: {customAddNotes.day}
|
||||
</p>
|
||||
|
||||
<Select
|
||||
mb={'md'}
|
||||
searchable
|
||||
label="Reason"
|
||||
data={dataReason.map((user) => ({
|
||||
value: user.c_code.toString(),
|
||||
label: user.c_name,
|
||||
}))}
|
||||
onChange={(e) => {
|
||||
setCustomAddNotes({ ...customAddNotes, reason: e! })
|
||||
}}
|
||||
/>
|
||||
{customAddNotes.reason != '' && (
|
||||
<Select
|
||||
mb={'md'}
|
||||
label="Time Type"
|
||||
data={dataTimeType.map((item) => {
|
||||
return { value: item.c_code.toString(), label: item.c_name }
|
||||
})}
|
||||
onChange={(e) => {
|
||||
setCustomAddNotes({ ...customAddNotes, type: e! })
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{customAddNotes.type != '' && (
|
||||
<Textarea
|
||||
mb={'md'}
|
||||
label="Note"
|
||||
onChange={(e) => {
|
||||
setCustomAddNotes({ ...customAddNotes, note: e.target.value })
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDisableBtn(true)
|
||||
if (
|
||||
customAddNotes.type === '' ||
|
||||
customAddNotes.reason === '' ||
|
||||
customAddNotes.note === '' ||
|
||||
customAddNotes.day === 0 ||
|
||||
customAddNotes.user.id === 0
|
||||
) {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Input data required',
|
||||
color: 'red',
|
||||
})
|
||||
setDisableBtn(false)
|
||||
} else {
|
||||
updateInfoNote(
|
||||
customAddNotes.user,
|
||||
customAddNotes.day,
|
||||
customAddNotes.type,
|
||||
customAddNotes.reason,
|
||||
customAddNotes.note,
|
||||
)
|
||||
}
|
||||
}}
|
||||
disabled={disableBtn}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Drawer>
|
||||
<Box display={'flex'}>
|
||||
<Box style={{ display: 'flex', flexFlow: 'column' }} w={'30%'}>
|
||||
<Box w="100%" display={'flex'}>
|
||||
|
|
@ -493,7 +702,7 @@ const Timekeeping = () => {
|
|||
<Text
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
open()
|
||||
open1()
|
||||
setCustomAddData({ ...customAddData, day: d })
|
||||
}}
|
||||
>
|
||||
|
|
@ -539,22 +748,11 @@ const Timekeeping = () => {
|
|||
<Table.Tr key={user.user.id} className={classes.tableTr}>
|
||||
<Table.Td>
|
||||
<Tooltip
|
||||
// position={'auto'}
|
||||
multiline
|
||||
label={
|
||||
<div>
|
||||
<p style={{ fontWeight: 'bold' }}>Day 1:</p>
|
||||
<p style={{ paddingLeft: '10px' }}>
|
||||
- Work For Home (Buổi Sáng): Bị bể bánh xe
|
||||
</p>
|
||||
<p style={{ paddingLeft: '10px' }}>- Nghỉ phép (Buổi Chiều): Bị cảm</p>
|
||||
|
||||
<p style={{ fontWeight: 'bold' }}>Day 2:</p>
|
||||
<p style={{ paddingLeft: '10px' }}>
|
||||
- Work For Home (Buổi Sáng): Bị bể bánh xe
|
||||
</p>
|
||||
<p style={{ paddingLeft: '10px' }}>- Nghỉ phép (Buổi Chiều): Bị cảm</p>
|
||||
</div>
|
||||
}
|
||||
// opened
|
||||
// offset={{ mainAxis: 5, crossAxis: 0 }}
|
||||
label={showTooltipAllNote(user)}
|
||||
>
|
||||
<div>{user.user.name}</div>
|
||||
</Tooltip>
|
||||
|
|
@ -577,52 +775,13 @@ const Timekeeping = () => {
|
|||
}
|
||||
>
|
||||
{total / 60 / 60 < 7 &&
|
||||
user.history.find((h) => h.day === d) ? (
|
||||
user.history.find(
|
||||
(h) => h.day === d && h.values && h.values.length > 0,
|
||||
) ? (
|
||||
total / 60 / 60 >= 3.5 ? (
|
||||
<Tooltip
|
||||
multiline
|
||||
label={
|
||||
<div key={d}>
|
||||
{`Total: ${(total / 60 / 60).toFixed(1)}h`}
|
||||
{user.history
|
||||
.find((h) => h.day === d)
|
||||
?.values.map((v) => {
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
key={v.id}
|
||||
>
|
||||
<p>
|
||||
{v.status + ': ' + v.time_string}
|
||||
</p>{' '}
|
||||
{v.image && (
|
||||
<Image
|
||||
w={100}
|
||||
h={100}
|
||||
src={
|
||||
import.meta.env.VITE_BACKEND_URL.includes(
|
||||
'local',
|
||||
)
|
||||
? import.meta.env
|
||||
.VITE_BACKEND_URL +
|
||||
'storage/' +
|
||||
v.image
|
||||
: import.meta.env
|
||||
.VITE_BACKEND_URL +
|
||||
'image/storage/' +
|
||||
v.image
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
label={showTooltip(user, total, d)}
|
||||
>
|
||||
<IconCheck
|
||||
size={20}
|
||||
|
|
@ -633,53 +792,23 @@ const Timekeeping = () => {
|
|||
padding: '2px',
|
||||
fontWeight: 700,
|
||||
}}
|
||||
onClick={() => {
|
||||
open2()
|
||||
setCustomAddNotes({
|
||||
...customAddNotes,
|
||||
day: d,
|
||||
user: {
|
||||
id: user.user.id,
|
||||
name: user.user.name,
|
||||
},
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip
|
||||
multiline
|
||||
label={
|
||||
<div>
|
||||
{`Total: ${(total / 60 / 60).toFixed(1)}h`}
|
||||
{user.history
|
||||
.find((h) => h.day === d)
|
||||
?.values.map((v) => {
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
key={v.id}
|
||||
>
|
||||
<p>
|
||||
{v.status + ': ' + v.time_string}
|
||||
</p>{' '}
|
||||
{v.image && (
|
||||
<Image
|
||||
w={100}
|
||||
h={100}
|
||||
src={
|
||||
import.meta.env.VITE_BACKEND_URL.includes(
|
||||
'local',
|
||||
)
|
||||
? import.meta.env
|
||||
.VITE_BACKEND_URL +
|
||||
'storage/' +
|
||||
v.image
|
||||
: import.meta.env
|
||||
.VITE_BACKEND_URL +
|
||||
'image/storage/' +
|
||||
v.image
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
label={showTooltip(user, total, d)}
|
||||
>
|
||||
<IconExclamationMark
|
||||
size={20}
|
||||
|
|
@ -690,6 +819,17 @@ const Timekeeping = () => {
|
|||
padding: '2px',
|
||||
fontWeight: 700,
|
||||
}}
|
||||
onClick={() => {
|
||||
open2()
|
||||
setCustomAddNotes({
|
||||
...customAddNotes,
|
||||
day: d,
|
||||
user: {
|
||||
id: user.user.id,
|
||||
name: user.user.name,
|
||||
},
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
|
|
@ -697,48 +837,7 @@ const Timekeeping = () => {
|
|||
<>
|
||||
<Tooltip
|
||||
multiline
|
||||
label={
|
||||
<div>
|
||||
{`Total: ${(total / 60 / 60).toFixed(1)}h`}
|
||||
{user.history
|
||||
.find((h) => h.day === d)
|
||||
?.values.map((v) => {
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
key={v.id}
|
||||
>
|
||||
<p>
|
||||
{v.status + ': ' + v.time_string}
|
||||
</p>{' '}
|
||||
{v.image && (
|
||||
<Image
|
||||
w={100}
|
||||
h={100}
|
||||
src={
|
||||
import.meta.env.VITE_BACKEND_URL.includes(
|
||||
'local',
|
||||
)
|
||||
? import.meta.env
|
||||
.VITE_BACKEND_URL +
|
||||
'storage/' +
|
||||
v.image
|
||||
: import.meta.env
|
||||
.VITE_BACKEND_URL +
|
||||
'image/storage/' +
|
||||
v.image
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
label={showTooltip(user, total, d)}
|
||||
>
|
||||
<IconCheck
|
||||
size={20}
|
||||
|
|
@ -748,155 +847,53 @@ const Timekeeping = () => {
|
|||
borderRadius: '5px',
|
||||
padding: '2px',
|
||||
}}
|
||||
id={`indexBySN${user.user.id}_${d}`}
|
||||
onClick={() => {
|
||||
setIsOpen(
|
||||
isOpen == user.user.email + d
|
||||
? ''
|
||||
: user.user.email + d,
|
||||
)
|
||||
open2()
|
||||
setCustomAddNotes({
|
||||
...customAddNotes,
|
||||
day: d,
|
||||
user: {
|
||||
id: user.user.id,
|
||||
name: user.user.name,
|
||||
},
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Popover
|
||||
placement="bottom"
|
||||
isOpen={isOpen === user.user.email + d}
|
||||
target={`indexBySN${user.user.id}_${d}`}
|
||||
toggle={() =>
|
||||
setIsOpen(
|
||||
isOpen == user.user.email + d
|
||||
? ''
|
||||
: user.user.email + d,
|
||||
)
|
||||
}
|
||||
innerRef={popoverRef}
|
||||
>
|
||||
<PopoverBody
|
||||
style={{
|
||||
width: '300px',
|
||||
backgroundColor: 'white',
|
||||
boxShadow:
|
||||
'0 5px 15px rgba(30, 32, 37, 0.12)',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<label htmlFor="option1">
|
||||
Select Option 1:
|
||||
</label>
|
||||
<select
|
||||
id="option1"
|
||||
value={selectedOption1}
|
||||
onChange={(e) =>
|
||||
setSelectedOption1(e.target.value)
|
||||
}
|
||||
className="form-select"
|
||||
>
|
||||
<option value="">Select...</option>
|
||||
<option value="option1">Option 1</option>
|
||||
<option value="option2">Option 2</option>
|
||||
</select>
|
||||
</div>
|
||||
{selectedOption1 && (
|
||||
<div className="mt-2">
|
||||
<label htmlFor="option2">
|
||||
Select Option 2:
|
||||
</label>
|
||||
<select
|
||||
id="option2"
|
||||
value={selectedOption2}
|
||||
onChange={(e) =>
|
||||
setSelectedOption2(e.target.value)
|
||||
}
|
||||
className="form-select"
|
||||
>
|
||||
<option value="">Select...</option>
|
||||
<option value="optionA">
|
||||
Option A
|
||||
</option>
|
||||
<option value="optionB">
|
||||
Option B
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{selectedOption2 && (
|
||||
<div className="mt-2">
|
||||
<label htmlFor="textInput">
|
||||
Enter Text:
|
||||
</label>
|
||||
<textarea
|
||||
id="textInput"
|
||||
value={textValue}
|
||||
onChange={(e) =>
|
||||
setTextValue(e.target.value)
|
||||
}
|
||||
className="form-control"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MultiSelect
|
||||
mb={'md'}
|
||||
searchable
|
||||
label="User(s)"
|
||||
data={data.map((user) => {
|
||||
return {
|
||||
value: user.user.id.toString(),
|
||||
label: user.user.name,
|
||||
}
|
||||
})}
|
||||
onChange={(e) => {
|
||||
setCustomAddData({
|
||||
...customAddData,
|
||||
data: e,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
mb={'md'}
|
||||
label="Type"
|
||||
data={[
|
||||
{ value: 'half', label: 'Half day' },
|
||||
{ value: 'one', label: 'A day' },
|
||||
]}
|
||||
onChange={(e) => {
|
||||
setCustomAddData({
|
||||
...customAddData,
|
||||
type: e!,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.popoverFooter}>
|
||||
<Button
|
||||
variant="filled"
|
||||
color="red"
|
||||
onClick={() => setIsOpen('')}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button variant="filled" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverBody>
|
||||
</Popover>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Tooltip
|
||||
multiline
|
||||
label={
|
||||
<div key={d}>
|
||||
<p>
|
||||
- Work For Home (Buổi Sáng): Bị bể bánh xe
|
||||
</p>
|
||||
<p>- Nghỉ phép (Buổi Chiều): Bị cảm</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{user.history.find(
|
||||
(h) =>
|
||||
h.day === d && h.notes && h.notes.length > 0,
|
||||
) ? (
|
||||
<Tooltip
|
||||
multiline
|
||||
label={showTooltipNote(user, d)}
|
||||
>
|
||||
<IconX
|
||||
size={20}
|
||||
style={{
|
||||
backgroundColor: '#ff4646',
|
||||
color: 'white',
|
||||
borderRadius: '5px',
|
||||
padding: '2px',
|
||||
}}
|
||||
id={`indexBySN${user.user.id}_${d}`}
|
||||
onClick={() => {
|
||||
open2()
|
||||
setCustomAddNotes({
|
||||
...customAddNotes,
|
||||
day: d,
|
||||
user: {
|
||||
id: user.user.id,
|
||||
name: user.user.name,
|
||||
},
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<IconX
|
||||
size={20}
|
||||
style={{
|
||||
|
|
@ -907,140 +904,18 @@ const Timekeeping = () => {
|
|||
}}
|
||||
id={`indexBySN${user.user.id}_${d}`}
|
||||
onClick={() => {
|
||||
setIsOpen(
|
||||
isOpen == user.user.email + d
|
||||
? ''
|
||||
: user.user.email + d,
|
||||
)
|
||||
open2()
|
||||
setCustomAddNotes({
|
||||
...customAddNotes,
|
||||
day: d,
|
||||
user: {
|
||||
id: user.user.id,
|
||||
name: user.user.name,
|
||||
},
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Popover
|
||||
placement="bottom"
|
||||
isOpen={isOpen === user.user.email + d}
|
||||
target={`indexBySN${user.user.id}_${d}`}
|
||||
toggle={() =>
|
||||
setIsOpen(
|
||||
isOpen == user.user.email + d
|
||||
? ''
|
||||
: user.user.email + d,
|
||||
)
|
||||
}
|
||||
innerRef={popoverRef}
|
||||
>
|
||||
<PopoverBody
|
||||
style={{
|
||||
width: '300px',
|
||||
backgroundColor: 'white',
|
||||
boxShadow:
|
||||
'0 5px 15px rgba(30, 32, 37, 0.12)',
|
||||
padding: '20px',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<label htmlFor="option1">
|
||||
Select Option 1:
|
||||
</label>
|
||||
<select
|
||||
id="option1"
|
||||
value={selectedOption1}
|
||||
onChange={(e) =>
|
||||
setSelectedOption1(e.target.value)
|
||||
}
|
||||
className="form-select"
|
||||
>
|
||||
<option value="">Select...</option>
|
||||
<option value="option1">Option 1</option>
|
||||
<option value="option2">Option 2</option>
|
||||
</select>
|
||||
</div>
|
||||
{selectedOption1 && (
|
||||
<div className="mt-2">
|
||||
<label htmlFor="option2">
|
||||
Select Option 2:
|
||||
</label>
|
||||
<select
|
||||
id="option2"
|
||||
value={selectedOption2}
|
||||
onChange={(e) =>
|
||||
setSelectedOption2(e.target.value)
|
||||
}
|
||||
className="form-select"
|
||||
>
|
||||
<option value="">Select...</option>
|
||||
<option value="optionA">
|
||||
Option A
|
||||
</option>
|
||||
<option value="optionB">
|
||||
Option B
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{selectedOption2 && (
|
||||
<div className="mt-2">
|
||||
<label htmlFor="textInput">
|
||||
Enter Text:
|
||||
</label>
|
||||
<textarea
|
||||
id="textInput"
|
||||
value={textValue}
|
||||
onChange={(e) =>
|
||||
setTextValue(e.target.value)
|
||||
}
|
||||
className="form-control"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MultiSelect
|
||||
mb={'md'}
|
||||
searchable
|
||||
label="User(s)"
|
||||
data={data.map((user) => {
|
||||
return {
|
||||
value: user.user.id.toString(),
|
||||
label: user.user.name,
|
||||
}
|
||||
})}
|
||||
onChange={(e) => {
|
||||
setCustomAddData({
|
||||
...customAddData,
|
||||
data: e,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
mb={'md'}
|
||||
label="Type"
|
||||
data={[
|
||||
{ value: 'half', label: 'Half day' },
|
||||
{ value: 'one', label: 'A day' },
|
||||
]}
|
||||
onChange={(e) => {
|
||||
setCustomAddData({
|
||||
...customAddData,
|
||||
type: e!,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.popoverFooter}>
|
||||
<Button
|
||||
variant="filled"
|
||||
color="red"
|
||||
onClick={() => setIsOpen('')}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button variant="filled" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverBody>
|
||||
</Popover>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Table.Td>
|
||||
|
|
|
|||
Loading…
Reference in New Issue