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[];
+}
+