├── .eslintrc.cjs
├── .gitignore
├── README.md
├── db.json
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── images
│ ├── Real estate agent offer house represented by model.webp
│ ├── agent-estate.jpg
│ └── logo_of_REMS.avif
└── logo.png
├── src
├── App.tsx
├── admin
│ ├── components
│ │ ├── agents
│ │ │ ├── AgentDrawer.tsx
│ │ │ ├── AgentForm.tsx
│ │ │ ├── AgentList.tsx
│ │ │ └── styles.css
│ │ ├── appointments
│ │ │ └── Appointments.tsx
│ │ ├── clients
│ │ │ ├── ClientDrawer.tsx
│ │ │ ├── ClientForm.tsx
│ │ │ ├── ClientList.tsx
│ │ │ └── styles.css
│ │ ├── dashboard
│ │ │ ├── HomePage.tsx
│ │ │ └── LineChartDashboard.tsx
│ │ ├── properties
│ │ │ ├── PropertyDetail.tsx
│ │ │ └── PropertyList.tsx
│ │ └── transactions
│ │ │ └── TransactionList.tsx
│ └── layouts
│ │ ├── DashboardLayout.tsx
│ │ ├── context
│ │ └── DashboradContext.tsx
│ │ ├── header
│ │ └── DashboardHeader.tsx
│ │ └── sidebar
│ │ ├── DashboardSidebar.tsx
│ │ └── style.css
├── agents
│ ├── agent-services
│ │ ├── ScrollToTop.ts
│ │ └── propertyFilterSearch.ts
│ ├── appointment
│ │ └── AgentAppointment.tsx
│ ├── defalult-page
│ │ ├── AgentDefaultPage.tsx
│ │ ├── AgentFooter.tsx
│ │ ├── NavCards.tsx
│ │ ├── RecentPosts.tsx
│ │ ├── VinylSearch.tsx
│ │ ├── YangonRecommended.tsx
│ │ └── components
│ │ │ └── PropertyCard.tsx
│ ├── header
│ │ └── AgentHeader.tsx
│ ├── property-crud
│ │ ├── PropertiesCRUD.tsx
│ │ ├── PropertiesForm.tsx
│ │ ├── PropertyCard.tsx
│ │ ├── PropertyDrawer.tsx
│ │ ├── db.ts
│ │ └── styles.css
│ ├── property-list
│ │ ├── AgentPropertyList.tsx
│ │ ├── DetailPage.tsx
│ │ ├── Dropdowns
│ │ │ ├── FilterDropdown.tsx
│ │ │ ├── PriceDropDown.tsx
│ │ │ └── SimpleFilterDropDown.tsx
│ │ ├── Filters
│ │ │ ├── BaAndBdsFilter.tsx
│ │ │ ├── CityFilter.tsx
│ │ │ ├── HomeTypeFilter.tsx
│ │ │ └── PriceRangeFilter.tsx
│ │ ├── Flyout.tsx
│ │ ├── PropertyDetails.tsx
│ │ ├── Reviews.tsx
│ │ ├── SingleCard.tsx
│ │ └── data-for-agent
│ │ │ ├── propertyData.ts
│ │ │ └── reviewsData.ts
│ └── transactions
│ │ └── AgentTransactions.tsx
├── app
│ ├── hook.ts
│ └── store.ts
├── client
│ ├── components
│ │ ├── appointment
│ │ │ ├── AppointHistory.tsx
│ │ │ ├── Appointment.tsx
│ │ │ ├── AppointmentForm.tsx
│ │ │ ├── AppointmentHistoryList.tsx
│ │ │ ├── PickDate.tsx
│ │ │ └── PickTime.tsx
│ │ ├── property
│ │ │ ├── Checkbox.tsx
│ │ │ ├── CheckboxGroup.tsx
│ │ │ ├── Container.tsx
│ │ │ ├── FilterHome.tsx
│ │ │ ├── Home.tsx
│ │ │ ├── PriceRangeSlider.tsx
│ │ │ ├── PropertyById.tsx
│ │ │ ├── PropertyCard.tsx
│ │ │ └── PropertyGroup.tsx
│ │ ├── review
│ │ │ ├── RatingReview.tsx
│ │ │ └── Review.tsx
│ │ └── transaction
│ │ │ ├── Transaction.tsx
│ │ │ ├── TransactionCreateForm.tsx
│ │ │ └── TransactionSummary.tsx
│ ├── db
│ │ ├── data.ts
│ │ └── mock.json
│ ├── layouts
│ │ └── Navbar.tsx
│ └── style
│ │ └── priceRange.css
├── error
│ └── Error.tsx
├── errorPage
│ └── ErrorPage.tsx
├── index.css
├── login
│ ├── AgentRegister.tsx
│ ├── ClientRegister.tsx
│ ├── Login.tsx
│ ├── Register.tsx
│ └── login-context
│ │ └── AuthContext.tsx
├── main.tsx
├── routes
│ └── Router.tsx
├── services
│ ├── admin
│ │ └── api
│ │ │ ├── agentApi.ts
│ │ │ ├── appointmentApi.ts
│ │ │ ├── clientApi.ts
│ │ │ ├── dashboardApi.ts
│ │ │ ├── propertiesApi.ts
│ │ │ └── transactionsApi.ts
│ ├── agent
│ │ └── api
│ │ │ ├── appointment.ts
│ │ │ ├── getAgentApiSlice.ts
│ │ │ ├── propertyApiSlice.ts
│ │ │ └── text.ts
│ ├── client
│ │ ├── api
│ │ │ ├── Review.ts
│ │ │ ├── appointmentApi.ts
│ │ │ ├── propertyApi.ts
│ │ │ ├── text.ts
│ │ │ ├── transactionApi.ts
│ │ │ └── userIdApi.ts
│ │ └── features
│ │ │ ├── appointmentSlice.ts
│ │ │ ├── currentPageSlice.ts
│ │ │ └── idSlice.ts
│ └── login-interceptors
│ │ └── axios.ts
├── type
│ └── type.ts
└── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:@typescript-eslint/recommended',
7 | 'plugin:react-hooks/recommended',
8 | ],
9 | ignorePatterns: ['dist', '.eslintrc.cjs'],
10 | parser: '@typescript-eslint/parser',
11 | plugins: ['react-refresh'],
12 | rules: {
13 | 'react-refresh/only-export-components': [
14 | 'warn',
15 | { allowConstantExport: true },
16 | ],
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Dependency directories
9 | node_modules/
10 |
11 | # Environment files
12 | .env
13 | .env.local
14 | .env.development.local
15 | .env.test.local
16 | .env.production.local
17 |
18 | # Build output
19 | build/
20 | dist/
21 |
22 | # Cache directories
23 | .cache/
24 | .next/
25 | out/
26 | public/static/
27 |
28 | # Testing
29 | coverage/
30 | junit.xml
31 | test-results.xml
32 |
33 | # Miscellaneous
34 | .DS_Store
35 | Thumbs.db
36 | .idea/
37 | .vscode/
38 | *.swp
39 | *~.swp
40 | *.swo
41 | *.swn
42 |
43 | # Editor directories and files
44 | /.vscode/
45 | /.vscode/*
46 | *.code-workspace
47 | .idea/
48 | *.sublime-project
49 | *.sublime-workspace
50 |
51 | # React Native specific
52 | .expo/
53 | .expo-shared/
54 |
55 | # Yarn specific
56 | .yarn/*
57 | !.yarn/patches
58 | !.yarn/plugins
59 | !.yarn/sdks
60 | !.yarn/versions
61 | .pnp.*
62 | .pnp/
63 |
64 | # SASS cache
65 | .sass-cache/
66 |
67 | # Optional npm cache directory
68 | .npm
69 |
70 | # eslint cache
71 | .eslintcache
72 |
73 | # Optional REACT-NATIVE CACHES
74 | react-native/packager-info.json
75 | react-native/metro-cache/
76 |
77 | .vs
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React + TypeScript + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## Expanding the ESLint configuration
11 |
12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
13 |
14 | - Configure the top-level `parserOptions` property like this:
15 |
16 | ```js
17 | export default {
18 | // other rules...
19 | parserOptions: {
20 | ecmaVersion: 'latest',
21 | sourceType: 'module',
22 | project: ['./tsconfig.json', './tsconfig.node.json'],
23 | tsconfigRootDir: __dirname,
24 | },
25 | }
26 | ```
27 |
28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
31 |
32 | json-server --watch ./src/client/db/mock.json
33 |
--------------------------------------------------------------------------------
/db.json:
--------------------------------------------------------------------------------
1 | {
2 | "appointment": [
3 | {
4 | "appointmentId": "1",
5 | "agentName": "John Doe",
6 | "propertyName": "Sunny Apartments",
7 | "appointmentDate": "2024-07-1",
8 | "appointmentTime": "10:00 AM",
9 | "status": "Pending",
10 | "notes": "Client prefers morning appointments."
11 | },
12 | {
13 | "appointmentId": "2",
14 | "agentName": "Jane Smith",
15 | "propertyName": "Green Meadows",
16 | "appointmentDate": "2024-07-22",
17 | "appointmentTime": "02:00 PM",
18 | "status": "Confirmed",
19 | "notes": "Bring property documents."
20 | },
21 | {
22 | "appointmentId": "3",
23 | "agentName": "Michael Brown",
24 | "propertyName": "Ocean View Villas",
25 | "appointmentDate": "2024-07-23",
26 | "appointmentTime": "11:00 AM",
27 | "status": "Done",
28 | "notes": "Client rescheduled for next week."
29 | },
30 | {
31 | "appointmentId": "4",
32 | "agentName": "Emily Davis",
33 | "propertyName": "Maple Residences",
34 | "appointmentDate": "2024-07-24",
35 | "appointmentTime": "09:00 AM",
36 | "status": "Done",
37 | "notes": "Awaiting client confirmation."
38 | }
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | REMS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rems_react",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build",
9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@ant-design/icons": "^5.3.7",
14 | "@reduxjs/toolkit": "^2.2.6",
15 | "@types/react-router": "^5.1.20",
16 | "@types/react-router-dom": "^5.3.3",
17 | "antd": "^5.19.1",
18 | "axios": "^1.7.7",
19 | "framer-motion": "^11.5.4",
20 | "jwt-decode": "^4.0.0",
21 | "react": "^18.3.1",
22 | "react-dom": "^18.3.1",
23 | "react-icons": "^5.3.0",
24 | "react-images-uploading": "^3.1.7",
25 | "react-redux": "^9.1.2",
26 | "react-router": "^6.24.1",
27 | "react-router-dom": "^6.24.1",
28 | "react-star-rating": "^1.4.2",
29 | "recharts": "^2.12.7",
30 | "redux-persist": "^6.0.0",
31 | "sonner": "^1.5.0",
32 | "swiper": "^11.1.14"
33 | },
34 | "devDependencies": {
35 | "@types/react": "^18.3.3",
36 | "@types/react-dom": "^18.3.0",
37 | "@typescript-eslint/eslint-plugin": "^7.13.1",
38 | "@typescript-eslint/parser": "^7.13.1",
39 | "@vitejs/plugin-react": "^4.3.1",
40 | "autoprefixer": "^10.4.19",
41 | "eslint": "^8.57.0",
42 | "eslint-plugin-react-hooks": "^4.6.2",
43 | "eslint-plugin-react-refresh": "^0.4.7",
44 | "postcss": "^8.4.39",
45 | "tailwindcss": "^3.4.4",
46 | "typescript": "^5.2.2",
47 | "vite": "^5.3.1"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/images/Real estate agent offer house represented by model.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/one-project-one-month/rems_react/18eeeda10a34cc3ebe073c7da9a52b7a876a944b/public/images/Real estate agent offer house represented by model.webp
--------------------------------------------------------------------------------
/public/images/agent-estate.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/one-project-one-month/rems_react/18eeeda10a34cc3ebe073c7da9a52b7a876a944b/public/images/agent-estate.jpg
--------------------------------------------------------------------------------
/public/images/logo_of_REMS.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/one-project-one-month/rems_react/18eeeda10a34cc3ebe073c7da9a52b7a876a944b/public/images/logo_of_REMS.avif
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/one-project-one-month/rems_react/18eeeda10a34cc3ebe073c7da9a52b7a876a944b/public/logo.png
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { ConfigProvider } from "antd";
2 | import Router from "./routes/Router";
3 | import { Toaster } from "sonner";
4 | import { AuthProvider } from "./login/login-context/AuthContext";
5 | import { Provider } from "react-redux";
6 | import { persistor, store } from "./app/store";
7 | import { PersistGate } from "redux-persist/integration/react";
8 |
9 | export default function App() {
10 | return (
11 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/src/admin/components/agents/AgentDrawer.tsx:
--------------------------------------------------------------------------------
1 | import { Drawer } from "antd";
2 | import AgentForm from "./AgentForm";
3 | import "./styles.css";
4 | import { Agent } from "../../../type/type";
5 |
6 | interface Props {
7 | onClose: () => void;
8 | open: boolean;
9 | records: Agent | null;
10 | refetch: () => void;
11 | }
12 |
13 | const AgentDrawer = ({ onClose, open, records, refetch }: Props) => {
14 | return (
15 | <>
16 |
26 |
31 |
32 | >
33 | );
34 | };
35 |
36 | export default AgentDrawer;
37 |
--------------------------------------------------------------------------------
/src/admin/components/agents/AgentForm.tsx:
--------------------------------------------------------------------------------
1 | import { Button, ConfigProvider, Form, Input, Space } from "antd";
2 | import { RuleObject } from "antd/lib/form";
3 | import { StoreValue } from "rc-field-form/lib/interface";
4 | import { useEffect } from "react";
5 | import { Agent } from "../../../type/type";
6 | import { toast } from "sonner";
7 | import {
8 | useCreateAgentMutation,
9 | useUpdateAgentByIdMutation,
10 | } from "../../../services/admin/api/agentApi";
11 |
12 | interface Props {
13 | onClose: () => void;
14 | initialValues: Agent | null;
15 | refetch: () => void;
16 | }
17 |
18 | const AgentForm = ({ onClose, initialValues, refetch }: Props) => {
19 | const [createAgent] = useCreateAgentMutation();
20 | const [updateAgent] = useUpdateAgentByIdMutation();
21 | const [form] = Form.useForm();
22 |
23 | const { TextArea } = Input;
24 |
25 | useEffect(() => {
26 | if (initialValues) {
27 | form.setFieldsValue(initialValues);
28 | } else {
29 | form.resetFields();
30 | }
31 | }, [initialValues, form]);
32 |
33 | const onFinish = async () => {
34 | const values = form.getFieldsValue();
35 | const passwordWithValue = { ...values, password: "password123" };
36 |
37 | try {
38 | if (initialValues && initialValues.agentId) {
39 | // Update agent if initialValues is provided
40 | await updateAgent({
41 | data: passwordWithValue,
42 | id: initialValues.agentId,
43 | });
44 | refetch();
45 | toast.success("Agent update successfully");
46 | onClose();
47 | } else {
48 | // Create new agent if no initialValues
49 | await createAgent(passwordWithValue).unwrap();
50 | refetch();
51 | toast.success("Agent create successfully");
52 | onClose();
53 | }
54 | onClose();
55 | } catch (error) {
56 | toast.error("Error submitting form");
57 | }
58 | };
59 |
60 | const validatePhoneNumber = (_: RuleObject, value: StoreValue) => {
61 | const phoneRegex = /^09\d{7,9}$/;
62 | if (!value) {
63 | return Promise.reject(new Error("Please enter your phone number."));
64 | }
65 | if (!phoneRegex.test(value)) {
66 | return Promise.reject(
67 | new Error("Please enter a valid Myanmar phone number (09xxxxxxxxx).")
68 | );
69 | }
70 | return Promise.resolve();
71 | };
72 |
73 | return (
74 | (
77 | <>
78 | {label}
79 | {required && (
80 | *
81 | )}
82 | >
83 | ),
84 | }}>
85 |
94 |
95 |
96 |
100 |
101 |
102 |
106 |
107 |
108 |
115 |
116 |
117 |
121 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 | Submit
135 |
136 | Cancel
137 |
138 |
139 |
140 |
141 | );
142 | };
143 |
144 | export default AgentForm;
145 |
--------------------------------------------------------------------------------
/src/admin/components/agents/styles.css:
--------------------------------------------------------------------------------
1 | .custom-form .ant-form-item-required::before {
2 | display: none !important;
3 | }
4 |
--------------------------------------------------------------------------------
/src/admin/components/appointments/Appointments.tsx:
--------------------------------------------------------------------------------
1 | import { Table } from "antd";
2 | import { TableProps } from "antd/lib";
3 | import dayjs from "dayjs";
4 | import React, { useState } from "react";
5 | import { useGetAppoitmentByAdminQuery } from "../../../services/client/api/appointmentApi";
6 | import { TAppointment } from "../../../type/type";
7 |
8 | const columns: TableProps['columns'] = [
9 | {
10 | title: "Appointment ID",
11 | dataIndex: "appointmentId",
12 | key: "appointmentId",
13 | width: 20,
14 | align: 'center',
15 | },
16 | {
17 | title: "Agent Name",
18 | dataIndex: "agentName",
19 | key: "agentName",
20 | },
21 | {
22 | title: "Appointment Date",
23 | dataIndex: "appointmentDate",
24 | key: "appointmentDate",
25 | render: (date: Date) => dayjs(date).format("DD/MM/YYYY"),
26 | },
27 | {
28 | title: "Appointment Time",
29 | dataIndex: "appointmentTime",
30 | key: "appointmentTime",
31 | render: (time: string) => {
32 | const formattedTime = time.split('.')[0];
33 | const dayjsTime = dayjs(formattedTime, "HH:mm:ss");
34 | return dayjs(dayjsTime).format("HH:mm:ss A");
35 | },
36 | },
37 | {
38 | title: "Features",
39 | dataIndex: "features",
40 | key: "features",
41 | render: (_, record) => {`${record.size} sq ft, ${record.numberOfBedrooms} bed, ${record.numberOfBathrooms} bath`}
42 | ,
43 | },
44 | {
45 | title: "Address",
46 | dataIndex: "address",
47 | key: "address",
48 | render: (_, record) => (
49 |
50 | {`${record.address}, (${record.city}, ${record.state})`} {" "}
51 |
52 |
53 | ),
54 | },
55 | {
56 | title: 'Price',
57 | dataIndex: 'price',
58 | key: 'price',
59 | render: (_, record) => {record.price} MMK
60 | },
61 | {
62 | title: "Notes",
63 | dataIndex: "notes",
64 | key: "notes",
65 | render: (notes: string | null) => notes || "No Notes",
66 | },
67 | ];
68 |
69 | const App: React.FC = () => {
70 | const [page, setPage] = useState({ pageNumber: 1, pageSize: 10 });
71 |
72 | const { isFetching, data } = useGetAppoitmentByAdminQuery(page)
73 |
74 | const pageSetting = data?.data?.pageSetting;
75 | const appoitmentData = data?.data?.appointmentDetails ?? [];
76 |
77 | const handlePagination = (page: number, pageSize: number) => {
78 | setPage({ pageNumber: page, pageSize: pageSize });
79 | };
80 |
81 | return (
82 |
89 | );
90 | }
91 |
92 | export default App;
93 |
--------------------------------------------------------------------------------
/src/admin/components/clients/ClientDrawer.tsx:
--------------------------------------------------------------------------------
1 | import { Drawer } from "antd";
2 | import UserForm from "./ClientForm";
3 | import "./styles.css";
4 | import { Client } from "../../../type/type";
5 |
6 | interface Props {
7 | onClose: () => void;
8 | open: boolean;
9 | records: Client | null;
10 | refetch: () => void;
11 | }
12 |
13 | const ClientDrawer = ({ onClose, open, records, refetch }: Props) => {
14 | return (
15 | <>
16 |
26 |
27 |
28 | >
29 | );
30 | };
31 |
32 | export default ClientDrawer;
33 |
--------------------------------------------------------------------------------
/src/admin/components/clients/ClientForm.tsx:
--------------------------------------------------------------------------------
1 | import { Button, ConfigProvider, Form, Input, Space } from "antd";
2 | import { RuleObject } from "antd/lib/form";
3 | import { StoreValue } from "rc-field-form/lib/interface";
4 | import { useEffect } from "react";
5 | import { Client } from "../../../type/type";
6 | import { toast } from "sonner";
7 | import {
8 | useCreateClientMutation,
9 | useUpdateClientByIdMutation,
10 | } from "../../../services/admin/api/clientApi";
11 |
12 | interface Props {
13 | onClose: () => void;
14 | initialValues: Client | null;
15 | refetch: () => void;
16 | }
17 |
18 | const ClientForm = ({ onClose, initialValues, refetch }: Props) => {
19 | const [createClient] = useCreateClientMutation();
20 | const [updateClient] = useUpdateClientByIdMutation();
21 | const [form] = Form.useForm();
22 |
23 | const { TextArea } = Input;
24 |
25 | useEffect(() => {
26 | if (initialValues) {
27 | form.setFieldsValue(initialValues);
28 | } else {
29 | form.resetFields();
30 | }
31 | }, [initialValues, form]);
32 |
33 | const onFinish = async () => {
34 | const values = form.getFieldsValue();
35 | const passwordWithValue = { ...values, password: "password123" };
36 |
37 | try {
38 | if (initialValues && initialValues.clientId) {
39 | // Update client if initialValues is provided
40 | await updateClient({
41 | data: passwordWithValue,
42 | id: initialValues.clientId,
43 | }).unwrap();
44 | refetch();
45 | toast.success("Client update successfully");
46 | onClose();
47 | } else {
48 | // Create new client if no initialValues
49 | await createClient(passwordWithValue).unwrap();
50 | refetch();
51 | toast.success("Client create successfully");
52 | onClose();
53 | }
54 | onClose();
55 | } catch (error) {
56 | console.error("Error submitting form:", error);
57 | toast.error("Error submitting form");
58 | }
59 | };
60 |
61 | const validatePhoneNumber = (_: RuleObject, value: StoreValue) => {
62 | const phoneRegex = /^09\d{7,10}$/;
63 | if (!value) {
64 | return Promise.reject(new Error("Please enter your phone number."));
65 | }
66 | if (!phoneRegex.test(value)) {
67 | return Promise.reject(
68 | new Error("Please enter a valid Myanmar phone number (09xxxxxxxxx).")
69 | );
70 | }
71 | return Promise.resolve();
72 | };
73 |
74 | return (
75 | (
78 | <>
79 | {label}
80 | {required && (
81 | *
82 | )}
83 | >
84 | ),
85 | }}>
86 |
97 |
98 |
99 |
103 |
104 |
105 |
112 |
113 |
114 |
118 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 | Submit
132 |
133 | Cancel
134 |
135 |
136 |
137 |
138 | );
139 | };
140 |
141 | export default ClientForm;
142 |
--------------------------------------------------------------------------------
/src/admin/components/clients/styles.css:
--------------------------------------------------------------------------------
1 | .custom-form .ant-form-item-required::before {
2 | display: none !important;
3 | }
4 |
--------------------------------------------------------------------------------
/src/admin/components/dashboard/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowDownOutlined, ArrowUpOutlined } from '@ant-design/icons'
2 | import { Card, Col, ConfigProvider, Flex, Row, Skeleton, Statistic, Table, TableProps, Typography } from 'antd'
3 | import { AgentActivity, DashboardData, useGetDashboardDataQuery } from '../../../services/admin/api/dashboardApi'
4 | import LineChartDashboard from './LineChartDashboard'
5 |
6 | interface statisticProps {
7 | title: String
8 | value?: number
9 | precision?: number
10 | valueStyle?: {
11 | color: string
12 | }
13 | prefix?: any
14 | suffix?: string
15 | }
16 |
17 | const columns: TableProps['columns'] = [
18 | {
19 | title: 'Agent Name',
20 | dataIndex: 'agentName',
21 | key: 'name'
22 | },
23 | {
24 | title: 'Properties Sold',
25 | dataIndex: 'sellProperty',
26 | key: 'sellProperty',
27 | align: 'center'
28 | },
29 | {
30 | title: 'Properties Rented',
31 | dataIndex: 'rentedProperty',
32 | key: 'rentedProperty',
33 | align: 'center'
34 | },
35 | {
36 | title: 'Total Sales',
37 | dataIndex: 'totalSales',
38 | key: 'totalSales',
39 | align: 'center'
40 | },
41 | {
42 | title: 'Commission Earned',
43 | dataIndex: 'commissionEarned',
44 | key: 'commissionEarned',
45 | align: 'center'
46 | },
47 | ];
48 |
49 | const CustomStatistic = (props: statisticProps) => {
50 | return (
59 |
62 | )
63 | }
64 |
65 | const HomePage = () => {
66 | const { isFetching, data } = useGetDashboardDataQuery();
67 |
68 | const dashboardData: DashboardData = {
69 | overview: Array.isArray(data?.data?.overview) ? data.data.overview : [],
70 | weeklyActivity: Array.isArray(data?.data?.weeklyActivity) ? data.data.weeklyActivity : [],
71 | agentActivity: Array.isArray(data?.data?.agentActivity) ? data.data.agentActivity : [],
72 | }
73 |
74 | const overviewArr = dashboardData.overview || [];
75 |
76 | return (
77 | <>
78 | Overview
79 | <>
80 | {
81 | isFetching ? : <>
82 |
83 |
84 |
85 |
86 |
87 |
88 |
92 |
96 |
100 |
101 |
102 |
103 |
104 | }
112 | suffix="$"
113 | />
114 | }
122 | suffix="$"
123 | />
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | Top Agent Activity
134 |
135 | >
136 | }
137 | >
138 | >
139 | )
140 | }
141 |
142 | export default HomePage
--------------------------------------------------------------------------------
/src/admin/components/dashboard/LineChartDashboard.tsx:
--------------------------------------------------------------------------------
1 | import { PureComponent } from 'react';
2 | import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
3 | import { WeeklyActivity } from '../../../services/admin/api/dashboardApi';
4 |
5 | interface tickProps {
6 | x: number;
7 | y: number;
8 | stroke?: string;
9 | payload?: {
10 | value: string;
11 | };
12 | value?: string;
13 | }
14 |
15 | interface LineChartProps {
16 | data?: WeeklyActivity[]
17 | }
18 |
19 | class CustomizedLabel extends PureComponent {
20 | render() {
21 | const { x, y, stroke, value } = this.props;
22 |
23 | return (
24 |
25 | {value}
26 |
27 | );
28 | }
29 | }
30 |
31 | class CustomizedAxisTick extends PureComponent {
32 | render() {
33 | const { x, y, payload } = this.props;
34 |
35 | return (
36 |
37 |
38 | {payload?.value}
39 |
40 |
41 | );
42 | }
43 | }
44 |
45 | export default class LineChartDashboard extends PureComponent {
46 | static demoUrl = 'https://codesandbox.io/p/sandbox/line-chart-with-customized-label-d6rytv';
47 |
48 |
49 | render() {
50 | const { data } = this.props;
51 |
52 | return (
53 |
54 |
65 |
66 | } />
70 |
71 |
72 |
73 | } />
77 |
78 |
79 |
80 | );
81 | }
82 | }
--------------------------------------------------------------------------------
/src/admin/components/properties/PropertyDetail.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Col, Divider, Flex, message, Row, Typography } from "antd";
2 | import dayjs from "dayjs";
3 | import { useLocation, useNavigate } from "react-router";
4 | import { useChangestatusMutation } from "../../../services/admin/api/propertiesApi.ts";
5 |
6 | const PropertyDetail = () => {
7 | const location = useLocation();
8 | const properties = location.state.properties;
9 | const navigate = useNavigate();
10 |
11 | const [changeStatus] = useChangestatusMutation();
12 |
13 | const handleApprove = async () => {
14 | try {
15 | await changeStatus({
16 | propertyId: properties?.property?.propertyId,
17 | propertyStatus: "Approved",
18 | approvedBy: "admin",
19 | }).unwrap();
20 | message.success("Successfully approved");
21 | } catch (error) {
22 | console.log("error is happening", error);
23 | }
24 | };
25 |
26 | const cancelApprove = async () => {
27 | try {
28 | await changeStatus({
29 | propertyId: properties?.property?.propertyId,
30 | propertyStatus: " Canceled",
31 | approvedBy: "admin",
32 | }).unwrap();
33 | message.success("Successfully Rejected");
34 | } catch (error) {
35 | console.log("error is is happening", error);
36 | }
37 | };
38 |
39 | return (
40 |
41 |
42 |
43 | Property Detail
44 |
45 | navigate(-1)}
48 | >
49 | {" "}
50 | back{" "}
51 |
52 |
53 |
54 |
55 |
56 | Approved By
57 |
58 | {properties.property.approvedby}
59 |
60 |
61 |
62 |
63 | Added Date & Edited Date
64 |
65 |
66 | {`${dayjs(properties.property.addDate).format(
67 | "YYYY-MM-DD HH:mm A"
68 | )}, ${dayjs(properties.property.editDate).format(
69 | "YYYY-MM-DD HH:mm A"
70 | )}`}
71 |
72 |
73 |
74 | Description
75 |
76 | {properties.property.description}
77 |
78 |
79 |
80 |
81 | {properties.reviews.length !== 0 && (
82 | <>
83 |
84 | Client Reviews
85 |
86 |
87 | {properties.reviews.map((review: any) => (
88 |
89 |
90 | User Name
91 | Testing username
92 |
93 |
94 | Rating
95 |
96 | {review.rating ? `${review.rating} ⭐` : "N/A"}
97 |
98 |
99 |
100 | Comments
101 |
102 | {review.comments || "No comments"}
103 |
104 |
105 |
106 | ))}
107 |
108 | >
109 | )}
110 | {properties.property.status === "Pending" && (
111 |
112 |
113 |
114 | Approve
115 |
116 |
117 | Reject
118 |
119 |
120 |
121 | )}
122 |
123 | );
124 | };
125 |
126 | export default PropertyDetail;
127 |
--------------------------------------------------------------------------------
/src/admin/components/properties/PropertyList.tsx:
--------------------------------------------------------------------------------
1 | import type { TableProps } from "antd";
2 | import { Table, Tag, Typography } from "antd";
3 | import React, { useState } from "react";
4 | import { Properties ,PropertyResponse} from "../../../type/type";
5 | import { useGetAllPropertiesQuery } from "../../../services/admin/api/propertiesApi";
6 | import { Link } from "react-router-dom";
7 |
8 | const renderStatus = (status: any) => {
9 | let color;
10 |
11 | switch (status) {
12 | case "AVAILABLE":
13 | color = "green";
14 | break;
15 | case "SOLD":
16 | color = "red";
17 | break;
18 | default:
19 | break;
20 | }
21 | return {status} ;
22 | };
23 |
24 | const columns: TableProps["columns"] = [
25 | {
26 | title: "Property ID",
27 | dataIndex: "propertyId",
28 | key: "propertyId",
29 | align: "center",
30 | render: (_, record) => {record?.property.propertyId} ,
31 | },
32 | {
33 | title: "Address",
34 | dataIndex: "address",
35 | key: "address",
36 | render: (_, record) => (
37 |
38 | {`${record.property.address}, (${record.property.city}, ${record.property.state})`} {" "}
39 |
40 |
41 | ),
42 | },
43 | {
44 | title: "Type",
45 | dataIndex: "property_type",
46 | key: "property_type",
47 | render: (_, record) => (
48 |
49 | {record.property.propertyType}
50 |
51 | {record.property.availiablityType}
52 |
53 |
54 | ),
55 | align: "center",
56 | },
57 | {
58 | title: 'Price',
59 | dataIndex: 'price',
60 | key: 'price',
61 | render: (_, record) => {record.property.price} MMK
62 | },
63 | {
64 | title: "Features",
65 | dataIndex: "features",
66 | key: "features",
67 | render: (_, record) => (
68 |
69 | {`${record.property.size} sq ft, ${record.property.numberOfBedrooms} bed, ${record.property.numberOfBathrooms} bath`} {" "}
70 |
71 | {`Built in ${record.property.yearBuilt}`}
72 |
73 | ),
74 | },
75 | {
76 | title: "Minimum Rental Period",
77 | dataIndex: "minrentalPeriod",
78 | key: "minrentalPeriod",
79 | render: (_, record) => (
80 |
81 | {record.property.minrentalPeriod}{" "}
82 | {record.property.minrentalPeriod > 1 ? "Months" : "Month"}{" "}
83 |
84 | ),
85 | width: 150,
86 | },
87 | {
88 | title: "Status",
89 | key: "status",
90 | dataIndex: "status",
91 | render: (_, record) => renderStatus(record.property.status),
92 | },
93 | {
94 | title: "Action",
95 | dataIndex: "action",
96 | key: "action",
97 | render: (_, record) => (
98 |
99 | Detail
100 |
101 | ),
102 | },
103 | ];
104 |
105 | const PropertyList: React.FC = () => {
106 | const [page, setPage] = useState({ pageNumber: 1, pageSize: 10 });
107 |
108 | const { data, isFetching } = useGetAllPropertiesQuery(page);
109 |
110 | const pageSetting = data?.data?.pageSetting;
111 | const properties: Properties[] = data?.data?.properties ?? [];
112 |
113 | const handlePagination = (pageNumber: number, pageSize: number) => {
114 | setPage({
115 | pageNumber,
116 | pageSize,
117 | });
118 | };
119 |
120 | return (
121 |
126 | );
127 | };
128 |
129 | export default PropertyList;
130 |
--------------------------------------------------------------------------------
/src/admin/components/transactions/TransactionList.tsx:
--------------------------------------------------------------------------------
1 | import type { TableProps } from "antd";
2 | import { Col, Row, Table, Tag } from "antd";
3 | import dayjs from "dayjs";
4 | import React, { useState } from "react";
5 | import { useGetAllTransactionsQuery } from "../../../services/admin/api/transactionsApi";
6 | import { Transactions, TransApiResponse } from "../../../type/type";
7 |
8 | const renderStatus = (status: any) => {
9 | let color;
10 |
11 | switch (status) {
12 | case "Rent":
13 | color = "orange";
14 | break;
15 | case "Sell":
16 | color = "green";
17 | break;
18 | default:
19 | break;
20 | }
21 | return {status} ;
22 | };
23 |
24 | const columns: TableProps['columns'] = [
25 | {
26 | title: 'Transaction ID',
27 | dataIndex: 'transactionId',
28 | key: 'transactionId',
29 | align: 'center',
30 | render: (_, record) => (
31 | {record?.transaction?.transactionId}
32 | )
33 | },
34 | {
35 | title: 'Client Info',
36 | dataIndex: 'clientId',
37 | key: 'clientId',
38 | align: 'center',
39 | render: (_, record) => (
40 |
41 | {`${record.client?.firstName}${record.client.lastName}`}
42 | {record.client.phone}
43 |
44 | )
45 | },
46 | {
47 | title: 'Transaction Date',
48 | dataIndex: 'transactionDate',
49 | key: 'date',
50 | render: (transactionDate: Date) => dayjs(transactionDate).format('YYYY-MM-DD HH:mm A')
51 | },
52 | {
53 | title: 'Sale Price',
54 | dataIndex: 'salePrice',
55 | key: 'sale',
56 | align: 'center',
57 | render: (_, record) => (
58 | {record.transaction.salePrice}
59 | )
60 | },
61 | {
62 | title: 'Property Price',
63 | dataIndex: 'propertyPrice',
64 | key: 'propertyPrice',
65 | align: 'center',
66 | render: (_, record) => (
67 | {record.property.price}
68 | )
69 | },
70 | {
71 | title: 'Commission',
72 | dataIndex: 'commission',
73 | key: 'commission',
74 | align: 'center',
75 | render: (_, record) => (
76 | {record.transaction.commission}
77 | )
78 | },
79 | {
80 | title: 'Status',
81 | key: 'status',
82 | dataIndex: 'status',
83 | render: (_, record) => renderStatus(record.transaction.status)
84 | }
85 | ];
86 |
87 | const TransactionList: React.FC = () => {
88 | const [page, setPage] = useState({ pageNumber: 1, pageSize: 10 });
89 |
90 | const { isFetching, data } = useGetAllTransactionsQuery(page);
91 |
92 | const pageSetting = data?.data?.pageSetting;
93 | const lstTransaction: Transactions[] = data?.data?.lstTransaction ?? [];
94 |
95 | const handlePagination = (pageNumber: number, pageSize: number) => {
96 | setPage({
97 | pageNumber,
98 | pageSize,
99 | });
100 | };
101 |
102 | return (
103 | record.transaction.transactionId}
106 | loading={isFetching}
107 | pagination={{
108 | total: pageSetting?.totalCount,
109 | current: page?.pageNumber,
110 | onChange: handlePagination
111 | }}
112 | expandable={{
113 | expandedRowRender: (record) => (
114 |
115 |
116 | Property Address
117 | {`${record.property.address}, ${record.property.city}, ${record.property.state}, ${record.property.zipCode}`}
118 |
119 |
120 | Property Features
121 | {`${record.property.size} sq ft, ${record.property.numberOfBedrooms} bed, ${record.property.numberOfBathrooms} bath, Built in ${record.property.yearBuilt}`}
122 |
123 |
124 | Minimum Rental Period
125 | {`${record.property.minrentalPeriod}`}
126 |
127 |
128 | Available Type
129 | {`${record.property.availiablityType}`}
130 |
131 |
132 | )
133 | }}
134 | />
135 | )
136 | };
137 |
138 | export default TransactionList;
139 |
--------------------------------------------------------------------------------
/src/admin/layouts/DashboardLayout.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Layout } from "antd";
3 | import DashboardSidebar from "./sidebar/DashboardSidebar";
4 | import DashboardHeader from "./header/DashboardHeader";
5 | import DashboardContext from "./context/DashboradContext";
6 |
7 | const DashboardLayout = () => {
8 | const [collapsed, setCollapsed] = useState(false);
9 |
10 | return (
11 |
12 |
16 |
22 |
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | export default DashboardLayout;
33 |
--------------------------------------------------------------------------------
/src/admin/layouts/context/DashboradContext.tsx:
--------------------------------------------------------------------------------
1 | import { Layout, theme } from "antd";
2 | import { Outlet } from "react-router-dom";
3 |
4 | const { Content } = Layout;
5 |
6 | const DashboardContext = () => {
7 | const {
8 | token: { colorBgLayout, borderRadiusLG },
9 | } = theme.useToken();
10 |
11 | return (
12 |
21 |
22 |
23 | );
24 | };
25 |
26 | export default DashboardContext;
27 |
--------------------------------------------------------------------------------
/src/admin/layouts/header/DashboardHeader.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar, Button, Dropdown, Layout, theme } from "antd";
2 | import {
3 | MenuFoldOutlined,
4 | MenuUnfoldOutlined,
5 | UserOutlined,
6 | } from "@ant-design/icons";
7 | import { collapseProp } from "../sidebar/DashboardSidebar";
8 | import type { MenuProps } from "antd";
9 | import { useAuth } from "../../../login/login-context/AuthContext";
10 |
11 | const { Header } = Layout;
12 |
13 | const DashboardHeader = ({ collapsed, setCollapsed }: collapseProp) => {
14 | const {
15 | token: { colorBgContainer },
16 | } = theme.useToken();
17 | const auth = useAuth();
18 |
19 | const items: MenuProps["items"] = [
20 | {
21 | key: "1",
22 | label: (
23 | auth.logout()} rel='noopener noreferrer'>
24 | Log out
25 |
26 | ),
27 | },
28 | ];
29 |
30 | return (
31 |
43 | : }
46 | onClick={() => setCollapsed(!collapsed)}
47 | style={{
48 | fontSize: "16px",
49 | width: 64,
50 | height: 64,
51 | }}
52 | />
53 |
54 | }
57 | style={{ cursor: "pointer", marginRight: "16px" }}
58 | />
59 |
60 |
61 | );
62 | };
63 |
64 | export default DashboardHeader;
65 |
--------------------------------------------------------------------------------
/src/admin/layouts/sidebar/DashboardSidebar.tsx:
--------------------------------------------------------------------------------
1 | import { Layout, Menu, Typography } from "antd";
2 | import {
3 | SwapOutlined,
4 | UserOutlined,
5 | UserSwitchOutlined,
6 | FundViewOutlined,
7 | CalendarOutlined,
8 | ProductOutlined,
9 | HomeOutlined
10 | } from "@ant-design/icons";
11 |
12 | import { useNavigate } from "react-router-dom";
13 | import "./style.css";
14 |
15 | const { Sider } = Layout;
16 | const { Title } = Typography;
17 |
18 | export interface collapseProp {
19 | collapsed: boolean;
20 | setCollapsed: (value: boolean) => void;
21 | }
22 |
23 | const DashboardSidebar = ({ collapsed }: collapseProp) => {
24 | const navigate = useNavigate();
25 |
26 | const handleItemClick = (key: string) => {
27 | navigate(key);
28 | };
29 |
30 | const navItems = [
31 | {
32 | key: '/admin',
33 | icon: ,
34 | label: "Dashboard"
35 | },
36 | {
37 | key: "clients",
38 | icon: ,
39 | label: "Clients",
40 | },
41 | {
42 | key: "agents",
43 | icon: ,
44 | label: "Agents",
45 | },
46 | {
47 | key: "transactions",
48 | icon: ,
49 | label: "Transactions",
50 | },
51 | {
52 | key: "appointments",
53 | icon: ,
54 | label: "Appointments",
55 | },
56 | {
57 | key: "properties",
58 | icon: ,
59 | label: "Properties",
60 | },
61 | ];
62 |
63 | return (
64 |
78 |
79 | {!collapsed && (
80 |
81 |
86 | Real Estate
87 |
88 | )}
89 |
90 | handleItemClick(key)}
94 | items={navItems}
95 | className='custom-menu'
96 | />
97 |
98 | );
99 | };
100 |
101 | export default DashboardSidebar;
102 |
--------------------------------------------------------------------------------
/src/admin/layouts/sidebar/style.css:
--------------------------------------------------------------------------------
1 | .logo-container {
2 | padding: 15px 0 15px 30px;
3 | transition: all 0.3s;
4 | height: 62px;
5 | }
6 |
7 | .logo-container.collapsed {
8 | padding: 32px 0;
9 | background-image: url("/images/logo_of_REMS.avif");
10 | background-size: cover;
11 | width: 75px;
12 | height: 62px;
13 | transition: all 0.3s;
14 | }
15 |
16 | .sidebar-title {
17 | color: #3d4252;
18 | white-space: nowrap;
19 | overflow: hidden;
20 | text-overflow: ellipsis;
21 | transition: all 0.3s;
22 | }
23 |
24 | .collapsed .sidebar-title {
25 | font-size: 0;
26 | opacity: 0;
27 | }
28 |
29 | .custom-menu {
30 | background-color: #fdfdfd;
31 | }
32 |
33 | .custom-menu .ant-menu-item:hover {
34 | background-color: #e6f7ff !important;
35 | color: #3c3cb8 !important;
36 | }
37 |
38 | .custom-menu .ant-menu-item-selected {
39 | background-color: #e6f7ff !important;
40 | color: #3c3cb8 !important;
41 | }
42 |
--------------------------------------------------------------------------------
/src/agents/agent-services/ScrollToTop.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import { useLocation } from 'react-router-dom'
3 |
4 | const ScrollToTop = () => {
5 | const {pathname} = useLocation();
6 |
7 | useEffect(() => {
8 | window.scrollTo(0, 0)
9 | }, [pathname]);
10 |
11 | return null;
12 | };
13 |
14 |
15 | export default ScrollToTop
--------------------------------------------------------------------------------
/src/agents/agent-services/propertyFilterSearch.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit";
2 |
3 | interface FilterState {
4 | cityFilter: string;
5 | bedRoomFilter: number;
6 | bathRoomFilter: number;
7 | homeType: string;
8 | minPrice: number | null;
9 | maxPrice: number | null;
10 | }
11 |
12 | const initialState: FilterState = {
13 | cityFilter: "All",
14 | bedRoomFilter: 0,
15 | bathRoomFilter: 0,
16 | homeType: "All",
17 | minPrice: 0,
18 | maxPrice: 0
19 | }
20 |
21 | const filtersSlice = createSlice({
22 | name: "filters",
23 | initialState,
24 | reducers: {
25 | setCityFilter: (state, action: PayloadAction ) => {
26 | state.cityFilter = action.payload;
27 | },
28 | setBedRoomFilter: (state, action: PayloadAction) => {
29 | state.bedRoomFilter = action.payload;
30 | },
31 | setBathRoomFilter: (state, action: PayloadAction) => {
32 | state.bathRoomFilter = action.payload;
33 | },
34 | setHomeTypeFilter: (state, action: PayloadAction) => {
35 | state.homeType = action.payload;
36 | },
37 | setMinPriceFilter: (state, action: PayloadAction) => {
38 | state.minPrice = action.payload;
39 | },
40 | setMaxPriceFilter: (state, action: PayloadAction) => {
41 | state.maxPrice = action.payload;
42 | },
43 | }
44 | })
45 |
46 | export const {
47 | setCityFilter,
48 | setBedRoomFilter,
49 | setBathRoomFilter,
50 | setHomeTypeFilter,
51 | setMinPriceFilter,
52 | setMaxPriceFilter
53 | } = filtersSlice.actions;
54 |
55 | export default filtersSlice.reducer;
--------------------------------------------------------------------------------
/src/agents/appointment/AgentAppointment.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react"
2 | import { Table, Tag, Alert } from "antd"
3 | import { LoadingOutlined } from "@ant-design/icons"
4 | import { useGetAppointmentsByAgentIdQuery } from "../../services/agent/api/appointment"
5 |
6 | interface Appointment {
7 | appointmentId: number
8 | agentName: string
9 | clientName: string
10 | appointmentDate: string
11 | appointmentTime: string
12 | agentPhoneNumber: string
13 | status: string
14 | note: string
15 | address: string
16 | city: string
17 | state: string
18 | price: number
19 | size: number
20 | numberOfBedrooms: number
21 | numberOfBathrooms: number
22 | }
23 |
24 | export default function AgentAppointment() {
25 | const AGENT_ID = 1
26 | const [page, setPage] = useState(1)
27 | const [pageSize, setPageSize] = useState(10)
28 |
29 | const { data, error, isLoading } = useGetAppointmentsByAgentIdQuery({
30 | id: AGENT_ID,
31 | pageNo: page - 1,
32 | pageSize: pageSize
33 | })
34 |
35 |
36 | const appointmentData = data?.data.appointmentDetails || [];
37 |
38 |
39 | const columns = [
40 | {
41 | title: "Client Name",
42 | dataIndex: "clientName",
43 | key: "clientName",
44 | },
45 | {
46 | title: "Appointment Date",
47 | dataIndex: "appointmentDate",
48 | key: "appointmentDate",
49 | render: (date: string) => new Date(date).toLocaleDateString(),
50 | sorter: (a: Appointment, b: Appointment) => new Date(a.appointmentDate).getTime() - new Date(b.appointmentDate).getTime(),
51 | },
52 | {
53 | title: "Appointment Time",
54 | dataIndex: "appointmentTime",
55 | key: "appointmentTime",
56 | render: (time: string) => new Date(`1970-01-01T${time}`).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
57 | sorter: (a: Appointment, b: Appointment) => new Date(`1970-01-01T${a.appointmentTime}`).getTime() - new Date(`1970-01-01T${b.appointmentTime}`).getTime(),
58 | },
59 | {
60 | title: "Status",
61 | dataIndex: "status",
62 | key: "status",
63 | render: (status: string) => getStatusTag(status),
64 | sorter : (a: Appointment, b: Appointment) => a.status.localeCompare(b.status),
65 | },
66 | {
67 | title: "Address",
68 | dataIndex: "address",
69 | key: "address",
70 | },
71 | {
72 | title: "City",
73 | dataIndex: "city",
74 | key: "city",
75 | },
76 | {
77 | title: "State",
78 | dataIndex: "state",
79 | key: "state",
80 | },
81 | {
82 | title: "Price",
83 | dataIndex: "price",
84 | key: "price",
85 | render: (price: number) => `$${price.toLocaleString()}`,
86 | },
87 | {
88 | title: "Size",
89 | dataIndex: "size",
90 | key: "size",
91 | render: (size: number) => `${size} sq ft`,
92 | },
93 | {
94 | title: "Bedrooms",
95 | dataIndex: "numberOfBedrooms",
96 | key: "numberOfBedrooms",
97 | },
98 | {
99 | title: "Bathrooms",
100 | dataIndex: "numberOfBathrooms",
101 | key: "numberOfBathrooms",
102 | },
103 |
104 | ]
105 |
106 | const getStatusTag = (status: string) => {
107 | const statusColors: { [key: string]: string } = {
108 | Approved: "green",
109 | pending: "gold",
110 | FDS: "red",
111 | }
112 | return (
113 |
114 | {status}
115 |
116 | )
117 | }
118 |
119 | if (isLoading) return
120 | if (error) {
121 | console.error("API Error:", error)
122 | return
123 | }
124 |
125 | return (
126 |
127 |
128 |
Agent Appointments
129 |
{
138 | setPage(page)
139 | setPageSize(pageSize ?? 10)
140 | }
141 | }}
142 | className="w-full"
143 | />
144 |
145 | )
146 | }
--------------------------------------------------------------------------------
/src/agents/defalult-page/AgentDefaultPage.tsx:
--------------------------------------------------------------------------------
1 | import AgentFooter from "./AgentFooter";
2 | import NavCards from "./NavCards";
3 | import RecentPosts from "./RecentPosts";
4 | import VinylSearch from "./VinylSearch";
5 | import YangonRecommended from "./YangonRecommended";
6 |
7 | const AgentDefaultPage: React.FC = () => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default AgentDefaultPage;
20 |
--------------------------------------------------------------------------------
/src/agents/defalult-page/AgentFooter.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const AgentFooter: React.FC = () => {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
Company
11 |
12 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque a arcu sit amet eros vehicula fermentum.
13 |
14 |
15 |
16 |
25 |
26 |
34 |
35 |
43 |
44 |
45 |
© 2024 REMS Ko Sann Lynn Htun. All rights reserved.
46 |
47 |
48 |
49 | )
50 | }
51 |
52 | export default AgentFooter
--------------------------------------------------------------------------------
/src/agents/defalult-page/NavCards.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { FaHouseChimney } from "react-icons/fa6";
3 | import { HiArrowLongRight } from "react-icons/hi2";
4 | import { CiTextAlignLeft } from "react-icons/ci";
5 | import { IoIosCreate } from "react-icons/io";
6 | import { useNavigate } from 'react-router';
7 |
8 | const NavCards:React.FC = () => {
9 |
10 | const navigate = useNavigate();
11 |
12 | return (
13 |
14 |
17 |
18 |
19 |
20 |
21 | If you want to explore for some Market Values?
22 |
23 |
24 | Click below
25 |
26 |
navigate("/agent/property-list")}
29 | >
30 |
31 |
32 |
33 |
34 |
37 |
38 |
39 |
40 |
41 | Manage your appointments with your clients?
42 |
43 |
44 | Click below
45 |
46 |
navigate("/agent/agent-appointments")}
49 | >
50 |
51 |
52 |
53 |
54 |
57 |
58 |
59 |
60 |
61 | Want to post or create the property ?
62 |
63 |
64 | Click below
65 |
66 |
navigate("/agent/property-create")}
69 | >
70 |
71 |
72 |
73 |
74 |
75 | )
76 | }
77 |
78 | export default NavCards
--------------------------------------------------------------------------------
/src/agents/defalult-page/RecentPosts.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useGetAgentByUserIdQuery } from "../../services/agent/api/getAgentApiSlice";
3 | import { useAuth } from "../../login/login-context/AuthContext";
4 | import { useGetPropertiesQuery } from "../../services/agent/api/propertyApiSlice";
5 | import { SwiperSlide, Swiper } from "swiper/react";
6 | import PropertyCard from "./components/PropertyCard";
7 | import { BsFillFileEarmarkPostFill } from "react-icons/bs";
8 | import { Pagination, Navigation } from "swiper/modules";
9 |
10 | import "swiper/css";
11 | import "swiper/css/pagination";
12 | import "swiper/css/navigation";
13 | import { LoadingOutlined } from "@ant-design/icons";
14 | import { Empty } from "antd";
15 |
16 | const RecentPosts: React.FC = () => {
17 | const { user } = useAuth();
18 | const userId = user?.UserId;
19 |
20 | const { data, isLoading, error } = useGetAgentByUserIdQuery(userId);
21 |
22 | const agent = data?.data;
23 | const agentId = agent?.agentId;
24 |
25 | const {
26 | data: propertyData,
27 | isLoading: isPropertyLoading,
28 | error: isPropertyError,
29 | } = useGetPropertiesQuery({
30 | page: 1,
31 | limit: 10,
32 | city: "",
33 | agentId: agentId,
34 | });
35 |
36 | const recentPosts = propertyData?.data.properties;
37 |
38 | if (isPropertyError) return Error...
39 |
40 | return (
41 |
42 |
43 |
44 | Your Recent Posts
45 |
46 |
47 |
48 |
49 |
50 |
51 |
84 | {isPropertyLoading ? (
85 |
86 |
87 |
88 | ) : recentPosts?.length === 0 ? (
89 |
95 | ) : (
96 | recentPosts?.slice(0, 5).map((property, index) => (
97 |
98 |
99 |
100 | ))
101 | )}
102 |
103 |
104 | );
105 | };
106 |
107 | export default RecentPosts;
108 |
--------------------------------------------------------------------------------
/src/agents/defalult-page/VinylSearch.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { FaSearch } from "react-icons/fa";
3 | import { cityData } from "../property-crud/db";
4 | import { setCityFilter } from "../agent-services/propertyFilterSearch";
5 | import { useAppDispatch } from "../../app/hook";
6 | import { useNavigate } from "react-router";
7 | import { toast } from "sonner";
8 |
9 | const VinylSearch: React.FC = () => {
10 | const navigate = useNavigate();
11 |
12 | const city = cityData.map(city => city.TownshipName);
13 |
14 | const [searchTerm, setSearchTerm] = useState("");
15 | const [options, setOptions] = useState([...city]);
16 | const [filteredOptions, setFilteredOptions] = useState([]);
17 |
18 | const dispatch = useAppDispatch();
19 |
20 | const handleSearch = () => {
21 | if (!searchTerm) {
22 | toast.error("Please type something in the search bar");
23 | return;
24 | }
25 | dispatch(setCityFilter(searchTerm));
26 | navigate("/agent/property-list");
27 | };
28 |
29 | // function for when typing the search filtered the dropdown
30 | const handleInputChange = (e: any) => {
31 | const value = e.target.value;
32 | setSearchTerm(value);
33 |
34 | if (value) {
35 | const filtered = options.filter(option =>
36 | option.toLowerCase().includes(value.toLowerCase())
37 | );
38 | setFilteredOptions(filtered);
39 | } else {
40 | setFilteredOptions([]);
41 | }
42 | };
43 |
44 | const handleOptionClick = (option: string) => {
45 | setSearchTerm(option);
46 | setFilteredOptions([]); // Clear options after selection
47 | };
48 |
49 | return (
50 |
53 |
54 |
55 | Agents.Tours.
56 | Sells.Homes
57 |
58 |
59 |
65 |
69 |
70 |
71 | {filteredOptions.length > 0 && (
72 |
73 | {filteredOptions.map((option, index) => (
74 | handleOptionClick(option)}
77 | className="cursor-pointer text-[1rem] p-2 hover:bg-blue-500 hover:text-white"
78 | >
79 | {option}
80 |
81 | ))}
82 |
83 | )}
84 |
85 |
86 |
87 | );
88 | };
89 |
90 | export default VinylSearch;
--------------------------------------------------------------------------------
/src/agents/defalult-page/YangonRecommended.tsx:
--------------------------------------------------------------------------------
1 | import PropertyCard from "./components/PropertyCard";
2 | // Import Swiper React components
3 | import { Swiper, SwiperSlide } from "swiper/react";
4 |
5 | // Import Swiper styles
6 | import "swiper/css";
7 | import "swiper/css/pagination";
8 | import "swiper/css/navigation";
9 | import { Pagination, Navigation } from "swiper/modules";
10 | import React from "react";
11 | import { useGetPropertiesQuery } from "../../services/agent/api/propertyApiSlice";
12 | import { FaHouseCircleCheck } from "react-icons/fa6";
13 | import { useAppDispatch } from "../../app/hook";
14 | import { setCityFilter } from "../agent-services/propertyFilterSearch";
15 | import { useNavigate } from "react-router";
16 | import { LoadingOutlined } from "@ant-design/icons";
17 | import { Empty } from "antd";
18 |
19 | const YangonRecommended: React.FC = () => {
20 | const { data, error, isLoading } = useGetPropertiesQuery({
21 | page: 1,
22 | limit: 100,
23 | city: "yangon",
24 | });
25 |
26 | const navigate = useNavigate();
27 |
28 | const properties = data?.data.properties;
29 | const dispatch = useAppDispatch();
30 | const handleClick = () => {
31 | dispatch(setCityFilter("yangon"));
32 | navigate("/agent/property-list");
33 | };
34 |
35 | if (error) return Error
;
36 |
37 | return (
38 |
39 |
40 |
44 | Homes In Yangon
45 |
46 |
50 |
51 |
52 |
53 |
54 |
87 | {isLoading ? (
88 |
89 |
90 |
91 | ) : properties?.length === 0 ? (
92 |
98 | ) : (
99 | properties?.map((property, index) => (
100 |
101 |
102 |
103 | ))
104 | )}
105 |
106 |
107 | );
108 | };
109 |
110 | export default YangonRecommended;
111 |
--------------------------------------------------------------------------------
/src/agents/defalult-page/components/PropertyCard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { PropertyDataType } from '../../property-list/data-for-agent/propertyData';
3 | import { useAppDispatch } from '../../../app/hook';
4 | import { setCityFilter } from '../../agent-services/propertyFilterSearch';
5 | import { useNavigate } from 'react-router';
6 | import { formatNumber } from '../../property-list/AgentPropertyList';
7 |
8 | interface PropertyCardProp {
9 | property: PropertyDataType;
10 | nav: string
11 | }
12 |
13 | const PropertyCard: React.FC = ({ property , nav}) => {
14 | const navigate = useNavigate();
15 | const dispatch = useAppDispatch();
16 |
17 | const handleClick = () => {
18 | dispatch(setCityFilter(property?.property.city));
19 | navigate(`/agent/${nav}`);
20 | };
21 |
22 | const images = property?.images;
23 | const imageURL = images[0]?.imgBase64;
24 |
25 |
26 |
27 | return (
28 |
29 |
30 |
38 |
39 |
40 | ${formatNumber(property?.property.price)} MMK
41 |
42 |
43 | {property?.property.city} | {property?.property.state}
44 |
45 |
46 |
47 |
48 |
49 |
50 | Bas: {property?.property.numberOfBathrooms}
51 |
52 |
53 | Bds: {property?.property.numberOfBedrooms}
54 |
55 |
56 |
57 |
58 | );
59 | };
60 |
61 | export default PropertyCard;
--------------------------------------------------------------------------------
/src/agents/property-crud/PropertyCard.tsx:
--------------------------------------------------------------------------------
1 | import React, { MouseEvent } from "react";
2 | import "./styles.css";
3 | import { Card, Carousel } from "antd";
4 | import { DeleteFilled, EditFilled } from "@ant-design/icons";
5 |
6 | interface Image {
7 | imgBase64: string;
8 | description: string;
9 | }
10 |
11 | export interface ItemProps {
12 | agentId: number;
13 | address: string;
14 | city: string;
15 | state: string;
16 | zipCode: string;
17 | propertyType: string;
18 | price: string;
19 | size: number;
20 | numberOfBedrooms: number;
21 | numberOfBathrooms: number;
22 | yearBuilt: number;
23 | description: string;
24 | availiablityType: string;
25 | minRentalPeriod: number;
26 | images: Image[];
27 | }
28 |
29 | interface PropertyCardProps {
30 | item: ItemProps;
31 | onEdit: (event: MouseEvent) => void;
32 | onDelete: (event: MouseEvent) => void;
33 | }
34 |
35 | const PropertyCard: React.FC = ({
36 | item,
37 | onEdit,
38 | onDelete,
39 | }) => {
40 | return (
41 |
42 |
47 | {item.images.map((img) => (
48 |
54 | ))}
55 |
56 | }
57 | actions={[
58 | ,
59 | ,
60 | ]}
61 | >
62 |
63 |
64 |
65 | Price: {item.price.toLocaleString()} MMK,
66 |
67 |
Size: {item.size},
68 |
69 | Bathrooms: {item.numberOfBathrooms},
70 |
71 |
72 | Bedrooms: {item.numberOfBedrooms},
73 |
74 |
75 |
76 |
77 | Address: {item.address},
78 |
79 |
80 | {item.city}, {item.zipCode},
81 |
82 |
{item.description}
83 |
84 |
85 |
86 |
87 | );
88 | };
89 |
90 | export default PropertyCard;
--------------------------------------------------------------------------------
/src/agents/property-crud/PropertyDrawer.tsx:
--------------------------------------------------------------------------------
1 | import { Drawer } from "antd";
2 | import "./styles.css";
3 | import PropertiesForm from "./PropertiesForm";
4 | import { ItemProps } from "./PropertyCard";
5 |
6 | interface IProps {
7 | onClose: () => void;
8 | open: boolean;
9 | data: ItemProps | null;
10 | }
11 |
12 | const PropertyDrawer = ({ onClose, open, data }: IProps) => {
13 | return (
14 | <>
15 |
26 |
27 |
28 | >
29 | );
30 | };
31 |
32 | export default PropertyDrawer;
--------------------------------------------------------------------------------
/src/agents/property-crud/styles.css:
--------------------------------------------------------------------------------
1 | /* .custom-card {
2 | border: 1px solid #ddd;
3 | border-radius: 8px;
4 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
5 | margin: 16px;
6 | width: 400px;
7 | }
8 |
9 | .custom-card__content {
10 | padding: 16px;
11 | }
12 |
13 | .custom-card__title {
14 | font-size: 1.25rem;
15 | margin: 0 0 8px 0;
16 | }
17 |
18 | .custom-card__description {
19 | font-size: 1rem;
20 | color: #555;
21 | }
22 |
23 | .custom-card__actions {
24 | display: flex;
25 | justify-content: space-between;
26 | margin-top: 16px;
27 | }
28 |
29 | .custom-card__button {
30 | background-color: #1890ff;
31 | color: white;
32 | border: none;
33 | border-radius: 4px;
34 | padding: 8px 12px;
35 | cursor: pointer;
36 | font-size: 0.875rem;
37 | }
38 |
39 | .custom-card__button:hover {
40 | background-color: #40a9ff;
41 | }
42 |
43 | .carousel {
44 | position: relative;
45 | width: 100%;
46 | max-width: 400px;
47 | height: 200px;
48 | overflow: hidden;
49 | margin: auto;
50 | }
51 |
52 | .carousel__image {
53 | width: 100%;
54 | height: 100%;
55 | object-fit: cover;
56 | }
57 |
58 | .carousel__button {
59 | position: absolute;
60 | top: 50%;
61 | transform: translateY(-50%);
62 | background-color: rgba(0, 0, 0, 0.5);
63 | color: white;
64 | border: none;
65 | padding: 8px 16px;
66 | cursor: pointer;
67 | z-index: 1;
68 | }
69 |
70 | .carousel__button--prev {
71 | left: 0;
72 | }
73 |
74 | .carousel__button--next {
75 | right: 0;
76 | }
77 |
78 | .properties-container {
79 | display: grid;
80 | grid-column: auto auto auto auto;
81 | padding: 10px;
82 | }
83 |
84 | .properties-button {
85 | background-color: #1890ff;
86 | color: white;
87 | border: none;
88 | border-radius: 4px;
89 | padding: 8px 12px;
90 | cursor: pointer;
91 | font-size: 0.875rem;
92 | margin-bottom: 16px;
93 | }
94 |
95 | .properties-button:hover {
96 | background-color: #40a9ff;
97 | }
98 |
99 | .properties-grid {
100 | display: grid;
101 | grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
102 | gap: 16px;
103 | }
104 |
105 | @media (min-width: 1024px) {
106 | .properties-grid {
107 | grid-template-columns: repeat(3, 1fr);
108 | }
109 | }
110 |
111 | .property-card {
112 | display: flex;
113 | flex-direction: column;
114 | justify-content: space-between;
115 | width: 100%;
116 | margin: 0.5rem;
117 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
118 | transition: box-shadow 0.3s ease-in-out;
119 | border-radius: 0.375rem;
120 | cursor: pointer;
121 | }
122 |
123 | .property-card:hover {
124 | box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
125 | }
126 |
127 | .property-info {
128 | display: flex;
129 | flex-direction: column;
130 | gap: 0.5rem;
131 | }
132 |
133 | .property-details,
134 | .property-address,
135 | .property-description {
136 | display: flex;
137 | flex-direction: column;
138 | gap: 0.25rem;
139 | }
140 |
141 | .property-info span,
142 | .property-info h1 {
143 | font-size: 1.125rem;
144 | font-weight: bold;
145 | word-break: break-word;
146 | }
147 |
148 | @media (min-width: 768px) {
149 | .property-card {
150 | width: calc(33.333% - 1rem);
151 | }
152 | }
153 |
154 | @media (min-width: 1024px) {
155 | .property-card {
156 | width: calc(25% - 1rem);
157 | }
158 | } */
--------------------------------------------------------------------------------
/src/agents/property-list/AgentPropertyList.tsx:
--------------------------------------------------------------------------------
1 | import { Empty, Pagination } from 'antd';
2 | import { useEffect, useMemo, useState } from 'react';
3 | import { useAppSelector } from '../../app/hook';
4 | import SingleCard from './SingleCard';
5 | import Flyout from './Flyout';
6 | import BaAndBdsFilter from './Filters/BaAndBdsFilter';
7 | import PriceRangeFilter from './Filters/PriceRangeFilter';
8 | import HomeTypeFilter from './Filters/HomeTypeFilter';
9 | import DetailPage from './DetailPage';
10 | import CityFilter from './Filters/CityFilter';
11 | import { useGetPropertiesQuery } from '../../services/agent/api/propertyApiSlice';
12 | import { LoadingOutlined } from '@ant-design/icons';
13 |
14 | const itemsPerPage = 6;
15 |
16 | export const formatNumber = (num: number) => {
17 | if (num >= 1_000_000_000) return Math.floor(num / 1_000_000_000) + 'B';
18 | if (num >= 1_000_000) return Math.floor(num / 1_000_000) + 'M';
19 | return num;
20 | };
21 |
22 | const AgentPropertyList: React.FC = () => {
23 | const [currentPage, setCurrentPage] = useState(1);
24 | const [modal, setModal] = useState(false);
25 |
26 | const toggleModal = () => setModal(!modal);
27 |
28 | const { data, error, isLoading } = useGetPropertiesQuery({
29 | page: 1,
30 | limit: 100,
31 | city: ""
32 | }) ;
33 |
34 | if (modal) {
35 | document.body.classList.add('active-modal');
36 | } else {
37 | document.body.classList.remove('active-modal');
38 | }
39 |
40 | const { cityFilter, bedRoomFilter, bathRoomFilter, minPrice, maxPrice, homeType } = useAppSelector(
41 | (state) => state.agentPropertyFilters
42 | );
43 |
44 |
45 | const filteredData = useMemo(() => {
46 | if (!data?.data.properties) return [];
47 | return data?.data.properties?.filter((item) => {
48 | return (
49 | (cityFilter === "All" || item.property.city.toLowerCase().includes(cityFilter.toLowerCase())) &&
50 | (bedRoomFilter === 0 || item.property.numberOfBedrooms >= bedRoomFilter) &&
51 | (bathRoomFilter === 0 || item.property.numberOfBathrooms >= bathRoomFilter) &&
52 | (
53 | (minPrice === 0 || item.property.price >= minPrice)
54 | &&
55 | (maxPrice === 0 || item.property.price <= maxPrice)
56 | ) &&
57 | (homeType === "All" || item.property.propertyType.toLowerCase().includes(homeType.toLowerCase()))
58 | );
59 | });
60 | }, [cityFilter, bedRoomFilter, bathRoomFilter, minPrice, maxPrice, homeType, data]);
61 |
62 | const handleChangePage = (page: number) => setCurrentPage(page);
63 |
64 | useEffect(() => {
65 | const totalPages = Math.ceil(filteredData?.length / itemsPerPage);
66 | if (currentPage > totalPages) {
67 | setCurrentPage(totalPages || 1);
68 | }
69 | }, [filteredData, currentPage, itemsPerPage]);
70 |
71 | useEffect(() => {
72 | window.scrollTo(0, 0);
73 | }, [currentPage]);
74 |
75 | const currentItems = filteredData?.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
76 |
77 | return (
78 |
79 |
Explore Some Market Values
80 |
81 |
82 | {cityFilter === 'All' ? "Select City" : cityFilter}
83 |
84 |
85 |
86 | Bas: {bathRoomFilter === 0 ? "Any" : bathRoomFilter + "+"}, Bds: {bedRoomFilter === 0 ? "Any" : bedRoomFilter + "+"}
87 |
88 |
89 |
90 |
91 | {(minPrice !== 0 && maxPrice !== 0) ? `Min: ${formatNumber(minPrice)}, Max: ${formatNumber(maxPrice)}` : "Price Range"}
92 |
93 |
94 |
95 | {homeType === "All" ? "Home Type" : homeType}
96 |
97 |
98 |
99 |
100 | {isLoading ? (
101 |
102 |
103 |
104 | ) : error ? (
105 |
Error
106 | ) : filteredData?.length === 0 ? (
107 |
112 | ) : (
113 | currentItems?.map((item, index) =>
114 |
)
118 | )}
119 |
120 | {modal &&
}
121 |
122 |
123 |
124 |
125 | );
126 | };
127 |
128 | export default AgentPropertyList;
--------------------------------------------------------------------------------
/src/agents/property-list/DetailPage.tsx:
--------------------------------------------------------------------------------
1 | import { CloseOutlined } from "@ant-design/icons"
2 | import { useParams } from "react-router"
3 | // import { propertyData } from "./data-for-agent/propertyData"
4 | // import { reviewsData } from "./data-for-agent/reviewsData"
5 | import PropertyDetails from "./PropertyDetails"
6 | import Reviews from "./Reviews"
7 | import { useState } from "react"
8 | import { useGetPropertiesQuery } from "../../services/agent/api/propertyApiSlice"
9 |
10 | interface DetailPgProp {
11 | toggle?: () => void
12 | }
13 |
14 | const DetailPage: React.FC= ({toggle}) => {
15 |
16 | const [currentPage, setCurrentPage] = useState('propertyDetails');
17 | const {id} = useParams();
18 |
19 | const { data, error, isLoading } = useGetPropertiesQuery({ page: 1, limit: 100}) ;
20 |
21 | const propertyData = data?.data.properties;
22 |
23 | const property = propertyData?.find(property => property.property.propertyId.toString() === id);
24 |
25 | const review = property?.reviews
26 |
27 |
28 | const renderPage = () => {
29 | switch (currentPage) {
30 | case 'propertyDetails':
31 | return ;
32 | case 'reviews':
33 | return ;
34 | default:
35 | return ;
36 | }
37 | };
38 |
39 | if (isLoading) return Loading...
;
40 | if (error) return Error...
41 |
42 | return (
43 |
44 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | setCurrentPage('propertyDetails')}
61 | >
62 | Property
63 |
64 |
65 | |
66 |
67 | setCurrentPage('reviews')}
73 | >
74 | Reviews
75 |
76 |
77 |
78 |
79 | {renderPage()}
80 |
81 | {/* ----------------- */}
82 |
83 | {/* close button */}
84 |
87 |
88 |
89 |
90 |
91 | )
92 | }
93 |
94 | export default DetailPage
--------------------------------------------------------------------------------
/src/agents/property-list/Dropdowns/FilterDropdown.tsx:
--------------------------------------------------------------------------------
1 | import { Select } from "antd";
2 | import { useAppDispatch } from "../../../app/hook"
3 |
4 | interface FilterDropdownProps {
5 | item: string;
6 | options: (string | number)[];
7 | selectedOptions: string | number;
8 | onFilterChange: (filter: any) => void;
9 | }
10 |
11 | const FilterDropdown: React.FC = ({
12 | item,
13 | options,
14 | selectedOptions,
15 | onFilterChange
16 | }) => {
17 |
18 | const dispatch = useAppDispatch();
19 |
20 | const handleChange = (selectedOptions: string | number) => {
21 | dispatch(onFilterChange(selectedOptions));
22 | };
23 |
24 | return (
25 |
26 |
Select {item}
27 |
(
33 | {
34 | label: `${option === 0 ? "Any" : option + "+"}`,
35 | value: option,
36 | }
37 | ))}
38 | />
39 |
40 | )
41 | }
42 |
43 | export default FilterDropdown
--------------------------------------------------------------------------------
/src/agents/property-list/Dropdowns/PriceDropDown.tsx:
--------------------------------------------------------------------------------
1 | import { Select } from "antd";
2 | import { useAppDispatch, useAppSelector } from "../../../app/hook"
3 |
4 | interface FilterDropdownProps {
5 | item: string;
6 | options: {value: number ; label: string}[] ;
7 | selectedOptions: number;
8 | onFilterChange: (filter: any) => void;
9 | }
10 |
11 | const PriceDropDown: React.FC = ({
12 | item,
13 | options,
14 | selectedOptions,
15 | onFilterChange
16 | }) => {
17 |
18 | const dispatch = useAppDispatch();
19 | const {minPrice, maxPrice} = useAppSelector(state => state.agentPropertyFilters)
20 |
21 | const handlePriceChange = (value: number) => {
22 | if (value === minPrice) {
23 | dispatch(onFilterChange(value));
24 |
25 | if (value > maxPrice) {
26 | dispatch(onFilterChange(0))
27 | }
28 | }
29 |
30 | dispatch(onFilterChange(value));
31 | };
32 |
33 | return (
34 |
35 |
Select {item}
36 |
(
44 | {
45 | label: `${option.label === "Any" ? "Any" : option.label }`,
46 | value: option.value,
47 | }
48 | )).filter(option => selectedOptions === maxPrice
49 | ?
50 | (option.value !== 0 ? option.value >= minPrice : option)
51 | :
52 | option)}
53 | />
54 |
55 | )
56 | }
57 |
58 | export default PriceDropDown;
--------------------------------------------------------------------------------
/src/agents/property-list/Dropdowns/SimpleFilterDropDown.tsx:
--------------------------------------------------------------------------------
1 | import { Select } from "antd";
2 | import { useAppDispatch } from "../../../app/hook"
3 |
4 | interface FilterDropdownProps {
5 | item: string;
6 | options: (string | number)[] ;
7 | selectedOptions: string | number;
8 | onFilterChange: (filter: any) => void;
9 | }
10 |
11 | const SimpleFilterDropDown: React.FC = ({
12 | item,
13 | options,
14 | selectedOptions,
15 | onFilterChange,
16 | }) => {
17 |
18 | const dispatch = useAppDispatch();
19 |
20 | const handleChange = (selectedOptions: string | number) => {
21 | dispatch(onFilterChange(selectedOptions));
22 | };
23 |
24 | return (
25 | <>
26 | (
33 | {
34 | label: option,
35 | value: option,
36 | }
37 | ))}
38 | />
39 | >
40 | )
41 | }
42 |
43 | export default SimpleFilterDropDown;
--------------------------------------------------------------------------------
/src/agents/property-list/Filters/BaAndBdsFilter.tsx:
--------------------------------------------------------------------------------
1 | import FilterDropdown from '../Dropdowns/FilterDropdown';
2 | import { useAppSelector } from '../../../app/hook';
3 | import {
4 | setBathRoomFilter,
5 | setBedRoomFilter
6 | } from '../../agent-services/propertyFilterSearch';
7 |
8 | const BaAndBdsFilter = () => {
9 | const {bedRoomFilter, bathRoomFilter} = useAppSelector(state => state.agentPropertyFilters);
10 |
11 | const bedRoomsOptions = [0,1,2,3,4,5,6,7];
12 | const bathRoomsOptions = [0,1,2,3,4,5,6,7];
13 | return (
14 |
15 |
16 |
22 |
23 |
29 |
30 |
Select some values for bed rooms and bathrooms, you will get the exact value or above.
31 |
32 | )
33 | }
34 |
35 | export default BaAndBdsFilter
--------------------------------------------------------------------------------
/src/agents/property-list/Filters/CityFilter.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import SimpleFilterDropDown from '../Dropdowns/SimpleFilterDropDown'
3 | import { useAppSelector } from '../../../app/hook';
4 | import { cityData } from '../../property-crud/db';
5 | import { setCityFilter } from '../../agent-services/propertyFilterSearch';
6 |
7 | const CityFilter:React.FC = () => {
8 |
9 | const city = cityData.map(city => city.TownshipName)
10 |
11 | const cityOptions = ["All", ...city];
12 |
13 | const {cityFilter} = useAppSelector(state => state.agentPropertyFilters)
14 |
15 | return (
16 |
17 |
Select Place
18 |
24 |
25 | )
26 | }
27 |
28 | export default CityFilter
29 |
--------------------------------------------------------------------------------
/src/agents/property-list/Filters/HomeTypeFilter.tsx:
--------------------------------------------------------------------------------
1 | import { useAppSelector } from "../../../app/hook"
2 | import { setHomeTypeFilter } from "../../agent-services/propertyFilterSearch"
3 | import SimpleFilterDropDown from "../Dropdowns/SimpleFilterDropDown";
4 |
5 | const HomeTypeFilter: React.FC = () => {
6 |
7 | const homeTypeOptions = ["All", "Houses", "Townhomes", "Multi-family", "Condominium", "Lands", "Apartments", "Manufactured", "Single Family Home"]
8 |
9 | const {homeType} = useAppSelector(state => state.agentPropertyFilters);
10 |
11 | return (
12 |
13 |
19 |
Select Property Type
20 |
21 | )
22 | }
23 |
24 | export default HomeTypeFilter
--------------------------------------------------------------------------------
/src/agents/property-list/Filters/PriceRangeFilter.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | setMinPriceFilter,
3 | setMaxPriceFilter
4 | } from '../../agent-services/propertyFilterSearch'
5 | import { useAppSelector } from '../../../app/hook'
6 | import PriceDropDown from '../Dropdowns/PriceDropDown'
7 |
8 | const PriceRangeFilter: React.FC = () => {
9 |
10 | const {minPrice, maxPrice} = useAppSelector(state => state.agentPropertyFilters)
11 |
12 | const priceOptions = [
13 | {value: 0, label: "Any"},
14 | {value: 90000000, label: "90M"},
15 | {value: 100000000, label: "100M"},
16 | {value: 200000000, label: "200M"},
17 | {value: 300000000, label: "300M"},
18 | {value: 400000000, label: "400M"},
19 | {value: 500000000, label: "500M"},
20 | {value: 600000000, label: "600M"},
21 | {value: 700000000, label: "700M"},
22 | {value: 800000000, label: "800M"},
23 | {value: 900000000, label: "900M"},
24 | {value: 1000000000, label: "1B"},
25 | {value: 2000000000, label: "2B"},
26 | {value: 3000000000, label: "3B"},
27 | {value: 4000000000, label: "4B"},
28 | {value: 5000000000, label: "5B"},
29 | {value: 6000000000, label: "6B"},
30 | {value: 7000000000, label: "7B"},
31 | {value: 8000000000, label: "8B"},
32 | {value: 9000000000, label: "9B"},
33 | {value: 10000000000, label: "10B"},
34 | {value: 11000000000, label: "11B"},
35 | {value: 12000000000, label: "12B"},
36 | {value: 13000000000, label: "13B"},
37 | {value: 14000000000, label: "14B"},
38 | {value: 15000000000, label: "15B"},
39 | ]
40 | return (
41 |
42 |
56 |
Please, select some values of minimum price and maximum price
57 |
58 | )
59 | }
60 |
61 | export default PriceRangeFilter
--------------------------------------------------------------------------------
/src/agents/property-list/Flyout.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import {AnimatePresence, motion} from "framer-motion"
3 | import { DownOutlined, UpOutlined } from "@ant-design/icons";
4 |
5 | interface FlyoutProps {
6 | children: React.ReactNode;
7 | FlyoutContent: React.FC;
8 | }
9 |
10 | const Flyout: React.FC = ({children, FlyoutContent}) => {
11 |
12 | const [open, setOpen] = useState(false);
13 | const showFlyout = open && FlyoutContent;
14 |
15 | return (
16 | setOpen(true)}
18 | onMouseLeave={() => setOpen(false)}
19 | className="relative h-fit w-fit "
20 | >
21 |
22 | {children}
23 | setOpen(!open)}
26 | >
27 | {open ? : }
28 |
29 |
30 |
31 |
32 | {showFlyout && (
33 |
41 |
42 |
43 |
44 | ) }
45 |
46 |
47 | )
48 | }
49 |
50 | export default Flyout;
--------------------------------------------------------------------------------
/src/agents/property-list/PropertyDetails.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Property, PropertyDataType } from './data-for-agent/propertyData'
3 | import { Carousel } from 'antd';
4 |
5 | interface PropertyDetailsProp {
6 | property: PropertyDataType | undefined;
7 | }
8 |
9 | const PropertyDetails: React.FC = ({property}) => {
10 | return (
11 |
12 |
13 |
14 | {property?.images.map(img => (
15 |
16 |
17 | {img.description}
18 |
19 | ))}
20 |
21 |
22 |
23 |
24 |
25 |
${property?.property.price.toLocaleString()} MMK
26 |
{property?.property.address}
27 |
28 |
29 |
{property?.property.numberOfBedrooms}-Beds
30 |
|
31 |
{property?.property.numberOfBathrooms}-Bathrooms
32 |
33 |
34 |
35 |
{property?.property.propertyType}
36 |
{property?.property.size} sqft
37 |
Built: {property?.property.yearBuilt}
38 |
For: {property?.property.availiablityType}
39 |
40 |
{property?.property.description}
41 |
42 |
43 | )
44 | }
45 |
46 | export default PropertyDetails
--------------------------------------------------------------------------------
/src/agents/property-list/SingleCard.tsx:
--------------------------------------------------------------------------------
1 | import { Card, Carousel } from "antd";
2 | import { Property, PropertyDataType } from "./data-for-agent/propertyData";
3 | import { formatNumber } from "./AgentPropertyList";
4 | import { Link } from "react-router-dom";
5 |
6 |
7 | interface SingleCardProp {
8 | item: PropertyDataType;
9 | key: number;
10 | toggle?: () => void;
11 | }
12 |
13 | const SingleCard: React.FC = ({item, key, toggle}) => {
14 |
15 | const imgs = item.images.map(img => (
16 |
17 | ))
18 |
19 | return (
20 |
21 |
25 | {imgs}
26 |
27 | }
28 |
29 | key={key}
30 | >
31 |
32 |
33 | ${formatNumber(item.property.price).toLocaleString()} MMK
34 |
35 |
36 |
37 |
Ba: {item.property.numberOfBathrooms} |
38 |
Bds: {item.property.numberOfBedrooms} |
39 |
{item.property.size} sqft -
40 |
{item.property.propertyType} for {item.property.availiablityType}
41 |
42 |
{item.property.address}
43 |
44 |
{item.property.city} |
45 |
{item.property.state} state
46 |
47 |
48 |
49 |
50 |
51 | )
52 | }
53 |
54 | export default SingleCard;
--------------------------------------------------------------------------------
/src/app/hook.ts:
--------------------------------------------------------------------------------
1 | import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
2 | import { AppDispatch, RootState } from "./store";
3 |
4 | import { fetchBaseQuery } from "@reduxjs/toolkit/query/react";
5 | const url = "http://65.18.112.78:44010/rems/api/v1/";
6 |
7 | const baseUrl = fetchBaseQuery({
8 | baseUrl: url,
9 |
10 |
11 | // when backend added auth we set the bearer token in below
12 | prepareHeaders: async (headers) => {
13 | const accessToken = localStorage.getItem('token');//get token from local storage or else
14 | if (accessToken) {
15 | headers.set("Authorization", `Bearer ${accessToken}`);
16 | headers.set("Cache-Control", "no-cache");
17 | }
18 | return headers;
19 | },
20 |
21 | // prepareHeaders: async (headers) => {
22 | // headers.set(
23 | // "Authorization",
24 | // `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJkNDQ1YTk5Ni03NTE4LTQ3NWEtOGE5MS1jMTU0OWM2Mzg0NzIiLCJhdWQiOiJSRU1TIiwiaXNzIjoiUkVNUyIsInJvbGUiOiJBZ2VudCIsInVuaXF1ZV9uYW1lIjoiaGVpbmh0ZXQiLCJUb2tlbkV4cGlyZWQiOiIyMDI0LTA4LTExVDA5OjQ3OjI1LjU5NTQ3MzVaIiwibmJmIjoxNzIzMjgzMjQ1LCJleHAiOjE3MjMzNjk2NDUsImlhdCI6MTcyMzI4MzI0NX0.9-GtNe7qh7LfkqWAYMPmtCo2lq41ocqKWcc4YeF_nho`
25 | // );
26 | // },
27 | });
28 |
29 | // Hooks for global state
30 | export const useAppSelector: TypedUseSelectorHook = useSelector;
31 | export const useAppDispatch = () => useDispatch();
32 |
33 | export default baseUrl;
34 |
--------------------------------------------------------------------------------
/src/app/store.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers, configureStore } from "@reduxjs/toolkit";
2 | import agentApi from "../services/admin/api/agentApi";
3 | import { clientApi } from "../services/admin/api/clientApi";
4 | import adminPropertiesApi from "../services/admin/api/propertiesApi";
5 | import transactionsApi from "../services/admin/api/transactionsApi";
6 | import { appointmentApi } from "../services/client/api/appointmentApi";
7 | import { clientReviewApi } from "../services/client/api/Review";
8 | import appointmentSlice from "../services/client/features/appointmentSlice";
9 | import currentPageSlice from "../services/client/features/currentPageSlice";
10 | import idSlice from "../services/client/features/idSlice";
11 |
12 | import agentPropertyFilter from "../agents/agent-services/propertyFilterSearch";
13 | // import {api} from "../agents/agent-services/appointmentaApiSlice"
14 | import { persistStore } from "redux-persist";
15 | import persistReducer from "redux-persist/es/persistReducer";
16 | import storage from "redux-persist/lib/storage";
17 | import dashboardApi from "../services/admin/api/dashboardApi";
18 | import { AgentAppointmentApi } from "../services/agent/api/appointment";
19 | import { propertyListApi } from "../services/agent/api/propertyApiSlice";
20 | import { apiAgentSlice } from "../services/agent/api/getAgentApiSlice";
21 | import userIdApi from "../services/client/api/userIdApi";
22 |
23 | const rootReducer = combineReducers({
24 | id: idSlice,
25 | appointment: appointmentSlice,
26 | currentPage: currentPageSlice,
27 | agentPropertyFilters: agentPropertyFilter,
28 | [agentApi.reducerPath]: agentApi.reducer,
29 | [clientApi.reducerPath]: clientApi.reducer,
30 | // [propertiesApi.reducerPath]: propertiesApi.reducer,
31 | [adminPropertiesApi.reducerPath]: adminPropertiesApi.reducer,
32 |
33 | [transactionsApi.reducerPath]: transactionsApi.reducer,
34 | [appointmentApi.reducerPath]: appointmentApi.reducer,
35 | [AgentAppointmentApi.reducerPath]: AgentAppointmentApi.reducer,
36 | [clientReviewApi.reducerPath]: clientReviewApi.reducer,
37 |
38 | [dashboardApi.reducerPath]: dashboardApi.reducer,
39 | [propertyListApi.reducerPath]: propertyListApi.reducer,
40 | [userIdApi.reducerPath]: userIdApi.reducer,
41 | [apiAgentSlice.reducerPath]: apiAgentSlice.reducer
42 | })
43 |
44 | const persistConfig = {
45 | key: 'root',
46 | storage,
47 | }
48 |
49 | const persistedReducer = persistReducer(persistConfig, rootReducer);
50 |
51 | export const store: any = configureStore({
52 | reducer: persistedReducer,
53 |
54 | middleware: (getDefaultMiddleware) =>
55 | getDefaultMiddleware().concat([
56 | agentApi.middleware,
57 | clientApi.middleware,
58 |
59 | // propertiesApi.middleware,
60 | adminPropertiesApi.middleware,
61 | transactionsApi.middleware,
62 | appointmentApi.middleware,
63 | AgentAppointmentApi.middleware,
64 | clientReviewApi.middleware,
65 | dashboardApi.middleware,
66 | propertyListApi.middleware,
67 | userIdApi.middleware,
68 | apiAgentSlice.middleware
69 | ]),
70 | });
71 |
72 | export const persistor = persistStore(store);
73 |
74 | // Infer the `RootState` and `AppDispatch` types from the store itself
75 | export type RootState = ReturnType;
76 | // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
77 | export type AppDispatch = typeof store.dispatch;
--------------------------------------------------------------------------------
/src/client/components/appointment/AppointHistory.tsx:
--------------------------------------------------------------------------------
1 | import { CalendarOutlined, ClockCircleOutlined, PhoneFilled } from "@ant-design/icons";
2 | import { Divider, Flex, Space, Tag, Typography } from "antd";
3 | import dayjs from "dayjs";
4 | import customParseFormat from "dayjs/plugin/customParseFormat";
5 | import { BiLocationPlus } from "react-icons/bi";
6 | import { GiNotebook } from "react-icons/gi";
7 | import { TAppointmentHistory } from "../../../services/client/api/appointmentApi";
8 | dayjs.extend(customParseFormat);
9 |
10 | const AppointHistory = ({
11 | data,
12 | }: {
13 | data: TAppointmentHistory[] | undefined;
14 | }) => {
15 | if (data?.length === 0) {
16 | return Empty record ;
17 | }
18 |
19 | return (
20 | <>
21 | {data?.map((appointment) => {
22 | const time24 = dayjs(appointment.appointmentTime, "HH:mm:ss.SSSSSSS");
23 |
24 | const formattedTime = time24.format("h:mm A");
25 |
26 | return (
27 |
31 |
32 |
36 |
37 |
38 |
39 | {new Date(appointment.appointmentDate)
40 | .toDateString()
41 | .slice(3, 10)}
42 |
43 |
44 |
45 |
46 |
47 | {formattedTime}
48 |
49 |
50 |
51 |
52 |
53 |
54 | Client {" "}
55 |
56 | {appointment.clientName}
57 |
58 | {" "}Appointment with Agent {" "}
59 |
60 | {appointment.agentName}
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
{appointment.agentPhoneNumber}
70 |
71 |
72 |
73 | {appointment.address},{appointment.state},{appointment.city}
74 |
75 | {
76 | appointment.note &&
77 |
78 |
79 | {appointment.note}
80 |
81 | }
82 |
83 |
{appointment.status}
84 |
85 |
86 |
87 |
88 |
89 | );
90 | })}
91 | >
92 | );
93 | };
94 |
95 | export default AppointHistory;
96 |
--------------------------------------------------------------------------------
/src/client/components/appointment/Appointment.tsx:
--------------------------------------------------------------------------------
1 | useAppSelector;
2 | import { Col, Flex, Row, Steps } from "antd";
3 | import { useState } from "react";
4 | import { useAppSelector } from "../../../app/hook";
5 | import AppointmentForm from "./AppointmentForm";
6 | import Calendar from "./PickDate";
7 | import PickTime from "./PickTime";
8 |
9 | interface AppointmentProps {
10 | propertyId?: number;
11 | closeDrawer?: () => void;
12 | }
13 |
14 | const Appointment: React.FC = ({ propertyId, closeDrawer }) => {
15 | const [currentPage, setCurrent] = useState(0);
16 |
17 | const next = () => {
18 | setCurrent(currentPage + 1);
19 | };
20 |
21 | const prev = () => {
22 | setCurrent(currentPage - 1);
23 | };
24 |
25 | return (
26 |
27 |
28 | setCurrent(value)}
32 | items={[
33 | {
34 | title: 'Step 1',
35 | },
36 | {
37 | title: 'Step 2',
38 | },
39 | {
40 | title: 'Step 3',
41 | },
42 | ]}
43 | />
44 |
45 | {
46 | currentPage === 0 &&
47 | }
48 | {
49 | currentPage === 1 &&
50 | }
51 | {
52 | currentPage === 2 &&
53 | }
54 |
55 |
56 |
57 | );
58 | };
59 |
60 | export default Appointment;
61 |
--------------------------------------------------------------------------------
/src/client/components/appointment/AppointmentForm.tsx:
--------------------------------------------------------------------------------
1 | import { CalendarOutlined, ClockCircleOutlined } from "@ant-design/icons";
2 | import type { FormProps } from "antd";
3 | import {
4 | Button,
5 | Flex,
6 | Form,
7 | Input,
8 | message,
9 | Space,
10 | Spin,
11 | Typography,
12 | } from "antd";
13 | import dayjs from "dayjs";
14 | import customParseFormat from "dayjs/plugin/customParseFormat";
15 | import React from "react";
16 | import { useDispatch, useSelector } from "react-redux";
17 | import { useAppSelector } from "../../../app/hook";
18 | import { usePostAppointmentMutation } from "../../../services/client/api/appointmentApi";
19 | import { clearInterval } from "../../../services/client/features/appointmentSlice";
20 | import { clientId } from "../../../services/client/features/idSlice";
21 | dayjs.extend(customParseFormat);
22 |
23 | const { Title } = Typography;
24 |
25 | type FieldType = {
26 | notes?: string;
27 | };
28 |
29 | interface PickTimeProps {
30 | propertyId?: number;
31 | prevPage: () => void;
32 | closeDrawer?: () => void;
33 | }
34 |
35 | const AppointmentForm: React.FC = ({ propertyId, prevPage, closeDrawer }) => {
36 | const [postAppointment, { isSuccess }] = usePostAppointmentMutation();
37 | const { appointmentTime, appointmentDate, rawAppointmentTime } = useAppSelector((state) => state.appointment);
38 | const dispatch = useDispatch();
39 | const id = useSelector(clientId)
40 |
41 | if (isSuccess) {
42 | return ;
43 | }
44 |
45 | const onFinish: FormProps["onFinish"] = async (value) => {
46 | try {
47 | await postAppointment({
48 | clientId: id,
49 | propertyId: propertyId || 0,
50 | appointmentDate: appointmentDate,
51 | appointmentTime: rawAppointmentTime,
52 | status: "Pending",
53 | notes: value.notes || "",
54 | })
55 | .unwrap()
56 | .then(() => message.success("Your appointment have been recorded"));
57 | dispatch(clearInterval())
58 | if (closeDrawer) {
59 | closeDrawer();
60 | }
61 | } catch (error) {
62 | message.error("Something went wrong , Please try again");
63 | }
64 | };
65 |
66 | return (
67 | <>
68 |
69 | Confirm your appointment
70 |
71 |
72 |
73 |
74 | {new Date(appointmentDate).toLocaleDateString()}
75 |
76 |
77 |
78 |
79 |
80 | {appointmentTime}
81 |
82 |
83 | {
84 | prevPage();
85 | }}>
86 | Change
87 |
88 |
89 |
90 | label="notes" name="notes">
99 |
100 |
101 |
102 |
103 | Submit
104 |
105 |
106 |
107 | >
108 | );
109 | };
110 |
111 | export default AppointmentForm;
112 |
--------------------------------------------------------------------------------
/src/client/components/appointment/AppointmentHistoryList.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Col, Row, Spin } from "antd";
2 | import { useState } from "react";
3 | import { useSelector } from "react-redux";
4 | import { useGetAppointmentHistoryQuery } from "../../../services/client/api/appointmentApi";
5 | import { clientId } from "../../../services/client/features/idSlice";
6 | import AppointHistory from "./AppointHistory";
7 |
8 | const AppointmentHistoryList = () => {
9 | const [currentPage, setCurrentPage] = useState(1);
10 | const [perPage] = useState(10);
11 | const id = useSelector(clientId)
12 |
13 | const originalIds = [id, currentPage, perPage];
14 | const ids: number[] = originalIds.filter(id => id !== undefined);
15 |
16 | const { data: appointment, isLoading } = useGetAppointmentHistoryQuery(ids);
17 |
18 | const isLastPage = appointment?.data.pageSetting.isEndOfPage;
19 |
20 | const next = () => {
21 | setCurrentPage((prev) => prev + 1);
22 | };
23 |
24 | const prev = () => {
25 | setCurrentPage((prev) => prev - 1);
26 | };
27 |
28 | if (isLoading) {
29 | return (
30 |
31 |
32 |
33 | );
34 | }
35 |
36 | return (
37 |
38 |
39 |
40 |
41 |
42 |
43 | Prev
44 |
45 | {currentPage}
46 |
47 | Next
48 |
49 |
50 |
51 |
52 | );
53 | };
54 |
55 | export default AppointmentHistoryList;
56 |
--------------------------------------------------------------------------------
/src/client/components/appointment/PickDate.tsx:
--------------------------------------------------------------------------------
1 | import { DatePicker, DatePickerProps } from "antd";
2 | import dayjs from "dayjs";
3 | import customParseFormat from "dayjs/plugin/customParseFormat";
4 | import { useAppDispatch, useAppSelector } from "../../../app/hook";
5 | import { addAppointmentDate } from "../../../services/client/features/appointmentSlice";
6 |
7 | dayjs.extend(customParseFormat);
8 |
9 | interface PickDateProps {
10 | nextPage: () => void;
11 | }
12 |
13 | const PickDate: React.FC = ({ nextPage }) => {
14 | const dispatch = useAppDispatch();
15 | const pickedDate = useAppSelector(
16 | (state) => state.appointment.appointmentDate
17 | );
18 |
19 | const onChange: DatePickerProps["onChange"] = (_, dateString) => {
20 | dispatch(addAppointmentDate(dateString));
21 | nextPage()
22 | };
23 |
24 | return (
25 | <>
26 | 1 ? dayjs(pickedDate) : null}
30 | />
31 | >
32 | );
33 | };
34 |
35 | export default PickDate;
36 |
--------------------------------------------------------------------------------
/src/client/components/appointment/PickTime.tsx:
--------------------------------------------------------------------------------
1 | import { ClockCircleOutlined } from "@ant-design/icons";
2 | import { Button, Space, TimePicker, TimePickerProps, Typography } from "antd";
3 | import dayjs from "dayjs";
4 | import customParseFormat from "dayjs/plugin/customParseFormat";
5 | import { useAppDispatch, useAppSelector } from "../../../app/hook";
6 | import { addAppointmentTime } from "../../../services/client/features/appointmentSlice";
7 |
8 | dayjs.extend(customParseFormat);
9 |
10 | interface PickTimeProps {
11 | nextPage: () => void;
12 | prevPage: () => void;
13 | }
14 |
15 | const PickTime: React.FC = ({ nextPage, prevPage }) => {
16 | const dispatch = useAppDispatch();
17 | const { appointmentDate, appointmentTime } = useAppSelector(
18 | (state) => state.appointment
19 | );
20 |
21 | const onTimeChange: TimePickerProps["onChange"] = (time, timeString) => {
22 | const rawTime = time ? dayjs(time) : null;
23 |
24 | const timeOnly = rawTime ? rawTime.format("HH:mm:ss") : null;
25 | dispatch(
26 | addAppointmentTime({
27 | appointmentTime: timeString,
28 | rawAppointmentTime: timeOnly,
29 | })
30 | );
31 | nextPage()
32 | };
33 | return (
34 |
35 |
36 |
37 |
38 |
39 | {new Date(appointmentDate).toDateString()}
40 |
41 |
42 | {
43 | prevPage()
44 | }}>
45 | Change
46 |
47 |
48 |
49 | 1 ? dayjs(appointmentTime, "h:mm a") : null
56 | }
57 | />
58 |
59 | );
60 | };
61 |
62 | export default PickTime;
63 |
--------------------------------------------------------------------------------
/src/client/components/property/Checkbox.tsx:
--------------------------------------------------------------------------------
1 | import React, { ChangeEvent } from 'react';
2 |
3 | interface CheckboxOption {
4 | label: string;
5 | value: string;
6 | }
7 |
8 | interface CheckBoxProps {
9 | name: string;
10 | index: number;
11 | checked: boolean;
12 | option: CheckboxOption;
13 | onChange: (e: ChangeEvent, value: string) => void;
14 | selectedValues: string[];
15 | }
16 |
17 |
18 | const CheckBox: React.FC = ({
19 | name,
20 | index,
21 | checked,
22 | option,
23 | onChange,
24 | selectedValues
25 | }) => {
26 | return (
27 |
28 | onChange(e, option.value)} checked={selectedValues.includes(option.value)} />
29 |
30 | {option.label}
31 |
32 |
33 | );
34 | };
35 |
36 | export default CheckBox;
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/client/components/property/CheckboxGroup.tsx:
--------------------------------------------------------------------------------
1 | import React, { ChangeEvent } from "react";
2 | import CheckBox from "./Checkbox";
3 |
4 | // Define types for the props
5 | interface CheckboxOption {
6 | label: string;
7 | value: string;
8 | }
9 |
10 | interface CheckboxGroupProps {
11 | label: string;
12 | name: string;
13 | options: CheckboxOption[];
14 | selectedValues: string[]; // Update to array of strings
15 | error?: string;
16 | optional?: boolean;
17 | onChange: (e: ChangeEvent, value: string) => void;
18 | }
19 |
20 | const CheckboxGroup: React.FC = ({
21 | label,
22 | name,
23 | options,
24 | selectedValues,
25 | optional = false,
26 | onChange,
27 | }) => {
28 | return (
29 |
30 |
31 |
32 | {label}
33 | {optional && * }
34 |
35 |
36 | {options.map((option, index) => (
37 |
44 | ))}
45 |
46 |
47 | );
48 | };
49 |
50 | export default CheckboxGroup;
51 |
--------------------------------------------------------------------------------
/src/client/components/property/Container.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 |
3 | interface ContainerProps {
4 | children: ReactNode;
5 | }
6 |
7 | const Container: React.FC = ({ children }) => {
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
15 | export default Container;
16 |
--------------------------------------------------------------------------------
/src/client/components/property/Home.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Skeleton } from "antd";
2 | import React, { useEffect, useState } from "react";
3 | import { useDispatch } from "react-redux";
4 | import { useAuth } from "../../../login/login-context/AuthContext";
5 | import { useGetAllPropertiesQuery } from "../../../services/admin/api/propertiesApi";
6 | import { useGetClientUserIdQuery } from "../../../services/client/api/userIdApi";
7 | import { updateClientId } from "../../../services/client/features/idSlice";
8 | import { HomeProperties, PropertyResponse } from "../../../type/type";
9 | import PropertyCard from "./PropertyCard";
10 |
11 |
12 | const Home: React.FC = () => {
13 | const [propertyTypes] = useState([
14 | "All",
15 | "Condominium",
16 | "Apartment",
17 | "Townhouses",
18 | ]);
19 | const [page, setPage] = useState({ pageNumber: 1, pageSize: 10 });
20 | const [activeType, setActiveType] = useState("All");
21 |
22 | const dispatch = useDispatch();
23 | const { user } = useAuth();
24 | const userId = user?.UserId;
25 |
26 | const { data: userIdData } = useGetClientUserIdQuery({ userId })
27 | const clientData = userIdData?.data || null;
28 |
29 | useEffect(() => {
30 | dispatch(updateClientId(clientData))
31 | }, [clientData])
32 |
33 | const params = {
34 | ...page,
35 | propertyType: activeType === "All" ? "" : activeType
36 | }
37 |
38 | const { isFetching, data: PropertyData } = useGetAllPropertiesQuery(params);
39 |
40 | const properties: HomeProperties = PropertyData?.data ?? [];
41 |
42 | const nextPage = () => {
43 | setPage({ ...page, pageNumber: page.pageNumber + 1 });
44 | }
45 |
46 | const prevPage = () => {
47 | setPage({ ...page, pageNumber: page.pageNumber - 1 });
48 | }
49 |
50 | return (
51 |
52 | {/* Fixed Background Image */}
53 |
60 |
61 | {/* Black Overlay */}
62 |
63 |
64 | {/* Scrollable Content */}
65 |
66 |
67 |
68 |
78 |
79 |
80 | {
81 | isFetching ?
82 | :
83 | <>
84 |
85 | {propertyTypes?.map((type, index) => (
86 | setActiveType(type)}
89 | className={`px-6 py-2 text-[16px] cursor-pointer rounded-md ${activeType === type
90 | ? "bg-blue-500 text-white"
91 | : "border border-blue-500 text-blue-500"
92 | }`}
93 | >
94 | {type}
95 |
96 | ))}
97 |
98 |
99 | {properties.properties?.map((property) => (
100 |
101 | ))}
102 |
103 |
104 |
108 | Prev
109 |
110 | {page.pageNumber}
111 |
115 | Next
116 |
117 |
118 | >
119 | }
120 |
121 |
122 |
123 |
124 |
125 |
126 | );
127 | };
128 |
129 | export default Home;
130 |
--------------------------------------------------------------------------------
/src/client/components/property/PriceRangeSlider.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 |
3 | interface PriceRangeSliderProps {
4 | minPrice: number;
5 | maxPrice: number;
6 | onMinimumPriceSent: (price: number) => void;
7 | onMaximumPriceSent: (price: number) => void;
8 | }
9 |
10 | const PriceRangeSlider: React.FC = ({ minPrice, maxPrice, onMinimumPriceSent, onMaximumPriceSent }) => {
11 | const [minimumPrice, setMinimumPrice] = useState(minPrice);
12 | const [maximumPrice, setMaximumPrice] = useState(maxPrice);
13 | const [isDragging, setIsDragging] = useState(false);
14 | const [draggingThumb, setDraggingThumb] = useState<'left' | 'right' | null>(null);
15 |
16 | const sliderRef = useRef(null);
17 | const rangeRef = useRef(null);
18 | const thumbLeftRef = useRef(null);
19 | const thumbRightRef = useRef(null);
20 |
21 | useEffect(() => {
22 | setMinimumPrice(minPrice);
23 | setMaximumPrice(maxPrice);
24 | updateRange();
25 | }, [minPrice, maxPrice]);
26 |
27 | const startDrag = (event: React.MouseEvent, thumb: 'left' | 'right'): void => {
28 | setIsDragging(true);
29 | setDraggingThumb(thumb);
30 | event.preventDefault();
31 | };
32 |
33 | const onMouseMove = (event: MouseEvent): void => {
34 | if (isDragging && draggingThumb) {
35 | const sliderRect = sliderRef.current?.getBoundingClientRect();
36 | if (!sliderRect) return;
37 |
38 | const minValue = minPrice; // real minPrice from api
39 | const maxValue = maxPrice; // real maxPrice from api
40 |
41 | let newLeftPercent = ((event.clientX - sliderRect.left) / sliderRect.width) * 100;
42 | if (draggingThumb === 'left') {
43 | if (newLeftPercent < 0) newLeftPercent = 0;
44 | if (newLeftPercent > ((maximumPrice - minValue) / (maxValue - minValue)) * 100) {
45 | newLeftPercent = ((maximumPrice - minValue) / (maxValue - minValue)) * 100;
46 | }
47 | const newMinPrice = Math.round((newLeftPercent / 100) * (maxValue - minValue) + minValue);
48 | setMinimumPrice(newMinPrice);
49 | onMinimumPriceSent(newMinPrice);
50 | } else if (draggingThumb === 'right') {
51 | if (newLeftPercent > 100) newLeftPercent = 100;
52 | if (newLeftPercent < ((minimumPrice - minValue) / (maxValue - minValue)) * 100) {
53 | newLeftPercent = ((minimumPrice - minValue) / (maxValue - minValue)) * 100;
54 | }
55 | const newMaxPrice = Math.round((newLeftPercent / 100) * (maxValue - minValue) + minValue);
56 | setMaximumPrice(newMaxPrice);
57 | onMaximumPriceSent(newMaxPrice);
58 | }
59 | updateRange();
60 | }
61 | };
62 |
63 | const onMouseUp = (): void => {
64 | setIsDragging(false);
65 | setDraggingThumb(null);
66 | };
67 |
68 | const updateRange = (): void => {
69 | const minValue = minPrice;
70 | const maxValue = maxPrice;
71 | const minPercent = ((minimumPrice - minValue) / (maxValue - minValue)) * 100;
72 | const maxPercent = ((maximumPrice - minValue) / (maxValue - minValue)) * 100;
73 | if (thumbLeftRef.current && thumbRightRef.current && rangeRef.current) {
74 | thumbLeftRef.current.style.left = `${minPercent}%`;
75 | thumbRightRef.current.style.left = `${maxPercent}%`;
76 | rangeRef.current.style.left = `${minPercent}%`;
77 | rangeRef.current.style.width = `${maxPercent - minPercent}%`;
78 | }
79 | };
80 |
81 | useEffect(() => {
82 | document.addEventListener('mousemove', onMouseMove);
83 | document.addEventListener('mouseup', onMouseUp);
84 | return () => {
85 | document.removeEventListener('mousemove', onMouseMove);
86 | document.removeEventListener('mouseup', onMouseUp);
87 | };
88 | }, [isDragging, draggingThumb, minimumPrice, maximumPrice]);
89 |
90 | return (
91 |
92 |
93 |
94 |
95 |
startDrag(e, 'left')}
99 | >
100 |
startDrag(e, 'right')}
104 | >
105 |
106 |
107 |
{minimumPrice}
108 | {maximumPrice}
109 |
110 |
111 | );
112 | };
113 |
114 | export default PriceRangeSlider;
115 |
--------------------------------------------------------------------------------
/src/client/components/property/PropertyById.tsx:
--------------------------------------------------------------------------------
1 | import { BackwardFilled } from "@ant-design/icons";
2 | import { Button, Drawer, Flex, Typography } from "antd";
3 | import { useState } from "react";
4 | import { BiLocationPlus, BiRuler } from "react-icons/bi";
5 | import { GiBathtub, GiBunkBeds } from "react-icons/gi";
6 | import { useNavigate, useParams } from "react-router";
7 | import { useAuth } from "../../../login/login-context/AuthContext";
8 | import { useGetPropertyByIdQuery } from "../../../services/admin/api/propertiesApi";
9 | import { PropertyIdResponse } from "../../../type/type";
10 | import Appointment from "../appointment/Appointment";
11 | import Review from "../review/Review";
12 | import TransactionCreateForm from "../transaction/TransactionCreateForm";
13 | import Container from "./Container";
14 |
15 | const PropertyById: React.FC = () => {
16 | const { id } = useParams();
17 | const [openDraw, setOpenDraw] = useState(false);
18 | const navigate = useNavigate();
19 | const { isFetching, data } = useGetPropertyByIdQuery(Number(id));
20 |
21 | const property = data?.data ?? [];
22 | const { user } = useAuth();
23 |
24 | return (
25 | <>
26 | setOpenDraw(false)}>
27 | setOpenDraw(false)} />
28 |
29 |
30 |
31 |
32 | } onClick={() => navigate(-1)} />
33 |
34 | Property Detail
35 |
36 |
37 |
38 |
39 | {/*
*/}
44 |
45 |
46 |
47 |
48 |
49 |
50 | {property?.property?.propertyType}
51 |
52 |
53 | {" "}
54 | {property?.property?.price} kyats
55 |
56 |
57 |
58 | setOpenDraw(true)}>
59 | Get appointment
60 |
61 |
62 |
63 |
64 |
65 |
Features:
66 |
67 |
68 |
69 |
{property?.property?.numberOfBedrooms} beds
70 |
71 |
72 |
73 | {property?.property?.numberOfBathrooms} bathrooms
74 |
75 |
76 |
77 |
78 | {property?.property?.size} SqFt
79 |
80 |
81 |
82 |
83 |
Location:
84 |
85 |
86 |
{property?.property?.address},
87 |
{property?.property?.city}
88 |
89 |
90 |
91 |
Year Built:
92 |
{property?.property?.yearBuilt}
93 |
94 |
95 |
96 |
Description
97 |
98 | {property?.property?.description}
99 |
100 |
101 |
102 |
103 |
104 |
109 |
110 |
111 |
112 |
113 | >
114 | );
115 | };
116 |
117 | export default PropertyById;
118 |
--------------------------------------------------------------------------------
/src/client/components/property/PropertyCard.tsx:
--------------------------------------------------------------------------------
1 | import { Badge, Col, Modal, Row, Tag } from "antd";
2 | import React, { useState } from "react";
3 | import { CiLocationOn } from "react-icons/ci";
4 | import { LiaRulerCombinedSolid } from "react-icons/lia";
5 | import { MdOutlineBedroomChild, MdOutlineReviews } from "react-icons/md";
6 | import { PiBathtubThin } from "react-icons/pi";
7 | import { useNavigate } from "react-router";
8 | import { Properties } from "../../../type/type";
9 |
10 | interface HomeCardProps {
11 | property: Properties;
12 | }
13 |
14 | const PropertyCard: React.FC = ({ property }) => {
15 | const nav = useNavigate();
16 | const [openModal, setOpenModal] = useState(false)
17 |
18 | const handlePropertyDetail = () => {
19 | nav(`./property/${property.property.propertyId}`);
20 | };
21 |
22 | return (
23 | <>
24 | setOpenModal(false)} footer={null} centered>
25 | {property.reviews.map((review: any) => (
26 |
27 |
28 | User Name
29 | Testing username
30 |
31 |
32 | Rating
33 |
34 | {review.rating ? `${review.rating} ⭐` : "N/A"}
35 |
36 |
37 |
38 | Comments
39 |
40 | {review.comments || "No comments"}
41 |
42 |
43 |
44 | ))}
45 |
46 |
47 |
48 |
53 |
54 |
55 |
{property.property.availiablityType}
56 | {
57 | property?.reviews?.length > 0 &&
58 |
59 |
60 | setOpenModal(true)}>
61 |
62 |
63 |
64 | }
65 | {/*
69 |
70 |
*/}
71 |
72 |
73 |
74 |
75 |
76 |
{property.property.address}
77 |
78 |
79 | {property.property.city}, {property.property.state} {/* Adjust as necessary */}
80 |
81 |
82 |
83 |
84 | {property.property.numberOfBedrooms}
85 |
86 |
87 | {property.property.numberOfBathrooms}
88 |
89 |
90 | {property.property.size} SqFt
91 |
92 |
93 |
94 |
95 | {/*
96 |
101 |
{property.property.agentId}
102 |
*/}
103 |
104 | {property.property.price}{" "}
105 | /month
106 |
107 |
111 | Schedule Visit
112 |
113 |
114 |
115 | >
116 | );
117 | };
118 |
119 | export default PropertyCard;
120 |
--------------------------------------------------------------------------------
/src/client/components/property/PropertyGroup.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { HomeGroupProps } from "../../../type/type";
3 | import PropertyCard from "./PropertyCard";
4 |
5 | const PropertyGroup: React.FC = ({
6 | properties,
7 | propertyTypes,
8 | agents
9 | }) => {
10 | const [activeType, setActiveType] = useState("All");
11 |
12 | const filteredProperties =
13 | activeType === "All"
14 | ? properties?.properties
15 | : properties?.properties?.filter((property) => property.property.propertyType === activeType);
16 |
17 |
18 | return (
19 |
20 |
21 |
22 | {propertyTypes?.map((type, index) => (
23 | setActiveType(type)}
26 | className={`px-6 py-2 text-[16px] cursor-pointer rounded-md ${activeType === type
27 | ? "bg-blue-500 text-white"
28 | : "border border-blue-500 text-blue-500"
29 | }`}
30 | >
31 | {type}
32 |
33 | ))}
34 |
35 |
36 |
37 | {filteredProperties?.map((property) => (
38 |
39 | ))}
40 |
41 |
42 | );
43 | };
44 |
45 | export default PropertyGroup;
46 |
--------------------------------------------------------------------------------
/src/client/components/review/RatingReview.tsx:
--------------------------------------------------------------------------------
1 | function RatingReview({ rating, setRating }) {
2 | return (
3 |
4 | {[1, 2, 3, 4, 5].map((star) => {
5 | return (
6 | = star ? "gold" : "gray",
11 | fontSize: `25px`,
12 | }}
13 | onClick={() => {
14 | setRating(star);
15 | }}
16 | >
17 | {" "}
18 | ★{" "}
19 |
20 | );
21 | })}
22 |
23 | );
24 | }
25 |
26 | export default RatingReview;
27 |
--------------------------------------------------------------------------------
/src/client/components/review/Review.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import RatingReview from "./RatingReview";
3 | import { Button, Modal, Input, message } from 'antd';
4 | import { usePostReviewMutation } from "../../../services/client/api/Review";
5 |
6 | const App: React.FC<{ userId?: number; propertyId: number }> = ({ userId, propertyId }) => {
7 |
8 | const [rating, setRating] = useState(0);
9 | const [isModalOpen, setIsModalOpen] = useState(false);
10 | const [reviewText, setReviewText] = useState("");
11 | const [postReview, { isLoading }] = usePostReviewMutation();
12 |
13 | const showModal = () => {
14 | setIsModalOpen(true);
15 | };
16 |
17 | const handleOk = async () => {
18 | try {
19 | await postReview({
20 | userId: Number(userId),
21 | propertyId,
22 | rating,
23 | comments: reviewText,
24 | }).unwrap();
25 | message.success("Review submitted successfully");
26 | setIsModalOpen(false);
27 | } catch (error) {
28 | message.error("Failed to submit the review");
29 | }
30 | };
31 |
32 | const handleCancel = () => {
33 | setIsModalOpen(false);
34 | };
35 |
36 | return (
37 |
38 |
39 | Review
40 |
41 |
48 | setReviewText(e.target.value)}
52 | />
53 |
54 |
55 |
56 |
57 |
58 | );
59 | };
60 |
61 | export default App;
62 |
--------------------------------------------------------------------------------
/src/client/components/transaction/Transaction.tsx:
--------------------------------------------------------------------------------
1 | import { Table, Tag} from "antd";
2 | import { TableProps } from "antd/lib";
3 | import dayjs from 'dayjs';
4 | import { useEffect, useState } from "react";
5 | import { useSelector } from "react-redux";
6 | import { useGetTransactionByClientIdQuery } from "../../../services/admin/api/transactionsApi";
7 | import { clientId } from "../../../services/client/features/idSlice";
8 | import { Transactions, TransApiResponse } from "../../../type/type";
9 | import TransactionSummary from "../transaction/TransactionSummary";
10 |
11 |
12 | const Transaction = () => {
13 | const [isSummaryShow, setIsSummaryShow] = useState(false);
14 | const [transDetailData, setTransDetailData] = useState(undefined);
15 | const id= useSelector(clientId)
16 |
17 |
18 | const initailParams = {
19 | clientId: id,
20 | pageNumber: 1,
21 | pageSize: 10
22 | }
23 | const [params, setParams] = useState(initailParams)
24 |
25 | const { isFetching, data } = useGetTransactionByClientIdQuery(params);
26 |
27 | const pageSetting = data?.data?.pageSetting;
28 | const transactionData: Transactions[] = data?.data?.lstTransaction ?? [];
29 |
30 | const handlePagination = (pageNumber: number, pageSize: number) => {
31 | setParams((prev) => ({
32 | ...prev,
33 | pageNumber,
34 | pageSize
35 | }))
36 | };
37 |
38 | const detailClickHandler = (id: number) => {
39 | setIsSummaryShow(true)
40 |
41 | if (transactionData) {
42 | const transactionDataById = transactionData.find((data) => data.transaction.transactionId === id)
43 | setTransDetailData(transactionDataById)
44 | }
45 | }
46 |
47 | useEffect(() => {
48 | const handleOutsideClick = () => {
49 | if (isSummaryShow) {
50 | setIsSummaryShow(false);
51 | }
52 | };
53 |
54 | document.addEventListener("mousedown", handleOutsideClick);
55 | return () => {
56 | document.removeEventListener("mousedown", handleOutsideClick);
57 | };
58 | }, [isSummaryShow]);
59 |
60 | const columns: TableProps['columns'] = [
61 | {
62 | title: "Transaction Id",
63 | dataIndex: "transactionId",
64 | key: "transactionId",
65 | align: 'center',
66 | render: (value, item, index) => {(params?.pageNumber - 1) * params?.pageSize + index + 1}
67 | },
68 |
69 | {
70 | title: 'Status',
71 | key: 'status',
72 | dataIndex: 'status',
73 | render: (_status: string, record) => getStatusTag(record.transaction.status),
74 | sorter: (a: Transactions, b: Transactions) => {
75 | const statusA = a.transaction?.status || "";
76 | const statusB = b.transaction?.status || "";
77 | return statusA.localeCompare(statusB);
78 | },
79 | },
80 |
81 | {
82 | title: 'Transaction Date',
83 | dataIndex: 'transactionDate',
84 | key: 'date',
85 | render: (transactionDate: Date) => dayjs(transactionDate).format('YYYY-MM-DD HH:mm A')
86 | },
87 | {
88 | title: 'Sale Price',
89 | dataIndex: 'salePrice',
90 | key: 'sale',
91 | align: 'center',
92 | render: (_, record) => (
93 | {record.transaction.salePrice}
94 | )
95 | },
96 | {
97 | title: 'Commission',
98 | dataIndex: 'commission',
99 | key: 'commission',
100 | align: 'center',
101 | render: (_, record) => (
102 | {record.transaction.commission}
103 | )
104 | },
105 | {
106 | title: "Action",
107 | dataIndex: "action",
108 | render: (_, record) => (
109 | detailClickHandler(record.transaction.transactionId)}>
110 | Detail
111 |
112 | ),
113 | },
114 | ];
115 |
116 | const getStatusTag = (status: string) => {
117 | const statusColors: { [key: string]: string } = {
118 | Approved: "green",
119 | pending: "gold",
120 | Rent: "red",
121 | Sell: "green",
122 | true: "yellow",
123 | string : "blue"
124 |
125 | }
126 | return (
127 |
128 | {status}
129 |
130 | )
131 | }
132 |
133 | return (
134 |
135 | {isSummaryShow &&
}
136 |
137 |
record.transaction.transactionId}
140 | dataSource={transactionData}
141 | columns={columns}
142 | pagination={{
143 | total: pageSetting?.totalCount,
144 | current: params?.pageNumber,
145 | onChange: handlePagination
146 | }}
147 | />
148 |
149 |
150 | );
151 | };
152 |
153 | export default Transaction;
154 |
--------------------------------------------------------------------------------
/src/client/components/transaction/TransactionCreateForm.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Form, Input, message, Select } from "antd";
2 | import { FormInstance } from "antd/lib";
3 | import { useRef } from "react";
4 | import { useSelector } from "react-redux";
5 | import { useCreateTransactionMutation } from "../../../services/client/api/transactionApi";
6 | import { clientId } from "../../../services/client/features/idSlice";
7 |
8 | const { Option } = Select;
9 |
10 | interface Props {
11 | id?: string;
12 | }
13 |
14 | const TransactionCreateForm = ({ id }: Props) => {
15 | const [createTransaction, { isLoading }] = useCreateTransactionMutation();
16 | const formRef = useRef(null);
17 | const client = useSelector(clientId)
18 |
19 | const handleSubmitHandler = async (values: any) => {
20 | const newTransaction = {
21 | propertyId: Number(id),
22 | clientId: client,
23 | transactionDate: new Date().toISOString(),
24 | salePrice: Number(values.salePrice),
25 | commission: Number(values.commission),
26 | status: values.status,
27 | };
28 |
29 | try {
30 | await createTransaction(newTransaction)
31 | .unwrap()
32 | .then(() => {
33 | message.success("Transaction created successfully")
34 | formRef.current?.resetFields()
35 | })
36 | .catch((error) => console.error('rejected', error));
37 | } catch (error) {
38 | console.error("Failed to create transaction:", error);
39 | }
40 | };
41 |
42 | return (
43 |
44 |
52 |
55 |
56 |
57 |
58 |
62 |
63 |
64 |
66 |
69 | Rent
70 | Buy
71 | Sell
72 |
73 |
74 |
75 |
77 |
81 |
82 |
83 |
85 |
89 |
90 |
91 |
92 |
98 | {isLoading ? "Submitting..." : "Submit"}
99 |
100 |
101 |
102 |
103 | );
104 | };
105 |
106 | export default TransactionCreateForm;
107 |
--------------------------------------------------------------------------------
/src/client/components/transaction/TransactionSummary.tsx:
--------------------------------------------------------------------------------
1 | import { Transactions } from "../../../type/type";
2 |
3 | interface Props {
4 | setIsShow: (value: boolean) => void;
5 | data: Transactions | undefined;
6 | }
7 | const TransactionSummary = ({ setIsShow ,data}: Props) => {
8 |
9 | function formatDate(dateString: string): string {
10 | const date = new Date(dateString);
11 |
12 | const day = date.getDate();
13 | const month = date.getMonth();
14 | const year = date.getFullYear();
15 |
16 | return `${day}/${month}/${year}`;
17 | }
18 |
19 |
20 | let details;
21 | if (data !== undefined) {
22 | const user = data.client;
23 | const property = data.property;
24 | const transaction = data.transaction;
25 | const formattedDate = formatDate("2024-08-09T14:35:30.957");
26 | console.log(data);
27 |
28 |
29 | details = {
30 | data: {
31 | userData: [
32 | { label: "Name :", value: `${user.firstName} ${user.lastName}` },
33 | { label: "Trans :", value: `${transaction.status}`},
34 | { label: "Agent :", value: `${property.agent}` },
35 | ],
36 | transData: [
37 | { label: "Contract Date", value: `${formattedDate}` },
38 | { label: "Property Name", value: `${property.propertyType}` },
39 | { label: "Location", value: `${property.address}` },
40 | { label: "Sale Price", value: `${transaction.salePrice}` },
41 | { label: "Commission", value: `${transaction.commission}` },
42 | ],
43 | },
44 | };
45 | }
46 |
47 | const userDetail = details?.data.userData;
48 | const transDetail = details?.data.transData;
49 |
50 |
51 |
52 | return (
53 |
54 |
55 |
56 |
57 |
58 |
59 | {userDetail?.map((detail, index) => (
60 |
61 |
62 | {detail.label}
63 |
64 |
{detail.value}
65 |
66 | ))}
67 |
68 |
69 |
70 | Transfer Details
71 |
72 |
73 |
74 | {transDetail?.map((data, index) => (
75 |
79 | {data.label}
80 |
81 | {data.value}
82 |
83 |
84 | ))}
85 |
86 |
87 |
88 | {/*
92 | Confirm
93 | */}
94 |
95 |
96 | );
97 | };
98 |
99 | export default TransactionSummary;
100 |
--------------------------------------------------------------------------------
/src/client/db/data.ts:
--------------------------------------------------------------------------------
1 | export const dataSource = [
2 | {
3 | transaction_id: 1,
4 | property_id:"Lakeview Haven",
5 | client_id: 201,
6 | agent: "Johnson",
7 | transaction_date: new Date("2023-01-01T12:00:00"),
8 | sale_price: 250000.00,
9 | commission: 12500.00,
10 | status: "Buy",
11 | action:"view"
12 | },
13 | {
14 | transaction_id: 2,
15 | property_id:"Tahoe Lake ",
16 | client_id: 202,
17 | agent: "Emily",
18 | transaction_date: new Date("2023-01-05T14:30:00"),
19 | sale_price: 300000.00,
20 | commission: 15000.00,
21 | status: "Sell",
22 | action:"view"
23 | },
24 | {
25 | transaction_id: 3,
26 | property_id:"Lakeview Haven",
27 | client_id: 203,
28 | agent: "Luna David",
29 | transaction_date: new Date("2023-01-10T10:15:00"),
30 | sale_price: 400000.00,
31 | commission: 20000.00,
32 | status: "Rent",
33 | action:"view"
34 | },
35 | {
36 | transaction_id: 4,
37 | property_id:"Lake Tahoe",
38 | client_id: 204,
39 | agent: "Rena Moon",
40 | transaction_date: new Date("2023-01-15T09:00:00"),
41 | sale_price: 500000.00,
42 | commission: 25000.00,
43 | status: "Buy",
44 | action:"view"
45 | },
46 | {
47 | transaction_id: 5,
48 | property_id:"Lakeview Haven",
49 | client_id: 201,
50 | agent: "Johnson",
51 | transaction_date: new Date("2023-01-01T12:00:00"),
52 | sale_price: 250000.00,
53 | commission: 12500.00,
54 | status: "Buy",
55 | action:"view"
56 | },
57 | {
58 | transaction_id: 6,
59 | property_id:"Lakeview Haven",
60 | client_id: 201,
61 | agent: "Johnson",
62 | transaction_date: new Date("2023-01-01T12:00:00"),
63 | sale_price: 250000.00,
64 | commission: 12500.00,
65 | status: "Buy",
66 | action:"view"
67 | },
68 | {
69 | transaction_id: 7,
70 | property_id:"Tahoe Lake ",
71 | client_id: 202,
72 | agent: "Emily",
73 | transaction_date: new Date("2023-01-05T14:30:00"),
74 | sale_price: 300000.00,
75 | commission: 15000.00,
76 | status: "Sell",
77 | action:"view"
78 | },
79 | {
80 | transaction_id: 8,
81 | property_id:"Lakeview Haven",
82 | client_id: 203,
83 | agent: "Luna David",
84 | transaction_date: new Date("2023-01-10T10:15:00"),
85 | sale_price: 400000.00,
86 | commission: 20000.00,
87 | status: "Rent",
88 | action:"view"
89 | },
90 | ];
91 |
92 |
93 |
94 | export const transactionData = [
95 | {id:1,label:"Contract Date :",value:"7/23/2024"},
96 | {id:1,label:"Possession Date :",value:"Upon Funding"},
97 | {id:1,label:"Property Name :",value:"Properties name"},
98 | {id:1,label:"Sale Price :",value:"$45,000"},
99 | {id:1,label:"Commission :",value:"$4,50"},
100 | ]
--------------------------------------------------------------------------------
/src/client/layouts/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link, Outlet } from "react-router-dom";
3 | import { useAuth } from "../../login/login-context/AuthContext";
4 | import { NavLink } from "react-router-dom";
5 |
6 | const Navbar: React.FC = () => {
7 |
8 | const auth = useAuth()
9 |
10 | const navList = [
11 | {
12 | path: '/',
13 | name: 'Home'
14 | },
15 | {
16 | path: 'appointment/history',
17 | name: 'Appointment History'
18 | },
19 | {
20 | path: 'transaction',
21 | name: 'Transaction History'
22 | }
23 | ]
24 |
25 | return (
26 |
27 |
28 |
29 |
30 |
REMS
31 |
32 |
33 | {
34 | navList.map(({ path, name }) => (
35 | `text-blue-500 ${isActive ? 'bg-blue-500 rounded-lg text-gray-100' : 'bg-transparent'} hover:text-gray-100 px-3 py-2 hover:px-3 hover:py-2 hover:bg-blue-500 hover:rounded-lg transition-all active:bg-blue-700`}
37 | >
38 | {name}
39 |
40 | ))
41 | }
42 | auth.logout()}>Logout
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default Navbar;
54 |
--------------------------------------------------------------------------------
/src/client/style/priceRange.css:
--------------------------------------------------------------------------------
1 | .slider-container {
2 | position: relative;
3 | width: 100%;
4 | margin: 0;
5 | }
6 |
7 | .slider {
8 | position: relative;
9 | width: 100%;
10 | height: 3px;
11 | background-color: #ddd;
12 | border-radius: 3px;
13 | }
14 |
15 | .slider-track {
16 | position: absolute;
17 | width: 100%;
18 | height: 3px;
19 | background-color: #ddd;
20 | border-radius: 3px;
21 | }
22 |
23 | .slider-range {
24 | position: absolute;
25 | height: 3px;
26 | background-color: #a15103;
27 | border-radius: 3px;
28 | }
29 |
30 | .slider-thumb {
31 | position: absolute;
32 | width: 14px;
33 | height: 14px;
34 | background-color: #a15103;
35 | border-radius: 50%;
36 | cursor: pointer;
37 | top: -6px; /* Adjust to center the thumb vertically */
38 | }
39 |
40 | .slider-thumb-left {
41 | left: 0;
42 | }
43 |
44 | .slider-thumb-right {
45 | right: 0;
46 | }
47 |
--------------------------------------------------------------------------------
/src/error/Error.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useNavigate } from "react-router";
3 |
4 | const Error = () => {
5 | const navigate = useNavigate();
6 | const goDashboard = () => {
7 | navigate("/admin");
8 | };
9 | const goClient = () => {
10 | navigate("/client");
11 | };
12 | const goAgent = () => {
13 | navigate("/agent")
14 | }
15 |
16 | return (
17 |
18 | Go To Dashboard
19 | Go To Client Web View
20 | Go to Agent Web View
21 |
22 | );
23 | };
24 |
25 | export default Error;
26 |
--------------------------------------------------------------------------------
/src/errorPage/ErrorPage.tsx:
--------------------------------------------------------------------------------
1 | import { useNavigate } from "react-router";
2 |
3 | const ErrorPage = () => {
4 | const navigate = useNavigate();
5 | const goDashboard = () => {
6 | navigate("/");
7 | };
8 | const goClient = () => {
9 | navigate("/web/clients");
10 | };
11 |
12 | return (
13 |
14 | Go To Dashboard
15 | Go To Client Web View
16 |
17 | );
18 | };
19 |
20 | export default ErrorPage;
21 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,100..900;1,100..900&display=swap');
2 | @import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap');
3 |
4 | @tailwind base;
5 | @tailwind components;
6 | @tailwind utilities;
7 |
8 | body {
9 | padding: 0;
10 | margin: 0;
11 | }
12 |
13 | @layer utilities {
14 | @variants responsive {
15 | /* Hide scrollbar for Chrome, Safari, and Opera */
16 | .no-scrollbar::-webkit-scrollbar {
17 | display: none;
18 | }
19 |
20 | /* Hide scrollbar for IE, Edge, and Firefox */
21 | .no-scrollbar {
22 | -ms-overflow-style: none; /* IE and Edge */
23 | scrollbar-width: none; /* Firefox */
24 | }
25 | }
26 | }
27 |
28 | @layer components {
29 | body.active-modal {
30 | @apply overflow-y-hidden
31 | }
32 | }
33 |
34 | .custom-pagination {
35 | position: fixed;
36 | right: 15px;
37 | }
38 |
--------------------------------------------------------------------------------
/src/login/AgentRegister.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useNavigate } from "react-router-dom";
3 | import axios from "axios";
4 | import { Button, Form, Input, Typography } from "antd";
5 | import { toast } from "sonner";
6 |
7 | const AgentRegister = () => {
8 | const navigate = useNavigate();
9 |
10 | const [loading, setLoading] = useState(false);
11 |
12 | const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
13 |
14 | const { Link, Text } = Typography;
15 |
16 | const handleSubmit = async (values: {
17 | agencyName: string;
18 | agentName: string;
19 | licenseNumber: string;
20 | email: string;
21 | password: string;
22 | phone: string;
23 | address: string;
24 | }) => {
25 | const {
26 | agencyName,
27 | agentName,
28 | licenseNumber,
29 | email,
30 | password,
31 | phone,
32 | address,
33 | } = values;
34 |
35 | try {
36 | // Simulate an API call
37 | setLoading(true);
38 | const res = await axios.post(
39 | "http://65.18.112.78:44010/rems/api/v1/agents",
40 | {
41 | agencyName,
42 | agentName,
43 | licenseNumber,
44 | email,
45 | password,
46 | phone,
47 | address,
48 | }
49 | );
50 |
51 | toast.success(res.data.message);
52 |
53 | // Redirect after a successful registration
54 | setTimeout(() => navigate("/"), 2000);
55 | } catch (error) {
56 | toast.error("An error occurred.");
57 | } finally {
58 | setLoading(false);
59 | }
60 | };
61 |
62 | return (
63 |
64 |
94 |
95 |
96 |
97 | {/* Email */}
98 |
104 |
105 |
106 |
107 | {/* Password */}
108 |
111 |
112 |
113 |
114 | {/* Confirm Password */}
115 |
({
120 | validator(_, value) {
121 | if (!value || getFieldValue("password") === value) {
122 | return Promise.resolve();
123 | }
124 | return Promise.reject(
125 | new Error("The two passwords do not match!")
126 | );
127 | },
128 | }),
129 | ]}>
130 |
131 |
132 |
133 | {/* Phone Number */}
134 |
139 |
140 |
141 |
142 | {/* Address */}
143 |
144 |
145 |
146 |
147 |
148 |
153 | Register
154 |
155 |
156 |
157 |
158 |
159 | Already have a account?{" "}
160 |
161 | Login
162 |
163 |
164 |
165 |
166 |
167 | );
168 | };
169 |
170 | export default AgentRegister;
171 |
--------------------------------------------------------------------------------
/src/login/ClientRegister.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useNavigate } from "react-router-dom";
3 | import axios from "axios";
4 | import { Button, Form, Input, Typography } from "antd";
5 | import { toast } from "sonner";
6 |
7 | const ClientRegister = () => {
8 | const navigate = useNavigate();
9 |
10 | const [loading, setLoading] = useState(false);
11 |
12 | const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
13 |
14 | const { Link, Text } = Typography;
15 |
16 | const handleSubmit = async (values: {
17 | address: string;
18 | confirmPassword: string;
19 | email: string;
20 | firstName: string;
21 | lastName: string;
22 | password: string;
23 | phone: string;
24 | }) => {
25 | const { address, email, firstName, lastName, password, phone } = values;
26 |
27 | try {
28 | // Simulate an API call
29 | setLoading(true);
30 | const res = await axios.post(
31 | "http://65.18.112.78:44010/rems/api/v1/clients",
32 | {
33 | firstName,
34 | lastName,
35 | email,
36 | password,
37 | phone,
38 | address,
39 | }
40 | );
41 | toast.success(res.data.message);
42 |
43 | // Redirect after a successful registration
44 | setTimeout(() => navigate("/"), 2000);
45 | } catch (error) {
46 | toast.error(
47 | (error as any)?.response?.data?.message || "An error occurred."
48 | );
49 | } finally {
50 | setLoading(false);
51 | }
52 | };
53 |
54 | return (
55 |
56 |
92 |
93 |
94 |
95 | {/* Password */}
96 |
99 |
100 |
101 |
102 | {/* Confirm Password */}
103 |
({
108 | validator(_, value) {
109 | if (!value || getFieldValue("password") === value) {
110 | return Promise.resolve();
111 | }
112 | return Promise.reject(
113 | new Error("The two passwords do not match!")
114 | );
115 | },
116 | }),
117 | ]}>
118 |
119 |
120 |
121 | {/* Phone Number */}
122 |
127 |
128 |
129 |
130 | {/* Address */}
131 |
132 |
133 |
134 |
135 |
136 |
141 | Register
142 |
143 |
144 |
145 |
146 |
147 | Already have a account?{" "}
148 |
149 | Login
150 |
151 |
152 |
153 |
154 |
155 | );
156 | };
157 |
158 | export default ClientRegister;
159 |
--------------------------------------------------------------------------------
/src/login/Login.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import axios from "axios";
3 | import { useAuth } from "./login-context/AuthContext";
4 | import { useNavigate } from "react-router-dom";
5 | import { Button, Form, Input, Typography } from "antd";
6 | import { toast } from "sonner";
7 |
8 | const Login: React.FC = () => {
9 | const [loading, setLoading] = useState(false);
10 |
11 | const auth = useAuth();
12 | const navigate = useNavigate();
13 |
14 | const { Text, Link } = Typography;
15 |
16 | const handleSubmit = async (values: { email: string; password: string }) => {
17 | try {
18 | setLoading(true);
19 | const { email, password } = values;
20 |
21 | const res = await axios.post(
22 | "http://65.18.112.78:44010/rems/api/v1/Signin",
23 | { email, password }
24 | );
25 |
26 | if (res.data.message.includes("Username or Password is incorrect.")) {
27 | toast.error(res.data.message);
28 | }
29 |
30 | if (res.data.message.includes("Operation Successful.")) {
31 | toast.success("Login successfully");
32 | }
33 |
34 | auth.login(res.data.data.tokens);
35 |
36 | const userRole = JSON.parse(
37 | atob(res.data.data.tokens.accessToken.split(".")[1])
38 | ).role;
39 |
40 | if (userRole.toLowerCase() === "admin") {
41 | navigate("/admin");
42 | } else if (userRole.toLowerCase() === "agent") {
43 | navigate("/agent");
44 | } else if (userRole.toLowerCase() === "client") {
45 | navigate("/client");
46 | } else {
47 | navigate("/");
48 | }
49 |
50 | setLoading(false);
51 | } catch (error) {
52 | setLoading(false);
53 | }
54 | };
55 |
56 | return (
57 | <>
58 |
68 |
69 |
70 |
71 |
74 |
75 |
76 |
77 |
78 |
84 | Login
85 |
86 |
87 |
88 |
89 |
90 | Don’t have an account yet?{" "}
91 |
92 | Register
93 |
94 |
95 |
96 |
97 | >
98 | );
99 | };
100 |
101 | export default Login;
102 |
--------------------------------------------------------------------------------
/src/login/Register.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import AgentRegister from "./AgentRegister";
3 | import ClientRegister from "./ClientRegister";
4 | import { Card, Select, Typography } from "antd";
5 |
6 | const Register: React.FC = () => {
7 | const [role, setRole] = useState("client");
8 |
9 | const { Text, Title } = Typography;
10 |
11 | const renderRegister = () => {
12 | if (role === "client") return ;
13 | if (role === "agent") return ;
14 | };
15 |
16 | return (
17 |
18 |
21 |
22 | Please choose the role
23 |
24 |
25 | Register as
26 | setRole(value)}
30 | options={[
31 | { value: "client", label: "Client" },
32 | { value: "agent", label: "Agent" },
33 | ]}
34 | />
35 |
36 |
37 |
38 |
{renderRegister()}
39 |
40 | );
41 | };
42 |
43 | export default Register;
44 |
--------------------------------------------------------------------------------
/src/login/login-context/AuthContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useState, useEffect } from "react";
2 | import axios from "axios";
3 | import { useDispatch } from "react-redux";
4 | import { clearId } from "../../services/client/features/idSlice";
5 |
6 | interface User {
7 | UserId: number;
8 | role: string;
9 | UserId: number;
10 | }
11 |
12 | interface AuthContextType {
13 | user: User | null;
14 | login: (tokens: Tokens) => void;
15 | logout: () => Promise;
16 | }
17 |
18 | interface Tokens {
19 | accessToken: string;
20 | refreshToken: string;
21 | }
22 |
23 | const AuthContext = createContext(undefined);
24 |
25 | export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
26 | children,
27 | }) => {
28 | const localStorageToken = () => {
29 | const token = localStorage.getItem("token");
30 | if (token) {
31 | return JSON.parse(atob(token.split(".")[1]));
32 | }
33 |
34 | return null;
35 | };
36 |
37 | const token = localStorageToken();
38 |
39 | const [user, setUser] = useState(token);
40 | const dispatch = useDispatch();
41 |
42 | useEffect(() => {
43 | const token = localStorage.getItem("token");
44 |
45 | if (token) {
46 | const decodedToken = JSON.parse(atob(token.split(".")[1]));
47 | setUser(decodedToken);
48 | }
49 | }, []);
50 |
51 | const login = (tokens: Tokens) => {
52 | const decodedToken: any = JSON.parse(atob(tokens.accessToken.split('.')[1]));
53 | setUser(decodedToken);
54 | localStorage.setItem("token", tokens.accessToken);
55 | localStorage.setItem("refreshToken", tokens.refreshToken);
56 | };
57 |
58 | const logout = async () => {
59 | setUser(null);
60 | const accessToken = localStorage.getItem("token");
61 | await axios.post(
62 | `http://65.18.112.78:44010/rems/api/v1/SignOut?accessToken=${accessToken}`,
63 | { accessToken }
64 | );
65 | localStorage.removeItem("token");
66 | localStorage.removeItem("refreshToken");
67 | dispatch(clearId())
68 | };
69 |
70 | return (
71 |
77 | {children}
78 |
79 | );
80 | };
81 |
82 | export const useAuth = () => {
83 | const context = useContext(AuthContext);
84 |
85 | if (context === undefined) {
86 | throw new Error("useAuth must be used within an AuthProvider");
87 | }
88 | return context;
89 | };
90 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import App from "./App.tsx";
4 | import "./index.css";
5 | import { store } from "./app/store.ts";
6 | import { Provider } from "react-redux";
7 |
8 | ReactDOM.createRoot(document.getElementById("root")!).render(
9 |
10 |
11 |
12 |
13 |
14 | );
15 |
--------------------------------------------------------------------------------
/src/services/admin/api/agentApi.ts:
--------------------------------------------------------------------------------
1 | import { createApi } from "@reduxjs/toolkit/query/react";
2 | import baseUrl from "../../../app/hook";
3 | import { AgentData } from "../../../type/type";
4 |
5 | export const agentApi = createApi({
6 | reducerPath: "agentApi",
7 | baseQuery: baseUrl,
8 | endpoints: (builder) => ({
9 | getAllAgents: builder.query<
10 | AgentData,
11 | { pageNumber: number; pageSize: number }
12 | >({
13 | query: ({ pageNumber, pageSize }) => ({
14 | url: `agents/${pageNumber}/${pageSize}`,
15 | method: "GET",
16 | }),
17 | }),
18 | createAgent: builder.mutation({
19 | query: (data) => ({
20 | url: "agents",
21 | method: "POST",
22 | body: data,
23 | }),
24 | }),
25 | getAgentById: builder.query({
26 | query: (id) => ({
27 | url: `agents/${id}`,
28 | method: "GET",
29 | }),
30 | }),
31 | updateAgentById: builder.mutation({
32 | query: ({ data, id }) => ({
33 | url: `agents/${id}`,
34 | method: "PATCH",
35 | body: data,
36 | }),
37 | }),
38 | deleteAgent: builder.mutation({
39 | query: (id) => ({
40 | url: `agents/${id}`,
41 | method: "DELETE",
42 | }),
43 | }),
44 | }),
45 | });
46 |
47 | // Export the hooks
48 | export const {
49 | useGetAllAgentsQuery,
50 | useCreateAgentMutation,
51 | useGetAgentByIdQuery,
52 | useUpdateAgentByIdMutation,
53 | useDeleteAgentMutation,
54 | } = agentApi;
55 |
56 | // Export the entire API for use in the store
57 | export default agentApi;
58 |
--------------------------------------------------------------------------------
/src/services/admin/api/appointmentApi.ts:
--------------------------------------------------------------------------------
1 | import { createApi } from '@reduxjs/toolkit/query/react';
2 | import baseUrl from '../../../app/hook';
3 |
4 | export const appointmentApi = createApi({
5 | reducerPath: 'appointmentApi',
6 | baseQuery: baseUrl,
7 | endpoints: (builder) => ({
8 | getAppointments: builder.query({
9 | query: () => 'appointments/GetAppointmentByClientId/1/1/10',
10 | }),
11 | }),
12 | });
13 |
14 | export const { useGetAppointmentsQuery } = appointmentApi;
15 |
16 | export default appointmentApi;
17 |
--------------------------------------------------------------------------------
/src/services/admin/api/clientApi.ts:
--------------------------------------------------------------------------------
1 | import { createApi } from "@reduxjs/toolkit/query/react";
2 | import baseUrl from "../../../app/hook";
3 | import { ClientData } from "../../../type/type";
4 |
5 | export const clientApi = createApi({
6 | reducerPath: "clientApi",
7 | baseQuery: baseUrl,
8 | endpoints: (builder) => ({
9 | getAllClients: builder.query<
10 | ClientData,
11 | { pageNumber: number; pageSize: number }
12 | >({
13 | query: ({ pageNumber, pageSize }) => ({
14 | url: `clients/${pageNumber}/${pageSize}`,
15 | method: "GET",
16 | }),
17 | }),
18 | createClient: builder.mutation({
19 | query: (data) => ({
20 | url: "clients",
21 | method: "POST",
22 | body: data,
23 | }),
24 | }),
25 | getClientById: builder.query({
26 | query: (id) => ({
27 | url: `clients/${id}`,
28 | method: "GET",
29 | }),
30 | }),
31 | updateClientById: builder.mutation({
32 | query: ({ data, id }) => ({
33 | url: `clients/${id}`,
34 | method: "PATCH",
35 | body: data,
36 | }),
37 | }),
38 | deleteClient: builder.mutation({
39 | query: (id) => ({
40 | url: `clients/${id}`,
41 | method: "DELETE",
42 | }),
43 | }),
44 | }),
45 | });
46 |
47 | // Export the hooks
48 | export const {
49 | useGetAllClientsQuery,
50 | useCreateClientMutation,
51 | useGetClientByIdQuery,
52 | useUpdateClientByIdMutation,
53 | useDeleteClientMutation,
54 | } = clientApi;
55 |
56 | // Export the entire API for use in the store
57 | export default clientApi;
58 |
--------------------------------------------------------------------------------
/src/services/admin/api/dashboardApi.ts:
--------------------------------------------------------------------------------
1 | import { createApi } from "@reduxjs/toolkit/query/react";
2 | import baseUrl from "../../../app/hook";
3 |
4 | interface overview {
5 | "agents": number,
6 | "clients": number,
7 | "properties": number,
8 | "propertySoldIncome": number,
9 | "propertyRentedIncome": number
10 | }
11 |
12 | export interface WeeklyActivity {
13 | "name": string,
14 | "sold": number,
15 | "rented": number
16 | }
17 |
18 | export interface AgentActivity {
19 | "agentName": string,
20 | "sellProperty": number,
21 | "rentedProperty": number,
22 | "totalSales": number,
23 | "commissionEarned": number
24 | }
25 |
26 | interface DashboardResponse {
27 | "isSuccess": boolean,
28 | "isError": boolean,
29 | "data": {
30 | "overview": overview,
31 | "weeklyActivity": WeeklyActivity,
32 | "agentActivity": AgentActivity
33 | },
34 | "message": string
35 | }
36 |
37 | export interface DashboardData{
38 | overview?: overview[],
39 | weeklyActivity?: WeeklyActivity[],
40 | agentActivity?: AgentActivity[]
41 | }
42 |
43 | export const dashboardApi = createApi({
44 | reducerPath: "dashboardApi",
45 | baseQuery: baseUrl,
46 | tagTypes: ['dashboard'],
47 | endpoints: (builder) => ({
48 | getDashboardData: builder.query({
49 | query: () => 'Dashboard',
50 | providesTags: ['dashboard'],
51 | }),
52 | })
53 | });
54 |
55 | export const { useGetDashboardDataQuery } = dashboardApi;
56 |
57 | export default dashboardApi;
58 |
--------------------------------------------------------------------------------
/src/services/admin/api/propertiesApi.ts:
--------------------------------------------------------------------------------
1 | import { createApi } from "@reduxjs/toolkit/query/react";
2 | import baseUrl from "../../../app/hook";
3 | import { Properties, ChangeStatus } from "../../../type/type";
4 |
5 | export const adminPropertiesApi = createApi({
6 | reducerPath: "propertiesApi",
7 | baseQuery: baseUrl,
8 | tagTypes: ["properties"],
9 | endpoints: (builder) => ({
10 | getAllProperties: builder.query({
11 |
12 | query: (params) => {
13 | const baseUrl = `properties/${params.pageNumber}/${params.pageSize}`;
14 | const url = params.propertyType
15 | ? `${baseUrl}?propertyType=${encodeURIComponent(params.propertyType)}`
16 | : baseUrl;
17 |
18 | return ({
19 | url,
20 | method: "GET",
21 | })},
22 | providesTags: ['properties']
23 | }),
24 | getPropertyById: builder.query({
25 | query: (propertyId) => ({
26 | url: `properties/${propertyId}`,
27 | method: "GET",
28 | }),
29 | providesTags: ["properties"],
30 | }),
31 | deleteProperty: builder.mutation({
32 | query: (id) => ({
33 | url: `properties/${id}`,
34 | method: "DELETE",
35 | }),
36 | invalidatesTags: ['properties']
37 | }),
38 | changestatus: builder.mutation({
39 | query: (changestatus) => ({
40 | url: `properties/ChangeStatus`,
41 | method: "PUT",
42 | body: changestatus,
43 | }),
44 | invalidatesTags: ['properties']
45 | }),
46 | }),
47 | });
48 |
49 | // Export the hooks
50 | export const { useGetAllPropertiesQuery, useGetPropertyByIdQuery, useDeletePropertyMutation, useChangestatusMutation } =
51 | adminPropertiesApi;
52 |
53 | export default adminPropertiesApi;
54 |
--------------------------------------------------------------------------------
/src/services/admin/api/transactionsApi.ts:
--------------------------------------------------------------------------------
1 | import { createApi } from "@reduxjs/toolkit/query/react";
2 | import baseUrl from "../../../app/hook";
3 | import { Transaction } from "../../../type/type";
4 | import { CreateTransactionRequest } from "../../client/api/transactionApi";
5 |
6 | export const transactionsApi = createApi({
7 | reducerPath: "transactionsApi",
8 | baseQuery: baseUrl,
9 | tagTypes: ['transaction'],
10 | endpoints: (builder) => ({
11 | getAllTransactions: builder.query({
12 | query: ({ pageNumber, pageSize }) => ({
13 | url: `/transactions?pageNumber=${pageNumber}&pageSize=${pageSize}`,
14 | method: "GET",
15 | }),
16 | providesTags: ['transaction']
17 | }),
18 | getTransactionByClientId: builder.query({
19 | query: ({ clientId, pageNumber, pageSize }) => ({
20 | url: `transactions/Client?clientId=${clientId}&pageNo=${pageNumber}&pageSize=${pageSize}`,
21 | method: "GET"
22 | }),
23 | providesTags: ['transaction']
24 | }),
25 | createTransaction: builder.mutation({
26 | query: (newTransaction) => ({
27 | url: `transactions`,
28 | method: "POST",
29 | body: newTransaction,
30 | }),
31 | invalidatesTags: ['transaction']
32 | }),
33 |
34 | }),
35 | });
36 |
37 | export const { useGetAllTransactionsQuery, useGetTransactionByClientIdQuery } = transactionsApi;
38 |
39 | export default transactionsApi;
40 |
--------------------------------------------------------------------------------
/src/services/agent/api/appointment.ts:
--------------------------------------------------------------------------------
1 | import { createApi } from "@reduxjs/toolkit/query/react";
2 | import baseUrl from "../../../app/hook";
3 |
4 | export interface Appointment {
5 | appointmentId: number ;
6 | agentName: string;
7 | clientName: string;
8 | appointmentDate: string;
9 | appointmentTime: string;
10 | agentPhoneNumber: string ;
11 | status: string;
12 | note: string ;
13 | address: string;
14 | city: string;
15 | state: string;
16 | price: number;
17 | size: number;
18 | numberOfBedrooms: number;
19 | numberOfBathrooms: number;
20 | }
21 |
22 | export interface AppointmentsResponse {
23 | isSuccess: boolean;
24 | isError: boolean;
25 | data: {
26 | pageSetting: {
27 | totalCount: number;
28 | pageSize: number;
29 | isEndOfPage: boolean;
30 | };
31 | appointmentDetails: Appointment[];
32 | };
33 | message: string;
34 | }
35 |
36 | export const AgentAppointmentApi = createApi({
37 | baseQuery: baseUrl,
38 | tagTypes: ["Appointment"],
39 | endpoints: (build) => ({
40 | getAppointmentsByAgentId: build.query({
41 | query: ({ id, pageNo, pageSize }) => `appointments/property/${id}/${pageNo + 1}/${pageSize}`,
42 | providesTags: ['Appointment']
43 | }),
44 |
45 | updateAppointmentsStatus: build.mutation({
46 | query: ({ appointmentId, data }) => ({
47 | url: `appointments/${appointmentId}`,
48 | method: "PATCH",
49 | body: data,
50 | }),
51 | invalidatesTags: (_result, _error, { id }) => [{ type: "Appointment", id }],
52 | }),
53 | }),
54 | });
55 |
56 | export const {
57 | useGetAppointmentsByAgentIdQuery,
58 | useUpdateAppointmentsStatusMutation
59 | } = AgentAppointmentApi;
--------------------------------------------------------------------------------
/src/services/agent/api/getAgentApiSlice.ts:
--------------------------------------------------------------------------------
1 | import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
2 |
3 | interface Agent {
4 | agentId: number,
5 | userId: number,
6 | agentName: string,
7 | agencyName: string,
8 | licenseNumber: string,
9 | email: string,
10 | phone: string,
11 | address: string,
12 | role: string
13 | }
14 |
15 | interface AgentResponse {
16 | data: Agent
17 | }
18 |
19 | export const apiAgentSlice = createApi({
20 | reducerPath: 'getAgentapi',
21 | baseQuery: fetchBaseQuery({
22 | baseUrl: 'http://65.18.112.78:44010/rems/api/v1/',
23 | }),
24 | endpoints: (builder) => ({
25 | getAgentByUserId: builder.query({
26 | query: (id) => `agents/Users/${id}`,
27 | }),
28 | }),
29 | });
30 |
31 |
32 | export const { useGetAgentByUserIdQuery } = apiAgentSlice;
--------------------------------------------------------------------------------
/src/services/agent/api/propertyApiSlice.ts:
--------------------------------------------------------------------------------
1 | import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
2 | import { PropertiesResponse } from '../../../agents/property-list/data-for-agent/propertyData';
3 |
4 | // Define the API slice
5 | export const propertyListApi = createApi({
6 | reducerPath: 'propertyListApi',
7 | baseQuery: fetchBaseQuery({ baseUrl: 'http://65.18.112.78:44010/rems/api/v1' }),
8 | endpoints: (builder) => ({
9 | getProperties: builder.query<{ data: PropertiesResponse }, {
10 | page: number,
11 | limit: number,
12 | city?: string,
13 | agentId?: number | undefined
14 | }>({
15 | query: ({ page, limit, city, agentId }) => {
16 | let queryStr = `properties/${page}/${limit}`;
17 | if (city) {
18 | queryStr += `?city=${encodeURIComponent(city)}`
19 | }
20 |
21 | if (agentId) {
22 | queryStr = `properties/${page}/${limit}?agentId=${agentId}`
23 | }
24 | return queryStr;
25 | },
26 |
27 | }),
28 | }),
29 | });
30 |
31 | // Export the auto-generated hook for the `getProperties` query
32 | export const { useGetPropertiesQuery } = propertyListApi;
--------------------------------------------------------------------------------
/src/services/agent/api/text.ts:
--------------------------------------------------------------------------------
1 | // web site view က agent နဲ့ သက်ဆိုင်တဲ့ api call များ ရေးရန်
2 | // sample ကို dashboard က ကြည့်လို့ရ
3 |
--------------------------------------------------------------------------------
/src/services/client/api/Review.ts:
--------------------------------------------------------------------------------
1 | import { createApi } from "@reduxjs/toolkit/query/react";
2 | import baseUrl from "../../../app/hook";
3 |
4 | export interface Review {
5 | userId: number;
6 | propertyId: number;
7 | rating: number;
8 | comments: string;
9 | }
10 |
11 | export const clientReviewApi = createApi({
12 | reducerPath: "review",
13 | baseQuery: baseUrl,
14 | tagTypes: ["review"],
15 | endpoints: (builder) => ({
16 | postReview: builder.mutation({
17 | query: (review) => ({
18 | url: "reviews",
19 | method: "POST",
20 | body: review,
21 | }),
22 | invalidatesTags: ["review"]
23 | }),
24 | }),
25 | });
26 |
27 | export const { usePostReviewMutation } = clientReviewApi;
28 |
29 | export default clientReviewApi;
30 |
--------------------------------------------------------------------------------
/src/services/client/api/appointmentApi.ts:
--------------------------------------------------------------------------------
1 | import { createApi } from "@reduxjs/toolkit/query/react";
2 | import baseUrl from "../../../app/hook";
3 | import { TAppointment, TResponse } from "../../../type/type";
4 |
5 | export interface TAppointmentHistory {
6 | appointmentId: number;
7 | agentName: string;
8 | agentPhoneNumber: string;
9 | clientName: string;
10 | appointmentDate: string;
11 | appointmentTime: string;
12 | status: "pending" | "confirmed" | "done";
13 | note?: string;
14 | }
15 |
16 | export interface TCreatePostRequest {
17 | clientId: number;
18 | propertyId: number;
19 | appointmentDate: string;
20 | appointmentTime: Date | null;
21 | status: string;
22 | notes: string;
23 | }
24 |
25 | export const appointmentApi = createApi({
26 | reducerPath: "appointmentHistory",
27 | baseQuery: baseUrl,
28 | tagTypes: ["appointments"],
29 | endpoints: (builder) => ({
30 | getAppointmentHistory: builder.query<
31 | TResponse,
32 | number[]
33 | >({
34 | query: (idArray) =>
35 | `appointments/client/${idArray.join("/")}`,
36 | providesTags: ["appointments"],
37 | }),
38 | getAppoitmentByAdmin: builder.query, { pageNumber: number, pageSize: number }>({
39 | query: ({ pageNumber, pageSize }) => ({
40 | url: `appointments/admin/${pageNumber}/${pageSize}`,
41 | }),
42 | providesTags: ["appointments"]
43 | }),
44 | postAppointment: builder.mutation({
45 | query: (newAppointment) => ({
46 | url: "appointments",
47 | method: "POST",
48 | body: newAppointment,
49 | }),
50 | })
51 | }),
52 | });
53 |
54 | export const { useGetAppointmentHistoryQuery, usePostAppointmentMutation, useGetAppoitmentByAdminQuery } =
55 | appointmentApi;
56 |
--------------------------------------------------------------------------------
/src/services/client/api/propertyApi.ts:
--------------------------------------------------------------------------------
1 | import { createApi } from "@reduxjs/toolkit/query/react";
2 | import baseUrl from "../../../app/hook";
3 |
4 | export interface TProperty {
5 | property: {
6 | propertyId: number;
7 | address: string;
8 | city: string;
9 | state: string;
10 | zipCode: string;
11 | propertyType: string;
12 | price: number;
13 | size: number;
14 | numberOfBedrooms: number;
15 | numberOfBathrooms: number;
16 | yearBuilt: number;
17 | description: string;
18 | status: string;
19 | availiablityType: string;
20 | adddate: string;
21 | };
22 | images: string[];
23 | }
24 |
25 | export interface TPropertyResponse {
26 | data: TProperty;
27 | }
28 |
29 | export const propertiesApi = createApi({
30 | reducerPath: "properties",
31 | baseQuery: baseUrl,
32 | tagTypes: ["properties"],
33 | endpoints: (builder) => ({
34 | getPropertyById: builder.query({
35 | query: (propertyId) => `properties/${propertyId}`,
36 | providesTags: ["properties"],
37 | }),
38 | }),
39 | });
40 |
41 | export const { useGetPropertyByIdQuery } = propertiesApi;
42 |
--------------------------------------------------------------------------------
/src/services/client/api/text.ts:
--------------------------------------------------------------------------------
1 | // web site view က agent နဲ့ သက်ဆိုင်တဲ့ api call များ ရေးရန်
2 | // sample ကို dashboard က ကြည့်လို့ရ
3 |
--------------------------------------------------------------------------------
/src/services/client/api/transactionApi.ts:
--------------------------------------------------------------------------------
1 | import { createApi } from "@reduxjs/toolkit/query/react";
2 | import baseUrl from "../../../app/hook";
3 | import { TransApiResponse } from "../../../type/type";
4 |
5 | export interface TTransactionHistory {
6 | appointmentId: number;
7 | agentName: string;
8 | clientName: string;
9 | appointmentDate: string;
10 | appointmentTime: string;
11 | status:string;
12 | notes?: string;
13 | }
14 |
15 | // export interface TCreatePostRequest {
16 | // clientId: number;
17 | // propertyId: number;
18 | // appointmentDate: string;
19 | // appointmentTime: Date | null;
20 | // status: string;
21 | // notes: string;
22 | // }
23 |
24 | export interface CreateTransactionRequest {
25 | propertyId: number;
26 | clientId: number;
27 | transactionDate: string;
28 | salePrice: number;
29 | commission: number;
30 | status: string;
31 | }
32 |
33 | export const transactionApi = createApi({
34 | reducerPath: "transactionHistory",
35 | baseQuery: baseUrl,
36 | tagTypes: ["appointments"],
37 | endpoints: (builder) => ({
38 | createTransaction: builder.mutation({
39 | query: (newTransaction) => ({
40 | url: `transactions`,
41 | method: "POST",
42 | body: newTransaction,
43 | }),
44 | invalidatesTags: ["appointments"],
45 | }),
46 |
47 | getAllTransactionByClientId: builder.query<
48 | TransApiResponse,
49 | { clientId: number; pageNumber: number; pageSize: number }
50 | >({
51 | query: ({ clientId, pageNumber, pageSize }) =>
52 | `transactions/Client?clientId=${clientId}&pageNo=${pageNumber}&pageSize=${pageSize}`,
53 | providesTags: ["appointments"]
54 | }),
55 | }),
56 | });
57 |
58 | export const {
59 | useCreateTransactionMutation,
60 | useGetAllTransactionByClientIdQuery,
61 | } = transactionApi;
62 |
--------------------------------------------------------------------------------
/src/services/client/api/userIdApi.ts:
--------------------------------------------------------------------------------
1 | import { createApi } from "@reduxjs/toolkit/query/react";
2 | import baseUrl from "../../../app/hook";
3 | import { Client } from "../../../type/type";
4 |
5 | interface Response {
6 | data: Client;
7 | }
8 |
9 | export const userIdApi = createApi({
10 | reducerPath: "userIdApi",
11 | baseQuery: baseUrl,
12 | tagTypes: ['dashboard'],
13 | endpoints: (builder) => ({
14 | getClientUserId: builder.query({
15 | query: ({userId}) => (
16 | {
17 | url: `clients/Users/${userId}`,
18 | method: 'GET',
19 | }
20 | ),
21 | providesTags: ['dashboard'],
22 | }),
23 | })
24 | });
25 |
26 | export const { useGetClientUserIdQuery } = userIdApi;
27 |
28 | export default userIdApi;
29 |
--------------------------------------------------------------------------------
/src/services/client/features/appointmentSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import { TAppointment } from "../../../type/type";
3 |
4 | // Initial state for the slice
5 | const initialAppointmentSlice: TAppointment = {
6 | appointmentDate: "",
7 | appointmentTime: "",
8 | rawAppointmentTime: null,
9 | status: "pending",
10 | notes: "",
11 | };
12 |
13 | // Create the slice
14 | export const appointmentSlice = createSlice({
15 | name: "appointment",
16 | initialState: initialAppointmentSlice,
17 | reducers: {
18 | addAppointmentDate: (state, action) => {
19 | state.appointmentDate = action.payload;
20 | },
21 | addAppointmentTime: (state, action) => {
22 | state.appointmentTime = action.payload.appointmentTime;
23 | state.rawAppointmentTime = action.payload.rawAppointmentTime;
24 | },
25 | updateStatus: (state, action) => {
26 | state.status = action.payload;
27 | },
28 | addNotes: (state, action) => {
29 | state.notes = action.payload;
30 | },
31 | clearInterval: (state) => {
32 | state.appointmentDate = "",
33 | state.appointmentTime = "";
34 | }
35 | },
36 | });
37 |
38 | // Export the actions
39 | export const {
40 | addAppointmentDate,
41 | addAppointmentTime,
42 | updateStatus,
43 | addNotes,
44 | clearInterval
45 | } = appointmentSlice.actions;
46 |
47 | // Export the reducer
48 | export default appointmentSlice.reducer;
49 |
--------------------------------------------------------------------------------
/src/services/client/features/currentPageSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | export interface TCurrentPage {
4 | currentPage: number;
5 | }
6 |
7 | const initialCurrentPage: TCurrentPage = {
8 | currentPage: 0,
9 | };
10 |
11 | export const currentPageSlice = createSlice({
12 | name: "currentPage",
13 | initialState: initialCurrentPage,
14 | reducers: {
15 | next: (state) => {
16 | state.currentPage += 1;
17 | },
18 | prev: (state) => {
19 | state.currentPage -= 1;
20 | },
21 | },
22 | });
23 |
24 | export const { next, prev } = currentPageSlice.actions;
25 |
26 | export default currentPageSlice.reducer;
27 |
--------------------------------------------------------------------------------
/src/services/client/features/idSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | interface Ids {
4 | clientId: number;
5 | }
6 |
7 | const initialState: Ids = {
8 | clientId: 0
9 | };
10 |
11 | export const idSlice = createSlice({
12 | name: "id",
13 | initialState,
14 | reducers: {
15 | updateClientId: (state, {payload}) => {
16 | console.log(payload);
17 |
18 | state.clientId = payload?.clientId;
19 | },
20 | clearId: (state) => {
21 | state.clientId = 0
22 | }
23 | }
24 | })
25 |
26 | export const { updateClientId, clearId } = idSlice.actions;
27 | export const clientId = (state: any) => state.id?.clientId;
28 |
29 | export default idSlice.reducer;
--------------------------------------------------------------------------------
/src/services/login-interceptors/axios.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { useAuth } from "../../login/login-context/AuthContext";
3 | import { jwtDecode } from "jwt-decode";
4 |
5 |
6 | const auth = useAuth()
7 | const api = axios.create();
8 |
9 | api.interceptors.request.use(async (config) => {
10 | const accessToken = localStorage.getItem('token');
11 |
12 | if (accessToken) {
13 | config.headers.Authorization = `Bearer ${accessToken}`;
14 | }
15 |
16 | if (accessToken && isTokenExpired(accessToken)) {
17 | try {
18 | const refreshToken = localStorage.getItem("refreshToken");
19 | const res = await axios.post("http://65.18.112.78:44010/rems/api/v1/Refresh-Token", { refreshToken });
20 |
21 | const newRefreshToken = res.data.data.refreshToken;
22 | localStorage.setItem("refreshToken", newRefreshToken);
23 |
24 | config.headers.Authorization = `Bearer ${accessToken}`;
25 |
26 | } catch (error) {
27 | auth.logout();
28 | return Promise.reject(error)
29 | }
30 | }
31 |
32 | return config;
33 |
34 | });
35 |
36 | // Helper function to check if token is expired
37 |
38 | const isTokenExpired = (token: string): boolean => {
39 | const decoded = JSON.parse(jwtDecode(token));
40 | const expirationTime = decoded.exp * 1000;
41 |
42 | return Date.now() > expirationTime;
43 | }
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
4 | theme: {
5 | extend: {
6 | colors: {
7 | primary: "#a15103",
8 | },
9 | fontFamily:{
10 | raleWay: ["Raleway", "sans-serif"],
11 | lato: ["Lato", "sans-serif"]
12 | }
13 | },
14 | },
15 | plugins: [],
16 | };
17 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
5 | "target": "ES2020",
6 | "useDefineForClassFields": true,
7 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
8 | "module": "ESNext",
9 | "skipLibCheck": true,
10 | "allowSyntheticDefaultImports": true,
11 | "esModuleInterop": true,
12 |
13 | /* Bundler mode */
14 | "moduleResolution": "Node",
15 | "allowImportingTsExtensions": true,
16 | "resolveJsonModule": true,
17 | "isolatedModules": true,
18 | "moduleDetection": "force",
19 | "noEmit": true,
20 | "jsx": "react-jsx",
21 |
22 | /* Linting */
23 | "strict": true,
24 | "noUnusedLocals": true,
25 | "noUnusedParameters": true,
26 | "noFallthroughCasesInSwitch": true
27 | },
28 | "include": ["src"]
29 | }
30 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | {
5 | "path": "./tsconfig.app.json"
6 | },
7 | {
8 | "path": "./tsconfig.node.json"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
5 | "skipLibCheck": true,
6 | "module": "ESNext",
7 | "moduleResolution": "bundler",
8 | "allowSyntheticDefaultImports": true,
9 | "esModuleInterop": true,
10 | "strict": true,
11 | "noEmit": true
12 | },
13 | "include": ["vite.config.ts"]
14 | }
15 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | });
8 |
--------------------------------------------------------------------------------