update build ok,
This commit is contained in:
@@ -11,6 +11,7 @@ dashboard.lessonCategories.edit.name
|
||||
dashboard.lessonCategories.edit.type
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
please read `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lesson_category/lesson-category-edit-form.tsx`
|
||||
@@ -20,3 +21,19 @@ and update `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/letter
|
||||
|
||||
please refactor `common.json` to smaller translation files, thanks
|
||||
e.g. `lessonTypes` -> `lesson_type`
|
||||
|
||||
---
|
||||
|
||||
Hi, i want you to help merge two translation files.
|
||||
|
||||
base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/public/locales/dev`
|
||||
|
||||
I want you to merge the content
|
||||
|
||||
from `<base_dir>/lesson_type.json` (source file)
|
||||
to `<base_dir>/listening_practice.json` (dest file)
|
||||
|
||||
please extract , link up and remember the document properties
|
||||
(e.g. types, functions, variables, constants, etc)
|
||||
|
||||
update the variables and properties of dest file to reflect `listening practice categories`/`lp_categories`
|
||||
|
@@ -1,5 +1,36 @@
|
||||
{
|
||||
"hello": "world",
|
||||
"add": "新加",
|
||||
"listening-practice": "聽力練習"
|
||||
}
|
||||
"add": "新增",
|
||||
"listening-practice": "聽力練習",
|
||||
"list": {
|
||||
"title": "聽力練習分類列表",
|
||||
"empty": "沒有找到聽力練習分類",
|
||||
"action": "動作?",
|
||||
"basic-details": "基本資料"
|
||||
},
|
||||
"helloworld": "listening_practice",
|
||||
"title": "聽力練習分類",
|
||||
"type": "聽力練習分類",
|
||||
"error": {
|
||||
"invalid": "無效的聽力練習分類",
|
||||
"not_found": "未找到聽力練習分類"
|
||||
},
|
||||
"create": {
|
||||
"title": "創建聽力練習分類",
|
||||
"success": "聽力練習分類創建成功",
|
||||
"error": "創建聽力練習分類時出錯"
|
||||
},
|
||||
"edit": {
|
||||
"title": "編輯聽力練習分類",
|
||||
"success": "聽力練習分類更新成功",
|
||||
"error": "更新聽力練習分類時出錯"
|
||||
},
|
||||
"delete": {
|
||||
"title": "刪除聽力練習分類",
|
||||
"confirm": "確定要刪除聽力練習分類嗎?",
|
||||
"success": "聽力練習分類刪除成功",
|
||||
"error": "刪除聽力練習分類時出錯"
|
||||
},
|
||||
"view": {
|
||||
"title": "查看聽力練習分類詳情"
|
||||
}
|
||||
}
|
79
002_source/cms/src/app/dashboard/Sample/BasicDetailCard.tsx
Normal file
79
002_source/cms/src/app/dashboard/Sample/BasicDetailCard.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
|
||||
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { paths } from '@/paths';
|
||||
import { PropertyItem } from '@/components/core/property-item';
|
||||
import { PropertyList } from '@/components/core/property-list';
|
||||
|
||||
export default function BasicDetailCard({
|
||||
lpCatId,
|
||||
handleEditClick,
|
||||
}: {
|
||||
lpCatId: string;
|
||||
handleEditClick: () => void;
|
||||
}): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handleEditClick();
|
||||
}}
|
||||
>
|
||||
<PencilSimpleIcon />
|
||||
</IconButton>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<UserIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title={t('list.basic-details')}
|
||||
/>
|
||||
<PropertyList divider={<Divider />} orientation="vertical" sx={{ '--PropertyItem-padding': '12px 24px' }}>
|
||||
{(
|
||||
[
|
||||
{ key: 'Customer ID', value: <Chip label="USR-001" size="small" variant="soft" /> },
|
||||
{ key: 'Name', value: 'Miron Vitold' },
|
||||
{ key: 'Email', value: 'miron.vitold@domain.com' },
|
||||
{ key: 'Phone', value: '(425) 434-5535' },
|
||||
{ key: 'Company', value: 'Devias IO' },
|
||||
{
|
||||
key: 'Quota',
|
||||
value: (
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<LinearProgress sx={{ flex: '1 1 auto' }} value={50} variant="determinate" />
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
50%
|
||||
</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
] satisfies { key: string; value: React.ReactNode }[]
|
||||
).map(
|
||||
(item): React.JSX.Element => (
|
||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
||||
)
|
||||
)}
|
||||
</PropertyList>
|
||||
</Card>
|
||||
);
|
||||
}
|
14
002_source/cms/src/app/dashboard/Sample/Helloworld.tsx
Normal file
14
002_source/cms/src/app/dashboard/Sample/Helloworld.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import useEnhancedEffect from '@mui/utils/useEnhancedEffect';
|
||||
|
||||
function Page(): React.JSX.Element {
|
||||
React.useLayoutEffect(() => {
|
||||
console.log('helloworld');
|
||||
}, []);
|
||||
|
||||
return <>helloworld</>;
|
||||
}
|
||||
|
||||
export default Page;
|
@@ -0,0 +1,80 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import RouterLink from 'next/link';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import getLessonCategoryById from '@/db/LessonCategories/GetById';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Link from '@mui/material/Link';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Grid from '@mui/material/Unstable_Grid2';
|
||||
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
||||
import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown';
|
||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||
import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard';
|
||||
import { House as HouseIcon } from '@phosphor-icons/react/dist/ssr/House';
|
||||
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
|
||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||
import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning';
|
||||
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
|
||||
import type { RecordModel } from 'pocketbase';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { Address } from '@/types/Address';
|
||||
import { LpCategory } from '@/types/LpCategory';
|
||||
import { paths } from '@/paths';
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
import { logger } from '@/lib/default-logger';
|
||||
import { pb } from '@/lib/pb';
|
||||
import { PropertyItem } from '@/components/core/property-item';
|
||||
import { PropertyList } from '@/components/core/property-list';
|
||||
import { toast } from '@/components/core/toaster';
|
||||
import ErrorDisplay from '@/components/dashboard/error';
|
||||
import LpCategoryDefaultValue, { defaultLpCategory } from '@/components/dashboard/lp_categories/_constants';
|
||||
import { Notifications } from '@/components/dashboard/lp_categories/notifications';
|
||||
import { Payments } from '@/components/dashboard/lp_categories/payments';
|
||||
import { ShippingAddress } from '@/components/dashboard/lp_categories/shipping-address';
|
||||
|
||||
import { SampleAddresses } from './SampleAddresses';
|
||||
import { SampleNotifications } from './SampleNotifications';
|
||||
import { SamplePayments } from './SamplePayments';
|
||||
|
||||
export default function SampleAddressCard(): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<Button color="secondary" startIcon={<PlusIcon />}>
|
||||
{t('list.add')}
|
||||
</Button>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<HouseIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title={t('list.shipping-addresses')}
|
||||
/>
|
||||
<CardContent>
|
||||
<Grid container spacing={3}>
|
||||
{(SampleAddresses satisfies Address[]).map((address) => (
|
||||
<Grid key={address.id} md={6} xs={12}>
|
||||
<ShippingAddress address={address} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
@@ -0,0 +1,95 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import RouterLink from 'next/link';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import getLessonCategoryById from '@/db/LessonCategories/GetById';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Link from '@mui/material/Link';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Grid from '@mui/material/Unstable_Grid2';
|
||||
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
||||
import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown';
|
||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||
import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard';
|
||||
import { House as HouseIcon } from '@phosphor-icons/react/dist/ssr/House';
|
||||
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
|
||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||
import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning';
|
||||
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
|
||||
import type { RecordModel } from 'pocketbase';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { Address } from '@/types/Address';
|
||||
import { LpCategory } from '@/types/LpCategory';
|
||||
import { paths } from '@/paths';
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
import { logger } from '@/lib/default-logger';
|
||||
import { pb } from '@/lib/pb';
|
||||
import { PropertyItem } from '@/components/core/property-item';
|
||||
import { PropertyList } from '@/components/core/property-list';
|
||||
import { toast } from '@/components/core/toaster';
|
||||
import ErrorDisplay from '@/components/dashboard/error';
|
||||
import LpCategoryDefaultValue, { defaultLpCategory } from '@/components/dashboard/lp_categories/_constants';
|
||||
import { Notifications } from '@/components/dashboard/lp_categories/notifications';
|
||||
import { Payments } from '@/components/dashboard/lp_categories/payments';
|
||||
import { ShippingAddress } from '@/components/dashboard/lp_categories/shipping-address';
|
||||
|
||||
import SampleAddressCard from './SampleAddressCard';
|
||||
import { SampleAddresses } from './SampleAddresses';
|
||||
import { SampleNotifications } from './SampleNotifications';
|
||||
import { SamplePayments } from './SamplePayments';
|
||||
|
||||
export default function SamplePaymentCard(): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Payments ordersValue={2069.48} payments={SamplePayments} refundsValue={324.5} totalOrders={5} />
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<Button color="secondary" startIcon={<PencilSimpleIcon />}>
|
||||
{t('list.edit')}
|
||||
</Button>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<CreditCardIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title={t('list.billing-details')}
|
||||
/>
|
||||
<CardContent>
|
||||
<Card sx={{ borderRadius: 1 }} variant="outlined">
|
||||
<PropertyList divider={<Divider />} sx={{ '--PropertyItem-padding': '16px' }}>
|
||||
{(
|
||||
[
|
||||
{ key: t('Credit card'), value: '**** 4142' },
|
||||
{ key: t('Country'), value: t('United States') },
|
||||
{ key: t('State'), value: t('Michigan') },
|
||||
{ key: t('City'), value: t('Southfield') },
|
||||
{ key: t('Address'), value: t('Address') },
|
||||
{ key: t('Tax ID'), value: t('Tax ID') },
|
||||
] satisfies { key: string; value: React.ReactNode }[]
|
||||
).map(
|
||||
(item): React.JSX.Element => (
|
||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
||||
)
|
||||
)}
|
||||
</PropertyList>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function SampleSecurityCard(): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar>
|
||||
<ShieldWarningIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title={t('list.security')}
|
||||
/>
|
||||
<CardContent>
|
||||
<Stack spacing={1}>
|
||||
<div>
|
||||
<Button color="error" variant="contained">
|
||||
{t('Delete account')}
|
||||
</Button>
|
||||
</div>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{t('a-deleted-customer-cannot-be-restored-all-data-will-be-permanently-removed')}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
44
002_source/cms/src/app/dashboard/Sample/SampleTitleCard.tsx
Normal file
44
002_source/cms/src/app/dashboard/Sample/SampleTitleCard.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Button } from '@mui/material';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown';
|
||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function SampleTitleCard(): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto' }}>
|
||||
<Avatar src="/assets/avatar-1.png" sx={{ '--Avatar-size': '64px' }}>
|
||||
empty
|
||||
</Avatar>
|
||||
<div>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Typography variant="h4">{t('list.customer-name')}</Typography>
|
||||
<Chip
|
||||
icon={<CheckCircleIcon color="var(--mui-palette-success-main)" weight="fill" />}
|
||||
label={t('list.active')}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Stack>
|
||||
<Typography color="text.secondary" variant="body1">
|
||||
{t('list.customer-email')}
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
<div>
|
||||
<Button endIcon={<CaretDownIcon />} variant="contained">
|
||||
{t('list.action')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
// import { dayjs } from 'dayjs';
|
||||
import type { Payment } from '@/types/Payment';
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
|
||||
export const SamplePayments: Payment[] = [
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 500,
|
||||
invoiceId: 'INV-005',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(5, 'minute').subtract(1, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 324.5,
|
||||
invoiceId: 'INV-004',
|
||||
status: 'refunded',
|
||||
createdAt: dayjs().subtract(21, 'minute').subtract(2, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 746.5,
|
||||
invoiceId: 'INV-003',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(7, 'minute').subtract(3, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 56.89,
|
||||
invoiceId: 'INV-002',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(48, 'minute').subtract(4, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 541.59,
|
||||
invoiceId: 'INV-001',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(31, 'minute').subtract(5, 'hour').toDate(),
|
||||
},
|
||||
];
|
@@ -3,59 +3,42 @@
|
||||
import * as React from 'react';
|
||||
import RouterLink from 'next/link';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Link from '@mui/material/Link';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Grid from '@mui/material/Unstable_Grid2';
|
||||
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
||||
import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown';
|
||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||
import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard';
|
||||
import { House as HouseIcon } from '@phosphor-icons/react/dist/ssr/House';
|
||||
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
|
||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||
import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning';
|
||||
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
|
||||
import type { RecordModel } from 'pocketbase';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { paths } from '@/paths';
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
import { logger } from '@/lib/default-logger';
|
||||
import { pb } from '@/lib/pb';
|
||||
import { PropertyItem } from '@/components/core/property-item';
|
||||
import { PropertyList } from '@/components/core/property-list';
|
||||
import { toast } from '@/components/core/toaster';
|
||||
import ErrorDisplay from '@/components/dashboard/error';
|
||||
import { defaultLessonType, LessonTypeDefaultValue } from '@/components/dashboard/lesson_type/_constants';
|
||||
// import { getLessonTypeById } from '@/components/dashboard/lesson_type/http-actions';
|
||||
// import { LessonTypeDefaultValue, type LessonType } from '@/components/dashboard/lesson_type/ILessonType';
|
||||
// import { defaultLessonType } from '@/components/dashboard/lesson_type/interfaces';
|
||||
import { Notifications } from '@/components/dashboard/lesson_type/notifications';
|
||||
import { Payments } from '@/components/dashboard/lesson_type/payments';
|
||||
import type { Address } from '@/components/dashboard/lesson_type/shipping-address';
|
||||
import { ShippingAddress } from '@/components/dashboard/lesson_type/shipping-address';
|
||||
import { type LessonType } from '@/components/dashboard/lesson_type/types';
|
||||
import FormLoading from '@/components/loading';
|
||||
|
||||
import BasicDetailCard from '../../Sample/BasicDetailCard';
|
||||
import SampleAddressCard from '../../Sample/SampleAddressCard';
|
||||
import { SampleNotifications } from '../../Sample/SampleNotifications';
|
||||
import SamplePaymentCard from '../../Sample/SamplePaymentCard';
|
||||
import SampleSecurityCard from '../../Sample/SampleSecurityCard';
|
||||
import SampleTitleCard from '../../Sample/SampleTitleCard';
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
const { t } = useTranslation(['common', 'lesson_type']);
|
||||
const { t } = useTranslation(['lesson_type']);
|
||||
const router = useRouter();
|
||||
//
|
||||
const { type_id: typeId } = useParams<{ type_id: string }>();
|
||||
|
||||
//
|
||||
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
||||
const [showError, setShowError] = React.useState<boolean>(false);
|
||||
const [errorDetails, setErrorDetails] = React.useState('');
|
||||
|
||||
//
|
||||
const [showLessonType, setShowLessonType] = React.useState<LessonType>(LessonTypeDefaultValue);
|
||||
|
||||
@@ -73,7 +56,7 @@ export default function Page(): React.JSX.Element {
|
||||
.catch((err) => {
|
||||
logger.error(err);
|
||||
toast(t('dashboard.lessonTypes.list.error'));
|
||||
|
||||
setErrorDetails(err);
|
||||
setShowError(true);
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -89,7 +72,7 @@ export default function Page(): React.JSX.Element {
|
||||
<ErrorDisplay
|
||||
message={t('error.unable-to-process-request', { ns: 'common' })}
|
||||
code="500"
|
||||
details={t('error.detailed-error-information', { ns: 'common' })}
|
||||
details={JSON.stringify(errorDetails, null, 2)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -113,261 +96,25 @@ export default function Page(): React.JSX.Element {
|
||||
variant="subtitle2"
|
||||
>
|
||||
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||
{t('list.title', { ns: 'lesson_type' })}
|
||||
{t('list.title')}
|
||||
</Link>
|
||||
</div>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto' }}>
|
||||
<Avatar src="/assets/avatar-1.png" sx={{ '--Avatar-size': '64px' }}>
|
||||
empty
|
||||
</Avatar>
|
||||
<div>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Typography variant="h4">{showLessonType.name}</Typography>
|
||||
<Chip
|
||||
icon={<CheckCircleIcon color="var(--mui-palette-success-main)" weight="fill" />}
|
||||
label={showLessonType.visible}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Stack>
|
||||
<Typography color="text.secondary" variant="body1">
|
||||
{showLessonType.id}
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
<div>
|
||||
<Button endIcon={<CaretDownIcon />} variant="contained">
|
||||
Action
|
||||
</Button>
|
||||
</div>
|
||||
<SampleTitleCard />
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Grid container spacing={4}>
|
||||
<Grid lg={4} xs={12}>
|
||||
<Stack spacing={4}>
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handleEditClick();
|
||||
}}
|
||||
>
|
||||
<PencilSimpleIcon />
|
||||
</IconButton>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<UserIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title={t('basic-details', { ns: 'lesson_type' })}
|
||||
/>
|
||||
<PropertyList
|
||||
divider={<Divider />}
|
||||
orientation="vertical"
|
||||
sx={{ '--PropertyItem-padding': '12px 24px' }}
|
||||
>
|
||||
{(
|
||||
[
|
||||
{ key: 'Customer ID', value: <Chip label={showLessonType.id} size="small" variant="soft" /> },
|
||||
{ key: 'Name', value: showLessonType.name },
|
||||
{ key: 'Type', value: showLessonType.type },
|
||||
{ key: 'Pos', value: showLessonType.pos },
|
||||
{
|
||||
key: 'Visible',
|
||||
value: (
|
||||
<Chip
|
||||
//
|
||||
label={showLessonType.visible}
|
||||
size="small"
|
||||
variant="soft"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'Quota',
|
||||
value: (
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<LinearProgress sx={{ flex: '1 1 auto' }} value={50} variant="determinate" />
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
50%
|
||||
</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
] satisfies { key: string; value: React.ReactNode }[]
|
||||
).map(
|
||||
(item): React.JSX.Element => (
|
||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
||||
)
|
||||
)}
|
||||
</PropertyList>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar>
|
||||
<ShieldWarningIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title={t('security', { ns: 'lesson_type' })}
|
||||
/>
|
||||
<CardContent>
|
||||
<Stack spacing={1}>
|
||||
<div>
|
||||
<Button color="error" variant="contained">
|
||||
Delete account
|
||||
</Button>
|
||||
</div>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
A deleted lesson type cannot be restored. All data will be permanently removed.
|
||||
</Typography>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<BasicDetailCard lpCatId={showLessonType.id} handleEditClick={handleEditClick} />
|
||||
<SampleSecurityCard />
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid lg={8} xs={12}>
|
||||
<Stack spacing={4}>
|
||||
<Payments
|
||||
ordersValue={2069.48}
|
||||
payments={[
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 500,
|
||||
invoiceId: 'INV-005',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(5, 'minute').subtract(1, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 324.5,
|
||||
invoiceId: 'INV-004',
|
||||
status: 'refunded',
|
||||
createdAt: dayjs().subtract(21, 'minute').subtract(2, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 746.5,
|
||||
invoiceId: 'INV-003',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(7, 'minute').subtract(3, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 56.89,
|
||||
invoiceId: 'INV-002',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(48, 'minute').subtract(4, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 541.59,
|
||||
invoiceId: 'INV-001',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(31, 'minute').subtract(5, 'hour').toDate(),
|
||||
},
|
||||
]}
|
||||
refundsValue={324.5}
|
||||
totalOrders={5}
|
||||
/>
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<Button color="secondary" startIcon={<PencilSimpleIcon />}>
|
||||
Edit
|
||||
</Button>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<CreditCardIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title={t('billing-details', { ns: 'lesson_type' })}
|
||||
/>
|
||||
<CardContent>
|
||||
<Card sx={{ borderRadius: 1 }} variant="outlined">
|
||||
<PropertyList divider={<Divider />} sx={{ '--PropertyItem-padding': '16px' }}>
|
||||
{(
|
||||
[
|
||||
{ key: 'Credit card', value: '**** 4142' },
|
||||
{ key: 'Country', value: 'United States' },
|
||||
{ key: 'State', value: 'Michigan' },
|
||||
{ key: 'City', value: 'Southfield' },
|
||||
{ key: 'Address', value: '1721 Bartlett Avenue, 48034' },
|
||||
{ key: 'Tax ID', value: 'EU87956621' },
|
||||
] satisfies { key: string; value: React.ReactNode }[]
|
||||
).map(
|
||||
(item): React.JSX.Element => (
|
||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
||||
)
|
||||
)}
|
||||
</PropertyList>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<Button color="secondary" startIcon={<PlusIcon />}>
|
||||
Add
|
||||
</Button>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<HouseIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title={t('shipping-addresses', { ns: 'lesson_type' })}
|
||||
/>
|
||||
<CardContent>
|
||||
<Grid container spacing={3}>
|
||||
{(
|
||||
[
|
||||
{
|
||||
id: 'ADR-001',
|
||||
country: 'United States',
|
||||
state: 'Michigan',
|
||||
city: 'Lansing',
|
||||
zipCode: '48933',
|
||||
street: '480 Haven Lane',
|
||||
primary: true,
|
||||
},
|
||||
{
|
||||
id: 'ADR-002',
|
||||
country: 'United States',
|
||||
state: 'Missouri',
|
||||
city: 'Springfield',
|
||||
zipCode: '65804',
|
||||
street: '4807 Lighthouse Drive',
|
||||
},
|
||||
] satisfies Address[]
|
||||
).map((address) => (
|
||||
<Grid key={address.id} md={6} xs={12}>
|
||||
<ShippingAddress address={address} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Notifications
|
||||
notifications={[
|
||||
{
|
||||
id: 'EV-002',
|
||||
type: 'Refund request approved',
|
||||
status: 'pending',
|
||||
createdAt: dayjs().subtract(34, 'minute').subtract(5, 'hour').subtract(3, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'EV-001',
|
||||
type: 'Order confirmation',
|
||||
status: 'delivered',
|
||||
createdAt: dayjs().subtract(49, 'minute').subtract(11, 'hour').subtract(4, 'day').toDate(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<SamplePaymentCard />
|
||||
<SampleAddressCard />
|
||||
<Notifications notifications={SampleNotifications} />
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
@@ -40,12 +40,9 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
||||
const [showError, setShowError] = React.useState<boolean>(false);
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
||||
//
|
||||
const [f, setF] = React.useState<LessonType[]>([]);
|
||||
const [currentPage, setCurrentPage] = React.useState<number>(0);
|
||||
//
|
||||
const [recordCount, setRecordCount] = React.useState<number>(0);
|
||||
|
||||
const [listOption, setListOption] = React.useState({});
|
||||
const [listSort, setListSort] = React.useState({});
|
||||
|
||||
@@ -143,10 +140,15 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
filters={{ email, phone, status, name, visible, type }}
|
||||
fullData={lessonTypesData}
|
||||
sortDir={sortDir}
|
||||
//
|
||||
/>
|
||||
<Divider />
|
||||
<Box sx={{ overflowX: 'auto' }}>
|
||||
<LessonTypesTable reloadRows={reloadRows} rows={f} />
|
||||
<LessonTypesTable
|
||||
reloadRows={reloadRows}
|
||||
rows={f}
|
||||
//
|
||||
/>
|
||||
</Box>
|
||||
<Divider />
|
||||
<LessonTypesPagination
|
||||
|
@@ -1,236 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import RouterLink from 'next/link';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Link from '@mui/material/Link';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Grid from '@mui/material/Unstable_Grid2';
|
||||
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
||||
import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown';
|
||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||
import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard';
|
||||
import { House as HouseIcon } from '@phosphor-icons/react/dist/ssr/House';
|
||||
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
|
||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||
import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning';
|
||||
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { Address } from '@/types/Address';
|
||||
import { paths } from '@/paths';
|
||||
import { PropertyItem } from '@/components/core/property-item';
|
||||
import { PropertyList } from '@/components/core/property-list';
|
||||
import { Notifications } from '@/components/dashboard/lp_categories/notifications';
|
||||
import { Payments } from '@/components/dashboard/lp_categories/payments';
|
||||
import { ShippingAddress } from '@/components/dashboard/lp_categories/shipping-address';
|
||||
|
||||
import { SampleAddresses } from './SampleAddresses';
|
||||
import { SampleNotifications } from './SampleNotifications';
|
||||
import { SamplePayments } from './SamplePayments';
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: 'var(--Content-maxWidth)',
|
||||
m: 'var(--Content-margin)',
|
||||
p: 'var(--Content-padding)',
|
||||
width: 'var(--Content-width)',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<Stack spacing={3}>
|
||||
<div>
|
||||
<Link
|
||||
color="text.primary"
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.lp_categories.list}
|
||||
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
||||
variant="subtitle2"
|
||||
>
|
||||
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||
{t('Customers')}
|
||||
</Link>
|
||||
</div>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto' }}>
|
||||
<Avatar src="/assets/avatar-1.png" sx={{ '--Avatar-size': '64px' }}>
|
||||
MV
|
||||
</Avatar>
|
||||
<div>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Typography variant="h4">{t('Customer name')}</Typography>
|
||||
<Chip
|
||||
icon={<CheckCircleIcon color="var(--mui-palette-success-main)" weight="fill" />}
|
||||
label={t('Active')}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Stack>
|
||||
<Typography color="text.secondary" variant="body1">
|
||||
{t('Customer email')}
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
<div>
|
||||
<Button endIcon={<CaretDownIcon />} variant="contained">
|
||||
{t('Action')}
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Grid container spacing={4}>
|
||||
<Grid lg={4} xs={12}>
|
||||
<Stack spacing={4}>
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<IconButton>
|
||||
<PencilSimpleIcon />
|
||||
</IconButton>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<UserIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title={t('Basic details')}
|
||||
/>
|
||||
<PropertyList
|
||||
divider={<Divider />}
|
||||
orientation="vertical"
|
||||
sx={{ '--PropertyItem-padding': '12px 24px' }}
|
||||
>
|
||||
{(
|
||||
[
|
||||
{ key: t('Customer ID'), value: <Chip label={t('USR-001')} size="small" variant="soft" /> },
|
||||
{ key: t('Name'), value: t('Customer name') },
|
||||
{ key: t('Email'), value: t('Customer email') },
|
||||
{ key: t('Phone'), value: t('Customer phone') },
|
||||
{ key: t('Company'), value: t('Company name') },
|
||||
{
|
||||
key: t('Quota'),
|
||||
value: (
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<LinearProgress sx={{ flex: '1 1 auto' }} value={50} variant="determinate" />
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
50%
|
||||
</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
] satisfies { key: string; value: React.ReactNode }[]
|
||||
).map(
|
||||
(item): React.JSX.Element => (
|
||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
||||
)
|
||||
)}
|
||||
</PropertyList>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar>
|
||||
<ShieldWarningIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title={t('Security')}
|
||||
/>
|
||||
<CardContent>
|
||||
<Stack spacing={1}>
|
||||
<div>
|
||||
<Button color="error" variant="contained">
|
||||
{t('Delete account')}
|
||||
</Button>
|
||||
</div>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{t('A deleted customer cannot be restored. All data will be permanently removed.')}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid lg={8} xs={12}>
|
||||
<Stack spacing={4}>
|
||||
<Payments ordersValue={2069.48} payments={SamplePayments} refundsValue={324.5} totalOrders={5} />
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<Button color="secondary" startIcon={<PencilSimpleIcon />}>
|
||||
{t('Edit')}
|
||||
</Button>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<CreditCardIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title={t('Billing details')}
|
||||
/>
|
||||
<CardContent>
|
||||
<Card sx={{ borderRadius: 1 }} variant="outlined">
|
||||
<PropertyList divider={<Divider />} sx={{ '--PropertyItem-padding': '16px' }}>
|
||||
{(
|
||||
[
|
||||
{ key: t('Credit card'), value: '**** 4142' },
|
||||
{ key: t('Country'), value: t('United States') },
|
||||
{ key: t('State'), value: t('Michigan') },
|
||||
{ key: t('City'), value: t('Southfield') },
|
||||
{ key: t('Address'), value: t('Address') },
|
||||
{ key: t('Tax ID'), value: t('Tax ID') },
|
||||
] satisfies { key: string; value: React.ReactNode }[]
|
||||
).map(
|
||||
(item): React.JSX.Element => (
|
||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
||||
)
|
||||
)}
|
||||
</PropertyList>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<Button color="secondary" startIcon={<PlusIcon />}>
|
||||
{t('Add')}
|
||||
</Button>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<HouseIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title={t('Shipping addresses')}
|
||||
/>
|
||||
<CardContent>
|
||||
<Grid container spacing={3}>
|
||||
{(SampleAddresses satisfies Address[]).map((address) => (
|
||||
<Grid key={address.id} md={6} xs={12}>
|
||||
<ShippingAddress address={address} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Notifications notifications={SampleNotifications} />
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -0,0 +1,121 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import RouterLink from 'next/link';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import getQuizListeningById from '@/db/QuizListenings/GetById';
|
||||
import Box from '@mui/material/Box';
|
||||
import Link from '@mui/material/Link';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Grid from '@mui/material/Unstable_Grid2';
|
||||
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
||||
import type { RecordModel } from 'pocketbase';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { LpCategory } from '@/types/LpCategory';
|
||||
import { paths } from '@/paths';
|
||||
import { logger } from '@/lib/default-logger';
|
||||
import { toast } from '@/components/core/toaster';
|
||||
import ErrorDisplay from '@/components/dashboard/error';
|
||||
import { defaultLpCategory, LpCategoryDefaultValue } from '@/components/dashboard/lp_categories/_constants';
|
||||
import { Notifications } from '@/components/dashboard/lp_categories/notifications';
|
||||
import FormLoading from '@/components/loading';
|
||||
|
||||
import BasicDetailCard from '../../Sample/BasicDetailCard';
|
||||
import SampleAddressCard from '../../Sample/SampleAddressCard';
|
||||
import { SampleNotifications } from '../../Sample/SampleNotifications';
|
||||
import SamplePaymentCard from '../../Sample/SamplePaymentCard';
|
||||
import SampleSecurityCard from '../../Sample/SampleSecurityCard';
|
||||
import SampleTitleCard from '../../Sample/SampleTitleCard';
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
const { t } = useTranslation(['listening_practice']);
|
||||
const router = useRouter();
|
||||
//
|
||||
const { lp_cat_id: lpCatId } = useParams<{ lp_cat_id: string }>();
|
||||
|
||||
//
|
||||
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
||||
const [showError, setShowError] = React.useState<boolean>(false);
|
||||
const [errorDetails, setErrorDetails] = React.useState('');
|
||||
|
||||
//
|
||||
const [showLessonType, setShowLessonType] = React.useState<LpCategory>(LpCategoryDefaultValue.default);
|
||||
|
||||
function handleEditClick(): void {
|
||||
router.push(paths.dashboard.lp_categories.edit(lpCatId));
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
getQuizListeningById(lpCatId)
|
||||
.then((model: RecordModel) => {
|
||||
setShowLessonType({ ...defaultLpCategory, ...model });
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error(err);
|
||||
toast(t('list.error'));
|
||||
setErrorDetails(err);
|
||||
setShowError(true);
|
||||
})
|
||||
.finally(() => {
|
||||
setShowLoading(false);
|
||||
});
|
||||
}, [lpCatId]);
|
||||
|
||||
if (showLoading) return <FormLoading />;
|
||||
|
||||
if (showError)
|
||||
return (
|
||||
<ErrorDisplay
|
||||
message={t('error.unable-to-process-request', { ns: 'common' })}
|
||||
code="500"
|
||||
details={JSON.stringify(errorDetails, null, 2)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: 'var(--Content-maxWidth)',
|
||||
m: 'var(--Content-margin)',
|
||||
p: 'var(--Content-padding)',
|
||||
width: 'var(--Content-width)',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<Stack spacing={3}>
|
||||
<div>
|
||||
<Link
|
||||
color="text.primary"
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.lp_categories.list}
|
||||
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
||||
variant="subtitle2"
|
||||
>
|
||||
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||
{t('list.title')}
|
||||
</Link>
|
||||
</div>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
||||
<SampleTitleCard />
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Grid container spacing={4}>
|
||||
<Grid lg={4} xs={12}>
|
||||
<Stack spacing={4}>
|
||||
<BasicDetailCard lpCatId={showLessonType.id} handleEditClick={handleEditClick} />
|
||||
<SampleSecurityCard />
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid lg={8} xs={12}>
|
||||
<Stack spacing={4}>
|
||||
<SamplePaymentCard />
|
||||
<SampleAddressCard />
|
||||
<Notifications notifications={SampleNotifications} />
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -2,35 +2,106 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import getAllQuizListenings from '@/db/QuizListenings/GetAll';
|
||||
import listWithOption from '@/db/QuizListenings/ListWithOption';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||
import type { ListResult, RecordModel } from 'pocketbase';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { LpCategory } from '@/types/LpCategory';
|
||||
import { paths } from '@/paths';
|
||||
import ErrorDisplay from '@/components/dashboard/error';
|
||||
import { defaultLpCategory } from '@/components/dashboard/lp_categories/_constants';
|
||||
import { LpCategoriesFilters } from '@/components/dashboard/lp_categories/lp-categories-filters';
|
||||
import type { Filters } from '@/components/dashboard/lp_categories/lp-categories-filters';
|
||||
import { LpCategoriesPagination } from '@/components/dashboard/lp_categories/lp-categories-pagination';
|
||||
import { LpCategoriesSelectionProvider } from '@/components/dashboard/lp_categories/lp-categories-selection-context';
|
||||
import { LpCategoriesTable } from '@/components/dashboard/lp_categories/lp-categories-table';
|
||||
|
||||
import { LpCategories } from './lp-categories';
|
||||
import FormLoading from '@/components/loading';
|
||||
|
||||
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation(['listening_practice']);
|
||||
const { email, phone, sortDir, status } = searchParams;
|
||||
const { email, phone, sortDir, status, name, visible, type } = searchParams;
|
||||
const router = useRouter();
|
||||
const [lpCategoriesData, setLpCategoriesData] = React.useState<LpCategory[]>([]);
|
||||
//
|
||||
|
||||
const sortedCustomers = applySort(LpCategories, sortDir);
|
||||
const lpCategories = applyFilters(sortedCustomers, { email, phone, status });
|
||||
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
|
||||
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
||||
const [showError, setShowError] = React.useState<boolean>(false);
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
||||
const [f, setF] = React.useState<LpCategory[]>([]);
|
||||
const [currentPage, setCurrentPage] = React.useState<number>(0);
|
||||
const [recordCount, setRecordCount] = React.useState<number>(0);
|
||||
const [listOption, setListOption] = React.useState({});
|
||||
const [listSort, setListSort] = React.useState({});
|
||||
|
||||
React.useEffect(() => {}, []);
|
||||
//
|
||||
const reloadRows = async (): Promise<void> => {
|
||||
try {
|
||||
const models: ListResult<RecordModel> = await listWithOption({
|
||||
currentPage: currentPage + 1,
|
||||
rowsPerPage,
|
||||
listOption,
|
||||
});
|
||||
|
||||
const { items, totalItems } = models;
|
||||
const tempLpCategories: LpCategory[] = items.map((lt) => {
|
||||
return { ...defaultLpCategory, ...lt };
|
||||
});
|
||||
|
||||
setLpCategoriesData(tempLpCategories);
|
||||
setRecordCount(totalItems);
|
||||
setF(tempLpCategories);
|
||||
} catch (error) {
|
||||
//
|
||||
} finally {
|
||||
setShowLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
void reloadRows();
|
||||
}, [currentPage, rowsPerPage, listOption]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let tempFilter = [],
|
||||
tempSortDir = '';
|
||||
|
||||
if (visible) {
|
||||
tempFilter.push(`visible = "${visible}"`);
|
||||
}
|
||||
|
||||
if (sortDir) {
|
||||
tempSortDir = `-created`;
|
||||
}
|
||||
|
||||
if (name) {
|
||||
tempFilter.push(`name ~ "%${name}%"`);
|
||||
}
|
||||
|
||||
if (type) {
|
||||
tempFilter.push(`type ~ "%${type}%"`);
|
||||
}
|
||||
|
||||
setListOption({
|
||||
filter: tempFilter.join(' && '),
|
||||
sort: tempSortDir,
|
||||
//
|
||||
});
|
||||
}, [visible, sortDir, name, type]);
|
||||
|
||||
if (f.length === 0 || showLoading) return <FormLoading />;
|
||||
|
||||
if (showError)
|
||||
return (
|
||||
<ErrorDisplay message={t('unable-to-process-request')} code="500" details={t('detailed-error-information')} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -44,29 +115,45 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
<Stack spacing={4}>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
||||
<Box sx={{ flex: '1 1 auto' }}>
|
||||
<Typography variant="h4">{t('listening-practice')}</Typography>
|
||||
<Typography variant="h4">{t('list.title')}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
<LoadingButton
|
||||
loading={isLoadingAddPage}
|
||||
onClick={(): void => {
|
||||
setIsLoadingAddPage(true);
|
||||
router.push(paths.dashboard.lp_categories.create);
|
||||
}}
|
||||
startIcon={<PlusIcon />}
|
||||
variant="contained"
|
||||
>
|
||||
{t('add')}
|
||||
</Button>
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
<LpCategoriesSelectionProvider LpCategories={lpCategories}>
|
||||
<LpCategoriesSelectionProvider LpCategories={f}>
|
||||
<Card>
|
||||
<LpCategoriesFilters filters={{ email, phone, status }} sortDir={sortDir} />
|
||||
<LpCategoriesFilters
|
||||
filters={{ email, phone, status }}
|
||||
fullData={f}
|
||||
sortDir={sortDir}
|
||||
//
|
||||
/>
|
||||
<Divider />
|
||||
<Box sx={{ overflowX: 'auto' }}>
|
||||
<LpCategoriesTable rows={lpCategories} />
|
||||
<LpCategoriesTable
|
||||
rows={f}
|
||||
//
|
||||
/>
|
||||
</Box>
|
||||
<Divider />
|
||||
<LpCategoriesPagination count={lpCategories.length + 100} page={0} />
|
||||
<LpCategoriesPagination
|
||||
count={recordCount}
|
||||
page={currentPage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
setPage={setCurrentPage}
|
||||
setRowsPerPage={setRowsPerPage}
|
||||
/>
|
||||
</Card>
|
||||
</LpCategoriesSelectionProvider>
|
||||
</Stack>
|
||||
@@ -86,7 +173,7 @@ function applySort(row: LpCategory[], sortDir: 'asc' | 'desc' | undefined): LpCa
|
||||
});
|
||||
}
|
||||
|
||||
function applyFilters(row: LpCategory[], { email, phone, status }: Filters): LpCategory[] {
|
||||
function applyFilters(row: LpCategory[], { email, phone, status, name, visible }: Filters): LpCategory[] {
|
||||
return row.filter((item) => {
|
||||
if (email) {
|
||||
if (!item.email?.toLowerCase().includes(email.toLowerCase())) {
|
||||
@@ -106,10 +193,31 @@ function applyFilters(row: LpCategory[], { email, phone, status }: Filters): LpC
|
||||
}
|
||||
}
|
||||
|
||||
if (name) {
|
||||
if (!item.name?.toLowerCase().includes(name.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
if (!item.visible?.toLowerCase().includes(visible.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
interface PageProps {
|
||||
searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string };
|
||||
searchParams: {
|
||||
email?: string;
|
||||
phone?: string;
|
||||
sortDir?: 'asc' | 'desc';
|
||||
status?: string;
|
||||
name?: string;
|
||||
visible?: string;
|
||||
type?: string;
|
||||
//
|
||||
};
|
||||
}
|
||||
|
@@ -39,9 +39,11 @@ function ErrorDetails({ details }: { details: string }): React.JSX.Element {
|
||||
|
||||
<Collapse in={expanded}>
|
||||
<Box sx={{ mt: 1, p: 1, bgcolor: 'background.paper' }}>
|
||||
<Typography variant="body2" component="pre" sx={{ whiteSpace: 'pre-wrap' }}>
|
||||
{details}
|
||||
</Typography>
|
||||
<pre>
|
||||
<Typography variant="body2" component="pre" sx={{ whiteSpace: 'pre-wrap' }}>
|
||||
{details}
|
||||
</Typography>
|
||||
</pre>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
|
@@ -24,9 +24,8 @@ import { pb } from '@/lib/pb';
|
||||
import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button';
|
||||
import { Option } from '@/components/core/option';
|
||||
|
||||
// import type { LessonType } from './ILessonType';
|
||||
import { useLessonTypesSelection } from './lesson-types-selection-context';
|
||||
import { LessonType } from './types';
|
||||
import type { LessonType } from './types';
|
||||
|
||||
export interface Filters {
|
||||
email?: string;
|
||||
@@ -61,18 +60,6 @@ export function LessonTypesFilters({
|
||||
|
||||
const selection = useLessonTypesSelection();
|
||||
|
||||
function getVisible(): number {
|
||||
return fullData.reduce((count, item: LessonType) => {
|
||||
return item.visible === 'visible' ? count + 1 : count;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function getHidden(): number {
|
||||
return fullData.reduce((count, item: LessonType) => {
|
||||
return item.visible === 'hidden' ? count + 1 : count;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// The tabs should be generated using API data.
|
||||
const tabs = [
|
||||
{ label: t('All'), value: '', count: totalCount },
|
||||
@@ -195,7 +182,13 @@ export function LessonTypesFilters({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Tabs onChange={handleVisibleChange} sx={{ px: 3 }} value={visible ?? ''} variant="scrollable">
|
||||
<Tabs
|
||||
onChange={handleVisibleChange}
|
||||
sx={{ px: 3 }}
|
||||
value={visible ?? ''}
|
||||
variant="scrollable"
|
||||
//
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<Tab
|
||||
icon={<Chip label={tab.count} size="small" variant="soft" />}
|
||||
|
@@ -0,0 +1,41 @@
|
||||
import { NO_NUM, NO_VALUE } from '@/constants';
|
||||
import type { RecordModel } from 'pocketbase';
|
||||
|
||||
import { CreateForm, LpCategory } from '@/types/LpCategory';
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
|
||||
// import type { CreateForm, LpCategory } from './types';
|
||||
|
||||
export const LpCategoryCreateFormDefault: CreateForm = {
|
||||
name: '',
|
||||
description: '',
|
||||
isActive: true,
|
||||
order: 1,
|
||||
imageUrl: '',
|
||||
};
|
||||
|
||||
export const defaultLpCategory: LpCategory = {
|
||||
id: '',
|
||||
name: '',
|
||||
description: '',
|
||||
isActive: true,
|
||||
order: 1,
|
||||
imageUrl: '',
|
||||
createdAt: dayjs().toDate(),
|
||||
updatedAt: dayjs().toDate(),
|
||||
visible: 'active',
|
||||
status: '',
|
||||
};
|
||||
|
||||
export const emptyLpCategory: LpCategory = {
|
||||
...defaultLpCategory,
|
||||
isActive: false,
|
||||
};
|
||||
|
||||
export const LpCategoryDefaultValue = {
|
||||
createForm: LpCategoryCreateFormDefault,
|
||||
default: defaultLpCategory,
|
||||
empty: emptyLpCategory,
|
||||
};
|
||||
|
||||
export default LpCategoryDefaultValue;
|
@@ -3,6 +3,8 @@
|
||||
import * as React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import GetAllCount from '@/db/QuizListenings/GetAllCount';
|
||||
import GetHiddenCount from '@/db/QuizListenings/GetHiddenCount';
|
||||
import GetVisibleCount from '@/db/QuizListenings/GetVisibleCount';
|
||||
import Button from '@mui/material/Button';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Divider from '@mui/material/Divider';
|
||||
@@ -16,41 +18,56 @@ import Tabs from '@mui/material/Tabs';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { LpCategory } from '@/types/LpCategory';
|
||||
import { paths } from '@/paths';
|
||||
import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button';
|
||||
import { Option } from '@/components/core/option';
|
||||
|
||||
import { useLpCategoriesSelection } from './lp-categories-selection-context';
|
||||
|
||||
// The tabs should be generated using API data.
|
||||
const tabs1 = [
|
||||
{ label: 'All', value: '', count: 5 },
|
||||
{ label: 'Active', value: 'active', count: 3 },
|
||||
{ label: 'Pending', value: 'pending', count: 1 },
|
||||
{ label: 'Blocked', value: 'blocked', count: 1 },
|
||||
] as const;
|
||||
|
||||
export interface Filters {
|
||||
email?: string;
|
||||
phone?: string;
|
||||
status?: string;
|
||||
name?: string;
|
||||
visible?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export type SortDir = 'asc' | 'desc';
|
||||
|
||||
export interface CustomersFiltersProps {
|
||||
export interface LpCategoriesFiltersProps {
|
||||
filters?: Filters;
|
||||
sortDir?: SortDir;
|
||||
fullData: LpCategory[];
|
||||
}
|
||||
|
||||
export function LpCategoriesFilters({ filters = {}, sortDir = 'desc' }: CustomersFiltersProps): React.JSX.Element {
|
||||
export function LpCategoriesFilters({
|
||||
filters = {},
|
||||
sortDir = 'desc',
|
||||
fullData,
|
||||
}: LpCategoriesFiltersProps): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const { email, phone, status } = filters;
|
||||
const { email, phone, status, name, visible, type } = filters;
|
||||
|
||||
const [totalCount, setTotalCount] = React.useState<number>(0);
|
||||
const [visibleCount, setVisibleCount] = React.useState<number>(0);
|
||||
const [hiddenCount, setHiddenCount] = React.useState<number>(0);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const selection = useLpCategoriesSelection();
|
||||
|
||||
// The tabs should be generated using API data.
|
||||
const tabs = [
|
||||
{ label: t('All'), value: '', count: totalCount },
|
||||
// { label: 'Active', value: 'active', count: 3 },
|
||||
// { label: 'Pending', value: 'pending', count: 1 },
|
||||
// { label: 'Blocked', value: 'blocked', count: 1 },
|
||||
{ label: t('visible'), value: 'visible', count: visibleCount },
|
||||
{ label: t('hidden'), value: 'hidden', count: hiddenCount },
|
||||
] as const;
|
||||
|
||||
const updateSearchParams = React.useCallback(
|
||||
(newFilters: Filters, newSortDir: SortDir): void => {
|
||||
const searchParams = new URLSearchParams();
|
||||
@@ -71,6 +88,18 @@ export function LpCategoriesFilters({ filters = {}, sortDir = 'desc' }: Customer
|
||||
searchParams.set('phone', newFilters.phone);
|
||||
}
|
||||
|
||||
if (newFilters.name) {
|
||||
searchParams.set('name', newFilters.name);
|
||||
}
|
||||
|
||||
if (newFilters.type) {
|
||||
searchParams.set('type', newFilters.type);
|
||||
}
|
||||
|
||||
if (newFilters.visible) {
|
||||
searchParams.set('visible', newFilters.visible);
|
||||
}
|
||||
|
||||
router.push(`${paths.dashboard.lp_categories.list}?${searchParams.toString()}`);
|
||||
},
|
||||
[router]
|
||||
@@ -87,6 +116,27 @@ export function LpCategoriesFilters({ filters = {}, sortDir = 'desc' }: Customer
|
||||
[updateSearchParams, filters, sortDir]
|
||||
);
|
||||
|
||||
const handleVisibleChange = React.useCallback(
|
||||
(_: React.SyntheticEvent, value: string) => {
|
||||
updateSearchParams({ ...filters, visible: value }, sortDir);
|
||||
},
|
||||
[updateSearchParams, filters, sortDir]
|
||||
);
|
||||
|
||||
const handleNameChange = React.useCallback(
|
||||
(value?: string) => {
|
||||
updateSearchParams({ ...filters, name: value }, sortDir);
|
||||
},
|
||||
[updateSearchParams, filters, sortDir]
|
||||
);
|
||||
|
||||
const handleTypeChange = React.useCallback(
|
||||
(value?: string) => {
|
||||
updateSearchParams({ ...filters, type: value }, sortDir);
|
||||
},
|
||||
[updateSearchParams, filters, sortDir]
|
||||
);
|
||||
|
||||
const handleEmailChange = React.useCallback(
|
||||
(value?: string) => {
|
||||
updateSearchParams({ ...filters, email: value }, sortDir);
|
||||
@@ -108,28 +158,35 @@ export function LpCategoriesFilters({ filters = {}, sortDir = 'desc' }: Customer
|
||||
[updateSearchParams, filters]
|
||||
);
|
||||
|
||||
const [allCount, setAllCount] = React.useState<number>(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
async function fetchAllCount(): Promise<void> {
|
||||
setAllCount(await GetAllCount());
|
||||
}
|
||||
const fetchCount = async (): Promise<void> => {
|
||||
try {
|
||||
const tc = await GetAllCount();
|
||||
setTotalCount(tc);
|
||||
|
||||
void fetchAllCount();
|
||||
const vc = await GetVisibleCount();
|
||||
setVisibleCount(vc);
|
||||
|
||||
const hc = await GetHiddenCount();
|
||||
setHiddenCount(hc);
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
};
|
||||
void fetchCount();
|
||||
}, []);
|
||||
|
||||
const tabs = [
|
||||
{ label: 'All', value: '', count: allCount },
|
||||
{ label: 'Active', value: 'active', count: 3 },
|
||||
{ label: 'Pending', value: 'pending', count: 1 },
|
||||
{ label: 'Blocked', value: 'blocked', count: 1 },
|
||||
] as const;
|
||||
|
||||
const hasFilters = status || email || phone;
|
||||
const hasFilters = status || email || phone || visible || name || type;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Tabs onChange={handleStatusChange} sx={{ px: 3 }} value={status ?? ''} variant="scrollable">
|
||||
<Tabs
|
||||
onChange={handleVisibleChange}
|
||||
sx={{ px: 3 }}
|
||||
value={visible ?? ''}
|
||||
variant="scrollable"
|
||||
//
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<Tab
|
||||
icon={<Chip label={tab.count} size="small" variant="soft" />}
|
||||
@@ -146,35 +203,37 @@ export function LpCategoriesFilters({ filters = {}, sortDir = 'desc' }: Customer
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flexWrap: 'wrap', px: 3, py: 2 }}>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto', flexWrap: 'wrap' }}>
|
||||
<FilterButton
|
||||
displayValue={email}
|
||||
label="Email"
|
||||
displayValue={name}
|
||||
label={t('Name')}
|
||||
onFilterApply={(value) => {
|
||||
handleEmailChange(value as string);
|
||||
handleNameChange(value as string);
|
||||
}}
|
||||
onFilterDelete={() => {
|
||||
handleEmailChange();
|
||||
handleNameChange();
|
||||
}}
|
||||
popover={<EmailFilterPopover />}
|
||||
value={email}
|
||||
popover={<NameFilterPopover />}
|
||||
value={name}
|
||||
/>
|
||||
|
||||
<FilterButton
|
||||
displayValue={phone}
|
||||
label="Phone number"
|
||||
displayValue={type}
|
||||
label={t('Type')}
|
||||
onFilterApply={(value) => {
|
||||
handlePhoneChange(value as string);
|
||||
handleTypeChange(value as string);
|
||||
}}
|
||||
onFilterDelete={() => {
|
||||
handlePhoneChange();
|
||||
handleTypeChange();
|
||||
}}
|
||||
popover={<PhoneFilterPopover />}
|
||||
value={phone}
|
||||
popover={<TypeFilterPopover />}
|
||||
value={type}
|
||||
/>
|
||||
|
||||
{hasFilters ? <Button onClick={handleClearFilters}>{t('Clear filters')}</Button> : null}
|
||||
</Stack>
|
||||
{selection.selectedAny ? (
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{selection.selected.size} selected
|
||||
{selection.selected.size} {t('selected')}
|
||||
</Typography>
|
||||
<Button color="error" variant="contained">
|
||||
{t('Delete')}
|
||||
@@ -190,7 +249,7 @@ export function LpCategoriesFilters({ filters = {}, sortDir = 'desc' }: Customer
|
||||
);
|
||||
}
|
||||
|
||||
function EmailFilterPopover(): React.JSX.Element {
|
||||
function TypeFilterPopover(): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
|
||||
const [value, setValue] = React.useState<string>('');
|
||||
@@ -200,7 +259,7 @@ function EmailFilterPopover(): React.JSX.Element {
|
||||
}, [initialValue]);
|
||||
|
||||
return (
|
||||
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title={t('Filter by email')}>
|
||||
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title={t('Filter by type')}>
|
||||
<FormControl>
|
||||
<OutlinedInput
|
||||
onChange={(event) => {
|
||||
@@ -226,8 +285,78 @@ function EmailFilterPopover(): React.JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
function NameFilterPopover(): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
|
||||
const [value, setValue] = React.useState<string>('');
|
||||
|
||||
React.useEffect(() => {
|
||||
setValue((initialValue as string | undefined) ?? '');
|
||||
}, [initialValue]);
|
||||
|
||||
return (
|
||||
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title={t('Filter by name')}>
|
||||
<FormControl>
|
||||
<OutlinedInput
|
||||
onChange={(event) => {
|
||||
setValue(event.target.value);
|
||||
}}
|
||||
onKeyUp={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
onApply(value);
|
||||
}
|
||||
}}
|
||||
value={value}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onApply(value);
|
||||
}}
|
||||
variant="contained"
|
||||
>
|
||||
{t('Apply')}
|
||||
</Button>
|
||||
</FilterPopover>
|
||||
);
|
||||
}
|
||||
|
||||
function EmailFilterPopover(): React.JSX.Element {
|
||||
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
|
||||
const [value, setValue] = React.useState<string>('');
|
||||
|
||||
React.useEffect(() => {
|
||||
setValue((initialValue as string | undefined) ?? '');
|
||||
}, [initialValue]);
|
||||
|
||||
return (
|
||||
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title="Filter by email">
|
||||
<FormControl>
|
||||
<OutlinedInput
|
||||
onChange={(event) => {
|
||||
setValue(event.target.value);
|
||||
}}
|
||||
onKeyUp={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
onApply(value);
|
||||
}
|
||||
}}
|
||||
value={value}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onApply(value);
|
||||
}}
|
||||
variant="contained"
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</FilterPopover>
|
||||
);
|
||||
}
|
||||
|
||||
function PhoneFilterPopover(): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
|
||||
const [value, setValue] = React.useState<string>('');
|
||||
|
||||
@@ -236,7 +365,7 @@ function PhoneFilterPopover(): React.JSX.Element {
|
||||
}, [initialValue]);
|
||||
|
||||
return (
|
||||
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title={t('Filter by phone number')}>
|
||||
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title="Filter by phone number">
|
||||
<FormControl>
|
||||
<OutlinedInput
|
||||
onChange={(event) => {
|
||||
@@ -248,7 +377,6 @@ function PhoneFilterPopover(): React.JSX.Element {
|
||||
}
|
||||
}}
|
||||
value={value}
|
||||
placeholder={t('Enter phone number')}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
@@ -257,7 +385,7 @@ function PhoneFilterPopover(): React.JSX.Element {
|
||||
}}
|
||||
variant="contained"
|
||||
>
|
||||
{t('Apply')}
|
||||
Apply
|
||||
</Button>
|
||||
</FilterPopover>
|
||||
);
|
||||
|
@@ -7,25 +7,42 @@ function noop(): void {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
interface CustomersPaginationProps {
|
||||
interface LpCategoriesPaginationProps {
|
||||
count: number;
|
||||
page: number;
|
||||
setPage: (page: number) => void;
|
||||
setRowsPerPage: (page: number) => void;
|
||||
rowsPerPage: number;
|
||||
}
|
||||
|
||||
export function LpCategoriesPagination({ count, page }: CustomersPaginationProps): React.JSX.Element {
|
||||
export function LpCategoriesPagination({
|
||||
count,
|
||||
page,
|
||||
setPage,
|
||||
setRowsPerPage,
|
||||
rowsPerPage,
|
||||
}: LpCategoriesPaginationProps): React.JSX.Element {
|
||||
// You should implement the pagination using a similar logic as the filters.
|
||||
// Note that when page change, you should keep the filter search params.
|
||||
const handleChangePage = (event: unknown, newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value));
|
||||
// console.log(parseInt(event.target.value));
|
||||
};
|
||||
|
||||
return (
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={count}
|
||||
onPageChange={noop}
|
||||
onRowsPerPageChange={noop}
|
||||
page={page}
|
||||
rowsPerPage={5}
|
||||
rowsPerPage={rowsPerPage}
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
//
|
||||
onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@@ -29,77 +29,6 @@ import type { ColumnDef } from '@/components/core/data-table';
|
||||
import type { LessonCategory } from '../lesson_category/types';
|
||||
import { useLpCategoriesSelection } from './lp-categories-selection-context';
|
||||
|
||||
const columns1 = [
|
||||
{
|
||||
formatter: (row): React.JSX.Element => (
|
||||
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
||||
<Avatar src={row.avatar} />{' '}
|
||||
<div>
|
||||
<Link
|
||||
color="inherit"
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.customers.details('1')}
|
||||
sx={{ whiteSpace: 'nowrap' }}
|
||||
variant="subtitle2"
|
||||
>
|
||||
{row.name}
|
||||
</Link>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{row.email}
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
),
|
||||
name: 'Name',
|
||||
width: '250px',
|
||||
},
|
||||
{
|
||||
formatter: (row): React.JSX.Element => (
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<LinearProgress sx={{ flex: '1 1 auto' }} value={row.quota} variant="determinate" />
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
),
|
||||
name: 'Quota',
|
||||
width: '250px',
|
||||
},
|
||||
{ field: 'phone', name: 'Phone number', width: '150px' },
|
||||
{
|
||||
formatter(row) {
|
||||
return dayjs(row.createdAt).format('MMM D, YYYY h:mm A');
|
||||
},
|
||||
name: 'Created at',
|
||||
width: '200px',
|
||||
},
|
||||
{
|
||||
formatter: (row): React.JSX.Element => {
|
||||
const mapping = {
|
||||
active: { label: 'Active', icon: <CheckCircleIcon color="var(--mui-palette-success-main)" weight="fill" /> },
|
||||
blocked: { label: 'Blocked', icon: <MinusIcon color="var(--mui-palette-error-main)" /> },
|
||||
pending: { label: 'Pending', icon: <ClockIcon color="var(--mui-palette-warning-main)" weight="fill" /> },
|
||||
} as const;
|
||||
const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null };
|
||||
|
||||
return <Chip icon={icon} label={label} size="small" variant="outlined" />;
|
||||
},
|
||||
name: 'Status',
|
||||
width: '150px',
|
||||
},
|
||||
{
|
||||
formatter: (): React.JSX.Element => (
|
||||
<IconButton component={RouterLink} href={paths.dashboard.customers.details('1')}>
|
||||
<PencilSimpleIcon />
|
||||
</IconButton>
|
||||
),
|
||||
name: 'Actions',
|
||||
hideName: true,
|
||||
width: '100px',
|
||||
align: 'right',
|
||||
},
|
||||
] satisfies ColumnDef<LpCategory>[];
|
||||
|
||||
function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LpCategory>[] {
|
||||
return [
|
||||
{
|
||||
@@ -134,7 +63,9 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LpCateg
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<LinearProgress sx={{ flex: '1 1 auto' }} value={row.quota} variant="determinate" />
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)}
|
||||
{new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(
|
||||
row?.quota ? row.quota / 100 : 0
|
||||
)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
),
|
||||
@@ -153,7 +84,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LpCateg
|
||||
pending: { label: 'Pending', icon: <ClockIcon color="var(--mui-palette-warning-main)" weight="fill" /> },
|
||||
NA: { label: 'NA', icon: <ClockIcon color="var(--mui-palette-warning-main)" weight="fill" /> },
|
||||
} as const;
|
||||
const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null };
|
||||
const { label, icon } = mapping[row.visible] ?? { label: 'Unknown', icon: null };
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -211,7 +142,8 @@ export interface LpCategoriesTableProps {
|
||||
}
|
||||
|
||||
function getCatImageFromId(row: LpCategory): string | undefined {
|
||||
return `http://127.0.0.1:8090/api/files/${row.collectionId}/${row.id}/${row.cat_image}`;
|
||||
return 'get cat image from id';
|
||||
// return `http://127.0.0.1:8090/api/files/${'row.collectionId'}/${'row.id'}/${'row.cat_image'}`;
|
||||
}
|
||||
|
||||
export function LpCategoriesTable({ rows }: LpCategoriesTableProps): React.JSX.Element {
|
||||
|
@@ -1,3 +1,4 @@
|
||||
// RULES: COL_<COLLECTION_NAME> = "<name in dbml file>"
|
||||
const COL_LESSON_TYPES = 'LessonsTypes';
|
||||
const COL_LESSON_CATEGORIES = 'LessonsCategories';
|
||||
const NO_VALUE = 'NO_VALUE';
|
||||
|
@@ -1,14 +1,26 @@
|
||||
# AI GUIDELINE
|
||||
|
||||
## Background information and References
|
||||
## getting started
|
||||
|
||||
please read `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006/schema.dbml`
|
||||
no need to reply me what you are going on and your digest in this phase.
|
||||
just reply me "OK" when done
|
||||
|
||||
please read `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/schema.json`
|
||||
base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project`
|
||||
|
||||
please look into the md files in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/_AI_GUIDELINE`
|
||||
please read `<base_dir>/001_documentation/Requirements/REQ0006/schema.dbml`
|
||||
this is file in dbml syntax state the main database
|
||||
|
||||
please read, remember and link up the ideas, i will tell you the task afterwards
|
||||
please read `<base_dir>/002_source/cms/src/db/schema.json`
|
||||
this is the file of live pocketbase schema output
|
||||
|
||||
please look into the md files in folder `<base_dir>/002_source/cms/_AI_GUIDELINE`
|
||||
|
||||
please read, remember and link up the ideas in file stated above,
|
||||
i will tell you the task afterwards
|
||||
|
||||
this is now not in debug phase,
|
||||
so, no need to reply me what you are going on or your insight throught the prompt.
|
||||
just reply me "OK" when done
|
||||
|
||||
---
|
||||
|
||||
@@ -45,3 +57,25 @@ when you draft coding, review file and append with `.tsx.draft`
|
||||
## tasks
|
||||
|
||||
Thanks
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
please take a look in `schema.dbml` and `schema.json`,
|
||||
associate the collection from json file to the table in dbml file
|
||||
|
||||
please modify the `schema.dbml` to align with `schema.json`
|
||||
|
||||
to the collection `QuizLPCategories` align the dbml file in the previous prompt
|
||||
|
||||
|
||||
---
|
||||
|
||||
please revise
|
||||
|
||||
please revise
|
||||
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx` `interface LpCategory`
|
||||
|
||||
to the collection `QuizLPCategories` align the dbml file in the previous prompt
|
||||
|
8
002_source/cms/src/db/QuizListenings/GetById.tsx
Normal file
8
002_source/cms/src/db/QuizListenings/GetById.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { COL_QUIZ_LISTENINGS } from '@/constants';
|
||||
import type { RecordModel } from 'pocketbase';
|
||||
|
||||
import { pb } from '@/lib/pb';
|
||||
|
||||
export default function getQuizListeningById(id: string): Promise<RecordModel> {
|
||||
return pb.collection(COL_QUIZ_LISTENINGS).getOne(id);
|
||||
}
|
14
002_source/cms/src/db/QuizListenings/GetHiddenCount.tsx
Normal file
14
002_source/cms/src/db/QuizListenings/GetHiddenCount.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
// REQ0006
|
||||
import { COL_QUIZ_LISTENINGS } from '@/constants';
|
||||
|
||||
import { pb } from '@/lib/pb';
|
||||
|
||||
export default async function GetHiddenCount(): Promise<number> {
|
||||
try {
|
||||
const result = await pb.collection(COL_QUIZ_LISTENINGS).getList(1, 9999, { filter: 'visible = "hidden"' });
|
||||
const { totalItems: count } = result;
|
||||
return count;
|
||||
} catch (error) {
|
||||
return 0;
|
||||
}
|
||||
}
|
14
002_source/cms/src/db/QuizListenings/GetVisibleCount.tsx
Normal file
14
002_source/cms/src/db/QuizListenings/GetVisibleCount.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
// REQ0006
|
||||
import { COL_QUIZ_LISTENINGS } from '@/constants';
|
||||
|
||||
import { pb } from '@/lib/pb';
|
||||
|
||||
export default async function GetVisibleCount(): Promise<number> {
|
||||
try {
|
||||
const result = await pb.collection(COL_QUIZ_LISTENINGS).getList(1, 9999, { filter: 'visible = "visible"' });
|
||||
const { totalItems: count } = result;
|
||||
return count;
|
||||
} catch (error) {
|
||||
return 0;
|
||||
}
|
||||
}
|
22
002_source/cms/src/db/QuizListenings/ListWithOption.tsx
Normal file
22
002_source/cms/src/db/QuizListenings/ListWithOption.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { COL_QUIZ_LISTENINGS } from '@/constants';
|
||||
import type { ListResult, RecordModel } from 'pocketbase';
|
||||
|
||||
import { pb } from '@/lib/pb';
|
||||
|
||||
interface ListWithOptionParams {
|
||||
currentPage: number;
|
||||
rowsPerPage: number;
|
||||
listOption?: {
|
||||
filter?: string;
|
||||
sort?: string;
|
||||
expand?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function listWithOption({
|
||||
currentPage,
|
||||
rowsPerPage,
|
||||
listOption = {},
|
||||
}: ListWithOptionParams): Promise<ListResult<RecordModel>> {
|
||||
return pb.collection(COL_QUIZ_LISTENINGS).getList(currentPage + 1, rowsPerPage, listOption);
|
||||
}
|
@@ -1,13 +1,59 @@
|
||||
export interface LpCategory {
|
||||
cat_image?: string;
|
||||
isEmpty?: boolean;
|
||||
collectionId?: string;
|
||||
id: string;
|
||||
name: string;
|
||||
avatar?: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
quota: number;
|
||||
status: 'pending' | 'active' | 'blocked';
|
||||
createdAt: Date;
|
||||
/*
|
||||
RULES:
|
||||
please follow the naming convention
|
||||
*/
|
||||
|
||||
export interface LpCategoryFormProps {
|
||||
name: string; // corresponds to cat_name
|
||||
description: string; // additional business field
|
||||
isActive: boolean; // additional business field
|
||||
order: number; // corresponds to pos
|
||||
imageUrl: string; // corresponds to cat_image
|
||||
initAnswer?: any; // corresponds to init_answer
|
||||
}
|
||||
|
||||
export interface RestLpCategoryUpdateForm {
|
||||
id: string;
|
||||
data: LpCategoryFormProps;
|
||||
}
|
||||
|
||||
// RULES: this one should be obsoleted, no need to take care
|
||||
// export function safeAssignment(inTemp: LpCategory | RecordModel): LpCategory {
|
||||
// const { id, name, description, isActive, order, imageUrl, createdAt, updatedAt, initAnswer } = {
|
||||
// ...defaultLpCategory,
|
||||
// ...inTemp,
|
||||
// };
|
||||
// return {
|
||||
// id,
|
||||
// name,
|
||||
// description,
|
||||
// isActive,
|
||||
// order,
|
||||
// imageUrl,
|
||||
// createdAt: dayjs(createdAt).toDate(),
|
||||
// updatedAt: updatedAt ? dayjs(updatedAt).toDate() : new Date(),
|
||||
// initAnswer,
|
||||
// visible: 'public', // default value
|
||||
// };
|
||||
// }
|
||||
|
||||
export interface LpCategory {
|
||||
isEmpty?: boolean;
|
||||
id: string;
|
||||
name: string; // corresponds to cat_name
|
||||
description: string; // additional business field
|
||||
isActive: boolean; // additional business field
|
||||
order: number; // corresponds to pos
|
||||
imageUrl: string; // corresponds to cat_image
|
||||
createdAt: Date;
|
||||
updatedAt: Date; // new field
|
||||
initAnswer?: any; // corresponds to init_answer
|
||||
visible: 'active' | 'blocked' | 'pending' | 'NA'; // additional business field
|
||||
quota?: number;
|
||||
//
|
||||
email?: string;
|
||||
phone?: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export type CreateForm = LpCategoryFormProps;
|
||||
|
Reference in New Issue
Block a user