feat: quick-create customer/vendor from unknown email sender
This commit is contained in:
parent
14b13b8239
commit
07584b1b0c
@ -5,13 +5,14 @@ API endpoints for email viewing, classification, and rule management
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from fastapi import APIRouter, HTTPException, Query, UploadFile, File
|
from fastapi import APIRouter, HTTPException, Query, UploadFile, File
|
||||||
from typing import List, Optional
|
from typing import List, Optional, Dict
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
|
|
||||||
from app.core.database import execute_query, execute_insert, execute_update
|
from app.core.database import execute_query, execute_insert, execute_update
|
||||||
from app.services.email_processor_service import EmailProcessorService
|
from app.services.email_processor_service import EmailProcessorService
|
||||||
from app.services.email_workflow_service import email_workflow_service
|
from app.services.email_workflow_service import email_workflow_service
|
||||||
|
from app.services.ollama_service import ollama_service
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -76,6 +77,8 @@ class EmailDetail(BaseModel):
|
|||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
attachments: List[EmailAttachment] = []
|
attachments: List[EmailAttachment] = []
|
||||||
|
customer_name: Optional[str] = None
|
||||||
|
supplier_name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class EmailRule(BaseModel):
|
class EmailRule(BaseModel):
|
||||||
@ -204,8 +207,13 @@ async def get_email(email_id: int):
|
|||||||
"""Get email detail by ID"""
|
"""Get email detail by ID"""
|
||||||
try:
|
try:
|
||||||
query = """
|
query = """
|
||||||
SELECT * FROM email_messages
|
SELECT em.*,
|
||||||
WHERE id = %s AND deleted_at IS NULL
|
c.name AS customer_name,
|
||||||
|
v.name AS supplier_name
|
||||||
|
FROM email_messages em
|
||||||
|
LEFT JOIN customers c ON em.customer_id = c.id
|
||||||
|
LEFT JOIN vendors v ON em.supplier_id = v.id
|
||||||
|
WHERE em.id = %s AND em.deleted_at IS NULL
|
||||||
"""
|
"""
|
||||||
result = execute_query(query, (email_id,))
|
result = execute_query(query, (email_id,))
|
||||||
logger.info(f"🔍 Query result type: {type(result)}, length: {len(result) if result else 0}")
|
logger.info(f"🔍 Query result type: {type(result)}, length: {len(result) if result else 0}")
|
||||||
@ -334,6 +342,161 @@ async def update_email(email_id: int, status: Optional[str] = None):
|
|||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/emails/{email_id}/link")
|
||||||
|
async def link_email(email_id: int, payload: Dict):
|
||||||
|
"""Link email to a customer and/or vendor/supplier"""
|
||||||
|
try:
|
||||||
|
updates = []
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if 'customer_id' in payload:
|
||||||
|
updates.append("customer_id = %s")
|
||||||
|
params.append(payload['customer_id'])
|
||||||
|
|
||||||
|
if 'supplier_id' in payload:
|
||||||
|
updates.append("supplier_id = %s")
|
||||||
|
params.append(payload['supplier_id'])
|
||||||
|
|
||||||
|
if not updates:
|
||||||
|
raise HTTPException(status_code=400, detail="Ingen felter at opdatere")
|
||||||
|
|
||||||
|
params.append(email_id)
|
||||||
|
query = f"UPDATE email_messages SET {', '.join(updates)}, updated_at = CURRENT_TIMESTAMP WHERE id = %s"
|
||||||
|
execute_update(query, tuple(params))
|
||||||
|
|
||||||
|
logger.info(f"✅ Linked email {email_id}: {payload}")
|
||||||
|
return {"success": True, "message": "Email linket"}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error linking email {email_id}: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/emails/{email_id}/extract-vendor-suggestion")
|
||||||
|
async def extract_vendor_suggestion(email_id: int):
|
||||||
|
"""
|
||||||
|
Forsøger at udtrække leverandørinfo fra email body og vedhæftede fakturaer.
|
||||||
|
Returnerer forslag til navn, CVR, adresse, telefon, email, domæne.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Hent email
|
||||||
|
email_result = execute_query(
|
||||||
|
"SELECT * FROM email_messages WHERE id = %s AND deleted_at IS NULL",
|
||||||
|
(email_id,)
|
||||||
|
)
|
||||||
|
if not email_result:
|
||||||
|
raise HTTPException(status_code=404, detail="Email ikke fundet")
|
||||||
|
email = email_result[0]
|
||||||
|
|
||||||
|
# Saml tekst fra body + vedhæftede PDF-filer
|
||||||
|
text_parts = []
|
||||||
|
if email.get('body_text'):
|
||||||
|
text_parts.append(email['body_text'])
|
||||||
|
|
||||||
|
attachments = execute_query(
|
||||||
|
"SELECT * FROM email_attachments WHERE email_id = %s ORDER BY id",
|
||||||
|
(email_id,)
|
||||||
|
)
|
||||||
|
for att in (attachments or []):
|
||||||
|
file_path = att.get('file_path')
|
||||||
|
if file_path and os.path.exists(file_path):
|
||||||
|
content_type = att.get('content_type', '')
|
||||||
|
if 'pdf' in content_type or file_path.lower().endswith('.pdf'):
|
||||||
|
try:
|
||||||
|
pdf_text = await ollama_service._extract_text_from_file(file_path)
|
||||||
|
if pdf_text:
|
||||||
|
text_parts.append(f"[PDF: {att['filename']}]\n{pdf_text}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"⚠️ Kunne ikke læse PDF {file_path}: {e}")
|
||||||
|
|
||||||
|
combined_text = "\n\n".join(text_parts)
|
||||||
|
|
||||||
|
suggestion = {
|
||||||
|
"name": email.get('sender_name') or '',
|
||||||
|
"email": email.get('sender_email') or '',
|
||||||
|
"cvr_number": None,
|
||||||
|
"phone": None,
|
||||||
|
"address": None,
|
||||||
|
"domain": None,
|
||||||
|
"source": "regex"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Regex fallback: uddrag CVR (8 cifre efter CVR/Momsnr/DK)
|
||||||
|
cvr_match = re.search(
|
||||||
|
r'(?:CVR|Cvr|cvr|Momsnr\.?|DK)[\s:.-]*([0-9]{8})',
|
||||||
|
combined_text
|
||||||
|
)
|
||||||
|
if cvr_match:
|
||||||
|
suggestion['cvr_number'] = cvr_match.group(1)
|
||||||
|
|
||||||
|
# Regex: telefon (dansk format)
|
||||||
|
phone_match = re.search(
|
||||||
|
r'(?:Tlf|Tel|Telefon|Phone)[\s.:]*([+\d][\d\s\-().]{6,15})',
|
||||||
|
combined_text, re.IGNORECASE
|
||||||
|
)
|
||||||
|
if phone_match:
|
||||||
|
suggestion['phone'] = phone_match.group(1).strip()
|
||||||
|
|
||||||
|
# Domæne fra sender email
|
||||||
|
if email.get('sender_email') and '@' in email['sender_email']:
|
||||||
|
suggestion['domain'] = email['sender_email'].split('@')[1]
|
||||||
|
|
||||||
|
# Brug AI hvis vi har tekst fra PDF
|
||||||
|
if len(combined_text) > 100:
|
||||||
|
try:
|
||||||
|
from app.core.config import settings
|
||||||
|
own_cvr = getattr(settings, 'OWN_CVR', '')
|
||||||
|
|
||||||
|
prompt = f"""OPGAVE: Udtræk leverandørens firmainfo fra denne tekst.
|
||||||
|
TEKSTEN er body/footer fra en faktura-email eller vedhæftet faktura.
|
||||||
|
RETURNER KUN VALID JSON - ingen forklaring!
|
||||||
|
|
||||||
|
{{
|
||||||
|
\"name\": \"Firmanavn ApS\",
|
||||||
|
\"cvr_number\": \"12345678\",
|
||||||
|
\"address\": \"Vejnavn 1, 2000 By\",
|
||||||
|
\"phone\": \"12345678\",
|
||||||
|
\"email\": \"kontakt@firma.dk\",
|
||||||
|
\"domain\": \"firma.dk\"
|
||||||
|
}}
|
||||||
|
|
||||||
|
REGLER:
|
||||||
|
- name: LEVERANDØRENS firmanavn (ikke køber, ikke BMC)
|
||||||
|
- cvr_number: 8-cifret CVR - IGNORER {own_cvr} (det er køber)
|
||||||
|
- Sæt null hvis ikke fundet
|
||||||
|
- KUN JSON output!
|
||||||
|
|
||||||
|
TEKST (max 3000 tegn):
|
||||||
|
{combined_text[:3000]}
|
||||||
|
|
||||||
|
KUN JSON:"""
|
||||||
|
|
||||||
|
ai_result = await ollama_service.extract_from_text(prompt)
|
||||||
|
if ai_result and isinstance(ai_result, dict):
|
||||||
|
# Merge AI resultat ind i suggestion (AI prioriteres over regex)
|
||||||
|
for field in ('name', 'cvr_number', 'address', 'phone', 'email', 'domain'):
|
||||||
|
val = ai_result.get(field)
|
||||||
|
if val and val not in (None, 'null', '', 'N/A'):
|
||||||
|
suggestion[field] = str(val).strip()
|
||||||
|
suggestion['source'] = 'ai'
|
||||||
|
logger.info(f"✅ AI vendor suggestion for email {email_id}: {suggestion}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"⚠️ AI udtræk fejlede, bruger regex-resultat: {e}")
|
||||||
|
|
||||||
|
return suggestion
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ extract-vendor-suggestion fejlede for email {email_id}: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/emails/{email_id}")
|
@router.delete("/emails/{email_id}")
|
||||||
async def delete_email(email_id: int):
|
async def delete_email(email_id: int):
|
||||||
"""Soft delete email"""
|
"""Soft delete email"""
|
||||||
|
|||||||
@ -1763,6 +1763,34 @@ function renderEmailAnalysis(email) {
|
|||||||
|
|
||||||
// Opdater kun AI Analysis tab indholdet - ikke hele sidebar
|
// Opdater kun AI Analysis tab indholdet - ikke hele sidebar
|
||||||
aiAnalysisTab.innerHTML = `
|
aiAnalysisTab.innerHTML = `
|
||||||
|
${!email.customer_id && !email.supplier_id ? `
|
||||||
|
<div class="analysis-card border border-warning">
|
||||||
|
<h6 class="text-warning"><i class="bi bi-person-question-fill me-2"></i>Ukendt Afsender</h6>
|
||||||
|
<div class="text-muted small mb-2">
|
||||||
|
<i class="bi bi-envelope me-1"></i>${escapeHtml(email.sender_email || '')}
|
||||||
|
${email.sender_name ? `<br><i class="bi bi-person me-1"></i>${escapeHtml(email.sender_name)}` : ''}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-column gap-2">
|
||||||
|
<button class="btn btn-sm btn-outline-primary w-100"
|
||||||
|
onclick="quickCreateCustomer(${email.id}, '${escapeHtml(email.sender_name || '')}', '${escapeHtml(email.sender_email || '')}')">
|
||||||
|
<i class="bi bi-person-plus me-2"></i>Opret som Kunde
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary w-100"
|
||||||
|
onclick="quickCreateVendor(${email.id}, '${escapeHtml(email.sender_name || '')}', '${escapeHtml(email.sender_email || '')}')">
|
||||||
|
<i class="bi bi-shop me-2"></i>Opret som Leverandør
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : `
|
||||||
|
<div class="analysis-card">
|
||||||
|
<h6 class="text-success"><i class="bi bi-person-check-fill me-2"></i>Linket Til</h6>
|
||||||
|
<ul class="metadata-list">
|
||||||
|
${email.customer_id ? `<li class="metadata-item"><div class="metadata-label">Kunde</div><div class="metadata-value">${escapeHtml(email.customer_name || '#' + email.customer_id)}</div></li>` : ''}
|
||||||
|
${email.supplier_id ? `<li class="metadata-item"><div class="metadata-label">Leverandør</div><div class="metadata-value">${escapeHtml(email.supplier_name || '#' + email.supplier_id)}</div></li>` : ''}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
|
||||||
${getClassificationActions(email) ? `
|
${getClassificationActions(email) ? `
|
||||||
<div class="analysis-card">
|
<div class="analysis-card">
|
||||||
<h6><i class="bi bi-lightning-charge-fill me-2"></i>Hurtig Action</h6>
|
<h6><i class="bi bi-lightning-charge-fill me-2"></i>Hurtig Action</h6>
|
||||||
@ -2251,6 +2279,141 @@ async function linkToCustomer(emailId) {
|
|||||||
showError('Kunde-linking er ikke implementeret endnu');
|
showError('Kunde-linking er ikke implementeret endnu');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Quick Create Customer ────────────────────────────────────────────────
|
||||||
|
function quickCreateCustomer(emailId, senderName, senderEmail) {
|
||||||
|
document.getElementById('qcEmailId').value = emailId;
|
||||||
|
document.getElementById('qcCustomerName').value = senderName || '';
|
||||||
|
document.getElementById('qcCustomerEmail').value = senderEmail || '';
|
||||||
|
document.getElementById('qcCustomerPhone').value = '';
|
||||||
|
document.getElementById('qcCustomerStatus').textContent = '';
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('quickCreateCustomerModal'));
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitQuickCustomer() {
|
||||||
|
const emailId = document.getElementById('qcEmailId').value;
|
||||||
|
const name = document.getElementById('qcCustomerName').value.trim();
|
||||||
|
const email = document.getElementById('qcCustomerEmail').value.trim();
|
||||||
|
const phone = document.getElementById('qcCustomerPhone').value.trim();
|
||||||
|
const statusEl = document.getElementById('qcCustomerStatus');
|
||||||
|
|
||||||
|
if (!name) { statusEl.className = 'text-danger small'; statusEl.textContent = 'Navn er påkrævet'; return; }
|
||||||
|
statusEl.className = 'text-muted small'; statusEl.textContent = 'Opretter…';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create customer
|
||||||
|
const custResp = await fetch('/api/v1/customers', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name, email: email || null, phone: phone || null })
|
||||||
|
});
|
||||||
|
if (!custResp.ok) throw new Error((await custResp.json()).detail || 'Opret fejlede');
|
||||||
|
const customer = await custResp.json();
|
||||||
|
|
||||||
|
// Link email
|
||||||
|
await fetch(`/api/v1/emails/${emailId}/link`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ customer_id: customer.id })
|
||||||
|
});
|
||||||
|
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('quickCreateCustomerModal')).hide();
|
||||||
|
showSuccess(`Kunde "${name}" oprettet og linket`);
|
||||||
|
loadEmailDetail(parseInt(emailId));
|
||||||
|
} catch (e) {
|
||||||
|
statusEl.className = 'text-danger small';
|
||||||
|
statusEl.textContent = e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Quick Create Vendor ──────────────────────────────────────────────────
|
||||||
|
async function quickCreateVendor(emailId, senderName, senderEmail) {
|
||||||
|
// Reset form
|
||||||
|
document.getElementById('qvEmailId').value = emailId;
|
||||||
|
document.getElementById('qvVendorName').value = senderName || '';
|
||||||
|
document.getElementById('qvVendorEmail').value = senderEmail || '';
|
||||||
|
document.getElementById('qvVendorCVR').value = '';
|
||||||
|
document.getElementById('qvVendorPhone').value = '';
|
||||||
|
document.getElementById('qvVendorAddress').value = '';
|
||||||
|
document.getElementById('qvVendorDomain').value = senderEmail && senderEmail.includes('@') ? senderEmail.split('@')[1] : '';
|
||||||
|
document.getElementById('qvVendorStatus').textContent = '';
|
||||||
|
document.getElementById('qvAiStatus').innerHTML = '<span class="text-muted small"><i class="bi bi-hourglass-split me-1"></i>Henter info fra email/bilag…</span>';
|
||||||
|
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('quickCreateVendorModal'));
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
// Attempt AI extraction in background
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/v1/emails/${emailId}/extract-vendor-suggestion`, { method: 'POST' });
|
||||||
|
if (resp.ok) {
|
||||||
|
const s = await resp.json();
|
||||||
|
if (s.name && s.name !== senderName) document.getElementById('qvVendorName').value = s.name;
|
||||||
|
if (s.cvr_number) document.getElementById('qvVendorCVR').value = s.cvr_number;
|
||||||
|
if (s.phone) document.getElementById('qvVendorPhone').value = s.phone;
|
||||||
|
if (s.address) document.getElementById('qvVendorAddress').value = s.address;
|
||||||
|
if (s.domain) document.getElementById('qvVendorDomain').value = s.domain;
|
||||||
|
if (s.email && !document.getElementById('qvVendorEmail').value)
|
||||||
|
document.getElementById('qvVendorEmail').value = s.email;
|
||||||
|
|
||||||
|
const srcLabel = s.source === 'ai' ? '🤖 AI' : '🔍 Regex';
|
||||||
|
document.getElementById('qvAiStatus').innerHTML =
|
||||||
|
`<span class="text-success small"><i class="bi bi-check-circle me-1"></i>${srcLabel} – felter preudfyldt</span>`;
|
||||||
|
} else {
|
||||||
|
document.getElementById('qvAiStatus').innerHTML =
|
||||||
|
'<span class="text-muted small"><i class="bi bi-info-circle me-1"></i>Ingen automatisk data fundet</span>';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('qvAiStatus').innerHTML =
|
||||||
|
'<span class="text-muted small"><i class="bi bi-info-circle me-1"></i>Udfyld manuelt</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitQuickVendor() {
|
||||||
|
const emailId = document.getElementById('qvEmailId').value;
|
||||||
|
const name = document.getElementById('qvVendorName').value.trim();
|
||||||
|
const email = document.getElementById('qvVendorEmail').value.trim();
|
||||||
|
const cvr = document.getElementById('qvVendorCVR').value.trim();
|
||||||
|
const phone = document.getElementById('qvVendorPhone').value.trim();
|
||||||
|
const address = document.getElementById('qvVendorAddress').value.trim();
|
||||||
|
const domain = document.getElementById('qvVendorDomain').value.trim();
|
||||||
|
const statusEl = document.getElementById('qvVendorStatus');
|
||||||
|
|
||||||
|
if (!name) { statusEl.className = 'text-danger small'; statusEl.textContent = 'Navn er påkrævet'; return; }
|
||||||
|
statusEl.className = 'text-muted small'; statusEl.textContent = 'Opretter…';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create vendor
|
||||||
|
const vendResp = await fetch('/api/v1/vendors', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
name,
|
||||||
|
email: email || null,
|
||||||
|
cvr_number: cvr || null,
|
||||||
|
phone: phone || null,
|
||||||
|
address: address || null,
|
||||||
|
domain: domain || null
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (!vendResp.ok) throw new Error((await vendResp.json()).detail || 'Opret fejlede');
|
||||||
|
const vendor = await vendResp.json();
|
||||||
|
|
||||||
|
// Link email
|
||||||
|
await fetch(`/api/v1/emails/${emailId}/link`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ supplier_id: vendor.id })
|
||||||
|
});
|
||||||
|
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('quickCreateVendorModal')).hide();
|
||||||
|
showSuccess(`Leverandør "${name}" oprettet og linket`);
|
||||||
|
loadEmailDetail(parseInt(emailId));
|
||||||
|
} catch (e) {
|
||||||
|
statusEl.className = 'text-danger small';
|
||||||
|
statusEl.textContent = e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveClassification() {
|
async function saveClassification() {
|
||||||
if (!currentEmailId) return;
|
if (!currentEmailId) return;
|
||||||
|
|
||||||
@ -4115,4 +4278,88 @@ async function uploadEmailFiles(files) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Quick Create Customer Modal -->
|
||||||
|
<div class="modal fade" id="quickCreateCustomerModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><i class="bi bi-person-plus me-2"></i>Opret Kunde</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="qcEmailId">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Navn <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control" id="qcCustomerName" placeholder="Firmanavn eller kontaktperson">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Email</label>
|
||||||
|
<input type="email" class="form-control" id="qcCustomerEmail">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Telefon</label>
|
||||||
|
<input type="text" class="form-control" id="qcCustomerPhone">
|
||||||
|
</div>
|
||||||
|
<div id="qcCustomerStatus"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" data-bs-dismiss="modal">Annuller</button>
|
||||||
|
<button class="btn btn-primary" onclick="submitQuickCustomer()">
|
||||||
|
<i class="bi bi-person-plus me-2"></i>Opret & Link
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Create Vendor Modal -->
|
||||||
|
<div class="modal fade" id="quickCreateVendorModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><i class="bi bi-shop me-2"></i>Opret Leverandør</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="qvEmailId">
|
||||||
|
<div id="qvAiStatus" class="mb-3"></div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Navn <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control" id="qvVendorName" placeholder="Firmanavn">
|
||||||
|
</div>
|
||||||
|
<div class="row g-2 mb-3">
|
||||||
|
<div class="col-6">
|
||||||
|
<label class="form-label fw-semibold">CVR-nummer</label>
|
||||||
|
<input type="text" class="form-control" id="qvVendorCVR" maxlength="8" placeholder="12345678">
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<label class="form-label fw-semibold">Telefon</label>
|
||||||
|
<input type="text" class="form-control" id="qvVendorPhone">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Email</label>
|
||||||
|
<input type="email" class="form-control" id="qvVendorEmail">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Adresse</label>
|
||||||
|
<input type="text" class="form-control" id="qvVendorAddress" placeholder="Vejnavn 1, 1234 By">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Domæne</label>
|
||||||
|
<input type="text" class="form-control" id="qvVendorDomain" placeholder="firma.dk">
|
||||||
|
</div>
|
||||||
|
<div id="qvVendorStatus"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" data-bs-dismiss="modal">Annuller</button>
|
||||||
|
<button class="btn btn-primary" onclick="submitQuickVendor()">
|
||||||
|
<i class="bi bi-shop me-2"></i>Opret & Link
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -74,9 +74,16 @@ class VendorBase(BaseModel):
|
|||||||
domain: Optional[str] = None
|
domain: Optional[str] = None
|
||||||
email: Optional[str] = None
|
email: Optional[str] = None
|
||||||
phone: Optional[str] = None
|
phone: Optional[str] = None
|
||||||
|
address: Optional[str] = None
|
||||||
|
postal_code: Optional[str] = None
|
||||||
|
city: Optional[str] = None
|
||||||
|
website: Optional[str] = None
|
||||||
|
email_pattern: Optional[str] = None
|
||||||
contact_person: Optional[str] = None
|
contact_person: Optional[str] = None
|
||||||
category: Optional[str] = None
|
category: Optional[str] = None
|
||||||
|
priority: Optional[int] = 100
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
is_active: bool = True
|
||||||
|
|
||||||
|
|
||||||
class VendorCreate(VendorBase):
|
class VendorCreate(VendorBase):
|
||||||
@ -100,7 +107,6 @@ class VendorUpdate(BaseModel):
|
|||||||
class Vendor(VendorBase):
|
class Vendor(VendorBase):
|
||||||
"""Full vendor schema"""
|
"""Full vendor schema"""
|
||||||
id: int
|
id: int
|
||||||
is_active: bool = True
|
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: Optional[datetime] = None
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user