diff --git a/dashboard/public/locales/en.json b/dashboard/public/locales/en.json index a41a8a2..6d3df9e 100644 --- a/dashboard/public/locales/en.json +++ b/dashboard/public/locales/en.json @@ -140,13 +140,14 @@ "users": { "note-placeholder": "Additional information about user", "data_limit": "Data Limit", + "traffic": "Traffic", "used_traffic": "Used Traffic", "data_limit_reset_strategy": "Data Reset Period", "online_at": "Last Use", "expire_date": "Expire Date", "unlimited": "Unlimited", "limited": "Limited", - "data_limit_method": "Data limit type", + "data_limit_method": "Data Limit", "determined_expire": "Determined", "expire_method": "Expiration", "onhold_expire": "On Hold", diff --git a/dashboard/src/features/users/dialogs/delete-confirmation/index.tsx b/dashboard/src/features/users/components/dialogs/delete-confirmation/index.tsx similarity index 100% rename from dashboard/src/features/users/dialogs/delete-confirmation/index.tsx rename to dashboard/src/features/users/components/dialogs/delete-confirmation/index.tsx diff --git a/dashboard/src/features/users/dialogs/index.ts b/dashboard/src/features/users/components/dialogs/index.ts similarity index 100% rename from dashboard/src/features/users/dialogs/index.ts rename to dashboard/src/features/users/components/dialogs/index.ts diff --git a/dashboard/src/features/users/dialogs/mutation/fields/data-limit-reset-strategy.tsx b/dashboard/src/features/users/components/dialogs/mutation/fields/data-limit-reset-strategy.tsx similarity index 100% rename from dashboard/src/features/users/dialogs/mutation/fields/data-limit-reset-strategy.tsx rename to dashboard/src/features/users/components/dialogs/mutation/fields/data-limit-reset-strategy.tsx diff --git a/dashboard/src/features/users/dialogs/mutation/fields/data-limit.tsx b/dashboard/src/features/users/components/dialogs/mutation/fields/data-limit.tsx similarity index 93% rename from dashboard/src/features/users/dialogs/mutation/fields/data-limit.tsx rename to dashboard/src/features/users/components/dialogs/mutation/fields/data-limit.tsx index 1c0f45c..bd1205f 100644 --- a/dashboard/src/features/users/dialogs/mutation/fields/data-limit.tsx +++ b/dashboard/src/features/users/components/dialogs/mutation/fields/data-limit.tsx @@ -21,7 +21,7 @@ export const DataLimitField: FC = () => { name="data_limit" render={({ field }) => ( - {t('page.users.data_limit')} + {t('page.users.traffic')} = ({
+ + +
diff --git a/dashboard/src/features/users/dialogs/mutation/schema.ts b/dashboard/src/features/users/components/dialogs/mutation/schema.ts similarity index 74% rename from dashboard/src/features/users/dialogs/mutation/schema.ts rename to dashboard/src/features/users/components/dialogs/mutation/schema.ts index 223f5a9..be058b2 100644 --- a/dashboard/src/features/users/dialogs/mutation/schema.ts +++ b/dashboard/src/features/users/components/dialogs/mutation/schema.ts @@ -5,15 +5,11 @@ export const UserSchema = z.object({ username: z.string().min(1, { message: 'Username is required' }), note: z.string().nullable(), data_limit: z - .string() - .min(0, 'The minimum number is 0') - .or(z.number()) + .union([z.string().transform((str) => Number(str) * DATA_LIMIT_METRIC), z.number()]) + .refine(val => val >= 0, { message: 'The minimum number is 0' }) .nullable() .optional() - .transform((str) => { - if (str) return Number((parseFloat(String(str)) * DATA_LIMIT_METRIC).toFixed(5)); - return 0; - }), + .transform((val) => val ?? 0), expire: z.string().or(z.number()).nullable().optional(), data_limit_reset_strategy: z.string().nullable().optional(), status: z.string(), diff --git a/dashboard/src/features/users/components/dialogs/mutation/sections/data-limit/index.tsx b/dashboard/src/features/users/components/dialogs/mutation/sections/data-limit/index.tsx new file mode 100644 index 0000000..027304f --- /dev/null +++ b/dashboard/src/features/users/components/dialogs/mutation/sections/data-limit/index.tsx @@ -0,0 +1,33 @@ +import { FC } from "react"; +import { FormLabel, Switch } from '@marzneshin/components'; +import { useTranslation } from 'react-i18next'; +import { DataLimitField, DataLimitResetStrategyField } from "../../fields"; +import { useFormContext } from "react-hook-form"; +import { useDataLimit } from "./use-data-limit"; + +export const DataLimitFields: FC = () => { + const { t } = useTranslation(); + const { isDataLimitEnabled, setIsDataLimitEnabled } = useDataLimit(); + const form = useFormContext(); + + const handleToggleChange = () => { + setIsDataLimitEnabled(!isDataLimitEnabled); + }; + + return ( +
+ + {t('page.users.data_limit_method')} + + +
+ {isDataLimitEnabled && ( +
+ + {form.watch().data_limit !== 0 && } +
+ )} +
+
+ ); +}; diff --git a/dashboard/src/features/users/components/dialogs/mutation/sections/data-limit/use-data-limit.ts b/dashboard/src/features/users/components/dialogs/mutation/sections/data-limit/use-data-limit.ts new file mode 100644 index 0000000..a75a5a5 --- /dev/null +++ b/dashboard/src/features/users/components/dialogs/mutation/sections/data-limit/use-data-limit.ts @@ -0,0 +1,22 @@ +import { useEffect, useState } from "react"; +import { useFormContext } from "react-hook-form"; + +export const useDataLimit = () => { + const form = useFormContext(); + const data_limit = form.watch("data_limit"); + + const [isDataLimitEnabled, setIsDataLimitEnabled] = useState(data_limit !== 0); + + useEffect(() => { + setIsDataLimitEnabled(data_limit !== 0); + }, [data_limit]); + + useEffect(() => { + if (!isDataLimitEnabled) { + form.setValue("data_limit", 0); + form.setValue("data_limit_reset_strategy", "no_reset"); + } + }, [isDataLimitEnabled, form]); + + return { isDataLimitEnabled, setIsDataLimitEnabled }; +}; diff --git a/dashboard/src/features/users/dialogs/mutation/sections/expiration-method.tsx b/dashboard/src/features/users/components/dialogs/mutation/sections/expiration-method/index.tsx similarity index 76% rename from dashboard/src/features/users/dialogs/mutation/sections/expiration-method.tsx rename to dashboard/src/features/users/components/dialogs/mutation/sections/expiration-method/index.tsx index 2d2eea1..660a9f8 100644 --- a/dashboard/src/features/users/dialogs/mutation/sections/expiration-method.tsx +++ b/dashboard/src/features/users/components/dialogs/mutation/sections/expiration-method/index.tsx @@ -12,31 +12,37 @@ import { ExpireDateField, OnHoldTimeoutField, OnHoldExpireDurationField, -} from "../fields"; +} from "@marzneshin/features/users/components/dialogs/mutation/fields"; import { TabsList } from "@radix-ui/react-tabs"; import { useTranslation } from "react-i18next"; -import { useExpirationMethodTabs } from "./use-expiration-method-tabs"; +import { + useExpirationMethodTabs, + type ExpirationMethodStrategy +} from "./use-expiration-method-tabs"; interface ExpirationMethodProps { entity: UserMutationType | null } - export const ExpirationMethodFields: FC = ({ entity }) => { - const { t } = useTranslation() + const { t } = useTranslation(); const { setSelectedExpirationMethodTab, defaultExpirationMethodTab - } = useExpirationMethodTabs({ entity }) + } = useExpirationMethodTabs({ entity }); + + const handleTabChange = (value: string) => { + setSelectedExpirationMethodTab(value as ExpirationMethodStrategy); + }; return ( <> - + {t('page.users.expire_method')} - @@ -65,5 +71,5 @@ export const ExpirationMethodFields: FC = ({ entity }) => - ) -} + ); +}; diff --git a/dashboard/src/features/users/components/dialogs/mutation/sections/expiration-method/use-expiration-method-tabs.ts b/dashboard/src/features/users/components/dialogs/mutation/sections/expiration-method/use-expiration-method-tabs.ts new file mode 100644 index 0000000..0388bc0 --- /dev/null +++ b/dashboard/src/features/users/components/dialogs/mutation/sections/expiration-method/use-expiration-method-tabs.ts @@ -0,0 +1,45 @@ +import { UserMutationType } from "@marzneshin/features/users"; +import { useEffect, useState } from "react"; +import { useFormContext } from "react-hook-form"; + +export type ExpirationMethodStrategy = "onhold" | "determined" | "unlimited"; + +const getUserExpirationMethod = (entity: UserMutationType): ExpirationMethodStrategy => { + if (entity.status === "on_hold") return 'onhold'; + if (entity.expire != null) return 'determined'; + return 'unlimited'; +}; + +export const useExpirationMethodTabs = ({ entity }: { entity: UserMutationType | null }) => { + const form = useFormContext(); + + const defaultExpirationMethodTab = entity ? getUserExpirationMethod(entity) : 'determined'; + const [selectedExpirationMethodTab, setSelectedExpirationMethodTab] = useState(defaultExpirationMethodTab); + + useEffect(() => { + form.setValue("status", selectedExpirationMethodTab === 'onhold' ? 'on_hold' : 'active'); + + if (selectedExpirationMethodTab === 'onhold') { + form.setValue("expire", undefined); + } else if (selectedExpirationMethodTab === "unlimited") { + form.setValue("expire", 0); + form.setValue("on_hold_expire_duration", undefined); + form.setValue("on_hold_timeout", undefined); + } else { + form.setValue("on_hold_expire_duration", 0); + form.setValue("on_hold_timeout", null); + } + + form.clearErrors("expire"); + form.clearErrors("on_hold_expire_duration"); + form.clearErrors("on_hold_timeout"); + + // eslint-disable-next-line + }, [selectedExpirationMethodTab]); + + return { + selectedExpirationMethodTab, + defaultExpirationMethodTab, + setSelectedExpirationMethodTab + }; +}; diff --git a/dashboard/src/features/users/dialogs/mutation/sections/index.ts b/dashboard/src/features/users/components/dialogs/mutation/sections/index.ts similarity index 100% rename from dashboard/src/features/users/dialogs/mutation/sections/index.ts rename to dashboard/src/features/users/components/dialogs/mutation/sections/index.ts diff --git a/dashboard/src/features/users/dialogs/settings/index.tsx b/dashboard/src/features/users/components/dialogs/settings/index.tsx similarity index 100% rename from dashboard/src/features/users/dialogs/settings/index.tsx rename to dashboard/src/features/users/components/dialogs/settings/index.tsx diff --git a/dashboard/src/features/users/dialogs/settings/user-info/index.ts b/dashboard/src/features/users/components/dialogs/settings/user-info/index.ts similarity index 100% rename from dashboard/src/features/users/dialogs/settings/user-info/index.ts rename to dashboard/src/features/users/components/dialogs/settings/user-info/index.ts diff --git a/dashboard/src/features/users/dialogs/settings/user-info/qrcode.tsx b/dashboard/src/features/users/components/dialogs/settings/user-info/qrcode.tsx similarity index 100% rename from dashboard/src/features/users/dialogs/settings/user-info/qrcode.tsx rename to dashboard/src/features/users/components/dialogs/settings/user-info/qrcode.tsx diff --git a/dashboard/src/features/users/dialogs/settings/user-info/subcription-link-button.tsx b/dashboard/src/features/users/components/dialogs/settings/user-info/subcription-link-button.tsx similarity index 100% rename from dashboard/src/features/users/dialogs/settings/user-info/subcription-link-button.tsx rename to dashboard/src/features/users/components/dialogs/settings/user-info/subcription-link-button.tsx diff --git a/dashboard/src/features/users/dialogs/settings/user-info/subscription-actions.tsx b/dashboard/src/features/users/components/dialogs/settings/user-info/subscription-actions.tsx similarity index 100% rename from dashboard/src/features/users/dialogs/settings/user-info/subscription-actions.tsx rename to dashboard/src/features/users/components/dialogs/settings/user-info/subscription-actions.tsx diff --git a/dashboard/src/features/users/dialogs/settings/user-info/table.tsx b/dashboard/src/features/users/components/dialogs/settings/user-info/table.tsx similarity index 100% rename from dashboard/src/features/users/dialogs/settings/user-info/table.tsx rename to dashboard/src/features/users/components/dialogs/settings/user-info/table.tsx diff --git a/dashboard/src/features/users/dialogs/settings/user-info/user-status-enable-button.tsx b/dashboard/src/features/users/components/dialogs/settings/user-info/user-status-enable-button.tsx similarity index 100% rename from dashboard/src/features/users/dialogs/settings/user-info/user-status-enable-button.tsx rename to dashboard/src/features/users/components/dialogs/settings/user-info/user-status-enable-button.tsx diff --git a/dashboard/src/features/users/components/index.ts b/dashboard/src/features/users/components/index.ts index 158fc13..904d22e 100644 --- a/dashboard/src/features/users/components/index.ts +++ b/dashboard/src/features/users/components/index.ts @@ -3,3 +3,6 @@ export * from './services-card' export * from './date-field' export * from './online-status' export * from './used-user-traffic' +export * from "./tables" +export * from "./dialogs" +export * from "./widgets" diff --git a/dashboard/src/features/users/tables/index.ts b/dashboard/src/features/users/components/tables/index.ts similarity index 100% rename from dashboard/src/features/users/tables/index.ts rename to dashboard/src/features/users/components/tables/index.ts diff --git a/dashboard/src/features/users/tables/services/columns.tsx b/dashboard/src/features/users/components/tables/services/columns.tsx similarity index 100% rename from dashboard/src/features/users/tables/services/columns.tsx rename to dashboard/src/features/users/components/tables/services/columns.tsx diff --git a/dashboard/src/features/users/tables/services/index.tsx b/dashboard/src/features/users/components/tables/services/index.tsx similarity index 100% rename from dashboard/src/features/users/tables/services/index.tsx rename to dashboard/src/features/users/components/tables/services/index.tsx diff --git a/dashboard/src/features/users/tables/users/columns.tsx b/dashboard/src/features/users/components/tables/users/columns.tsx similarity index 88% rename from dashboard/src/features/users/tables/users/columns.tsx rename to dashboard/src/features/users/components/tables/users/columns.tsx index 8ef6009..585a2b4 100644 --- a/dashboard/src/features/users/tables/users/columns.tsx +++ b/dashboard/src/features/users/components/tables/users/columns.tsx @@ -71,10 +71,21 @@ export const columns = (actions: ColumnAction): ColumnDef[] => [ { id: "actions", cell: ({ row }) => { + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + actions.onOpen(row.original); + } + }; + return (
e.stopPropagation()} + onKeyDown={handleKeyDown} + tabIndex={0} + role="button" > [] => [
); }, - }, + } ]; diff --git a/dashboard/src/features/users/tables/users/index.tsx b/dashboard/src/features/users/components/tables/users/index.tsx similarity index 100% rename from dashboard/src/features/users/tables/users/index.tsx rename to dashboard/src/features/users/components/tables/users/index.tsx diff --git a/dashboard/src/features/users/widgets/index.ts b/dashboard/src/features/users/components/widgets/index.ts similarity index 100% rename from dashboard/src/features/users/widgets/index.ts rename to dashboard/src/features/users/components/widgets/index.ts diff --git a/dashboard/src/features/users/widgets/users-stats-widget/index.tsx b/dashboard/src/features/users/components/widgets/users-stats-widget/index.tsx similarity index 100% rename from dashboard/src/features/users/widgets/users-stats-widget/index.tsx rename to dashboard/src/features/users/components/widgets/users-stats-widget/index.tsx diff --git a/dashboard/src/features/users/widgets/users-stats-widget/legend.tsx b/dashboard/src/features/users/components/widgets/users-stats-widget/legend.tsx similarity index 100% rename from dashboard/src/features/users/widgets/users-stats-widget/legend.tsx rename to dashboard/src/features/users/components/widgets/users-stats-widget/legend.tsx diff --git a/dashboard/src/features/users/widgets/users-stats-widget/users-stats-widget.stories.ts b/dashboard/src/features/users/components/widgets/users-stats-widget/users-stats-widget.stories.ts similarity index 100% rename from dashboard/src/features/users/widgets/users-stats-widget/users-stats-widget.stories.ts rename to dashboard/src/features/users/components/widgets/users-stats-widget/users-stats-widget.stories.ts diff --git a/dashboard/src/features/users/router-context/index.ts b/dashboard/src/features/users/contexts/router.ts similarity index 100% rename from dashboard/src/features/users/router-context/index.ts rename to dashboard/src/features/users/contexts/router.ts diff --git a/dashboard/src/features/users/dialogs/mutation/sections/data-limit.tsx b/dashboard/src/features/users/dialogs/mutation/sections/data-limit.tsx deleted file mode 100644 index d302285..0000000 --- a/dashboard/src/features/users/dialogs/mutation/sections/data-limit.tsx +++ /dev/null @@ -1,67 +0,0 @@ - -import { FC, useEffect, useState } from "react"; -import { - Tabs, - TabsTrigger, - TabsContent, - FormLabel -} from '@marzneshin/components'; -import { useTranslation } from 'react-i18next'; -import { - DataLimitField, - DataLimitResetStrategyField, -} from "../fields"; -import { TabsList } from "@radix-ui/react-tabs"; -import { useFormContext } from 'react-hook-form'; - -export const DataLimitFields: FC = () => { - const form = useFormContext() - - const { t } = useTranslation() - const data_limit = form.watch().data_limit; - - const [ - selectedDataLimitTab, - setSelectedDataLimitTab - ] = useState((data_limit === 0) ? "unlimited" : "limited"); - - useEffect(() => { - setSelectedDataLimitTab((data_limit === 0) ? "unlimited" : "limited") - }, [data_limit]) - - useEffect(() => { - if (selectedDataLimitTab !== 'limited') { - form.setValue("data_limit", undefined) - form.setValue("data_limit_reset_strategy", "no_reset"); - } - // eslint-disable-next-line - }, [selectedDataLimitTab]); - - return ( - <> - - {t('page.users.data_limit_method')} - -
- - - - {t('page.users.limited')} - - - {t('page.users.unlimited')} - - - - - {form.watch().data_limit !== 0 && } - - -
- - ) -} diff --git a/dashboard/src/features/users/dialogs/mutation/sections/use-expiration-method-tabs.ts b/dashboard/src/features/users/dialogs/mutation/sections/use-expiration-method-tabs.ts deleted file mode 100644 index 10f657f..0000000 --- a/dashboard/src/features/users/dialogs/mutation/sections/use-expiration-method-tabs.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { UserMutationType } from "@marzneshin/features/users"; -import { useEffect, useState } from "react"; -import { useFormContext } from "react-hook-form"; - -type ExpirationMethodStrategy = "onhold" | "determined" | "unlimited"; - -const getUserExpirationMethod = (entity: UserMutationType): ExpirationMethodStrategy => { - if (entity.status === "on_hold") - return 'onhold' - else if (entity.expire !== undefined && entity.expire !== null) - return 'determined' - else - return 'unlimited' -} - - -export const useExpirationMethodTabs = ({ entity }: { entity: UserMutationType | null }) => { - const form = useFormContext() - - const defaultExpirationMethodTab = - (entity !== null) ? getUserExpirationMethod(entity) : 'determined' - const [ - selectedExpirationMethodTab, - setSelectedExpirationMethodTab - ] = useState(defaultExpirationMethodTab); - - useEffect(() => { - form.setValue("status", "active") - if (selectedExpirationMethodTab === 'onhold') { - form.setValue("status", "on_hold") - form.setValue("expire", undefined); - form.clearErrors("expire"); - } else if (selectedExpirationMethodTab === "unlimited") { - form.setValue("expire", 0); - form.setValue("on_hold_expire_duration", undefined); - form.setValue("on_hold_timeout", undefined); - form.clearErrors("expire"); - form.clearErrors("on_hold_expire_duration"); - form.clearErrors("on_hold_timeout"); - } else { - form.setValue("on_hold_expire_duration", 0); - form.setValue("on_hold_timeout", null); - form.clearErrors("on_hold_expire_duration"); - form.clearErrors("on_hold_timeout"); - } - // eslint-disable-next-line - }, [selectedExpirationMethodTab]); - - return { selectedExpirationMethodTab, defaultExpirationMethodTab, setSelectedExpirationMethodTab } -} diff --git a/dashboard/src/features/users/index.ts b/dashboard/src/features/users/index.ts index 80e6fe7..b07e497 100644 --- a/dashboard/src/features/users/index.ts +++ b/dashboard/src/features/users/index.ts @@ -1,8 +1,5 @@ export * from './services' export * from './types' -export * from './tables' export * from './components' -export * from './dialogs' export * from './constants' -export * from './widgets' -export * from './router-context' +export * from './contexts/router'