Added attorney engagment letter
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user