View note theo ngày và note tổng chức năng timekepping

This commit is contained in:
Truong Vo 2024-08-05 08:38:39 +07:00
parent 9a5e81111a
commit b203e8d82c
11 changed files with 4728 additions and 5250 deletions

View File

@ -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
);
}
}

View File

@ -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);
}
}

View File

@ -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']);

View File

@ -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;
}

View File

@ -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']);
});

View File

@ -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;
}

View File

@ -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

View File

@ -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"

View File

@ -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;
}

View File

@ -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>
)