Merge branch 'master' into dev

This commit is contained in:
Truong Vo 2025-02-21 11:12:15 +07:00
commit 1018172b58
13 changed files with 6415 additions and 2744 deletions

View File

@ -337,4 +337,20 @@ class JiraController extends Controller
Mail::to([$email])->cc(['admin@apactech.io', 'joseph@apactech.io'])->send(new WarningLongTask($user_info[$email])); Mail::to([$email])->cc(['admin@apactech.io', 'joseph@apactech.io'])->send(new WarningLongTask($user_info[$email]));
} }
} }
public function getUserProjectParticipating(Request $request)
{
$userID = $request->input('userID');
$startDate = $request->input('fromDate');
$endDate = $request->input('toDate');
$user = User::find($userID);
$userJira = $this->jiraService->getUserByEmail($user->email);
$projects = $this->jiraService->getUserWorkLogs($userJira[0]['accountId'], $startDate, $endDate);
return response()->json([
'data' => $projects,
"accountId" => $userJira[0]['accountId'],
'status' => true
], 200);
}
} }

View File

@ -17,6 +17,7 @@ use Illuminate\Support\Facades\Storage;
use Modules\Admin\app\Models\Admin; use Modules\Admin\app\Models\Admin;
use Modules\Admin\app\Models\MonthlyTimekeeping; use Modules\Admin\app\Models\MonthlyTimekeeping;
use Modules\Admin\app\Models\Tracking; use Modules\Admin\app\Models\Tracking;
use Illuminate\Support\Facades\DB;
class TrackingController extends Controller class TrackingController extends Controller
{ {
@ -24,7 +25,7 @@ class TrackingController extends Controller
use HasFilterRequest; use HasFilterRequest;
use HasSearchRequest; use HasSearchRequest;
use AnalyzeData; use AnalyzeData;
private $CHECK_IN = 'check in'; private $CHECK_IN = 'check in';
private $CHECK_OUT = 'check out'; private $CHECK_OUT = 'check out';
@ -207,15 +208,14 @@ class TrackingController extends Controller
// $status = $this->CHECK_IN; // $status = $this->CHECK_IN;
// $lastCheck =Tracking::find(1)->created_at; // $lastCheck =Tracking::find(1)->created_at;
// $user = Admin::where('name', 'LE TAN LUAN')->first(); // $user = Admin::where('name', 'LE TAN LUAN')->first();
if($status === $this->CHECK_IN){ if ($status === $this->CHECK_IN) {
$morning_time = Carbon::createFromTimeString('07:30AM')->setTimezone(env('TIME_ZONE')); $morning_time = Carbon::createFromTimeString('07:30AM')->setTimezone(env('TIME_ZONE'));
$morning_condition_time = Carbon::createFromTimeString('07:40AM')->setTimezone(env('TIME_ZONE')); $morning_condition_time = Carbon::createFromTimeString('07:40AM')->setTimezone(env('TIME_ZONE'));
$afternoon_time = Carbon::createFromTimeString('11:30AM')->setTimezone(env('TIME_ZONE')); $afternoon_time = Carbon::createFromTimeString('11:30AM')->setTimezone(env('TIME_ZONE'));
$afternoon_condition_time = Carbon::createFromTimeString('01:10PM')->setTimezone(env('TIME_ZONE')); $afternoon_condition_time = Carbon::createFromTimeString('01:10PM')->setTimezone(env('TIME_ZONE'));
$admin_mails = Admin::where('permission', 'like', '%admin%')->pluck('email'); $admin_mails = Admin::where('permission', 'like', '%admin%')->pluck('email');
if ($time->greaterThan($morning_condition_time) && $time->lessThan($afternoon_time)) if ($time->greaterThan($morning_condition_time) && $time->lessThan($afternoon_time)) {
{
$period = 'morning'; $period = 'morning';
$minutes_late = $morning_time->diffInMinutes($time); $minutes_late = $morning_time->diffInMinutes($time);
$data = array( $data = array(
@ -223,9 +223,9 @@ class TrackingController extends Controller
"email" => $user->email, "email" => $user->email,
"name" => $user->name, "name" => $user->name,
"admin_mails" => $admin_mails, "admin_mails" => $admin_mails,
"message1" => "Your ". $period ." starts ". $minutes_late ." minutes late", "message1" => "Your " . $period . " starts " . $minutes_late . " minutes late",
"message2" => "You checked in at [" . $time ."]", "message2" => "You checked in at [" . $time . "]",
"url" => env('ADMIN_URL')."/tracking?search=&per_page=10&page=1&timezone=Asia%2FSaigon&name=".$user->name."&time_string=".$time->format("Y-m-d H:i")."&status=check+in", "url" => env('ADMIN_URL') . "/tracking?search=&per_page=10&page=1&timezone=Asia%2FSaigon&name=" . $user->name . "&time_string=" . $time->format("Y-m-d H:i") . "&status=check+in",
"subject" => "[Management System] Late warning - " . $user->name "subject" => "[Management System] Late warning - " . $user->name
); );
Mail::to($user->email)->cc($admin_mails)->send(new CheckinLateMail($data)); Mail::to($user->email)->cc($admin_mails)->send(new CheckinLateMail($data));
@ -247,7 +247,117 @@ class TrackingController extends Controller
// Mail::to($user->email)->cc($admin_mails)->send(new CheckinLateMail($data)); // Mail::to($user->email)->cc($admin_mails)->send(new CheckinLateMail($data));
// } // }
} }
} }
public function getSummaryTracking(Request $request)
{
$request->validate([
'userID' => 'required|exists:users,id',
'fromDate' => 'nullable|date',
'toDate' => 'nullable|date',
]);
$userID = $request->input('userID');
$startDate = $request->input('fromDate');
$endDate = $request->input('toDate');
$trackingData = Tracking::where('user_id', $userID);
if ($startDate && $endDate) {
$trackingData->whereBetween(
DB::raw("STR_TO_DATE(time_string, '%Y-%m-%d %H:%i:%s')"),
[$startDate, $endDate . ' 23:59:59']
);
} elseif ($startDate) {
$trackingData->where(
DB::raw("STR_TO_DATE(time_string, '%Y-%m-%d %H:%i:%s')"),
'>=',
$startDate
);
} elseif ($endDate) {
$trackingData->where(
DB::raw("STR_TO_DATE(time_string, '%Y-%m-%d %H:%i:%s')"),
'<=',
$endDate . ' 23:59:59'
);
}
$onTimeMorning = 0;
$lateMorning = 0;
$onTimeAfternoon = 0;
$lateAfternoon = 0;
$returnTracking = $trackingData->get();
$listLate = [];
$returnTracking->groupBy(function ($record) {
return Carbon::parse($record->time_string)->toDateString();
})->each(function ($records, $date) use ($userID, &$listLate, &$onTimeMorning, &$lateMorning, &$onTimeAfternoon, &$lateAfternoon, &$datesChecked) {
$morningCheck = $records->filter(function ($record) {
return Carbon::parse($record->time_string)->hour < 12;
})->sortBy('time_string')->first();
$afternoonCheck = $records->filter(function ($record) {
$time = Carbon::parse($record->time_string)->hour;
return $time >= 12 && $time <= 14;
})->sortBy('time_string')->first();
$morningTime = Carbon::parse($date)->setTime(7, 40, 0);
$afternoonTime = Carbon::parse($date)->setTime(13, 10, 0);
$checkOutAfternoonTime = Carbon::parse($date)->setTime(15, 00, 0);
if ($morningCheck) {
$checkInTime = Carbon::parse($morningCheck->time_string);
if ($checkInTime->lessThanOrEqualTo($morningTime)) {
$onTimeMorning++;
} else {
array_push($listLate, $morningCheck->id);
$lateMorning++;
}
}
if ($afternoonCheck) {
$checkInTime = Carbon::parse($afternoonCheck->time_string);
if ($checkInTime->lessThanOrEqualTo($afternoonTime)) {
$onTimeAfternoon++;
} else {
array_push($listLate, $afternoonCheck->id);
$lateAfternoon++;
}
} else {
// print ($date . "\n");
$checkOutAfternoon = Tracking::where('user_id', $userID)->whereBetween(
DB::raw("STR_TO_DATE(time_string, '%Y-%m-%d %H:%i:%s')"),
[$date, $date . ' 23:59:59'],
)->where("status", "check out")->first();
// check if not check-in afternoon but has check out
if ($checkOutAfternoon) {
$timeCheck = Carbon::parse($checkOutAfternoon->time_string);
// print ($timeCheck . "\n");
if ($checkOutAfternoon && $timeCheck->greaterThanOrEqualTo($checkOutAfternoonTime)) {
$onTimeAfternoon++;
}
}
}
});
// ** Add `isLate` flag to each record **
foreach ($returnTracking as $record) {
if (in_array($record->id, $listLate)) {
$record->isLate = true;
}
}
return AbstractController::ResultSuccess([
'on_time_morning' => $onTimeMorning,
'late_morning' => $lateMorning,
'on_time_afternoon' => $onTimeAfternoon,
'late_afternoon' => $lateAfternoon,
'value' => $returnTracking
]);
}
} }

View File

@ -116,6 +116,7 @@ Route::middleware('api')
Route::get('/worklogs', [JiraController::class, 'getAllUserWorkLogs'])->middleware('check.permission:admin.staff'); Route::get('/worklogs', [JiraController::class, 'getAllUserWorkLogs'])->middleware('check.permission:admin.staff');
Route::get('/allocation', [JiraController::class, 'getAllUserDoing'])->middleware('check.permission:admin.staff'); Route::get('/allocation', [JiraController::class, 'getAllUserDoing'])->middleware('check.permission:admin.staff');
Route::get('/issue/detail', [JiraController::class, 'getDetailIssueById'])->middleware('check.permission:admin.staff'); Route::get('/issue/detail', [JiraController::class, 'getDetailIssueById'])->middleware('check.permission:admin.staff');
Route::get('/project-participating', [JiraController::class, 'getUserProjectParticipating'])->middleware('check.permission:admin');
}); });
Route::group([ Route::group([
@ -136,6 +137,7 @@ Route::middleware('api')
Route::post('/create', [TrackingController::class, 'create'])->middleware('check.permission:admin.hr'); Route::post('/create', [TrackingController::class, 'create'])->middleware('check.permission:admin.hr');
Route::post('/update', [TrackingController::class, 'update'])->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::get('/delete', [TrackingController::class, 'delete'])->middleware('check.permission:admin.hr');
Route::get('/summary', [TrackingController::class, 'getSummaryTracking'])->middleware('check.permission:admin');
}); });
Route::group([ Route::group([

View File

@ -231,12 +231,12 @@ class JiraService
$users_data = []; $users_data = [];
$user_warning = []; $user_warning = [];
foreach ($users as $user) { foreach ($users as $user) {
$user = (array)$user[0]; $user = (array) $user[0];
$users_data[$user['displayName']]['user'] = $user; $users_data[$user['displayName']]['user'] = $user;
$users_data[$user['displayName']]['total_spent'] = 0; $users_data[$user['displayName']]['total_spent'] = 0;
$users_data[$user['displayName']]['total_est'] = 0; $users_data[$user['displayName']]['total_est'] = 0;
$body = [ $body = [
'expand' => ['names', 'schema','changelog'], 'expand' => ['names', 'schema', 'changelog'],
'fields' => ['summary', 'status', 'timeoriginalestimate', 'timespent', 'assignee', 'project', 'updated'], 'fields' => ['summary', 'status', 'timeoriginalestimate', 'timespent', 'assignee', 'project', 'updated'],
'jql' => sprintf( 'jql' => sprintf(
"assignee = '%s' AND status IN ('to do', 'todo', 'in progress') ORDER BY updated DESC", "assignee = '%s' AND status IN ('to do', 'todo', 'in progress') ORDER BY updated DESC",

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@
}, },
"dependencies": { "dependencies": {
"@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-javascript": "^6.2.2",
"@mantine/charts": "^7.16.3",
"@mantine/core": "^7.13.2", "@mantine/core": "^7.13.2",
"@mantine/dates": "^7.13.2", "@mantine/dates": "^7.13.2",
"@mantine/form": "^7.13.2", "@mantine/form": "^7.13.2",
@ -36,7 +37,7 @@
"react-redux": "^8.1.3", "react-redux": "^8.1.3",
"react-router-dom": "^6.19.0", "react-router-dom": "^6.19.0",
"reactstrap": "^9.2.2", "reactstrap": "^9.2.2",
"recharts": "^2.11.0", "recharts": "^2.15.1",
"tailwind-merge": "^2.0.0", "tailwind-merge": "^2.0.0",
"tests": "^0.4.2" "tests": "^0.4.2"
}, },

View File

@ -11,6 +11,7 @@ import { BrowserRouter, useRoutes } from 'react-router-dom'
import mainRoutes from '@/routes/main' import mainRoutes from '@/routes/main'
import classes from '@/App.module.css' import classes from '@/App.module.css'
import '@mantine/dates/styles.css' import '@mantine/dates/styles.css'
import '@mantine/charts/styles.css'
export const App = () => { export const App = () => {
const element = useRoutes(mainRoutes) const element = useRoutes(mainRoutes)

View File

@ -7,6 +7,7 @@ export const getListTracking = API_URL + 'v1/admin/tracking'
export const addTracking = API_URL + 'v1/admin/tracking/create' export const addTracking = API_URL + 'v1/admin/tracking/create'
export const updateTracking = API_URL + 'v1/admin/tracking/update' export const updateTracking = API_URL + 'v1/admin/tracking/update'
export const deleteTracking = API_URL + 'v1/admin/tracking/delete' export const deleteTracking = API_URL + 'v1/admin/tracking/delete'
export const getListTrackingSummary = API_URL + 'v1/admin/tracking/summary'
// Worklogs // Worklogs
export const fetchAllIssues = API_URL + 'v1/admin/jira/fetch-issues' export const fetchAllIssues = API_URL + 'v1/admin/jira/fetch-issues'
@ -17,6 +18,8 @@ export const getAllIssuesByProject =
export const getAllUserWorklogs = API_URL + 'v1/admin/jira/worklogs' export const getAllUserWorklogs = API_URL + 'v1/admin/jira/worklogs'
export const getAllUserDoing = API_URL + 'v1/admin/jira/allocation' export const getAllUserDoing = API_URL + 'v1/admin/jira/allocation'
export const getDetailIssByKey = API_URL + 'v1/admin/jira/issue/detail' export const getDetailIssByKey = API_URL + 'v1/admin/jira/issue/detail'
export const getPJParticipating =
API_URL + 'v1/admin/jira/project-participating'
//Timekeeping //Timekeeping
export const getTheTimesheet = API_URL + 'v1/admin/timekeeping' export const getTheTimesheet = API_URL + 'v1/admin/timekeeping'

View File

@ -11,6 +11,7 @@ import {
MultiSelect, MultiSelect,
Pagination, Pagination,
RadioGroup, RadioGroup,
ScrollArea,
Select, Select,
Skeleton, Skeleton,
Table, Table,
@ -85,6 +86,8 @@ export const DataTableAll = ({
size, size,
infoTotal, infoTotal,
componentRight, componentRight,
height = 600,
keyHighlight = '',
}: { }: {
data: any[] data: any[]
columns: Column[] columns: Column[]
@ -94,6 +97,8 @@ export const DataTableAll = ({
size: string size: string
infoTotal?: React.ReactNode // Set the type to ReactNode to allow JSX elements infoTotal?: React.ReactNode // Set the type to ReactNode to allow JSX elements
componentRight?: React.ReactNode componentRight?: React.ReactNode
height?: number
keyHighlight?: string
}) => { }) => {
const [Tdata, setTData] = useState<any[]>(data) const [Tdata, setTData] = useState<any[]>(data)
// const [tempData, setTempData] = useState<any[]>([]) // const [tempData, setTempData] = useState<any[]>([])
@ -206,6 +211,9 @@ export const DataTableAll = ({
? 'var(--mantine-color-blue-light)' ? 'var(--mantine-color-blue-light)'
: undefined : undefined
} }
style={{
backgroundColor: element[keyHighlight] ? '#ff70704d' : 'transparent',
}}
> >
<Table.Td display={checkBox ? 'block' : 'none'}> <Table.Td display={checkBox ? 'block' : 'none'}>
<Checkbox <Checkbox
@ -369,43 +377,45 @@ export const DataTableAll = ({
</Box> </Box>
{componentRight} {componentRight}
</Box> </Box>
<Box className={classes.box}> <Box className={classes.box} style={{ height: height + 50 }}>
<Table <ScrollArea h={height}>
stickyHeader <Table
stickyHeaderOffset={-1} stickyHeader
striped stickyHeaderOffset={-1}
highlightOnHover striped
withTableBorder highlightOnHover
withColumnBorders withTableBorder
> withColumnBorders
<Table.Thead className={classes.headers}> >
<Table.Tr> <Table.Thead className={classes.headers}>
<Table.Th display={checkBox ? 'block' : 'none'}> <Table.Tr>
<Checkbox <Table.Th display={checkBox ? 'block' : 'none'}>
aria-label="Select row" <Checkbox
checked={ aria-label="Select row"
checkSubArray(Tdata, selectedRows) && checked={
Tdata.length === selectedRows.length checkSubArray(Tdata, selectedRows) &&
} Tdata.length === selectedRows.length
onChange={(event) => }
setSelectedRows( onChange={(event) =>
event.currentTarget.checked setSelectedRows(
? (pre) => [...pre, ...Tdata] event.currentTarget.checked
: selectedRows.filter( ? (pre) => [...pre, ...Tdata]
(item) => : selectedRows.filter(
!Tdata.some((removeItem) => (item) =>
areObjectsEqual(item, removeItem), !Tdata.some((removeItem) =>
), areObjectsEqual(item, removeItem),
), ),
) ),
} )
/> }
</Table.Th> />
{headers} </Table.Th>
</Table.Tr> {headers}
</Table.Thead> </Table.Tr>
<Table.Tbody>{rows}</Table.Tbody> </Table.Thead>
</Table> <Table.Tbody>{rows}</Table.Tbody>
</Table>
</ScrollArea>
</Box> </Box>
</Container> </Container>
) )

View File

@ -55,8 +55,6 @@
display: flex; display: flex;
margin-top: 20px; margin-top: 20px;
gap: 10px; gap: 10px;
max-height: 72vh;
overflow-y: scroll;
} }
.titleSidebar { .titleSidebar {
@ -202,3 +200,46 @@
width: rem(20px); width: rem(20px);
height: rem(22px); height: rem(22px);
} }
.chartContainer {
display: flex;
justify-content: center;
margin-top: 20px;
}
.boxContainer {
display: flex;
justify-content: center;
align-items: center;
}
.pjParticipatingContainer {
margin-top: 20px;
display: flex;
justify-content: center;
align-items: center;
}
.boxColorLime {
width: 12px;
height: 12px;
background-color: #82c91e;
}
.boxColorRed {
width: 12px;
height: 12px;
background-color: #fa5252;
}
.boxColorOrange {
width: 12px;
height: 12px;
background-color: #fd7e14;
}
.boxColorTeal {
width: 12px;
height: 12px;
background-color: #12b886;
}

View File

@ -8,6 +8,8 @@ import {
projectReviewUpdate, projectReviewUpdate,
projectReviewDelete, projectReviewDelete,
evaluationReportAllUsers, evaluationReportAllUsers,
getListTrackingSummary,
getPJParticipating,
} from '@/api/Admin' } from '@/api/Admin'
import DataTableAll from '@/components/DataTable/DataTable' import DataTableAll from '@/components/DataTable/DataTable'
import ProjectInvolvement from '@/components/ProjectInvolvement/ProjectInvolvement' import ProjectInvolvement from '@/components/ProjectInvolvement/ProjectInvolvement'
@ -35,10 +37,12 @@ import {
IconClearAll, IconClearAll,
IconEdit, IconEdit,
IconPresentationAnalytics, IconPresentationAnalytics,
IconReportAnalytics,
IconX, IconX,
} from '@tabler/icons-react' } from '@tabler/icons-react'
import { useForm } from '@mantine/form' import { useForm } from '@mantine/form'
import { update, Xdelete } from '@/rtk/helpers/CRUD' import { update, Xdelete } from '@/rtk/helpers/CRUD'
import { PieChart } from '@mantine/charts'
interface User { interface User {
id: number id: number
@ -76,8 +80,32 @@ interface DataProjectReview {
updated_at: string updated_at: string
} }
interface DataPJParticipating {
name: string
total_task: number
total_time_spent: number
}
type TLog = {
id: number
name: string
status: string
time_string: Date
}
interface DataSummaryTracking {
on_time_morning: number
late_morning: number
on_time_afternoon: number
late_afternoon: number
value: TLog[]
}
const StaffEvaluation = () => { const StaffEvaluation = () => {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [loadingReview, setLoadingReview] = useState(false)
const [loadingWorkingStyle, setLoadingWorkingStyle] = useState(false)
const [loadingPJParticipating, setLoadingPJParticipating] = useState(false)
const [loadingTechnical, setLoadingTechnical] = useState(false) const [loadingTechnical, setLoadingTechnical] = useState(false)
const [dataProfile, setDataProfile] = useState<any>([]) const [dataProfile, setDataProfile] = useState<any>([])
const [dataTechnical, setDataTechnical] = useState<DataTechnical[]>([]) const [dataTechnical, setDataTechnical] = useState<DataTechnical[]>([])
@ -96,6 +124,17 @@ const StaffEvaluation = () => {
const [activeBtn, setActiveBtn] = useState(false) const [activeBtn, setActiveBtn] = useState(false)
const [loadingExport, setLoadingExport] = useState(false) const [loadingExport, setLoadingExport] = useState(false)
const [loadingExportAll, setLoadingExportAll] = useState(false) const [loadingExportAll, setLoadingExportAll] = useState(false)
const [dataPJParticipating, setDataPJParticipating] = useState<
DataPJParticipating[]
>([])
const [dataSummaryTracking, setDataSummaryTracking] =
useState<DataSummaryTracking>({
on_time_morning: 0,
late_morning: 0,
on_time_afternoon: 0,
late_afternoon: 0,
value: [],
})
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
@ -103,6 +142,9 @@ const StaffEvaluation = () => {
name: '', name: '',
role: '', role: '',
note: '', note: '',
user_id: 0,
created_at: '',
updated_at: '',
}, },
validate: { validate: {
name: (value) => name: (value) =>
@ -295,17 +337,147 @@ const StaffEvaluation = () => {
return [] return []
} }
const getListSummaryTracking = async (filterSearch: Filter) => {
try {
const params = {
userID: filterSearch.userID ?? '',
fromDate: filterSearch.fromDate
? moment(filterSearch.fromDate).format('YYYY-MM-DD')
: null,
toDate: filterSearch.toDate
? moment(filterSearch.toDate).format('YYYY-MM-DD')
: null,
}
const res = await get(getListTrackingSummary, params)
if (res.status) {
return res.data
}
} catch (error: any) {
notifications.show({
title: 'Error',
message: error.message ?? error,
color: 'red',
})
}
return []
}
const getListProjectParticipating = async (filterSearch: Filter) => {
try {
const fromDate = filterSearch.fromDate
? moment(filterSearch.fromDate).format('YYYY-MM-DD')
: moment(new Date()).format('YYYY-MM-DD')
const toDate = filterSearch.toDate
? moment(filterSearch.toDate).format('YYYY-MM-DD')
: moment(new Date()).format('YYYY-MM-DD')
const params = {
userID: filterSearch.userID ?? '',
fromDate: fromDate,
toDate: toDate,
}
const res = await get(getPJParticipating, params)
if (res.status) {
const value = processJiraData(res.data, fromDate, toDate, res.accountId)
return value
}
} catch (error: any) {
notifications.show({
title: 'Error',
message: error.message ?? error,
color: 'red',
})
}
return []
}
function processJiraData(
data: any,
startDate: any,
endDate: any,
accountId: string,
) {
const projectSummary: any = {}
const start = new Date(startDate)
const end = new Date(endDate)
data.issues.forEach((issue: any) => {
const projectName = issue.fields.project.name
const worklogs = issue.fields.worklog.worklogs
// Filter worklogs based on 'started' date range
const filteredWorklogs = worklogs.filter((log: any) => {
const logDate = new Date(log.started)
return (
logDate >= start &&
logDate <= end &&
accountId === log?.updateAuthor?.accountId
)
})
if (filteredWorklogs.length === 0) return // Skip if no worklogs in range
if (!projectSummary[projectName]) {
projectSummary[projectName] = {
project_name: projectName,
total_task: 0,
total_time_spent: 0,
}
}
// Get unique issueIds within the filtered worklogs
const uniqueIssues = new Set(
filteredWorklogs.map((log: any) => log.issueId),
)
// Sum up total time spent from filtered worklogs
const totalTimeSpent = filteredWorklogs.reduce(
(sum: number, log: any) => sum + log.timeSpentSeconds,
0,
)
projectSummary[projectName].total_task += uniqueIssues.size
projectSummary[projectName].total_time_spent += totalTimeSpent
})
const returnValue: DataPJParticipating[] = Object.values(projectSummary)
return returnValue
}
useEffect(() => { useEffect(() => {
if (filter?.userID) { if (filter?.userID) {
setLoading(true) setLoading(true)
setLoadingReview(true)
setLoadingWorkingStyle(true)
const fetchData = async () => { const fetchData = async () => {
const result = await getListProfilesData(filter)
setDataProfile(result ?? [])
setLoading(false)
}
const fetchDataProject = async () => {
const result = await getListProfilesData(filter) const result = await getListProfilesData(filter)
const resultProject = await getListProjectReview(filter) const resultProject = await getListProjectReview(filter)
setDataProfile(result ?? []) setDataProfile(result ?? [])
setDataProjectReview(resultProject ?? []) setDataProjectReview(resultProject ?? [])
setLoading(false) setLoadingReview(false)
}
const fetchDataTracking = async () => {
const resultTracking = await getListSummaryTracking(filter)
setDataSummaryTracking(resultTracking ?? [])
setLoadingWorkingStyle(false)
}
const fetchDataPJParticipating = async () => {
const resultPJParticipating = await getListProjectParticipating(filter)
setDataPJParticipating(resultPJParticipating ?? [])
setLoadingPJParticipating(false)
} }
fetchData() fetchData()
fetchDataProject()
fetchDataTracking()
if (filter?.fromDate && filter?.toDate) {
setLoadingPJParticipating(true)
fetchDataPJParticipating()
}
} }
}, [filter]) }, [filter])
@ -430,7 +602,7 @@ const StaffEvaluation = () => {
{ {
name: 'name', name: 'name',
size: '15%', size: '15%',
header: 'Project Name', header: 'Name',
}, },
{ {
name: 'role', name: 'role',
@ -509,11 +681,54 @@ const StaffEvaluation = () => {
}, },
] ]
const columnsPJParticipating = [
{
name: 'project_name',
size: '50%',
header: 'Name',
},
{
name: 'total_task',
size: '25%',
header: 'Total task',
},
{
name: 'total_time_spent',
size: '25%',
header: 'Total time spent',
render: (row: any) => {
return <div>{row?.total_time_spent / 60 / 60}h</div>
},
},
]
const columnsDetailWorking = [
{
name: 'name',
size: '40%',
header: 'Name',
},
{
name: 'time_string',
size: '40%',
header: 'Time',
render: (row: any) => {
return moment(row.time_string).format('YYYY/MM/DD - HH:mm:ss')
},
},
{
name: 'status',
size: '20%',
header: 'Status',
},
]
const handleCreate = async (values: DataProjectReview) => { const handleCreate = async (values: DataProjectReview) => {
try { try {
const { id, ...data } = values
const res = await post(projectReviewAdd, { const res = await post(projectReviewAdd, {
...data, name: values.name,
role: values.role,
note: values.note,
user_id: filter.userID, user_id: filter.userID,
}) })
if (res.id) { if (res.id) {
@ -530,7 +745,10 @@ const StaffEvaluation = () => {
const handleUpdate = async (values: DataProjectReview) => { const handleUpdate = async (values: DataProjectReview) => {
try { try {
const res = await update(projectReviewUpdate, { const res = await update(projectReviewUpdate, {
...values, id: values.id,
name: values.name,
role: values.role,
note: values.note,
user_id: filter.userID, user_id: filter.userID,
}) })
if (res) { if (res) {
@ -710,6 +928,12 @@ const StaffEvaluation = () => {
> >
<span style={{ fontSize: '16px' }}>Project review</span> <span style={{ fontSize: '16px' }}>Project review</span>
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab
value="working_style"
leftSection={<IconReportAnalytics size={16} color="#fab005" />}
>
<span style={{ fontSize: '16px' }}>Working review</span>
</Tabs.Tab>
</Tabs.List> </Tabs.List>
<Tabs.Panel value="general"> <Tabs.Panel value="general">
@ -778,9 +1002,12 @@ const StaffEvaluation = () => {
<Tabs.Panel value="project_review"> <Tabs.Panel value="project_review">
<Box className={classes.userInfoSection} display="flex"> <Box className={classes.userInfoSection} display="flex">
{loading ? ( {loadingReview ? (
<Box <Box
style={{ width: '100%', display: loading ? 'block' : 'none' }} style={{
width: '100%',
display: loadingReview ? 'block' : 'none',
}}
> >
<Box <Box
style={{ style={{
@ -825,6 +1052,163 @@ const StaffEvaluation = () => {
)} )}
</Box> </Box>
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="working_style">
{loadingWorkingStyle ? (
<Box
style={{
width: '100%',
display: loadingWorkingStyle ? 'block' : 'none',
}}
>
<Box
style={{
marginTop: '10%',
textAlign: 'center',
// display: 'none',
}}
>
<Loader size={'sm'} color="green" type="bars" m={'0 auto'} />
<Text fw={600} c={'gray'}>
Loading . . .
</Text>
</Box>
</Box>
) : (
<Box>
<Tabs defaultValue="overview" orientation="vertical">
<Tabs.List justify="center">
<Tabs.Tab value="overview">Overview</Tabs.Tab>
<Tabs.Tab value="detail">Detail</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="overview">
<Box style={{ height: 350, marginTop: 8 }}>
<Box className={classes.chartContainer} display="flex">
<PieChart
withLabelsLine
labelsPosition="outside"
labelsType="value"
withLabels={dataSummaryTracking.value.length > 0}
withTooltip
data={
dataSummaryTracking.value.length > 0
? [
{
name: 'On time morning',
value:
dataSummaryTracking?.on_time_morning ?? 0,
color: 'lime',
},
{
name: 'Late morning',
value: dataSummaryTracking?.late_morning ?? 0,
color: 'red',
},
{
name: 'On time afternoon',
value:
dataSummaryTracking?.on_time_afternoon ?? 0,
color: 'teal',
},
{
name: 'Late afternoon',
value:
dataSummaryTracking?.late_afternoon ?? 0,
color: 'orange',
},
]
: [
{
name: 'No data',
value: 1,
color: 'gray.6',
},
]
}
/>
</Box>
<Box className={classes.boxContainer} display="flex">
<Box className={classes.boxContainer} display="flex">
<div className={classes.boxColorLime}></div>
<div
style={{ paddingLeft: '10px', paddingRight: '20px' }}
>
On time morning
</div>
</Box>
<Box className={classes.boxContainer} display="flex">
<div className={classes.boxColorRed}></div>
<div
style={{ paddingLeft: '10px', paddingRight: '20px' }}
>
Late morning
</div>
</Box>
<Box className={classes.boxContainer} display="flex">
<div className={classes.boxColorTeal}></div>
<div
style={{ paddingLeft: '10px', paddingRight: '20px' }}
>
On time afternoon
</div>
</Box>
<Box className={classes.boxContainer} display="flex">
<div className={classes.boxColorOrange}></div>
<div style={{ paddingLeft: '10px' }}>
Late afternoon
</div>
</Box>
</Box>
</Box>
</Tabs.Panel>
<Tabs.Panel value="detail">
<Box style={{ marginTop: 8 }}>
<DataTableAll
data={dataSummaryTracking.value}
columns={columnsDetailWorking}
size=""
height={300}
keyHighlight={'isLate'}
/>
</Box>
</Tabs.Panel>
</Tabs>
</Box>
)}
<Box className={classes.pjParticipatingContainer}>
<Title order={4}>Project Participating</Title>
</Box>
<Box className={classes.boxContainer} display="flex">
{loadingPJParticipating ? (
<Box
style={{
width: '100%',
display: loadingPJParticipating ? 'block' : 'none',
}}
>
<Box
style={{
marginTop: '10%',
textAlign: 'center',
// display: 'none',
}}
>
<Loader size={'sm'} color="green" type="bars" m={'0 auto'} />
<Text fw={600} c={'gray'}>
Analyzing . . .
</Text>
</Box>
</Box>
) : (
<DataTableAll
data={dataPJParticipating}
columns={columnsPJParticipating}
size=""
height={300}
/>
)}
</Box>
</Tabs.Panel>
</Tabs> </Tabs>
{/* Add/Edit User modal */} {/* Add/Edit User modal */}

View File

@ -14,7 +14,7 @@ import {
Modal, Modal,
MultiSelect, MultiSelect,
Text, Text,
TextInput TextInput,
} from '@mantine/core' } from '@mantine/core'
import { useForm } from '@mantine/form' import { useForm } from '@mantine/form'
import { IconEdit, IconX } from '@tabler/icons-react' import { IconEdit, IconX } from '@tabler/icons-react'

View File

@ -3,7 +3,6 @@ import { getFormDataHeader, getHeaderInfo } from '@/rtk/helpers/tokenCreator'
import { removeTokens } from '@/rtk/localStorage' import { removeTokens } from '@/rtk/localStorage'
import { notifications } from '@mantine/notifications' import { notifications } from '@mantine/notifications'
import axios from 'axios' import axios from 'axios'
import moment from 'moment'
const handleResponse = (response: any) => { const handleResponse = (response: any) => {
if (response.status === 401) { if (response.status === 401) {
removeTokens() removeTokens()
@ -158,14 +157,18 @@ export const postImage = async (url: string, body: any, method: any) => {
} }
} }
export const exportFile = async (url: string, params: any = {}, fileName: string) => { export const exportFile = async (
url: string,
params: any = {},
fileName: string,
) => {
const header = await getHeaderInfo() const header = await getHeaderInfo()
try { try {
const response = await axios.get(url, { const response = await axios.get(url, {
...header, ...header,
params, params,
responseType: 'blob' responseType: 'blob',
}) })
if (response.status === 200) { if (response.status === 200) {
@ -174,7 +177,7 @@ export const exportFile = async (url: string, params: any = {}, fileName: string
const link = document.createElement('a') const link = document.createElement('a')
link.href = downloadUrl link.href = downloadUrl
link.download = fileName link.download = fileName
document.body.appendChild(link) document.body.appendChild(link)
link.click() link.click()
window.URL.revokeObjectURL(downloadUrl) window.URL.revokeObjectURL(downloadUrl)
@ -188,7 +191,7 @@ export const exportFile = async (url: string, params: any = {}, fileName: string
return true return true
} }
throw new Error('Export failed') throw new Error('Export failed')
} catch (error: any) { } catch (error: any) {
notifications.show({ notifications.show({