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;
// 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');

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 { 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<DocumentManagerProps> = ({}) => {
const DocumentManager: React.FC<DocumentManagerProps> = ({ }) => {
const { account, accountLoading } = useContext(AccountContext);
const [searchParams] = useSearchParams();
const [documents, setDocuments] = useState<DocumentAPI[]>([]);
@@ -131,7 +133,7 @@ const DocumentManager: React.FC<DocumentManagerProps> = ({}) => {
: `/properties/${selectedDocument.property}/`;
const { data }: AxiosResponse<PropertiesAPI> = await axiosInstance.get(other_url);
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);
if (!selectedDocument) {
return (
@@ -203,6 +205,14 @@ const DocumentManager: React.FC<DocumentManagerProps> = ({}) => {
// 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 {
return <Typography>Not sure what this is</Typography>;
}

View File

@@ -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<PropertiesAPI | null>(null);
const [loading, setLoading] = useState(true);
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);
useEffect(() => {
@@ -69,7 +69,7 @@ const PropertyDetailPage: React.FC = () => {
const { data }: AxiosResponse<SavedPropertiesAPI[]> =
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 = (
<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({
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(

View File

@@ -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<PropertiesAPI, 'owner' | 'id'> {
@@ -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: {