This commit is contained in:
louiscklaw
2025-04-21 06:39:13 +08:00
parent 72bc7a67e2
commit 3e73668a3f
27 changed files with 298 additions and 142 deletions

View File

@@ -3,7 +3,7 @@
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 { COL_LISTENINGS_PRACTICE_CATEGORIES } from '@/constants'; import { COL_QUIZ_LP_CATEGORIES } from '@/constants';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Link from '@mui/material/Link'; import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
@@ -47,7 +47,7 @@ export default function Page(): React.JSX.Element {
React.useEffect(() => { React.useEffect(() => {
if (catId) { if (catId) {
pb.collection(COL_LISTENINGS_PRACTICE_CATEGORIES) pb.collection(COL_QUIZ_LP_CATEGORIES)
.getOne(catId) .getOne(catId)
.then((model: RecordModel) => { .then((model: RecordModel) => {
setShowLessonCategory({ ...defaultLpCategory, ...model }); setShowLessonCategory({ ...defaultLpCategory, ...model });

View File

@@ -6,7 +6,7 @@
// //
import * as React from 'react'; import * as React from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { COL_LISTENINGS_PRACTICE_CATEGORIES } from '@/constants'; import { COL_QUIZ_LP_CATEGORIES } from '@/constants';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Card from '@mui/material/Card'; import Card from '@mui/material/Card';
@@ -56,7 +56,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
const reloadRows = async (): Promise<void> => { const reloadRows = async (): Promise<void> => {
try { try {
const models: ListResult<RecordModel> = await pb const models: ListResult<RecordModel> = await pb
.collection(COL_LISTENINGS_PRACTICE_CATEGORIES) .collection(COL_QUIZ_LP_CATEGORIES)
.getList(currentPage + 1, rowsPerPage, {}); .getList(currentPage + 1, rowsPerPage, {});
const { items, totalItems } = models; const { items, totalItems } = models;
const tempLessonTypes: LpCategory[] = items.map((lt) => { const tempLessonTypes: LpCategory[] = items.map((lt) => {

View File

@@ -3,7 +3,7 @@
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 { COL_LISTENINGS_PRACTICE_CATEGORIES } from '@/constants'; import { COL_QUIZ_MF_CATEGORIES } from '@/constants';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Link from '@mui/material/Link'; import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
@@ -47,7 +47,7 @@ export default function Page(): React.JSX.Element {
React.useEffect(() => { React.useEffect(() => {
if (catId) { if (catId) {
pb.collection(COL_LISTENINGS_PRACTICE_CATEGORIES) pb.collection(COL_QUIZ_MF_CATEGORIES)
.getOne(catId) .getOne(catId)
.then((model: RecordModel) => { .then((model: RecordModel) => {
setShowLessonCategory({ ...defaultLpCategory, ...model }); setShowLessonCategory({ ...defaultLpCategory, ...model });

View File

@@ -6,7 +6,7 @@
// //
import * as React from 'react'; import * as React from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { COL_LISTENINGS_PRACTICE_CATEGORIES, COL_MF_CATEGORIES } from '@/constants'; import { COL_QUIZ_LP_CATEGORIES, COL_QUIZ_MF_CATEGORIES } from '@/constants';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Card from '@mui/material/Card'; import Card from '@mui/material/Card';
@@ -56,7 +56,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
const reloadRows = async (): Promise<void> => { const reloadRows = async (): Promise<void> => {
try { try {
const models: ListResult<RecordModel> = await pb const models: ListResult<RecordModel> = await pb
.collection(COL_MF_CATEGORIES) .collection(COL_QUIZ_MF_CATEGORIES)
.getList(currentPage + 1, rowsPerPage, {}); .getList(currentPage + 1, rowsPerPage, {});
const { items, totalItems } = models; const { items, totalItems } = models;
const tempLessonTypes: LpCategory[] = items.map((lt) => { const tempLessonTypes: LpCategory[] = items.map((lt) => {

View File

@@ -2,6 +2,7 @@
import * as React from 'react'; import * as React from 'react';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import { LoadingButton } from '@mui/lab';
import Avatar from '@mui/material/Avatar'; 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 Button from '@mui/material/Button';
@@ -27,13 +28,17 @@ import type { ColumnDef } from '@/components/core/data-table';
import ConfirmDeleteModal from './confirm-delete-modal'; import ConfirmDeleteModal from './confirm-delete-modal';
import { useLessonCategoriesSelection } from './lesson-categories-selection-context'; import { useLessonCategoriesSelection } from './lesson-categories-selection-context';
import { LessonCategory } from './type'; import type { LessonCategory } from './type';
function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonCategory>[] { function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonCategory>[] {
return [ return [
{ {
formatter: (row): React.JSX.Element => ( formatter: (row): React.JSX.Element => (
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}> <Stack
direction="row"
spacing={1}
sx={{ alignItems: 'center' }}
>
<Link <Link
color="inherit" color="inherit"
component={RouterLink} component={RouterLink}
@@ -41,7 +46,11 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonC
sx={{ whiteSpace: 'nowrap' }} sx={{ whiteSpace: 'nowrap' }}
variant="subtitle2" variant="subtitle2"
> >
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}> <Stack
direction="row"
spacing={1}
sx={{ alignItems: 'center' }}
>
<Avatar <Avatar
src={`http://127.0.0.1:8090/api/files/${row.collectionId}/${row.id}/${row.cat_image}`} src={`http://127.0.0.1:8090/api/files/${row.collectionId}/${row.id}/${row.cat_image}`}
variant="rounded" variant="rounded"
@@ -50,7 +59,10 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonC
</Avatar>{' '} </Avatar>{' '}
<div> <div>
<Box sx={{ whiteSpace: 'nowrap' }}>{row.cat_name}</Box> <Box sx={{ whiteSpace: 'nowrap' }}>{row.cat_name}</Box>
<Typography color="text.secondary" variant="body2"> <Typography
color="text.secondary"
variant="body2"
>
slug: {row.cat_name} slug: {row.cat_name}
</Typography> </Typography>
</div> </div>
@@ -63,9 +75,20 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonC
}, },
{ {
formatter: (row): React.JSX.Element => ( formatter: (row): React.JSX.Element => (
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}> <Stack
<LinearProgress sx={{ flex: '1 1 auto' }} value={row.quota} variant="determinate" /> direction="row"
<Typography color="text.secondary" variant="body2"> spacing={2}
sx={{ alignItems: 'center' }}
>
<LinearProgress
sx={{ flex: '1 1 auto' }}
value={row.quota}
variant="determinate"
/>
<Typography
color="text.secondary"
variant="body2"
>
{new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)} {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)}
</Typography> </Typography>
</Stack> </Stack>
@@ -77,13 +100,36 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonC
{ {
formatter: (row): React.JSX.Element => { formatter: (row): React.JSX.Element => {
// eslint-disable-next-line react-hooks/rules-of-hooks // eslint-disable-next-line react-hooks/rules-of-hooks
const { t } = useTranslation();
const mapping = { const mapping = {
active: { label: 'Active', icon: <CheckCircleIcon color="var(--mui-palette-success-main)" weight="fill" /> }, active: {
label: 'Active',
icon: (
<CheckCircleIcon
color="var(--mui-palette-success-main)"
weight="fill"
/>
),
},
blocked: { label: 'Blocked', icon: <MinusIcon color="var(--mui-palette-error-main)" /> }, blocked: { label: 'Blocked', icon: <MinusIcon color="var(--mui-palette-error-main)" /> },
pending: { label: 'Pending', icon: <ClockIcon color="var(--mui-palette-warning-main)" weight="fill" /> }, pending: {
NA: { label: 'NA', icon: <ClockIcon color="var(--mui-palette-warning-main)" weight="fill" /> }, label: 'Pending',
icon: (
<ClockIcon
color="var(--mui-palette-warning-main)"
weight="fill"
/>
),
},
NA: {
label: 'NA',
icon: (
<ClockIcon
color="var(--mui-palette-warning-main)"
weight="fill"
/>
),
},
} as const; } as const;
const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null }; const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null };
@@ -111,23 +157,27 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonC
}, },
{ {
formatter: (row): React.JSX.Element => ( formatter: (row): React.JSX.Element => (
<Stack direction="row" spacing={1}> <Stack
<IconButton direction="row"
spacing={1}
>
<LoadingButton
// //
color="secondary"
component={RouterLink} component={RouterLink}
href={paths.dashboard.lesson_categories.details(row.id)} href={paths.dashboard.lesson_categories.details(row.id)}
> >
<PencilSimpleIcon /> <PencilSimpleIcon size={24} />
</IconButton> </LoadingButton>
<IconButton <LoadingButton
color="error" color="error"
disabled={row.isEmpty} disabled={row.isEmpty}
onClick={() => { onClick={() => {
handleDeleteClick(row.id); handleDeleteClick(row.id);
}} }}
> >
<TrashSimpleIcon /> <TrashSimpleIcon size={24} />
</IconButton> </LoadingButton>
</Stack> </Stack>
), ),
name: 'Actions', name: 'Actions',
@@ -157,7 +207,12 @@ export function LessonCategoriesTable({ rows, reloadRows }: LessonCategoriesTabl
return ( return (
<React.Fragment> <React.Fragment>
<ConfirmDeleteModal idToDelete={idToDelete} open={open} reloadRows={reloadRows} setOpen={setOpen} /> <ConfirmDeleteModal
idToDelete={idToDelete}
open={open}
reloadRows={reloadRows}
setOpen={setOpen}
/>
<DataTable<LessonCategory> <DataTable<LessonCategory>
columns={columns(handleDeleteClick)} columns={columns(handleDeleteClick)}
onDeselectAll={deselectAll} onDeselectAll={deselectAll}
@@ -174,7 +229,11 @@ export function LessonCategoriesTable({ rows, reloadRows }: LessonCategoriesTabl
/> />
{!rows.length ? ( {!rows.length ? (
<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>

View File

@@ -3,7 +3,7 @@
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 deleteQuizListening from '@/db/QuizListenings/Delete'; import deleteQuizLPCategories from '@/db/QuizListenings/Delete';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import { Button, Container, Modal, Paper } from '@mui/material'; import { Button, Container, Modal, Paper } from '@mui/material';
import Avatar from '@mui/material/Avatar'; import Avatar from '@mui/material/Avatar';
@@ -17,8 +17,6 @@ import { useTranslation } from 'react-i18next';
import { logger } from '@/lib/default-logger'; import { logger } from '@/lib/default-logger';
import { toast } from '@/components/core/toaster'; import { toast } from '@/components/core/toaster';
const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL);
export default function ConfirmDeleteModal({ export default function ConfirmDeleteModal({
open, open,
setOpen, setOpen,
@@ -45,30 +43,19 @@ export default function ConfirmDeleteModal({
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
}; };
function performDelete(id: string): Promise<void> {
return deleteQuizListening(id)
.then(() => {
toast(t('dashboard.lessonTypes.delete.success'));
reloadRows();
})
.catch((err) => {
logger.error(err);
toast(t('dashboard.lessonTypes.delete.error'));
})
.finally(() => {});
}
function handleUserConfirmDelete(): void { function handleUserConfirmDelete(): void {
if (idToDelete) { if (idToDelete) {
setIsDeleteing(true); setIsDeleteing(true);
performDelete(idToDelete) deleteQuizLPCategories(idToDelete)
.then(() => { .then(() => {
reloadRows();
handleClose(); handleClose();
toast(t('delete.success'));
}) })
.catch((err) => { .catch((err) => {
// console.error(err) // console.error(err)
logger.error(err); logger.error(err);
toast(t('dashboard.lessonTypes.delete.error')); toast(t('delete.error'));
}) })
.finally(() => { .finally(() => {
setIsDeleteing(false); setIsDeleteing(false);
@@ -87,19 +74,33 @@ export default function ConfirmDeleteModal({
<Box sx={style}> <Box sx={style}>
<Container maxWidth="sm"> <Container maxWidth="sm">
<Paper sx={{ border: '1px solid var(--mui-palette-divider)', boxShadow: 'var(--mui-shadows-16)' }}> <Paper sx={{ border: '1px solid var(--mui-palette-divider)', boxShadow: 'var(--mui-shadows-16)' }}>
<Stack direction="row" spacing={2} sx={{ display: 'flex', p: 3 }}> <Stack
direction="row"
spacing={2}
sx={{ display: 'flex', p: 3 }}
>
<Avatar sx={{ bgcolor: 'var(--mui-palette-error-50)', color: 'var(--mui-palette-error-main)' }}> <Avatar sx={{ bgcolor: 'var(--mui-palette-error-50)', color: 'var(--mui-palette-error-main)' }}>
<NoteIcon fontSize="var(--Icon-fontSize)" /> <NoteIcon fontSize="var(--Icon-fontSize)" />
</Avatar> </Avatar>
<Stack spacing={3}> <Stack spacing={3}>
<Stack spacing={1}> <Stack spacing={1}>
<Typography variant="h5">{t('Delete Lesson Type ?')}</Typography> <Typography variant="h5">{t('Delete Lesson Type ?')}</Typography>
<Typography color="text.secondary" variant="body2"> <Typography
color="text.secondary"
variant="body2"
>
{t('Are you sure you want to delete lesson type ?')} {t('Are you sure you want to delete lesson type ?')}
</Typography> </Typography>
</Stack> </Stack>
<Stack direction="row" spacing={2} sx={{ justifyContent: 'flex-end' }}> <Stack
<Button color="secondary" onClick={handleClose}> direction="row"
spacing={2}
sx={{ justifyContent: 'flex-end' }}
>
<Button
color="secondary"
onClick={handleClose}
>
{t('Cancel')} {t('Cancel')}
</Button> </Button>
<LoadingButton <LoadingButton

View File

@@ -20,11 +20,9 @@ import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next'; 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 { LessonCategory } from '../lp_categories/type';
import { useLpCategoriesSelection } from './lp-categories-selection-context'; import { useLpCategoriesSelection } from './lp-categories-selection-context';
import { LpCategory } from './type'; import { LpCategory } from './type';
@@ -196,10 +194,21 @@ export function LpCategoriesFilters({
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"
/>
}
iconPosition="end" iconPosition="end"
key={tab.value} key={tab.value}
label={tab.label} label={tab.label}
@@ -210,8 +219,16 @@ export function LpCategoriesFilters({
))} ))}
</Tabs> </Tabs>
<Divider /> <Divider />
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flexWrap: 'wrap', px: 3, py: 2 }}> <Stack
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto', flexWrap: 'wrap' }}> direction="row"
spacing={2}
sx={{ alignItems: 'center', flexWrap: 'wrap', px: 3, py: 2 }}
>
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center', flex: '1 1 auto', flexWrap: 'wrap' }}
>
<FilterButton <FilterButton
displayValue={name} displayValue={name}
label={t('Name')} label={t('Name')}
@@ -241,16 +258,31 @@ export function LpCategoriesFilters({
{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
<Typography color="text.secondary" variant="body2"> direction="row"
spacing={2}
sx={{ alignItems: 'center' }}
>
<Typography
color="text.secondary"
variant="body2"
>
{selection.selected.size} {t('selected')} {selection.selected.size} {t('selected')}
</Typography> </Typography>
<Button color="error" variant="contained"> <Button
color="error"
variant="contained"
>
{t('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">{t('Newest')}</Option> <Option value="desc">{t('Newest')}</Option>
<Option value="asc">{t('Oldest')}</Option> <Option value="asc">{t('Oldest')}</Option>
</Select> </Select>
@@ -269,7 +301,12 @@ function TypeFilterPopover(): React.JSX.Element {
}, [initialValue]); }, [initialValue]);
return ( return (
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title={t('Filter by type')}> <FilterPopover
anchorEl={anchorEl}
onClose={onClose}
open={open}
title={t('Filter by type')}
>
<FormControl> <FormControl>
<OutlinedInput <OutlinedInput
onChange={(event) => { onChange={(event) => {
@@ -305,7 +342,12 @@ function NameFilterPopover(): React.JSX.Element {
}, [initialValue]); }, [initialValue]);
return ( return (
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title={t('Filter by name')}> <FilterPopover
anchorEl={anchorEl}
onClose={onClose}
open={open}
title={t('Filter by name')}
>
<FormControl> <FormControl>
<OutlinedInput <OutlinedInput
onChange={(event) => { onChange={(event) => {
@@ -341,7 +383,12 @@ function EmailFilterPopover(): React.JSX.Element {
}, [initialValue]); }, [initialValue]);
return ( return (
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title="Filter by email"> <FilterPopover
anchorEl={anchorEl}
onClose={onClose}
open={open}
title="Filter by email"
>
<FormControl> <FormControl>
<OutlinedInput <OutlinedInput
onChange={(event) => { onChange={(event) => {
@@ -377,7 +424,12 @@ function PhoneFilterPopover(): React.JSX.Element {
}, [initialValue]); }, [initialValue]);
return ( return (
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title="Filter by phone number"> <FilterPopover
anchorEl={anchorEl}
onClose={onClose}
open={open}
title="Filter by phone number"
>
<FormControl> <FormControl>
<OutlinedInput <OutlinedInput
onChange={(event) => { onChange={(event) => {

View File

@@ -2,6 +2,7 @@
import * as React from 'react'; import * as React from 'react';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import { LoadingButton } from '@mui/lab';
import Avatar from '@mui/material/Avatar'; 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 Button from '@mui/material/Button';
@@ -159,22 +160,23 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LpCateg
direction="row" direction="row"
spacing={1} spacing={1}
> >
<IconButton <LoadingButton
// //
color="secondary"
component={RouterLink} component={RouterLink}
href={paths.dashboard.lp_categories.details(row.id)} href={paths.dashboard.lp_categories.details(row.id)}
> >
<PencilSimpleIcon /> <PencilSimpleIcon size={24} />
</IconButton> </LoadingButton>
<IconButton <LoadingButton
color="error" color="error"
disabled={row.isEmpty} disabled={row.isEmpty}
onClick={() => { onClick={() => {
handleDeleteClick(row.id); handleDeleteClick(row.id);
}} }}
> >
<TrashSimpleIcon /> <TrashSimpleIcon size={24} />
</IconButton> </LoadingButton>
</Stack> </Stack>
), ),
name: 'Actions', name: 'Actions',

View File

@@ -3,7 +3,7 @@
import * as React from 'react'; import * as React from 'react';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { COL_LISTENINGS_PRACTICE_CATEGORIES } from '@/constants'; import { COL_QUIZ_LP_CATEGORIES } from '@/constants';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import { Avatar, Divider, MenuItem, Select } from '@mui/material'; import { Avatar, Divider, MenuItem, Select } from '@mui/material';
@@ -101,7 +101,7 @@ export function LpCategoryCreateForm(): React.JSX.Element {
}; };
try { try {
const result = await pb.collection(COL_LISTENINGS_PRACTICE_CATEGORIES).create(payload); const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).create(payload);
logger.debug(result); logger.debug(result);
toast.success(t('create.success')); toast.success(t('create.success'));

View File

@@ -4,7 +4,7 @@ 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 { COL_LISTENINGS_PRACTICE_CATEGORIES } from '@/constants'; import { COL_QUIZ_LP_CATEGORIES } from '@/constants';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
// //
@@ -129,7 +129,7 @@ export function LpCategoryEditForm(): React.JSX.Element {
}; };
try { try {
const result = await pb.collection(COL_LISTENINGS_PRACTICE_CATEGORIES).update(catId, tempUpdate); const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).update(catId, tempUpdate);
logger.debug(result); logger.debug(result);
toast.success(t('edit.success')); toast.success(t('edit.success'));
router.push(paths.dashboard.lp_categories.list); router.push(paths.dashboard.lp_categories.list);
@@ -171,7 +171,7 @@ export function LpCategoryEditForm(): React.JSX.Element {
setShowLoading(true); setShowLoading(true);
try { try {
const result = await pb.collection(COL_LISTENINGS_PRACTICE_CATEGORIES).getOne(id); const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getOne(id);
reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) }); reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) });
setTextDescription(result.description); setTextDescription(result.description);

View File

@@ -1,9 +1,7 @@
'use client'; 'use client';
import * as React from 'react'; import * as React from 'react';
import { useRouter } from 'next/navigation'; import deleteQuizMFCategories from '@/db/QuizMFCategories/Delete';
import { COL_LESSON_TYPES } from '@/constants';
import deleteQuizListening from '@/db/QuizListenings/Delete';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import { Button, Container, Modal, Paper } from '@mui/material'; import { Button, Container, Modal, Paper } from '@mui/material';
import Avatar from '@mui/material/Avatar'; import Avatar from '@mui/material/Avatar';
@@ -17,8 +15,6 @@ import { useTranslation } from 'react-i18next';
import { logger } from '@/lib/default-logger'; import { logger } from '@/lib/default-logger';
import { toast } from '@/components/core/toaster'; import { toast } from '@/components/core/toaster';
const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL);
export default function ConfirmDeleteModal({ export default function ConfirmDeleteModal({
open, open,
setOpen, setOpen,
@@ -45,30 +41,19 @@ export default function ConfirmDeleteModal({
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
}; };
function performDelete(id: string): Promise<void> {
return deleteQuizListening(id)
.then(() => {
toast(t('dashboard.lessonTypes.delete.success'));
reloadRows();
})
.catch((err) => {
logger.error(err);
toast(t('dashboard.lessonTypes.delete.error'));
})
.finally(() => {});
}
function handleUserConfirmDelete(): void { function handleUserConfirmDelete(): void {
if (idToDelete) { if (idToDelete) {
setIsDeleteing(true); setIsDeleteing(true);
performDelete(idToDelete) deleteQuizMFCategories(idToDelete)
.then(() => { .then(() => {
reloadRows();
handleClose(); handleClose();
toast(t('delete.success'));
}) })
.catch((err) => { .catch((err) => {
// console.error(err) // console.error(err)
logger.error(err); logger.error(err);
toast(t('dashboard.lessonTypes.delete.error')); toast(t('delete.error'));
}) })
.finally(() => { .finally(() => {
setIsDeleteing(false); setIsDeleteing(false);
@@ -87,19 +72,33 @@ export default function ConfirmDeleteModal({
<Box sx={style}> <Box sx={style}>
<Container maxWidth="sm"> <Container maxWidth="sm">
<Paper sx={{ border: '1px solid var(--mui-palette-divider)', boxShadow: 'var(--mui-shadows-16)' }}> <Paper sx={{ border: '1px solid var(--mui-palette-divider)', boxShadow: 'var(--mui-shadows-16)' }}>
<Stack direction="row" spacing={2} sx={{ display: 'flex', p: 3 }}> <Stack
direction="row"
spacing={2}
sx={{ display: 'flex', p: 3 }}
>
<Avatar sx={{ bgcolor: 'var(--mui-palette-error-50)', color: 'var(--mui-palette-error-main)' }}> <Avatar sx={{ bgcolor: 'var(--mui-palette-error-50)', color: 'var(--mui-palette-error-main)' }}>
<NoteIcon fontSize="var(--Icon-fontSize)" /> <NoteIcon fontSize="var(--Icon-fontSize)" />
</Avatar> </Avatar>
<Stack spacing={3}> <Stack spacing={3}>
<Stack spacing={1}> <Stack spacing={1}>
<Typography variant="h5">{t('Delete Lesson Type ?')}</Typography> <Typography variant="h5">{t('Delete Lesson Type ?')}</Typography>
<Typography color="text.secondary" variant="body2"> <Typography
color="text.secondary"
variant="body2"
>
{t('Are you sure you want to delete lesson type ?')} {t('Are you sure you want to delete lesson type ?')}
</Typography> </Typography>
</Stack> </Stack>
<Stack direction="row" spacing={2} sx={{ justifyContent: 'flex-end' }}> <Stack
<Button color="secondary" onClick={handleClose}> direction="row"
spacing={2}
sx={{ justifyContent: 'flex-end' }}
>
<Button
color="secondary"
onClick={handleClose}
>
{t('Cancel')} {t('Cancel')}
</Button> </Button>
<LoadingButton <LoadingButton

View File

@@ -3,9 +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_CATEGORIES } from '@/constants'; // import { COL_LESSON_CATEGORIES } from '@/constants';
import GetAllCount from '@/db/QuizListenings/GetAllCount'; import GetAllCount from '@/db/QuizMFCategories/GetAllCount';
import GetHiddenCount from '@/db/QuizListenings/GetHiddenCount'; import GetHiddenCount from '@/db/QuizMFCategories/GetHiddenCount';
import GetVisibleCount from '@/db/QuizListenings/GetVisibleCount'; import GetVisibleCount from '@/db/QuizMFCategories/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';
@@ -20,13 +20,11 @@ import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next'; 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 { LessonCategory } from '../mf_categories/type';
import { useLpCategoriesSelection } from './mf-categories-selection-context'; import { useLpCategoriesSelection } from './mf-categories-selection-context';
import { LpCategory } from './type'; import type { LpCategory } from './type';
export interface Filters { export interface Filters {
email?: string; email?: string;

View File

@@ -2,6 +2,7 @@
import * as React from 'react'; import * as React from 'react';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import { LoadingButton } from '@mui/lab';
import Avatar from '@mui/material/Avatar'; 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 Button from '@mui/material/Button';
@@ -159,22 +160,23 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LpCateg
direction="row" direction="row"
spacing={1} spacing={1}
> >
<IconButton <LoadingButton
// //
color="secondary"
component={RouterLink} component={RouterLink}
href={paths.dashboard.mf_categories.details(row.id)} href={paths.dashboard.mf_categories.details(row.id)}
> >
<PencilSimpleIcon /> <PencilSimpleIcon size={24} />
</IconButton> </LoadingButton>
<IconButton <LoadingButton
color="error" color="error"
disabled={row.isEmpty} disabled={row.isEmpty}
onClick={() => { onClick={() => {
handleDeleteClick(row.id); handleDeleteClick(row.id);
}} }}
> >
<TrashSimpleIcon /> <TrashSimpleIcon size={24} />
</IconButton> </LoadingButton>
</Stack> </Stack>
), ),
name: 'Actions', name: 'Actions',

View File

@@ -3,7 +3,7 @@
import * as React from 'react'; import * as React from 'react';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { COL_LISTENINGS_PRACTICE_CATEGORIES } from '@/constants'; import { COL_QUIZ_MF_CATEGORIES } from '@/constants';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import { Avatar, Divider, MenuItem, Select } from '@mui/material'; import { Avatar, Divider, MenuItem, Select } from '@mui/material';
@@ -101,7 +101,7 @@ export function LpCategoryCreateForm(): React.JSX.Element {
}; };
try { try {
const result = await pb.collection(COL_LISTENINGS_PRACTICE_CATEGORIES).create(payload); const result = await pb.collection(COL_QUIZ_MF_CATEGORIES).create(payload);
logger.debug(result); logger.debug(result);
toast.success(t('create.success')); toast.success(t('create.success'));

View File

@@ -4,7 +4,7 @@ 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 { COL_LISTENINGS_PRACTICE_CATEGORIES } from '@/constants'; import { COL_QUIZ_MF_CATEGORIES } from '@/constants';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
// //
@@ -129,7 +129,7 @@ export function LpCategoryEditForm(): React.JSX.Element {
}; };
try { try {
const result = await pb.collection(COL_LISTENINGS_PRACTICE_CATEGORIES).update(catId, tempUpdate); const result = await pb.collection(COL_QUIZ_MF_CATEGORIES).update(catId, tempUpdate);
logger.debug(result); logger.debug(result);
toast.success(t('edit.success')); toast.success(t('edit.success'));
router.push(paths.dashboard.mf_categories.list); router.push(paths.dashboard.mf_categories.list);
@@ -171,7 +171,7 @@ export function LpCategoryEditForm(): React.JSX.Element {
setShowLoading(true); setShowLoading(true);
try { try {
const result = await pb.collection(COL_LISTENINGS_PRACTICE_CATEGORIES).getOne(id); const result = await pb.collection(COL_QUIZ_MF_CATEGORIES).getOne(id);
reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) }); reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) });
setTextDescription(result.description); setTextDescription(result.description);

View File

@@ -1,4 +1,6 @@
// RULES: COL_<COLLECTION_NAME> = "<name in dbml file>" // RULES:
// COL_<COLLECTION_NAME> = "<table name in dbml file>"
// e.g. COL_APPLE = "Apple" table in dbml
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';
@@ -7,10 +9,12 @@ const NS_LESSON_CATEGORY = 'lesson_category';
const COL_USERS = 'users'; const COL_USERS = 'users';
const COL_USER_METAS = 'UserMetas'; const COL_USER_METAS = 'UserMetas';
// do not use LP_CATEGORIES // RULES:
const COL_LISTENINGS_PRACTICE_CATEGORIES = 'QuizLPCategories'; // do not use LP_CATEGORIES anymore
const COL_QUIZ_LP_CATEGORIES = 'QuizLPCategories';
const COL_QUIZ_LP_QUESTIONS = 'QuizLPQuestions'; const COL_QUIZ_LP_QUESTIONS = 'QuizLPQuestions';
const COL_MF_CATEGORIES = 'QuizMFCategories'; //
const COL_QUIZ_MF_CATEGORIES = 'QuizMFCategories';
export { export {
COL_LESSON_TYPES, COL_LESSON_TYPES,
@@ -21,9 +25,9 @@ export {
COL_USERS, COL_USERS,
COL_USER_METAS, COL_USER_METAS,
// //
COL_LISTENINGS_PRACTICE_CATEGORIES, COL_QUIZ_LP_CATEGORIES,
COL_QUIZ_LP_QUESTIONS, COL_QUIZ_LP_QUESTIONS,
// //
COL_MF_CATEGORIES, COL_QUIZ_MF_CATEGORIES,
// //
}; };

View File

@@ -1,8 +1,7 @@
import { COL_LISTENINGS_PRACTICE_CATEGORIES } from '@/constants'; import { COL_QUIZ_LP_CATEGORIES } from '@/constants';
import type { RecordModel } from 'pocketbase';
import { pb } from '@/lib/pb'; import { pb } from '@/lib/pb';
export default function deleteQuizListening(id: string): Promise<boolean> { export default function deleteQuizLPCategories(id: string): Promise<boolean> {
return pb.collection(COL_LISTENINGS_PRACTICE_CATEGORIES).delete(id); return pb.collection(COL_QUIZ_LP_CATEGORIES).delete(id);
} }

View File

@@ -1,8 +1,8 @@
import { COL_LISTENINGS_PRACTICE_CATEGORIES } from '@/constants'; import { COL_QUIZ_LP_CATEGORIES } from '@/constants';
import type { RecordModel } from 'pocketbase'; import type { RecordModel } from 'pocketbase';
import { pb } from '@/lib/pb'; import { pb } from '@/lib/pb';
export default function getAllQuizListenings(): Promise<RecordModel[]> { export default function getAllQuizListenings(): Promise<RecordModel[]> {
return pb.collection(COL_LISTENINGS_PRACTICE_CATEGORIES).getFullList(); return pb.collection(COL_QUIZ_LP_CATEGORIES).getFullList();
} }

View File

@@ -1,9 +1,9 @@
// REQ0006 // REQ0006
import { COL_LISTENINGS_PRACTICE_CATEGORIES } from '@/constants'; import { COL_QUIZ_LP_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.collection(COL_LISTENINGS_PRACTICE_CATEGORIES).getList(1, 9999, {}); const { totalItems: count } = await pb.collection(COL_QUIZ_LP_CATEGORIES).getList(1, 9999, {});
return count; return count;
} }

View File

@@ -1,8 +1,8 @@
import { COL_LISTENINGS_PRACTICE_CATEGORIES } from '@/constants'; import { COL_QUIZ_LP_CATEGORIES } from '@/constants';
import type { RecordModel } from 'pocketbase'; import type { RecordModel } from 'pocketbase';
import { pb } from '@/lib/pb'; import { pb } from '@/lib/pb';
export default function getQuizListeningById(id: string): Promise<RecordModel> { export default function getQuizListeningById(id: string): Promise<RecordModel> {
return pb.collection(COL_LISTENINGS_PRACTICE_CATEGORIES).getOne(id); return pb.collection(COL_QUIZ_LP_CATEGORIES).getOne(id);
} }

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { COL_LISTENINGS_PRACTICE_CATEGORIES } from '@/constants'; import { COL_QUIZ_LP_CATEGORIES } from '@/constants';
import type { ListResult, RecordModel } from 'pocketbase'; import type { ListResult, RecordModel } from 'pocketbase';
import { pb } from '@/lib/pb'; import { pb } from '@/lib/pb';
@@ -18,5 +18,5 @@ export default function listWithOption({
rowsPerPage, rowsPerPage,
listOption = {}, listOption = {},
}: ListWithOptionParams): Promise<ListResult<RecordModel>> { }: ListWithOptionParams): Promise<ListResult<RecordModel>> {
return pb.collection(COL_LISTENINGS_PRACTICE_CATEGORIES).getList(currentPage + 1, rowsPerPage, listOption); return pb.collection(COL_QUIZ_LP_CATEGORIES).getList(currentPage + 1, rowsPerPage, listOption);
} }

View File

@@ -0,0 +1,7 @@
import { COL_QUIZ_MF_CATEGORIES } from '@/constants';
import { pb } from '@/lib/pb';
export default function deleteQuizMFCategories(id: string): Promise<boolean> {
return pb.collection(COL_QUIZ_MF_CATEGORIES).delete(id);
}

View File

@@ -0,0 +1,9 @@
// REQ0006
import { COL_QUIZ_MF_CATEGORIES } from '@/constants';
import { pb } from '@/lib/pb';
export default async function GetAllCount(): Promise<number> {
const { totalItems: count } = await pb.collection(COL_QUIZ_MF_CATEGORIES).getList(1, 9999, {});
return count;
}

View File

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