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