diff --git a/packages/client/src/api/hooks/categoryHooks.ts b/packages/client/src/api/hooks/categoryHooks.ts index 5b05377..cacfde7 100644 --- a/packages/client/src/api/hooks/categoryHooks.ts +++ b/packages/client/src/api/hooks/categoryHooks.ts @@ -1,9 +1,17 @@ -import { Category, CategoryWithCriteria, CreateCategory, CreateResponse, EntityWithEditableIndicator, PontozoError } from '@pontozo/common' +import { + Category, + CategoryWithCriteria, + CategoryWithSeasons, + CreateCategory, + CreateResponse, + EntityWithEditableIndicator, + PontozoError, +} from '@pontozo/common' import { useMutation, useQuery } from '@tanstack/react-query' import { functionAxios } from '../../util/axiosConfig' export const useFetchCategories = () => { - return useQuery(['fetchCategories'], async () => (await functionAxios.get(`/categories`)).data, { + return useQuery(['fetchCategories'], async () => (await functionAxios.get(`/categories`)).data, { retry: false, }) } @@ -35,3 +43,7 @@ export const useUpdateCategoryMutation = (categoryId: number) => { export const useDeleteCategoryMutation = (categoryId: number) => { return useMutation(async () => (await functionAxios.delete(`/categories/${categoryId}`)).data) } + +export const useDuplicateCategoryMutation = (categoryId: number) => { + return useMutation(async () => (await functionAxios.post(`/categories/${categoryId}/duplicate`)).data) +} diff --git a/packages/client/src/api/hooks/seasonHooks.ts b/packages/client/src/api/hooks/seasonHooks.ts index ff21597..877bceb 100644 --- a/packages/client/src/api/hooks/seasonHooks.ts +++ b/packages/client/src/api/hooks/seasonHooks.ts @@ -33,3 +33,7 @@ export const useUpdateSeasonMutation = (seasonId: number) => { export const useDeleteSeasonMutation = (seasonId: number) => { return useMutation(async () => (await functionAxios.delete(`/seasons/${seasonId}`)).data) } + +export const useDuplicateSeasonMutation = (seasonId: number) => { + return useMutation(async () => (await functionAxios.post(`/seasons/${seasonId}/duplicate`)).data) +} diff --git a/packages/client/src/pages/categories/CategoryCreate.page.tsx b/packages/client/src/pages/categories/CategoryCreate.page.tsx index 385005c..730e788 100644 --- a/packages/client/src/pages/categories/CategoryCreate.page.tsx +++ b/packages/client/src/pages/categories/CategoryCreate.page.tsx @@ -1,4 +1,18 @@ -import { Button, Flex, FormControl, FormErrorMessage, FormLabel, Heading, HStack, Input, Text, VStack } from '@chakra-ui/react' +import { + Alert, + AlertIcon, + AlertTitle, + Button, + Flex, + FormControl, + FormErrorMessage, + FormLabel, + Heading, + HStack, + Input, + useToast, + VStack, +} from '@chakra-ui/react' import { CreateCategoryForm } from '@pontozo/common' import { FormProvider, SubmitHandler, useForm } from 'react-hook-form' import { FaArrowLeft } from 'react-icons/fa' @@ -9,6 +23,7 @@ import { NavigateWithError } from 'src/components/commons/NavigateWithError' import { useCreateCategoryMutation, useDeleteCategoryMutation, + useDuplicateCategoryMutation, useFetchCategory, useUpdateCategoryMutation, } from '../../api/hooks/categoryHooks' @@ -35,9 +50,11 @@ export const CategoryCreatePage = () => { formState: { errors }, } = form const navigate = useNavigate() + const toast = useToast() const createMutation = useCreateCategoryMutation() const updateMutation = useUpdateCategoryMutation(categoryId) const deleteMutation = useDeleteCategoryMutation(categoryId) + const duplicateMutation = useDuplicateCategoryMutation(categoryId) const onSubmit: SubmitHandler = ({ criteria, ...restOfData }) => { if (categoryId === -1) { @@ -47,6 +64,15 @@ export const CategoryCreatePage = () => { } } + const onDuplicateClick = () => { + duplicateMutation.mutate(undefined, { + onSuccess: (res) => { + navigate(`${PATHS.CATEGORIES}/${res.id}/edit`) + toast({ title: 'Kategória duplikálva!', description: 'Most már az újonnan létrejött kategóriát szerkeszted!', status: 'success' }) + }, + }) + } + if (isLoading && isFetching) { return } @@ -57,7 +83,13 @@ export const CategoryCreatePage = () => { {categoryId === -1 ? 'Új kategória' : 'Kategória szerkesztése'} - {!categoryEditable && Ez a kategória már nem szerkeszthető, mert része egy olyan szezonnak, ami már elkezdődött!} + + {!categoryEditable && ( + + + Ez a kategória nem szerkeszthető, mert része egy olyan szezonnak, ami már elkezdődött! + + )} Név @@ -78,6 +110,11 @@ export const CategoryCreatePage = () => { Vissza + {categoryId > -1 && ( + + )} {categoryId > -1 && ( deleteMutation.mutate(undefined, { onSuccess: () => navigate(PATHS.CATEGORIES) })} diff --git a/packages/client/src/pages/categories/CategoryList.page.tsx b/packages/client/src/pages/categories/CategoryList.page.tsx index e45450e..1cbc044 100644 --- a/packages/client/src/pages/categories/CategoryList.page.tsx +++ b/packages/client/src/pages/categories/CategoryList.page.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Flex, Heading, Text, VStack } from '@chakra-ui/react' +import { Badge, Box, Button, Flex, Heading, HStack, Text, VStack } from '@chakra-ui/react' import { Link } from 'react-router-dom' import { HelmetTitle } from 'src/components/commons/HelmetTitle' import { NavigateWithError } from 'src/components/commons/NavigateWithError' @@ -26,7 +26,16 @@ export const CategoryListPage = () => { {data?.map((c) => ( - {c.name} + + {c.name} + + {c.seasons.map((s) => ( + + {s.name} + + ))} + + {c.description} ))} diff --git a/packages/client/src/pages/categories/components/CriteriaSelector.tsx b/packages/client/src/pages/categories/components/CriteriaSelector.tsx index 2270c8b..24ae71c 100644 --- a/packages/client/src/pages/categories/components/CriteriaSelector.tsx +++ b/packages/client/src/pages/categories/components/CriteriaSelector.tsx @@ -84,7 +84,7 @@ export const CriteriaSelector = ({ editable }: { editable: boolean }) => { } colorScheme="brand" onClick={() => moveCriterion(idx, c, -1)} @@ -92,7 +92,7 @@ export const CriteriaSelector = ({ editable }: { editable: boolean }) => { } colorScheme="brand" onClick={() => moveCriterion(idx, c, 1)} diff --git a/packages/client/src/pages/criteria/CriteriaCreate.page.tsx b/packages/client/src/pages/criteria/CriteriaCreate.page.tsx index feb09c8..e54f783 100644 --- a/packages/client/src/pages/criteria/CriteriaCreate.page.tsx +++ b/packages/client/src/pages/criteria/CriteriaCreate.page.tsx @@ -1,4 +1,7 @@ import { + Alert, + AlertIcon, + AlertTitle, Badge, Button, Checkbox, @@ -12,7 +15,6 @@ import { SimpleGrid, Stack, Switch, - Text, useToast, VStack, } from '@chakra-ui/react' @@ -122,7 +124,12 @@ export const CriteriaCreatePage = () => { - {!criterionEditable && Ez a szempont már nem szerkeszthető, mert része egy olyan szezonnak, ami már elkezdődött!} + {!criterionEditable && ( + + + Ez a szempont nem szerkeszthető, mert része egy olyan szezonnak, ami már elkezdődött! + + )} Név diff --git a/packages/client/src/pages/seasons/SeasonCreate.page.tsx b/packages/client/src/pages/seasons/SeasonCreate.page.tsx index 015b770..cc0cb29 100644 --- a/packages/client/src/pages/seasons/SeasonCreate.page.tsx +++ b/packages/client/src/pages/seasons/SeasonCreate.page.tsx @@ -1,4 +1,7 @@ import { + Alert, + AlertIcon, + AlertTitle, Button, Flex, FormControl, @@ -8,7 +11,6 @@ import { HStack, Input, Stack, - Text, useToast, VStack, } from '@chakra-ui/react' @@ -21,7 +23,13 @@ import { ConfirmDialogButton } from 'src/components/commons/ConfirmDialogButton' import { HelmetTitle } from 'src/components/commons/HelmetTitle' import { NavigateWithError } from 'src/components/commons/NavigateWithError' import { onError } from 'src/util/onError' -import { useCreateSeasonMutation, useDeleteSeasonMutation, useFetchSeason, useUpdateSeasonMutation } from '../../api/hooks/seasonHooks' +import { + useCreateSeasonMutation, + useDeleteSeasonMutation, + useDuplicateSeasonMutation, + useFetchSeason, + useUpdateSeasonMutation, +} from '../../api/hooks/seasonHooks' import { LoadingSpinner } from '../../components/commons/LoadingSpinner' import { PATHS } from '../../util/paths' import { CategorySelector } from './components/CategorySelector' @@ -51,6 +59,7 @@ export const SeasonCreatePage = () => { const createMutation = useCreateSeasonMutation() const updateMutation = useUpdateSeasonMutation(seasonId) const deleteMutation = useDeleteSeasonMutation(seasonId) + const duplicateMutation = useDuplicateSeasonMutation(seasonId) useEffect(() => { if (seasonId !== -1 && data && !isLoading) { @@ -64,6 +73,15 @@ export const SeasonCreatePage = () => { } }, [data, isLoading, seasonId, setValue]) + const onDuplicateClick = () => { + duplicateMutation.mutate(undefined, { + onSuccess: (res) => { + navigate(`${PATHS.SEASONS}/${res.id}/edit`) + toast({ title: 'Szezon duplikálva!', description: 'Most már az újonnan létrejött szezont szerkeszted!', status: 'success' }) + }, + }) + } + if (isLoading && isFetching) { return } @@ -90,7 +108,13 @@ export const SeasonCreatePage = () => { {seasonId === -1 ? 'Új szezon' : 'Szezon szerkesztése'} - {!seasonEditable && Ez a szezon már nem szerkeszthető, mert már elkezdődött!} + + {!seasonEditable && ( + + + Ez a szezon nem szerkeszthető, mert már elkezdődött! + + )} Név @@ -138,6 +162,11 @@ export const SeasonCreatePage = () => { Vissza + {seasonId > -1 && ( + + )} {seasonId > -1 && ( diff --git a/packages/client/src/pages/seasons/components/CategorySelector.tsx b/packages/client/src/pages/seasons/components/CategorySelector.tsx index fe09e01..880543d 100644 --- a/packages/client/src/pages/seasons/components/CategorySelector.tsx +++ b/packages/client/src/pages/seasons/components/CategorySelector.tsx @@ -1,9 +1,11 @@ import { + Badge, Box, Button, FormControl, FormErrorMessage, FormLabel, + Heading, HStack, IconButton, Input, @@ -173,7 +175,8 @@ export const CategorySelector = ({ editable }: { editable: boolean }) => { Nincs találat ) : ( filteredCategoryList.map((c) => ( - { onClose() }} > - {c.name} - + + {c.name} + + + {c.seasons.map((s) => ( + + {s.name} + + ))} + + )) )} diff --git a/packages/common/src/lib/types/categories.ts b/packages/common/src/lib/types/categories.ts index a5ee042..550b3fe 100644 --- a/packages/common/src/lib/types/categories.ts +++ b/packages/common/src/lib/types/categories.ts @@ -1,5 +1,6 @@ import { ArrayUnique, IsInt, IsNotEmpty, IsString, Min } from 'class-validator' import { Criterion } from './criteria' +import { Season } from './seasons' export interface Category { id: number @@ -25,3 +26,7 @@ export type CreateCategoryForm = Omit export interface CategoryWithCriteria extends Category { criteria: Criterion[] } + +export interface CategoryWithSeasons extends Category { + seasons: Season[] +} diff --git a/packages/functions/src/functions/categories/duplicate.ts b/packages/functions/src/functions/categories/duplicate.ts new file mode 100644 index 0000000..5fc6138 --- /dev/null +++ b/packages/functions/src/functions/categories/duplicate.ts @@ -0,0 +1,48 @@ +import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions' +import { PontozoException } from '@pontozo/common' +import { getUserFromHeaderAndAssertAdmin } from '../../service/auth.service' +import Category from '../../typeorm/entities/Category' +import { CategoryToCriterion } from '../../typeorm/entities/CategoryToCriterion' +import { getAppDataSource } from '../../typeorm/getConfig' +import { handleException } from '../../util/handleException' +import { validateId } from '../../util/validation' + +export const duplicateCategory = async (req: HttpRequest, context: InvocationContext): Promise => { + try { + const user = await getUserFromHeaderAndAssertAdmin(req, context) + const id = validateId(req) + + const ads = await getAppDataSource(context) + const oldCategory = await ads.manager.findOne(Category, { + where: { id }, + relations: { criteria: true }, + }) + if (oldCategory === null) { + throw new PontozoException('A kategória nem található!', 404) + } + + const newCategory = new Category() + newCategory.name = oldCategory.name + newCategory.description = oldCategory.description + newCategory.criteria = oldCategory.criteria.map((ctc) => { + const newCtc = new CategoryToCriterion() + newCtc.criterionId = ctc.criterionId + newCtc.order = ctc.order + return newCtc + }) + await ads.manager.save(newCategory) + + context.log(`User #${user.szemely_id} duplicated category #${oldCategory.id}`) + return { + jsonBody: newCategory, + } + } catch (error) { + return handleException(req, context, error) + } +} + +app.http('categories-duplicate', { + methods: ['POST'], + route: 'categories/{id}/duplicate', + handler: duplicateCategory, +}) diff --git a/packages/functions/src/functions/categories/getAll.ts b/packages/functions/src/functions/categories/getAll.ts index a4840ec..d7311e0 100644 --- a/packages/functions/src/functions/categories/getAll.ts +++ b/packages/functions/src/functions/categories/getAll.ts @@ -10,9 +10,9 @@ export const getCategories = async (req: HttpRequest, context: InvocationContext await getUserFromHeaderAndAssertAdmin(req, context) const categoryRepo = (await getAppDataSource(context)).getRepository(Category) - const categories = await categoryRepo.find({ relations: { criteria: true } }) + const categories = await categoryRepo.find({ relations: { seasons: { season: true } } }) return { - jsonBody: categories, + jsonBody: categories.map((c) => ({ ...c, seasons: c.seasons.map((cts) => cts.season) })), } } catch (error) { return handleException(req, context, error) diff --git a/packages/functions/src/functions/seasons/duplicate.ts b/packages/functions/src/functions/seasons/duplicate.ts new file mode 100644 index 0000000..9463c9a --- /dev/null +++ b/packages/functions/src/functions/seasons/duplicate.ts @@ -0,0 +1,50 @@ +import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions' +import { PontozoException } from '@pontozo/common' +import { getUserFromHeaderAndAssertAdmin } from '../../service/auth.service' +import Season from '../../typeorm/entities/Season' +import { SeasonToCategory } from '../../typeorm/entities/SeasonToCategory' +import { getAppDataSource } from '../../typeorm/getConfig' +import { handleException } from '../../util/handleException' +import { validateId } from '../../util/validation' + +export const duplicateSeason = async (req: HttpRequest, context: InvocationContext): Promise => { + try { + const user = await getUserFromHeaderAndAssertAdmin(req, context) + const id = validateId(req) + + const ads = await getAppDataSource(context) + const oldSeason = await ads.manager.findOne(Season, { + where: { id }, + relations: { categories: true }, + }) + if (oldSeason === null) { + throw new PontozoException('A szezon nem található!', 404) + } + + const newSeason = new Season() + newSeason.name = oldSeason.name + ' másolata' + newSeason.startDate = new Date(Date.now() + 1000 * 60 * 60 * 24) + newSeason.endDate = new Date(newSeason.startDate.getTime() + 1000 * 60 * 60 * 24 * 365) + + newSeason.categories = oldSeason.categories.map((stc) => { + const newCtc = new SeasonToCategory() + newCtc.categoryId = stc.categoryId + newCtc.order = stc.order + return newCtc + }) + await ads.manager.save(newSeason) + + context.log(`User #${user.szemely_id} duplicated season #${oldSeason.id}`) + return { + jsonBody: newSeason, + } + } catch (error) { + return handleException(req, context, error) + } +} + +app.http('season-duplicate', { + methods: ['POST'], + route: 'seasons/{id}/duplicate', + handler: duplicateSeason, +})