diff --git a/eslint.config.js b/eslint.config.js index 643d473..9c9e099 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,7 +1,7 @@ -import eslint from "@eslint/js"; -import tseslint from "typescript-eslint"; +const eslint = require("@eslint/js"); +const tseslint = require("typescript-eslint"); -export default tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended, { +module.exports = tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended, { rules: { semi: "error", "prefer-const": "error", diff --git a/package.json b/package.json index effac26..f7e7550 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,6 @@ "name": "adminka", "version": "0.1.0", "private": true, - "type": "module", "dependencies": { "@date-io/dayjs": "^2.15.0", "@emotion/react": "^11.10.4", diff --git a/src/api/promocode/requests.ts b/src/api/promocode/requests.ts index 87c18cd..2be9e07 100644 --- a/src/api/promocode/requests.ts +++ b/src/api/promocode/requests.ts @@ -1,11 +1,12 @@ import makeRequest from "@root/api/makeRequest"; import type { - CreatePromocodeBody, - GetPromocodeListBody, - Promocode, - PromocodeList, - PromocodeStatistics, + CreatePromocodeBody, + EditPromocodeBody, + GetPromocodeListBody, + Promocode, + PromocodeList, + PromocodeStatistics, } from "@root/model/promocodes"; import { parseAxiosError } from "@root/utils/parse-error"; @@ -68,6 +69,33 @@ export const getAllPromocodes = async () => { } }; +export const getNotActivePromocodes = async () => { + try { + const promocodes: Promocode[] = []; + + let page = 0; + while (true) { + const promocodeList = await getPromocodeList({ + limit: 100, + filter: { + active: false, + }, + page, + }); + + if (promocodeList.items.length === 0) break; + + promocodes.push(...promocodeList.items); + page++; + } + + return promocodes; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + throw new Error(`Ошибка при получении списка промокодов. ${error}`); + } +}; + const createPromocode = async (body: CreatePromocodeBody) => { try { const createPromocodeResponse = await makeRequest({ @@ -88,6 +116,20 @@ const createPromocode = async (body: CreatePromocodeBody) => { } }; +const editPromocode = async (body: EditPromocodeBody) => { + try { + const editPromocodeResponse = await makeRequest({ + url: process.env.REACT_APP_DOMAIN + "/codeword/promocode" + "/edit", + method: "PUT", + body, + }); + return [editPromocodeResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + return [null, `Ошибка редактирования промокода. ${error}`]; + } +}; + const deletePromocode = async (id: string): Promise => { try { await makeRequest({ @@ -121,10 +163,12 @@ const getPromocodeStatistics = async (id: string, from: number, to: number) => { }; export const promocodeApi = { - getPromocodeList, - createPromocode, - deletePromocode, - getAllPromocodes, - getPromocodeStatistics, - createFastlink, + getPromocodeList, + createPromocode, + editPromocode, + deletePromocode, + getAllPromocodes, + getNotActivePromocodes, + getPromocodeStatistics, + createFastlink, }; diff --git a/src/api/promocode/swr.ts b/src/api/promocode/swr.ts index 8126796..d2d5ca1 100644 --- a/src/api/promocode/swr.ts +++ b/src/api/promocode/swr.ts @@ -3,20 +3,30 @@ import useSwr, { mutate } from "swr"; import { enqueueSnackbar } from "notistack"; import { promocodeApi } from "./requests"; -import type { CreatePromocodeBody, PromocodeList } from "@root/model/promocodes"; +import type { + CreatePromocodeBody, EditPromocodeBody, + PromocodeList, +} from "@root/model/promocodes"; -export function usePromocodes(page: number, pageSize: number, promocodeId: string, to: number, from: number) { - const promocodesCountRef = useRef(0); - const swrResponse = useSwr( - ["promocodes", page, pageSize], - async (key) => { - const result = await promocodeApi.getPromocodeList({ - limit: key[2], - filter: { - active: true, - }, - page: key[1], - }); +export function usePromocodes( + page: number, + pageSize: number, + promocodeId: string, + to: number, + from: number, + active: boolean +) { + const promocodesCountRef = useRef(0); + const swrResponse = useSwr( + ["promocodes", page, pageSize, active], + async (key) => { + const result = await promocodeApi.getPromocodeList({ + limit: key[2], + filter: { + active: key[3], + }, + page: key[1], + }); promocodesCountRef.current = result.count; return result; @@ -44,15 +54,38 @@ export function usePromocodes(page: number, pageSize: number, promocodeId: strin [page, pageSize] ); - const deletePromocode = useCallback( - async function (id: string) { - try { - await mutate( - ["promocodes", page, pageSize], - promocodeApi.deletePromocode(id), - { - optimisticData(currentData, displayedData) { - if (!displayedData) return; + const editPromocode = useCallback( + async function (body: EditPromocodeBody) { + try { + await promocodeApi.editPromocode(body); + mutate( + ["promocodes", page, pageSize, active], + ); + await promocodeApi.getPromocodeList({ + limit: pageSize, + filter: { + active: active, + }, + page: page, + }); + } catch (error) { + console.error("Error editing promocode", error); + if (error instanceof Error) + enqueueSnackbar(error.message, { variant: "error" }); + } + }, + [page, pageSize,] + ); + + const deletePromocode = useCallback( + async function (id: string) { + try { + await mutate( + ["promocodes", page, pageSize], + promocodeApi.deletePromocode(id), + { + optimisticData(currentData, displayedData) { + if (!displayedData) return; return { count: displayedData.count - 1, @@ -112,14 +145,15 @@ export function usePromocodes(page: number, pageSize: number, promocodeId: strin [page, pageSize] ); - return { - ...swrResponse, - createPromocode, - deletePromocode, - createFastLink, - promocodeStatistics: promocodeStatistics.data, - promocodesCount: promocodesCountRef.current, - }; + return { + ...swrResponse, + createPromocode, + deletePromocode, + editPromocode, + createFastLink, + promocodeStatistics: promocodeStatistics.data, + promocodesCount: promocodesCountRef.current, + }; } export function useAllPromocodes() { @@ -134,3 +168,16 @@ export function useAllPromocodes() { return data; } + +export function useNotActivePromocodes() { + const { data } = useSwr("notActivePromocodes", promocodeApi.getNotActivePromocodes, { + keepPreviousData: true, + suspense: true, + onError(err) { + console.error("Error fetching all promocodes", err); + enqueueSnackbar(err.message, { variant: "error" }); + }, + }); + + return data; +} diff --git a/src/model/promocodes.ts b/src/model/promocodes.ts index 34ca326..9a52bcc 100644 --- a/src/model/promocodes.ts +++ b/src/model/promocodes.ts @@ -49,3 +49,13 @@ export type PromocodeStatistics = { usageCount: number; usageMap: Record; }; + +export type EditPromocodeBody = { + id: string, + description: string, + greetings: string, + dueTo: number, + activationCount: number, + delete: boolean + +} diff --git a/src/pages/dashboard/Content/PromocodeManagement/EditModal.tsx b/src/pages/dashboard/Content/PromocodeManagement/EditModal.tsx new file mode 100644 index 0000000..7da3a29 --- /dev/null +++ b/src/pages/dashboard/Content/PromocodeManagement/EditModal.tsx @@ -0,0 +1,202 @@ +import { + Box, + Button, + Typography, + Modal, + TextField, + useTheme, + useMediaQuery, + TextFieldProps, +} from "@mui/material"; +import {CustomTextField} from "@kitUI/CustomTextField"; +import {DesktopDatePicker} from "@mui/x-date-pickers/DesktopDatePicker"; +import {useEffect, useState} from "react"; +import {EditPromocodeBody, Promocode} from "@root/model/promocodes"; +import {enqueueSnackbar} from "notistack"; + + +const host = window.location.hostname; +const isTest = host.includes("s"); + +type EditModalProps = { + id: string; + setId: (id: string) => void; + promocodes: Promocode[]; + editPromocode: (body: EditPromocodeBody) => Promise +}; + +export const EditModal = ({id, setId, promocodes, editPromocode}: EditModalProps) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(550)); + const promocode = promocodes.find((item) => item.id === id); + const [descriptionField, setDescriptionField] = useState(""); + const [greetingsField, setGreetingsField] = useState(""); + const [dueToField, setDueToField] = useState(0); + const [activationField, setActivationField] = useState(0); + + useEffect( + function setCurrentPromocodeFields() { + if (!promocode) return; + + setDescriptionField(promocode.description); + setGreetingsField(promocode.greetings); + setDueToField(promocode.dueTo); + setActivationField(promocode.activationCount); + }, + [promocode] + ); + + + async function handleEditClick() { + if (!promocode) return enqueueSnackbar(`Тариф ${id} не найден`); + + if (!descriptionField) return enqueueSnackbar('Поле "Описание" пустое'); + if (!greetingsField) return enqueueSnackbar('Поле "Приветственное сообщение" пустое'); + + const editPromocodeBody: EditPromocodeBody = { + id: id, + description: descriptionField, + greetings: greetingsField, + dueTo: dueToField, + activationCount: activationField, + delete: promocode.delete + }; + + await editPromocode(editPromocodeBody); + setId(""); + + + } + + + return ( + { + setId(""); + }} + sx={{ + "& > .MuiBox-root": { outline: "none", padding: "32px 32px 16px" }, + }} + > + + + + Редактировать промокод: {promocode?.codeword} + {promocode && ( + <> + setDescriptionField(event.target.value)} + /> + setGreetingsField(event.target.value)} + /> + + Время существования промокода + + { + setDueToField(event.$d.getTime() / 1000 || 0); + }} + renderInput={(params: TextFieldProps) => } + InputProps={{ + sx: { + height: "40px", + color: theme.palette.secondary.main, + border: "1px solid", + borderColor: theme.palette.secondary.main, + "& .MuiSvgIcon-root": { color: theme.palette.secondary.main }, + }, + }} + /> + + + setActivationField(Number(target.value.replace(/\D/g, ""))) + } + /> + + + )} + + + + + + + + + + ); +}; diff --git a/src/pages/dashboard/Content/PromocodeManagement/index.tsx b/src/pages/dashboard/Content/PromocodeManagement/index.tsx index e5be7c5..f405285 100644 --- a/src/pages/dashboard/Content/PromocodeManagement/index.tsx +++ b/src/pages/dashboard/Content/PromocodeManagement/index.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Box, Typography, useTheme } from "@mui/material"; +import {Box, Button, FormControl, InputLabel, MenuItem, Select, Typography, useTheme} from "@mui/material"; import { DataGrid, GridLoadingOverlay, GridToolbar } from "@mui/x-data-grid"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; @@ -9,6 +9,9 @@ import { CreatePromocodeForm } from "./CreatePromocodeForm"; import { usePromocodeGridColDef } from "./usePromocodeGridColDef"; import { StatisticsModal } from "./StatisticsModal"; import DeleteModal from "./DeleteModal"; +import {promocodeApi} from "@root/api/promocode/requests"; +import {SelectChangeEvent} from "@mui/material/Select"; +import {EditModal} from "@pages/dashboard/Content/PromocodeManagement/EditModal"; export const PromocodeManagement = () => { const theme = useTheme(); @@ -16,91 +19,135 @@ export const PromocodeManagement = () => { const [deleteModal, setDeleteModal] = useState(""); const deleteModalHC = (id: string) => setDeleteModal(id); - const [showStatisticsModalId, setShowStatisticsModalId] = useState(""); - const [page, setPage] = useState(0); - const [to, setTo] = useState(0); - const [from, setFrom] = useState(0); - const [pageSize, setPageSize] = useState(10); - const { - data, - error, - isValidating, - promocodesCount, - promocodeStatistics, - deletePromocode, - createPromocode, - createFastLink, - } = usePromocodes(page, pageSize, showStatisticsModalId, to, from); - const columns = usePromocodeGridColDef(setShowStatisticsModalId, deleteModalHC); - if (error) return Ошибка загрузки промокодов; + const [showStatisticsModalId, setShowStatisticsModalId] = + useState(""); + const [showEditModalId, setShowEditModalId] = + useState(""); + const [page, setPage] = useState(0); + const [to, setTo] = useState(0); + const [from, setFrom] = useState(0); + const [pageSize, setPageSize] = useState(10); + const [active, setActive] = useState(true); + const { + data, + error, + isValidating, + promocodesCount, + promocodeStatistics, + deletePromocode, + createPromocode, + editPromocode, + createFastLink, + } = usePromocodes(page, pageSize, showStatisticsModalId, to, from, active); + const columns = usePromocodeGridColDef( + setShowEditModalId, + setShowStatisticsModalId, + deleteModalHC + ); + if (error) return Ошибка загрузки промокодов; - return ( - - - Создание промокода - - - - - - - - - ); + return ( + + + Создание промокода + + + + + + + + + + + + + + + ); }; diff --git a/src/pages/dashboard/Content/PromocodeManagement/usePromocodeGridColDef.tsx b/src/pages/dashboard/Content/PromocodeManagement/usePromocodeGridColDef.tsx index dba8423..e0daffa 100644 --- a/src/pages/dashboard/Content/PromocodeManagement/usePromocodeGridColDef.tsx +++ b/src/pages/dashboard/Content/PromocodeManagement/usePromocodeGridColDef.tsx @@ -3,95 +3,117 @@ import { GridColDef } from "@mui/x-data-grid"; import { Promocode } from "@root/model/promocodes"; import { useMemo, useState } from "react"; -import { BarChart, Delete } from "@mui/icons-material"; +import { BarChart, Delete, Edit } from "@mui/icons-material"; import { promocodeApi } from "@root/api/promocode/requests"; -export function usePromocodeGridColDef(setStatistics: (id: string) => void, deletePromocode: (id: string) => void) { - const validity = (value: string | number) => { - if (value === 0) { - return "неоганичен"; - } else { - return new Date(value).toLocaleString(); - } - }; - return useMemo[]>( - () => [ - { - field: "id", - headerName: "ID", - width: 30, - sortable: false, - valueGetter: ({ row }) => row.id, - }, - { - field: "codeword", - headerName: "Кодовое слово", - width: 160, - sortable: false, - valueGetter: ({ row }) => row.codeword, - }, - { - field: "factor", - headerName: "Коэф. скидки", - width: 120, - sortable: false, - valueGetter: ({ row }) => Math.round(row.bonus.discount.factor * 1000) / 1000, - }, - { - field: "activationCount", - headerName: "Кол-во активаций", - width: 140, - sortable: false, - valueGetter: ({ row }) => row.activationCount, - }, - { - field: "dueTo", - headerName: "Истекает", - width: 160, - sortable: false, - valueGetter: ({ row }) => row.dueTo * 1000, - valueFormatter: ({ value }) => `${validity(value)}`, - }, - { - field: "description", - headerName: "Описание", - minWidth: 200, - flex: 1, - sortable: false, - valueGetter: ({ row }) => row.description, - }, - { - field: "settings", - headerName: "", - width: 60, - sortable: false, - renderCell: (params) => { - return ( - { - setStatistics(params.row.id); - promocodeApi.getPromocodeStatistics(params.row.id, 0, 0); - }} - > - - - ); - }, - }, - { - field: "delete", - headerName: "", - width: 60, - sortable: false, - renderCell: (params) => { - return ( - deletePromocode(params.row.id)}> - - - ); - }, - }, - ], - [deletePromocode, setStatistics] - ); +export function usePromocodeGridColDef( + setEdit: (id: string) => void, + setStatistics: (id: string) => void, + deletePromocode: (id: string) => void +) { + const validity = (value: string | number) => { + if (value === 0) { + return "неоганичен"; + } else { + return new Date(value).toLocaleString(); + } + }; + return useMemo[]>( + () => [ + { + field: "id", + headerName: "ID", + width: 30, + sortable: false, + valueGetter: ({ row }) => row.id, + }, + { + field: "codeword", + headerName: "Кодовое слово", + width: 160, + sortable: false, + valueGetter: ({ row }) => row.codeword, + }, + { + field: "factor", + headerName: "Коэф. скидки", + width: 120, + sortable: false, + valueGetter: ({ row }) => + Math.round(row.bonus.discount.factor * 1000) / 1000, + }, + { + field: "activationCount", + headerName: "Кол-во активаций", + width: 140, + sortable: false, + valueGetter: ({ row }) => row.activationCount, + }, + { + field: "dueTo", + headerName: "Истекает", + width: 160, + sortable: false, + valueGetter: ({ row }) => row.dueTo * 1000, + valueFormatter: ({ value }) => `${validity(value)}`, + }, + { + field: "description", + headerName: "Описание", + minWidth: 200, + flex: 1, + sortable: false, + valueGetter: ({ row }) => row.description, + }, + { + field: "edit", + headerName: "", + width: 60, + sortable: false, + renderCell: (params) => { + return ( + { + setEdit(params.row.id); + }} + > + + + ); + }, + }, + { + field: "settings", + headerName: "", + width: 60, + sortable: false, + renderCell: (params) => { + return ( + { + setStatistics(params.row.id); + promocodeApi.getPromocodeStatistics(params.row.id, 0, 0); + }} + > + + + ); + }, + }, + { + field: "delete", + headerName: "", + width: 60, + sortable: false, + renderCell: (params) => { + return ( + deletePromocode(params.row.id)}> + + + ); + }, + }, + ], + [deletePromocode, setStatistics, setEdit] + ); } diff --git a/src/pages/dashboard/Content/QuizStatistics/StastisticsPromocode.tsx b/src/pages/dashboard/Content/QuizStatistics/StastisticsPromocode.tsx index c5c109c..84b0afb 100644 --- a/src/pages/dashboard/Content/QuizStatistics/StastisticsPromocode.tsx +++ b/src/pages/dashboard/Content/QuizStatistics/StastisticsPromocode.tsx @@ -1,70 +1,144 @@ import { useState } from "react"; import moment from "moment"; -import { Table, TableBody, TableCell, TableHead, TableRow, Typography, useTheme } from "@mui/material"; +import { + Box, + FormControl, MenuItem, Select, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Typography, + useTheme, +} from "@mui/material"; import { DateFilter } from "./DateFilter"; -import { useAllPromocodes } from "@root/api/promocode/swr"; +import {useAllPromocodes, useNotActivePromocodes} from "@root/api/promocode/swr"; import { usePromocodeStatistics } from "@root/utils/hooks/usePromocodeStatistics"; import type { Moment } from "moment"; +import {SelectChangeEvent} from "@mui/material/Select"; + +type PropStatistic = { + "id": string, + "Regs": number, + "Money": number, + "codeword": string, +} export const StatisticsPromocode = () => { - const [from, setFrom] = useState(moment(moment().subtract(4, "weeks"))); - const [to, setTo] = useState(moment()); - const promocodes = useAllPromocodes(); - const promocodeStatistics = usePromocodeStatistics({ to, from }); - const theme = useTheme(); + const [from, setFrom] = useState( + moment(moment().subtract(4, "weeks")) + ); + const [to, setTo] = useState(moment()); + const [active, setActive] = useState(true); + const promocodes = useAllPromocodes(); + const promocodesNot = useNotActivePromocodes(); + const promocodeStatistics = usePromocodeStatistics({ to, from }); + const theme = useTheme(); - return ( - <> - Статистика промокодов - - - - - - Промокод - - - Регистации - - - Внесено - - - - {Object.entries(promocodeStatistics).map(([key, { Regs, Money }]) => ( - - - - {promocodes.find(({ id }) => id === key)?.codeword ?? ""} - - - {Regs} - - - {(Money / 100).toFixed(2)} - - - - ))} -
- - ); + const filterPromo = active ? promocodes : promocodesNot; + console.log(promocodes, "active"); + console.log(promocodesNot); + + const filteredPromoStatistics = () => { + const copyPromocodeStatistics:PropStatistic[] = []; + Object.entries(promocodeStatistics).map(([key, { Regs, Money }]) => { + + for(const i in filterPromo){ + if(filterPromo[i].id === key) { + copyPromocodeStatistics.push( + { + "id": key, + "Regs": Regs, + "Money": Money, + "codeword": filterPromo[i].codeword, + } + ); + + }} + + }); + return copyPromocodeStatistics; + }; + +const filteredStat = filteredPromoStatistics(); + + return ( + <> + Статистика промокодов + + + + + + + + + + + Промокод + + + Регистации + + + Внесено + + + + {filteredStat.map((stat) => ( + + + + {stat?.codeword} + + + {stat?.Regs} + + + {(stat?.Money / 100).toFixed(2)} + + + + ))} +
+ + ); }; diff --git a/src/test/test.tsx b/src/test/test.tsx new file mode 100644 index 0000000..a3127ef --- /dev/null +++ b/src/test/test.tsx @@ -0,0 +1,10 @@ +export const Test = () => { + console.log(1); + + + + + let a; + + return
; +};