diff --git a/ditch-the-agent/.env b/ditch-the-agent/.env index 7ccb34e..4a6ce0f 100644 --- a/ditch-the-agent/.env +++ b/ditch-the-agent/.env @@ -1 +1,2 @@ REACT_APP_Maps_API_KEY="AIzaSyDJTApP1OoMbo7b1CPltPu8IObxe8UQt7w" +REAL_ESTATE_API_KEY=AIMLOPERATIONSLLC-a7ce-7525-b38f-cf8b4d52ca70 diff --git a/ditch-the-agent/.env.beta b/ditch-the-agent/.env.beta new file mode 100644 index 0000000..bcb9557 --- /dev/null +++ b/ditch-the-agent/.env.beta @@ -0,0 +1,3 @@ +VITE_API_URL=https://beta.backend.ditchtheagent.com/api/ +ENABLE_REGISTRATION=true +USE_LIVE_DATA=false diff --git a/ditch-the-agent/.env.development b/ditch-the-agent/.env.development new file mode 100644 index 0000000..5c4a1e7 --- /dev/null +++ b/ditch-the-agent/.env.development @@ -0,0 +1,3 @@ +VITE_API_URL=http://127.0.0.1:8010/api/ +ENABLE_REGISTRATION=true +USE_LIVE_DATA=false diff --git a/ditch-the-agent/.env.production b/ditch-the-agent/.env.production new file mode 100644 index 0000000..683d689 --- /dev/null +++ b/ditch-the-agent/.env.production @@ -0,0 +1,3 @@ +VITE_API_URL=https://backend.ditchtheagent.com/api/ +ENABLE_REGISTRATION=false +USE_LIVE_DATA=true diff --git a/ditch-the-agent/package-lock.json b/ditch-the-agent/package-lock.json index f9e96de..b2e7d1a 100644 --- a/ditch-the-agent/package-lock.json +++ b/ditch-the-agent/package-lock.json @@ -10,15 +10,20 @@ "dependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@mui/material": "^5.15.14", + "@mui/icons-material": "^7.3.2", + "@mui/material": "^7.3.2", "@mui/x-data-grid": "^7.2.0", "@mui/x-data-grid-generator": "^7.2.0", + "@mui/x-date-pickers": "^8.11.2", "@react-google-maps/api": "^2.20.7", + "@types/zxcvbn": "^4.4.5", "@vis.gl/react-google-maps": "^1.5.4", "axios": "^1.10.0", + "date-fns": "^4.1.0", "dayjs": "^1.11.10", "echarts": "^5.5.0", "echarts-for-react": "^3.0.2", + "eslint-config-prettier": "^10.1.8", "formik": "^2.4.6", "js-cookie": "^3.0.5", "jwt-decode": "^4.0.0", @@ -27,7 +32,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.22.3", - "simplebar-react": "^3.2.5" + "simplebar-react": "^3.2.5", + "zxcvbn": "^4.4.2" }, "devDependencies": { "@iconify/react": "^4.1.1", @@ -355,9 +361,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", - "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "engines": { "node": ">=6.9.0" } @@ -428,21 +434,31 @@ } }, "node_modules/@emotion/cache": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", - "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", "dependencies": { - "@emotion/memoize": "^0.8.1", - "@emotion/sheet": "^1.2.2", - "@emotion/utils": "^1.2.1", - "@emotion/weak-memoize": "^0.3.1", + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } }, + "node_modules/@emotion/cache/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" + }, + "node_modules/@emotion/cache/node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" + }, "node_modules/@emotion/hash": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", - "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" }, "node_modules/@emotion/is-prop-valid": { "version": "1.2.2", @@ -481,21 +497,26 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", - "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", "dependencies": { - "@emotion/hash": "^0.9.1", - "@emotion/memoize": "^0.8.1", - "@emotion/unitless": "^0.8.1", - "@emotion/utils": "^1.2.1", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", "csstype": "^3.0.2" } }, + "node_modules/@emotion/serialize/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" + }, "node_modules/@emotion/sheet": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", - "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" }, "node_modules/@emotion/styled": { "version": "11.11.5", @@ -520,9 +541,9 @@ } }, "node_modules/@emotion/unitless": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", - "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.1", @@ -533,9 +554,9 @@ } }, "node_modules/@emotion/utils": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", - "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==" }, "node_modules/@emotion/weak-memoize": { "version": "0.3.1", @@ -1244,33 +1265,33 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.14.tgz", - "integrity": "sha512-on75VMd0XqZfaQW+9pGjSNiqW+ghc5E2ZSLRBXwcXl/C4YzjfyjrLPhrEpKnR9Uym9KXBvxrhoHfPcczYHweyA==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.2.tgz", + "integrity": "sha512-AOyfHjyDKVPGJJFtxOlept3EYEdLoar/RvssBTWVAvDJGIE676dLi2oT/Kx+FoVXFoA/JdV7DEMq/BVWV3KHRw==", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" } }, "node_modules/@mui/icons-material": { - "version": "5.15.15", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.15.tgz", - "integrity": "sha512-kkeU/pe+hABcYDH6Uqy8RmIsr2S/y5bP2rp+Gat4CcRjCcVne6KudS1NrZQhUCRysrTDCAhcbcf9gt+/+pGO2g==", - "peer": true, + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.2.tgz", + "integrity": "sha512-TZWazBjWXBjR6iGcNkbKklnwodcwj0SrChCNHc9BhD9rBgET22J1eFhHsEmvSvru9+opDy3umqAimQjokhfJlQ==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.23.9" + "@babel/runtime": "^7.28.3" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@mui/material": "^5.0.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" + "@mui/material": "^7.3.2", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -1279,25 +1300,25 @@ } }, "node_modules/@mui/material": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.14.tgz", - "integrity": "sha512-kEbRw6fASdQ1SQ7LVdWR5OlWV3y7Y54ZxkLzd6LV5tmz+NpO3MJKZXSfgR0LHMP7meKsPiMm4AuzV0pXDpk/BQ==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.2.tgz", + "integrity": "sha512-qXvbnawQhqUVfH1LMgMaiytP+ZpGoYhnGl7yYq2x57GYzcFL/iPzSZ3L30tlbwEjSVKNYcbiKO8tANR1tadjUg==", "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/base": "5.0.0-beta.40", - "@mui/core-downloads-tracker": "^5.15.14", - "@mui/system": "^5.15.14", - "@mui/types": "^7.2.14", - "@mui/utils": "^5.15.14", - "@types/react-transition-group": "^4.4.10", - "clsx": "^2.1.0", + "@babel/runtime": "^7.28.3", + "@mui/core-downloads-tracker": "^7.3.2", + "@mui/system": "^7.3.2", + "@mui/types": "^7.4.6", + "@mui/utils": "^7.3.2", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1", - "react-is": "^18.2.0", + "react-is": "^19.1.1", "react-transition-group": "^4.4.5" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" }, "funding": { "type": "opencollective", @@ -1306,9 +1327,10 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" + "@mui/material-pigment-css": "^7.3.2", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { @@ -1317,30 +1339,91 @@ "@emotion/styled": { "optional": true }, + "@mui/material-pigment-css": { + "optional": true + }, "@types/react": { "optional": true } } }, - "node_modules/@mui/private-theming": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.14.tgz", - "integrity": "sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==", + "node_modules/@mui/material/node_modules/@mui/utils": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.2.tgz", + "integrity": "sha512-4DMWQGenOdLnM3y/SdFQFwKsCLM+mqxzvoWp9+x2XdEzXapkznauHLiXtSohHs/mc0+5/9UACt1GdugCX2te5g==", "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/utils": "^5.15.14", - "prop-types": "^15.8.1" + "@babel/runtime": "^7.28.3", + "@mui/types": "^7.4.6", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.1.1" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.2.tgz", + "integrity": "sha512-ha7mFoOyZGJr75xeiO9lugS3joRROjc8tG1u4P50dH0KR7bwhHznVMcYg7MouochUy0OxooJm/OOSpJ7gKcMvg==", + "dependencies": { + "@babel/runtime": "^7.28.3", + "@mui/utils": "^7.3.2", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming/node_modules/@mui/utils": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.2.tgz", + "integrity": "sha512-4DMWQGenOdLnM3y/SdFQFwKsCLM+mqxzvoWp9+x2XdEzXapkznauHLiXtSohHs/mc0+5/9UACt1GdugCX2te5g==", + "dependencies": { + "@babel/runtime": "^7.28.3", + "@mui/types": "^7.4.6", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.1.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -1349,17 +1432,19 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.14.tgz", - "integrity": "sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.2.tgz", + "integrity": "sha512-PkJzW+mTaek4e0nPYZ6qLnW5RGa0KN+eRTf5FA2nc7cFZTeM+qebmGibaTLrgQBy3UpcpemaqfzToBNkzuxqew==", "dependencies": { - "@babel/runtime": "^7.23.9", - "@emotion/cache": "^11.11.0", + "@babel/runtime": "^7.28.3", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" }, "funding": { "type": "opencollective", @@ -1368,7 +1453,7 @@ "peerDependencies": { "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { @@ -1380,21 +1465,21 @@ } }, "node_modules/@mui/system": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.14.tgz", - "integrity": "sha512-auXLXzUaCSSOLqJXmsAaq7P96VPRXg2Rrz6OHNV7lr+kB8lobUF+/N84Vd9C4G/wvCXYPs5TYuuGBRhcGbiBGg==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.2.tgz", + "integrity": "sha512-9d8JEvZW+H6cVkaZ+FK56R53vkJe3HsTpcjMUtH8v1xK6Y1TjzHdZ7Jck02mGXJsE6MQGWVs3ogRHTQmS9Q/rA==", "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/private-theming": "^5.15.14", - "@mui/styled-engine": "^5.15.14", - "@mui/types": "^7.2.14", - "@mui/utils": "^5.15.14", - "clsx": "^2.1.0", + "@babel/runtime": "^7.28.3", + "@mui/private-theming": "^7.3.2", + "@mui/styled-engine": "^7.3.2", + "@mui/types": "^7.4.6", + "@mui/utils": "^7.3.2", + "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" }, "funding": { "type": "opencollective", @@ -1403,8 +1488,8 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { @@ -1418,57 +1503,17 @@ } } }, - "node_modules/@mui/types": { - "version": "7.2.14", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.14.tgz", - "integrity": "sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==", - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/utils": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.14.tgz", - "integrity": "sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==", + "node_modules/@mui/system/node_modules/@mui/utils": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.2.tgz", + "integrity": "sha512-4DMWQGenOdLnM3y/SdFQFwKsCLM+mqxzvoWp9+x2XdEzXapkznauHLiXtSohHs/mc0+5/9UACt1GdugCX2te5g==", "dependencies": { - "@babel/runtime": "^7.23.9", - "@types/prop-types": "^15.7.11", + "@babel/runtime": "^7.28.3", + "@mui/types": "^7.4.6", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^18.2.0" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/x-data-grid": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.2.0.tgz", - "integrity": "sha512-WKmFo0eKhj3W7Fv8u5n2XP4UcdzuJ+mEYALiMUDAYsah/hPBH9mA1miXn9DjXF3i3dxgzrTjdJemTgTJxAQZKg==", - "dependencies": { - "@babel/runtime": "^7.24.0", - "@mui/system": "^5.15.14", - "@mui/utils": "^5.15.14", - "clsx": "^2.1.0", - "prop-types": "^15.8.1", - "reselect": "^4.1.8" + "react-is": "^19.1.1" }, "engines": { "node": ">=14.0.0" @@ -1478,9 +1523,108 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@mui/material": "^5.15.14", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.6.tgz", + "integrity": "sha512-NVBbIw+4CDMMppNamVxyTccNv0WxtDb7motWDlMeSC8Oy95saj1TIZMGynPpFLePt3yOD8TskzumeqORCgRGWw==", + "dependencies": { + "@babel/runtime": "^7.28.3" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.17.1.tgz", + "integrity": "sha512-jEZ8FTqInt2WzxDV8bhImWBqeQRD99c/id/fq83H0ER9tFl+sfZlaAoCdznGvbSQQ9ividMxqSV2c7cC1vBcQg==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/types": "~7.2.15", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils/node_modules/@mui/types": { + "version": "7.2.24", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz", + "integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-data-grid": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.29.9.tgz", + "integrity": "sha512-RfK7Fnuu4eyv/4eD3MEB1xxZsx0xRBsofb1kifghIjyQV1EKAeRcwvczyrzQggj7ZRT5AqkwCzhLsZDvE5O0nQ==", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0 || ^7.0.0", + "@mui/x-internals": "7.29.0", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", + "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } } }, "node_modules/@mui/x-data-grid-generator": { @@ -1504,15 +1648,117 @@ "react": "^17.0.0 || ^18.0.0" } }, - "node_modules/@mui/x-data-grid-generator/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "node_modules/@mui/x-data-grid-generator/node_modules/@mui/private-theming": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.17.1.tgz", + "integrity": "sha512-XMxU0NTYcKqdsG8LRmSoxERPXwMbp16sIXPcLVgLGII/bVNagX0xaheWAwFv8+zDK7tI3ajllkuD3GZZE++ICQ==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/utils": "^5.17.1", + "prop-types": "^15.8.1" + }, "engines": { - "node": ">=12" + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@mui/x-data-grid-premium": { + "node_modules/@mui/x-data-grid-generator/node_modules/@mui/styled-engine": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.18.0.tgz", + "integrity": "sha512-BN/vKV/O6uaQh2z5rXV+MBlVrEkwoS/TK75rFQ2mjxA7+NBo8qtTAOA4UaM0XeJfn7kh2wZ+xQw2HAx0u+TiBg==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/x-data-grid-generator/node_modules/@mui/system": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.18.0.tgz", + "integrity": "sha512-ojZGVcRWqWhu557cdO3pWHloIGJdzVtxs3rk0F9L+x55LsUjcMUVkEhiF7E4TMxZoF9MmIHGGs0ZX3FDLAf0Xw==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/private-theming": "^5.17.1", + "@mui/styled-engine": "^5.18.0", + "@mui/types": "~7.2.15", + "@mui/utils": "^5.17.1", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-data-grid-generator/node_modules/@mui/types": { + "version": "7.2.24", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz", + "integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-data-grid-generator/node_modules/@mui/x-data-grid-premium": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@mui/x-data-grid-premium/-/x-data-grid-premium-7.2.0.tgz", "integrity": "sha512-VQuXD289RuSKs5bz4fwHg6WTo1Rmh2QFokCAU66+yJ5ZTpqljp371y9pfvn+OMdGx4C7EkNad73VUKfP/xo1xQ==", @@ -1538,7 +1784,32 @@ "react-dom": "^17.0.0 || ^18.0.0" } }, - "node_modules/@mui/x-data-grid-pro": { + "node_modules/@mui/x-data-grid-generator/node_modules/@mui/x-data-grid-premium/node_modules/@mui/x-data-grid": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.2.0.tgz", + "integrity": "sha512-WKmFo0eKhj3W7Fv8u5n2XP4UcdzuJ+mEYALiMUDAYsah/hPBH9mA1miXn9DjXF3i3dxgzrTjdJemTgTJxAQZKg==", + "dependencies": { + "@babel/runtime": "^7.24.0", + "@mui/system": "^5.15.14", + "@mui/utils": "^5.15.14", + "clsx": "^2.1.0", + "prop-types": "^15.8.1", + "reselect": "^4.1.8" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^5.15.14", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + } + }, + "node_modules/@mui/x-data-grid-generator/node_modules/@mui/x-data-grid-premium/node_modules/@mui/x-data-grid-pro": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@mui/x-data-grid-pro/-/x-data-grid-pro-7.2.0.tgz", "integrity": "sha512-QZG30g0OspTaD9oRfLDIJGROQwpalZUhI//DVpvTZW2ZvAJAkfR1t38MJzRMdnksPmCE6sdSWUFru8eSvNg1Cg==", @@ -1562,6 +1833,211 @@ "react-dom": "^17.0.0 || ^18.0.0" } }, + "node_modules/@mui/x-data-grid-generator/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@mui/x-data-grid-generator/node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, + "node_modules/@mui/x-data-grid/node_modules/@mui/utils": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.2.tgz", + "integrity": "sha512-4DMWQGenOdLnM3y/SdFQFwKsCLM+mqxzvoWp9+x2XdEzXapkznauHLiXtSohHs/mc0+5/9UACt1GdugCX2te5g==", + "dependencies": { + "@babel/runtime": "^7.28.3", + "@mui/types": "^7.4.6", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.1.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-data-grid/node_modules/@mui/x-internals": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.29.0.tgz", + "integrity": "sha512-+Gk6VTZIFD70XreWvdXBwKd8GZ2FlSCuecQFzm6znwqXg1ZsndavrhG9tkxpxo2fM1Zf7Tk8+HcOO0hCbhTQFA==", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0 || ^7.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@mui/x-date-pickers": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-8.11.2.tgz", + "integrity": "sha512-izosRFdlo0Aq4nrQ2klOQBLB+yCX3bIlErF/gxZfaXK/kb8NToweZjhHdiyy+hr+VrxK0A71AWI6LkPyfG2WCg==", + "dependencies": { + "@babel/runtime": "^7.28.2", + "@mui/utils": "^7.3.2", + "@mui/x-internals": "8.11.2", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", + "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", + "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0", + "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2 || ^3.0.0", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, + "node_modules/@mui/x-date-pickers/node_modules/@mui/utils": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.2.tgz", + "integrity": "sha512-4DMWQGenOdLnM3y/SdFQFwKsCLM+mqxzvoWp9+x2XdEzXapkznauHLiXtSohHs/mc0+5/9UACt1GdugCX2te5g==", + "dependencies": { + "@babel/runtime": "^7.28.3", + "@mui/types": "^7.4.6", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.1.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-internals": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.11.2.tgz", + "integrity": "sha512-3BFZ0Njgih+eWQBzSsdKEkRMlHtKRGFWz+/CGUrSBb5IApO0apkUSvG4v5augNYASsjksqWOXVlds7Wwznd0Lg==", + "dependencies": { + "@babel/runtime": "^7.28.2", + "@mui/utils": "^7.3.2", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@mui/x-internals/node_modules/@mui/utils": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.2.tgz", + "integrity": "sha512-4DMWQGenOdLnM3y/SdFQFwKsCLM+mqxzvoWp9+x2XdEzXapkznauHLiXtSohHs/mc0+5/9UACt1GdugCX2te5g==", + "dependencies": { + "@babel/runtime": "^7.28.3", + "@mui/types": "^7.4.6", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.1.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/x-license": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@mui/x-license/-/x-license-7.2.0.tgz", @@ -2013,26 +2489,15 @@ "@types/lodash": "*" } }, - "node_modules/@types/node": { - "version": "20.12.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", - "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" }, "node_modules/@types/prop-types": { - "version": "15.7.12", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==" }, "node_modules/@types/react": { "version": "18.2.74", @@ -2053,10 +2518,10 @@ } }, "node_modules/@types/react-transition-group": { - "version": "4.4.10", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", - "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", - "dependencies": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "peerDependencies": { "@types/react": "*" } }, @@ -2066,6 +2531,12 @@ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, + "node_modules/@types/zxcvbn": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@types/zxcvbn/-/zxcvbn-4.4.5.tgz", + "integrity": "sha512-FZJgC5Bxuqg7Rhsm/bx6gAruHHhDQ55r+s0JhDh8CQ16fD7NsJJ+p8YMMQDhSQoIrSmjpqqYWA96oQVMNkjRyA==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.5.0.tgz", @@ -2420,9 +2891,9 @@ } }, "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" }, "node_modules/asynckit": { "version": "0.4.0", @@ -2678,9 +3149,9 @@ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" }, "node_modules/clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "engines": { "node": ">=6" } @@ -2782,6 +3253,15 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/dayjs": { "version": "1.11.10", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", @@ -2936,9 +3416,9 @@ "dev": true }, "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "dependencies": { "once": "^1.4.0" } @@ -3105,6 +3585,21 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-plugin-prettier": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.3.tgz", @@ -3592,6 +4087,7 @@ "version": "1.0.12", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", "dependencies": { "graceful-fs": "^4.1.2", "inherits": "~2.0.0", @@ -3606,6 +4102,7 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dependencies": { "glob": "^7.1.3" }, @@ -4226,7 +4723,8 @@ "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead." }, "node_modules/lodash.isfunction": { "version": "3.0.9", @@ -4627,22 +5125,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", - "dev": true, - "peer": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/prettier-linter-helpers": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", @@ -4738,9 +5220,9 @@ "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" }, "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", + "integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==" }, "node_modules/react-refresh": { "version": "0.14.0", @@ -4829,9 +5311,9 @@ } }, "node_modules/reselect": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", - "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" }, "node_modules/resolve": { "version": "1.22.8", @@ -5192,9 +5674,9 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" }, "node_modules/tmp": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", - "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", "engines": { "node": ">=14.14" } @@ -5293,14 +5775,6 @@ "node": ">=14.17" } }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/unzipper": { "version": "0.10.14", "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", @@ -5384,6 +5858,14 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5566,6 +6048,12 @@ "dependencies": { "tslib": "2.3.0" } + }, + "node_modules/zxcvbn": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz", + "integrity": "sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ==", + "license": "MIT" } } } diff --git a/ditch-the-agent/package.json b/ditch-the-agent/package.json index 0102468..ef14a25 100644 --- a/ditch-the-agent/package.json +++ b/ditch-the-agent/package.json @@ -1,51 +1,59 @@ -{ - "name": "mui-dta-dashboard", - "private": true, - "version": "1.0.1", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview", - "predeploy": "vite build && cp ./dist/index.html ./dist/404.html", - "deploy": "gh-pages -d dist" - }, - "dependencies": { - "@emotion/react": "^11.11.4", - "@emotion/styled": "^11.11.5", - "@mui/material": "^5.15.14", - "@mui/x-data-grid": "^7.2.0", - "@mui/x-data-grid-generator": "^7.2.0", - "@react-google-maps/api": "^2.20.7", - "@vis.gl/react-google-maps": "^1.5.4", - "axios": "^1.10.0", - "dayjs": "^1.11.10", - "echarts": "^5.5.0", - "echarts-for-react": "^3.0.2", - "formik": "^2.4.6", - "js-cookie": "^3.0.5", - "jwt-decode": "^4.0.0", - "lucide-react": "^0.525.0", - "material-ui-popup-state": "^5.1.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router-dom": "^6.22.3", - "simplebar-react": "^3.2.5" - }, - "devDependencies": { - "@iconify/react": "^4.1.1", - "@types/react": "^18.2.66", - "@types/react-dom": "^18.2.22", - "@typescript-eslint/eslint-plugin": "^7.2.0", - "@typescript-eslint/parser": "^7.2.0", - "@vitejs/plugin-react": "^4.2.1", - "eslint": "^8.57.0", - "eslint-plugin-prettier": "^5.5.3", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.6", - "typescript": "^5.2.2", - "vite": "^5.2.0", - "vite-tsconfig-paths": "^4.3.2" - } -} +{ + "name": "mui-dta-dashboard", + "private": true, + "version": "1.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "build:beta": "tsc && vite build --mode beta", + "build:prod": "tsc && vite build --mode production", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview", + "predeploy": "vite build && cp ./dist/index.html ./dist/404.html", + "deploy": "gh-pages -d dist" + }, + "dependencies": { + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.5", + "@mui/icons-material": "^7.3.2", + "@mui/material": "^7.3.2", + "@mui/x-data-grid": "^7.2.0", + "@mui/x-data-grid-generator": "^7.2.0", + "@mui/x-date-pickers": "^8.11.2", + "@react-google-maps/api": "^2.20.7", + "@types/zxcvbn": "^4.4.5", + "@vis.gl/react-google-maps": "^1.5.4", + "axios": "^1.10.0", + "date-fns": "^4.1.0", + "dayjs": "^1.11.10", + "echarts": "^5.5.0", + "echarts-for-react": "^3.0.2", + "eslint-config-prettier": "^10.1.8", + "formik": "^2.4.6", + "js-cookie": "^3.0.5", + "jwt-decode": "^4.0.0", + "lucide-react": "^0.525.0", + "material-ui-popup-state": "^5.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.22.3", + "simplebar-react": "^3.2.5", + "zxcvbn": "^4.4.2" + }, + "devDependencies": { + "@iconify/react": "^4.1.1", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.57.0", + "eslint-plugin-prettier": "^5.5.3", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "typescript": "^5.2.2", + "vite": "^5.2.0", + "vite-tsconfig-paths": "^4.3.2" + } +} diff --git a/ditch-the-agent/src/axiosApi.js b/ditch-the-agent/src/axiosApi.js index 71a6191..f1f51f8 100644 --- a/ditch-the-agent/src/axiosApi.js +++ b/ditch-the-agent/src/axiosApi.js @@ -1,14 +1,14 @@ import axios from 'axios'; import Cookies from 'js-cookie'; -const baseURL = 'http://127.0.0.1:8010/api/'; -//const baseURL = 'https://backend.ditchtheagent.com/api/'; +const baseURL = import.meta.env.VITE_API_URL; +console.log(baseURL); export const axiosRealEstateApi = axios.create({ baseURL: 'https://api.realestateapi.com/v2/', headers: { 'Content-Type': 'application/json', - 'X-API-Key': 'AIMLOPERATIONSLLC-a7ce-7525-b38f-cf8b4d52ca70', + 'X-API-Key': import.meta.env.REAL_ESTATE_API_KEY, 'X-User-Id': 'UniqueUserIdentifier', }, }); diff --git a/ditch-the-agent/src/components/CategoryGridTemplate.tsx b/ditch-the-agent/src/components/CategoryGridTemplate.tsx index b141453..75afc66 100644 --- a/ditch-the-agent/src/components/CategoryGridTemplate.tsx +++ b/ditch-the-agent/src/components/CategoryGridTemplate.tsx @@ -3,7 +3,6 @@ import React, { ReactNode } from 'react'; import { Grid } from '@mui/material'; import { GenericCategory } from 'types'; - interface CategoryGridTemplateProps { categories: TCategory[]; onSelectCategory: (categoryId: string) => void; @@ -18,7 +17,7 @@ function CategoryGridTemplate({ return ( {categories.map((category) => ( - + {renderCategoryCard(category, onSelectCategory)} ))} @@ -26,4 +25,4 @@ function CategoryGridTemplate({ ); } -export default CategoryGridTemplate; \ No newline at end of file +export default CategoryGridTemplate; diff --git a/ditch-the-agent/src/components/ItemListDetailTemplate.tsx b/ditch-the-agent/src/components/ItemListDetailTemplate.tsx index 8a51497..3c27df5 100644 --- a/ditch-the-agent/src/components/ItemListDetailTemplate.tsx +++ b/ditch-the-agent/src/components/ItemListDetailTemplate.tsx @@ -1,14 +1,32 @@ // src/templates/ItemListDetailTemplate.tsx import React, { useState, useEffect, ReactNode } from 'react'; -import { Box, Grid, List, ListItem, ListItemText, Typography, Paper, Button, Stack, IconButton } from '@mui/material'; +import { + Box, + Grid, + List, + ListItem, + ListItemText, + Typography, + Paper, + Button, + Stack, + IconButton, +} from '@mui/material'; import { GenericCategory, GenericItem } from 'types'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -interface ItemListDetailTemplateProps { +interface ItemListDetailTemplateProps< + TCategory extends GenericCategory, + TItem extends GenericItem, +> { category: TCategory; items: TItem[]; onBack: () => void; - renderListItem: (item: TItem, isSelected: boolean, onSelect: (itemId: string) => void) => ReactNode; + renderListItem: ( + item: TItem, + isSelected: boolean, + onSelect: (itemId: string) => void, + ) => ReactNode; renderItemDetail: (item: TItem) => ReactNode; } @@ -22,36 +40,35 @@ function ItemListDetailTemplate(null); // Default to the first item in the list - let temp = null + let temp = null; useEffect(() => { - if (items.length > 0) { - temp = items[0].id + temp = items[0].id; setSelectedItemId(items[0].id); } else { - setSelectedItemId(null); } }, [items]); const selectedItem = selectedItemId ? items.find((item) => item.id === selectedItemId) : null; - console.log(selectedItemId, selectedItem) - + console.log(selectedItemId, selectedItem); + const handleItemSelect = (itemId: string) => { setSelectedItemId(itemId); }; return ( - + - - + - {category.name} List + + {category.name} List + {items.map((item) => ( @@ -62,12 +79,23 @@ function ItemListDetailTemplate - + {selectedItem ? ( renderItemDetail(selectedItem) ) : ( - - Select an item to view details + + + Select an item to view details + )} @@ -75,4 +103,4 @@ function ItemListDetailTemplate = ({ password }) => { + const strength = password ? zxcvbn(password).score : 0; + + const getStrengthLabel = () => { + switch (strength) { + case 0: + return 'Weak'; + case 1: + return 'Fair'; + case 2: + return 'Good'; + case 3: + return 'Strong'; + case 4: + return 'Very Strong'; + default: + return ''; + } + }; + + const getStrengthColor = () => { + switch (strength) { + case 0: + return 'error'; + case 1: + return 'warning'; + case 2: + return 'info'; + case 3: + return 'success'; + case 4: + return 'success'; + default: + return 'grey'; + } + }; + + return ( + + + + {getStrengthLabel()} + + + ); +}; + +export default PasswordStrengthChecker; diff --git a/ditch-the-agent/src/components/base/LoadingSkeleton.tsx b/ditch-the-agent/src/components/base/LoadingSkeleton.tsx index 79c1e0a..1e836e9 100644 --- a/ditch-the-agent/src/components/base/LoadingSkeleton.tsx +++ b/ditch-the-agent/src/components/base/LoadingSkeleton.tsx @@ -1,25 +1,36 @@ import { Paper, Container, Grid, Box, Typography, Skeleton } from '@mui/material'; -import { ReactElement} from 'react'; +import { ReactElement } from 'react'; const LoadingSkeleton = (): ReactElement => { - return ( - - - - - - - - - - - - + return ( + + + + + + + + + + + + + + ); +}; - - ) - -} - -export default LoadingSkeleton; \ No newline at end of file +export default LoadingSkeleton; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Bids/VendorBids.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Bids/VendorBids.tsx index f6cb8ce..428f172 100644 --- a/ditch-the-agent/src/components/sections/dashboard/Home/Bids/VendorBids.tsx +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Bids/VendorBids.tsx @@ -30,7 +30,7 @@ const VendorBidsPage: React.FC = () => { {bids.map((bid) => ( - + ))} diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Dashboard/AttorneyDashboard.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Dashboard/AttorneyDashboard.tsx index 5f7f03f..f70fab2 100644 --- a/ditch-the-agent/src/components/sections/dashboard/Home/Dashboard/AttorneyDashboard.tsx +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Dashboard/AttorneyDashboard.tsx @@ -80,7 +80,7 @@ const AttorneyDashboard = ({ account }: DashboardProps): ReactElement => { )} {/* Active Cases Card */} - + @@ -113,7 +113,7 @@ const AttorneyDashboard = ({ account }: DashboardProps): ReactElement => { {/* Upcoming Deadlines Card */} - + @@ -141,7 +141,7 @@ const AttorneyDashboard = ({ account }: DashboardProps): ReactElement => { {/* Documents to Review Card */} - + diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Dashboard/PropertyOwnerDashboard.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Dashboard/PropertyOwnerDashboard.tsx index 617cbb2..396fefd 100644 --- a/ditch-the-agent/src/components/sections/dashboard/Home/Dashboard/PropertyOwnerDashboard.tsx +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Dashboard/PropertyOwnerDashboard.tsx @@ -1,11 +1,12 @@ import { AxiosResponse } from 'axios'; import { ReactElement, useContext, useEffect, useState } from 'react'; -import { PropertiesAPI, UserAPI } from 'types'; +import { DocumentAPI, PropertiesAPI, SavedPropertiesAPI, UserAPI } from 'types'; import { axiosInstance } from '../../../../../axiosApi'; -import Grid from '@mui/material/Unstable_Grid2'; +//==import Grid from '@mui/material/Unstable_Grid2'; import { Alert, Button, + Grid, Card, CardActionArea, CardContent, @@ -26,6 +27,7 @@ import { DataGrid, GridRenderCellParams } from '@mui/x-data-grid'; import { GridColDef } from '@mui/x-data-grid'; import PropertyDetailCard from '../Property/PropertyDetailCard'; import { DashboardProps } from 'pages/home/Dashboard'; +import SavedPropertiesTable from './SavedPropertiesTable'; interface Row { id: number; @@ -35,6 +37,9 @@ const PropertyOwnerDashboard = ({ account }: DashboardProps): ReactElement => { const [properties, setProperties] = useState([]); const [numBids, setNumBids] = useState(0); const [numOffers, setNumOffers] = useState(0); + const [savedProperties, setSavedProperties] = useState([]); + const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + const [documents, setDocuments] = useState([]); const navigate = useNavigate(); useEffect(() => { @@ -69,16 +74,72 @@ const PropertyOwnerDashboard = ({ account }: DashboardProps): ReactElement => { console.log(error); } }; + const fetchSavedProperties = async () => { + try { + let expandedSavedProperties: PropertiesAPI[] = []; + const { data }: AxiosResponse = + await axiosInstance.get('/saved-properties/'); + const requests = data.map((item) => + axiosInstance.get(`/properties/${item.property}/?search=1`), + ); + const responses = await Promise.all(requests); + + expandedSavedProperties = responses.map((response) => response.data); + console.log(expandedSavedProperties); + setSavedProperties(expandedSavedProperties); + } catch (error) { + console.log(error); + } + }; + const fetchDocuments = async () => { + try { + const { data }: AxiosResponse = await axiosInstance.get('/document/'); + console.log('documents', data); + setDocuments(data); + } catch (error) { + console.log(error); + } + }; fetchProperties(); fetchOffers(); fetchBids(); + fetchSavedProperties(); + fetchDocuments(); }, []); - const handleSaveProperty = (updatedProperty: PropertiesAPI) => { - console.log('handle save. IMPLEMENT ME'); + const handleSaveProperty = async (updatedProperty: PropertiesAPI) => { + try { + const { data } = await axiosInstance.patch( + `/properties/${updatedProperty.id}/`, + { + ...updatedProperty, + owner: account.id, + }, + ); + const updatedProperties = properties.map((item) => { + if (item.id === data.id) { + return { ...item, ...data }; + } + return item; + }); + setProperties(updatedProperties); + setMessage({ type: 'success', text: 'Property has been updated' }); + setTimeout(() => setMessage(null), 3000); + } catch (error) { + setMessage({ type: 'error', text: 'Error while saving the property. Please try again' }); + setTimeout(() => setMessage(null), 3000); + } }; - const handleDeleteProperty = (propertyId: number) => { - console.log('handle delete. IMPLEMENT ME'); + const handleDeleteProperty = async (propertyId: number) => { + try { + await axiosInstance.delete(`/properties/${propertyId}/`); + setProperties((prev) => prev.filter((item) => item.id !== propertyId)); + setMessage({ type: 'success', text: 'Property has been removed' }); + setTimeout(() => setMessage(null), 3000); + } catch (error) { + setMessage({ type: 'error', text: 'Error while removing the property. Please try again' }); + setTimeout(() => setMessage(null), 3000); + } }; const documentColumns: GridColDef[] = [ @@ -122,6 +183,8 @@ const PropertyOwnerDashboard = ({ account }: DashboardProps): ReactElement => { const numSaves = properties.reduce((accum, currProperty) => { return accum + currProperty.saves; }, 0); + const savedPropertiesCardLength: number = savedProperties.length === 0 ? 6 : 12; + const documentsCardLength: number = documents.length === 0 ? 6 : 12; return ( { - - - + + {account.tier === 'basic' && ( + + + + Upgrade your account + + Unlock premium features to get more features and sell faster + + + + + + + + + + )} {/* Properties */} + {message && ( + + {message.text} + + )} + {properties.length > 0 && ( + + + + )} {properties.map((item) => ( { - + + Documents Requiring Attention - something - - - 70} rows={DocumentRows} columns={documentColumns} /> - + {documents.length === 0 ? ( + + There are no documents that require your attention at this point + + ) : ( + + 70} rows={DocumentRows} columns={documentColumns} /> + + )} - + - Video Progress - - Complete our FSBO training to maximize your sale potential - - + + Saved Properties + Keep track of the properties you have saved + - - - + + + + - - - - Upgrade your account - - Unlock premium features to get more features and sell faster - - + + {account.tier === 'premium' ? ( + + + + Video Progress + + Complete our FSBO training to maximize your sale potential + + - - - - - - - {/* - - + + + + + + ) : ( + + + Video Progress + + Upgrade to get access to FSBO educational videos + + - - + + + + + + )} - - - - - - - - - - - - - - - - - - */} ); }; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Dashboard/RealEstateAgentDashboard.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Dashboard/RealEstateAgentDashboard.tsx index 4909615..4ca8dd9 100644 --- a/ditch-the-agent/src/components/sections/dashboard/Home/Dashboard/RealEstateAgentDashboard.tsx +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Dashboard/RealEstateAgentDashboard.tsx @@ -61,7 +61,7 @@ const RealEstateAgentDashboard = ({ account }: DashboardProps): ReactElement => {/* Listings Summary Card */} - + @@ -83,7 +83,7 @@ const RealEstateAgentDashboard = ({ account }: DashboardProps): ReactElement => {/* New Offers Card */} - + @@ -114,7 +114,7 @@ const RealEstateAgentDashboard = ({ account }: DashboardProps): ReactElement => {/* Upcoming Showings Card */} - + @@ -148,7 +148,7 @@ const RealEstateAgentDashboard = ({ account }: DashboardProps): ReactElement => {/* Example of other cards */} - + diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Dashboard/SavedPropertiesTable.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Dashboard/SavedPropertiesTable.tsx new file mode 100644 index 0000000..a0062c1 --- /dev/null +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Dashboard/SavedPropertiesTable.tsx @@ -0,0 +1,97 @@ +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Typography, + Button, + Box, +} from '@mui/material'; +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { PropertiesAPI, SavedPropertiesAPI } from 'types'; + +interface SavedPropertiesTableProps { + savedProperties: PropertiesAPI[]; +} + +const SavedPropertiesTable: React.FC = ({ savedProperties }) => { + const navigate = useNavigate(); + + const getUpcomingOpenHouseDate = (property: PropertiesAPI): string => { + if (!property.open_houses || property.open_houses.length === 0) { + return 'N/A'; + } + const today = new Date(); + const futureOpenHouses = property.open_houses.filter( + (openHouse) => new Date(openHouse.listed_date) >= today, + ); + if (futureOpenHouses.length > 0) { + // Sort to get the soonest upcoming one + futureOpenHouses.sort( + (a, b) => new Date(a.listed_date).getTime() - new Date(b.listed_date).getTime(), + ); + // Format the date for display + return new Date(futureOpenHouses[0].listed_date).toLocaleDateString(); + } + return 'No upcoming dates'; + }; + + if (savedProperties.length === 0) { + return ( + + + You don't have any saved properties. + + + ); + } + + return ( + + + + + Street Address + Status + Price + Upcoming Open House + Actions + + + + {savedProperties.map((property) => { + const displayPrice = property.listed_price + ? `$${property.listed_price}` + : `$${property.market_value}`; + + const openHouseDate = getUpcomingOpenHouseDate(property); + + return ( + + {property.street} + {property.property_status} + {displayPrice} + {openHouseDate} + + + + + ); + })} + +
+
+ ); +}; + +export default SavedPropertiesTable; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Dashboard/VendorDashboard.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Dashboard/VendorDashboard.tsx index ba8f7df..67bfd3b 100644 --- a/ditch-the-agent/src/components/sections/dashboard/Home/Dashboard/VendorDashboard.tsx +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Dashboard/VendorDashboard.tsx @@ -165,7 +165,7 @@ const VendorDashboard = ({ account }: DashboardProps): ReactElement => { )} {/* Views Card */} - + @@ -189,7 +189,7 @@ const VendorDashboard = ({ account }: DashboardProps): ReactElement => { {/* Bids Card */} - + @@ -240,7 +240,7 @@ const VendorDashboard = ({ account }: DashboardProps): ReactElement => { {/* Conversations Card */} - + @@ -278,10 +278,10 @@ const VendorDashboard = ({ account }: DashboardProps): ReactElement => { */} - + - + diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Documents/AddDocumentDialog.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/AddDocumentDialog.tsx new file mode 100644 index 0000000..852f6c3 --- /dev/null +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/AddDocumentDialog.tsx @@ -0,0 +1,75 @@ +import { + Button, + Box, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Stack, + Typography, + Autocomplete, + TextField, + Paper, +} from '@mui/material'; +import { ReactElement, useState } from 'react'; + +import { PropertiesAPI, DocumentAPI, UserAPI, BidAPI } from 'types'; +import AttorneyDocumentDialog from './Dialog/AttorneyDocumentDialog'; +import PropertyOwnerDocumentDialog from './Dialog/PropertyOwnerDocumentDialog'; +import VendorDocumentDialog from './Dialog/VendorDocumentDialog'; + +export type DocumentDialogProps = { + showDialog: boolean; + closeDialog: () => void; + properties: PropertiesAPI[]; + bids: BidAPI[]; +}; + +type AddDocumentDialogProps = DocumentDialogProps & { + account: UserAPI; +}; + +const AddDocumentDialog = ({ + showDialog, + closeDialog, + properties, + bids, + account, +}: AddDocumentDialogProps): ReactElement => { + if (account.user_type === 'property_owner') { + return ( + + ); + } else if (account.user_type === 'vendor') { + return ( + + ); + } else if (account.user_type === 'attorney') { + return ( + + ); + } else { + return ( + + Woops, we have encountered an error + + ); + } +}; + +export default AddDocumentDialog; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Documents/Dialog/AttorneyDocumentDialog.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/Dialog/AttorneyDocumentDialog.tsx new file mode 100644 index 0000000..9f5319e --- /dev/null +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/Dialog/AttorneyDocumentDialog.tsx @@ -0,0 +1,50 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + MenuItem, + Select, + Typography, +} from '@mui/material'; +import { DocumentDialogProps } from '../AddDocumentDialog'; +import { ReactElement } from 'react'; + +const AttorneyDocumentDialog = ({ showDialog, closeDialog }: DocumentDialogProps): ReactElement => { + const [documentType, setDocumentType] = useState(''); + + const getDialogContent = (document_type: string) => { + if (document_type === 'offer') { + } else { + return Please select a document type; + } + }; + return ( + + Create a new document + + + {getDialogContent(documentType)} + + + + + + + ); +}; + +export default AttorneyDocumentDialog; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Documents/Dialog/HomeImprovementReciptDialogContent.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/Dialog/HomeImprovementReciptDialogContent.tsx new file mode 100644 index 0000000..a55e5e1 --- /dev/null +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/Dialog/HomeImprovementReciptDialogContent.tsx @@ -0,0 +1,231 @@ +import React, { useState, useEffect, useImperativeHandle, forwardRef } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + Button, + Autocomplete, + Box, + Typography, + Grid, + CircularProgress, + InputAdornment, + Select, + MenuItem, +} from '@mui/material'; +import { formatCurrency } from 'utils'; +import { BidAPI, PropertiesAPI, VendorAPI } from 'types'; +import { PropertyOwnerDocumentType } from './PropertyOwnerDocumentDialog'; + +export interface HomeImprovementReceiptData { + propertyId: number; + vendorId: number | null; + dateOfWork: string; + description: string; + cost: number; + receiptFile?: File | null; +} + +export interface HomeImprovementReceiptDialogContentHandle { + submitForm: () => void; +} + +interface HomeImprovementReceiptDialogProps { + onSubmit: (docType: PropertyOwnerDocumentType, receipt: HomeImprovementReceiptData) => void; + properties: PropertiesAPI[]; // The specific property for which the receipt is being submitted + vendors: VendorAPI[]; // List of available vendors for autocomplete + bids: BidAPI[]; + homeImprovmentErrors: { [key: string]: string }; + loadingVendors?: boolean; // Optional: indicate if vendors are still loading +} + +const HomeImprovementReciptDialogContent = forwardRef< + HomeImprovementReceiptDialogContentHandle, + HomeImprovementReceiptDialogProps +>(({ onSubmit, properties, vendors, bids, homeImprovmentErrors, loadingVendors = false }, ref) => { + const [selectedProperty, setSelectedProperty] = useState(null); + const [selectedBid, setSelectedBid] = useState(null); + const [selectedVendor, setSelectedVendor] = useState(null); + const [dateOfWork, setDateOfWork] = useState(''); + const [description, setDescription] = useState(''); + const [cost, setCost] = useState(''); + // const [receiptFile, setReceiptFile] = useState(null); // For file upload + + // Reset form when dialog opens/closes + useEffect(() => { + if (!open) { + setSelectedVendor(null); + setDateOfWork(''); + setDescription(''); + setCost(''); + // setReceiptFile(null); + } + }, [open]); + + const submitForm = () => { + // Basic validation + if (!selectedProperty) { + return; + } else if (!selectedProperty.id || !selectedVendor || !dateOfWork || !cost) { + return; + } + + const receiptData: HomeImprovementReceiptData = { + propertyId: selectedProperty?.id, + vendorId: selectedVendor.user.id, + dateOfWork, + description, + cost: parseFloat(cost), + // receiptFile, // Include if handling file upload + }; + + onSubmit('contractor_recipt', receiptData); + }; + + useImperativeHandle(ref, () => ({ + submitForm, + })); + + console.log(properties); + + const getVendorOptionLabel = (option: VendorAPI) => + `${option.business_name} (${option.business_type})`; + return ( + + + {selectedProperty && ( + <> + + For Property: {selectedProperty.address}, {selectedProperty.city},{' '} + {selectedProperty.state} {selectedProperty.zip_code} + + + Record details of home improvements, maintenance, or repairs. + + {bids.length > 0 ? ( + <> + + + + + { + setSelectedVendor(newValue); + }} + renderInput={(params) => ( + + {loadingVendors ? ( + + ) : null} + {params.InputAdornments?.end} + + ), + }} + /> + )} + /> + + + setDateOfWork(e.target.value)} + fullWidth + required + margin="normal" + InputLabelProps={{ + shrink: true, + }} + /> + + + setDescription(e.target.value)} + fullWidth + margin="normal" + helperText="e.g., Replaced water heater, Repaired leaky faucet, Painted living room." + /> + + + setCost(e.target.value)} + fullWidth + required + margin="normal" + InputProps={{ + startAdornment: $, + }} + /> + + {/* Uncomment and implement if you need file uploads */} + + + Upload Receipt (Optional) + + setReceiptFile(e.target.files ? e.target.files[0] : null)} + /> + + + + ) : ( + There are no bids to put the recipt against + )} + + )} + + ); +}); + +export default HomeImprovementReciptDialogContent; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Documents/Dialog/OfferDialogContent.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/Dialog/OfferDialogContent.tsx new file mode 100644 index 0000000..ece98e7 --- /dev/null +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/Dialog/OfferDialogContent.tsx @@ -0,0 +1,329 @@ +import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react'; +import { + TextField, + Button, + Autocomplete, + Box, + Typography, + Grid, + CircularProgress, + InputAdornment, +} from '@mui/material'; +import { formatCurrency } from 'utils'; // Assuming this utility function is available +import { axiosInstance } from '../../../../../../axiosApi'; // Assuming axiosInstance is configured +import { PropertiesAPI } from 'types'; // Assuming PropertiesAPI is defined in your types file +import { AxiosResponse } from 'axios'; +import { PropertyOwnerDocumentType } from './PropertyOwnerDocumentDialog'; + +// Define the shape of the offer data to be submitted +export interface OfferData { + propertyId: number | null; + offerPrice: number; + closingDate?: string; + closingDays?: number; + contingencies: string; + parent_offer?: OfferData; +} + +// Define the handle for the ref that the parent can use to interact with this component +export interface OfferDialogContentHandle { + submitForm: () => void; +} + +// Define the type for properties passed down +type OfferPropertyType = { + address: string; + marketValue: string; + property_id: number; +}; + +// Interface for props of OfferDialogContent +interface OfferFormDialogProps { + onSubmit: (docType: PropertyOwnerDocumentType, offer: OfferData) => void; // Callback function to send data to parent + offerErrors: { [key: string]: string }; +} + +const OfferDialogContent = forwardRef( + ({ onSubmit, offerErrors }, ref) => { + const [inputValue, setInputValue] = useState(''); + const [options, setOptions] = useState([]); + const [filteredProperties, setFilteredProperties] = useState([]); + const [loadingProperties, setLoadingProperties] = useState(false); + + // State for form fields + const [selectedProperty, setSelectedProperty] = useState(null); + const [offerPrice, setOfferPrice] = useState(''); + const [closingDate, setClosingDate] = useState(''); + const [closingDays, setClosingDays] = useState('30'); // Default to 30 days + const [contingencies, setContingencies] = useState(''); + + // Mortgage Calculation States + const [loanInterestRate, setLoanInterestRate] = useState('7.0'); // Annual percentage + const [loanTermYears, setLoanTermYears] = useState('30'); // Years + const [downPaymentPercentage, setDownPaymentPercentage] = useState('20'); // Percentage of offer price + + const [estimatedMonthlyPayment, setEstimatedMonthlyPayment] = useState('N/A'); + + // Handle input change for the Autocomplete component, fetching properties + const handleInputChange = async (event: React.SyntheticEvent, newInputValue: string) => { + setInputValue(newInputValue); + if (newInputValue) { + setLoadingProperties(true); + try { + // Fetch properties based on search input + const { data }: AxiosResponse = await axiosInstance.get( + `/properties/?search=${newInputValue}`, + ); + + // Map API response to OfferPropertyType + const mappedResults: OfferPropertyType[] = data.map((item) => ({ + address: item.address, + marketValue: item.market_value, + property_id: item.id, + })); + setFilteredProperties(mappedResults); + setOptions(mappedResults.map((item) => item.address)); // Set options for Autocomplete + } catch (error) { + console.error('Failed to fetch properties:', error); + setOptions([]); + setFilteredProperties([]); + } finally { + setLoadingProperties(false); + } + } else { + setOptions([]); + setFilteredProperties([]); + } + }; + + // Effect to recalculate mortgage whenever relevant inputs change + useEffect(() => { + const calculateMortgage = () => { + const price = parseFloat(offerPrice); + const interestRate = parseFloat(loanInterestRate); + const termYears = parseFloat(loanTermYears); + const downPaymentPct = parseFloat(downPaymentPercentage); + + // Validate inputs + if ( + isNaN(price) || + price <= 0 || + isNaN(interestRate) || + isNaN(termYears) || + termYears <= 0 || + isNaN(downPaymentPct) + ) { + setEstimatedMonthlyPayment('N/A'); + return; + } + + const downPaymentAmount = price * (downPaymentPct / 100); + const principalLoanAmount = price - downPaymentAmount; + + if (principalLoanAmount <= 0) { + setEstimatedMonthlyPayment(formatCurrency(0)); + return; + } + + const monthlyInterestRate = interestRate / 100 / 12; + const numberOfPayments = termYears * 12; + + let monthlyPayment = 0; + if (monthlyInterestRate === 0) { + // Handle zero interest rate scenario + monthlyPayment = principalLoanAmount / numberOfPayments; + } else { + // Standard mortgage formula + monthlyPayment = + (principalLoanAmount * + monthlyInterestRate * + Math.pow(1 + monthlyInterestRate, numberOfPayments)) / + (Math.pow(1 + monthlyInterestRate, numberOfPayments) - 1); + } + + setEstimatedMonthlyPayment(formatCurrency(monthlyPayment)); + }; + + calculateMortgage(); + }, [offerPrice, loanInterestRate, loanTermYears, downPaymentPercentage]); + + // Function to handle form submission + const submitForm = () => { + // Basic validation + if (!selectedProperty || !offerPrice) { + console.error('Validation Error: Please select a property and enter an offer price.'); + // In a real app, you would display a user-friendly error message here (e.g., Snackbar) + return; + } + + // Construct the offer data object + const formData: OfferData = { + propertyId: selectedProperty.property_id, + property: selectedProperty.property_id, + offer_price: parseFloat(offerPrice), + closing_date: closingDate || undefined, + closing_days: closingDays ? parseInt(closingDays) : undefined, + contingencies, + }; + + // Call the onSubmit prop to pass data to the parent + onSubmit('offer', formData); + }; + + // Expose the submitForm function via useImperativeHandle so parent can call it + useImperativeHandle(ref, () => ({ + submitForm, + })); + + console.log(selectedProperty); + + return ( + + {/* Offer Form Section */} + + { + // Find the selected property object from filteredProperties + const selectedAddr = filteredProperties.find((item) => item.address === newValue); + setSelectedProperty(selectedAddr || null); // Set selected property or null + }} + onInputChange={handleInputChange} + noOptionsText={'Type the address to search for'} + renderInput={(params) => ( + + {loadingProperties ? : null} + {params.InputProps.endAdornment} {/* Pass existing endAdornment */} + + ), + }} + /> + )} + /> + setOfferPrice(e.target.value)} + fullWidth + required + margin="normal" + InputProps={{ + startAdornment: $, + }} + helperText={offerErrors.offer_price} + error={!!offerErrors.offer_price} + /> + setClosingDate(e.target.value)} + fullWidth + margin="normal" + InputLabelProps={{ + shrink: true, + }} + helperText={offerErrors.closing_date} + error={!!offerErrors.closing_date} + /> + + OR + + setClosingDays(e.target.value)} + fullWidth + margin="normal" + helperText={offerErrors.closing_days} + error={!!offerErrors.closing_days} + /> + setContingencies(e.target.value)} + fullWidth + margin="normal" + helperText={offerErrors.contingencies} + error={!!offerErrors.contingencies} + /> + + + {/* Estimated Mortgage Payment Section */} + + + + Estimated Mortgage Payment + + setDownPaymentPercentage(e.target.value)} + fullWidth + margin="normal" + InputProps={{ + endAdornment: %, + }} + /> + setLoanInterestRate(e.target.value)} + fullWidth + margin="normal" + InputProps={{ + endAdornment: %, + }} + /> + setLoanTermYears(e.target.value)} + fullWidth + margin="normal" + /> + + Estimated Monthly Payment (P&I): + + {estimatedMonthlyPayment} + + + *This is an estimate for Principal & Interest only. It does not include taxes, + insurance, or HOA fees. + + + + + + ); + }, +); + +export default OfferDialogContent; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Documents/Dialog/PropertyOwnerDocumentDialog.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/Dialog/PropertyOwnerDocumentDialog.tsx new file mode 100644 index 0000000..724c270 --- /dev/null +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/Dialog/PropertyOwnerDocumentDialog.tsx @@ -0,0 +1,217 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + MenuItem, + Select, + Typography, +} from '@mui/material'; +import { DocumentDialogProps } from '../AddDocumentDialog'; +import React, { ReactElement, useState, useRef } from 'react'; +import OfferDialogContent, { OfferDialogContentHandle, OfferData } from './OfferDialogContent'; // Import the handle and data types +import SellerDisclousureDialogContent, { + SellerDisclousureDialogContentHandle, + SellerDisclousureData, +} from './SellerDisclousureDialogContent'; +import HomeImprovementReciptDialogContent, { + HomeImprovementReceiptData, + HomeImprovementReceiptDialogContentHandle, +} from './HomeImprovementReciptDialogContent'; +import { axiosInstance } from '../../../../../../axiosApi'; +// Assuming BidAPI, PropertiesAPI, VendorAPI are defined in types or available + +export type PropertyOwnerDocumentType = 'offer' | 'seller_disclosure' | 'home_improvement_receipt'; + +// Custom message box component for user feedback +const MessageBox = ({ message, onClose }: { message: string; onClose: () => void }) => ( + + Error + + {message} + + + + + +); + +const PropertyOwnerDocumentDialog = ({ + showDialog, + closeDialog, + properties, + bids, +}: DocumentDialogProps): ReactElement => { + const [documentType, setDocumentType] = useState(null); + const offerDialogRef = useRef(null); // Ref for OfferDialogContent + const sellerDisclosureRef = useRef(null); + const homeImprovementRef = useRef(null); + const [submittedOfferData, setSubmittedOfferData] = useState(null); // State to hold submitted data + const [showError, setShowError] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + const [homeImprovmentErrors, setHomeImprovementErrors] = useState<{ [key: string]: string }>({}); + const [sellerDisclosureErrors, setSellerDisclosureErrors] = useState<{ [key: string]: string }>( + {}, + ); + const [offerErrors, setOfferErrors] = useState<{ [key: string]: string }>({}); + + // Callback function to receive data from OfferDialogContent + const handleOfferSubmit = async ( + docType: PropertyOwnerDocumentType, + data: OfferData | SellerDisclousureData | HomeImprovementReceiptData, + ) => { + if (docType === 'offer') { + console.log(data); + try { + const response = await axiosInstance.post('/documents/upload/', { + document_type: docType, + ...data, + }); + if (response.error) { + setOfferErrors(error.response.data.errors); + } + closeDialog(); // Close the dialog after successful submission + } catch (error) { + console.log(error.response.data.errors); + setOfferErrors(error.response.data.errors); + } + } else if (docType === 'seller_disclosure') { + console.log(data); + try { + const response = await axiosInstance.post('/documents/upload/', { + document_type: docType, + ...data, + }); + console.log(response); + if (response.error) { + setSellerDisclosureErrors(response.error); + } + closeDialog(); // Close the dialog after successful submission + } catch (error) { + console.log(error.response.data.errors); + setSellerDisclosureErrors(error.response.data.errors); + } + + console.log('SellerDisclousureData Data Received in PropertyOwnerDocumentDialog:', data); + } else if (docType === 'home_improvement_receipt') { + console.log('HomeImprovementReceiptData Data Received in PropertyOwnerDocumentDialog:', data); + closeDialog(); // Close the dialog after successful submission + } + + // Here you would typically send this 'data' to your backend API + }; + + // Function to render the appropriate dialog content based on document type + const getDialogContent = (document_type: PropertyOwnerDocumentType | null): ReactElement => { + if (document_type === 'offer') { + return ( + + ); + } else if (document_type === 'seller_disclosure') { + // If SellerDisclousureDialogContent also needs to pass data, it would need + // a similar ref and onSubmit prop setup. + return ( + + ); + } else if (document_type === 'home_improvement_receipt') { + // Similar to offer, if HomeImprovementReciptDialogContent needs to pass data. + return ( + + ); + } else { + return Please select a document type; + } + }; + + // Store the currently active dialog content as a React element + const dialogContentElement = getDialogContent(documentType); + + // Function to handle the "Create" button click in the parent dialog + const handleCreate = async () => { + if (documentType === 'offer') { + if (offerDialogRef.current) { + // Trigger the submitForm method exposed by OfferDialogContent via the ref + offerDialogRef.current.submitForm(); + } else { + setErrorMessage('Offer dialog content not ready. Please select a document type.'); + setShowError(true); + } + } else if (documentType === 'seller_disclosure') { + if (sellerDisclosureRef.current) { + sellerDisclosureRef.current.submitForm(); + } else { + setErrorMessage('Seller Disclosure submission not implemented yet.'); + setShowError(true); + } + } else if (documentType === 'contractor_recipt') { + // Placeholder for home improvement receipt submission + if (homeImprovementRef.current) { + homeImprovementRef.current.submitForm(); + } else { + setErrorMessage('Home Improvement Recipt submission not implemented yet.'); + setShowError(true); + } + } else { + // If no document type is selected + setErrorMessage('Please select a document type before creating.'); + setShowError(true); + } + }; + + return ( + + Create a new document + + + {dialogContentElement} {/* Render the selected dialog content */} + + + + + + {/* Render the message box if there's an error */} + {showError && setShowError(false)} />} + + ); +}; + +export default PropertyOwnerDocumentDialog; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Documents/Dialog/SellerDisclousureDialogContent.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/Dialog/SellerDisclousureDialogContent.tsx new file mode 100644 index 0000000..d4952a9 --- /dev/null +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/Dialog/SellerDisclousureDialogContent.tsx @@ -0,0 +1,358 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + MenuItem, + Select, + Typography, + Grid, + TextField, + Checkbox, + FormControlLabel, + FormGroup, + Box, + FormControl, + FormHelperText, +} from '@mui/material'; +import { DocumentDialogProps } from '../AddDocumentDialog'; +import { ReactElement, forwardRef, useImperativeHandle, useState } from 'react'; +import { PropertiesAPI } from 'types'; +import { PropertyOwnerDocumentType } from './PropertyOwnerDocumentDialog'; + +export interface SellerDisclousureData { + generalDefects: string; + roofCondition: string; + roofAge: string; + knownRoofLeaks: boolean; + plumbingIssues: string; + electricalIssues: string; + hvacCondition: string; + hvacAge: string; + knownLeadPaint: boolean; + knownAsbestos: boolean; + knownRadon: boolean; + pastWaterDamage: string; + structuralIssues: string; + neighborhoodNuisances: string; + propertyLineDisputes: string; + appliancesIncluded: string; +} + +// Define the handle for the ref that the parent can use to interact with this component +export interface SellerDisclousureDialogContentHandle { + submitForm: () => void; +} + +// Interface for props of OfferDialogContent +interface SellerDisclousureFormDialogProps { + onSubmit: (docType: PropertyOwnerDocumentType, disclosure: SellerDisclousureData) => void; // Callback function to send data to parent + properties: PropertiesAPI[]; + sellerDisclosureErrors: { [key: string]: string }; +} + +const SellerDisclousureDialogContent = forwardRef< + SellerDisclousureDialogContentHandle, + SellerDisclousureFormDialogProps +>(({ onSubmit, properties, sellerDisclosureErrors }, ref) => { + const [selectedProperty, setSelectedProperty] = useState(null); + + const [generalDefects, setGeneralDefects] = useState(''); + const [roofCondition, setRoofCondition] = useState(''); + const [roofAge, setRoofAge] = useState(''); + const [knownRoofLeaks, setKnownRoofLeaks] = useState(false); + const [plumbingIssues, setPlumbingIssues] = useState(''); + const [electricalIssues, setElectricalIssues] = useState(''); + const [hvacCondition, setHvacCondition] = useState(''); + const [hvacAge, setHvacAge] = useState(''); + const [knownLeadPaint, setKnownLeadPaint] = useState(false); + const [knownAsbestos, setKnownAsbestos] = useState(false); + const [knownRadon, setKnownRadon] = useState(false); + const [pastWaterDamage, setPastWaterDamage] = useState(''); + const [structuralIssues, setStructuralIssues] = useState(''); + const [neighborhoodNuisances, setNeighborhoodNuisances] = useState(''); + const [propertyLineDisputes, setPropertyLineDisputes] = useState(''); + const [appliancesIncluded, setAppliancesIncluded] = useState(''); + + const submitForm = () => { + const formData: SellerDisclousureData = { + property: selectedProperty?.id, + general_defects: generalDefects, + roof_condition: roofCondition, + roof_age: roofAge, + known_roof_leaks: knownRoofLeaks, + plumbing_issues: plumbingIssues, + electrical_issues: electricalIssues, + hvac_condition: hvacCondition, + hvac_age: hvacAge, + known_lead_paint: knownLeadPaint, + known_asbestos: knownAsbestos, + known_radon: knownRadon, + past_water_damage: pastWaterDamage, + structural_issues: structuralIssues, + neighborhood_nuisances: neighborhoodNuisances, + property_line_disputes: propertyLineDisputes, + appliances_included: appliancesIncluded, + }; + onSubmit('seller_disclosure', formData); + }; + + useImperativeHandle(ref, () => ({ + submitForm, + })); + + console.log(sellerDisclosureErrors); + + return ( + <> + + {selectedProperty && ( + <> + + For Property: {selectedProperty.address}, {selectedProperty.city},{' '} + {selectedProperty.state} {selectedProperty.zip_code} + + + Please disclose any known material defects or information about the property. Honesty is + crucial to avoid potential legal issues. + + + + + setGeneralDefects(e.target.value)} + fullWidth + margin="normal" + helperText={`Describe any general issues not covered elsewhere (e.g., drainage problems, pest infestations). ${sellerDisclosureErrors.general_defects}`} + error={!!sellerDisclosureErrors.general_defects} + /> + + + {/* Property Systems Section */} + + + Roof Information + + setRoofCondition(e.target.value)} + fullWidth + margin="normal" + error={!!sellerDisclosureErrors.roof_condition} + helperText={sellerDisclosureErrors.roof_condition} + /> + setRoofAge(e.target.value)} + fullWidth + margin="normal" + error={!!sellerDisclosureErrors.roof_age} + helperText={sellerDisclosureErrors.roof_age} + /> + + setKnownRoofLeaks(e.target.checked)} + /> + } + label="Known Past or Present Roof Leaks?" + /> + {sellerDisclosureErrors.known_roof_leaks} + + + + + + Major Systems + + setPlumbingIssues(e.target.value)} + fullWidth + margin="normal" + error={!!sellerDisclosureErrors.plumbing_issues} + helperText={sellerDisclosureErrors.plumbing_issues} + /> + setElectricalIssues(e.target.value)} + fullWidth + margin="normal" + error={!!sellerDisclosureErrors.electrical_issues} + helperText={sellerDisclosureErrors.electrical_issues} + /> + setHvacCondition(e.target.value)} + fullWidth + margin="normal" + error={!!sellerDisclosureErrors.hvac_condition} + helperText={sellerDisclosureErrors.hvac_condition} + /> + setHvacAge(e.target.value)} + fullWidth + margin="normal" + error={!!sellerDisclosureErrors.hvac_age} + helperText={sellerDisclosureErrors.hvac_age} + /> + + + {/* Environmental & Other Issues */} + + + Environmental & Other Issues + + + + setKnownLeadPaint(e.target.checked)} + /> + } + label="Known Lead-Based Paint?" + /> + {sellerDisclosureErrors.known_lead_paint} + + + + setKnownAsbestos(e.target.checked)} + /> + } + label="Known Asbestos?" + /> + {sellerDisclosureErrors.known_asbestos} + + + setKnownRadon(e.target.checked)} + /> + } + label="Known Radon?" + /> + {sellerDisclosureErrors.known_radon} + + + setPastWaterDamage(e.target.value)} + fullWidth + margin="normal" + helperText={`Describe location, cause, and remediation if applicable. ${sellerDisclosureErrors.past_water_damage}`} + error={!!sellerDisclosureErrors.past_water_damage} + /> + setStructuralIssues(e.target.value)} + fullWidth + margin="normal" + error={!!sellerDisclosureErrors.structural_issues} + helperText={sellerDisclosureErrors.structural_issues} + /> + setNeighborhoodNuisances(e.target.value)} + fullWidth + margin="normal" + error={!!sellerDisclosureErrors.neighborhood_nuisances} + helperText={sellerDisclosureErrors.neighborhood_nuisances} + /> + setPropertyLineDisputes(e.target.value)} + fullWidth + margin="normal" + error={!!sellerDisclosureErrors.past_water_damage} + helperText={sellerDisclosureErrors.past_water_damage} + /> + + + {/* Appliances Included */} + + + Appliances Included in Sale + + setAppliancesIncluded(e.target.value)} + fullWidth + margin="normal" + placeholder="e.g., Refrigerator, Washer, Dryer, Dishwasher..." + error={!!sellerDisclosureErrors.appliances_included} + helperText={sellerDisclosureErrors.appliances_included} + /> + + + + + + By submitting this form, you acknowledge that the information provided is accurate to + the best of your knowledge and belief. + + + + )} + + ); +}); + +export default SellerDisclousureDialogContent; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Documents/Dialog/VendorDocumentDialog.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/Dialog/VendorDocumentDialog.tsx new file mode 100644 index 0000000..a4f7d09 --- /dev/null +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/Dialog/VendorDocumentDialog.tsx @@ -0,0 +1,13 @@ +import { Dialog, Typography } from '@mui/material'; +import { DocumentDialogProps } from '../AddDocumentDialog'; +import { ReactElement } from 'react'; + +const VendorDocumentDialog = ({ showDialog, closeDialog }: DocumentDialogProps): ReactElement => { + return ( + + Show the Vendor dialog + + ); +}; + +export default VendorDocumentDialog; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Documents/DocumentManager.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/DocumentManager.tsx new file mode 100644 index 0000000..96873f3 --- /dev/null +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/DocumentManager.tsx @@ -0,0 +1,320 @@ +// src/components/DocumentManager.tsx + +import React, { useState, useEffect, useContext } from 'react'; +import { useSearchParams } from 'react-router-dom'; + +import { + Button, + List, + ListItem, + ListItemText, + Typography, + Box, + Grid, + Paper, + Container, + Stack, +} from '@mui/material'; +import { AxiosResponse } from 'axios'; +import { BidAPI, DocumentAPI, PropertiesAPI } from 'types'; +import { axiosInstance } from '../../../../../axiosApi'; +import { AccountContext } from 'contexts/AccountContext'; +import DashboardLoading from '../Dashboard/DashboardLoading'; +import DashboardErrorPage from '../Dashboard/DashboardErrorPage'; + +import ArticleIcon from '@mui/icons-material/Article'; +import { formatTimestamp } from 'utils'; +import AddDocumentDialog from './AddDocumentDialog'; +import SellerDisclosureDisplay from './SellerDisclosureDisplay'; +import { PropertyOwnerDocumentType } from './Dialog/PropertyOwnerDocumentDialog'; +import HomeImprovementReceiptDisplay from './HomeImprovementReceiptDisplay'; +import OfferDisplay from './OfferDisplay'; +import OfferNegotiationHistory from './OfferNegotiationHistory'; + +interface DocumentManagerProps {} + +const getDocumentTitle = (docType: PropertyOwnerDocumentType) => { + if (docType === 'seller_disclosure') { + return 'Seller Disclosure'; + } else if (docType === 'offer_letter') { + return 'Offer'; + } else if (docType === 'home_improvement_receipt') { + return 'Home Improvement Receipt'; + } else { + return docType; + } +}; + +const isMyTypeDocument = (upload_by: number, account_id: number, document_type: string) => { + console.log(upload_by, account_id, document_type); + if (document_type === 'offer_letter') { + return !(upload_by === account_id); + } else if (document_type === 'seller_disclosure') { + return upload_by === account_id; + } +}; + +const DocumentManager: React.FC = ({}) => { + const { account, accountLoading } = useContext(AccountContext); + const [searchParams] = useSearchParams(); + const [documents, setDocuments] = useState([]); + const [properties, setProperties] = useState([]); + const [bids, setBids] = useState([]); + const [selectedDocument, setSelectedDocument] = useState(null); + const [showDialog, setShowDialog] = useState(false); + const [selectedPropertyForDocument, setSelectedPropertyForDocument] = + useState(null); + + const closeDialog = () => { + setShowDialog(false); + }; + + useEffect(() => { + const fetchDocuments = async () => { + try { + const { data }: AxiosResponse = await axiosInstance.get('/document/'); + setDocuments(data); + } catch (error) { + console.error('Failed to fetch documents:', error); + } + }; + const fetchProperties = async () => { + try { + const { data }: AxiosResponse = await axiosInstance.get('/properties/'); + setProperties(data); + } catch (error) { + console.error('Failed to fetch properties:', error); + } + }; + const fetchBids = async () => { + try { + const { data }: AxiosResponse = await axiosInstance.get('/bids/'); + setBids(data); + } catch (error) { + console.error('Failed to fetch properties'); + } + }; + fetchDocuments(); + fetchProperties(); + fetchBids(); + }, []); + + useEffect(() => { + const selectedDocumentId = searchParams.get('selectedDocument'); + if (selectedDocumentId) { + fetchDocument(parseInt(selectedDocumentId, 10)); + } + }, [searchParams]); + + useEffect(() => { + const fetchProperty = async () => { + console.log(account.id); + console.log(selectedDocument?.uploaded_by); + console.log( + isMyTypeDocument(selectedDocument?.uploaded_by, account.id, selectedDocument.document_type), + ); + const url = isMyTypeDocument( + selectedDocument?.uploaded_by, + account.id, + selectedDocument.document_type, + ) + ? `/properties/${selectedDocument.property}/` + : `/properties/${selectedDocument.property}/?search=1`; + try { + const { data }: AxiosResponse = await axiosInstance.get(url); + setSelectedPropertyForDocument(data); + } catch (error) { + try { + const other_url = + url === `/properties/${selectedDocument.property}/` + ? `/properties/${selectedDocument.property}/?search=1` + : `/properties/${selectedDocument.property}/`; + const { data }: AxiosResponse = await axiosInstance.get(other_url); + setSelectedPropertyForDocument(data); + } catch (error) {} + } + }; + + fetchProperty(); + }, [selectedDocument]); + + console.log(documents); + + const fetchDocument = async (documentId: number) => { + try { + const response = await axiosInstance.get(`/documents/retrieve/?docId=${documentId}`); + if (response?.data) { + setSelectedDocument(response.data); + } + } catch (error) { + setSelectedDocument(null); + console.error('Failed to fetch document:', error); + } + }; + + const getDocumentPaneComponent = (selectedDocument: DocumentAPI) => { + console.log(selectedDocument?.document_type); + if (!selectedDocument) { + return ( + + + Select a document to view + + Click on a document from the left panel to get started. + + + ); + } else if (selectedDocument.document_type === 'seller_disclosure') { + console.log(selectedDocument); + return ( + + ); + } else if (selectedDocument.document_type === 'home_improvement_receipt') { + return ( + + ); + } else if (selectedDocument.document_type === 'offer_letter') { + return ( + + // + ); + } else { + return Not sure what this is; + } + }; + + if (accountLoading) { + return ; + } else if (!account) { + return ; + } + + return ( + + + + {/* Left Panel: Document List */} + + + + + Documents + + + + + + {documents.length === 0 ? ( + + + There are no documents yet. + + ) : ( + documents + .sort( + (a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(), + ) + .map((document) => ( + fetchDocument(document.id)} + sx={{ py: 1.5, px: 2 }} + > + + {getDocumentTitle(document.document_type)} +
+ } + secondary={ + + + {document.description} + + + {formatTimestamp(document.updated_at)} + + + } + /> + + )) + )} + +
+ {/* Right Panel: Offer Detail */} + + {getDocumentPaneComponent(selectedDocument)} + +
+ + + + ); +}; + +export default DocumentManager; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Documents/HomeImprovementReceiptDisplay.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/HomeImprovementReceiptDisplay.tsx new file mode 100644 index 0000000..fa2e211 --- /dev/null +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/HomeImprovementReceiptDisplay.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import { + Box, + Typography, + Card, + CardContent, + Divider, + List, + ListItem, + ListItemText, + Grid, +} from '@mui/material'; + +// Interfaces for consistency across components +interface Property { + id: number; + address: string; + city: string; + state: string; + zip_code: string; +} + +interface Vendor { + user: { + id: number; + email: string; + first_name: string; + last_name: string; + }; + business_name: string; + business_type: string; +} + +interface HomeImprovementReceiptData { + propertyId: number; + vendor: Vendor; // Assuming the full vendor object is passed for display + dateOfWork: string; + description: string; + cost: number; + // receiptFile?: string; // Assume URL string for display +} + +interface HomeImprovementReceiptDisplayProps { + receiptData: HomeImprovementReceiptData; + property: Property; +} + +const formatCurrency = (value: number): string => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(value); +}; + +const HomeImprovementReceiptDisplay: React.FC = ({ + receiptData, + property, +}) => { + if (!receiptData || !property) { + return ( + + + No receipt information available to display. + + + ); + } + + const { vendor, dateOfWork, description, cost } = receiptData; + + return ( + + + + Home Improvement Receipt + + + For Property: {property.address}, {property.city}, {property.state} {property.zip_code} + + + + + + + + + + + + + + + + + + + + + + + + + + + Description of Work Performed + + + {description || 'No description provided.'} + + + + + + + ); +}; + +export default HomeImprovementReceiptDisplay; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Documents/OfferDisplay.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/OfferDisplay.tsx new file mode 100644 index 0000000..4806e1b --- /dev/null +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/OfferDisplay.tsx @@ -0,0 +1,148 @@ +import React from 'react'; +import { + Box, + Typography, + Card, + CardContent, + Divider, + Grid, + List, + ListItem, + ListItemText, + CardActions, + Button, +} from '@mui/material'; +import { axiosInstance } from '../../../../../axiosApi'; + +// Interfaces for consistency across components +interface Property { + id: number; + address: string; + city: string; + state: string; + zip_code: string; +} + +interface OfferData { + propertyId: number; + offer_price: number; + closing_date?: string; + closing_days?: number; + contingencies: string; + status: string; +} + +interface OfferDisplayProps { + offerData: OfferData; + property: Property; + isPropertyOwner: boolean; + onAccept: () => void; + onReject: () => void; + onCounter: () => void; +} + +const formatCurrency = (value: number): string => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(value); +}; + +const OfferDisplay: React.FC = ({ + offerData, + property, + isPropertyOwner, + onAccept, + onReject, + onCounter, +}) => { + if (!offerData || !property) { + return ( + + + No offer information available to display. + + + ); + } + + const { offer_price, closing_date, closing_days, contingencies, status } = offerData; + + const getClosingText = () => { + if (closing_days) { + return `${closing_days} days`; + } + if (closing_date) { + try { + const formattedDate = new Date(closing_date).toLocaleDateString(); + return `By ${formattedDate}`; + } catch (error) { + console.error('Invalid closingDate format:', error); + return 'Invalid Date'; + } + } + return 'Not specified'; + }; + + const showActions = + (isPropertyOwner && (status === 'submitted' || status === 'pending')) || + (!isPropertyOwner && status === 'countered'); + + return ( + + + + Residential House Offer + + + For Property: {property.address}, {property.city}, {property.state} {property.zip_code} + + + Status: {status.charAt(0).toUpperCase() + status.slice(1)} + + + + + + + + + + + + + + + + + + + + + + + Other Contingencies + + + {contingencies || 'No contingencies listed.'} + + + + + + {showActions && ( + + + + + + )} + + ); +}; + +export default OfferDisplay; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Documents/OfferNegotiationHistory.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/OfferNegotiationHistory.tsx new file mode 100644 index 0000000..9f941e5 --- /dev/null +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/OfferNegotiationHistory.tsx @@ -0,0 +1,177 @@ +import React, { useState, useEffect } from 'react'; +import { + Box, + Typography, + CircularProgress, + Accordion, + AccordionSummary, + AccordionDetails, +} from '@mui/material'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { axiosInstance } from '../../../../../axiosApi'; +import OfferDisplay, { OfferData } from './OfferDisplay'; +import { DocumentAPI } from 'types'; + +// Interfaces for consistency across components +interface Property { + id: number; + address: string; + city: string; + state: string; + zip_code: string; +} + +interface OfferNegotiationHistoryProps { + property: Property; + isPropertyOwner: boolean; + offerData: DocumentAPI; +} + +const OfferNegotiationHistory: React.FC = ({ + property, + isPropertyOwner, + offerData, +}) => { + const [offer, setOffer] = useState(offerData); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [expandedAccordion, setExpandedAccordion] = useState(false); + + const [history, setHistory] = useState([]); + // This useEffect builds the negotiation history + useEffect(() => { + let currentOffer = offerData.sub_document; + const negotiationHistory: OfferData[] = []; + + // Traverse the parent chain until there is no parent + while (currentOffer) { + negotiationHistory.push(currentOffer); + currentOffer = currentOffer.parent_offer; + } + console.log(negotiationHistory); + const reversedHistory = negotiationHistory.reverse(); + + setHistory(reversedHistory); + if (reversedHistory.length > 0) { + setExpandedAccordion(reversedHistory[reversedHistory.length - 1].id); + } + }, [offerData]); + + const handleChange = (panelId: number) => (event: React.SyntheticEvent, isExpanded: boolean) => { + console.log(panelId, isExpanded); + setExpandedAccordion(isExpanded ? panelId : false); + }; + + // const fetchOffers = async () => { + // setLoading(true); + // setError(null); + // try { + // // Assuming a new endpoint or a modified one that returns all offers for a property. + // // You may need to create this in your Django views. + // const response = await axiosInstance.get(`/documents/retrieve/?docId=${property.id}/`); + // setOffers(response.data); + // } catch (err) { + // console.error('Failed to fetch offers:', err); + // setError('Failed to load offers. Please try again later.'); + // } finally { + // setLoading(false); + // } + // }; + // console.log(property); + // useEffect(() => { + // if (property) { + // fetchOffers(); + // } + // }, [property]); + + const handleOfferAction = async ( + documentId: number, + action: 'accept' | 'reject' | 'counter', + newOfferData?: any, + ) => { + setLoading(true); + try { + const payload = { + document_id: documentId, + action: action, + ...newOfferData, // Pass new offer data for a counter-offer + }; + await axiosInstance.patch('/documents/retrieve/', payload); + //await fetchOffers(); // Re-fetch offers to see the updated status + } catch (err) { + console.error(`Failed to perform action '${action}':`, err); + setError('Failed to update offer status. Please try again.'); + } finally { + setLoading(false); + } + }; + + if (loading) { + return ( + + + + ); + } + + if (error) { + return ( + + + {error} + + + ); + } + + // if (offers.length === 0) { + // return ( + // + // + // No offers have been submitted for this property. + // + // + // ); + // } + + return ( + + + Negotiation History + + + {history.length > 0 ? ( + history.map((offer, index) => ( + + }> + + Offer - {offer.status.toUpperCase()} - ${offer.offer_price} + + + + handleOfferAction(offer.id, 'accept')} + onReject={() => handleOfferAction(offer.id, 'reject')} + onCounter={() => { + const newPrice = offer.offer_price * 0.95; + handleOfferAction(offer.id, 'counter', { offer_price: newPrice }); + }} + /> + + + )) + ) : ( + No negotiation history available. + )} + + ); +}; + +export default OfferNegotiationHistory; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Documents/SellerDisclosureDisplay.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/SellerDisclosureDisplay.tsx new file mode 100644 index 0000000..4459b27 --- /dev/null +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/SellerDisclosureDisplay.tsx @@ -0,0 +1,230 @@ +import React from 'react'; +import { + Box, + Typography, + Card, + CardContent, + Divider, + Grid, + List, + ListItem, + ListItemText, +} from '@mui/material'; + +// Reuse the interfaces from the SellerDisclosureDialog for consistency +interface Property { + id: number; + address: string; + city: string; + state: string; + zip_code: string; +} + +interface SellerDisclosureData { + propertyId: number; + general_defects: string; + roof_condition: string; + roof_age: number | null; + known_roof_leaks: boolean; + plumbing_issues: string; + electrical_issues: string; + hvac_condition: string; + hvac_age: number | null; + known_lead_paint: boolean; + known_asbestos: boolean; + known_radon: boolean; + past_water_damage: string; + structural_issues: string; + neighborhood_nuisances: string; + property_line_disputes: string; + appliances_included: string; +} + +interface SellerDisclosureDisplayProps { + disclosureData: SellerDisclosureData; + property: Property; // The property associated with this disclosure +} + +const SellerDisclosureDisplay: React.FC = ({ + disclosureData, + property, +}) => { + console.log(disclosureData); + if (!disclosureData || !property) { + return ( + + + No disclosure information available to display. + + + ); + } + + // Helper to format boolean values + const formatBoolean = (value: boolean) => (value ? 'Yes' : 'No'); + + const { + general_defects, + roof_condition, + roof_age, + known_roof_leaks, + plumbing_issues, + electrical_issues, + hvac_condition, + hvac_age, + known_lead_paint, + known_asbestos, + known_radon, + past_water_damage, + structural_issues, + neighborhood_nuisances, + property_line_disputes, + appliances_included, + } = disclosureData; + + return ( + + + + Seller's Property Disclosure + + + For Property: {property.address}, {property.city}, {property.state} {property.zip_code} + + + + + {/* General Information */} + + + General Information + + + + + + + + + + + + + + {/* Property Systems */} + + + Property Systems + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Environmental & Other Issues */} + + + Environmental & Other Issues + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default SellerDisclosureDisplay; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Education/CategoryGrid.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Education/CategoryGrid.tsx index 1696b02..dd33cac 100644 --- a/ditch-the-agent/src/components/sections/dashboard/Home/Education/CategoryGrid.tsx +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Education/CategoryGrid.tsx @@ -13,7 +13,7 @@ const CategoryGrid: React.FC = ({ categories, onSelectCategor return ( {categories.map((category) => ( - + ))} @@ -21,4 +21,4 @@ const CategoryGrid: React.FC = ({ categories, onSelectCategor ); }; -export default CategoryGrid; \ No newline at end of file +export default CategoryGrid; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Education/EducationInfo.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Education/EducationInfo.tsx index 6051db4..6339794 100644 --- a/ditch-the-agent/src/components/sections/dashboard/Home/Education/EducationInfo.tsx +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Education/EducationInfo.tsx @@ -1,205 +1,160 @@ +import { useState, useEffect, ReactElement } from 'react'; +import { + Card, + CardContent, + Stack, + Typography, + LinearProgress, + Box, + Grid, + Alert, +} from '@mui/material'; -import { ReactElement } from 'react'; -import { Box, Card, CardContent, CardMedia, Divider, LinearProgress, Stack, Typography } from '@mui/material'; -import { DataGrid, GridRenderCellParams } from '@mui/x-data-grid'; -import { useNavigate } from 'react-router-dom' -import { renderProgress } from '@mui/x-data-grid-generator'; -import { GridColDef } from '@mui/x-data-grid'; +import { axiosInstance } from '../../../../../axiosApi'; +import { VideoProgressAPI } from 'types'; -type EducationInfoProps = { - title: string; +interface CategoryProgress { + categoryName: string; + totalProgress: number; + videoCount: number; + averageProgress: number; } -interface Row { - id: number; - task: string; - progress: number; // Value from 0 to 100 for the progress bar -} -export const EducationInfoCards = () => { - return( - - +interface EducationInfoCardProps { + category: string; + progress: number; + totalVideos: number; + completedVideos: number; +} +const EducationInfoCard = ({ + category, + progress, + totalVideos, + completedVideos, +}: EducationInfoCardCardProps): ReactElement => { + return ( + + + + + {category} + + + + {completedVideos} of {totalVideos} videos complete + - ) -} - -const EducationInfo = ({ title }: EducationInfoProps): ReactElement => { - - const navigate = useNavigate(); - - const columns: GridColDef[] = [ - { - field: 'id', - headerName: 'ID' - }, - { - field: 'title', - headerName: 'Title', - flex: 1, - }, - - { - field: 'category', - headerName: 'Category', - flex: 1, - }, - - { - field: 'progress', - headerName: 'Progress', - flex: 1, - renderCell: (params: GridRenderCellParams) => { - const progressValue = params.value; // Access the progress value from the row data - - return ( - - - - - - {`${progressValue}%`} - - - ); - }, - }, - { - field: 'status', - headerName: 'Status', - flex: 1, - }, - - ] - - const rows = [ - { - id: 1, - title: "How to Research Comparable Properties Like a Pro", - category: "Pricing Strategy", - progress: 100, - status: "COMPLETED", - }, - { - id: 2, - title: "Understanding Price Per Square Foot in Your Neighborhood", - category: "Pricing Strategy", - progress: 100, - status: "COMPLETED", - }, - { - id: 3, - title: "Psychological Pricing: Why $399,900 Works Better Than $400,000", - category: "Pricing Strategy", - progress: 100, - status: "COMPLETED", - }, - { - id: 4, - title: "When and How to Adjust Your Asking Price", - category: "Pricing Strategy", - progress: 100, - status: "COMPLETED", - }, - { - id: 5, - title: "Handling Lowball Offers: Strategies That Work", - category: "Pricing Strategy", - progress: 100, - status: "COMPLETED", - }, - { - id: 6, - title: "The Ultimate Home Staging Checklist for FSBO Sellers", - category: "Property Preparation", - progress: 90, - status: "IN_PROGRESS", - }, - { - id: 7, - title: "DIY Curb Appeal Upgrades Under $500", - category: "Property Preparation", - progress: 80, - status: "IN_PROGRESS", - }, - { - id: 8, - title: "Decluttering Secrets for Faster Sales", - category: "Property Preparation", - progress: 5, - status: "IN_PROGRESS", - }, - { - id: 9, - title: "Professional Photography Tips Using Just Your Smartphone", - category: "Property Preparation", - progress: 50, - status: "IN_PROGRESS", - }, - { - id: 10, - title: "Deep Cleaning Checklist Before Listing", - category: "Property Preparation", - progress: 50, - status: "IN_PROGRESS", - }, - { - id: 11, - title: "How to stage a home", - category: "", - progress: 0, - status: "NOT_STARTED", - }, - ] - return( - ({ - boxShadow: theme.shadows[4], - width: 1, - height: 'auto', - })} - > - - - - {title} - - - - - theme.shadows[4]} - height={1} - > - 70} - rows={rows} - columns={columns} - onRowClick={(event) => navigate('lesson')} - /> - - - - - - - - - + - ) + ); +}; -} +export const EducationInfoCards = () => { + const [videoProgressData, setVideoProgressData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); -export default EducationInfo; \ No newline at end of file + useEffect(() => { + // This is a mock function. Replace with your actual API call. + const fetchVideoProgress = async (): Promise => { + try { + const { data } = await axiosInstance.get(`/videos/progress/`); + + return data; + } catch (error) { + return []; + } + }; + + const loadData = async () => { + try { + setLoading(true); + const data = await fetchVideoProgress(); + setVideoProgressData(data); + } catch (err) { + setError('Failed to fetch video progress data.'); + console.error(err); + } finally { + setLoading(false); + } + }; + + loadData(); + }, []); + + const getCategoryProgress = () => { + if (!videoProgressData) return {}; + + const categories: { [key: string]: CategoryProgress } = {}; + + videoProgressData.forEach((item) => { + // Access the category name from the nested object + const categoryName = item.video.category?.name; + if (!categoryName) return; // Skip items without a category name + + if (!categories[categoryName]) { + categories[categoryName] = { + categoryName: categoryName, + totalProgress: 0, + videoCount: 0, + averageProgress: 0, + }; + } + categories[categoryName].totalProgress += item.progress; + categories[categoryName].videoCount++; + }); + + // Calculate average progress for each category + for (const key in categories) { + const categoryInfo = categories[key]; + categoryInfo.averageProgress = Math.round( + categoryInfo.totalProgress / categoryInfo.videoCount, + ); + } + + return categories; + }; + + const categoryProgressData = getCategoryProgress(); + + if (loading) { + return Loading progress...; + } + + if (error) { + return {error}; + } + if (videoProgressData.length === 0) { + return There are no videos yet; + } else { + return ( + + {Object.values(categoryProgressData).map((data, index) => ( + + + item.video.category?.name === data.categoryName && item.progress === 100, + ).length + } + /> + + ))} + + ); + } +}; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Education/VideoCategoryCard.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Education/VideoCategoryCard.tsx index 48962be..fd5ed91 100644 --- a/ditch-the-agent/src/components/sections/dashboard/Home/Education/VideoCategoryCard.tsx +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Education/VideoCategoryCard.tsx @@ -1,25 +1,25 @@ // src/components/VideoApp/VideoCategoryCard.tsx import React from 'react'; -import { Card, CardContent, CardMedia, Typography, Button, LinearProgress, Box } from '@mui/material'; +import { + Card, + CardContent, + CardMedia, + Typography, + Button, + LinearProgress, + Box, +} from '@mui/material'; import { VideoCategory } from 'types'; - - interface VideoCategoryCardProps { category: VideoCategory; onSelectCategory: (categoryId: string) => void; // Now uses categoryId } const VideoCategoryCard: React.FC = ({ category, onSelectCategory }) => { - console.log(category) return ( - + {category.name} @@ -28,9 +28,20 @@ const VideoCategoryCard: React.FC = ({ category, onSelec {category.description} - {`${category.completedVideos}/${category.totalVideos} videos completed`} - - {`${category.categoryProgress.toFixed(0)}%`} + {`${category.completedVideos}/${category.totalVideos} videos completed`} + + {`${category.categoryProgress.toFixed(0)}%`} @@ -42,4 +53,4 @@ const VideoCategoryCard: React.FC = ({ category, onSelec ); }; -export default VideoCategoryCard; \ No newline at end of file +export default VideoCategoryCard; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Education/VideoPlayer.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Education/VideoPlayer.tsx index baa9d52..47631b6 100644 --- a/ditch-the-agent/src/components/sections/dashboard/Home/Education/VideoPlayer.tsx +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Education/VideoPlayer.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; import { Box, Typography, Paper, Tooltip, IconButton } from '@mui/material'; import { AccountContext } from 'contexts/AccountContext'; -import {axiosInstance} from '../../../../../axiosApi'; +import { axiosInstance } from '../../../../../axiosApi'; import { VideoItem, VideoProgressAPI } from 'types'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import PauseIcon from '@mui/icons-material/Pause'; @@ -20,22 +20,34 @@ interface VideoProgress { progress: number; } - interface VideoPlayerProps { video: VideoItem; + updateVideoItem: (time: number, completed: boolean, progress: number, videoId: number) => void; } -const VideoPlayer: React.FC = ({ video }) => { - const {account, accountLoading} = useContext(AccountContext); - +const VideoPlayer: React.FC = ({ video, updateVideoItem }) => { + const { account, accountLoading } = useContext(AccountContext); + if (!video || accountLoading) { return ( - - No video selected + + + No video selected + ); } - // + + // const videoRef = useRef(null); const [currentTime, setCurrentTime] = useState(0); const [isPlaying, setIsPlaying] = useState(false); @@ -47,37 +59,41 @@ const VideoPlayer: React.FC = ({ video }) => { const debounceTimeoutRef = useRef(null); // Function to save progress to the backend - const saveProgress = useCallback(async (time: number, completed: boolean = false) => { - if (!videoRef.current) return; + const saveProgress = useCallback( + async (time: number, completed: boolean = false) => { + if (!videoRef.current) return; - const progressData: VideoProgress = { - - progress: Math.round(time), - - }; + const progressData: VideoProgress = { + progress: Math.round(time), + }; - try { - // First, try to fetch existing progress - const response = await axiosInstance.get(`/videos/progress/?user=${account?.id}&video=${video.id}`); - if (response.data.length > 0) { - // If progress exists, update it - const existingProgress = response.data[0]; - await axiosInstance.patch(`/videos/progress/${existingProgress.id}/`, progressData); - } else { - // If no progress, create a new one - await axiosInstance.post('/videos/progress/', progressData); + try { + // First, try to fetch existing progress + const response = await axiosInstance.get( + `/videos/progress/?user=${account?.id}&video=${video.id}`, + ); + if (response.data.length > 0) { + // If progress exists, update it + const existingProgress = response.data[0]; + await axiosInstance.patch(`/videos/progress/${existingProgress.id}/`, progressData); + await updateVideoItem(time, completed, progressData.progress, video.id); + } else { + // If no progress, create a new one + await axiosInstance.post('/videos/progress/', progressData); + } + if (completed) { + setSnackbarMessage('Video progress saved: Completed!'); + } else { + setSnackbarMessage(`Video progress saved: ${Math.floor(time)}s`); + } + setSnackbarOpen(true); + } catch (err) { + console.error('Failed to save video progress:', err); + setError('Failed to save video progress.'); } - if (completed) { - setSnackbarMessage('Video progress saved: Completed!'); - } else { - setSnackbarMessage(`Video progress saved: ${Math.floor(time)}s`); - } - setSnackbarOpen(true); - } catch (err) { - console.error('Failed to save video progress:', err); - setError('Failed to save video progress.'); - } - }, [video.id, account?.id]); + }, + [video.id, account?.id], + ); // Fetch initial progress when video changes or component mounts useEffect(() => { @@ -85,14 +101,15 @@ const VideoPlayer: React.FC = ({ video }) => { setIsLoading(true); setError(null); try { - const {data,} = await axiosInstance.get(`/videos/progress/${video.id}/?user=${account?.id}`); - - if (data) { + const { data } = await axiosInstance.get( + `/videos/progress/${video.id}/?user=${account?.id}`, + ); + if (data) { const progress: VideoProgress = { current_time: data.progress, progress: data.progress, - } + }; setCurrentTime(progress?.current_time || 0); if (videoRef.current) { @@ -128,7 +145,6 @@ const VideoPlayer: React.FC = ({ video }) => { }; }, [video.id, account?.id, saveProgress]); - // Video event handlers const handleTimeUpdate = () => { if (videoRef.current) { @@ -148,7 +164,7 @@ const VideoPlayer: React.FC = ({ video }) => { const handlePlay = () => { setIsPlaying(true); if (videoRef.current) { - videoRef.current.play().catch(e => console.error("Error playing video:", e)); + videoRef.current.play().catch((e) => console.error('Error playing video:', e)); } }; @@ -170,7 +186,7 @@ const VideoPlayer: React.FC = ({ video }) => { // Attempt to play if it was playing before, or if it's the first load if (videoRef.current && currentTime > 0) { videoRef.current.currentTime = currentTime; - videoRef.current.play().catch(e => console.error("Error resuming video:", e)); + videoRef.current.play().catch((e) => console.error('Error resuming video:', e)); setIsPlaying(true); } }; @@ -195,7 +211,7 @@ const VideoPlayer: React.FC = ({ video }) => { if (!videoRef.current) return; if (!document.fullscreenElement) { - videoRef.current.requestFullscreen().catch(err => { + videoRef.current.requestFullscreen().catch((err) => { alert(`Error attempting to enable full-screen mode: ${err.message} (${err.name})`); }); } else { @@ -227,18 +243,23 @@ const VideoPlayer: React.FC = ({ video }) => { setSnackbarOpen(false); }; - return ( - {video.name} - {video.description} - - @@ -276,15 +293,20 @@ const VideoPlayer: React.FC = ({ video }) => { - {isFullScreen ? : } + {isFullScreen ? ( + + ) : ( + + )} - Current Progress: {Math.round(video.progress/video.duration*100)}% - Status: {video.status} + Current Progress: {Math.round((video.progress / video.duration) * 100)}% - Status:{' '} + {video.status} ); }; -export default VideoPlayer; \ No newline at end of file +export default VideoPlayer; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Messages/CreateConversationDialogContent.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Messages/CreateConversationDialogContent.tsx new file mode 100644 index 0000000..a237fc4 --- /dev/null +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Messages/CreateConversationDialogContent.tsx @@ -0,0 +1,93 @@ +import { + Button, + Box, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Stack, + Typography, + Autocomplete, + TextField, + Paper, +} from '@mui/material'; + +import { ReactElement, useState, useEffect } from 'react'; +import { axiosInstance } from '../../../../../axiosApi'; +import { PropertiesAPI, VendorAPI, VendorItem } from 'types'; +import { AxiosResponse } from 'axios'; + +type CreateOfferDialogProps = { + showDialog: boolean; + closeDialog: () => void; + createConversation: () => void; + setSelectedVendor: React.Dispatch>; + vendors: VendorAPI[]; + selectedVendor: VendorAPI | null; +}; + +const CreateConversationDialogContent = ({ + showDialog, + closeDialog, + createConversation, + setSelectedVendor, + vendors, + selectedVendor, +}: CreateOfferDialogProps): ReactElement => { + const [options, setOptions] = useState(vendors.map((vendor) => vendor.business_name)); + + const [inputValue, setInputValue] = useState(''); + + useEffect(() => { + const filteredVendorNames: string[] = vendors.map((vendor) => { + return vendor.business_name; + }); + setOptions(filteredVendorNames); + }, []); + + const handleInputChange = async (event, newInputValue) => { + setInputValue(newInputValue); + if (newInputValue) { + const inputValue = newInputValue.toLowerCase(); + const filteredVendors = vendors.filter((vendor) => + vendor.business_name.toLowerCase().includes(inputValue), + ); + const filteredVendorNames: string[] = filteredVendors.map((vendor) => { + return vendor.business_name; + }); + setOptions(filteredVendorNames); + } else { + setOptions([]); + } + }; + + return ( + + Start a new Conversation + + option.name} + onChange={(event, newValue) => { + const filteredVendors = vendors.filter((vendor) => vendor.business_name === newValue); + setSelectedVendor(filteredVendors[0]); + }} + inputValue={inputValue} + onInputChange={handleInputChange} + noOptionsText={'Type the vendor to search for'} + renderInput={(params) => ( + + )} + /> + + + + + + + ); +}; + +export default CreateConversationDialogContent; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Profile/AddOpenHouseDialog.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/AddOpenHouseDialog.tsx new file mode 100644 index 0000000..c904378 --- /dev/null +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/AddOpenHouseDialog.tsx @@ -0,0 +1,140 @@ +// src/components/PropertyOwnerProfile/AddOpenHouseDialog.tsx + +import React, { useState } from 'react'; +import { + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + MenuItem, + Autocomplete, +} from '@mui/material'; +import { PropertiesAPI, OpenHouseAPI } from 'types'; // Ensure you have OpenHouseAPI in your types + +import { DatePicker, TimePicker, LocalizationProvider } from '@mui/x-date-pickers'; +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; + +interface AddOpenHouseDialogProps { + open: boolean; + onClose: () => void; + onAddOpenHouse: (openHouse: Omit & { listed_date: string }) => void; + properties: PropertiesAPI[]; + errors: { [key: string]: string }; +} + +const AddOpenHouseDialog: React.FC = ({ + open, + onClose, + onAddOpenHouse, + properties, + errors, +}) => { + const [propertyId, setPropertyId] = useState(''); + const [selectedProperty, setSelectedProperty] = useState(null); + const [startTime, setStartTime] = useState(new Date()); + const [endTime, setEndTime] = useState(new Date()); + const [date, setDate] = useState(new Date()); + const [inputValue, setInputValue] = useState(''); + + const [options, setOptions] = useState([]); + + const handleAddOpenHouse = () => { + if (selectedProperty && startTime && endTime && date) { + const newOpenHouse = { + property: selectedProperty.id, + start_time: startTime.toTimeString().split(' ')[0], + end_time: endTime.toTimeString().split(' ')[0], + listed_date: date.toISOString().split('T')[0], + }; + + onAddOpenHouse(newOpenHouse); + // onClose(); + // selectedProperty(null); + // setStartTime(new Date()); + // setEndTime(new Date()); + } + }; + + console.log(errors); + + const handleInputChange = async (event: React.SyntheticEvent, newInputValue: string) => { + setInputValue(newInputValue); + if (newInputValue) { + const filteredPropertiesNames: string[] = properties.map((item) => { + return item.address; + }); + setOptions(filteredPropertiesNames); + } else { + setOptions([]); + } + }; + + return ( + +       Add New Open House     {' '} + +        {' '} + +          {' '} + { + const selectedAddr = properties.find((item) => item.address === newValue); + setSelectedProperty(selectedAddr || null); + }} + onInputChange={handleInputChange} + noOptionsText={'Type the address you want to set an open house for'} + renderInput={(params) => ( + + )} + /> + setDate(newValue)} + slotProps={{ textField: { fullWidth: true } }} + helperText={errors.listed_date} + error={!!errors.listed_date} + /> + setStartTime(newValue)} + slotProps={{ textField: { fullWidth: true } }} + /> + setEndTime(newValue)} + slotProps={{ textField: { fullWidth: true } }} + /> +        {' '} + +      {' '} + +      {' '} + +        {' '} + +        {' '} + +      {' '} + +    {' '} + + ); +}; + +export default AddOpenHouseDialog; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Profile/AddPropertyDialog.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/AddPropertyDialog.tsx index f067a2e..b2841ff 100644 --- a/ditch-the-agent/src/components/sections/dashboard/Home/Profile/AddPropertyDialog.tsx +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/AddPropertyDialog.tsx @@ -101,7 +101,7 @@ const AddPropertyDialog: React.FC = ({ open, onClose, on event: React.SyntheticEvent, value: string, ) => { - const test: boolean = true; + const test: boolean = !import.meta.env.USE_LIVE_DATA; let data: AutocompleteDataResponseAPI[] = []; if (value.length > 2) { if (test) { @@ -285,7 +285,7 @@ const AddPropertyDialog: React.FC = ({ open, onClose, on Add New Property - + option.description} @@ -319,7 +319,7 @@ const AddPropertyDialog: React.FC = ({ open, onClose, on )} /> - + = ({ open, onClose, on helperText={formErrors.city} /> - + = ({ open, onClose, on helperText={formErrors.state} /> - + = ({ open, onClose, on helperText={formErrors.zip_code} /> - + = ({ open, onClose, on onChange={handleInputChange} /> - + = ({ open, onClose, on helperText={formErrors.sq_ft} /> - + = ({ open, onClose, on helperText={formErrors.num_bedrooms} /> - + = ({ open, onClose, on helperText={formErrors.num_bathrooms} /> - + = ({ open, onClose, on } /> - + = ({ open, onClose, on onChange={handleInputChange} /> - + = ({ open, onClose, on onChange={handleInputChange} /> - + = ({ open, onClose, on onChange={handleInputChange} /> - + = ({ open, onClose, on onChange={handleInputChange} /> - + - + Subscription Tier: {attorney.user.tier === 'premium' ? 'Premium' : 'Basic'} @@ -477,7 +477,7 @@ const AttorneyProfileCard: React.FC = ({ )} {editedAttorney.latitude && editedAttorney.longitude && ( - + Firm Location on Map: diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Profile/ChangePasswordCard.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/ChangePasswordCard.tsx new file mode 100644 index 0000000..817bfe2 --- /dev/null +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/ChangePasswordCard.tsx @@ -0,0 +1,151 @@ +import { + Alert, + Box, + Button, + Card, + CardContent, + CardHeader, + Divider, + Grid, + LinearProgress, + TextField, + Typography, +} from '@mui/material'; +import { axiosInstance } from 'axiosApi'; +import { useState } from 'react'; +import zxcvbn from 'zxcvbn'; + +const ChangePasswordCard = () => { + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [passwordStrength, setPasswordStrength] = useState(0); + const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + const [errors, setErrors] = useState<{ [key: string]: string }>({}); + + const handlePasswordChange = (e: React.ChangeEvent) => { + const password = e.target.value; + setNewPassword(password); + const strength = zxcvbn(password).score; + setPasswordStrength(strength); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setErrors({}); + setMessage(null); + + if (newPassword !== confirmPassword) { + setErrors({ confirmPassword: 'Passwords do not match' }); + return; + } + + if (passwordStrength < 2) { + setErrors({ newPassword: 'Password is too weak' }); + return; + } + + try { + await axiosInstance.post('/auth/password/change/', { + old_password: currentPassword, + new_password: newPassword, + }); + setMessage({ type: 'success', text: 'Password updated successfully!' }); + setCurrentPassword(''); + setNewPassword(''); + setConfirmPassword(''); + setPasswordStrength(0); + } catch (error: any) { + setMessage({ type: 'error', text: 'Error updating password.' }); + if (error.response && error.response.data) { + setErrors(error.response.data); + } + } + }; + + const passwordStrengthColor = () => { + switch (passwordStrength) { + case 0: + case 1: + return 'error'; + case 2: + return 'warning'; + case 3: + case 4: + return 'success'; + default: + return 'grey'; + } + }; + + return ( + + + + +
+ + {message && ( + + {message.text} + + )} + + setCurrentPassword(e.target.value)} + error={!!errors.old_password} + helperText={errors.old_password} + required + /> + + + + + + + {['Very Weak', 'Weak', 'Fair', 'Good', 'Strong'][passwordStrength]} + + + + + setConfirmPassword(e.target.value)} + error={!!errors.confirmPassword} + helperText={errors.confirmPassword} + required + /> + + + + + +
+
+
+ ); +}; + +export default ChangePasswordCard; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Profile/OfferSubmissionCard.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/OfferSubmissionCard.tsx index fdbd5ee..9d3889e 100644 --- a/ditch-the-agent/src/components/sections/dashboard/Home/Profile/OfferSubmissionCard.tsx +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/OfferSubmissionCard.tsx @@ -1,28 +1,97 @@ import React, { useState } from 'react'; -import { Card, CardContent, Typography, TextField, Button, Box, Alert } from '@mui/material'; +import { Card, CardContent, Typography, TextField, Button, Alert, Tooltip } from '@mui/material'; +import { useNavigate } from 'react-router-dom'; interface OfferSubmissionCardProps { - onOfferSubmit: (offerAmount: number) => void; + onOfferSubmit: ( + offerAmount: number, + closing_days: number, + contingencies: string, + ) => Promise<{ status: number; message?: string }>; listingStatus: 'active' | 'pending' | 'contingent' | 'sold' | 'off_market'; + listingPrice: number; + existingOffer?: { + document_id: string; + }; } const OfferSubmissionCard: React.FC = ({ onOfferSubmit, listingStatus, + listingPrice, + existingOffer, }) => { const [offerAmount, setOfferAmount] = useState(''); + const [closingDuration, setClosingDuration] = useState(''); + const [contingencies, setContingencies] = useState('None'); const [submitted, setSubmitted] = useState(false); + const [error, setError] = useState(null); + const navigate = useNavigate(); + + const getClosingDate = () => { + if (closingDuration) { + const days = parseInt(closingDuration, 10); + if (!isNaN(days)) { + const closingDate = new Date(); + closingDate.setDate(closingDate.getDate() + days); + return closingDate.toLocaleDateString(); + } + } + return ''; + }; + + const offerPercentage = + offerAmount && listingPrice ? (parseFloat(offerAmount) / listingPrice) * 100 : 0; + + if (existingOffer) { + return ( + + + + Offer Already Submitted + + + You have already submitted an offer for this property. + + + + + ); + } if (listingStatus === 'active') { - const handleSubmit = () => { + const handleSubmit = async () => { const amount = parseFloat(offerAmount); - if (amount > 0) { - onOfferSubmit(amount); - setSubmitted(true); - setTimeout(() => setSubmitted(false), 5000); + const closing_days = parseFloat(closingDuration); + if (amount > 0 && closing_days) { + try { + const response = await onOfferSubmit(amount, closing_days, contingencies); + if (response.status === 200 || response.status === 201) { + setSubmitted(true); + setError(null); + setTimeout(() => setSubmitted(false), 5000); + } else { + setError(response.message || 'An unknown error occurred.'); + setSubmitted(false); + setTimeout(() => setError(null), 5000); + } + } catch (err: any) { + setError(err.message || 'Failed to submit offer.'); + setSubmitted(false); + setTimeout(() => setError(null), 5000); + } } }; + const isButtonDisabled = !offerAmount || !closingDuration; + return ( @@ -36,8 +105,38 @@ const OfferSubmissionCard: React.FC = ({ value={offerAmount} onChange={(e) => setOfferAmount(e.target.value)} sx={{ mb: 2 }} + helperText={ + offerPercentage > 0 + ? `This offer is ${offerPercentage.toFixed(2)}% of the listing price.` + : '' + } /> - {submitted && ( @@ -45,6 +144,11 @@ const OfferSubmissionCard: React.FC = ({ Your offer of ${offerAmount} has been submitted! )} + {error && ( + + {error} + + )} ); diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Profile/OpenHouseCard.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/OpenHouseCard.tsx index 03d2323..9d99418 100644 --- a/ditch-the-agent/src/components/sections/dashboard/Home/Profile/OpenHouseCard.tsx +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/OpenHouseCard.tsx @@ -1,58 +1,66 @@ import React from 'react'; -import { Card, CardContent, Typography, List, ListItem, ListItemText, Divider } from '@mui/material'; +import { + Card, + CardContent, + Typography, + List, + ListItem, + ListItemText, + Divider, +} from '@mui/material'; import { OpenHouseAPI } from 'types'; - +import { format } from 'date-fns'; interface OpenHouseCardProps { - openHouses: OpenHouseAPI[] | undefined; + openHouses: OpenHouseAPI[] | undefined; } const OpenHouseCard: React.FC = ({ openHouses }) => { - if(openHouses){ - return ( - - - - Open House Information - - {openHouses.length > 0 ? ( - - {openHouses.map((openHouse, index) => ( - - - - - {index < openHouses.length - 1 && } - - ))} - - ) : ( - - No upcoming open houses scheduled. - - )} - - + if (openHouses) { + return ( + + + + Open House Information + + {openHouses.length > 0 ? ( + + {openHouses.map((openHouse, index) => ( + + + + + {index < openHouses.length - 1 && } + + ))} + + ) : ( + + No upcoming open houses scheduled. + + )} + + ); - - }else{ - return ( - - - - Open House Information - - - No upcoming open houses scheduled. - - - + } else { + return ( + + + + Open House Information + + + No upcoming open houses scheduled. + + + ); - } - + } }; -export default OpenHouseCard; \ No newline at end of file +export default OpenHouseCard; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Profile/OpenHouseDialogContext.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/OpenHouseDialogContext.tsx new file mode 100644 index 0000000..4ceb050 --- /dev/null +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/OpenHouseDialogContext.tsx @@ -0,0 +1,37 @@ +// src/components/PropertyOwnerProfile/OpenHouseCard.tsx + +import React from 'react'; +import { Card, CardContent, Typography, Box } from '@mui/material'; +import { OpenHouseAPI } from 'types'; +import { format } from 'date-fns'; + +interface OpenHouseCardProps { + openHouse: OpenHouseAPI; +} + +const OpenHouseDialogContent: React.FC = ({ openHouse }) => { + const startTime = new Date(`${openHouse.start_time}`); + const endTime = new Date(`${openHouse.end_time}`); + + console.log(endTime); + + return ( + + + + Open House at Property: {openHouse.property.address_line_1} + + + + {openHouse.start_time} + + + {openHouse.end_time} + + + + + ); +}; + +export default OpenHouseDialogContent; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Profile/ProfileCard.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/ProfileCard.tsx index 36e336e..ea9f3c9 100644 --- a/ditch-the-agent/src/components/sections/dashboard/Home/Profile/ProfileCard.tsx +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/ProfileCard.tsx @@ -20,9 +20,15 @@ interface ProfileCardProps { user: UserAPI; onUpgrade: () => void; onSave: (updatedUser: UserAPI) => void; + setMessage: ( + value: React.SetStateAction<{ + type: 'success' | 'error'; + text: string; + } | null>, + ) => void; } -const ProfileCard: React.FC = ({ user, onUpgrade, onSave }) => { +const ProfileCard: React.FC = ({ user, onUpgrade, onSave, setMessage }) => { const [isEditing, setIsEditing] = useState(false); const [editedUser, setEditedUser] = useState(user); const [formErrors, setFormErrors] = useState<{ [key: string]: string }>({}); @@ -39,18 +45,23 @@ const ProfileCard: React.FC = ({ user, onUpgrade, onSave }) => console.log(editedUser); const handleChange = (e: React.ChangeEvent) => { - const { name, value, type, checked } = e.target; - setEditedUser((prev) => ({ - ...prev, - [name]: type === 'checkbox' ? checked : value, - })); - // Clear error for the field being edited - if (formErrors[name]) { - setFormErrors((prev) => { - const newErrors = { ...prev }; - delete newErrors[name]; - return newErrors; - }); + if (isEditing) { + const { name, value, type, checked } = e.target; + setEditedUser((prev) => ({ + ...prev, + [name]: type === 'checkbox' ? checked : value, + })); + // Clear error for the field being edited + if (formErrors[name]) { + setFormErrors((prev) => { + const newErrors = { ...prev }; + delete newErrors[name]; + return newErrors; + }); + } + } else { + setMessage({ type: 'error', text: 'Enable editing in the top right' }); + setTimeout(() => setMessage(null), 3000); } }; @@ -101,31 +112,29 @@ const ProfileCard: React.FC = ({ user, onUpgrade, onSave }) => )}
- + - + - + = ({ user, onUpgrade, onSave }) => type="email" value={editedUser.email} onChange={handleChange} - disabled={!isEditing} error={!!formErrors.email} helperText={formErrors.email} /> - + Subscription Tier: {user.tier === 'premium' ? 'Premium' : 'Basic'} @@ -148,7 +156,7 @@ const ProfileCard: React.FC = ({ user, onUpgrade, onSave }) => )} - + Notification Settings: {/* Example Checkboxes - You'd manage these with state too */} diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Profile/PropertyCard..tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/PropertyCard..tsx index e9eff5c..4c04039 100644 --- a/ditch-the-agent/src/components/sections/dashboard/Home/Profile/PropertyCard..tsx +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/PropertyCard..tsx @@ -1,112 +1,104 @@ import React from 'react'; import { - Card, - CardContent, - Typography, - Grid, - Box, - ImageList, - ImageListItem, - ImageListItemBar, - Button, + Card, + CardContent, + Typography, + Grid, + Box, + ImageList, + ImageListItem, + ImageListItemBar, + Button, } from '@mui/material'; import MapComponent from '../../../../base/MapComponent'; import { PropertiesAPI } from 'types'; interface PropertyCardProps { - property: PropertiesAPI; + property: PropertiesAPI; } const PropertyCard: React.FC = ({ property }) => { - // Dummy latitude and longitude for demonstration - // In a real app, you'd geocode the address to get these. - const demoLat = 34.0522; - const demoLng = -118.2437; // Example: Los Angeles coordinates - console.log(property) + // Dummy latitude and longitude for demonstration + // In a real app, you'd geocode the address to get these. + const demoLat = 34.0522; + const demoLng = -118.2437; // Example: Los Angeles coordinates + console.log(property); - return ( - - - - {property.address}, {property.city}, {property.state} {property.zip_code} - - - - {property.pictures && property.pictures.length > 0 && ( - 1 ? 2 : 1} rowHeight={164} sx={{ maxWidth: 500 }}> - {property.pictures.map((item, index) => ( - - + + + {property.address}, {property.city}, {property.state} {property.zip_code} + + + + {property.pictures && property.pictures.length > 0 && ( + 1 ? 2 : 1} + rowHeight={164} + sx={{ maxWidth: 500 }} + > + {property.pictures.map((item, index) => ( + + {`Property - - - ))} - - )} - - Description: - - { property.description ? ( - - {property.description} - - - ) : ( - - - )} - - - - - Stats: - - - Sq Ft: {property.sq_ft || 'N/A'} - - - Bedrooms: {property.num_bedrooms || 'N/A'} - - - Bathrooms: {property.num_bathrooms || 'N/A'} - - - Features: {property.features && property.features.length > 0 - ? property.features.join(', ') - : 'None'} - - - Market Value: ${property.market_value || 'N/A'} - - - Loan Amount: ${property.loan_amount || 'N/A'} - - - Loan Term: {property.loan_term ? `${property.loan_term} years` : 'N/A'} - - - Loan Start Date: {property.loan_start_date || 'N/A'} - - {property.latitude && property.longitude ? ( - - ) : ( -

Error loading the map

- )} -
-
-
-
- ); + src={`${item}?w=164&h=164&fit=crop&auto=format`} + alt={`Property image ${index + 1}`} + loading="lazy" + /> + + + ))} + + )} + + Description: + + {property.description ? ( + + {property.description} + + ) : ( + + )} +
+ + + Stats: + + Sq Ft: {property.sq_ft || 'N/A'} + Bedrooms: {property.num_bedrooms || 'N/A'} + Bathrooms: {property.num_bathrooms || 'N/A'} + + Features:{' '} + {property.features && property.features.length > 0 + ? property.features.join(', ') + : 'None'} + + Market Value: ${property.market_value || 'N/A'} + Loan Amount: ${property.loan_amount || 'N/A'} + + Loan Term: {property.loan_term ? `${property.loan_term} years` : 'N/A'} + + + Loan Start Date: {property.loan_start_date || 'N/A'} + + {property.latitude && property.longitude ? ( + + ) : ( +

Error loading the map

+ )} +
+
+ +
+ ); }; -export default PropertyCard; \ No newline at end of file +export default PropertyCard; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Profile/PropertyOwnerProfile.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/PropertyOwnerProfile.tsx index 16277ff..cdb0bcb 100644 --- a/ditch-the-agent/src/components/sections/dashboard/Home/Profile/PropertyOwnerProfile.tsx +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/PropertyOwnerProfile.tsx @@ -1,7 +1,8 @@ import { Alert, Box, Button, Container, Divider, Grid, Paper, Typography } from '@mui/material'; +import ChangePasswordCard from './ChangePasswordCard'; import { ReactElement, useContext, useEffect, useState } from 'react'; -import { PropertiesAPI, PropertyOwnerAPI, PropertyRequestAPI, UserAPI } from 'types'; +import { PropertiesAPI, PropertyOwnerAPI, PropertyRequestAPI, UserAPI, OpenHouseAPI } from 'types'; import ProfileCard from './ProfileCard'; import { AxiosResponse } from 'axios'; import PropertyCard from './PropertyCard.'; @@ -13,12 +14,18 @@ import DashboardErrorPage from '../Dashboard/DashboardErrorPage'; import DashboardLoading from '../Dashboard/DashboardLoading'; import { ProfileProps } from 'pages/Profile/Profile'; import { useNavigate } from 'react-router-dom'; +import AddOpenHouseDialog from './AddOpenHouseDialog'; +import OpenHouseDialogContent from './OpenHouseDialogContext'; const PropertyOwnerProfile = ({ account }: ProfileProps): ReactElement => { const [loadingData, setLoadingData] = useState(true); const navigate = useNavigate(); const [user, setUser] = useState(null); + const [openHouses, setOpenHouses] = useState([]); + const [openAddOpenHouseDialog, setOpenAddOpenHouseDialog] = useState(false); + const [openHouseErrors, setOpenHouseErrors] = useState<{ [key: string]: string }>({}); + useEffect(() => { const fetchPropertyOwner = async () => { try { @@ -52,6 +59,11 @@ const PropertyOwnerProfile = ({ account }: ProfileProps): ReactElement => { console.log('setting the user to: ', data[0].owner); setUser(data[0].owner); } + + const { data: openHousesData }: AxiosResponse = await axiosInstance.get( + '/properties/open-houses/', + ); + setOpenHouses(openHousesData); } catch (error) { console.log(error); } finally { @@ -104,6 +116,35 @@ const PropertyOwnerProfile = ({ account }: ProfileProps): ReactElement => { setOpenAddPropertyDialog(false); }; + const handleOpenAddOpenHouseDialog = () => { + setOpenAddOpenHouseDialog(true); + }; + + const handleCloseAddOpenHouseDialog = () => { + setOpenAddOpenHouseDialog(false); + setOpenHouseErrors({}); + }; + + const handleAddOpenHouse = async ( + newOpenHouseData: Omit & { listed_date: string }, + ) => { + try { + const { data }: AxiosResponse = await axiosInstance.post( + '/properties/open-houses/', + newOpenHouseData, + ); + setOpenHouses((prev) => [...prev, data]); + setMessage({ type: 'success', text: 'Open house added successfully!' }); + setTimeout(() => setMessage(null), 3000); + setOpenAddOpenHouseDialog(false); + } catch (error) { + console.log(error); + setOpenHouseErrors(error.response.data); + setMessage({ type: 'error', text: 'Error adding open house.' }); + setTimeout(() => setMessage(null), 3000); + } + }; + const handleAddProperty = ( newPropertyData: Omit, ) => { @@ -134,29 +175,39 @@ const PropertyOwnerProfile = ({ account }: ProfileProps): ReactElement => { } }; - const handleSaveProperty = (updatedProperty: PropertiesAPI) => { - // In a real app, this would be an API call to update the property - console.log('Saving property: IMPLEMENT ME', updatedProperty); + const handleSaveProperty = async (updatedProperty: PropertiesAPI) => { + try { + const { data } = await axiosInstance.patch( + `/properties/${updatedProperty.id}/`, + { + ...updatedProperty, + owner: account.id, + }, + ); + const updatedProperties = properties.map((item) => { + if (item.id === data.id) { + return { ...item, ...data }; + } + return item; + }); + setProperties(updatedProperties); + setMessage({ type: 'success', text: 'Property has been updated' }); + setTimeout(() => setMessage(null), 3000); + } catch (error) { + setMessage({ type: 'error', text: 'Error while saving the property. Please try again' }); + setTimeout(() => setMessage(null), 3000); + } }; const handleDeleteProperty = async (propertyId: number) => { - console.log('handle delete. IMPLEMENT ME'); try { - const { data }: AxiosResponse = await axiosInstance.delete( - `/properties/${propertyId}/`, - ); - console.log(data); - // remove the proprty from the list - setProperties((prevProperty) => prevProperty.filter((item) => item.id !== propertyId)); - // const indexToRemove = properties.findIndex(property => property.id === propertyId); - // console.log(indexToRemove) - // if (indexToRemove !== -1) { - // const updatedProperties = properties.splice(indexToRemove, 1) - // console.log(updatedProperties) - // setProperties(updatedProperties); - // } - } catch { - console.log('error removing'); + await axiosInstance.delete(`/properties/${propertyId}/`); + setProperties((prev) => prev.filter((item) => item.id !== propertyId)); + setMessage({ type: 'success', text: 'Property has been removed' }); + setTimeout(() => setMessage(null), 3000); + } catch (error) { + setMessage({ type: 'error', text: 'Error while removing the property. Please try again' }); + setTimeout(() => setMessage(null), 3000); } }; @@ -182,8 +233,13 @@ const PropertyOwnerProfile = ({ account }: ProfileProps): ReactElement => { user={user.user} onUpgrade={handleUpgradeSubscription} onSave={handleSaveProfile} + setMessage={setMessage} /> + + + + @@ -194,6 +250,11 @@ const PropertyOwnerProfile = ({ account }: ProfileProps): ReactElement => { Add Property + {message && ( + + {message.text} + + )} {properties.length === 0 ? ( @@ -203,7 +264,7 @@ const PropertyOwnerProfile = ({ account }: ProfileProps): ReactElement => { ) : ( {properties.map((property) => ( - + {/* */} { onClose={handleCloseAddPropertyDialog} onAddProperty={handleAddProperty} /> + + + + {properties.length > 0 ? ( + <> + + + My Open Houses + + + + {openHouses.length === 0 ? ( + + + You have no open houses scheduled. + + + ) : ( + + {openHouses.map((openHouse) => ( + + {/* You will create a component to display the open house details */} + + + ))} + + )} + + ) : ( + + Please add a property before you can schedule an open house. + + )} + + ); } diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Profile/VendorProfile.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/VendorProfile.tsx index 186cbda..cd3bc8c 100644 --- a/ditch-the-agent/src/components/sections/dashboard/Home/Profile/VendorProfile.tsx +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/VendorProfile.tsx @@ -1,5 +1,6 @@ import { ReactElement, useContext, useEffect, useState } from 'react'; import { UserAPI, VendorAPI } from 'types'; +import ChangePasswordCard from './ChangePasswordCard'; import { Container, Typography, @@ -165,6 +166,10 @@ const VendorProfile = ({ account }: ProfileProps): ReactElement => { onSave={handleSaveVendorProfile} /> + + + + = ({ vendor, onUpgrade event: React.SyntheticEvent, value: string, ) => { - const test: boolean = true; + const test: boolean = !import.meta.env.USE_LIVE_DATA; let data: AutocompleteDataResponseAPI[] = []; if (value.length > 2) { if (test) { @@ -196,7 +196,7 @@ const VendorProfileCard: React.FC = ({ vendor, onUpgrade )} - + = ({ vendor, onUpgrade disabled={!isEditing} /> - + = ({ vendor, onUpgrade disabled={!isEditing} /> - + = ({ vendor, onUpgrade disabled={!isEditing} /> - + = ({ vendor, onUpgrade helperText={formErrors.business_name} /> - + Business Type - + = ({ vendor, onUpgrade helperText={formErrors.phone_number} /> - + option.description} @@ -307,7 +307,7 @@ const VendorProfileCard: React.FC = ({ vendor, onUpgrade )} /> - + = ({ vendor, onUpgrade helperText={formErrors.city} /> - + = ({ vendor, onUpgrade helperText={formErrors.state} /> - + = ({ vendor, onUpgrade helperText={formErrors.zip_code} /> - + = ({ vendor, onUpgrade disabled={!isEditing} /> - + = ({ vendor, onUpgrade disabled={!isEditing} /> - + = ({ vendor, onUpgrade disabled={!isEditing} /> - + Subscription Tier: {vendor.user.tier === 'premium' ? 'Premium' : 'Basic'} diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Property/LogInNotificationCard.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Property/LogInNotificationCard.tsx new file mode 100644 index 0000000..1194038 --- /dev/null +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Property/LogInNotificationCard.tsx @@ -0,0 +1,30 @@ +import React, { useContext } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Typography, Card, CardContent, Button, Grid } from '@mui/material'; + +interface LogInNotificationCardProps {} + +const LogInNotificationCard: React.FC = ({}) => { + const navigate = useNavigate(); + const goToLogin = async () => { + navigate('/authentication/login'); + }; + return ( + + + + Want to know more? + + + Sign in or create an account to view the seller disclosure document and message the owner + with any questions. + + + + + ); +}; + +export default LogInNotificationCard; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Property/PropertyDetailCard.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Property/PropertyDetailCard.tsx index b2ab33c..dce1a15 100644 --- a/ditch-the-agent/src/components/sections/dashboard/Home/Property/PropertyDetailCard.tsx +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Property/PropertyDetailCard.tsx @@ -159,10 +159,11 @@ const PropertyDetailCard: React.FC = ({ setIsGernerating(true); const response = await axiosInstance.put(`/property-description-generator/${property.id}/`); - console.log(response); + setEditedProperty((prev) => ({ + ...prev, + description: response.data.description, + })); setIsGernerating(false); - - // TODO: toggle the update }; return ( @@ -195,10 +196,10 @@ const PropertyDetailCard: React.FC = ({ {/* Property Address & Basic Info */} - + {isEditing ? ( - + = ({ helperText={formErrors.address} /> - + = ({ helperText={formErrors.city} /> - + = ({ helperText={formErrors.state} /> - + = ({ {/* Pictures */} - + Pictures: @@ -306,8 +307,8 @@ const PropertyDetailCard: React.FC = ({ )} - {/* Description & Stats */} - + {/* Description */} + Description: @@ -333,13 +334,16 @@ const PropertyDetailCard: React.FC = ({ // {property.description || 'No description provided.'} // )} +
- + {/* Stats */} + + Stats: {isEditing ? ( - + = ({ helperText={formErrors.sq_ft} /> - + = ({ helperText={formErrors.num_bedrooms} /> - + = ({ helperText={formErrors.num_bathrooms} /> - + = ({ onChange={handleFeaturesChange} /> - + = ({ onChange={handleChange} /> - + = ({ onChange={handleChange} /> - + = ({ onChange={handleNumericChange} /> - + = ({ {/* Map */} - + Location on Map: diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Property/PropertyListItem.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Property/PropertyListItem.tsx index 7e814ce..a58bee7 100644 --- a/ditch-the-agent/src/components/sections/dashboard/Home/Property/PropertyListItem.tsx +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Property/PropertyListItem.tsx @@ -7,14 +7,23 @@ import { PropertiesAPI } from 'types'; interface PropertyListItemProps { property: PropertiesAPI; onViewDetails: (propertyId: number) => void; // For navigation in search page + isPublic: boolean; } -const PropertyListItem: React.FC = ({ property, onViewDetails }) => { +const PropertyListItem: React.FC = ({ + property, + onViewDetails, + isPublic = false, +}) => { const navigate = useNavigate(); const handleViewDetailsClick = () => { // Navigate to the full detail page for this property - navigate(`/property/${property.id}/?search=1`); + if (!isPublic) { + navigate(`/property/${property.id}/?search=1`); + } else { + navigate(`/public/${property.id}`); + } }; const value_price = property.listed_price ? property.listed_price : property.market_value; const value_text = property.listed_price ? 'Listed Price' : 'Market Value'; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Property/PropertySearchFilters.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Property/PropertySearchFilters.tsx index f5adcf1..9aebeb2 100644 --- a/ditch-the-agent/src/components/sections/dashboard/Home/Property/PropertySearchFilters.tsx +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Property/PropertySearchFilters.tsx @@ -1,107 +1,209 @@ import React, { useState } from 'react'; -import { TextField, Button, Box, Grid, Paper, Typography } from '@mui/material'; +import { + TextField, + Button, + Box, + Grid, + Paper, + Typography, + Collapse, + IconButton, +} from '@mui/material'; import SearchIcon from '@mui/icons-material/Search'; import ClearIcon from '@mui/icons-material/Clear'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; interface SearchFilters { - address: string; - city: string; - state: string; - zipCode: string; - minSqFt: number | ''; - maxSqFt: number | ''; - minBedrooms: number | ''; - maxBedrooms: number | ''; - minBathrooms: number | ''; - maxBathrooms: number | ''; + address: string; + city: string; + state: string; + zipCode: string; + minSqFt: number | ''; + maxSqFt: number | ''; + minBedrooms: number | ''; + maxBedrooms: number | ''; + minBathrooms: number | ''; + maxBathrooms: number | ''; } interface PropertySearchFiltersProps { - onSearch: (filters: SearchFilters) => void; - onClear: () => void; + onSearch: (filters: SearchFilters) => void; + onClear: () => void; } const initialFilters: SearchFilters = { - address: '', - city: '', - state: '', - zipCode: '', - minSqFt: '', - maxSqFt: '', - minBedrooms: '', - maxBedrooms: '', - minBathrooms: '', - maxBathrooms: '', + address: '', + city: '', + state: '', + zipCode: '', + minSqFt: '', + maxSqFt: '', + minBedrooms: '', + maxBedrooms: '', + minBathrooms: '', + maxBathrooms: '', }; const PropertySearchFilters: React.FC = ({ onSearch, onClear }) => { - const [filters, setFilters] = useState(initialFilters); + const [filters, setFilters] = useState(initialFilters); + const [expanded, setExpanded] = useState(false); - const handleChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setFilters(prev => ({ ...prev, [name]: value })); - }; + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFilters((prev) => ({ ...prev, [name]: value })); + }; - const handleNumericChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - const numValue = value === '' ? '' : parseFloat(value); - setFilters(prev => ({ ...prev, [name]: numValue })); - }; + const handleNumericChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + const numValue = value === '' ? '' : parseFloat(value); + setFilters((prev) => ({ ...prev, [name]: numValue })); + }; - const handleSearchClick = () => { - onSearch(filters); - }; + const handleSearchClick = () => { + onSearch(filters); + }; - const handleClearClick = () => { - setFilters(initialFilters); - onClear(); - }; + const handleClearClick = () => { + setFilters(initialFilters); + onClear(); + }; - return ( - - Search Properties - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); + const handleToggleExpand = () => { + setExpanded(!expanded); + }; + + return ( + + + + Property Filters + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); }; -export default PropertySearchFilters; \ No newline at end of file +export default PropertySearchFilters; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Property/PropertyStatusCard.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Property/PropertyStatusCard.tsx index 8e8a0ea..068635a 100644 --- a/ditch-the-agent/src/components/sections/dashboard/Home/Property/PropertyStatusCard.tsx +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Property/PropertyStatusCard.tsx @@ -13,13 +13,22 @@ import { import VisibilityIcon from '@mui/icons-material/Visibility'; import FavoriteIcon from '@mui/icons-material/Favorite'; import AccessTimeIcon from '@mui/icons-material/AccessTime'; -import { PropertiesAPI } from 'types'; +import { PropertiesAPI, SavedPropertiesAPI } from 'types'; +import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder'; + +const getIcon = ( + savedProperty: SavedPropertiesAPI | null, +): typeof FavoriteBorderIcon | typeof FavoriteIcon => { + return savedProperty ? FavoriteIcon : FavoriteBorderIcon; +}; interface PropertyStatusCardProps { property: PropertiesAPI; isOwner: boolean; - onStatusChange?: () => void; + onStatusChange?: (string) => void; onSavedPropertySave?: () => void; + savedProperty: SavedPropertiesAPI | null; + sellerDisclosureExists: boolean; } const PropertyStatusCard: React.FC = ({ @@ -27,7 +36,17 @@ const PropertyStatusCard: React.FC = ({ isOwner, onStatusChange, onSavedPropertySave, + savedProperty, + sellerDisclosureExists, }) => { + const handleStatusChange = (e) => { + const newStatus = e.target.value; + if (newStatus === 'active' && !sellerDisclosureExists) { + alert('A seller disclosure document is required before putting the property on the market.'); + return; + } + onStatusChange(newStatus); + }; const getStatusColor = (status: PropertiesAPI['property_status']) => { switch (status) { case 'active': @@ -44,7 +63,7 @@ const PropertyStatusCard: React.FC = ({ }; const timeSinceListed = (dateString: string) => { - const listedDate = new Date(dateString); + const listedDate = new Date(dateString.split('T')[0]); const now = new Date(); const diffInMs = now.getTime() - listedDate.getTime(); const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24)); @@ -82,7 +101,7 @@ const PropertyStatusCard: React.FC = ({ {isOwner ? ( setSearchRadius(e.target.value as number)} + label="Search Radius" + > + 10 miles + 25 miles + 50 miles + + */} + + + + pageTitle="Service Vendors" + data={{ categories: filteredCategories, items: allVendors }} + renderCategoryGrid={(categories, onSelectCategory) => ( + onSelectCategory(id)} + renderCategoryCard={(category, onSelect) => ( + + )} + /> + )} + renderItemListDetail={(selectedCategory, itemsInSelectedCategory, onBack) => ( + ( + onSelect(item.id)} + /> + )} + renderItemDetail={(item) => ( + + )} + /> + )} + /> + ); }; diff --git a/ditch-the-agent/src/pages/authentication/Login.tsx b/ditch-the-agent/src/pages/authentication/Login.tsx index 1b6dc93..1493d25 100644 --- a/ditch-the-agent/src/pages/authentication/Login.tsx +++ b/ditch-the-agent/src/pages/authentication/Login.tsx @@ -1,210 +1,205 @@ -import { ReactElement, Suspense, useContext, useState } from 'react'; -import { - Alert, - Button, - FormControl, - IconButton, - InputAdornment, - InputLabel, - Link, - Skeleton, - Stack, - TextField, - Typography, -} from '@mui/material'; -import loginBanner from 'assets/authentication-banners/green.png'; -import IconifyIcon from 'components/base/IconifyIcon'; -import logo from 'assets/logo/favicon-logo.png'; -import Image from 'components/base/Image'; -import{axiosInstance} from '../../axiosApi.js'; -import { useNavigate } from 'react-router-dom'; -import { Form, Formik } from 'formik'; -import { AuthContext } from 'contexts/AuthContext.js'; - -type loginValues = { - email: string; - password: string; -} - - -const Login = (): ReactElement => { - const [showPassword, setShowPassword] = useState(false); - - const handleClickShowPassword = () => setShowPassword(!showPassword); - - const [errorMessage, setErrorMessage] = useState(null); - const navigate = useNavigate(); - const {setAuthentication} = useContext(AuthContext); - - const handleLogin = async({email, password}: loginValues): Promise => { - try{ - const response = await axiosInstance.post('/token/', - { - email: email, - password: password - } - ) - axiosInstance.defaults.headers['Authorization'] = 'JWT ' + response.data.access; - localStorage.setItem('access_token', response.data.access); - localStorage.setItem('refresh_token', response.data.refresh); - const get_user_response = await axiosInstance.get('/user/') - - setAuthentication(true) - - navigate("/") - - - - }catch (error) { - const hasErrors = Object.keys(error.response.data).length > 0; - if (hasErrors) { - setErrorMessage(error.response.data) - }else{ - setErrorMessage(null); - } - } - } - - return ( - theme.shadows[3]} - height={560} - width={{ md: 960 }} - > - - - - - - Login - - {({setFieldValue}) => ( - -
- - {errorMessage ? ( - -
    - {Object.entries(errorMessage).map(([fieldName, errorMessages]) => ( -
  • - {fieldName} - {errorMessages.length > 0 ? ( -
      - {errorMessages.map((message, index) => ( -
    • {message}
    • // Key for each message - ))} -
    - ) : ( - No specific errors for this field. - )} -
  • - ))} -
- -
- - ): null} - - Email - - setFieldValue('email', event.target.value)} - InputProps={{ - endAdornment: ( - - - - ), - }} - /> -
- - - Password - - setFieldValue('password', event.target.value)} - type={showPassword ? 'text' : 'password'} - id="password" - InputProps={{ - endAdornment: ( - - - {showPassword ? ( - - ) : ( - - )} - - - ), - }} - /> - - - - Forget password - - - -
- )} -
- - Don't have an account ?{' '} - theme.typography.body1.fontSize} - > - Sign up - - -
-
- - } - > - Login banner - -
- ); -}; - -export default Login; +import { ReactElement, Suspense, useContext, useState } from 'react'; +import { + Alert, + Button, + FormControl, + IconButton, + InputAdornment, + InputLabel, + Link, + Skeleton, + Stack, + TextField, + Typography, +} from '@mui/material'; +import loginBanner from 'assets/authentication-banners/green.png'; +import IconifyIcon from 'components/base/IconifyIcon'; +import logo from 'assets/logo/favicon-logo.png'; +import Image from 'components/base/Image'; +import { axiosInstance } from '../../axiosApi.js'; +import { useNavigate } from 'react-router-dom'; +import { Form, Formik } from 'formik'; +import { AuthContext } from 'contexts/AuthContext.js'; + +type loginValues = { + email: string; + password: string; +}; + +const Login = (): ReactElement => { + const [showPassword, setShowPassword] = useState(false); + + const handleClickShowPassword = () => setShowPassword(!showPassword); + + const [errorMessage, setErrorMessage] = useState(null); + const navigate = useNavigate(); + const { setAuthentication } = useContext(AuthContext); + + const handleLogin = async ({ email, password }: loginValues): Promise => { + try { + const response = await axiosInstance.post('/token/', { + email: email, + password: password, + }); + axiosInstance.defaults.headers['Authorization'] = 'JWT ' + response.data.access; + localStorage.setItem('access_token', response.data.access); + localStorage.setItem('refresh_token', response.data.refresh); + const get_user_response = await axiosInstance.get('/user/'); + + setAuthentication(true); + + navigate('/dashboard'); + } catch (error) { + const hasErrors = Object.keys(error.response.data).length > 0; + if (hasErrors) { + setErrorMessage(error.response.data); + } else { + setErrorMessage(null); + } + } + }; + + return ( + theme.shadows[3]} + height={560} + width={{ md: 960 }} + > + + + + + + Login + + {({ setFieldValue }) => ( +
+ + {errorMessage ? ( + + {errorMessage.detail ? ( + {errorMessage.detail} + ) : ( +
    + {Object.entries(errorMessage).map(([fieldName, errorMessages]) => ( +
  • + {fieldName} + {Array.isArray(errorMessages) ? ( +
      + {errorMessages.map((message, index) => ( +
    • {message}
    • + ))} +
    + ) : ( + : {String(errorMessages)} + )} +
  • + ))} +
+ )} +
+ ) : null} + + Email + + setFieldValue('email', event.target.value)} + InputProps={{ + endAdornment: ( + + + + ), + }} + /> +
+ + + Password + + setFieldValue('password', event.target.value)} + type={showPassword ? 'text' : 'password'} + id="password" + InputProps={{ + endAdornment: ( + + + {showPassword ? ( + + ) : ( + + )} + + + ), + }} + /> + + + + Forget password + + + +
+ )} +
+ + Don't have an account ?{' '} + theme.typography.body1.fontSize} + > + Sign up + + +
+
+ + } + > + Login banner + +
+ ); +}; + +export default Login; diff --git a/ditch-the-agent/src/pages/authentication/ResetPassword.tsx b/ditch-the-agent/src/pages/authentication/ResetPassword.tsx index f1e1e50..561c7f3 100644 --- a/ditch-the-agent/src/pages/authentication/ResetPassword.tsx +++ b/ditch-the-agent/src/pages/authentication/ResetPassword.tsx @@ -1,166 +1,170 @@ -import { ReactElement, Suspense, useState } from 'react'; -import { - Button, - FormControl, - IconButton, - InputAdornment, - InputLabel, - Link, - Skeleton, - Stack, - TextField, - Typography, -} from '@mui/material'; -import logo from 'assets/logo/favicon-logo.png'; -import resetPassword from 'assets/authentication-banners/green.png'; -import passwordUpdated from 'assets/authentication-banners/password-updated.png'; -import successTick from 'assets/authentication-banners/successTick.png'; -import Image from 'components/base/Image'; -import IconifyIcon from 'components/base/IconifyIcon'; - -const ResetPassword = (): ReactElement => { - const [showNewPassword, setShowNewPassword] = useState(false); - const [showConfirmPassword, setShowConfirmPassword] = useState(false); - - const handleClickShowNewPassword = () => setShowNewPassword(!showNewPassword); - const handleClickShowConfirmPassword = () => setShowConfirmPassword(!showConfirmPassword); - const [resetSuccessful, setResetSuccessful] = useState(false); - - const handleResetPassword = () => { - const passwordField: HTMLInputElement = document.getElementById( - 'new-password', - ) as HTMLInputElement; - const confirmPasswordField: HTMLInputElement = document.getElementById( - 'confirm-password', - ) as HTMLInputElement; - - if (passwordField.value !== confirmPasswordField.value) { - alert("Passwords don't match"); - return; - } - setResetSuccessful(true); - }; - - return ( - theme.shadows[3]} - height={560} - width={{ md: 960 }} - > - - - - - {!resetSuccessful ? ( - - Reset Password - - - Password - - - - {showNewPassword ? ( - - ) : ( - - )} - - - ), - }} - /> - - - - Password - - - - {showConfirmPassword ? ( - - ) : ( - - )} - - - ), - }} - /> - - - - Back to{' '} - theme.typography.body1.fontSize} - > - Log in - - - - ) : ( - - - Reset Successfully - - Your Ditch the Agent log in password has been updated successfully - - - - )} - - - } - > - {resetSuccessful - - - ); -}; - -export default ResetPassword; +import { ReactElement, Suspense, useState } from 'react'; +import { + Button, + FormControl, + IconButton, + InputAdornment, + InputLabel, + Link, + Skeleton, + Stack, + TextField, + Typography, +} from '@mui/material'; +import logo from 'assets/logo/favicon-logo.png'; +import resetPassword from 'assets/authentication-banners/green.png'; +import passwordUpdated from 'assets/authentication-banners/password-updated.png'; +import successTick from 'assets/authentication-banners/successTick.png'; +import Image from 'components/base/Image'; +import IconifyIcon from 'components/base/IconifyIcon'; +import PasswordStrengthChecker from '../../components/PasswordStrengthChecker'; + +const ResetPassword = (): ReactElement => { + const [showNewPassword, setShowNewPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [password, setPassword] = useState(''); + + const handleClickShowNewPassword = () => setShowNewPassword(!showNewPassword); + const handleClickShowConfirmPassword = () => setShowConfirmPassword(!showConfirmPassword); + const [resetSuccessful, setResetSuccessful] = useState(false); + + const handleResetPassword = () => { + const passwordField: HTMLInputElement = document.getElementById( + 'new-password', + ) as HTMLInputElement; + const confirmPasswordField: HTMLInputElement = document.getElementById( + 'confirm-password', + ) as HTMLInputElement; + + if (passwordField.value !== confirmPasswordField.value) { + alert("Passwords don't match"); + return; + } + setResetSuccessful(true); + }; + + return ( + theme.shadows[3]} + height={560} + width={{ md: 960 }} + > + + + + + {!resetSuccessful ? ( + + Reset Password + + + Password + + setPassword(e.target.value)} + InputProps={{ + endAdornment: ( + + + {showNewPassword ? ( + + ) : ( + + )} + + + ), + }} + /> + + + + + Password + + + + {showConfirmPassword ? ( + + ) : ( + + )} + + + ), + }} + /> + + + + Back to{' '} + theme.typography.body1.fontSize} + > + Log in + + + + ) : ( + + + Reset Successfully + + Your Ditch the Agent log in password has been updated successfully + + + + )} + + + } + > + {resetSuccessful + + + ); +}; + +export default ResetPassword; diff --git a/ditch-the-agent/src/pages/authentication/SignUp.tsx b/ditch-the-agent/src/pages/authentication/SignUp.tsx index f2b65b7..74d7a8a 100644 --- a/ditch-the-agent/src/pages/authentication/SignUp.tsx +++ b/ditch-the-agent/src/pages/authentication/SignUp.tsx @@ -22,6 +22,7 @@ import IconifyIcon from 'components/base/IconifyIcon'; import logo from 'assets/logo/favicon-logo.png'; import Image from 'components/base/Image'; import { axiosInstance } from '../../axiosApi.js'; +import PasswordStrengthChecker from '../../components/PasswordStrengthChecker'; import { useNavigate } from 'react-router-dom'; type SignUpValues = { @@ -36,6 +37,7 @@ type SignUpValues = { const SignUp = (): ReactElement => { const [showPassword, setShowPassword] = useState(false); const [showPassword2, setShowPassword2] = useState(false); + const [password, setPassword] = useState(''); const [errorMessage, setErrorMessage] = useState(null); @@ -217,7 +219,10 @@ const SignUp = (): ReactElement => { setFieldValue('password', event.target.value)} + onChange={(event) => { + setFieldValue('password', event.target.value); + setPassword(event.target.value); + }} type={showPassword ? 'text' : 'password'} id="password" InputProps={{ @@ -241,6 +246,7 @@ const SignUp = (): ReactElement => { ), }} /> + diff --git a/ditch-the-agent/src/routes/paths.ts b/ditch-the-agent/src/routes/paths.ts index ac51ed0..b8b8f96 100644 --- a/ditch-the-agent/src/routes/paths.ts +++ b/ditch-the-agent/src/routes/paths.ts @@ -1,5 +1,6 @@ export const rootPaths = { homeRoot: '', + dashboardRoot: 'dashboard', pagesRoot: 'pages', applicationsRoot: 'applications', ecommerceRoot: 'ecommerce', @@ -16,12 +17,17 @@ export const rootPaths = { profileRoot: 'profile', offersRoot: 'offers', bidsRoot: 'bids', + documentsRoot: 'documents', vendorBidsRoot: 'vendor-bids', upgradeRoot: 'upgrade', + propertySearchRoot: 'property-search', + supportRoot: 'support', + publicRoot: 'public', }; export default { home: `/${rootPaths.homeRoot}`, + dashboard: `/${rootPaths.dashboardRoot}`, login: `/${rootPaths.authRoot}/login`, signup: `/${rootPaths.authRoot}/sign-up`, resetPassword: `/${rootPaths.authRoot}/reset-password`, @@ -31,7 +37,9 @@ export default { educationLesson: `/${rootPaths.educationRoot}/lesson`, property: `/${rootPaths.propertyRoot}`, propertyDetail: `/${rootPaths.propertyRoot}/:propertyId`, - propertySearch: `/${rootPaths.propertyRoot}/search`, + propertySearch: `/${rootPaths.propertySearchRoot}`, + publicPropertySearch: `/${rootPaths.homeRoot}`, + publicPropertyDetail: `/${rootPaths.publicRoot}/:propertyId`, vendors: `/${rootPaths.vendorsRoot}`, termsOfService: `/${rootPaths.termsOfServiceRoot}`, mortageCalculator: `/${rootPaths.toolsRoot}/mortgage-calculator`, @@ -43,6 +51,9 @@ export default { bids: `/${rootPaths.bidsRoot}/`, vendorBids: `/${rootPaths.vendorBidsRoot}/`, upgrade: `/${rootPaths.upgradeRoot}/`, + documents: `/${rootPaths.documentsRoot}/`, + support: `/${rootPaths.supportRoot}/`, + supportManager: `/${rootPaths.supportRoot}/manager/`, // need to do these pages profile: `/${rootPaths.profileRoot}/`, diff --git a/ditch-the-agent/src/routes/router.tsx b/ditch-the-agent/src/routes/router.tsx index 1b27124..2b78b6d 100644 --- a/ditch-the-agent/src/routes/router.tsx +++ b/ditch-the-agent/src/routes/router.tsx @@ -29,9 +29,25 @@ import ProfilePage from 'pages/Profile/Profile'; import Dashboard from 'pages/home/Dashboard'; import PropertyDetailPage from 'pages/Property/PropertyDetailPage'; import PropertySearchPage from 'pages/Property/PropertySearchPage'; +import PublicPropertySearch from 'pages/Property/PublicPropertySearch'; import BidsPage from 'pages/Bids/Bids'; import VendorBidsPage from 'components/sections/dashboard/Home/Bids/VendorBids'; import UpgradePage from 'pages/Upgrade/UpgradePage'; +import DocumentManager from 'components/sections/dashboard/Home/Documents/DocumentManager'; +import { Typography } from '@mui/material'; +import PublicPropertyDetail from 'pages/Property/PublicPropertyDetail'; +import FAQPage from 'pages/Support/FAQ'; +import SupportManager from 'pages/Support/SupportManager'; + +const RootRedirect = () => { + const { authenticated } = useContext(AuthContext); + + if (authenticated) { + return ; + } + + return ; +}; const App = lazy(() => import('App')); const MainLayout = lazy(async () => { @@ -47,6 +63,13 @@ const AuthLayout = lazy(async () => { ]).then(([moduleExports]) => moduleExports); }); +const PublicLayout = lazy(async () => { + return Promise.all([ + import('layouts/public-layout'), + new Promise((resolve) => setTimeout(resolve, 1000)), + ]).then(([moduleExports]) => moduleExports); +}); + const Error404 = lazy(async () => { await new Promise((resolve) => setTimeout(resolve, 500)); return import('pages/errors/Error404'); @@ -92,7 +115,7 @@ const routes: RouteObject[] = [ ), children: [ { - path: rootPaths.homeRoot, + path: rootPaths.dashboardRoot, element: ( @@ -104,9 +127,41 @@ const routes: RouteObject[] = [ ), children: [ { - path: paths.home, + path: paths.dashboard, element: , - //element: , + }, + ], + }, + { + path: rootPaths.homeRoot, + element: ( + + }> + + + + ), + children: [ + { + path: paths.publicPropertySearch, + element: , + }, + ], + }, + + { + path: rootPaths.publicRoot, + element: ( + + }> + + + + ), + children: [ + { + path: paths.publicPropertyDetail, + element: , }, ], }, @@ -175,6 +230,20 @@ const routes: RouteObject[] = [ path: paths.propertyDetail, element: , }, + ], + }, + { + path: rootPaths.propertySearchRoot, + element: ( + + + }> + + + + + ), + children: [ { path: paths.propertySearch, element: , @@ -259,6 +328,25 @@ const routes: RouteObject[] = [ }, ], }, + { + path: rootPaths.documentsRoot, + + element: ( + + + }> + + + + + ), + children: [ + { + path: paths.documents, + element: , + }, + ], + }, { path: rootPaths.bidsRoot, @@ -316,6 +404,29 @@ const routes: RouteObject[] = [ }, ], }, + { + path: rootPaths.supportRoot, + + element: ( + + + }> + + + + + ), + children: [ + { + path: paths.support, + element: , + }, + { + path: paths.supportManager, + element: , + }, + ], + }, { path: rootPaths.profileRoot, diff --git a/ditch-the-agent/src/types.ts b/ditch-the-agent/src/types.ts index ba640ac..6b7177b 100644 --- a/ditch-the-agent/src/types.ts +++ b/ditch-the-agent/src/types.ts @@ -1,5 +1,9 @@ // src/templates/types.ts +import { HomeImprovementReceiptData } from 'components/sections/dashboard/Home/Documents/Dialog/HomeImprovementReciptDialogContent'; +import { OfferData } from 'components/sections/dashboard/Home/Documents/Dialog/OfferDialogContent'; +import { SellerDisclousureData } from 'components/sections/dashboard/Home/Documents/Dialog/SellerDisclousureDialogContent'; + export interface NavItem { title: string; path: string; @@ -105,6 +109,14 @@ export interface PropertyOwnerAPI { phone_number: string; } +export interface OpenHouseAPI { + id: number; + property: number | PropertiesAPI; // It can be an ID or the full object + start_time: string; + end_time: string; + listed_date: string; +} + export interface VendorAPI { user: UserAPI; business_name: string; @@ -222,10 +234,6 @@ export interface TaxHistoryAPI { year: number; } -export interface OpenHouseAPI { - lsited_date: string; -} - export interface SchoolAPI { id?: number; address: string; @@ -297,6 +305,7 @@ export interface PropertiesAPI { tax_info: TaxHistoryAPI; open_houses?: OpenHouseAPI[]; schools: SchoolAPI[]; + documents?: DocumentAPI[]; } export interface BidImageAPI { @@ -327,6 +336,19 @@ export interface BidAPI { updated_at: string; } +export interface DocumentAPI { + id: number; + property: number; + file: string; + document_type: string; + description?: string; + uploaded_by: number; // or a more detailed user object + shared_with: number[]; + updated_at: string; + created_at: string; + sub_document?: SellerDisclousureData | HomeImprovementReceiptData | OfferData; +} + export interface PropertyRequestAPI extends Omit { owner: number; } @@ -711,4 +733,37 @@ export interface PropertyResponseAPI { compsLookupExecutionTimeMS: string; } +export interface SavedPropertiesAPI { + id: number; + user: number; + property: number; + created_at: string; +} + +export interface FaqApi { + order: number; + answer: string; + question: string; +} + +export interface SupportMessageApi { + id: number; + text: string; + user: id; + user_first_name: string; + user_last_name: string; + created_at: string; + updated_at: string; +} + +export interface SupportCaseApi { + id: number; + title: string; + description: string; + category: string; + status: string; + messages: SupportMessageApi[]; + created_at: string; + updated_at: string; +} // Walk Score API Type Definitions diff --git a/ditch-the-agent/src/utils.tsx b/ditch-the-agent/src/utils.tsx index ca71f88..a2b8518 100644 --- a/ditch-the-agent/src/utils.tsx +++ b/ditch-the-agent/src/utils.tsx @@ -24,3 +24,16 @@ export function extractLatLon(pointString: string): { latitude: number; longitud } return { latitude: 0, longitude: 0 }; // Return null if the string format is not as expected or parsing fails } + +/** + * Formats a number as a currency string using US dollar locale. + * + * @param value The number to format. + * @returns A string representing the formatted currency value (e.g., "$1,234.56"). + */ +export const formatCurrency = (value: number): string => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + }).format(value); +};