View note theo ngày và note tổng chức năng timekepping
This commit is contained in:
parent
9a5e81111a
commit
b203e8d82c
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace Modules\Admin\app\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class AbstractController extends Controller
|
||||
{
|
||||
public static function ResultData($models)
|
||||
{
|
||||
return $models->withPath('');
|
||||
}
|
||||
|
||||
public static function ResultSuccess($data = [], $message = "Successfull")
|
||||
{
|
||||
return response()->json(
|
||||
[
|
||||
'message' => $message,
|
||||
'errors' => false,
|
||||
'result' => "Success",
|
||||
'status' => true,
|
||||
'data' => $data,
|
||||
],
|
||||
200
|
||||
);
|
||||
}
|
||||
public static function ResultError($message, $data = [], $statusCode = 500)
|
||||
{
|
||||
return response()->json(
|
||||
[
|
||||
'message' => $message,
|
||||
'errors' => true,
|
||||
'status' => false,
|
||||
'result' => 'ERROR',
|
||||
'data' => $data,
|
||||
],
|
||||
$statusCode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace Modules\Admin\app\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\Admin\app\Models\Category;
|
||||
|
||||
class CategoryController extends Controller
|
||||
{
|
||||
/**
|
||||
* Retrieves a list of master data based on the given request type.
|
||||
*
|
||||
* @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)
|
||||
{
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,8 @@ 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']);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace Modules\Admin\app\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Category extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ use App\Http\Middleware\CheckPermission;
|
|||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Admin\app\Http\Controllers\AdminController;
|
||||
use Modules\Admin\app\Http\Controllers\BannerController;
|
||||
use Modules\Admin\app\Http\Controllers\CategoryController;
|
||||
use Modules\Admin\app\Http\Controllers\ClientController;
|
||||
use Modules\Admin\app\Http\Controllers\CountryController;
|
||||
use Modules\Admin\app\Http\Controllers\CustomThemeController;
|
||||
|
|
@ -116,21 +117,28 @@ 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::group([
|
||||
'prefix' => 'v1/admin/tracking',
|
||||
});
|
||||
|
||||
Route::group([
|
||||
'prefix' => 'category',
|
||||
], function () {
|
||||
Route::get('/get-list-master', [CategoryController::class, 'getListMaster']);
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Route::group([
|
||||
'prefix' => 'v1/admin/tracking',
|
||||
], function () {
|
||||
Route::get('/', [TrackingController::class, 'get'])->middleware('check.permission:admin.hr.staff');
|
||||
Route::post('/scan-create', [TrackingController::class, 'create']);
|
||||
Route::post('/send-image', [TrackingController::class, 'saveImage']);
|
||||
// Route::get('/clear-cache', [SettingController::class, 'clearCache']);
|
||||
});
|
||||
|
||||
Route::group([
|
||||
'prefix' => 'v1/admin/jira',
|
||||
], function () {
|
||||
});
|
||||
|
||||
Route::group([
|
||||
'prefix' => 'v1/admin/jira',
|
||||
], function () {
|
||||
Route::get('/send-worklog-report', [JiraController::class, 'sendReport']);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,40 +34,42 @@ trait AnalyzeData
|
|||
$history = DB::table('tracking')->select('*')->whereBetween('tracking.created_at', [$startOfMonth, $endOfMonth])->orderBy('tracking.created_at', 'asc')->get();
|
||||
$history = collect($history);
|
||||
$result = [];
|
||||
foreach ($admins as $admin) {
|
||||
$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) {
|
||||
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;
|
||||
});
|
||||
// echo($hasEntry);
|
||||
return Carbon::parse($entry->created_at)->setTimezone(env('TIME_ZONE'))->format('Y-m-d') === $date && $entry->user_id == $admin->id;
|
||||
});
|
||||
// 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 (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;
|
||||
if ($value->status == 'check in') {
|
||||
$last_checkin = $createdAt;
|
||||
}
|
||||
}
|
||||
$user_data[] = ['values' => array_values($hasEntry->toArray()), 'total' => $total, 'day' => $i];
|
||||
}
|
||||
$user_data[] = ['values' => array_values($hasEntry->toArray()), 'total' => $total, 'day' => $i];
|
||||
}
|
||||
}
|
||||
|
||||
$result[] = ['user' => $admin, 'history' => $user_data];
|
||||
$result[] = ['user' => $admin, 'history' => $user_data];
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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('categories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('c_code');
|
||||
$table->string('c_name');
|
||||
$table->string('c_type');
|
||||
$table->string('c_value');
|
||||
$table->integer('c_active');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('categories');
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -25,17 +25,18 @@
|
|||
"@uiw/react-codemirror": "^4.21.21",
|
||||
"@vanilla-extract/css": "^1.13.0",
|
||||
"axios": "^1.6.1",
|
||||
"clsx": "^2.0.0",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.10",
|
||||
"dotenv": "^16.3.1",
|
||||
"handsontable": "^14.0.0",
|
||||
"history": "^5.3.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"moment": "^2.30.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "^8.1.3",
|
||||
"react-router-dom": "^6.19.0",
|
||||
"reactstrap": "^9.2.2",
|
||||
"recharts": "^2.11.0",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tests": "^0.4.2"
|
||||
|
|
|
|||
|
|
@ -1,51 +1,57 @@
|
|||
.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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
.optionIcon {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.deleteIcon {
|
||||
color: red;
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
border-radius: 25%;
|
||||
}
|
||||
.deleteIcon {
|
||||
color: red;
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
border-radius: 25%;
|
||||
}
|
||||
|
||||
.deleteIcon:hover {
|
||||
background-color: rgba(203, 203, 203, 0.809);
|
||||
}
|
||||
.deleteIcon:hover {
|
||||
background-color: rgba(203, 203, 203, 0.809);
|
||||
}
|
||||
|
||||
.editIcon {
|
||||
color: rgb(9, 132, 132);
|
||||
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);
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background-color: light-dark(white, #2d353c);
|
||||
text-align: center;
|
||||
border: solid 1px rgb(255, 145, 0);
|
||||
}
|
||||
|
||||
.dialogText {
|
||||
color: light-dark(#2d353c, white);
|
||||
}
|
||||
.editIcon:hover {
|
||||
background-color: rgba(203, 203, 203, 0.809);
|
||||
}
|
||||
|
||||
.tableTr:hover {
|
||||
background-color: rgb(205, 255, 255);
|
||||
}
|
||||
.dialog {
|
||||
background-color: light-dark(white, #2d353c);
|
||||
text-align: center;
|
||||
border: solid 1px rgb(255, 145, 0);
|
||||
}
|
||||
|
||||
.dialogText {
|
||||
color: light-dark(#2d353c, white);
|
||||
}
|
||||
|
||||
.tableTr:hover {
|
||||
background-color: rgb(205, 255, 255);
|
||||
}
|
||||
|
||||
.popoverFooter {
|
||||
margin-top: 5px;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,13 +16,16 @@ import {
|
|||
Table,
|
||||
Text,
|
||||
TextInput,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
} from '@mantine/core'
|
||||
import { useDisclosure } from '@mantine/hooks'
|
||||
import { notifications } from '@mantine/notifications'
|
||||
import { IconCheck, IconExclamationMark, IconX } from '@tabler/icons-react'
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Popover, PopoverBody } from 'reactstrap'
|
||||
|
||||
import classes from './Timekeeping.module.css'
|
||||
|
||||
interface User {
|
||||
|
|
@ -64,12 +67,34 @@ const Timekeeping = () => {
|
|||
const [daysInMonth, setDaysInMonth] = useState(
|
||||
Array.from({ length: 31 }, (_, index) => index + 1),
|
||||
)
|
||||
const [customAddData, setCustomAddData] = useState<{data:string[], type: string, day: number}>({
|
||||
const [customAddData, setCustomAddData] = useState<{
|
||||
data: string[]
|
||||
type: string
|
||||
day: number
|
||||
}>({
|
||||
data: [],
|
||||
type: '',
|
||||
day: 0
|
||||
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,
|
||||
}
|
||||
console.log(data) // Replace with actual API call
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
const [data, setData] = useState<UserData[]>([])
|
||||
const [date, setDate] = useState({
|
||||
month: (new Date().getMonth() + 1).toString(),
|
||||
|
|
@ -168,7 +193,49 @@ const Timekeeping = () => {
|
|||
getTimeSheet()
|
||||
}, [date])
|
||||
|
||||
console.log(customAddData)
|
||||
// useEffect(() => {
|
||||
// const handleClickOutside = (event: MouseEvent | React.MouseEvent) => {
|
||||
// if (
|
||||
// popoverRef.current &&
|
||||
// !(popoverRef.current as HTMLElement).contains(event.target as Node)
|
||||
// ) {
|
||||
// setIsOpen('')
|
||||
// }
|
||||
// }
|
||||
|
||||
// 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')
|
||||
return (
|
||||
<div>
|
||||
<div className={classes.title}>
|
||||
|
|
@ -190,7 +257,9 @@ const Timekeeping = () => {
|
|||
data={data.map((user) => {
|
||||
return { value: user.user.id.toString(), label: user.user.name }
|
||||
})}
|
||||
onChange={(e)=>{setCustomAddData({...customAddData, data: e})}}
|
||||
onChange={(e) => {
|
||||
setCustomAddData({ ...customAddData, data: e })
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
mb={'md'}
|
||||
|
|
@ -199,21 +268,36 @@ const Timekeeping = () => {
|
|||
{ value: 'half', label: 'Half day' },
|
||||
{ value: 'one', label: 'A day' },
|
||||
]}
|
||||
onChange={(e)=>{setCustomAddData({...customAddData, type: e!})}}
|
||||
onChange={(e) => {
|
||||
setCustomAddData({ ...customAddData, type: e! })
|
||||
}}
|
||||
/>
|
||||
<Button onClick={()=>{
|
||||
setDisableBtn(true)
|
||||
if(customAddData.type === "" || customAddData.data.length === 0 || customAddData.day === 0){
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: "Input data required",
|
||||
color: 'red',
|
||||
})
|
||||
setDisableBtn(false)
|
||||
}else{
|
||||
updateMultipleUser(customAddData.data.map((u)=>parseInt(u)), customAddData.day, customAddData.type)
|
||||
}
|
||||
}} disabled={disableBtn}>Submit</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDisableBtn(true)
|
||||
if (
|
||||
customAddData.type === '' ||
|
||||
customAddData.data.length === 0 ||
|
||||
customAddData.day === 0
|
||||
) {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Input data required',
|
||||
color: 'red',
|
||||
})
|
||||
setDisableBtn(false)
|
||||
} else {
|
||||
updateMultipleUser(
|
||||
customAddData.data.map((u) => parseInt(u)),
|
||||
customAddData.day,
|
||||
customAddData.type,
|
||||
)
|
||||
}
|
||||
}}
|
||||
disabled={disableBtn}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Drawer>
|
||||
<Box display={'flex'}>
|
||||
<Box style={{ display: 'flex', flexFlow: 'column' }} w={'30%'}>
|
||||
|
|
@ -371,7 +455,7 @@ const Timekeeping = () => {
|
|||
<Table.Th></Table.Th>
|
||||
{daysInMonth.map((d) => {
|
||||
return (
|
||||
<Menu width={200} shadow="md">
|
||||
<Menu width={200} shadow="md" key={d}>
|
||||
<Menu.Target>
|
||||
<Table.Th
|
||||
key={d}
|
||||
|
|
@ -406,10 +490,13 @@ const Timekeeping = () => {
|
|||
+ Add 1 day of work
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Text size="sm" onClick={()=>{
|
||||
open()
|
||||
setCustomAddData({...customAddData, day: d})
|
||||
}}>
|
||||
<Text
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
open()
|
||||
setCustomAddData({ ...customAddData, day: d })
|
||||
}}
|
||||
>
|
||||
+ Add custom worklog
|
||||
</Text>
|
||||
</Menu.Item>
|
||||
|
|
@ -450,7 +537,28 @@ const Timekeeping = () => {
|
|||
2
|
||||
return (
|
||||
<Table.Tr key={user.user.id} className={classes.tableTr}>
|
||||
<Table.Td>{user.user.name}</Table.Td>
|
||||
<Table.Td>
|
||||
<Tooltip
|
||||
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>
|
||||
}
|
||||
>
|
||||
<div>{user.user.name}</div>
|
||||
</Tooltip>
|
||||
</Table.Td>
|
||||
<Table.Td ta={'center'}>{totalDays}</Table.Td>
|
||||
<Table.Td ta={'center'}>{workingDays - totalDays}</Table.Td>
|
||||
{daysInMonth.map((d) => {
|
||||
|
|
@ -586,69 +694,354 @@ const Timekeeping = () => {
|
|||
</Tooltip>
|
||||
)
|
||||
) : total >= 7 ? (
|
||||
<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}
|
||||
<>
|
||||
<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>
|
||||
}
|
||||
>
|
||||
<IconCheck
|
||||
size={20}
|
||||
style={{
|
||||
backgroundColor: 'green',
|
||||
color: 'white',
|
||||
borderRadius: '5px',
|
||||
padding: '2px',
|
||||
}}
|
||||
id={`indexBySN${user.user.id}_${d}`}
|
||||
onClick={() => {
|
||||
setIsOpen(
|
||||
isOpen == user.user.email + d
|
||||
? ''
|
||||
: user.user.email + d,
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</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"
|
||||
>
|
||||
<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>
|
||||
}
|
||||
>
|
||||
<IconCheck
|
||||
size={20}
|
||||
style={{
|
||||
backgroundColor: 'green',
|
||||
color: 'white',
|
||||
borderRadius: '5px',
|
||||
padding: '2px',
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<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>
|
||||
</>
|
||||
) : (
|
||||
<IconX
|
||||
size={20}
|
||||
style={{
|
||||
backgroundColor: '#ff4646',
|
||||
color: 'white',
|
||||
borderRadius: '5px',
|
||||
padding: '2px',
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<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>
|
||||
}
|
||||
>
|
||||
<IconX
|
||||
size={20}
|
||||
style={{
|
||||
backgroundColor: '#ff4646',
|
||||
color: 'white',
|
||||
borderRadius: '5px',
|
||||
padding: '2px',
|
||||
}}
|
||||
id={`indexBySN${user.user.id}_${d}`}
|
||||
onClick={() => {
|
||||
setIsOpen(
|
||||
isOpen == user.user.email + d
|
||||
? ''
|
||||
: user.user.email + d,
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</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