Skip to content

Commit

Permalink
feat: add projects select to analytics page gf-348 (#470)
Browse files Browse the repository at this point in the history
* feat: add a select component to the analytics page gf-348

* feat: add the ability to get activity logs by ID project on the backend gf-348

* feat: add projects select to analytics page gf-348

* fix: remove from remove the project from the array dependencie in the useEffect gf-348

* feat: make queries is optional gf-348

* fix: change type and code style gf-348

* fix: improve query gf-348

* fix: change folder structure gf-348

* fix: change projects store gf-348

* fix: create a separate request to receive all projects gf-348

* fix: delete route /all gf-348
  • Loading branch information
AnutkaGn authored Sep 24, 2024
1 parent 8760d76 commit 47d8620
Show file tree
Hide file tree
Showing 12 changed files with 124 additions and 16 deletions.
17 changes: 12 additions & 5 deletions apps/backend/src/modules/projects/project.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,12 +232,19 @@ class ProjectController extends BaseController {
): Promise<APIHandlerResponse> {
const { name, page, pageSize } = options.query;

if (page && pageSize) {
return {
payload: await this.projectService.findAll({
name,
page: Number(page),
pageSize: Number(pageSize),
}),
status: HTTPCode.OK,
};
}

return {
payload: await this.projectService.findAll({
name,
page: Number(page),
pageSize: Number(pageSize),
}),
payload: await this.projectService.findAllWithoutPagination(),
status: HTTPCode.OK,
};
}
Expand Down
9 changes: 9 additions & 0 deletions apps/backend/src/modules/projects/project.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ class ProjectRepository implements Repository {
};
}

public async findAllWithoutPagination(): Promise<ProjectEntity[]> {
const projects = await this.projectModel
.query()
.orderBy("created_at", SortType.DESCENDING)
.execute();

return projects.map((project) => ProjectEntity.initialize(project));
}

public async findByName(name: string): Promise<null | ProjectEntity> {
const item = await this.projectModel.query().findOne({ name });

Expand Down
8 changes: 8 additions & 0 deletions apps/backend/src/modules/projects/project.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,14 @@ class ProjectService implements Service {
};
}

public async findAllWithoutPagination(): Promise<
ProjectGetAllItemResponseDto[]
> {
const projects = await this.projectRepository.findAllWithoutPagination();

return projects.map((project) => project.toObject());
}

public async findInactiveProjects(
thresholdInDays: number,
): Promise<ProjectGetAllItemResponseDto[]> {
Expand Down
4 changes: 3 additions & 1 deletion apps/frontend/src/libs/components/select/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import styles from "./styles.module.css";
type Properties<TFieldValues extends FieldValues, TOptionValue> = {
background?: "primary" | "secondary";
control: Control<TFieldValues, null>;
isClearable?: boolean;
isLabelHidden?: boolean;
isMulti?: boolean;
isSearchable?: boolean;
Expand All @@ -30,6 +31,7 @@ type Properties<TFieldValues extends FieldValues, TOptionValue> = {
const Select = <TFieldValues extends FieldValues, TOptionValue>({
background = "secondary",
control,
isClearable = false,
isLabelHidden = false,
isMulti = false,
isSearchable = false,
Expand Down Expand Up @@ -122,7 +124,7 @@ const Select = <TFieldValues extends FieldValues, TOptionValue>({
),
valueContainer: () => styles["value-container"] as string,
}}
isClearable={false}
isClearable={isClearable}
isMulti={isMulti}
isSearchable={isSearchable}
name={name}
Expand Down
13 changes: 12 additions & 1 deletion apps/frontend/src/modules/activity/slices/actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createAsyncThunk } from "@reduxjs/toolkit";

import { type AsyncThunkConfig } from "~/libs/types/types.js";
import { type ProjectGetAllItemResponseDto } from "~/modules/projects/projects.js";

import {
type ActivityLogGetAllAnalyticsResponseDto,
Expand All @@ -18,4 +19,14 @@ const loadAll = createAsyncThunk<
return await activityLogApi.getAll(query);
});

export { loadAll };
const loadAllProjects = createAsyncThunk<
ProjectGetAllItemResponseDto[],
undefined,
AsyncThunkConfig
>(`${sliceName}/load-all-projects`, async (_, { extra }) => {
const { projectApi } = extra;

return await projectApi.getAllWithoutPagination();
});

export { loadAll, loadAllProjects };
17 changes: 16 additions & 1 deletion apps/frontend/src/modules/activity/slices/activity.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@ import { createSlice } from "@reduxjs/toolkit";

import { DataStatus } from "~/libs/enums/enums.js";
import { type ValueOf } from "~/libs/types/types.js";
import { type ProjectGetAllItemResponseDto } from "~/modules/projects/projects.js";

import { type ActivityLogGetAllItemAnalyticsResponseDto } from "../libs/types/types.js";
import { loadAll } from "./actions.js";
import { loadAll, loadAllProjects } from "./actions.js";

type State = {
activityLogs: ActivityLogGetAllItemAnalyticsResponseDto[];
dataStatus: ValueOf<typeof DataStatus>;
projects: ProjectGetAllItemResponseDto[];
};

const initialState: State = {
activityLogs: [],
dataStatus: DataStatus.IDLE,
projects: [],
};

const { actions, name, reducer } = createSlice({
Expand All @@ -29,6 +32,18 @@ const { actions, name, reducer } = createSlice({
state.activityLogs = [];
state.dataStatus = DataStatus.REJECTED;
});

builder.addCase(loadAllProjects.pending, (state) => {
state.dataStatus = DataStatus.PENDING;
});
builder.addCase(loadAllProjects.fulfilled, (state, action) => {
state.projects = action.payload;
state.dataStatus = DataStatus.FULFILLED;
});
builder.addCase(loadAllProjects.rejected, (state) => {
state.projects = [];
state.dataStatus = DataStatus.REJECTED;
});
},
initialState,
name: "activity",
Expand Down
3 changes: 2 additions & 1 deletion apps/frontend/src/modules/activity/slices/activity.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { loadAll } from "./actions.js";
import { loadAll, loadAllProjects } from "./actions.js";
import { actions } from "./activity.slice.js";

const allActions = {
...actions,
loadAll,
loadAllProjects,
};

export { allActions as actions };
Expand Down
14 changes: 14 additions & 0 deletions apps/frontend/src/modules/projects/projects-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,20 @@ class ProjectApi extends BaseHTTPApi {
return await response.json<ProjectGetAllResponseDto>();
}

public async getAllWithoutPagination(): Promise<
ProjectGetAllItemResponseDto[]
> {
const response = await this.load(
this.getFullEndpoint(ProjectsApiPath.ROOT, {}),
{
hasAuth: true,
method: "GET",
},
);

return await response.json<ProjectGetAllItemResponseDto[]>();
}

public async getById(payload: {
id: string;
}): Promise<ProjectGetByIdResponseDto> {
Expand Down
36 changes: 29 additions & 7 deletions apps/frontend/src/pages/analytics/analytics.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DateInput, PageLayout } from "~/libs/components/components.js";
import { DateInput, PageLayout, Select } from "~/libs/components/components.js";
import { DataStatus } from "~/libs/enums/enums.js";
import { subtractDays } from "~/libs/helpers/helpers.js";
import {
Expand All @@ -16,6 +16,7 @@ import {
ANALYTICS_DATE_MAX_RANGE,
ANALYTICS_LOOKBACK_DAYS_COUNT,
} from "./libs/constants/constants.js";
import { getProjectOptions } from "./libs/helpers/helpers.js";
import styles from "./styles.module.css";

const Analytics = (): JSX.Element => {
Expand All @@ -26,29 +27,36 @@ const Analytics = (): JSX.Element => {
ANALYTICS_LOOKBACK_DAYS_COUNT,
);

const { activityLogs, dataStatus } = useAppSelector(
const { activityLogs, dataStatus, projects } = useAppSelector(
({ activityLogs }) => activityLogs,
);

useEffect(() => {
void dispatch(activityLogActions.loadAllProjects());
}, [dispatch]);

const { control, handleSubmit, isDirty } = useAppForm({
defaultValues: {
dateRange: [
subtractDays(todayDate, ANALYTICS_DATE_MAX_RANGE),
todayDate,
] as [Date, Date],
project: null,
},
});

const dateRangeValue = useFormWatch({ control, name: "dateRange" });
const projectValue = useFormWatch({ control, name: "project" });

const handleLoadLogs = useCallback(
([startDate, endDate]: [Date, Date]) => {
([startDate, endDate]: [Date, Date], projectId?: null | string) => {
const formattedStartDate = startDate.toISOString();
const formattedEndDate = endDate.toISOString();

void dispatch(
activityLogActions.loadAll({
endDate: formattedEndDate,
projectId: projectId ?? undefined,
startDate: formattedStartDate,
}),
);
Expand All @@ -57,23 +65,25 @@ const Analytics = (): JSX.Element => {
);

useEffect(() => {
handleLoadLogs(dateRangeValue);
}, [dateRangeValue, handleLoadLogs]);
handleLoadLogs(dateRangeValue, projectValue);
}, [dateRangeValue, projectValue, handleLoadLogs]);

const handleFormSubmit = useCallback(
(event_?: React.BaseSyntheticEvent): void => {
void handleSubmit((formData) => {
handleLoadLogs(formData.dateRange);
handleLoadLogs(formData.dateRange, formData.project);
})(event_);
},
[handleLoadLogs, handleSubmit],
);

const projectOptions = getProjectOptions(projects);

useEffect(() => {
if (isDirty) {
handleFormSubmit();
}
}, [dateRangeValue, isDirty, handleFormSubmit]);
}, [dateRangeValue, projectValue, isDirty, handleFormSubmit]);

const isLoading =
dataStatus === DataStatus.IDLE || dataStatus === DataStatus.PENDING;
Expand All @@ -83,6 +93,18 @@ const Analytics = (): JSX.Element => {
<h1 className={styles["title"]}>Analytics</h1>
<section>
<form className={styles["filters-form"]} onSubmit={handleFormSubmit}>
<div className={styles["select-wrapper"]}>
<Select
control={control}
isClearable
isLabelHidden
isSearchable
label="Select project"
name="project"
options={projectOptions}
placeholder="Select project"
/>
</div>
<DateInput
control={control}
maxDate={todayDate}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { type SelectOption } from "~/libs/types/select-option.type.js";
import { type ProjectGetAllItemResponseDto } from "~/modules/projects/projects.js";

const getProjectOptions = (
projects: ProjectGetAllItemResponseDto[],
): SelectOption<number>[] =>
projects.map((project) => ({
label: project.name,
value: project.id,
}));

export { getProjectOptions };
1 change: 1 addition & 0 deletions apps/frontend/src/pages/analytics/libs/helpers/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { getAnalyticsColumns } from "./get-analytics-columns/get-analytics-columns.helper.jsx";
export { getAnalyticsRows } from "./get-analytics-rows/get-analytics-rows.helper.js";
export { getProjectOptions } from "./get-project-options/get-project-options.helper.js";
6 changes: 6 additions & 0 deletions apps/frontend/src/pages/analytics/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,11 @@

.filters-form {
display: flex;
gap: 12px;
align-items: center;
margin-top: 24px;
}

.select-wrapper {
min-width: 270px;
}

0 comments on commit 47d8620

Please sign in to comment.