update timekeeping

This commit is contained in:
JOSEPH LE 2024-06-05 14:00:47 +07:00
parent 8cc13276f0
commit 54e5d8e780
11 changed files with 624 additions and 96 deletions

View File

@ -0,0 +1,74 @@
<?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 Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Modules\Admin\app\Models\Admin;
use Modules\Admin\app\Models\Tracking;
use function PHPSTORM_META\type;
class TimekeepingController extends Controller
{
use HasOrderByRequest;
use HasFilterRequest;
use HasSearchRequest;
public function get(Request $request)
{
// dd($request->date);
$now = Carbon::create($request->year, $request->month);
// $now = Carbon::create(2024, 5, 30); // Nếu muốn khởi tạo với một ngày cụ thể
$daysInMonth = $now->daysInMonth;
// return response()->json(['status'=> true, 'data'=>$request->all()]);
// Lấy ngày đầu tháng
$startOfMonth = $now->startOfMonth()->toDateString();
// Lấy ngày cuối tháng
$endOfMonth = $now->endOfMonth()->toDateString();
$admins = Admin::all();
$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)->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) {
return Carbon::parse($entry->created_at)->format('Y-m-d') === $date && $entry->user_id == $admin->id;
});
if (count($hasEntry) > 0) {
$values = array_values($hasEntry->toArray());
$last_checkin = null;
$total = 0;
foreach ($values as $value) {
$createdAt = Carbon::parse($value->created_at);
if ($value->status == 'check out' && $last_checkin != null) {
$lastCheckInTime = Carbon::parse($last_checkin);
// 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;
}
}
$user_data[] = ['values'=>array_values($hasEntry->toArray()), 'total' => $total, 'day' => $i];
}
}
$result[] = ['user' => $admin, 'history' => $user_data];
}
return response()->json(['status'=> true, 'data'=>$result]);
}
}

View File

@ -32,6 +32,7 @@ class TrackingController extends Controller
// Order by
$this->orderByRequest($tracking, $request);
$tracking->orderBy('created_at', 'desc');
// Filter
$this->filterRequest(
builder: $tracking,
@ -59,6 +60,8 @@ class TrackingController extends Controller
['status' => true]
);
$users = Admin::where('permission', '!=', 'admin')->get();
$responseData['users'] = $users;
return response()->json($responseData);
}
@ -72,6 +75,9 @@ class TrackingController extends Controller
]);
$payload = $request->only(['name', 'time_string', 'status']);
if($request->has('created_at')){
$payload['created_at'] = $request->created_at;
}
$user = Admin::where('name', $payload['name'])->first();
if ($user) {
$payload['user_id'] = $user->id;

View File

@ -4,17 +4,12 @@ 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\ClientController;
use Modules\Admin\app\Http\Controllers\ContactController;
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\DiscountController;
use Modules\Admin\app\Http\Controllers\DiscountTypeController;
use Modules\Admin\app\Http\Controllers\JiraController;
use Modules\Admin\app\Http\Controllers\OrderController;
use Modules\Admin\app\Http\Controllers\PackageController;
use Modules\Admin\app\Http\Controllers\SNCheckController;
use Modules\Admin\app\Http\Controllers\SettingController;
use Modules\Admin\app\Http\Controllers\TimekeepingController;
use Modules\Admin\app\Http\Controllers\TrackingController;
use Modules\Admin\app\Http\Middleware\AdminMiddleware;
@ -55,28 +50,6 @@ Route::middleware('api')
Route::get('/clear-cache', [SettingController::class, 'clearCache']);
});
Route::group([
'prefix' => 'package',
], function () {
Route::get('/all', [PackageController::class, 'all']);
Route::post('/create', [PackageController::class, 'create']);
Route::post('/update', [PackageController::class, 'update']);
Route::get('/delete', [PackageController::class, 'delete']);
Route::post('/updates', [PackageController::class, 'updates']);
Route::post('/deletes', [PackageController::class, 'deletes']);
});
Route::group([
'prefix' => 'discount',
], function () {
Route::get('/get', [DiscountController::class, 'get']);
Route::post('/create', [DiscountController::class, 'create']);
Route::post('/update', [DiscountController::class, 'update']);
Route::get('/delete', [DiscountController::class, 'delete']);
Route::post('/updates', [DiscountController::class, 'updates']);
Route::post('/deletes', [DiscountController::class, 'deletes']);
});
Route::group([
'prefix' => 'client',
], function () {
@ -99,25 +72,6 @@ Route::middleware('api')
Route::post('/deletes', [BannerController::class, 'deletes']);
});
Route::group([
'prefix' => 'order',
], function () {
Route::get('/get', [OrderController::class, 'get']);
Route::post('/update', [OrderController::class, 'update']);
});
Route::group([
'prefix' => 'sn-check-history',
], function () {
Route::get('/get', [SNCheckController::class, 'get']);
Route::get('/show-detail', [SNCheckController::class, 'showDetail']);
});
Route::group([
'prefix' => 'discount-type',
], function () {
Route::get('/all', [DiscountTypeController::class, 'all']);
});
Route::group([
'prefix' => 'custom-theme',
@ -130,11 +84,6 @@ Route::middleware('api')
], function () {
Route::get('/', [CountryController::class, 'all']);
});
Route::group([
'prefix' => 'contact',
], function () {
Route::get('/get', [ContactController::class, 'get']);
});
Route::group([
'prefix' => 'dashboard',
@ -146,13 +95,26 @@ Route::middleware('api')
Route::group([
'prefix' => 'jira',
], function () {
Route::get('/fetch-issues', [JiraController::class, 'fetchAllIssues']);
Route::get('/export-issues', [JiraController::class, 'exportToExcel']);
Route::get('/all-project', [JiraController::class, 'getAllProject']);
Route::get('/all-issue-by-project', [JiraController::class, 'fetchIssuesByProject']);
Route::get('/worklogs', [JiraController::class, 'getAllUserWorkLogs']);
});
Route::group([
'prefix' => 'timekeeping',
], function () {
Route::get('/', [TimekeepingController::class, 'get']);
});
Route::group([
'prefix' => 'tracking',
], function () {
Route::post('/create', [TrackingController::class, 'create']);
Route::post('/update', [TrackingController::class, 'update']);
Route::get('/delete', [TrackingController::class, 'delete']);
});
});
});
@ -161,7 +123,6 @@ Route::middleware('api')
], function () {
Route::get('/', [TrackingController::class, 'get']);
Route::post('/scan-create', [TrackingController::class, 'create']);
Route::get('/delete', [TrackingController::class, 'delete']);
// Route::get('/clear-cache', [SettingController::class, 'clearCache']);
});

View File

@ -53,11 +53,16 @@ export const statisticRevenuesByMonth = API_URL + 'v1/admin/dashboard/statistics
// Tracking
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'
// // Tracking
// Worklogs
export const fetchAllIssues = API_URL + 'v1/admin/jira/fetch-issues'
export const exportIssues = API_URL + 'v1/admin/jira/export-issues'
export const getAllProjects = API_URL + 'v1/admin/jira/all-project'
export const getAllIssuesByProject = API_URL + 'v1/admin/jira/all-issue-by-project'
export const getAllUserWorklogs = API_URL + 'v1/admin/jira/worklogs'
//Timekeeping
export const getTheTimesheet = API_URL + 'v1/admin/timekeeping'

View File

@ -19,6 +19,7 @@ import {
import { notifications } from '@mantine/notifications'
import {
IconBrush,
IconCalendar,
IconLayoutSidebarLeftExpand,
IconLayoutSidebarRightExpand,
IconLogout,
@ -38,6 +39,7 @@ import classes from './NavbarSimpleColored.module.css'
const data = [
// { link: '/dashboard', label: 'Dashboard', icon: IconHome },
{ link: '/timekeeping', label: 'Timekeeping', icon: IconCalendar },
{ link: '/tracking', label: 'Check in/out', icon: IconScan },
{ link: '/worklogs', label: 'Worklogs', icon: IconReport },
{ link: '/jira', label: 'Jira', icon: IconSubtask },

View File

@ -0,0 +1,51 @@
.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%;
}
.deleteIcon:hover {
background-color: rgba(203, 203, 203, 0.809);
}
.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);
}
.tableTr:hover {
background-color: rgb(205, 255, 255);
}

View File

@ -0,0 +1,239 @@
import { getTheTimesheet } from '@/api/Admin'
import { get } from '@/rtk/helpers/apiService'
import { Box, Select, Table, Text, Tooltip } from '@mantine/core'
import { IconCheck, IconX } from '@tabler/icons-react'
import { useEffect, useState } from 'react'
import classes from './Timekeeping.module.css'
interface User {
id: number
name: string
email: string
email_verified_at: string | null
permission: string
remember_token: string | null
created_at: string | null
updated_at: string | null
}
interface HistoryValue {
id: number
name: string
user_id: number
status: string
time_string: string
created_at: string
updated_at: string
}
interface History {
values: HistoryValue[]
total: number
day: number
}
interface UserData {
user: User
history: History[]
}
const Timekeeping = () => {
let permission = ['staff']
const [daysInMonth, setDaysInMonth] = useState(
Array.from({ length: 31 }, (_, index) => index + 1),
)
const [data, setData] = useState<UserData[]>([])
const [date, setDate] = useState({
month: (new Date().getMonth() + 1).toString(),
year: new Date().getFullYear().toString(),
})
const getTimeSheet = async () => {
try {
const res = await get(getTheTimesheet, {
month: date.month,
year: date.year,
})
if (res.status) {
setData(res.data.filter((u:UserData)=>permission.includes(u.user.permission)))
setDaysInMonth(
Array.from({ length: getDaysInMonth() }, (_, index) => index + 1),
)
}
} catch (error) {
console.log(error)
}
}
function getDayName(dateString: string) {
// Tạo đối tượng Date từ chuỗi ngày tháng năm
const date = new Date(dateString)
// Lấy ngày trong tuần (0-6)
const dayNumber = date.getDay()
// Mảng chứa các tên ngày bằng tiếng Việt
const daysInVietnamese = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
// Trả về tên ngày bằng tiếng Việt
return daysInVietnamese[dayNumber]
}
function getDaysInMonth() {
// Tạo một đối tượng Date cho ngày đầu tiên của tháng tiếp theo
// Chú ý: tháng trong đối tượng Date là từ 0 (tháng 1) đến 11 (tháng 12)
const days = new Date(parseInt(date.year), parseInt(date.month), 0)
// Trả về ngày, tức là số ngày trong tháng
return days.getDate()
}
useEffect(() => {
getTimeSheet()
}, [date])
return (
<div>
<div className={classes.title}>
<h3>
<Text>Admin/</Text>
Timekeeping
</h3>
</div>
<Box w="50%" display={'flex'}>
<Select
w="10%"
value={date.month}
size='xs'
label="Month"
data={Array.from({ length: 12 }, (_, index) =>
(1 + index).toString(),
)}
onChange={(e) => {
setDate({ ...date, month: e! })
}}
></Select>
<Select
w="10%"
value={date.year}
size='xs'
ml={'sm'}
label="Year"
data={Array.from({ length: 20 }, (_, index) =>
(2023 + 1 + index).toString(),
)}
onChange={(e) => {
setDate({ ...date, year: e! })
}}
></Select>
</Box>
<Box>
<Table striped highlightOnHover withTableBorder withColumnBorders mt={'md'}>
<Table.Thead>
<Table.Tr bg={'#228be66b'}>
<Table.Th>Day</Table.Th>
{daysInMonth.map((d) => {
return <Table.Th key={d} ta={'center'}>{d}</Table.Th>
})}
</Table.Tr>
<Table.Tr bg={'#228be66b'}>
<Table.Th></Table.Th>
{daysInMonth.map((d) => {
return (
<Table.Th key={d} ta={'center'}>
{getDayName(`${date.year}-${date.month}-${d}`)}
</Table.Th>
)
})}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{data.map((user) => {
return (
<Table.Tr
key={user.user.id}
className={classes.tableTr}
>
<Table.Td >{user.user.name}</Table.Td>
{daysInMonth.map((d) => {
var total =
user.history.find((h) => h.day === d)?.total ?? 0
return (
<Table.Td key={d} ta={'center'}>
{total / 60 / 60 < 8 && total !== 0 ? (
<Tooltip
multiline
label={
<div>
{`Total: ${(total / 60 / 60).toFixed(1)}h`}
{user.history
.find((h) => h.day === d)
?.values.map((v) => {
return (
<p>{v.status + ': ' + v.time_string}</p>
)
})}
</div>
}
>
<IconCheck
size={20}
style={{
backgroundColor: 'orange',
color: 'white',
borderRadius:"5px",
padding: "2px",
fontWeight: 700
}}
/>
</Tooltip>
) : total >= 8 ? (
<Tooltip
multiline
label={
<div>
{`Total: ${(total / 60 / 60).toFixed(1)}h`}
{user.history
.find((h) => h.day === d)
?.values.map((v) => {
return (
<p>{v.status + ': ' + v.time_string}</p>
)
})}
</div>
}
>
<IconCheck
size={20}
style={{
backgroundColor: 'green',
color: 'white',
borderRadius:"5px",
padding: "2px"
}}
/>
</Tooltip>
) : (
<IconX
size={20}
style={{
backgroundColor: '#ff4646',
color: 'white',
borderRadius:"5px",
padding: "2px"
}}
/>
)}
</Table.Td>
)
})}
</Table.Tr>
)
})}
</Table.Tbody>
</Table>
</Box>
</div>
)
}
export default Timekeeping

View File

@ -21,6 +21,18 @@
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);
}

View File

@ -1,19 +1,55 @@
import { deleteTracking, getListTracking } from "@/api/Admin"
import { DataTablePagination } from "@/components/DataTable/DataTable"
import { Xdelete } from "@/rtk/helpers/CRUD"
import { get } from "@/rtk/helpers/apiService"
import { Box, Button, Dialog, Group, Text } from "@mantine/core"
import { IconTrash } from "@tabler/icons-react"
import moment from "moment"
import { useEffect, useState } from "react"
import { addTracking, deleteTracking, getListTracking, updateTracking } from '@/api/Admin'
import { DataTablePagination } from '@/components/DataTable/DataTable'
import { Xdelete, create, update } from '@/rtk/helpers/CRUD'
import { get } from '@/rtk/helpers/apiService'
import {
Box,
Button,
Dialog,
Group,
Modal,
Select,
Text
} from '@mantine/core'
import { DateTimePicker } from '@mantine/dates'
import { useForm } from '@mantine/form'
import { IconEdit, IconTrash } from '@tabler/icons-react'
import moment from 'moment'
import { useEffect, useState } from 'react'
import classes from './Tracking.module.css'
type TLog = {
id: number
name: string
status: string
time_string: Date
}
type TListTracking = {
data: TLog[]
users: User[]
}
interface User {
id: number
name: string
email: string
email_verified_at: string | null
permission: string
remember_token: string | null
created_at: string | null
updated_at: string | null
}
const Tracking = () => {
const [listTracking, setListTracking] = useState({
data: []
const [listTracking, setListTracking] = useState<TListTracking>({
data: [],
users: [],
})
const [action, setAction] = useState('')
const [item, setItem] = useState({id: 0})
const [item, setItem] = useState({ id: 0 })
const [activeBtn, setActiveBtn] = useState(false)
const [disableBtn, setDisableBtn] = useState(false)
const columns = [
{
name: 'id',
@ -40,7 +76,7 @@ const Tracking = () => {
size: '15%',
header: 'Created At',
render: (row: any) => {
return moment(row.updated_at).format('YYYY/MM/DD - HH:mm:ss')
return moment(row.created_at).format('YYYY/MM/DD - HH:mm:ss')
},
},
{
@ -49,9 +85,7 @@ const Tracking = () => {
header: 'Action',
render: (row: any) => {
return (
<Box
className={classes.optionIcon}
>
<Box className={classes.optionIcon}>
<IconTrash
className={classes.deleteIcon}
onClick={() => {
@ -61,6 +95,16 @@ const Tracking = () => {
width={20}
height={20}
/>
<IconEdit
className={classes.editIcon}
onClick={() => {
setAction('edit')
console.log(row)
form.setValues(row)
}}
width={20}
height={20}
/>
</Box>
)
},
@ -85,7 +129,17 @@ const Tracking = () => {
},
]
const getAllTracking = async () =>{
const form = useForm({
initialValues: {
id: 0,
name: '',
time_string: new Date(),
status: '',
created_at: '',
},
})
const getAllTracking = async () => {
try {
const searchParams = new URLSearchParams(window.location.search)
const params = {}
@ -99,7 +153,7 @@ const Tracking = () => {
}
const res = await get(getListTracking, params)
if(res.status){
if (res.status) {
setListTracking(res)
}
} catch (error) {
@ -107,6 +161,41 @@ const Tracking = () => {
}
}
const handleCreate = async (values: TLog) => {
try {
const res = await create(addTracking, {
status: values.status,
name: values.name,
time_string: moment(values.time_string).format('YYYY-MM-DD HH:mm:ss'),
created_at: values.time_string
}, getAllTracking)
if (res === true) {
setAction('')
form.reset()
}
} catch (error) {
console.log(error)
}
}
const handleUpdate = async (values: TLog) => {
try {
const res = await update(updateTracking, {
status: values.status,
name: values.name,
time_string: moment(values.time_string).format('YYYY-MM-DD HH:mm:ss'),
created_at: new Date(values.time_string),
id: values.id
}, getAllTracking)
if (res === true) {
setAction('')
form.reset()
}
} catch (error) {
console.log(error)
}
}
const handleDelete = async (id: number) => {
try {
await Xdelete(deleteTracking, { id: id }, getAllTracking)
@ -115,15 +204,15 @@ const Tracking = () => {
}
}
useEffect(()=>{
if(listTracking.data.length === 0){
useEffect(() => {
if (listTracking.data.length === 0) {
getAllTracking()
}
setInterval(()=>{
if(window.location.pathname.includes('tracking')){
getAllTracking()
}
}, 7000)
// setInterval(() => {
// if (window.location.pathname.includes('tracking')) {
// getAllTracking()
// }
// }, 7000)
}, [])
return (
<div>
@ -132,28 +221,101 @@ const Tracking = () => {
<Text>Admin/</Text>
Tracking
</h3>
{/* <Button
<Button
m={5}
onClick={() => {
setAction('add')
form.reset()
}}
> */}
{/* Add discount */}
{/* </Button> */}
>
+ Add
</Button>
</div>
<Box p={20}>
{
listTracking.data.length>0 && <DataTablePagination
filterInfo={filterInfo}
data={listTracking}
columns={columns}
searchInput
size=""
/>
}
{listTracking.data.length > 0 && (
<DataTablePagination
filterInfo={filterInfo}
data={listTracking}
columns={columns}
searchInput
size=""
/>
)}
</Box>
{/* Add/Edit User modal */}
<Modal
opened={action === 'add' || action === 'edit'}
onClose={() => {
setAction('')
form.reset()
}}
title={
<Text pl={'sm'} fw={700} fz={'lg'}>
{action === 'add' ? 'Add log' : 'Update log'}
</Text>
}
>
<form
onSubmit={form.onSubmit(async (values) => {
setDisableBtn(true)
action === 'edit'
? await handleUpdate(values)
: await handleCreate(values)
setDisableBtn(false)
})}
>
<Box pl={'md'} pr={'md'}>
<Select
label={'Name'}
placeholder="Banner x"
maxLength={255}
required
data={listTracking.users.map((i: User) => i.name)}
value={form.values.name}
error={form.errors.name}
onChange={(e) => form.setFieldValue('name', e!)}
/>
<Select
label={'Status'}
required
data={['check in', 'check out']}
value={form.values.status}
error={form.errors.status}
onChange={(e) => form.setFieldValue('status', e!)}
/>
<DateTimePicker
label={'Time'}
value={new Date(form.values.time_string)}
onChange={(e) => form.setFieldValue('time_string', e!)}
></DateTimePicker>
<Box ta={'center'}>
{action === 'add' ? (
<Button
mt={'lg'}
bg={'green'}
type="submit"
disabled={disableBtn}
>
Create
</Button>
) : (
<Button
mt={'lg'}
bg={'green'}
type="submit"
disabled={disableBtn}
>
Save
</Button>
)}
</Box>
</Box>
</form>
</Modal>
<Dialog
className={classes.dialog}
opened={action === 'delete'}
@ -195,4 +357,4 @@ const Tracking = () => {
)
}
export default Tracking
export default Tracking

View File

@ -221,7 +221,7 @@ const Worklogs = () => {
label="From date:"
value={new Date(date.startDate)}
w={'20%'}
clearable
// clearable
onChange={(e) => {
setDate({ ...date, startDate: moment(e).format('YYYY-MM-DD') })
}}
@ -230,7 +230,8 @@ const Worklogs = () => {
size="xs"
label="To date:"
value={new Date(date.endDate)}
clearable
// clearable
m={'0 10px'}
w={'20%'}
onChange={(e) => {

View File

@ -8,6 +8,7 @@ import Dashboard from '@/pages/Dashboard/Dashboard'
import GeneralSetting from '@/pages/GeneralSetting/GeneralSetting'
import Jira from '@/pages/Jira/Jira'
import PageNotFound from '@/pages/NotFound/NotFound'
import Timekeeping from '@/pages/Timekeeping/Timekeeping'
import Tracking from '@/pages/Tracking/Tracking'
import PageWelcome from '@/pages/Welcome/Welcome'
import Worklogs from '@/pages/Worklogs/Worklogs'
@ -19,7 +20,7 @@ const mainRoutes = [
// element: <ProtectedRoute mode="home"><PageHome /></ProtectedRoute>,
element: (
<ProtectedRoute mode="home">
<Navigate to="/tracking"></Navigate>
<Navigate to="/timekeeping"></Navigate>
</ProtectedRoute>
),
},
@ -115,6 +116,20 @@ const mainRoutes = [
</ProtectedRoute>
),
},
{
path: '/timekeeping',
element: (
<ProtectedRoute mode="route">
<BasePage
main={
<>
<Timekeeping />
</>
}
></BasePage>
</ProtectedRoute>
),
},
// {
// path: '/packages',
// element: (