diff --git a/ditch-the-agent/src/axiosApi.js b/ditch-the-agent/src/axiosApi.js index f1f51f8..6b3d784 100644 --- a/ditch-the-agent/src/axiosApi.js +++ b/ditch-the-agent/src/axiosApi.js @@ -57,7 +57,7 @@ axiosInstance.interceptors.response.use( const originalRequest = error.config; // Prevent infinite loop - if (error.response.status === 401 && originalRequest.url === baseURL + '/token/refresh/') { + if (error.response.status === 401 && originalRequest.url.includes('/token/refresh/')) { window.location.href = '/authentication/login/'; //console.log('remove the local storage here') return Promise.reject(error); @@ -90,6 +90,9 @@ axiosInstance.interceptors.response.use( }) .catch((err) => { console.log(err); + localStorage.removeItem('access_token'); + localStorage.removeItem('refresh_token'); + window.location.href = '/authentication/login/'; }); } else { console.log('Refresh token is expired'); diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Documents/AttorneyEngagementLetterDisplay.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/AttorneyEngagementLetterDisplay.tsx new file mode 100644 index 0000000..a21e796 --- /dev/null +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/AttorneyEngagementLetterDisplay.tsx @@ -0,0 +1,109 @@ +import React, { useState } from 'react'; +import { + Box, + Typography, + Card, + CardContent, + Divider, + Button, + CircularProgress, + Alert, +} from '@mui/material'; +import { AttorneyEngagementLetterData } from 'types'; +import { axiosInstance } from '../../../../../axiosApi'; + +interface AttorneyEngagementLetterDisplayProps { + letterData: AttorneyEngagementLetterData; + documentId: number; + onSignSuccess?: () => void; +} + +const AttorneyEngagementLetterDisplay: React.FC = ({ + letterData, + documentId, + onSignSuccess, +}) => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [isSigned, setIsSigned] = useState(letterData.is_accepted); + + const handleSign = async () => { + setLoading(true); + setError(null); + try { + // POST to sign the document + await axiosInstance.post(`/document/${documentId}/sign/`); + setIsSigned(true); + if (onSignSuccess) { + onSignSuccess(); + } + } catch (err) { + console.error('Failed to sign document:', err); + setError('Failed to sign the document. Please try again.'); + } finally { + setLoading(false); + } + }; + + return ( + + + + Attorney Engagement Letter + + + + + + + ATTORNEY ENGAGEMENT LETTER + + + This Attorney Engagement Letter ("Agreement") is entered into by and between the Client and the Attorney. + + + 1. Scope of Representation. The Attorney agrees to represent the Client in connection with the sale/purchase of the property. + + + 2. Fees. The Client agrees to pay the Attorney for legal services rendered in accordance with the fee schedule attached hereto. + + + 3. Duties. The Attorney will perform all necessary legal services to close the transaction. + + + 4. Termination. Either party may terminate this Agreement at any time upon written notice. + + + [This is a boilerplate contract. Specific details will be updated shortly.] + + + + {error && ( + + {error} + + )} + + + {isSigned ? ( + + Document Signed on {letterData.accepted_at ? new Date(letterData.accepted_at).toLocaleDateString() : 'Unknown Date'} + + ) : ( + + )} + + + + ); +}; + +export default AttorneyEngagementLetterDisplay; 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 index 96873f3..415c757 100644 --- a/ditch-the-agent/src/components/sections/dashboard/Home/Documents/DocumentManager.tsx +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Documents/DocumentManager.tsx @@ -28,18 +28,20 @@ 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'; +import AttorneyEngagementLetterDisplay from './AttorneyEngagementLetterDisplay'; -interface DocumentManagerProps {} +interface DocumentManagerProps { } -const getDocumentTitle = (docType: PropertyOwnerDocumentType) => { +const getDocumentTitle = (docType: string) => { 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 if (docType === 'attorney_engagement_letter') { + return 'Attorney Engagement Letter'; } else { return docType; } @@ -54,7 +56,7 @@ const isMyTypeDocument = (upload_by: number, account_id: number, document_type: } }; -const DocumentManager: React.FC = ({}) => { +const DocumentManager: React.FC = ({ }) => { const { account, accountLoading } = useContext(AccountContext); const [searchParams] = useSearchParams(); const [documents, setDocuments] = useState([]); @@ -131,7 +133,7 @@ const DocumentManager: React.FC = ({}) => { : `/properties/${selectedDocument.property}/`; const { data }: AxiosResponse = await axiosInstance.get(other_url); setSelectedPropertyForDocument(data); - } catch (error) {} + } catch (error) { } } }; @@ -152,7 +154,7 @@ const DocumentManager: React.FC = ({}) => { } }; - const getDocumentPaneComponent = (selectedDocument: DocumentAPI) => { + const getDocumentPaneComponent = (selectedDocument: DocumentAPI | null) => { console.log(selectedDocument?.document_type); if (!selectedDocument) { return ( @@ -203,6 +205,14 @@ const DocumentManager: React.FC = ({}) => { // documentId={selectedDocument.id} // /> ); + } else if (selectedDocument.document_type === 'attorney_engagement_letter') { + return ( + fetchDocument(selectedDocument.id)} + /> + ); } else { return Not sure what this is; } diff --git a/ditch-the-agent/src/pages/Property/PropertyDetailPage.tsx b/ditch-the-agent/src/pages/Property/PropertyDetailPage.tsx index a1a338f..e03d4e8 100644 --- a/ditch-the-agent/src/pages/Property/PropertyDetailPage.tsx +++ b/ditch-the-agent/src/pages/Property/PropertyDetailPage.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useContext } from 'react'; -import { useLocation, useParams, useNavigate } from 'react-router-dom'; +import { useLocation, useParams, useNavigate, Link } from 'react-router-dom'; import { Container, Typography, @@ -38,7 +38,7 @@ const PropertyDetailPage: React.FC = () => { const [property, setProperty] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + const [message, setMessage] = useState<{ type: 'success' | 'error'; text: React.ReactNode } | null>(null); const [savedProperty, setSavedProperty] = useState(null); useEffect(() => { @@ -69,7 +69,7 @@ const PropertyDetailPage: React.FC = () => { const { data }: AxiosResponse = await axiosInstance.get('/saved-properties/'); setSavedProperty(data.find((item) => item.property.toString() === propertyId)); - } catch (error) {} + } catch (error) { } } }; getProperty(); @@ -92,12 +92,37 @@ const PropertyDetailPage: React.FC = () => { }); setProperty((prev) => (prev ? { ...prev, property_status: value } : null)); setMessage({ type: 'success', text: `Your listing is now ${value}` }); - } catch (error) { + } catch (error: any) { + let errorMsg: React.ReactNode = 'There was an error saving your selection. Please try again'; + let timeoutDuration = 3000; + + if (axios.isAxiosError(error) && error.response?.data) { + const data = error.response.data; + const errorString = JSON.stringify(data); + if ( + errorString.includes( + 'Cannot list property as active without an accepted Attorney Engagement Letter', + ) + ) { + errorMsg = ( + + You cannot list the property as active without a signed Attorney Engagement Letter. + Please go to the{' '} + + documents page + {' '} + to sign it first. + + ); + timeoutDuration = 10000; + } + } + setMessage({ type: 'error', - text: 'There was an error saving your selection. Please try again', + text: errorMsg, }); - setTimeout(() => setMessage(null), 3000); + setTimeout(() => setMessage(null), timeoutDuration); } } else { setMessage({ @@ -117,9 +142,9 @@ const PropertyDetailPage: React.FC = () => { setProperty((prev) => prev ? { - ...prev, - saves: prev.saves - 1, - } + ...prev, + saves: prev.saves - 1, + } : null, ); } else { @@ -131,9 +156,9 @@ const PropertyDetailPage: React.FC = () => { setProperty((prev) => prev ? { - ...prev, - saves: prev.saves + 1, - } + ...prev, + saves: prev.saves + 1, + } : null, ); } @@ -218,8 +243,8 @@ const PropertyDetailPage: React.FC = () => { const sellerDisclosureExists = property.documents ? property.documents.some( - (doc) => doc.document_type === 'seller_disclosure' && doc.sub_document, - ) + (doc) => doc.document_type === 'seller_disclosure' && doc.sub_document, + ) : false; const disclosureDocument = property.documents?.find( diff --git a/ditch-the-agent/src/types.ts b/ditch-the-agent/src/types.ts index da30177..5e09eaa 100644 --- a/ditch-the-agent/src/types.ts +++ b/ditch-the-agent/src/types.ts @@ -336,6 +336,12 @@ export interface BidAPI { updated_at: string; } +export interface AttorneyEngagementLetterData { + is_accepted: boolean; + accepted_at: string | null; + attorney: number; +} + export interface DocumentAPI { id: number; property: number; @@ -346,7 +352,11 @@ export interface DocumentAPI { shared_with: number[]; updated_at: string; created_at: string; - sub_document?: SellerDisclousureData | HomeImprovementReceiptData | OfferData; + sub_document?: + | SellerDisclousureData + | HomeImprovementReceiptData + | OfferData + | AttorneyEngagementLetterData; } export interface PropertyRequestAPI extends Omit { @@ -459,9 +469,9 @@ export interface PropertyResponseDataSaleAPI { transactionType: string; } -export interface PropertyResponseDataLotInfoAPI {} +export interface PropertyResponseDataLotInfoAPI { } -export interface PropertyResponseDataMortgageHistoryAPI {} +export interface PropertyResponseDataMortgageHistoryAPI { } export interface PropertyResponseDataOnwerInfoAPI { mailAddress: {