diff --git a/components/ClientEmailSection.vue b/components/ClientEmailSection.vue new file mode 100644 index 0000000..68ae514 --- /dev/null +++ b/components/ClientEmailSection.vue @@ -0,0 +1,425 @@ + + + + + diff --git a/components/EOISection.vue b/components/EOISection.vue index d431787..30809fb 100644 --- a/components/EOISection.vue +++ b/components/EOISection.vue @@ -5,9 +5,31 @@ EOI Management - - -
+ + +
+
EOI Documents
+
+ + {{ doc.title || `EOI Document ${index + 1}` }} + +
+
+ + +
+ + +
+ + {{ hasEOI ? 'Upload Signed EOI' : 'Upload EOI Document' }} + +
@@ -100,6 +134,37 @@
+ + + + + Upload EOI Document + + + + + + Cancel + + Upload + + + +
@@ -115,8 +180,11 @@ const emit = defineEmits<{ 'update': []; }>(); -const { showToast } = useToast(); +const toast = useToast(); const isGenerating = ref(false); +const showUploadDialog = ref(false); +const isUploading = ref(false); +const selectedFile = ref(null); const hasEOI = computed(() => { return !!(props.interest['Signature Link Client'] || @@ -124,7 +192,15 @@ const hasEOI = computed(() => { props.interest['Signature Link Developer']); }); -const generateEOI = async () => { +const eoiDocuments = computed(() => { + return props.interest['EOI Document'] || []; +}); + +const hasEOIDocuments = computed(() => { + return eoiDocuments.value.length > 0; +}); + +const generateEOI = async (retryCount = 0) => { isGenerating.value = true; try { @@ -144,16 +220,27 @@ const generateEOI = async () => { }); if (response.success) { - showToast(response.documentId === 'existing' + toast.success(response.documentId === 'existing' ? 'EOI already exists - signature links retrieved' : 'EOI generated successfully'); emit('eoi-generated', { signingLinks: response.signingLinks }); emit('update'); // Trigger parent to refresh data + } else { + throw new Error('EOI generation failed'); } } catch (error: any) { console.error('Failed to generate EOI:', error); - showToast(error.data?.statusMessage || 'Failed to generate EOI'); + + // Retry logic + if (retryCount < 3) { + console.log(`Retrying EOI generation... Attempt ${retryCount + 2}/4`); + await new Promise(resolve => setTimeout(resolve, (retryCount + 1) * 1000)); + return generateEOI(retryCount + 1); + } + + // Show error message after all retries failed + toast.error(error.data?.statusMessage || error.message || 'Failed to generate EOI after multiple attempts'); } finally { isGenerating.value = false; } @@ -164,7 +251,7 @@ const copyLink = async (link: string | undefined) => { try { await navigator.clipboard.writeText(link); - showToast('Signature link copied to clipboard'); + toast.success('Signature link copied to clipboard'); // Update EOI Time Sent if not already set if (!props.interest['EOI Time Sent']) { @@ -187,7 +274,7 @@ const copyLink = async (link: string | undefined) => { } } } catch (error) { - showToast('Failed to copy link'); + toast.error('Failed to copy link'); } }; @@ -225,4 +312,45 @@ const getStatusColor = (status: string) => { return 'grey'; } }; + +const removeDocument = async (index: number) => { + // For now, we'll just show a message since removing documents + // would require updating the database + toast.warning('Document removal is not yet implemented'); +}; + +const uploadEOI = async (file: File) => { + isUploading.value = true; + + try { + const formData = new FormData(); + formData.append('file', file); + + const response = await $fetch<{ success: boolean; document: any; message: string }>('/api/eoi/upload-document', { + method: 'POST', + query: { + interestId: props.interest.Id.toString() + }, + body: formData + }); + + if (response.success) { + toast.success('EOI document uploaded successfully'); + showUploadDialog.value = false; + selectedFile.value = null; // Reset file selection + emit('update'); // Refresh parent data + } + } catch (error: any) { + console.error('Failed to upload EOI:', error); + toast.error(error.data?.statusMessage || 'Failed to upload EOI document'); + } finally { + isUploading.value = false; + } +}; + +const handleUpload = () => { + if (selectedFile.value) { + uploadEOI(selectedFile.value); + } +}; diff --git a/components/EmailComposer.vue b/components/EmailComposer.vue index 3974d5e..d73a352 100644 --- a/components/EmailComposer.vue +++ b/components/EmailComposer.vue @@ -184,14 +184,12 @@ - -
- -
+ + diff --git a/components/InterestDetailsModal.vue b/components/InterestDetailsModal.vue index a5bd9cf..60f5f25 100644 --- a/components/InterestDetailsModal.vue +++ b/components/InterestDetailsModal.vue @@ -131,7 +131,6 @@ v-if="interest['EOI Time Sent']" color="warning" variant="tonal" - prepend-icon="mdi-email-fast" > EOI Sent: {{ formatDate(interest["EOI Time Sent"]) }} @@ -624,10 +623,11 @@ -
@@ -658,7 +658,7 @@ function debounce any>( return debounced; } import PhoneInput from "./PhoneInput.vue"; -import EmailCommunication from "./EmailCommunication.vue"; +import ClientEmailSection from "./ClientEmailSection.vue"; import EOISection from "./EOISection.vue"; import { InterestSalesProcessLevelFlow, diff --git a/docs/eoi-automation-system.md b/docs/eoi-automation-system.md new file mode 100644 index 0000000..0d1a8c4 --- /dev/null +++ b/docs/eoi-automation-system.md @@ -0,0 +1,235 @@ +# EOI Automation System Documentation + +## Overview + +The EOI (Expression of Interest) automation system provides comprehensive management of EOI documents, including: +- Manual EOI upload capability +- Automated signature status tracking via Documeso API +- Automated reminder emails for unsigned documents +- Automated processing of EOI attachments from sales@portnimara.com + +## Components + +### 1. EOI Section Component (`components/EOISection.vue`) + +**Features:** +- Display EOI documents associated with an interest +- Generate new EOI documents via Documeso +- Upload signed EOI documents manually +- Display signature links for all parties (Client, CC, Developer) +- Track EOI status and signing time + +**Key Functions:** +- `generateEOI()` - Creates new EOI document via Documeso API +- `uploadEOI()` - Uploads PDF documents to MinIO +- `copyLink()` - Copies signature link and tracks when sent + +### 2. Documeso API Integration (`server/utils/documeso.ts`) + +**Configuration:** +- API URL: https://signatures.portnimara.dev/api/v1 +- API Key: Bearer api_malptg62zqyb0wrp + +**Key Functions:** +- `getDocumesoDocument()` - Fetch document by ID +- `getDocumesoDocumentByExternalId()` - Find document by external ID (e.g., 'loi-94') +- `checkDocumentSignatureStatus()` - Check signature status of all recipients +- `getRecipientsToRemind()` - Get recipients who need reminders (after client has signed) + +### 3. Reminder System + +#### API Endpoints: +- `/api/eoi/check-signature-status` - Check signature status of an EOI +- `/api/eoi/send-reminders` - Send reminder emails + +#### Scheduled Tasks (`server/tasks/eoi-reminders.ts`): +- Runs at 9am and 4pm daily (Europe/Paris timezone) +- Checks all interests with EOI documents +- Sends reminders based on rules: + - 4pm only: Reminder to sales if client hasn't signed + - 9am & 4pm: Reminders to CC/Developer if client has signed but they haven't + - Maximum one reminder per 12 hours per interest + +#### Email Templates: +- **Sales Reminder**: Notifies sales team when client hasn't signed +- **Recipient Reminder**: Personalized reminder for CC/Developer to sign + +### 4. Sales Email Processing (`server/api/email/process-sales-eois.ts`) + +**Features:** +- Monitors sales@portnimara.com inbox every 30 minutes +- Processes unread emails with PDF attachments +- Automatically extracts client name from filename or subject +- Uploads EOI documents to MinIO +- Updates interest record with EOI document and status + +**Client Name Extraction Patterns:** +- Filename: "John_Doe_EOI_signed.pdf", "EOI_John_Doe.pdf", "John Doe - EOI.pdf" +- Subject: "EOI for John Doe signed", "Signed EOI - John Doe" + +## Database Schema Updates + +### Interest Table Fields: + +```typescript +// EOI Document storage +'EOI Document': EOIDocument[] // Array of uploaded EOI documents + +// Signature tracking +'Signature Link Client': string +'Signature Link CC': string +'Signature Link Developer': string +'documeso_document_id': string // Documeso document ID + +// Reminder tracking +'reminder_enabled': boolean // Enable/disable reminders +'last_reminder_sent': string // ISO timestamp of last reminder + +// Status tracking +'EOI Status': 'Waiting for Signatures' | 'Signed' +'EOI Time Sent': string // When EOI was first sent +``` + +### EOIDocument Type: + +```typescript +interface EOIDocument { + title: string + filename: string + url: string + size: number + mimetype: string + icon: string + uploadedAt?: string + source?: 'email' | 'manual' + from?: string // Email sender if from email +} +``` + +## API Endpoints + +### Generate EOI Document +```http +POST /api/email/generate-eoi-document +Headers: x-tag: 094ut234 +Body: { interestId: string } +``` + +### Upload EOI Document +```http +POST /api/eoi/upload-document?interestId=123 +Headers: x-tag: 094ut234 +Body: FormData with 'file' field +``` + +### Check Signature Status +```http +GET /api/eoi/check-signature-status?interestId=123 +Headers: x-tag: 094ut234 +``` + +### Send Reminders +```http +POST /api/eoi/send-reminders +Headers: x-tag: 094ut234 +Body: { interestId: string, documentId: string } +``` + +### Process Sales Emails +```http +POST /api/email/process-sales-eois +Headers: x-tag: 094ut234 +``` + +## Email Configuration + +### Reminder Emails (noreply@portnimara.com) +- Host: mail.portnimara.com +- Port: 465 +- Secure: true +- User: noreply@portnimara.com +- Pass: sJw6GW5G5bCI1EtBIq3J2hVm8xCOMw1kQs1puS6g0yABqkrwj + +### Sales Email Monitoring (sales@portnimara.com) +- Host: mail.portnimara.com +- Port: 993 (IMAP) +- TLS: true +- User: sales@portnimara.com +- Pass: MDze7cSClQok8qWOf23X8Mb6lArdk0i42YnwJ1FskdtO2NCc9 + +## Testing + +### Manual Testing Commands + +1. **Generate EOI for Interest #94:** +```javascript +await $fetch('/api/email/generate-eoi-document', { + method: 'POST', + headers: { 'x-tag': '094ut234' }, + body: { interestId: '94' } +}) +``` + +2. **Check Signature Status:** +```javascript +await $fetch('/api/eoi/check-signature-status?interestId=94', { + headers: { 'x-tag': '094ut234' } +}) +``` + +3. **Trigger Reminder Processing:** +```javascript +// In server console +import { triggerReminders } from '~/server/tasks/eoi-reminders' +await triggerReminders() +``` + +4. **Trigger Email Processing:** +```javascript +// In server console +import { triggerEmailProcessing } from '~/server/tasks/process-sales-emails' +await triggerEmailProcessing() +``` + +## Troubleshooting + +### Common Issues: + +1. **EOI Generation Fails** + - Check Documeso API credentials + - Verify interest has required fields (Full Name, Email, etc.) + - Check API rate limits + +2. **Reminders Not Sending** + - Verify SMTP credentials + - Check reminder_enabled field is not false + - Ensure documeso_document_id is set + - Check last_reminder_sent timestamp + +3. **Email Processing Not Working** + - Verify IMAP credentials + - Check sales@portnimara.com inbox access + - Ensure emails have PDF attachments + - Verify client name extraction patterns + +4. **Signature Status Not Updating** + - Check Documeso API connectivity + - Verify document exists in Documeso + - Check external ID format (loi-{interestId}) + +## Security Considerations + +1. All API endpoints require x-tag authentication +2. Email credentials are stored securely +3. Uploaded files are stored in MinIO with access control +4. Signature links are unique and time-limited +5. Reminder emails are sent to verified addresses only + +## Future Enhancements + +1. Add webhook support for real-time signature updates +2. Implement customizable reminder schedules +3. Add email template customization +4. Support for multiple document types beyond EOI +5. Add audit logging for all EOI operations +6. Implement retry queue for failed email processing diff --git a/package-lock.json b/package-lock.json index e98657e..2480daa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "hasInstallScript": true, "dependencies": { "@types/lodash-es": "^4.17.12", + "@types/node-cron": "^3.0.11", "@vite-pwa/nuxt": "^0.10.6", "formidable": "^3.5.4", "imap": "^0.8.19", @@ -14,6 +15,7 @@ "mailparser": "^3.7.3", "mime-types": "^3.0.1", "minio": "^8.0.5", + "node-cron": "^4.1.0", "nodemailer": "^7.0.3", "nuxt": "^3.15.4", "nuxt-directus": "^5.7.0", @@ -23,8 +25,10 @@ "vuetify-nuxt-module": "^0.18.3" }, "devDependencies": { + "@types/formidable": "^3.4.5", "@types/imap": "^0.8.42", "@types/mailparser": "^3.4.6", + "@types/mime-types": "^3.0.1", "@types/nodemailer": "^6.4.17" } }, @@ -3693,6 +3697,16 @@ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "license": "MIT" }, + "node_modules/@types/formidable": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-3.4.5.tgz", + "integrity": "sha512-s7YPsNVfnsng5L8sKnG/Gbb2tiwwJTY1conOkJzTMRvJAlLFW1nEua+ADsJQu8N1c0oTHx9+d5nqg10WuT9gHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/http-proxy": { "version": "1.17.15", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", @@ -3738,6 +3752,13 @@ "iconv-lite": "^0.6.3" } }, + "node_modules/@types/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.12.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.12.0.tgz", @@ -3747,6 +3768,12 @@ "undici-types": "~6.20.0" } }, + "node_modules/@types/node-cron": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", + "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", + "license": "MIT" + }, "node_modules/@types/nodemailer": { "version": "6.4.17", "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", @@ -9446,6 +9473,15 @@ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "license": "MIT" }, + "node_modules/node-cron": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.1.0.tgz", + "integrity": "sha512-OS+3ORu+h03/haS6Di8Qr7CrVs4YaKZZOynZwQpyPZDnR3tqRbwJmuP2gVR16JfhLgyNlloAV1VTrrWlRogCFA==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", diff --git a/package.json b/package.json index 5035bc1..4bf79b9 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "@types/lodash-es": "^4.17.12", + "@types/node-cron": "^3.0.11", "@vite-pwa/nuxt": "^0.10.6", "formidable": "^3.5.4", "imap": "^0.8.19", @@ -16,6 +17,7 @@ "mailparser": "^3.7.3", "mime-types": "^3.0.1", "minio": "^8.0.5", + "node-cron": "^4.1.0", "nodemailer": "^7.0.3", "nuxt": "^3.15.4", "nuxt-directus": "^5.7.0", @@ -25,8 +27,10 @@ "vuetify-nuxt-module": "^0.18.3" }, "devDependencies": { + "@types/formidable": "^3.4.5", "@types/imap": "^0.8.42", "@types/mailparser": "^3.4.6", + "@types/mime-types": "^3.0.1", "@types/nodemailer": "^6.4.17" } } diff --git a/pages/dashboard/file-browser.vue b/pages/dashboard/file-browser.vue index 0915481..d7aea81 100644 --- a/pages/dashboard/file-browser.vue +++ b/pages/dashboard/file-browser.vue @@ -1,7 +1,7 @@