diff --git a/src/pages/articles/Articles.tsx b/src/pages/articles/Articles.tsx index 1d41c349..3458a3b0 100644 --- a/src/pages/articles/Articles.tsx +++ b/src/pages/articles/Articles.tsx @@ -16,6 +16,7 @@ import { IArticle } from '@/types/article.types'; import { getArticleArticlesSelector } from '@/redux/reducers/article.reducer'; import { createArticle, deleteArticle, goToArticle } from '@/redux/actions/article.action'; import Head from '@/components/Head'; +import { getUserFullName } from '@/utils/user.utils'; const Articles = () => { const articles = useSelector(getArticleArticlesSelector); @@ -29,9 +30,9 @@ const Articles = () => { dispatch(createArticle(values)); } - const handleDelete = (id: string) => { - dispatch(deleteArticle(id)); - } + // const handleDelete = (id: string) => { + // dispatch(deleteArticle(id)); + // } const handlePreview = (id: string) => { navigate(goToArticle(id)); @@ -46,7 +47,7 @@ const Articles = () => { Id Title - {/* Author */} + Author Actions @@ -60,9 +61,9 @@ const Articles = () => { {article.objectId} {article.title} - {/* + {article.has("author") ? getUserFullName(article.get("author")) : "-"} - */} + handlePreview(article.objectId)}> @@ -70,9 +71,9 @@ const Articles = () => { {/* handleEdit(article.id)}> */} - handleDelete(article.objectId)}> + {/* handleDelete(article.objectId)}> - + */} ))} diff --git a/src/pages/estimates/Estimate.tsx b/src/pages/estimates/Estimate.tsx new file mode 100644 index 00000000..6fb06738 --- /dev/null +++ b/src/pages/estimates/Estimate.tsx @@ -0,0 +1,54 @@ +import { useTranslation } from 'react-i18next'; +import { useState } from 'react'; +import Head from '@/components/Head'; +import Dialog from '@/components/Dialog'; +import AddFab from '@/components/AddFab'; +import EstimateForm from './EstimateForm'; +import { EstimateInput } from '@/types/estimate.type'; +import { SubmitHandler } from 'react-hook-form'; +import { useDispatch } from 'react-redux'; +import { createEstimate, deleteEstimate, goToEstimates } from '@/redux/actions/estimate.action'; +import { useNavigate } from '@tanstack/react-router'; + +const ESTIMATE_FORM_ID = 'estimate-form-id'; + +// eslint-disable-next-line react-hooks/rules-of-hooks + +const Estimate = () => { + const { t } = useTranslation(); + + const dispatch = useDispatch(); + + + const [openFormDialog, setOpenFormDialog] = useState(false); + + const onSubmitHandler: SubmitHandler = values => { + dispatch(createEstimate(values)); + }; + + const toggleDialog = () => setOpenFormDialog(!openFormDialog); + + return ( +
+ +

Estimates

+ + + + +
+ ); +} + +export default Estimate; diff --git a/src/pages/estimates/Estimates.tsx b/src/pages/estimates/Estimates.tsx index 1d21ba90..f0556b70 100644 --- a/src/pages/estimates/Estimates.tsx +++ b/src/pages/estimates/Estimates.tsx @@ -1,49 +1,62 @@ +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Paper from '@mui/material/Paper'; +import { useDispatch, useSelector } from 'react-redux'; import { useTranslation } from 'react-i18next'; -import { useState } from 'react'; +import { FiTrash2 } from 'react-icons/fi'; +import { IconButton } from '@mui/material'; import Head from '@/components/Head'; -import Dialog from '@/components/Dialog'; -import AddFab from '@/components/AddFab'; -import EstimateForm from './EstimateForm'; -import { EstimateInput } from '@/types/estimate.type'; -import { SubmitHandler } from 'react-hook-form'; -import { useDispatch } from 'react-redux'; -import { createEstimate } from '@/redux/actions/estimate.action'; - -const ESTIMATE_FORM_ID = 'estimate-form-id'; +import { getEstimateEstimatesSelector } from '@/redux/reducers/estimate.reducer'; +import { IEstimate } from '@/types/estimate.type'; +import { deleteEstimate, goToEstimates } from '@/redux/actions/estimate.action'; const Estimates = () => { - const { t } = useTranslation(); - + const estimates = useSelector(getEstimateEstimatesSelector); const dispatch = useDispatch(); + const { t } = useTranslation(); - const [openFormDialog, setOpenFormDialog] = useState(false); - - const onSubmitHandler: SubmitHandler = values => { - dispatch(createEstimate(values)); - }; - - const toggleDialog = () => setOpenFormDialog(!openFormDialog); - + const handleDelete = async (id: string) => { + await dispatch(deleteEstimate(id)); + } return (
- -

Estimates

- - - - + + + + + + Id + Title + Actions + + + + {estimates.map((estimate: IEstimate, index: number) => ( + + + {estimate.objectId} + + + {estimate.url} + + + handleDelete(estimate.objectId)}> + + + + + ))} + +
+
); } diff --git a/src/pages/users/Users.tsx b/src/pages/users/Users.tsx index 0710a82e..4ce6a795 100644 --- a/src/pages/users/Users.tsx +++ b/src/pages/users/Users.tsx @@ -131,7 +131,7 @@ const Users = () => { } const onSendEmailFormSubmit = async (values: SendEmailInput) => { - console.log('values: ', values); + // console.log('values: ', values); if (!selectedUser) return; await dispatch(sendEmailToUser(selectedUser, values)); handleCloseDialog(); diff --git a/src/redux/actions/app.action.ts b/src/redux/actions/app.action.ts index b5097464..9cd162a2 100644 --- a/src/redux/actions/app.action.ts +++ b/src/redux/actions/app.action.ts @@ -81,7 +81,7 @@ export const changeSettings = (values: ISettingsInput): any => { * @param routeParams * @returns */ -export const onEnter = (onEnterAction: (dispatch: AppDispatch, getState?: () => RootState) => AppThunkAction) => (routeParams: any) => { +export const onEnter = (onEnterAction: (dispatch: AppDispatch, getState?: () => RootState) => AppThunkAction) => (routeParams: any) => { // get store from context (passed in RouterProvider) const { store } = routeParams.context; if (!store) return; diff --git a/src/redux/actions/estimate.action.ts b/src/redux/actions/estimate.action.ts index 8ace7e20..c1ac5401 100644 --- a/src/redux/actions/estimate.action.ts +++ b/src/redux/actions/estimate.action.ts @@ -1,15 +1,45 @@ -import Parse from "parse"; +import Parse, { Attributes } from "parse"; import { PATH_NAMES } from "@/utils/pathnames"; import { setValues } from "@/utils/parse.utils"; import { actionWithLoader } from "@/utils/app.utils"; -import { AppDispatch } from "../store"; +import { AppDispatch, RootState } from "../store"; import { setMessageSlice } from "../reducers/app.reducer"; import i18n from "@/config/i18n"; +import { addEstimateToEstimateSlice, deleteEstimateFromEstimatesSlice, loadEstimatesSlice } from "../reducers/estimate.reducer"; const Estimate = Parse.Object.extend("Estimate"); const ESTIMATE_PROPERTIES = new Set(['url']); +export const getEstimate = async (id: string): Promise => { + const estimate = await new Parse.Query(Estimate) + .equalTo('objectId', id) + .include(["comments"]) + .notEqualTo('deleted', true) + .first(); + + console.log("id estimate ---------:", id); + + if (!estimate) { + throw new Error("Estimate not found"); + } + return estimate; +} + +export const loadEstimates = (): any => { + return actionWithLoader(async (dispatch: AppDispatch): Promise => { + // user from BO + const result: any = await new Parse.Query(Estimate) + .withCount() + .notEqualTo('deleted', true) + .find(); + + const estimates = result.results.map((estimate: Attributes) => estimate.toJSON()); + + dispatch(loadEstimatesSlice(estimates)); + }); +}; + export const createEstimate = (values: any): any => { return actionWithLoader(async (dispatch: AppDispatch): Promise => { const estimate = new Estimate() @@ -20,7 +50,30 @@ export const createEstimate = (values: any): any => { const savedEstimate = await estimate.save(); dispatch(setMessageSlice(i18n.t('common:estimateCreatedSuccessfully'))); - return savedEstimate; + dispatch(addEstimateToEstimateSlice((savedEstimate as Attributes).toJSON())); + }); +}; + +export const deleteEstimate = (id: string,): any => { + return actionWithLoader(async (dispatch: AppDispatch): Promise => { + const estimate = await getEstimate(id); + + if (!estimate) return; + + estimate.set('deleted', true); + const deletedEstimate = await estimate.save(); + + dispatch(deleteEstimateFromEstimatesSlice(deletedEstimate.id)); + + dispatch(setMessageSlice('Estimate deleted successfully')); + }); +}; + + +export const onEstimatesEnter = (): any => { + return actionWithLoader(async (dispatch: AppDispatch): Promise => { + + dispatch(loadEstimates()); }); }; diff --git a/src/redux/reducers/estimate.reducer.ts b/src/redux/reducers/estimate.reducer.ts new file mode 100644 index 00000000..d69d9d9a --- /dev/null +++ b/src/redux/reducers/estimate.reducer.ts @@ -0,0 +1,46 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { IEstimate, IEstimateState } from '@/types/estimate.type'; + +const initialState: IEstimateState = { + loading: false, + estimate: null, + estimates: [], +}; + +export const estimate = createSlice({ + name: 'estimate', + initialState, + reducers: { + addEstimateToEstimateSlice: (state: IEstimateState, action: PayloadAction) => { + state.estimates = [...state.estimates, action.payload]; + }, + loadEstimatesSlice: (state: IEstimateState, action: PayloadAction) => { + state.estimates = action.payload; + }, + deleteEstimateFromEstimatesSlice: (state: IEstimateState, action: PayloadAction) => { + state.estimates = state.estimates.filter((estimate: IEstimate) => estimate.objectId !== action.payload); + }, + }, +}); + +export const { + addEstimateToEstimateSlice, + loadEstimatesSlice, + deleteEstimateFromEstimatesSlice +} = estimate.actions; + +// ---------------------------------------------- // +// ------------------ SELECTOR ------------------ // +// ---------------------------------------------- // +// NOTE: do not use RootState as state type to avoid import circular dependencies (from store.ts) +// we can not commit to git if there is circular dependencies +// export const getArticleSelector = (state: Record): IArticleState => state.article; +// export const getArticleArticleSelector = (state: Record): IArticle => state.article.article; +// export const getArticleLoadingSelector = (state: Record): boolean => state.article.loading; +export const getEstimateEstimatesSelector = (state: Record): IEstimate[] => state.estimate.estimates; + + +// export const getArticleCountSelector = (state: Record): number => state.article.count; + + +export default estimate.reducer; diff --git a/src/redux/store.ts b/src/redux/store.ts index 5c6353ed..1dfbbb5e 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -7,12 +7,14 @@ import appReducer from './reducers/app.reducer'; import roleReducer from './reducers/role.reducer'; import settingsReducer from './reducers/settings.reducer'; import userReducer from './reducers/user.reducer'; +import estimateReducer from './reducers/estimate.reducer'; import articleReducer from './reducers/article.reducer'; const reducers = { app: appReducer, user: userReducer, article: articleReducer, + estimate: estimateReducer, settings: settingsReducer, role: roleReducer, }; diff --git a/src/routes/protected/estimate.routes.tsx b/src/routes/protected/estimate.routes.tsx index 0f8b342a..418d2cea 100644 --- a/src/routes/protected/estimate.routes.tsx +++ b/src/routes/protected/estimate.routes.tsx @@ -2,7 +2,10 @@ import { Outlet, createRoute } from "@tanstack/react-router"; import { privateLayout } from "./private.routes"; import { PATH_NAMES } from "@/utils/pathnames"; +import { onEnter } from "@/redux/actions/app.action"; import Estimates from "@/pages/estimates/Estimates"; +import Estimate from "@/pages/estimates/Estimate"; +import { onEstimatesEnter } from "@/redux/actions/estimate.action"; export const estimatesLayout = createRoute({ getParentRoute: () => privateLayout, @@ -12,11 +15,18 @@ export const estimatesLayout = createRoute({ export const estimatesRoute = createRoute({ getParentRoute: () => estimatesLayout, - // beforeLoad: onEnter(onArticlesEnter), + beforeLoad: onEnter(onEstimatesEnter), component: Estimates, path: "/", }); -const estimateRoutes = [estimatesRoute]; +export const estimateRoute = createRoute({ + getParentRoute: () => estimatesLayout, + // beforeLoad: onEnter(onArticleEnter), + component: Estimate, + path: "$id", +}); + +const estimateRoutes = [estimatesRoute, estimateRoute]; export default estimateRoutes; diff --git a/src/types/estimate.type.ts b/src/types/estimate.type.ts index 5c227d29..d73bbb77 100644 --- a/src/types/estimate.type.ts +++ b/src/types/estimate.type.ts @@ -1,4 +1,18 @@ import { z } from "zod"; +import { Attributes } from "parse"; import { estimateSchema } from "@/validations/estimate.validation"; export type EstimateInput = z.infer; + +export interface IEstimate extends Attributes { + id: string; + url: string; + updatedAt?: string; + createdAt?: string; +} + +export interface IEstimateState { + loading: boolean; + estimate: IEstimate | null; + estimates: IEstimate[]; +} \ No newline at end of file diff --git a/src/types/util.type.ts b/src/types/util.type.ts index f5789251..376c1308 100644 --- a/src/types/util.type.ts +++ b/src/types/util.type.ts @@ -1,5 +1,6 @@ import { Dayjs } from "dayjs"; import { ISelectOption } from "./app.type"; +import { Attributes } from "parse"; export type DateType = string | number | Date | Dayjs | null | undefined; @@ -21,3 +22,8 @@ export interface ICreatableSelectOption extends Partial { inputValue?: any; disabled?: boolean; } + +export interface ParseResult { + results: Attributes[]; +} +