diff --git a/api/DTOs/ESLOffsetDTO.cs b/api/DTOs/ESLOffsetDTO.cs index 79aa8f80..63aa4ac9 100644 --- a/api/DTOs/ESLOffsetDTO.cs +++ b/api/DTOs/ESLOffsetDTO.cs @@ -6,6 +6,7 @@ public class ESLOffsetDTO : ESLOffset { public new string TimeIn { get; set; } = default!; public new string TimeOut { get; set; } = default!; + public string UserName { get; set; } = default!; public ESLOffsetDTO(ESLOffset eslOffset) { @@ -13,6 +14,7 @@ public ESLOffsetDTO(ESLOffset eslOffset) { Id = eslOffset.Id; UserId = eslOffset.UserId; + UserName = eslOffset.User?.Name ?? string.Empty; TimeEntryId = eslOffset.TimeEntryId; TeamLeaderId = eslOffset.TeamLeaderId; TimeIn = eslOffset.TimeIn.ToString(@"hh\:mm"); @@ -20,7 +22,7 @@ public ESLOffsetDTO(ESLOffset eslOffset) Title = eslOffset.Title; Description = eslOffset.Description; IsLeaderApproved = eslOffset.IsLeaderApproved; - User = eslOffset.User; + User = eslOffset.User ?? new User(); TeamLeader = eslOffset.TeamLeader; TimeEntry = eslOffset.TimeEntry; IsUsed = eslOffset.IsUsed; diff --git a/api/DTOs/WorkInterruptionDTO.cs b/api/DTOs/WorkInterruptionDTO.cs index dce0a502..91f587a4 100644 --- a/api/DTOs/WorkInterruptionDTO.cs +++ b/api/DTOs/WorkInterruptionDTO.cs @@ -6,6 +6,7 @@ public class WorkInterruptionDTO : WorkInterruption { public new string? TimeOut { get; set; } public new string? TimeIn { get; set; } + public string? UserName { get; set; } public WorkInterruptionDTO(WorkInterruption interruption) { Id = interruption.Id; @@ -18,6 +19,7 @@ public WorkInterruptionDTO(WorkInterruption interruption) WorkInterruptionType = interruption.WorkInterruptionType; CreatedAt = interruption.CreatedAt; UpdatedAt = interruption.UpdatedAt; + UserName = interruption.TimeEntry?.User?.Name; } } } diff --git a/api/Schema/Queries/ESLOffsetQuery.cs b/api/Schema/Queries/ESLOffsetQuery.cs index 9406ea33..51970e2d 100644 --- a/api/Schema/Queries/ESLOffsetQuery.cs +++ b/api/Schema/Queries/ESLOffsetQuery.cs @@ -11,15 +11,17 @@ public ESLOffsetQuery(ESLOffsetService eslOffsetService) { _eslOffsetService = eslOffsetService; } - public async Task> GetESLOffsetsByTimeEntry(int timeEntryId, bool onlyUnused = false) { return await _eslOffsetService.GetTimeEntryOffsets(timeEntryId, onlyUnused); } - public async Task> GetAllESLOffsets(bool? isUsed = null) { return await _eslOffsetService.GetAllESLOffsets(isUsed); } + public async Task> GetAllFiledOffsets() + { + return await _eslOffsetService.GetAllFiledOffsets(); + } } } diff --git a/api/Schema/Queries/InterruptionQuery.cs b/api/Schema/Queries/InterruptionQuery.cs index 3c916218..f1ca1b09 100644 --- a/api/Schema/Queries/InterruptionQuery.cs +++ b/api/Schema/Queries/InterruptionQuery.cs @@ -21,5 +21,17 @@ public async Task> GetInterruptionsByTimeEntryId(ShowI { return await _interruptionService.Show(interruption); } + public async Task> GetAllWorkInterruptions() + { + var interruptions = await _interruptionService.GetAllInterruptions(); + + // Ensure related data is included + foreach (var interruption in interruptions) + { + await _interruptionService.IncludeRelatedData(interruption); + } + + return interruptions; + } } } diff --git a/api/Services/ESLOffsetService.cs b/api/Services/ESLOffsetService.cs index 08c875ef..b2510ae2 100644 --- a/api/Services/ESLOffsetService.cs +++ b/api/Services/ESLOffsetService.cs @@ -52,6 +52,7 @@ public async Task> GetTimeEntryOffsets(int timeEntryId, bool { var eslOffsets = await context.ESLOffsets .Include(x => x.TeamLeader) + .Include(x => x.User) .Where(x => x.TimeEntryId == timeEntryId && (onlyUnused ? x.IsUsed == false && x.IsLeaderApproved == true : true)) .OrderByDescending(x => x.CreatedAt) .Select(x => new ESLOffsetDTO(x)) @@ -67,6 +68,7 @@ public async Task> GetAllESLOffsets(bool? isUsed) { var eslOffsets = await context.ESLOffsets .Include(x => x.TeamLeader) + .Include(x => x.User) .Where(x => isUsed != null ? x.IsUsed == isUsed : true) .Select(x => new ESLOffsetDTO(x)) .ToListAsync(); @@ -74,7 +76,20 @@ public async Task> GetAllESLOffsets(bool? isUsed) return eslOffsets; } } + public async Task> GetAllFiledOffsets() + { + using (HrisContext context = _contextFactory.CreateDbContext()) + { + var filedOffsets = await context.ESLOffsets + .Include(x => x.TeamLeader) + .Include(x => x.User) + .OrderByDescending(x => x.CreatedAt) + .Select(x => new ESLOffsetDTO(x)) + .ToListAsync(); + return filedOffsets; + } + } public string GetRequestStatus(ESLOffset request) { if (request.IsLeaderApproved == true) return RequestStatus.APPROVED; diff --git a/api/Services/InterruptionService.cs b/api/Services/InterruptionService.cs index 68a9c536..e489efaa 100644 --- a/api/Services/InterruptionService.cs +++ b/api/Services/InterruptionService.cs @@ -85,5 +85,32 @@ public async Task Create(CreateInterruptionRequest interrup return new WorkInterruptionDTO(work); } } + public async Task> GetAllInterruptions() + { + using HrisContext context = _contextFactory.CreateDbContext(); +#pragma warning disable CS8602 // Dereference of a possibly null reference. + var interruptions = await context.WorkInterruptions + .Include(wi => wi.WorkInterruptionType) + .Include(wi => wi.TimeEntry) + .ThenInclude(te => te.User) + .ToListAsync(); + + return interruptions.Select(wi => new WorkInterruptionDTO(wi)).ToList(); + } + public async Task IncludeRelatedData(WorkInterruptionDTO interruption) + { + using (HrisContext context = _contextFactory.CreateDbContext()) + { + var timeEntry = await context.TimeEntries + .Include(te => te.User) + .FirstOrDefaultAsync(te => te.Id == interruption.TimeEntryId); + + if (timeEntry != null) + { + interruption.TimeEntry = timeEntry; + interruption.UserName = timeEntry.User?.Name; + } + } + } } } diff --git a/api/appsettings.json b/api/appsettings.json index 4962f42f..8598dd62 100644 --- a/api/appsettings.json +++ b/api/appsettings.json @@ -8,6 +8,9 @@ "AllowedHosts": "*", "FileSystemStorageConfig": { "Directory": "wwwroot/media", - "EnableLogging": true + "EnableLogging": true, + "ConnectionStrings": { + "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=sample;Trusted_Connection=True;" + } } } diff --git a/client/.prettierrc b/client/.prettierrc index 299a0443..8f6debb3 100644 --- a/client/.prettierrc +++ b/client/.prettierrc @@ -3,5 +3,6 @@ "tabWidth": 2, "printWidth": 100, "singleQuote": true, - "trailingComma": "none" + "trailingComma": "none", + "endOfLine": "lf" } diff --git a/client/Dockerfile b/client/Dockerfile index 891364e9..d37c1958 100644 --- a/client/Dockerfile +++ b/client/Dockerfile @@ -4,8 +4,8 @@ ENV PORT 3000 WORKDIR /usr/src/app -COPY package.json /usr/src/app -COPY package-lock.json /usr/src/app +COPY package*.json /usr/src/app +COPY package*-lock.json /usr/src/app RUN npm install diff --git a/client/package-lock.json b/client/package-lock.json index 7b0cbc4e..86fe8b0b 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -31,6 +31,7 @@ "react": "18.2.0", "react-apexcharts": "^1.4.0", "react-confirm-alert": "^3.0.6", + "react-csv": "^2.2.2", "react-dom": "18.2.0", "react-feather": "^2.0.10", "react-file-icon": "^1.3.0", @@ -50,6 +51,7 @@ "@tailwindcss/line-clamp": "^0.4.2", "@types/node": "18.11.3", "@types/react": "18.0.21", + "@types/react-csv": "^1.1.10", "@types/react-dom": "18.0.6", "@types/react-file-icon": "^1.0.1", "@typescript-eslint/eslint-plugin": "^5.47.1", @@ -1260,6 +1262,15 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-csv": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@types/react-csv/-/react-csv-1.1.10.tgz", + "integrity": "sha512-PESAyASL7Nfi/IyBR3ufd8qZkyoS+7jOylKmJxRZUZLFASLo4NZaRsJ8rNP8pCcbIziADyWBbLPD1nPddhsL4g==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-dom": { "version": "18.0.6", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.6.tgz", @@ -4641,6 +4652,11 @@ "react-dom": ">=10.0.0" } }, + "node_modules/react-csv": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/react-csv/-/react-csv-2.2.2.tgz", + "integrity": "sha512-RG5hOcZKZFigIGE8LxIEV/OgS1vigFQT4EkaHeKgyuCbUAu9Nbd/1RYq++bJcJJ9VOqO/n9TZRADsXNDR4VEpw==" + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -6501,6 +6517,15 @@ "csstype": "^3.0.2" } }, + "@types/react-csv": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@types/react-csv/-/react-csv-1.1.10.tgz", + "integrity": "sha512-PESAyASL7Nfi/IyBR3ufd8qZkyoS+7jOylKmJxRZUZLFASLo4NZaRsJ8rNP8pCcbIziADyWBbLPD1nPddhsL4g==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-dom": { "version": "18.0.6", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.6.tgz", @@ -8862,6 +8887,11 @@ "integrity": "sha512-rplP6Ed9ZSNd0KFV5BUzk4EPQ77BxsrayllBXGFuA8xPXc7sbBjgU5KUrNpl7aWFmP7mXRlVXfuy1IT5DbffYw==", "requires": {} }, + "react-csv": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/react-csv/-/react-csv-2.2.2.tgz", + "integrity": "sha512-RG5hOcZKZFigIGE8LxIEV/OgS1vigFQT4EkaHeKgyuCbUAu9Nbd/1RYq++bJcJJ9VOqO/n9TZRADsXNDR4VEpw==" + }, "react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", diff --git a/client/package.json b/client/package.json index 947a8c1e..2c3b1f00 100644 --- a/client/package.json +++ b/client/package.json @@ -37,6 +37,7 @@ "react": "18.2.0", "react-apexcharts": "^1.4.0", "react-confirm-alert": "^3.0.6", + "react-csv": "^2.2.2", "react-dom": "18.2.0", "react-feather": "^2.0.10", "react-file-icon": "^1.3.0", @@ -56,6 +57,7 @@ "@tailwindcss/line-clamp": "^0.4.2", "@types/node": "18.11.3", "@types/react": "18.0.21", + "@types/react-csv": "^1.1.10", "@types/react-dom": "18.0.6", "@types/react-file-icon": "^1.0.1", "@typescript-eslint/eslint-plugin": "^5.47.1", diff --git a/client/src/graphql/queries/eslFiledOffsets.ts b/client/src/graphql/queries/eslFiledOffsets.ts index 0a51258a..5ab4350a 100644 --- a/client/src/graphql/queries/eslFiledOffsets.ts +++ b/client/src/graphql/queries/eslFiledOffsets.ts @@ -13,6 +13,27 @@ export const GET_ALL_ESL_FILED_OFFSETS = gql` id name } + userName + description + isLeaderApproved + isUsed + } + } +` +export const GET_ALL_FILED_OFFSETS = gql` + { + allFiledOffsets { + id + title + timeIn + timeOut + createdAt + updatedAt + teamLeader { + id + name + } + userName description isLeaderApproved isUsed diff --git a/client/src/graphql/queries/workInterruptionQuery.ts b/client/src/graphql/queries/workInterruptionQuery.ts index fcb041ce..e90f0343 100644 --- a/client/src/graphql/queries/workInterruptionQuery.ts +++ b/client/src/graphql/queries/workInterruptionQuery.ts @@ -25,3 +25,22 @@ export const GET_ALL_WORK_INTERRUPTIONS_QUERY = gql` } } ` +export const GET_ALL_INTERRUPTIONS_QUERY = gql` + { + allWorkInterruptions { + userName + id + timeOut + timeIn + otherReason + remarks + workInterruptionType { + id + name + } + workInterruptionTypeId + timeEntryId + createdAt + } + } +` diff --git a/client/src/hooks/useFileOffset.ts b/client/src/hooks/useFileOffset.ts index 35fe23a4..9dd3c6a6 100644 --- a/client/src/hooks/useFileOffset.ts +++ b/client/src/hooks/useFileOffset.ts @@ -2,7 +2,7 @@ import toast from 'react-hot-toast' import { useQuery, useMutation, UseQueryResult, UseMutationResult } from '@tanstack/react-query' import { client } from '~/utils/shared/client' -import { GET_ALL_ESL_FILED_OFFSETS } from '~/graphql/queries/eslFiledOffsets' +import { GET_ALL_ESL_FILED_OFFSETS, GET_ALL_FILED_OFFSETS } from '~/graphql/queries/eslFiledOffsets' import { IFiledOffsetData, IFileOffset } from '~/utils/interfaces/fileOffsetInterface' import { CREATE_FILE_OFFSET_MUTATION } from '~/graphql/mutations/fileOffsetMutation' @@ -11,10 +11,15 @@ type FiledOffsetFuncReturnType = UseQueryResult< { eslOffsetsByTimeEntry: IFiledOffsetData[] }, unknown > +type AllFiledOffsetsFuncReturnType = UseQueryResult< + { allFiledOffsets: IFiledOffsetData[] }, + unknown +> type HookReturnType = { handleAddFileOffsetMutation: () => FileOffsetFuncReturnType getESLFiledOffsetsQuery: (timeEntryId: number) => FiledOffsetFuncReturnType + getAllFiledOffsetsQuery: () => AllFiledOffsetsFuncReturnType } const useFileOffset = (): HookReturnType => { @@ -36,9 +41,17 @@ const useFileOffset = (): HookReturnType => { select: (data: { eslOffsetsByTimeEntry: IFiledOffsetData[] }) => data }) + const getAllFiledOffsetsQuery = (): AllFiledOffsetsFuncReturnType => + useQuery({ + queryKey: ['GET_ALL_FILED_OFFSETS'], + queryFn: async () => await client.request(GET_ALL_FILED_OFFSETS), + select: (data: { allFiledOffsets: IFiledOffsetData[] }) => data + }) + return { handleAddFileOffsetMutation, - getESLFiledOffsetsQuery + getESLFiledOffsetsQuery, + getAllFiledOffsetsQuery } } diff --git a/client/src/hooks/useInterruptionType.ts b/client/src/hooks/useInterruptionType.ts index f318657f..ec4820d5 100644 --- a/client/src/hooks/useInterruptionType.ts +++ b/client/src/hooks/useInterruptionType.ts @@ -10,7 +10,8 @@ import { toast } from 'react-hot-toast' import { client } from '~/utils/shared/client' import { GET_ALL_WORK_INTERRUPTIONS_QUERY, - GET_INTERRUPTION_TYPES_QUERY + GET_INTERRUPTION_TYPES_QUERY, + GET_ALL_INTERRUPTIONS_QUERY } from '~/graphql/queries/workInterruptionQuery' import { CREATE_INTERRUPTION_MUTATION, @@ -61,6 +62,7 @@ type returnType = { DeleteInterruptionRequest, unknown > + useAllWorkInterruptions: () => UseQueryResult } type handleInterruptionTypeQueryType = UseQueryResult type handleGetAllWorkInterruptionsQueryType = UseQueryResult @@ -142,12 +144,22 @@ const useInterruptionType = (): returnType => { toast.error('Something went wrong') } }) + const useAllWorkInterruptions = (): UseQueryResult => { + return useQuery({ + queryKey: ['GET_ALL_INTERRUPTIONS_QUERY'], + queryFn: async () => { + const data = await client.request(GET_ALL_INTERRUPTIONS_QUERY) + return data + } + }) + } return { handleInterruptionTypeQuery, handleInterruptionMutation, handleGetAllWorkInterruptionsQuery, handleUpdateInterruptionMutation, - handleDeleteInterruptionMutation + handleDeleteInterruptionMutation, + useAllWorkInterruptions } } diff --git a/client/src/pages/dtr-management.tsx b/client/src/pages/dtr-management.tsx index 66b81fc4..613e6bf3 100644 --- a/client/src/pages/dtr-management.tsx +++ b/client/src/pages/dtr-management.tsx @@ -4,6 +4,9 @@ import classNames from 'classnames' import { useRouter } from 'next/router' import { PulseLoader } from 'react-spinners' import React, { useEffect, useState } from 'react' +import { CSVLink } from 'react-csv' +import useInterruptionType from '~/hooks/useInterruptionType' +import useFileOffset from '~/hooks/useFileOffset' import NotFound from './404' import useUserQuery from '~/hooks/useUserQuery' @@ -160,6 +163,24 @@ const DTRManagement: NextPage = (): JSX.Element => { } } + const { useAllWorkInterruptions } = useInterruptionType() + const workInterruption = useAllWorkInterruptions() + + const [fetchedWorkInterruptionsData, setFetchedWorkInterruptionsData] = useState({ + data: workInterruption.data, + error: workInterruption.error, + isLoading: workInterruption.isLoading + }) + + const { getAllFiledOffsetsQuery } = useFileOffset() + const filedOffset = getAllFiledOffsetsQuery() + + const [fetchedFiledOffsetData, setFetchedFiledOffsetData] = useState({ + data: filedOffset.data, + error: filedOffset.error, + isLoading: filedOffset.isLoading + }) + const setFetchedData = (data: any, setState: any): void => { if (data !== undefined) { setState({ @@ -169,6 +190,17 @@ const DTRManagement: NextPage = (): JSX.Element => { }) } } + const getSummaryFilename = (): string => { + const startDate = moment(filters.startDate) + const endDate = moment(filters.endDate) + const dateRange = + startDate.date() === 1 && endDate.date() === 15 + ? '1-15' + : startDate.date() === 16 + ? '16-31' + : `${startDate.date()}-${endDate.date()}` + return `Summary-${startDate.format('MMM-YYYY')}-(${dateRange}).csv` + } useEffect(() => { if (router.isReady) { @@ -216,10 +248,74 @@ const DTRManagement: NextPage = (): JSX.Element => { setFetchedData(summary, setFetchedSummaryData) }, [summary.data]) + useEffect(() => { + setFetchedWorkInterruptionsData({ + data: workInterruption.data, + error: workInterruption.error, + isLoading: workInterruption.isLoading + }) + }, [workInterruption.data, workInterruption.error, workInterruption.isLoading]) + + useEffect(() => { + setFetchedFiledOffsetData({ + data: filedOffset.data, + error: filedOffset.error, + isLoading: filedOffset.isLoading + }) + }, [filedOffset.data, filedOffset.error, filedOffset.isLoading]) + if (process.env.NODE_ENV === 'production' && currentUser?.userById.role.name !== Roles.HR_ADMIN) { return } + const SummaryHeaders = [ + { label: 'Name', key: 'user.name' }, + { label: 'Leave(days)', key: 'leave' }, + { label: 'Abscenses', key: 'absences' }, + { label: 'Late', key: 'late' }, + { label: 'Undertime(min)', key: 'undertime' }, + { label: 'Overtime(min)', key: 'overtime' } + ] + const DTRheaders = [ + { label: 'Date', key: 'date' }, + { label: 'Name', key: 'user.name' }, + { label: 'Status', key: 'status' }, + { label: 'Time In', key: 'timeIn.timeHour' }, + { label: 'Time In Remarks', key: 'timeIn.remarks' }, + { label: 'Time Out', key: 'timeOut.timeHour' }, + { label: 'Time Out Remarks', key: 'timeOut.remarks' }, + { label: 'Start Time', key: 'startTime' }, + { label: 'End Time', key: 'endTime' }, + { label: 'Work Hours', key: 'workedHours' }, + { label: 'Late(min)', key: 'late' }, + { label: 'Undertime(min)', key: 'undertime' }, + { label: 'Overtime(min)', key: 'overtime.approvedMinutes' } + ] + + const InterruptionHeader = [ + { label: 'Name', key: 'userName' }, + { label: 'ID', key: 'id' }, + { label: 'Time Out', key: 'timeOut' }, + { label: 'Time In', key: 'timeIn' }, + { label: 'Other Reason', key: 'otherReason' }, + { label: 'Remarks', key: 'remarks' }, + { label: 'Work Interruption Type ID', key: 'workInterruptionTypeId' }, + { label: 'Work Interruption Type', key: 'workInterruptionType.name' }, + { label: 'Time Entry ID', key: 'timeEntryId' }, + { label: 'Created At', key: 'createdAt' } + ] + + const FiledOffsetHeader = [ + { label: 'Name', key: 'userName' }, + { label: 'Title', key: 'title' }, + { label: 'Time Out', key: 'timeOut' }, + { label: 'Time In', key: 'timeIn' }, + { label: 'Team Leader', key: 'teamLeader.name' }, + { label: 'Status', key: 'isLeaderApproved' }, + { label: 'Is Used', key: 'isUsed' }, + { label: 'Remarks', key: 'description' } + ] + return (
@@ -246,7 +342,7 @@ const DTRManagement: NextPage = (): JSX.Element => { placeholder="Search" />
-
+
{ } }} /> + + + + + + + + + + +
diff --git a/client/src/utils/types/workInterruptionTypes.ts b/client/src/utils/types/workInterruptionTypes.ts index 868b3148..9fd36c91 100644 --- a/client/src/utils/types/workInterruptionTypes.ts +++ b/client/src/utils/types/workInterruptionTypes.ts @@ -6,7 +6,17 @@ export type InterruptionType = { } export type WorkInterruptionType = { - allWorkInterruptionTypes: InterruptionType[] + allWorkInterruptionTypes: Array<{ + id: number + name: string + createdAt: string + timeEntryId: number + workInterruptionType: { name: string } + otherReason: string + timeOut: string + timeIn: string + remarks: string + }> } export type WorkInterruption = { @@ -19,4 +29,5 @@ export type WorkInterruption = { } export type WorkInterruptions = { interruptionsByTimeEntryId: IInterruptionTimeEntry[] + allWorkInterruptions: IInterruptionTimeEntry[] }