Added attorney engagment letter

This commit is contained in:
2025-11-25 05:37:18 -06:00
parent 5c443bd1b2
commit b3ce2af164
5 changed files with 181 additions and 24 deletions

View File

@@ -57,7 +57,7 @@ axiosInstance.interceptors.response.use(
const originalRequest = error.config; const originalRequest = error.config;
// Prevent infinite loop // 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/'; window.location.href = '/authentication/login/';
//console.log('remove the local storage here') //console.log('remove the local storage here')
return Promise.reject(error); return Promise.reject(error);
@@ -90,6 +90,9 @@ axiosInstance.interceptors.response.use(
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.log(err);
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
window.location.href = '/authentication/login/';
}); });
} else { } else {
console.log('Refresh token is expired'); console.log('Refresh token is expired');

View File

@@ -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<AttorneyEngagementLetterDisplayProps> = ({
letterData,
documentId,
onSignSuccess,
}) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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 (
<Card elevation={3} sx={{ my: 4, borderRadius: 2 }}>
<CardContent>
<Typography variant="h5" component="div" gutterBottom sx={{ fontWeight: 'bold' }}>
Attorney Engagement Letter
</Typography>
<Divider sx={{ my: 2 }} />
<Box sx={{ maxHeight: '400px', overflowY: 'auto', mb: 3, p: 2, bgcolor: 'grey.50', borderRadius: 1 }}>
<Typography variant="body1" paragraph>
<strong>ATTORNEY ENGAGEMENT LETTER</strong>
</Typography>
<Typography variant="body2" paragraph>
This Attorney Engagement Letter ("Agreement") is entered into by and between the Client and the Attorney.
</Typography>
<Typography variant="body2" paragraph>
1. <strong>Scope of Representation.</strong> The Attorney agrees to represent the Client in connection with the sale/purchase of the property.
</Typography>
<Typography variant="body2" paragraph>
2. <strong>Fees.</strong> The Client agrees to pay the Attorney for legal services rendered in accordance with the fee schedule attached hereto.
</Typography>
<Typography variant="body2" paragraph>
3. <strong>Duties.</strong> The Attorney will perform all necessary legal services to close the transaction.
</Typography>
<Typography variant="body2" paragraph>
4. <strong>Termination.</strong> Either party may terminate this Agreement at any time upon written notice.
</Typography>
<Typography variant="body2" paragraph>
[This is a boilerplate contract. Specific details will be updated shortly.]
</Typography>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
<Box sx={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
{isSigned ? (
<Alert severity="success">
Document Signed on {letterData.accepted_at ? new Date(letterData.accepted_at).toLocaleDateString() : 'Unknown Date'}
</Alert>
) : (
<Button
variant="contained"
color="primary"
onClick={handleSign}
disabled={loading}
startIcon={loading ? <CircularProgress size={20} /> : null}
>
{loading ? 'Signing...' : 'Acknowledge & Sign'}
</Button>
)}
</Box>
</CardContent>
</Card>
);
};
export default AttorneyEngagementLetterDisplay;

View File

@@ -28,18 +28,20 @@ import AddDocumentDialog from './AddDocumentDialog';
import SellerDisclosureDisplay from './SellerDisclosureDisplay'; import SellerDisclosureDisplay from './SellerDisclosureDisplay';
import { PropertyOwnerDocumentType } from './Dialog/PropertyOwnerDocumentDialog'; import { PropertyOwnerDocumentType } from './Dialog/PropertyOwnerDocumentDialog';
import HomeImprovementReceiptDisplay from './HomeImprovementReceiptDisplay'; import HomeImprovementReceiptDisplay from './HomeImprovementReceiptDisplay';
import OfferDisplay from './OfferDisplay';
import OfferNegotiationHistory from './OfferNegotiationHistory'; import OfferNegotiationHistory from './OfferNegotiationHistory';
import AttorneyEngagementLetterDisplay from './AttorneyEngagementLetterDisplay';
interface DocumentManagerProps {} interface DocumentManagerProps { }
const getDocumentTitle = (docType: PropertyOwnerDocumentType) => { const getDocumentTitle = (docType: string) => {
if (docType === 'seller_disclosure') { if (docType === 'seller_disclosure') {
return 'Seller Disclosure'; return 'Seller Disclosure';
} else if (docType === 'offer_letter') { } else if (docType === 'offer_letter') {
return 'Offer'; return 'Offer';
} else if (docType === 'home_improvement_receipt') { } else if (docType === 'home_improvement_receipt') {
return 'Home Improvement Receipt'; return 'Home Improvement Receipt';
} else if (docType === 'attorney_engagement_letter') {
return 'Attorney Engagement Letter';
} else { } else {
return docType; return docType;
} }
@@ -54,7 +56,7 @@ const isMyTypeDocument = (upload_by: number, account_id: number, document_type:
} }
}; };
const DocumentManager: React.FC<DocumentManagerProps> = ({}) => { const DocumentManager: React.FC<DocumentManagerProps> = ({ }) => {
const { account, accountLoading } = useContext(AccountContext); const { account, accountLoading } = useContext(AccountContext);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [documents, setDocuments] = useState<DocumentAPI[]>([]); const [documents, setDocuments] = useState<DocumentAPI[]>([]);
@@ -131,7 +133,7 @@ const DocumentManager: React.FC<DocumentManagerProps> = ({}) => {
: `/properties/${selectedDocument.property}/`; : `/properties/${selectedDocument.property}/`;
const { data }: AxiosResponse<PropertiesAPI> = await axiosInstance.get(other_url); const { data }: AxiosResponse<PropertiesAPI> = await axiosInstance.get(other_url);
setSelectedPropertyForDocument(data); setSelectedPropertyForDocument(data);
} catch (error) {} } catch (error) { }
} }
}; };
@@ -152,7 +154,7 @@ const DocumentManager: React.FC<DocumentManagerProps> = ({}) => {
} }
}; };
const getDocumentPaneComponent = (selectedDocument: DocumentAPI) => { const getDocumentPaneComponent = (selectedDocument: DocumentAPI | null) => {
console.log(selectedDocument?.document_type); console.log(selectedDocument?.document_type);
if (!selectedDocument) { if (!selectedDocument) {
return ( return (
@@ -203,6 +205,14 @@ const DocumentManager: React.FC<DocumentManagerProps> = ({}) => {
// documentId={selectedDocument.id} // documentId={selectedDocument.id}
// /> // />
); );
} else if (selectedDocument.document_type === 'attorney_engagement_letter') {
return (
<AttorneyEngagementLetterDisplay
letterData={selectedDocument.sub_document as any}
documentId={selectedDocument.id}
onSignSuccess={() => fetchDocument(selectedDocument.id)}
/>
);
} else { } else {
return <Typography>Not sure what this is</Typography>; return <Typography>Not sure what this is</Typography>;
} }

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useContext } from 'react'; 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 { import {
Container, Container,
Typography, Typography,
@@ -38,7 +38,7 @@ const PropertyDetailPage: React.FC = () => {
const [property, setProperty] = useState<PropertiesAPI | null>(null); const [property, setProperty] = useState<PropertiesAPI | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(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<SavedPropertiesAPI | null>(null); const [savedProperty, setSavedProperty] = useState<SavedPropertiesAPI | null>(null);
useEffect(() => { useEffect(() => {
@@ -69,7 +69,7 @@ const PropertyDetailPage: React.FC = () => {
const { data }: AxiosResponse<SavedPropertiesAPI[]> = const { data }: AxiosResponse<SavedPropertiesAPI[]> =
await axiosInstance.get('/saved-properties/'); await axiosInstance.get('/saved-properties/');
setSavedProperty(data.find((item) => item.property.toString() === propertyId)); setSavedProperty(data.find((item) => item.property.toString() === propertyId));
} catch (error) {} } catch (error) { }
} }
}; };
getProperty(); getProperty();
@@ -92,12 +92,37 @@ const PropertyDetailPage: React.FC = () => {
}); });
setProperty((prev) => (prev ? { ...prev, property_status: value } : null)); setProperty((prev) => (prev ? { ...prev, property_status: value } : null));
setMessage({ type: 'success', text: `Your listing is now ${value}` }); 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 = (
<span>
You cannot list the property as active without a signed Attorney Engagement Letter.
Please go to the{' '}
<Link to="/documents" style={{ color: 'inherit', textDecoration: 'underline' }}>
documents page
</Link>{' '}
to sign it first.
</span>
);
timeoutDuration = 10000;
}
}
setMessage({ setMessage({
type: 'error', type: 'error',
text: 'There was an error saving your selection. Please try again', text: errorMsg,
}); });
setTimeout(() => setMessage(null), 3000); setTimeout(() => setMessage(null), timeoutDuration);
} }
} else { } else {
setMessage({ setMessage({
@@ -117,9 +142,9 @@ const PropertyDetailPage: React.FC = () => {
setProperty((prev) => setProperty((prev) =>
prev prev
? { ? {
...prev, ...prev,
saves: prev.saves - 1, saves: prev.saves - 1,
} }
: null, : null,
); );
} else { } else {
@@ -131,9 +156,9 @@ const PropertyDetailPage: React.FC = () => {
setProperty((prev) => setProperty((prev) =>
prev prev
? { ? {
...prev, ...prev,
saves: prev.saves + 1, saves: prev.saves + 1,
} }
: null, : null,
); );
} }
@@ -218,8 +243,8 @@ const PropertyDetailPage: React.FC = () => {
const sellerDisclosureExists = property.documents const sellerDisclosureExists = property.documents
? property.documents.some( ? property.documents.some(
(doc) => doc.document_type === 'seller_disclosure' && doc.sub_document, (doc) => doc.document_type === 'seller_disclosure' && doc.sub_document,
) )
: false; : false;
const disclosureDocument = property.documents?.find( const disclosureDocument = property.documents?.find(

View File

@@ -336,6 +336,12 @@ export interface BidAPI {
updated_at: string; updated_at: string;
} }
export interface AttorneyEngagementLetterData {
is_accepted: boolean;
accepted_at: string | null;
attorney: number;
}
export interface DocumentAPI { export interface DocumentAPI {
id: number; id: number;
property: number; property: number;
@@ -346,7 +352,11 @@ export interface DocumentAPI {
shared_with: number[]; shared_with: number[];
updated_at: string; updated_at: string;
created_at: string; created_at: string;
sub_document?: SellerDisclousureData | HomeImprovementReceiptData | OfferData; sub_document?:
| SellerDisclousureData
| HomeImprovementReceiptData
| OfferData
| AttorneyEngagementLetterData;
} }
export interface PropertyRequestAPI extends Omit<PropertiesAPI, 'owner' | 'id'> { export interface PropertyRequestAPI extends Omit<PropertiesAPI, 'owner' | 'id'> {
@@ -459,9 +469,9 @@ export interface PropertyResponseDataSaleAPI {
transactionType: string; transactionType: string;
} }
export interface PropertyResponseDataLotInfoAPI {} export interface PropertyResponseDataLotInfoAPI { }
export interface PropertyResponseDataMortgageHistoryAPI {} export interface PropertyResponseDataMortgageHistoryAPI { }
export interface PropertyResponseDataOnwerInfoAPI { export interface PropertyResponseDataOnwerInfoAPI {
mailAddress: { mailAddress: {