update build ok,
This commit is contained in:
@@ -11,6 +11,7 @@ dashboard.lessonCategories.edit.name
|
|||||||
dashboard.lessonCategories.edit.type
|
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`
|
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
|
please refactor `common.json` to smaller translation files, thanks
|
||||||
e.g. `lessonTypes` -> `lesson_type`
|
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": "新增",
|
||||||
"add": "新加",
|
"listening-practice": "聽力練習",
|
||||||
"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 * as React from 'react';
|
||||||
import RouterLink from 'next/link';
|
import RouterLink from 'next/link';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import Avatar from '@mui/material/Avatar';
|
|
||||||
import Box from '@mui/material/Box';
|
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 Link from '@mui/material/Link';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
import Grid from '@mui/material/Unstable_Grid2';
|
import Grid from '@mui/material/Unstable_Grid2';
|
||||||
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
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 type { RecordModel } from 'pocketbase';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { paths } from '@/paths';
|
import { paths } from '@/paths';
|
||||||
import { dayjs } from '@/lib/dayjs';
|
|
||||||
import { logger } from '@/lib/default-logger';
|
import { logger } from '@/lib/default-logger';
|
||||||
import { pb } from '@/lib/pb';
|
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 { toast } from '@/components/core/toaster';
|
||||||
import ErrorDisplay from '@/components/dashboard/error';
|
import ErrorDisplay from '@/components/dashboard/error';
|
||||||
import { defaultLessonType, LessonTypeDefaultValue } from '@/components/dashboard/lesson_type/_constants';
|
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 { 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 { type LessonType } from '@/components/dashboard/lesson_type/types';
|
||||||
import FormLoading from '@/components/loading';
|
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 {
|
export default function Page(): React.JSX.Element {
|
||||||
const { t } = useTranslation(['common', 'lesson_type']);
|
const { t } = useTranslation(['lesson_type']);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
//
|
//
|
||||||
const { type_id: typeId } = useParams<{ type_id: string }>();
|
const { type_id: typeId } = useParams<{ type_id: string }>();
|
||||||
|
|
||||||
//
|
//
|
||||||
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
||||||
const [showError, setShowError] = React.useState<boolean>(false);
|
const [showError, setShowError] = React.useState<boolean>(false);
|
||||||
|
const [errorDetails, setErrorDetails] = React.useState('');
|
||||||
|
|
||||||
//
|
//
|
||||||
const [showLessonType, setShowLessonType] = React.useState<LessonType>(LessonTypeDefaultValue);
|
const [showLessonType, setShowLessonType] = React.useState<LessonType>(LessonTypeDefaultValue);
|
||||||
|
|
||||||
@@ -73,7 +56,7 @@ export default function Page(): React.JSX.Element {
|
|||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
toast(t('dashboard.lessonTypes.list.error'));
|
toast(t('dashboard.lessonTypes.list.error'));
|
||||||
|
setErrorDetails(err);
|
||||||
setShowError(true);
|
setShowError(true);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@@ -89,7 +72,7 @@ export default function Page(): React.JSX.Element {
|
|||||||
<ErrorDisplay
|
<ErrorDisplay
|
||||||
message={t('error.unable-to-process-request', { ns: 'common' })}
|
message={t('error.unable-to-process-request', { ns: 'common' })}
|
||||||
code="500"
|
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"
|
variant="subtitle2"
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||||
{t('list.title', { ns: 'lesson_type' })}
|
{t('list.title')}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
||||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto' }}>
|
<SampleTitleCard />
|
||||||
<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>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Grid container spacing={4}>
|
<Grid container spacing={4}>
|
||||||
<Grid lg={4} xs={12}>
|
<Grid lg={4} xs={12}>
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<Card>
|
<BasicDetailCard lpCatId={showLessonType.id} handleEditClick={handleEditClick} />
|
||||||
<CardHeader
|
<SampleSecurityCard />
|
||||||
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>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid lg={8} xs={12}>
|
<Grid lg={8} xs={12}>
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<Payments
|
<SamplePaymentCard />
|
||||||
ordersValue={2069.48}
|
<SampleAddressCard />
|
||||||
payments={[
|
<Notifications notifications={SampleNotifications} />
|
||||||
{
|
|
||||||
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(),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@@ -40,12 +40,9 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
||||||
const [showError, setShowError] = React.useState<boolean>(false);
|
const [showError, setShowError] = React.useState<boolean>(false);
|
||||||
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
||||||
//
|
|
||||||
const [f, setF] = React.useState<LessonType[]>([]);
|
const [f, setF] = React.useState<LessonType[]>([]);
|
||||||
const [currentPage, setCurrentPage] = React.useState<number>(0);
|
const [currentPage, setCurrentPage] = React.useState<number>(0);
|
||||||
//
|
|
||||||
const [recordCount, setRecordCount] = React.useState<number>(0);
|
const [recordCount, setRecordCount] = React.useState<number>(0);
|
||||||
|
|
||||||
const [listOption, setListOption] = React.useState({});
|
const [listOption, setListOption] = React.useState({});
|
||||||
const [listSort, setListSort] = 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 }}
|
filters={{ email, phone, status, name, visible, type }}
|
||||||
fullData={lessonTypesData}
|
fullData={lessonTypesData}
|
||||||
sortDir={sortDir}
|
sortDir={sortDir}
|
||||||
|
//
|
||||||
/>
|
/>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Box sx={{ overflowX: 'auto' }}>
|
<Box sx={{ overflowX: 'auto' }}>
|
||||||
<LessonTypesTable reloadRows={reloadRows} rows={f} />
|
<LessonTypesTable
|
||||||
|
reloadRows={reloadRows}
|
||||||
|
rows={f}
|
||||||
|
//
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Divider />
|
<Divider />
|
||||||
<LessonTypesPagination
|
<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 * as React from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
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 Box from '@mui/material/Box';
|
||||||
import Button from '@mui/material/Button';
|
|
||||||
import Card from '@mui/material/Card';
|
import Card from '@mui/material/Card';
|
||||||
import Divider from '@mui/material/Divider';
|
import Divider from '@mui/material/Divider';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||||
|
import type { ListResult, RecordModel } from 'pocketbase';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import type { LpCategory } from '@/types/LpCategory';
|
import type { LpCategory } from '@/types/LpCategory';
|
||||||
import { paths } from '@/paths';
|
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 { LpCategoriesFilters } from '@/components/dashboard/lp_categories/lp-categories-filters';
|
||||||
import type { Filters } 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 { LpCategoriesPagination } from '@/components/dashboard/lp_categories/lp-categories-pagination';
|
||||||
import { LpCategoriesSelectionProvider } from '@/components/dashboard/lp_categories/lp-categories-selection-context';
|
import { LpCategoriesSelectionProvider } from '@/components/dashboard/lp_categories/lp-categories-selection-context';
|
||||||
import { LpCategoriesTable } from '@/components/dashboard/lp_categories/lp-categories-table';
|
import { LpCategoriesTable } from '@/components/dashboard/lp_categories/lp-categories-table';
|
||||||
|
import FormLoading from '@/components/loading';
|
||||||
import { LpCategories } from './lp-categories';
|
|
||||||
|
|
||||||
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||||
const router = useRouter();
|
|
||||||
const { t } = useTranslation(['listening_practice']);
|
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 [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
|
||||||
const lpCategories = applyFilters(sortedCustomers, { email, phone, status });
|
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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -44,29 +115,45 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
||||||
<Box sx={{ flex: '1 1 auto' }}>
|
<Box sx={{ flex: '1 1 auto' }}>
|
||||||
<Typography variant="h4">{t('listening-practice')}</Typography>
|
<Typography variant="h4">{t('list.title')}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
<Button
|
<LoadingButton
|
||||||
onClick={() => {
|
loading={isLoadingAddPage}
|
||||||
|
onClick={(): void => {
|
||||||
|
setIsLoadingAddPage(true);
|
||||||
router.push(paths.dashboard.lp_categories.create);
|
router.push(paths.dashboard.lp_categories.create);
|
||||||
}}
|
}}
|
||||||
startIcon={<PlusIcon />}
|
startIcon={<PlusIcon />}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
>
|
>
|
||||||
{t('add')}
|
{t('add')}
|
||||||
</Button>
|
</LoadingButton>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
<LpCategoriesSelectionProvider LpCategories={lpCategories}>
|
<LpCategoriesSelectionProvider LpCategories={f}>
|
||||||
<Card>
|
<Card>
|
||||||
<LpCategoriesFilters filters={{ email, phone, status }} sortDir={sortDir} />
|
<LpCategoriesFilters
|
||||||
|
filters={{ email, phone, status }}
|
||||||
|
fullData={f}
|
||||||
|
sortDir={sortDir}
|
||||||
|
//
|
||||||
|
/>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Box sx={{ overflowX: 'auto' }}>
|
<Box sx={{ overflowX: 'auto' }}>
|
||||||
<LpCategoriesTable rows={lpCategories} />
|
<LpCategoriesTable
|
||||||
|
rows={f}
|
||||||
|
//
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Divider />
|
<Divider />
|
||||||
<LpCategoriesPagination count={lpCategories.length + 100} page={0} />
|
<LpCategoriesPagination
|
||||||
|
count={recordCount}
|
||||||
|
page={currentPage}
|
||||||
|
rowsPerPage={rowsPerPage}
|
||||||
|
setPage={setCurrentPage}
|
||||||
|
setRowsPerPage={setRowsPerPage}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</LpCategoriesSelectionProvider>
|
</LpCategoriesSelectionProvider>
|
||||||
</Stack>
|
</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) => {
|
return row.filter((item) => {
|
||||||
if (email) {
|
if (email) {
|
||||||
if (!item.email?.toLowerCase().includes(email.toLowerCase())) {
|
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;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PageProps {
|
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}>
|
<Collapse in={expanded}>
|
||||||
<Box sx={{ mt: 1, p: 1, bgcolor: 'background.paper' }}>
|
<Box sx={{ mt: 1, p: 1, bgcolor: 'background.paper' }}>
|
||||||
<Typography variant="body2" component="pre" sx={{ whiteSpace: 'pre-wrap' }}>
|
<pre>
|
||||||
{details}
|
<Typography variant="body2" component="pre" sx={{ whiteSpace: 'pre-wrap' }}>
|
||||||
</Typography>
|
{details}
|
||||||
|
</Typography>
|
||||||
|
</pre>
|
||||||
</Box>
|
</Box>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</Box>
|
</Box>
|
||||||
|
@@ -24,9 +24,8 @@ import { pb } from '@/lib/pb';
|
|||||||
import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button';
|
import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button';
|
||||||
import { Option } from '@/components/core/option';
|
import { Option } from '@/components/core/option';
|
||||||
|
|
||||||
// import type { LessonType } from './ILessonType';
|
|
||||||
import { useLessonTypesSelection } from './lesson-types-selection-context';
|
import { useLessonTypesSelection } from './lesson-types-selection-context';
|
||||||
import { LessonType } from './types';
|
import type { LessonType } from './types';
|
||||||
|
|
||||||
export interface Filters {
|
export interface Filters {
|
||||||
email?: string;
|
email?: string;
|
||||||
@@ -61,18 +60,6 @@ export function LessonTypesFilters({
|
|||||||
|
|
||||||
const selection = useLessonTypesSelection();
|
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.
|
// The tabs should be generated using API data.
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ label: t('All'), value: '', count: totalCount },
|
{ label: t('All'), value: '', count: totalCount },
|
||||||
@@ -195,7 +182,13 @@ export function LessonTypesFilters({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Tabs onChange={handleVisibleChange} sx={{ px: 3 }} value={visible ?? ''} variant="scrollable">
|
<Tabs
|
||||||
|
onChange={handleVisibleChange}
|
||||||
|
sx={{ px: 3 }}
|
||||||
|
value={visible ?? ''}
|
||||||
|
variant="scrollable"
|
||||||
|
//
|
||||||
|
>
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<Tab
|
<Tab
|
||||||
icon={<Chip label={tab.count} size="small" variant="soft" />}
|
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 * as React from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import GetAllCount from '@/db/QuizListenings/GetAllCount';
|
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 Button from '@mui/material/Button';
|
||||||
import Chip from '@mui/material/Chip';
|
import Chip from '@mui/material/Chip';
|
||||||
import Divider from '@mui/material/Divider';
|
import Divider from '@mui/material/Divider';
|
||||||
@@ -16,41 +18,56 @@ import Tabs from '@mui/material/Tabs';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import type { LpCategory } from '@/types/LpCategory';
|
||||||
import { paths } from '@/paths';
|
import { paths } from '@/paths';
|
||||||
import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button';
|
import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button';
|
||||||
import { Option } from '@/components/core/option';
|
import { Option } from '@/components/core/option';
|
||||||
|
|
||||||
import { useLpCategoriesSelection } from './lp-categories-selection-context';
|
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 {
|
export interface Filters {
|
||||||
email?: string;
|
email?: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
|
name?: string;
|
||||||
|
visible?: string;
|
||||||
|
type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SortDir = 'asc' | 'desc';
|
export type SortDir = 'asc' | 'desc';
|
||||||
|
|
||||||
export interface CustomersFiltersProps {
|
export interface LpCategoriesFiltersProps {
|
||||||
filters?: Filters;
|
filters?: Filters;
|
||||||
sortDir?: SortDir;
|
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 { 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 router = useRouter();
|
||||||
|
|
||||||
const selection = useLpCategoriesSelection();
|
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(
|
const updateSearchParams = React.useCallback(
|
||||||
(newFilters: Filters, newSortDir: SortDir): void => {
|
(newFilters: Filters, newSortDir: SortDir): void => {
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
@@ -71,6 +88,18 @@ export function LpCategoriesFilters({ filters = {}, sortDir = 'desc' }: Customer
|
|||||||
searchParams.set('phone', newFilters.phone);
|
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.push(`${paths.dashboard.lp_categories.list}?${searchParams.toString()}`);
|
||||||
},
|
},
|
||||||
[router]
|
[router]
|
||||||
@@ -87,6 +116,27 @@ export function LpCategoriesFilters({ filters = {}, sortDir = 'desc' }: Customer
|
|||||||
[updateSearchParams, filters, sortDir]
|
[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(
|
const handleEmailChange = React.useCallback(
|
||||||
(value?: string) => {
|
(value?: string) => {
|
||||||
updateSearchParams({ ...filters, email: value }, sortDir);
|
updateSearchParams({ ...filters, email: value }, sortDir);
|
||||||
@@ -108,28 +158,35 @@ export function LpCategoriesFilters({ filters = {}, sortDir = 'desc' }: Customer
|
|||||||
[updateSearchParams, filters]
|
[updateSearchParams, filters]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [allCount, setAllCount] = React.useState<number>(0);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
async function fetchAllCount(): Promise<void> {
|
const fetchCount = async (): Promise<void> => {
|
||||||
setAllCount(await GetAllCount());
|
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 = [
|
const hasFilters = status || email || phone || visible || name || type;
|
||||||
{ 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;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Tabs onChange={handleStatusChange} sx={{ px: 3 }} value={status ?? ''} variant="scrollable">
|
<Tabs
|
||||||
|
onChange={handleVisibleChange}
|
||||||
|
sx={{ px: 3 }}
|
||||||
|
value={visible ?? ''}
|
||||||
|
variant="scrollable"
|
||||||
|
//
|
||||||
|
>
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<Tab
|
<Tab
|
||||||
icon={<Chip label={tab.count} size="small" variant="soft" />}
|
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', flexWrap: 'wrap', px: 3, py: 2 }}>
|
||||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto', flexWrap: 'wrap' }}>
|
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto', flexWrap: 'wrap' }}>
|
||||||
<FilterButton
|
<FilterButton
|
||||||
displayValue={email}
|
displayValue={name}
|
||||||
label="Email"
|
label={t('Name')}
|
||||||
onFilterApply={(value) => {
|
onFilterApply={(value) => {
|
||||||
handleEmailChange(value as string);
|
handleNameChange(value as string);
|
||||||
}}
|
}}
|
||||||
onFilterDelete={() => {
|
onFilterDelete={() => {
|
||||||
handleEmailChange();
|
handleNameChange();
|
||||||
}}
|
}}
|
||||||
popover={<EmailFilterPopover />}
|
popover={<NameFilterPopover />}
|
||||||
value={email}
|
value={name}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FilterButton
|
<FilterButton
|
||||||
displayValue={phone}
|
displayValue={type}
|
||||||
label="Phone number"
|
label={t('Type')}
|
||||||
onFilterApply={(value) => {
|
onFilterApply={(value) => {
|
||||||
handlePhoneChange(value as string);
|
handleTypeChange(value as string);
|
||||||
}}
|
}}
|
||||||
onFilterDelete={() => {
|
onFilterDelete={() => {
|
||||||
handlePhoneChange();
|
handleTypeChange();
|
||||||
}}
|
}}
|
||||||
popover={<PhoneFilterPopover />}
|
popover={<TypeFilterPopover />}
|
||||||
value={phone}
|
value={type}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{hasFilters ? <Button onClick={handleClearFilters}>{t('Clear filters')}</Button> : null}
|
{hasFilters ? <Button onClick={handleClearFilters}>{t('Clear filters')}</Button> : null}
|
||||||
</Stack>
|
</Stack>
|
||||||
{selection.selectedAny ? (
|
{selection.selectedAny ? (
|
||||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||||
<Typography color="text.secondary" variant="body2">
|
<Typography color="text.secondary" variant="body2">
|
||||||
{selection.selected.size} selected
|
{selection.selected.size} {t('selected')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button color="error" variant="contained">
|
<Button color="error" variant="contained">
|
||||||
{t('Delete')}
|
{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 { t } = useTranslation();
|
||||||
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
|
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
|
||||||
const [value, setValue] = React.useState<string>('');
|
const [value, setValue] = React.useState<string>('');
|
||||||
@@ -200,7 +259,7 @@ function EmailFilterPopover(): React.JSX.Element {
|
|||||||
}, [initialValue]);
|
}, [initialValue]);
|
||||||
|
|
||||||
return (
|
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>
|
<FormControl>
|
||||||
<OutlinedInput
|
<OutlinedInput
|
||||||
onChange={(event) => {
|
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 {
|
function PhoneFilterPopover(): React.JSX.Element {
|
||||||
const { t } = useTranslation();
|
|
||||||
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
|
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
|
||||||
const [value, setValue] = React.useState<string>('');
|
const [value, setValue] = React.useState<string>('');
|
||||||
|
|
||||||
@@ -236,7 +365,7 @@ function PhoneFilterPopover(): React.JSX.Element {
|
|||||||
}, [initialValue]);
|
}, [initialValue]);
|
||||||
|
|
||||||
return (
|
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>
|
<FormControl>
|
||||||
<OutlinedInput
|
<OutlinedInput
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
@@ -248,7 +377,6 @@ function PhoneFilterPopover(): React.JSX.Element {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
value={value}
|
value={value}
|
||||||
placeholder={t('Enter phone number')}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<Button
|
<Button
|
||||||
@@ -257,7 +385,7 @@ function PhoneFilterPopover(): React.JSX.Element {
|
|||||||
}}
|
}}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
>
|
>
|
||||||
{t('Apply')}
|
Apply
|
||||||
</Button>
|
</Button>
|
||||||
</FilterPopover>
|
</FilterPopover>
|
||||||
);
|
);
|
||||||
|
@@ -7,25 +7,42 @@ function noop(): void {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CustomersPaginationProps {
|
interface LpCategoriesPaginationProps {
|
||||||
count: number;
|
count: number;
|
||||||
page: 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.
|
// You should implement the pagination using a similar logic as the filters.
|
||||||
// Note that when page change, you should keep the filter search params.
|
// 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 (
|
return (
|
||||||
<TablePagination
|
<TablePagination
|
||||||
component="div"
|
component="div"
|
||||||
count={count}
|
count={count}
|
||||||
onPageChange={noop}
|
|
||||||
onRowsPerPageChange={noop}
|
|
||||||
page={page}
|
page={page}
|
||||||
rowsPerPage={5}
|
rowsPerPage={rowsPerPage}
|
||||||
rowsPerPageOptions={[5, 10, 25]}
|
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 type { LessonCategory } from '../lesson_category/types';
|
||||||
import { useLpCategoriesSelection } from './lp-categories-selection-context';
|
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>[] {
|
function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LpCategory>[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -134,7 +63,9 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LpCateg
|
|||||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||||
<LinearProgress sx={{ flex: '1 1 auto' }} value={row.quota} variant="determinate" />
|
<LinearProgress sx={{ flex: '1 1 auto' }} value={row.quota} variant="determinate" />
|
||||||
<Typography color="text.secondary" variant="body2">
|
<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>
|
</Typography>
|
||||||
</Stack>
|
</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" /> },
|
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" /> },
|
NA: { label: 'NA', icon: <ClockIcon color="var(--mui-palette-warning-main)" weight="fill" /> },
|
||||||
} as const;
|
} as const;
|
||||||
const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null };
|
const { label, icon } = mapping[row.visible] ?? { label: 'Unknown', icon: null };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -211,7 +142,8 @@ export interface LpCategoriesTableProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCatImageFromId(row: LpCategory): string | undefined {
|
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 {
|
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_TYPES = 'LessonsTypes';
|
||||||
const COL_LESSON_CATEGORIES = 'LessonsCategories';
|
const COL_LESSON_CATEGORIES = 'LessonsCategories';
|
||||||
const NO_VALUE = 'NO_VALUE';
|
const NO_VALUE = 'NO_VALUE';
|
||||||
|
@@ -1,14 +1,26 @@
|
|||||||
# AI GUIDELINE
|
# 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
|
## tasks
|
||||||
|
|
||||||
Thanks
|
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;
|
RULES:
|
||||||
isEmpty?: boolean;
|
please follow the naming convention
|
||||||
collectionId?: string;
|
*/
|
||||||
id: string;
|
|
||||||
name: string;
|
export interface LpCategoryFormProps {
|
||||||
avatar?: string;
|
name: string; // corresponds to cat_name
|
||||||
email: string;
|
description: string; // additional business field
|
||||||
phone?: string;
|
isActive: boolean; // additional business field
|
||||||
quota: number;
|
order: number; // corresponds to pos
|
||||||
status: 'pending' | 'active' | 'blocked';
|
imageUrl: string; // corresponds to cat_image
|
||||||
createdAt: Date;
|
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