From 54e5d8e780144eb04da906326258a7770b945f6e Mon Sep 17 00:00:00 2001 From: JOSEPH LE Date: Wed, 5 Jun 2024 14:00:47 +0700 Subject: [PATCH] update timekeeping --- .../Controllers/TimekeepingController.php | 74 ++++++ .../Http/Controllers/TrackingController.php | 6 + BACKEND/Modules/Admin/routes/api.php | 69 ++--- FRONTEND/src/api/Admin.ts | 7 +- FRONTEND/src/components/Navbar/Navbar.tsx | 2 + .../pages/Timekeeping/Timekeeping.module.css | 51 ++++ .../src/pages/Timekeeping/Timekeeping.tsx | 239 ++++++++++++++++++ .../src/pages/Tracking/Tracking.module.css | 12 + FRONTEND/src/pages/Tracking/Tracking.tsx | 238 ++++++++++++++--- FRONTEND/src/pages/Worklogs/Worklogs.tsx | 5 +- FRONTEND/src/routes/main.tsx | 17 +- 11 files changed, 624 insertions(+), 96 deletions(-) create mode 100644 BACKEND/Modules/Admin/app/Http/Controllers/TimekeepingController.php create mode 100644 FRONTEND/src/pages/Timekeeping/Timekeeping.module.css create mode 100644 FRONTEND/src/pages/Timekeeping/Timekeeping.tsx diff --git a/BACKEND/Modules/Admin/app/Http/Controllers/TimekeepingController.php b/BACKEND/Modules/Admin/app/Http/Controllers/TimekeepingController.php new file mode 100644 index 0000000..02febd7 --- /dev/null +++ b/BACKEND/Modules/Admin/app/Http/Controllers/TimekeepingController.php @@ -0,0 +1,74 @@ +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]); + } +} diff --git a/BACKEND/Modules/Admin/app/Http/Controllers/TrackingController.php b/BACKEND/Modules/Admin/app/Http/Controllers/TrackingController.php index 2a93879..31a10dd 100755 --- a/BACKEND/Modules/Admin/app/Http/Controllers/TrackingController.php +++ b/BACKEND/Modules/Admin/app/Http/Controllers/TrackingController.php @@ -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; diff --git a/BACKEND/Modules/Admin/routes/api.php b/BACKEND/Modules/Admin/routes/api.php index bb926c7..9369f6c 100755 --- a/BACKEND/Modules/Admin/routes/api.php +++ b/BACKEND/Modules/Admin/routes/api.php @@ -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']); }); diff --git a/FRONTEND/src/api/Admin.ts b/FRONTEND/src/api/Admin.ts index 9deaac4..eda18e0 100755 --- a/FRONTEND/src/api/Admin.ts +++ b/FRONTEND/src/api/Admin.ts @@ -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' \ No newline at end of file diff --git a/FRONTEND/src/components/Navbar/Navbar.tsx b/FRONTEND/src/components/Navbar/Navbar.tsx index ddf0eb8..f365d08 100755 --- a/FRONTEND/src/components/Navbar/Navbar.tsx +++ b/FRONTEND/src/components/Navbar/Navbar.tsx @@ -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 }, diff --git a/FRONTEND/src/pages/Timekeeping/Timekeeping.module.css b/FRONTEND/src/pages/Timekeeping/Timekeeping.module.css new file mode 100644 index 0000000..322fbb2 --- /dev/null +++ b/FRONTEND/src/pages/Timekeeping/Timekeeping.module.css @@ -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); + } \ No newline at end of file diff --git a/FRONTEND/src/pages/Timekeeping/Timekeeping.tsx b/FRONTEND/src/pages/Timekeeping/Timekeeping.tsx new file mode 100644 index 0000000..ce146ee --- /dev/null +++ b/FRONTEND/src/pages/Timekeeping/Timekeeping.tsx @@ -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([]) + 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 ( +
+
+

+ Admin/ + Timekeeping +

+
+ + + + + + + + + Day + {daysInMonth.map((d) => { + return {d} + })} + + + + {daysInMonth.map((d) => { + return ( + + {getDayName(`${date.year}-${date.month}-${d}`)} + + ) + })} + + + + {data.map((user) => { + return ( + + {user.user.name} + {daysInMonth.map((d) => { + var total = + user.history.find((h) => h.day === d)?.total ?? 0 + return ( + + {total / 60 / 60 < 8 && total !== 0 ? ( + + {`Total: ${(total / 60 / 60).toFixed(1)}h`} + {user.history + .find((h) => h.day === d) + ?.values.map((v) => { + return ( +

{v.status + ': ' + v.time_string}

+ ) + })} + + } + > + +
+ ) : total >= 8 ? ( + + {`Total: ${(total / 60 / 60).toFixed(1)}h`} + {user.history + .find((h) => h.day === d) + ?.values.map((v) => { + return ( +

{v.status + ': ' + v.time_string}

+ ) + })} + + } + > + +
+ ) : ( + + )} +
+ ) + })} +
+ ) + })} +
+
+
+
+ ) +} + +export default Timekeeping diff --git a/FRONTEND/src/pages/Tracking/Tracking.module.css b/FRONTEND/src/pages/Tracking/Tracking.module.css index 7deaf8b..b35dd04 100755 --- a/FRONTEND/src/pages/Tracking/Tracking.module.css +++ b/FRONTEND/src/pages/Tracking/Tracking.module.css @@ -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); } diff --git a/FRONTEND/src/pages/Tracking/Tracking.tsx b/FRONTEND/src/pages/Tracking/Tracking.tsx index 3d0afaa..ad4bf9b 100755 --- a/FRONTEND/src/pages/Tracking/Tracking.tsx +++ b/FRONTEND/src/pages/Tracking/Tracking.tsx @@ -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({ + 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 ( - + { @@ -61,6 +95,16 @@ const Tracking = () => { width={20} height={20} /> + { + setAction('edit') + console.log(row) + form.setValues(row) + }} + width={20} + height={20} + /> ) }, @@ -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 (
@@ -132,28 +221,101 @@ const Tracking = () => { Admin/ Tracking - {/* */} + > + + Add +
- { - listTracking.data.length>0 && - } + {listTracking.data.length > 0 && ( + + )} + {/* Add/Edit User modal */} + { + setAction('') + form.reset() + }} + title={ + + {action === 'add' ? 'Add log' : 'Update log'} + + } + > +
{ + setDisableBtn(true) + action === 'edit' + ? await handleUpdate(values) + : await handleCreate(values) + setDisableBtn(false) + })} + > + + form.setFieldValue('status', e!)} + /> + form.setFieldValue('time_string', e!)} + > + + + {action === 'add' ? ( + + ) : ( + + )} + + +
+
+ { ) } -export default Tracking \ No newline at end of file +export default Tracking diff --git a/FRONTEND/src/pages/Worklogs/Worklogs.tsx b/FRONTEND/src/pages/Worklogs/Worklogs.tsx index 074230b..90d5276 100644 --- a/FRONTEND/src/pages/Worklogs/Worklogs.tsx +++ b/FRONTEND/src/pages/Worklogs/Worklogs.tsx @@ -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) => { diff --git a/FRONTEND/src/routes/main.tsx b/FRONTEND/src/routes/main.tsx index 43bd7fd..bdf79b0 100755 --- a/FRONTEND/src/routes/main.tsx +++ b/FRONTEND/src/routes/main.tsx @@ -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: , element: ( - + ), }, @@ -115,6 +116,20 @@ const mainRoutes = [ ), }, + { + path: '/timekeeping', + element: ( + + + + + } + > + + ), + }, // { // path: '/packages', // element: (