This commit is contained in:
louiscklaw
2025-04-19 02:11:44 +08:00
parent 50b69d3ce8
commit 60a0cdc11d
30 changed files with 391 additions and 184 deletions

View File

@@ -1,3 +1,6 @@
.env
.env.production
**/*.draft **/*.draft
**/~* **/~*
**/*copy*.tsx **/*copy*.tsx

View File

@@ -9,7 +9,7 @@
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "rm -rf .next && next build", "build": "rm -rf .next && next build",
"build:w": "pnpx nodemon --ext ts,tsx,json,mjs,js,jsx --delay 1 --exec \"pnpm run build\"", "build:w": "pnpx nodemon --ext ts,tsx,json,mjs,js,jsx --delay 15 --exec \"pnpm run build\"",
"start": "next start", "start": "next start",
"lint": "next lint --quiet", "lint": "next lint --quiet",
"lint:fix": "next lint --fix", "lint:fix": "next lint --fix",

View File

@@ -113,7 +113,6 @@
"connective_revision": "連接詞 (Connective Revision)", "connective_revision": "連接詞 (Connective Revision)",
"settings": "設定", "settings": "設定",
"students": "學生", "students": "學生",
"dashboard.lessonCategories.add": "新增課程分類",
"dashboard.lessonCategories.title": "課程分類", "dashboard.lessonCategories.title": "課程分類",
"dashboard.lessonCategorys.edit.name": "課程分類名稱", "dashboard.lessonCategorys.edit.name": "課程分類名稱",
"dashboard.lessonCategorys.edit.position": "課程分類順序", "dashboard.lessonCategorys.edit.position": "課程分類順序",

View File

@@ -59,7 +59,8 @@
"message": "請建立課程類型", "message": "請建立課程類型",
"create": "建立課程類型" "create": "建立課程類型"
}, },
"error": "課程類型載入失敗" "error": "課程類型載入失敗",
"add": "新增課程分類"
}, },
"view": { "view": {
"basic-details": "基本資訊" "basic-details": "基本資訊"

View File

@@ -1,3 +0,0 @@
{
"git.ignoreLimitWarning": true
}

View File

@@ -19,7 +19,6 @@ import { pb } from '@/lib/pb';
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 { defaultLessonCategory } from '@/components/dashboard/lesson_category/_constants'; import { defaultLessonCategory } from '@/components/dashboard/lesson_category/_constants';
// import { defaultLessonCategory, type LessonCategory } from '@/components/dashboard/lesson_category/interfaces';
import { LessonCategoriesFilters } from '@/components/dashboard/lesson_category/lesson-categories-filters'; import { LessonCategoriesFilters } from '@/components/dashboard/lesson_category/lesson-categories-filters';
import type { Filters } from '@/components/dashboard/lesson_category/lesson-categories-filters'; import type { Filters } from '@/components/dashboard/lesson_category/lesson-categories-filters';
import { LessonCategoriesPagination } from '@/components/dashboard/lesson_category/lesson-categories-pagination'; import { LessonCategoriesPagination } from '@/components/dashboard/lesson_category/lesson-categories-pagination';
@@ -30,38 +29,27 @@ import FormLoading from '@/components/loading';
// import { lessonCategoriesSampleData } from './lesson-categories-sample-data'; // import { lessonCategoriesSampleData } from './lesson-categories-sample-data';
interface PageProps {
searchParams: {
email?: string;
phone?: string;
sortDir?: 'asc' | 'desc';
spStatus?: string;
spName?: string;
spVisible?: string;
spType?: string;
//
};
}
export default function Page({ searchParams }: PageProps): React.JSX.Element { export default function Page({ searchParams }: PageProps): React.JSX.Element {
const { t } = useTranslation(['common']); const { t } = useTranslation(['lesson_category']);
const { email, phone, sortDir, spStatus } = searchParams; const { email, phone, sortDir, status, name, visible, type } = searchParams;
const router = useRouter(); const router = useRouter();
const [lessonCategoriesData, setLessonCategoriesData] = React.useState<LessonCategory[]>([]);
//
const [recordCount, setRecordCount] = React.useState<number>(0); const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
const [showLoading, setShowLoading] = React.useState<boolean>(true);
const [showError, setShowError] = React.useState<boolean>(false);
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5); const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
//
const [f, setF] = React.useState<LessonCategory[]>([]);
const [currentPage, setCurrentPage] = React.useState<number>(1); const [currentPage, setCurrentPage] = React.useState<number>(1);
// //
const [showError, setShowError] = React.useState<boolean>(false); const [recordCount, setRecordCount] = React.useState<number>(0);
const [showLoading, setShowLoading] = React.useState<boolean>(true);
//
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
const [lessonCategoriesData, setLessonCategoriesData] = React.useState<LessonCategory[]>([]);
const sortedLessonCategories = applySort(lessonCategoriesData, sortDir); const sortedLessonCategories = applySort(lessonCategoriesData, sortDir);
const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status: spStatus }); const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status: status });
//
const reloadRows = () => { const reloadRows = () => {
setShowLoading(true); setShowLoading(true);
@@ -116,7 +104,7 @@ 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('dashboard.lessonCategorys.list.title')}</Typography> <Typography variant="h4">{t('list.title')}</Typography>
</Box> </Box>
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}> <Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<LoadingButton <LoadingButton
@@ -128,13 +116,17 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
startIcon={<PlusIcon />} startIcon={<PlusIcon />}
variant="contained" variant="contained"
> >
{t('dashboard.lessonCategories.add')} {t('list.add')}
</LoadingButton> </LoadingButton>
</Box> </Box>
</Stack> </Stack>
<LessonCategoriesSelectionProvider lessonCategories={filteredLessonCategories}> <LessonCategoriesSelectionProvider lessonCategories={f}>
<Card> <Card>
<LessonCategoriesFilters filters={{ email, phone, status: spStatus }} sortDir={sortDir} /> <LessonCategoriesFilters
filters={{ email, phone, status, name, visible, type }}
fullData={lessonCategoriesData}
sortDir={sortDir}
/>
<Divider /> <Divider />
<Box sx={{ overflowX: 'auto' }}> <Box sx={{ overflowX: 'auto' }}>
<LessonCategoriesTable reloadRows={reloadRows} rows={filteredLessonCategories} /> <LessonCategoriesTable reloadRows={reloadRows} rows={filteredLessonCategories} />
@@ -183,3 +175,16 @@ function applyFilters(row: LessonCategory[], { email, phone, status }: Filters):
return true; return true;
}); });
} }
interface PageProps {
searchParams: {
email?: string;
phone?: string;
sortDir?: 'asc' | 'desc';
status?: string;
name?: string;
visible?: string;
type?: string;
//
};
}

View File

@@ -37,14 +37,15 @@ import { PropertyItem } from '@/components/core/property-item';
import { PropertyList } from '@/components/core/property-list'; 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 } 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 { getLessonTypeById } from '@/components/dashboard/lesson_type/http-actions';
import { LessonTypeDefaultValue, type LessonType } from '@/components/dashboard/lesson_type/ILessonType'; // import { LessonTypeDefaultValue, type LessonType } from '@/components/dashboard/lesson_type/ILessonType';
// import { defaultLessonType } from '@/components/dashboard/lesson_type/interfaces'; // 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 { Payments } from '@/components/dashboard/lesson_type/payments';
import type { Address } from '@/components/dashboard/lesson_type/shipping-address'; import type { Address } from '@/components/dashboard/lesson_type/shipping-address';
import { ShippingAddress } from '@/components/dashboard/lesson_type/shipping-address'; import { ShippingAddress } from '@/components/dashboard/lesson_type/shipping-address';
import { type LessonType } from '@/components/dashboard/lesson_type/types';
import FormLoading from '@/components/loading'; import FormLoading from '@/components/loading';
export default function Page(): React.JSX.Element { export default function Page(): React.JSX.Element {
@@ -58,7 +59,7 @@ export default function Page(): React.JSX.Element {
// //
const [showLessonType, setShowLessonType] = React.useState<LessonType>(LessonTypeDefaultValue); const [showLessonType, setShowLessonType] = React.useState<LessonType>(LessonTypeDefaultValue);
function handleEditClick() { function handleEditClick(): void {
router.push(paths.dashboard.lesson_types.edit(showLessonType.id)); router.push(paths.dashboard.lesson_types.edit(showLessonType.id));
} }

View File

@@ -1,5 +1,7 @@
import { dayjs } from '@/lib/dayjs'; import { dayjs } from '@/lib/dayjs';
import type { LessonType } from '@/components/dashboard/lesson_type/ILessonType'; import { LessonType } from '@/components/dashboard/lesson_type/types';
// import type { LessonType } from '@/components/dashboard/lesson_type/ILessonType';
// import { helloworld } from '@/components/dashboard/lesson_type/helloworld'; // import { helloworld } from '@/components/dashboard/lesson_type/helloworld';
// export const metadata = { title: `List | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; // export const metadata = { title: `List | Customers | Dashboard | ${config.site.name}` } satisfies Metadata;

View File

@@ -1,5 +1,7 @@
import { dayjs } from '@/lib/dayjs'; import { dayjs } from '@/lib/dayjs';
import type { LessonType } from '@/components/dashboard/lesson_type/ILessonType'; import { LessonType } from '@/components/dashboard/lesson_type/types';
// import type { LessonType } from '@/components/dashboard/lesson_type/ILessonType';
// import { helloworld } from '@/components/dashboard/lesson_type/helloworld'; // import { helloworld } from '@/components/dashboard/lesson_type/helloworld';
// export const metadata = { title: `List | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; // export const metadata = { title: `List | Customers | Dashboard | ${config.site.name}` } satisfies Metadata;

View File

@@ -19,28 +19,31 @@ import { pb } from '@/lib/pb';
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 } from '@/components/dashboard/lesson_type/_constants'; import { defaultLessonType } from '@/components/dashboard/lesson_type/_constants';
import type { LessonType } from '@/components/dashboard/lesson_type/ILessonType'; // import type { LessonType } from '@/components/dashboard/lesson_type/ILessonType';
// import { defaultLessonType, emptyLessonType, safeAssignment } from '@/components/dashboard/lesson_type/interfaces'; // import { defaultLessonType, emptyLessonType, safeAssignment } from '@/components/dashboard/lesson_type/interfaces';
import { LessonTypesFilters } from '@/components/dashboard/lesson_type/lesson-types-filters'; import { LessonTypesFilters } from '@/components/dashboard/lesson_type/lesson-types-filters';
import type { Filters } from '@/components/dashboard/lesson_type/lesson-types-filters'; import type { Filters } from '@/components/dashboard/lesson_type/lesson-types-filters';
import { LessonTypesPagination } from '@/components/dashboard/lesson_type/lesson-types-pagination'; import { LessonTypesPagination } from '@/components/dashboard/lesson_type/lesson-types-pagination';
import { LessonTypesSelectionProvider } from '@/components/dashboard/lesson_type/lesson-types-selection-context'; import { LessonTypesSelectionProvider } from '@/components/dashboard/lesson_type/lesson-types-selection-context';
import { LessonTypesTable } from '@/components/dashboard/lesson_type/lesson-types-table'; import { LessonTypesTable } from '@/components/dashboard/lesson_type/lesson-types-table';
import type { LessonType } from '@/components/dashboard/lesson_type/types';
import FormLoading from '@/components/loading'; import FormLoading from '@/components/loading';
export default function Page({ searchParams }: PageProps): React.JSX.Element { export default function Page({ searchParams }: PageProps): React.JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation(['lesson_type']);
const { email, phone, sortDir, status, name, visible, type } = searchParams; const { email, phone, sortDir, status, name, visible, type } = searchParams;
const router = useRouter(); const router = useRouter();
const [lessonTypesData, setLessonTypesData] = React.useState<LessonType[]>([]); const [lessonTypesData, setLessonTypesData] = React.useState<LessonType[]>([]);
//
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false); const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
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({});
@@ -117,7 +120,7 @@ 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('Lesson Types')}</Typography> <Typography variant="h4">{t('list.title')}</Typography>
</Box> </Box>
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}> <Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<LoadingButton <LoadingButton

View File

@@ -26,8 +26,8 @@ import { Events } from '@/components/dashboard/overview/events';
import { HelperWidget } from '@/components/dashboard/overview/helper-widget'; import { HelperWidget } from '@/components/dashboard/overview/helper-widget';
import { Subscriptions } from '@/components/dashboard/overview/subscriptions'; import { Subscriptions } from '@/components/dashboard/overview/subscriptions';
import { Summary } from '@/components/dashboard/overview/summary'; import { Summary } from '@/components/dashboard/overview/summary';
import ActiveUserCount from '@/components/dashboard/overview/summary/ActiveUserCount';
import ActiveUserCount from '../../components/dashboard/overview/summary/ActiveUserCount';
import { events as SampleEvents } from './SampleEvents'; import { events as SampleEvents } from './SampleEvents';
import { messages as SampleMessages } from './SampleMessages'; import { messages as SampleMessages } from './SampleMessages';
import { SamplesubScriptions } from './SamplesubScriptions'; import { SamplesubScriptions } from './SamplesubScriptions';

View File

@@ -2,6 +2,10 @@
import * as React from 'react'; import * as React from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { COL_LESSON_CATEGORIES } from '@/constants';
import GetAllCount from '@/db/LessonCategories/GetAllCount';
import GetHiddenCount from '@/db/LessonCategories/GetHiddenCount';
import GetVisibleCount from '@/db/LessonCategories/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';
@@ -13,25 +17,23 @@ import Stack from '@mui/material/Stack';
import Tab from '@mui/material/Tab'; import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs'; import Tabs from '@mui/material/Tabs';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths'; import { paths } from '@/paths';
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 { useLessonCategoriesSelection } from './lesson-categories-selection-context'; import { useLessonCategoriesSelection } from './lesson-categories-selection-context';
import { LessonCategory } from './types';
// The tabs should be generated using API data.
const tabs = [
{ 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';
@@ -39,18 +41,47 @@ export type SortDir = 'asc' | 'desc';
export interface LessonCategoriesFiltersProps { export interface LessonCategoriesFiltersProps {
filters?: Filters; filters?: Filters;
sortDir?: SortDir; sortDir?: SortDir;
fullData: LessonCategory[];
} }
export function LessonCategoriesFilters({ export function LessonCategoriesFilters({
filters = {}, filters = {},
sortDir = 'desc', sortDir = 'desc',
fullData,
}: LessonCategoriesFiltersProps): React.JSX.Element { }: LessonCategoriesFiltersProps): React.JSX.Element {
const { email, phone, status } = filters; const { t } = useTranslation();
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 = useLessonCategoriesSelection(); const selection = useLessonCategoriesSelection();
function getVisible(): number {
return fullData.reduce((count, item: LessonCategory) => {
return item.visible === 'visible' ? count + 1 : count;
}, 0);
}
function getHidden(): number {
return fullData.reduce((count, item: LessonCategory) => {
return item.visible === 'hidden' ? count + 1 : count;
}, 0);
}
// 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 +102,18 @@ export function LessonCategoriesFilters({
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.lesson_categories.list}?${searchParams.toString()}`); router.push(`${paths.dashboard.lesson_categories.list}?${searchParams.toString()}`);
}, },
[router] [router]
@@ -87,6 +130,27 @@ export function LessonCategoriesFilters({
[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,11 +172,29 @@ export function LessonCategoriesFilters({
[updateSearchParams, filters] [updateSearchParams, filters]
); );
const hasFilters = status || email || phone; React.useEffect(() => {
const fetchCount = async (): Promise<void> => {
try {
const tc = await GetAllCount();
setTotalCount(tc);
const vc = await GetVisibleCount();
setVisibleCount(vc);
const hc = await GetHiddenCount();
setHiddenCount(hc);
} catch (error) {
//
}
};
void fetchCount();
}, []);
const hasFilters = status || email || phone || visible || name || type;
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" />}
@@ -129,50 +211,124 @@ export function LessonCategoriesFilters({
<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}>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">
Delete {t('Delete')}
</Button> </Button>
</Stack> </Stack>
) : null} ) : null}
<Select name="sort" onChange={handleSortChange} sx={{ maxWidth: '100%', width: '120px' }} value={sortDir}> <Select name="sort" onChange={handleSortChange} sx={{ maxWidth: '100%', width: '120px' }} value={sortDir}>
<Option value="desc">Newest</Option> <Option value="desc">{t('Newest')}</Option>
<Option value="asc">Oldest</Option> <Option value="asc">{t('Oldest')}</Option>
</Select> </Select>
</Stack> </Stack>
</div> </div>
); );
} }
function TypeFilterPopover(): 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 type')}>
<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 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 { function EmailFilterPopover(): React.JSX.Element {
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>('');

View File

@@ -145,7 +145,7 @@ export interface LessonCategoriesTableProps {
} }
export function LessonCategoriesTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element { export function LessonCategoriesTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation(['lesson_category']);
const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLessonCategoriesSelection(); const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLessonCategoriesSelection();
const [idToDelete, setIdToDelete] = React.useState(''); const [idToDelete, setIdToDelete] = React.useState('');
@@ -177,7 +177,7 @@ export function LessonCategoriesTable({ rows, reloadRows }: LessonCategoriesTabl
<Box sx={{ p: 3 }}> <Box sx={{ p: 3 }}>
<Typography color="text.secondary" sx={{ textAlign: 'center' }} variant="body2"> <Typography color="text.secondary" sx={{ textAlign: 'center' }} variant="body2">
{/* TODO: use hyphen here */} {/* TODO: use hyphen here */}
{t('No lesson categories found')} {t('no-lesson-categories-found')}
</Typography> </Typography>
</Box> </Box>
) : null} ) : null}

View File

@@ -2,58 +2,3 @@
// please use src/components/dashboard/lesson_type/interfaces.ts for defining new interfaces // please use src/components/dashboard/lesson_type/interfaces.ts for defining new interfaces
import { dayjs } from '@/lib/dayjs'; import { dayjs } from '@/lib/dayjs';
export interface LessonType {
id: string;
isEmpty?: boolean;
name: string;
type: string;
pos: number;
visible: 'visible' | 'hidden';
createdAt: Date;
//
// original
// id: string;
// name: string;
//
avatar?: string;
email: string;
phone?: string;
quota: number;
status: 'pending' | 'active' | 'blocked';
// createdAt: Date;
}
export const LessonTypeDefaultValue: LessonType = {
id: 'string',
name: 'string',
type: 'string',
pos: 1,
visible: 'visible',
createdAt: dayjs().toDate(),
//
// original
// id: 'string',
// name: 'string',
//
avatar: 'string',
email: 'string',
phone: 'string',
quota: 1,
status: 'pending',
// createdAt: Date;
};
export interface DBLessonType {
id: string;
name: string;
type: string;
pos: number;
visible: 'visible' | 'hidden';
createdAt: Date;
created: 'string';
}
export interface Helloworld {
id: string;
}

View File

@@ -3,8 +3,8 @@ import type { RecordModel } from 'pocketbase';
import { dayjs } from '@/lib/dayjs'; import { dayjs } from '@/lib/dayjs';
import type { LessonType } from './ILessonType'; // import type { LessonType } from './ILessonType';
import type { CreateForm } from './types'; import type { CreateForm, LessonType } from './types';
export const LessonTypeCreateFormDefault: CreateForm = { export const LessonTypeCreateFormDefault: CreateForm = {
name: '', name: '',
@@ -55,3 +55,23 @@ export function safeAssignment(inTemp: LessonType | RecordModel): LessonType {
}; };
return output; return output;
} }
export const LessonTypeDefaultValue: LessonType = {
id: 'string',
name: 'string',
type: 'string',
pos: 1,
visible: 'visible',
createdAt: dayjs().toDate(),
//
// original
// id: 'string',
// name: 'string',
//
avatar: 'string',
email: 'string',
phone: 'string',
quota: 1,
status: 'pending',
// createdAt: Date;
};

View File

@@ -40,7 +40,7 @@ import FormLoading from '@/components/loading';
import { defaultLessonType } from './_constants'; import { defaultLessonType } from './_constants';
// import { getLessonTypeById, updateLessonType } from './http-actions'; // import { getLessonTypeById, updateLessonType } from './http-actions';
// TODO: this may be wrong // TODO: this may be wrong
import type { LessonType } from './ILessonType'; // import type { LessonType } from './ILessonType';
import { LessonTypeEditFormProps } from './types'; import { LessonTypeEditFormProps } from './types';
// import { defaultLessonType, type LessonTypeEditFormProps } from './interfaces'; // import { defaultLessonType, type LessonTypeEditFormProps } from './interfaces';

View File

@@ -3,6 +3,9 @@
import * as React from 'react'; import * as React from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { COL_LESSON_TYPES } from '@/constants'; import { COL_LESSON_TYPES } from '@/constants';
import GetAllCount from '@/db/LessonTypes/GetAllCount';
import GetHiddenCount from '@/db/LessonTypes/GetHiddenCount';
import GetVisibleCount from '@/db/LessonTypes/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';
@@ -21,8 +24,9 @@ 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 type { LessonType } from './ILessonType';
import { useLessonTypesSelection } from './lesson-types-selection-context'; import { useLessonTypesSelection } from './lesson-types-selection-context';
import { LessonType } from './types';
export interface Filters { export interface Filters {
email?: string; email?: string;
@@ -172,17 +176,13 @@ export function LessonTypesFilters({
React.useEffect(() => { React.useEffect(() => {
const fetchCount = async (): Promise<void> => { const fetchCount = async (): Promise<void> => {
try { try {
const { totalItems: tc } = await pb.collection(COL_LESSON_TYPES).getList(1, 9999); const tc = await GetAllCount();
setTotalCount(tc); setTotalCount(tc);
const { totalItems: vc } = await pb const vc = await GetVisibleCount();
.collection(COL_LESSON_TYPES)
.getList(1, 9999, { filter: 'visible = "visible"' });
setVisibleCount(vc); setVisibleCount(vc);
const { totalItems: hc } = await pb const hc = await GetHiddenCount();
.collection(COL_LESSON_TYPES)
.getList(1, 9999, { filter: 'visible = "hidden"' });
setHiddenCount(hc); setHiddenCount(hc);
} catch (error) { } catch (error) {
// //
@@ -237,36 +237,6 @@ export function LessonTypesFilters({
value={type} value={type}
/> />
{/*
<FilterButton
displayValue={email}
label="Email"
onFilterApply={(value) => {
handleEmailChange(value as string);
}}
onFilterDelete={() => {
handleEmailChange();
}}
popover={<EmailFilterPopover />}
value={email}
/>
*/}
{/*
<FilterButton
displayValue={phone}
label="Phone number"
onFilterApply={(value) => {
handlePhoneChange(value as string);
}}
onFilterDelete={() => {
handlePhoneChange();
}}
popover={<PhoneFilterPopover />}
value={phone}
/>
*/}
{hasFilters ? <Button onClick={handleClearFilters}>{t('Clear filters')}</Button> : null} {hasFilters ? <Button onClick={handleClearFilters}>{t('Clear filters')}</Button> : null}
</Stack> </Stack>
{selection.selectedAny ? ( {selection.selectedAny ? (

View File

@@ -5,7 +5,9 @@ import * as React from 'react';
import { useSelection } from '@/hooks/use-selection'; import { useSelection } from '@/hooks/use-selection';
import type { Selection } from '@/hooks/use-selection'; import type { Selection } from '@/hooks/use-selection';
import { LessonType } from './ILessonType'; import { LessonType } from './types';
// import { LessonType } from './ILessonType';
function noop(): void { function noop(): void {
return undefined; return undefined;

View File

@@ -13,6 +13,7 @@ import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock'; import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock';
import { Images as ImagesIcon } from '@phosphor-icons/react/dist/ssr/Images';
import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus'; import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus';
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
import { TrashSimple as TrashSimpleIcon } from '@phosphor-icons/react/dist/ssr/TrashSimple'; import { TrashSimple as TrashSimpleIcon } from '@phosphor-icons/react/dist/ssr/TrashSimple';
@@ -25,8 +26,9 @@ import { DataTable } from '@/components/core/data-table';
import type { ColumnDef } from '@/components/core/data-table'; import type { ColumnDef } from '@/components/core/data-table';
import ConfirmDeleteModal from './confirm-delete-modal'; import ConfirmDeleteModal from './confirm-delete-modal';
import type { LessonType } from './ILessonType'; // import type { LessonType } from './ILessonType';
import { useLessonTypesSelection } from './lesson-types-selection-context'; import { useLessonTypesSelection } from './lesson-types-selection-context';
import { LessonType } from './types';
function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonType>[] { function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonType>[] {
return [ return [
@@ -132,7 +134,6 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonT
name: 'Created at', name: 'Created at',
width: '100px', width: '100px',
}, },
{ {
formatter: (row): React.JSX.Element => ( formatter: (row): React.JSX.Element => (
<Stack direction="row" spacing={1}> <Stack direction="row" spacing={1}>
@@ -169,7 +170,7 @@ export interface LessonTypesTableProps {
} }
export function LessonTypesTable({ rows, reloadRows }: LessonTypesTableProps): React.JSX.Element { export function LessonTypesTable({ rows, reloadRows }: LessonTypesTableProps): React.JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation(['lesson_type']);
const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLessonTypesSelection(); const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLessonTypesSelection();
const [idToDelete, setIdToDelete] = React.useState(''); const [idToDelete, setIdToDelete] = React.useState('');
@@ -201,7 +202,7 @@ export function LessonTypesTable({ rows, reloadRows }: LessonTypesTableProps): R
<Box sx={{ p: 3 }}> <Box sx={{ p: 3 }}>
<Typography color="text.secondary" sx={{ textAlign: 'center' }} variant="body2"> <Typography color="text.secondary" sx={{ textAlign: 'center' }} variant="body2">
{/* TODO: use hyphen here */} {/* TODO: use hyphen here */}
{t('No lesson types found')} {t('no-lesson-types-found')}
</Typography> </Typography>
</Box> </Box>
) : null} ) : null}

View File

@@ -4,7 +4,8 @@ import type { RecordModel } from 'pocketbase';
import { dayjs } from '@/lib/dayjs'; import { dayjs } from '@/lib/dayjs';
import { defaultLessonType } from './_constants'; import { defaultLessonType } from './_constants';
import type { LessonType } from './ILessonType';
// import type { LessonType } from './ILessonType';
export interface LessonTypeEditFormProps { export interface LessonTypeEditFormProps {
name: string; name: string;
@@ -42,3 +43,38 @@ export function safeAssignment(inTemp: LessonType | RecordModel): LessonType {
}; };
return output; return output;
} }
export interface LessonType {
id: string;
isEmpty?: boolean;
name: string;
type: string;
pos: number;
visible: 'visible' | 'hidden';
createdAt: Date;
//
// original
// id: string;
// name: string;
//
avatar?: string;
email: string;
phone?: string;
quota: number;
status: 'pending' | 'active' | 'blocked';
// createdAt: Date;
}
export interface DBLessonType {
id: string;
name: string;
type: string;
pos: number;
visible: 'visible' | 'hidden';
createdAt: Date;
created: 'string';
}
export interface Helloworld {
id: string;
}

View File

@@ -12,6 +12,10 @@ please read, remember and link up the ideas, i will tell you the task afterwards
working directory: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db` working directory: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db`
clone `GetVisibleCount.tsx` and `GetHiddenCount.tsx` from `LessonTypes` to `LessonCategories` and update it
please draft `GetHiddenCount.tsx` for COL_LESSON_TYPES and `status = hidden`
pleaes clone the `tsx` files from `LessonTypes` and `LessonCategories` to `UserMetas` and update the content pleaes clone the `tsx` files from `LessonTypes` and `LessonCategories` to `UserMetas` and update the content
when you draft coding, review file and append with `.tsx.draft` when you draft coding, review file and append with `.tsx.draft`

View File

View File

@@ -4,8 +4,6 @@ import { COL_LESSON_CATEGORIES } from '@/constants';
import { pb } from '@/lib/pb'; import { pb } from '@/lib/pb';
export default async function GetAllCount(): Promise<number> { export default async function GetAllCount(): Promise<number> {
const { totalItems: count } = await pb const { totalItems: count } = await pb.collection(COL_LESSON_CATEGORIES).getList(1, 9999, {});
.collection(COL_LESSON_CATEGORIES)
.getList(1, 9999, { filter: 'visible = "visible"' });
return count; return count;
} }

View File

@@ -0,0 +1,14 @@
// REQ0006
import { COL_LESSON_CATEGORIES } from '@/constants';
import { pb } from '@/lib/pb';
export default async function GetHiddenCount(): Promise<number> {
try {
const result = await pb.collection(COL_LESSON_CATEGORIES).getList(1, 9999, { filter: 'visible = "hidden"' });
const { totalItems: count } = result;
return count;
} catch (error) {
return 0;
}
}

View File

@@ -0,0 +1,14 @@
// REQ0006
import { COL_LESSON_CATEGORIES } from '@/constants';
import { pb } from '@/lib/pb';
export default async function GetVisibleCount(): Promise<number> {
try {
const result = await pb.collection(COL_LESSON_CATEGORIES).getList(1, 9999, { filter: 'visible = "visible"' });
const { totalItems: count } = result;
return count;
} catch (error) {
return 0;
}
}

View File

@@ -5,7 +5,7 @@ import { pb } from '@/lib/pb';
export default async function GetAllCount(): Promise<number> { export default async function GetAllCount(): Promise<number> {
try { try {
const result = await pb.collection(COL_LESSON_TYPES).getList(1, 9999, { filter: 'visible = "visible"' }); const result = await pb.collection(COL_LESSON_TYPES).getList(1, 9999, {});
const { totalItems: count } = result; const { totalItems: count } = result;
return count; return count;
} catch (error) { } catch (error) {

View File

@@ -0,0 +1,14 @@
// REQ0006
import { COL_LESSON_TYPES } from '@/constants';
import { pb } from '@/lib/pb';
export default async function GetHiddenCount(): Promise<number> {
try {
const result = await pb.collection(COL_LESSON_TYPES).getList(1, 9999, { filter: 'visible = "hidden"' });
const { totalItems: count } = result;
return count;
} catch (error) {
return 0;
}
}

View File

@@ -0,0 +1,14 @@
// REQ0006
import { COL_LESSON_TYPES } from '@/constants';
import { pb } from '@/lib/pb';
export default async function GetVisibleCount(): Promise<number> {
try {
const result = await pb.collection(COL_LESSON_TYPES).getList(1, 9999, { filter: 'visible = "visible"' });
const { totalItems: count } = result;
return count;
} catch (error) {
return 0;
}
}

View File

@@ -93,6 +93,12 @@ export const paths = {
create: '/dashboard/customers/create', create: '/dashboard/customers/create',
details: (customerId: string) => `/dashboard/customers/${customerId}`, details: (customerId: string) => `/dashboard/customers/${customerId}`,
}, },
students: {
list: '/dashboard/students',
create: '/dashboard/students/create',
details: (studentId: string) => `/dashboard/students/${studentId}`,
edit: (studentId: string) => `/dashboard/students/edit/${studentId}`,
},
eCommerce: '/dashboard/e-commerce', eCommerce: '/dashboard/e-commerce',
fileStorage: '/dashboard/file-storage', fileStorage: '/dashboard/file-storage',
invoices: { invoices: {