diff --git a/.DS_Store b/.DS_Store index 4737d85..64dbd43 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/auto-bid-admin/src/apis/config.ts b/auto-bid-admin/src/apis/config.ts new file mode 100644 index 0000000..599478e --- /dev/null +++ b/auto-bid-admin/src/apis/config.ts @@ -0,0 +1,33 @@ +import { handleError } from "."; +import axios from "../lib/axios"; +import { IConfig } from "../system/type"; +import { removeFalsyValues } from "../utils"; + +export const CONFIG_KEYS = { + MAIL_SCRAP_REPORT: "MAIL_SCRAP_REPORT", +}; + +export const getConfig = async (key: keyof typeof CONFIG_KEYS) => { + return await axios({ + url: "configs/" + key, + withCredentials: true, + method: "GET", + }); +}; + +export const upsertConfig = async (data: IConfig) => { + const { key_name, value, type } = removeFalsyValues(data, ["value"]); + + try { + const { data } = await axios({ + url: "configs/upsert", + withCredentials: true, + method: "POST", + data: { key_name, value: value || null, type }, + }); + + return data; + } catch (error) { + handleError(error); + } +}; diff --git a/auto-bid-admin/src/apis/scrap.ts b/auto-bid-admin/src/apis/scrap.ts index c78ead5..4409971 100644 --- a/auto-bid-admin/src/apis/scrap.ts +++ b/auto-bid-admin/src/apis/scrap.ts @@ -16,7 +16,10 @@ export const createScrapConfig = async ( url: "scrap-configs", withCredentials: true, method: "POST", - data: newData, + data: { + ...newData, + enable: newData.enable === "1", + }, }); handleSuccess(data); @@ -28,14 +31,14 @@ export const createScrapConfig = async ( }; export const updateScrapConfig = async (scrapConfig: Partial) => { - const { search_url, keywords, id } = removeFalsyValues(scrapConfig); + const { search_url, keywords, id, enable } = removeFalsyValues(scrapConfig); try { const { data } = await axios({ url: "scrap-configs/" + id, withCredentials: true, method: "PUT", - data: { search_url, keywords }, + data: { search_url, keywords, enable: enable === "1" }, }); handleSuccess(data); diff --git a/auto-bid-admin/src/components/config/mails-config.tsx b/auto-bid-admin/src/components/config/mails-config.tsx new file mode 100644 index 0000000..f10e059 --- /dev/null +++ b/auto-bid-admin/src/components/config/mails-config.tsx @@ -0,0 +1,161 @@ +import { + ActionIcon, + Box, + Card, + Group, + LoadingOverlay, + Text, + TextInput, +} from "@mantine/core"; +import { useForm, zodResolver } from "@mantine/form"; +import { IconAt, IconMinus, IconPlus } from "@tabler/icons-react"; +import { useEffect, useMemo, useState } from "react"; +import { z } from "zod"; +import { getConfig, upsertConfig } from "../../apis/config"; +import { IConfig } from "../../system/type"; +import { useConfirmStore } from "../../lib/zustand/use-confirm"; +import { useDisclosure } from "@mantine/hooks"; + +const schema = z.object({ + email: z + .string({ message: "Email is required" }) + .email({ message: "Invalid email address" }), +}); + +const MailInput = ({ + initValue, + onDelete, + onAdd, +}: { + initValue?: string; + onDelete?: (data: string) => void; + onAdd?: (data: string) => Promise; +}) => { + const form = useForm({ + initialValues: { + email: initValue || "", + }, + validate: zodResolver(schema), + }); + + return ( +
{ + await onAdd(values.email); + form.reset(); + } + : () => {} + )} + className="flex items-start gap-2 w-full" + > + } + placeholder="Enter email" + className="flex-1" + size="xs" + /> + onDelete(initValue) : undefined} + type={!initValue ? "submit" : "button"} + color={initValue ? "red" : "blue"} + variant="light" + > + {initValue ? : } + + + ); +}; + +export default function MailsConfig() { + const [config, setConfig] = useState(null); + const { setConfirm } = useConfirmStore(); + const [opened, { open, close }] = useDisclosure(false); + useEffect(() => { + fetchConfig(); + }, []); + + const mails = useMemo(() => { + if (!config) return []; + + return config?.value?.split(", ").length > 0 + ? config?.value.split(",") + : []; + }, [config]); + + const fetchConfig = async () => { + const response = await getConfig("MAIL_SCRAP_REPORT"); + + if (!response || ![200, 201].includes(response.data?.status_code)) return; + + setConfig(response.data.data); + }; + + const handleDelete = (mail: string) => { + setConfirm({ + message: "Are you want to delete: " + mail, + title: "Delete", + handleOk: async () => { + open(); + const newMails = mails.filter((item) => item !== mail); + + if (!config) return; + + const response = await upsertConfig({ + ...(config as IConfig), + value: newMails.join(", "), + }); + + if (response) { + fetchConfig(); + } + close(); + }, + }); + }; + + const handleAdd = async (mail: string) => { + const newMails = [...mails, mail]; + + open(); + const response = await upsertConfig({ + ...(config as IConfig), + value: newMails.join(", "), + }); + + if (response) { + fetchConfig(); + } + close(); + }; + + return ( + + + + Mails + + + + + + {mails.length > 0 && + mails.map((mail) => { + return ( + + ); + })} + + + + + + + ); +} diff --git a/auto-bid-admin/src/components/user-menu.tsx b/auto-bid-admin/src/components/user-menu.tsx index b986611..ad4895d 100644 --- a/auto-bid-admin/src/components/user-menu.tsx +++ b/auto-bid-admin/src/components/user-menu.tsx @@ -1,122 +1,180 @@ -import { Avatar, Button, LoadingOverlay, Menu, Modal, PasswordInput } from '@mantine/core'; -import { useForm, zodResolver } from '@mantine/form'; -import { useDisclosure } from '@mantine/hooks'; -import { IconKey, IconLogout, IconSettings, IconUser } from '@tabler/icons-react'; -import { useState } from 'react'; -import { useNavigate } from 'react-router'; -import { Link } from 'react-router-dom'; -import { z } from 'zod'; -import { changePassword, logout } from '../apis/auth'; -import { useConfirmStore } from '../lib/zustand/use-confirm'; -import Links from '../system/links'; +import { + Avatar, + Button, + LoadingOverlay, + Menu, + Modal, + PasswordInput, +} from "@mantine/core"; +import { useForm, zodResolver } from "@mantine/form"; +import { useDisclosure } from "@mantine/hooks"; +import { + IconCode, + IconKey, + IconLogout, + IconSettings, + IconUser, +} from "@tabler/icons-react"; +import { useState } from "react"; +import { useNavigate } from "react-router"; +import { Link } from "react-router-dom"; +import { z } from "zod"; +import { changePassword, logout } from "../apis/auth"; +import { useConfirmStore } from "../lib/zustand/use-confirm"; +import Links from "../system/links"; const schema = z - .object({ - currentPassword: z.string().min(6, 'Current password must be at least 6 characters'), - newPassword: z.string().min(6, 'New password must be at least 6 characters'), - confirmPassword: z.string(), - }) - .refine((data) => data.newPassword === data.confirmPassword, { - path: ['confirmPassword'], - message: 'Passwords do not match', - }); + .object({ + currentPassword: z + .string() + .min(6, "Current password must be at least 6 characters"), + newPassword: z + .string() + .min(6, "New password must be at least 6 characters"), + confirmPassword: z.string(), + }) + .refine((data) => data.newPassword === data.confirmPassword, { + path: ["confirmPassword"], + message: "Passwords do not match", + }); export default function UserMenu() { - const [opened, { open, close }] = useDisclosure(false); + const [opened, { open, close }] = useDisclosure(false); - const { setConfirm } = useConfirmStore(); + const { setConfirm } = useConfirmStore(); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(false); - const navigate = useNavigate(); - const form = useForm({ - initialValues: { - currentPassword: '', - newPassword: '', - confirmPassword: '', - }, - validate: zodResolver(schema), + const navigate = useNavigate(); + const form = useForm({ + initialValues: { + currentPassword: "", + newPassword: "", + confirmPassword: "", + }, + validate: zodResolver(schema), + }); + + const handleSubmit = async (values: typeof form.values) => { + await handleChangePassword(values); + }; + + const handleLogout = async () => { + setConfirm({ + title: "Are you wan't to logout?", + message: "This account will logout !", + okButton: { value: "Logout" }, + handleOk: async () => { + const data = await logout(); + + if (data && data.data) { + navigate(Links.LOGIN); + } + }, }); + }; - const handleSubmit = async (values: typeof form.values) => { - await handleChangePassword(values); - }; - - const handleLogout = async () => { - setConfirm({ - title: "Are you wan't to logout?", - message: 'This account will logout !', - okButton: { value: 'Logout' }, - handleOk: async () => { - const data = await logout(); - - if (data && data.data) { - navigate(Links.LOGIN); - } - }, + const handleChangePassword = async (values: typeof form.values) => { + setConfirm({ + title: "Are you wan't to update password", + message: "This account will change password !", + okButton: { value: "Sure" }, + handleOk: async () => { + setLoading(true); + const data = await changePassword({ + newPassword: values.newPassword, + password: values.currentPassword, }); - }; - const handleChangePassword = async (values: typeof form.values) => { - setConfirm({ - title: "Are you wan't to update password", - message: 'This account will change password !', - okButton: { value: 'Sure' }, - handleOk: async () => { - setLoading(true); - const data = await changePassword({ - newPassword: values.newPassword, - password: values.currentPassword, - }); + setLoading(false); - setLoading(false); + if (data && data.data) { + navigate(Links.LOGIN); + close(); + } + }, + }); + }; - if (data && data.data) { - navigate(Links.LOGIN); - close(); - } - }, - }); - }; + return ( + <> + + + + + + - return ( - <> - - - - - - + + Account + }> + Change password + + } + > + Keys + - - Account - }> - Change password - - }> - Keys - + } + > + Configs + - + - }> - Logout - - - + } + > + Logout + + + - -
- - - - - + +
+ + + + + - -
- - ); + +
+ + ); } diff --git a/auto-bid-admin/src/components/web-bid/scrap-config.modal.tsx b/auto-bid-admin/src/components/web-bid/scrap-config.modal.tsx index 00f95f6..4e1f976 100644 --- a/auto-bid-admin/src/components/web-bid/scrap-config.modal.tsx +++ b/auto-bid-admin/src/components/web-bid/scrap-config.modal.tsx @@ -4,6 +4,7 @@ import { LoadingOverlay, Modal, ModalProps, + Select, Textarea, TextInput, } from "@mantine/core"; @@ -28,6 +29,7 @@ const schema = z.object({ .string({ message: "Keyword is required" }) .min(1, { message: "Keyword is required" }) .optional(), + enable: z.enum(["1", "0"], { required_error: "Enable is required" }), }); export default function ScrapConfigModal({ @@ -93,9 +95,18 @@ export default function ScrapConfigModal({ form.reset(); if (!data) return; - form.setValues(data.scrap_config); + const values = { + ...data.scrap_config, + enable: (data.scrap_config?.enable === undefined + ? "1" + : data.scrap_config.enable + ? "1" + : "0") as "0" | "1", + }; - prevData.current = data.scrap_config; + form.setValues(values); + + prevData.current = values; // eslint-disable-next-line react-hooks/exhaustive-deps }, [data]); @@ -121,6 +132,23 @@ export default function ScrapConfigModal({ onSubmit={form.onSubmit(handleSubmit)} className="grid grid-cols-2 gap-2.5" > +