Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,19 @@ public ResponseEntity<Page<History>> getHistoryById(
HttpServletRequest request,
@RequestParam(defaultValue = "0") int pageNumber,
@RequestParam(defaultValue = "10") int pageSize,
@RequestParam(defaultValue = "") String search
@RequestParam(defaultValue = "") String search,
@RequestParam(required = false) String type,
@RequestParam(required = false) String lessonName,
@RequestParam(required = false) String dateFrom,
@RequestParam(required = false) String dateTo
) {
return ResponseEntity.ok().body(
historyService.getAllHistory((String) request.getAttribute("vkUserId"),
search,
type,
lessonName,
dateFrom,
dateTo,
pageNumber,
pageSize
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,21 @@ public interface HistoryRepository extends CrudRepository<History, UUID> {
"AND (LOWER(h.title) LIKE LOWER(concat('%', :substring, '%')) " +
"OR LOWER(m.content) LIKE LOWER(concat('%', :substring, '%')))")
Page<History> findByVkUserIdAndByLessonNameOrMessageContentContainingIgnoreCase(@Param("vkId") UUID vkId, @Param("substring") String substring, PageRequest pageable);

@Query("SELECT DISTINCT h FROM History h " +
"INNER JOIN Message m ON h.id = m.history.id " +
"WHERE h.vkUser.id = :vkId " +
"AND (:substring IS NULL OR :substring = '' OR LOWER(h.title) LIKE LOWER(concat('%', :substring, '%')) " +
"OR LOWER(m.content) LIKE LOWER(concat('%', :substring, '%'))) " +
"AND (:type IS NULL OR h.type = :type) " +
"AND (:lessonName IS NULL OR h.lessonName = :lessonName) " +
"AND (:dateFrom IS NULL OR h.lastUpdated >= CAST(:dateFrom AS timestamp)) " +
"AND (:dateTo IS NULL OR h.lastUpdated <= CAST(:dateTo AS timestamp))")
Page<History> findByVkUserIdWithFilters(@Param("vkId") UUID vkId,
@Param("substring") String substring,
@Param("type") String type,
@Param("lessonName") String lessonName,
@Param("dateFrom") String dateFrom,
@Param("dateTo") String dateTo,
PageRequest pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,14 @@ public History createHistory(String vkUserId, CreateHistoryRequest createHistory
}


public Page<History> getAllHistory(String vkUserId, String substring, int pageNumber, int pageSize) {
public Page<History> getAllHistory(String vkUserId, String substring, String type, String lessonName, String dateFrom, String dateTo, int pageNumber, int pageSize) {
PageRequest pageable = PageRequest.of(pageNumber, pageSize, Sort.by("lastUpdated").descending());
var user = userService.getOrCreateVkUser(vkUserId);
return historyRepository.findByVkUserIdAndByLessonNameOrMessageContentContainingIgnoreCase(user.getId(), substring, pageable);
return historyRepository.findByVkUserIdWithFilters(user.getId(), substring, type, lessonName, dateFrom, dateTo, pageable);
}

public Page<History> getAllHistory(String vkUserId, String substring, int pageNumber, int pageSize) {
return getAllHistory(vkUserId, substring, null, null, null, null, pageNumber, pageSize);
}

public void deleteHistory(String vkUserId, UUID historyId) {
Expand Down
32 changes: 21 additions & 11 deletions GPTutor-Frontend/src/api/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,28 @@ export function createHistory(params: HistoryCreate): Promise<History> {

export function getHistoryById(
pageNumber: number,
search: string
search: string,
type?: string,
lessonName?: string,
dateFrom?: string,
dateTo?: string
): Promise<Pageable<History>> {
return fetch(
`${BACKEND_HOST}history?pageNumber=${pageNumber}&search=${search}`,
{
method: "GET",
headers: {
Authorization: httpService.authorization,
"Content-Type": "application/json",
},
}
).then((res) => res.json());
const params = new URLSearchParams();
params.append("pageNumber", pageNumber.toString());
params.append("search", search);

if (type) params.append("type", type);
if (lessonName) params.append("lessonName", lessonName);
if (dateFrom) params.append("dateFrom", dateFrom);
if (dateTo) params.append("dateTo", dateTo);

return fetch(`${BACKEND_HOST}history?${params.toString()}`, {
method: "GET",
headers: {
Authorization: httpService.authorization,
"Content-Type": "application/json",
},
}).then((res) => res.json());
}

export function deleteHistory(id: string) {
Expand Down
44 changes: 42 additions & 2 deletions GPTutor-Frontend/src/entity/GPT/GptHistoryDialogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export class GptHistoryDialogs {

dialogs = sig<History[]>([]);
searchValue$ = sig("");
typeFilter$ = sig<string | null>(null);
lessonNameFilter$ = sig<string | null>(null);
dateFromFilter$ = sig<string | null>(null);
dateToFilter$ = sig<string | null>(null);

pageNumber = 0;

Expand All @@ -37,7 +41,11 @@ export class GptHistoryDialogs {
this.pageNumber = 0;
const history = await this.getHistory$.run(
this.pageNumber,
this.searchValue$.get().trim()
this.searchValue$.get().trim(),
this.typeFilter$.get() || undefined,
this.lessonNameFilter$.get() || undefined,
this.dateFromFilter$.get() || undefined,
this.dateToFilter$.get() || undefined
);
this.dialogs.set(history.content);
}
Expand All @@ -48,7 +56,11 @@ export class GptHistoryDialogs {
this.pageNumber++;
const history = await this.getHistory$.run(
this.pageNumber,
this.searchValue$.get().trim()
this.searchValue$.get().trim(),
this.typeFilter$.get() || undefined,
this.lessonNameFilter$.get() || undefined,
this.dateFromFilter$.get() || undefined,
this.dateToFilter$.get() || undefined
);

this.dialogs.set([...this.dialogs.get(), ...history.content]);
Expand All @@ -59,6 +71,34 @@ export class GptHistoryDialogs {
this.searchValue$.set(value);
};

setTypeFilter = (type: string | null) => {
this.typeFilter$.set(type);
};

setLessonNameFilter = (lessonName: string | null) => {
this.lessonNameFilter$.set(lessonName);
};

setDateFromFilter = (dateFrom: string | null) => {
this.dateFromFilter$.set(dateFrom);
};

setDateToFilter = (dateTo: string | null) => {
this.dateToFilter$.set(dateTo);
};

clearAllFilters = () => {
this.searchValue$.set("");
this.typeFilter$.set(null);
this.lessonNameFilter$.set(null);
this.dateFromFilter$.set(null);
this.dateToFilter$.set(null);
};

applyFilters = async () => {
await this.loadHistory();
};

search = async () => {
await this.loadHistory();
};
Expand Down
23 changes: 21 additions & 2 deletions GPTutor-Frontend/src/panels/History/History.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";

import {
Div,
IconButton,
Panel,
PanelHeaderBack,
Search,
Spinner,
Title,
} from "@vkontakte/vkui";
import { Icon28FilterOutline } from "@vkontakte/icons";

import { AppContainer } from "$/components/AppContainer";
import { useNavigationContext } from "$/NavigationContext";
Expand All @@ -17,6 +19,7 @@ import { chatGpt } from "$/entity/GPT";
import { AppPanelHeader } from "$/components/AppPanelHeader";

import { HistoryDelete } from "./HistoryDelete";
import { HistoryFilter } from "./HistoryFilter";

import classes from "./History.module.css";
import useDebounce from "$/hooks/useDebounce";
Expand All @@ -26,6 +29,8 @@ interface IProps {
}

function History({ id }: IProps) {
const [showFilter, setShowFilter] = useState(false);

const pageNumber = chatGpt.history.pageNumber;
const loading = chatGpt.history.getHistory$.loading.get();
const hasNextPage = chatGpt.history.hasNextHistory$.get();
Expand All @@ -51,14 +56,28 @@ function History({ id }: IProps) {
headerChildren={
<AppPanelHeader
before={<PanelHeaderBack onClick={goBack} />}
after={<HistoryDelete />}
after={
<>
<IconButton
onClick={() => setShowFilter(!showFilter)}
aria-label="Открыть фильтры"
>
<Icon28FilterOutline />
</IconButton>
<HistoryDelete />
</>
}
>
<Title level="1" Component="h1">
История
</Title>
</AppPanelHeader>
}
>
<HistoryFilter
isOpen={showFilter}
onClose={() => setShowFilter(false)}
/>
<Search
placeholder="Поиск по сообщениям"
value={chatGpt.history.searchValue$.get()}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.filterContainer {
padding: 16px;
background: var(--vkui--color_background_content);
border-radius: 12px;
margin-bottom: 16px;
}

.buttonGroup {
margin-top: 16px;
display: flex;
gap: 8px;
flex-wrap: wrap;
}

.buttonGroup button {
flex: 1;
min-width: max-content;
}

@media (max-width: 768px) {
.buttonGroup {
flex-direction: column;
}
}
128 changes: 128 additions & 0 deletions GPTutor-Frontend/src/panels/History/HistoryFilter/HistoryFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import React from "react";
import {
Button,
ButtonGroup,
FormItem,
Group,
Header,
Input,
Select,
} from "@vkontakte/vkui";
import { chatGpt } from "$/entity/GPT";
import { ModeType } from "$/entity/lessons";

import classes from "./HistoryFilter.module.css";

const typeOptions = [
{ label: "Все типы", value: "" },
{ label: "Свободный чат", value: "Free" },
{ label: "JavaScript", value: ModeType.JS },
{ label: "React", value: ModeType.React },
{ label: "TypeScript", value: ModeType.Typescript },
{ label: "Vue", value: ModeType.Vue },
{ label: "Git", value: ModeType.Git },
{ label: "HTML/CSS", value: ModeType.HTMLCSS },
{ label: "Go", value: ModeType.Go },
{ label: "HTML/CSS Собеседование", value: ModeType.HTMLCSS_INTERVIEW },
{ label: "React Собеседование", value: ModeType.REACT_INTERVIEW },
{ label: "JavaScript Собеседование", value: ModeType.JAVASCRIPT_INTERVIEW },
{ label: "LeetCode", value: ModeType.LeetCode },
{ label: "JS Тренировка", value: ModeType.JS_TRAINING },
{ label: "Python Тренировка", value: ModeType.PYTHON_TRAINING },
{ label: "Go Тренировка", value: ModeType.GO_TRAINING },
];

interface IProps {
isOpen: boolean;
onClose: () => void;
}

function HistoryFilter({ isOpen, onClose }: IProps) {
if (!isOpen) return null;

const typeFilter = chatGpt.history.typeFilter$.get();
const dateFromFilter = chatGpt.history.dateFromFilter$.get();
const dateToFilter = chatGpt.history.dateToFilter$.get();

const handleApplyFilters = async () => {
await chatGpt.history.applyFilters();
onClose();
};

const handleClearFilters = async () => {
chatGpt.history.clearAllFilters();
await chatGpt.history.applyFilters();
onClose();
};

return (
<Group className={classes.filterContainer}>
<Header>Фильтры истории</Header>

<FormItem htmlFor="type-filter" top="Тип диалога">
<Select
id="type-filter"
value={typeFilter || ""}
onChange={(e) => chatGpt.history.setTypeFilter(e.target.value || null)}
options={typeOptions}
/>
</FormItem>

<FormItem htmlFor="date-from-filter" top="С даты">
<Input
id="date-from-filter"
type="date"
value={dateFromFilter || ""}
onChange={(e) =>
chatGpt.history.setDateFromFilter(
e.target.value || null
)
}
/>
</FormItem>

<FormItem htmlFor="date-to-filter" top="По дату">
<Input
id="date-to-filter"
type="date"
value={dateToFilter || ""}
onChange={(e) =>
chatGpt.history.setDateToFilter(
e.target.value || null
)
}
/>
</FormItem>

<ButtonGroup className={classes.buttonGroup}>
<Button
size="l"
appearance="positive"
onClick={handleApplyFilters}
disabled={chatGpt.history.getHistory$.loading.get()}
>
Применить фильтры
</Button>
<Button
size="l"
appearance="neutral"
mode="outline"
onClick={handleClearFilters}
disabled={chatGpt.history.getHistory$.loading.get()}
>
Сбросить
</Button>
<Button
size="l"
appearance="neutral"
mode="tertiary"
onClick={onClose}
>
Отмена
</Button>
</ButtonGroup>
</Group>
);
}

export default HistoryFilter;
1 change: 1 addition & 0 deletions GPTutor-Frontend/src/panels/History/HistoryFilter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as HistoryFilter } from './HistoryFilter';
Loading