fix: Redirect loop in membership apply + add registry lookup for admin + action legends
This commit is contained in:
parent
3a12c659ab
commit
ebc3dd63d3
@ -372,6 +372,88 @@ def admin_membership_start_review(app_id):
|
||||
db.close()
|
||||
|
||||
|
||||
@bp.route('/membership/<int:app_id>/update-from-registry', methods=['POST'])
|
||||
@login_required
|
||||
def admin_membership_update_from_registry(app_id):
|
||||
"""Update membership application with data from KRS/CEIDG registry."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
application = db.query(MembershipApplication).get(app_id)
|
||||
if not application:
|
||||
return jsonify({'success': False, 'error': 'Nie znaleziono deklaracji'}), 404
|
||||
|
||||
if application.status not in ['submitted', 'under_review']:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Można aktualizować tylko deklaracje oczekujące na rozpatrzenie'
|
||||
}), 400
|
||||
|
||||
data = request.get_json() or {}
|
||||
updated_fields = []
|
||||
|
||||
# Update fields from registry data
|
||||
if data.get('name'):
|
||||
application.company_name = data['name']
|
||||
updated_fields.append('company_name')
|
||||
|
||||
if data.get('address_postal_code'):
|
||||
application.address_postal_code = data['address_postal_code']
|
||||
updated_fields.append('address_postal_code')
|
||||
|
||||
if data.get('address_city'):
|
||||
application.address_city = data['address_city']
|
||||
updated_fields.append('address_city')
|
||||
|
||||
if data.get('address_street'):
|
||||
application.address_street = data['address_street']
|
||||
updated_fields.append('address_street')
|
||||
|
||||
if data.get('address_number'):
|
||||
application.address_number = data['address_number']
|
||||
updated_fields.append('address_number')
|
||||
|
||||
if data.get('regon'):
|
||||
application.regon = data['regon']
|
||||
updated_fields.append('regon')
|
||||
|
||||
if data.get('krs'):
|
||||
application.krs_number = data['krs']
|
||||
updated_fields.append('krs_number')
|
||||
|
||||
if data.get('founded_date'):
|
||||
try:
|
||||
from datetime import datetime as dt
|
||||
application.founded_date = dt.strptime(data['founded_date'], '%Y-%m-%d').date()
|
||||
updated_fields.append('founded_date')
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Store registry data
|
||||
application.registry_data = data
|
||||
application.registry_source = data.get('source', 'KRS')
|
||||
|
||||
db.commit()
|
||||
|
||||
logger.info(
|
||||
f"Membership application {app_id} updated from registry by {current_user.email}. "
|
||||
f"Updated fields: {updated_fields}"
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'updated_fields': updated_fields
|
||||
})
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error updating application {app_id} from registry: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# COMPANY DATA REQUESTS
|
||||
# ============================================================
|
||||
|
||||
@ -41,7 +41,11 @@ def apply():
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
# Redirect to continue existing application
|
||||
# If already submitted, redirect to status page
|
||||
if existing.status in ['submitted', 'under_review']:
|
||||
flash('Masz już wysłaną deklarację oczekującą na rozpatrzenie.', 'info')
|
||||
return redirect(url_for('membership.status'))
|
||||
# Otherwise continue editing
|
||||
return redirect(url_for('membership.apply_step', step=1))
|
||||
|
||||
# Create new draft
|
||||
@ -81,8 +85,18 @@ def apply_step(step):
|
||||
).first()
|
||||
|
||||
if not application:
|
||||
# Check if user has submitted application
|
||||
submitted = db.query(MembershipApplication).filter(
|
||||
MembershipApplication.user_id == current_user.id,
|
||||
MembershipApplication.status.in_(['submitted', 'under_review'])
|
||||
).first()
|
||||
if submitted:
|
||||
flash('Twoja deklaracja oczekuje na rozpatrzenie.', 'info')
|
||||
return redirect(url_for('membership.status'))
|
||||
|
||||
# No application at all - redirect to start
|
||||
flash('Nie znaleziono aktywnej deklaracji.', 'error')
|
||||
return redirect(url_for('membership.apply'))
|
||||
return redirect(url_for('membership.status'))
|
||||
|
||||
if request.method == 'POST':
|
||||
action = request.form.get('action', 'save')
|
||||
|
||||
104
scripts/test_ai_proposal.py
Normal file
104
scripts/test_ai_proposal.py
Normal file
@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for AI enrichment proposal workflow.
|
||||
Creates a test proposal for Waterm company.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Setup path for production environment
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
from dotenv import load_dotenv
|
||||
# Load .env from project root
|
||||
env_path = os.path.join(project_root, '.env')
|
||||
load_dotenv(env_path)
|
||||
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from database import SessionLocal, Company, AiEnrichmentProposal, User
|
||||
import gemini_service
|
||||
|
||||
def main():
|
||||
# Initialize Gemini - use flash-lite to avoid quota issues
|
||||
gemini_service.init_gemini_service(model='flash-lite')
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
company = db.query(Company).filter_by(id=12).first()
|
||||
admin_user = db.query(User).filter_by(is_admin=True).first()
|
||||
|
||||
if not company:
|
||||
print('Firma nie znaleziona')
|
||||
return 1
|
||||
|
||||
print(f'Test AI enrichment dla: {company.name}')
|
||||
print(f'Website: {company.website}')
|
||||
|
||||
service = gemini_service.get_gemini_service()
|
||||
if not service:
|
||||
print('Brak usługi Gemini')
|
||||
return 1
|
||||
|
||||
prompt = f'''Przeanalizuj firmę: {company.name}
|
||||
Strona: {company.website}
|
||||
Opis: {company.description_short or 'brak'}
|
||||
|
||||
Wygeneruj JSON z informacjami o firmie:
|
||||
{{"business_summary": "opis działalności 1-2 zdania", "services_list": ["usługa1", "usługa2", "usługa3"], "industry_tags": ["tag1", "tag2"]}}
|
||||
Odpowiedz TYLKO JSON bez markdown.'''
|
||||
|
||||
print('Wysyłam zapytanie do Gemini...')
|
||||
response = service.generate_text(
|
||||
prompt=prompt,
|
||||
temperature=0.7,
|
||||
feature='ai_enrichment_test',
|
||||
user_id=admin_user.id if admin_user else 1,
|
||||
company_id=company.id
|
||||
)
|
||||
print(f'Odpowiedź Gemini ({len(response)} znaków)')
|
||||
|
||||
# Parse response
|
||||
clean = response.strip()
|
||||
if clean.startswith('```'):
|
||||
parts = clean.split('```')
|
||||
if len(parts) > 1:
|
||||
clean = parts[1]
|
||||
if clean.startswith('json'):
|
||||
clean = clean[4:]
|
||||
clean = clean.strip()
|
||||
|
||||
try:
|
||||
ai_data = json.loads(clean)
|
||||
print(f'Parsed data: {json.dumps(ai_data, indent=2, ensure_ascii=False)[:500]}')
|
||||
except json.JSONDecodeError as e:
|
||||
print(f'Błąd parsowania JSON: {e}')
|
||||
print(f'Raw response: {response[:300]}')
|
||||
return 1
|
||||
|
||||
# Create proposal
|
||||
proposal = AiEnrichmentProposal(
|
||||
company_id=company.id,
|
||||
status='pending',
|
||||
proposal_type='ai_enrichment',
|
||||
data_source=company.website,
|
||||
proposed_data=ai_data,
|
||||
ai_explanation='Test AI enrichment - propozycja wzbogacenia danych',
|
||||
confidence_score=0.85,
|
||||
expires_at=datetime.utcnow() + timedelta(days=30)
|
||||
)
|
||||
db.add(proposal)
|
||||
db.commit()
|
||||
|
||||
print(f'\n✅ Utworzono propozycję ID: {proposal.id}')
|
||||
print(f'Status: {proposal.status}')
|
||||
print(f'Data wygaśnięcia: {proposal.expires_at}')
|
||||
|
||||
return 0
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@ -284,6 +284,132 @@
|
||||
border: 1px solid var(--warning);
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.action-help {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-secondary);
|
||||
margin-top: var(--spacing-xs);
|
||||
padding-left: var(--spacing-sm);
|
||||
border-left: 2px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--background);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.registry-actions {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
padding: var(--spacing-md);
|
||||
background: var(--background);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.registry-actions h4 {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin: 0 0 var(--spacing-sm) 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.registry-result {
|
||||
margin-top: var(--spacing-md);
|
||||
padding: var(--spacing-md);
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--success);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.registry-result.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.registry-result.error {
|
||||
border-color: var(--error);
|
||||
}
|
||||
|
||||
.registry-result h5 {
|
||||
margin: 0 0 var(--spacing-sm) 0;
|
||||
color: var(--success);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.registry-result.error h5 {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.registry-diff {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.registry-diff-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-xs) 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.registry-diff-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.registry-diff-label {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.registry-diff-old {
|
||||
color: var(--error);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.registry-diff-new {
|
||||
color: var(--success);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.action-legend {
|
||||
margin-top: var(--spacing-lg);
|
||||
padding-top: var(--spacing-md);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.action-legend h4 {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin: 0 0 var(--spacing-sm) 0;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.legend-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.legend-icon.approve { background: var(--success); }
|
||||
.legend-icon.changes { background: var(--warning); }
|
||||
.legend-icon.reject { background: var(--error); }
|
||||
.legend-icon.review { background: var(--primary); }
|
||||
|
||||
.legend-text {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@ -307,6 +433,32 @@
|
||||
<!-- Dane firmy -->
|
||||
<div class="section">
|
||||
<h2>Dane firmy</h2>
|
||||
|
||||
<!-- Pobieranie z rejestru -->
|
||||
{% if application.nip and application.status in ['submitted', 'under_review'] %}
|
||||
<div class="registry-actions">
|
||||
<h4>Weryfikacja w rejestrze</h4>
|
||||
<button class="btn-action btn-secondary" onclick="lookupRegistry()" id="btnLookupRegistry">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<path d="M21 21l-4.35-4.35"/>
|
||||
</svg>
|
||||
Pobierz aktualne dane z KRS/CEIDG
|
||||
</button>
|
||||
<p class="action-help">Sprawdzi NIP {{ application.nip }} w rejestrach i pokaże różnice między danymi zgłoszonymi a oficjalnymi.</p>
|
||||
|
||||
<div class="registry-result" id="registryResult">
|
||||
<h5 id="registryResultTitle">Dane z rejestru</h5>
|
||||
<div class="registry-diff" id="registryDiff"></div>
|
||||
<div style="margin-top: var(--spacing-md);">
|
||||
<button class="btn-action btn-secondary" onclick="applyRegistryData()" id="btnApplyRegistry" style="display: none;">
|
||||
Zastosuj dane z rejestru
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="data-grid">
|
||||
<div class="data-item">
|
||||
<div class="data-label">Nazwa</div>
|
||||
@ -486,6 +638,9 @@
|
||||
Rozpocznij rozpatrywanie
|
||||
</button>
|
||||
</div>
|
||||
<p class="action-help">
|
||||
Zmieni status na "W trakcie rozpatrywania" i odblokuje opcje zatwierdzenia, odrzucenia lub prośby o poprawki.
|
||||
</p>
|
||||
|
||||
{% elif application.status == 'under_review' %}
|
||||
<div class="action-buttons">
|
||||
@ -510,6 +665,22 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="action-legend">
|
||||
<h4>Co robią przyciski?</h4>
|
||||
<div class="legend-item">
|
||||
<div class="legend-icon approve"></div>
|
||||
<div class="legend-text"><strong>Zatwierdź</strong> — Utworzy nową firmę w katalogu, przypisze użytkownika jako właściciela i nada numer członkowski.</div>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-icon changes"></div>
|
||||
<div class="legend-text"><strong>Poproś o poprawki</strong> — Wyśle deklarację z powrotem do użytkownika z prośbą o korektę. Użytkownik zobaczy Twój komentarz.</div>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-icon reject"></div>
|
||||
<div class="legend-text"><strong>Odrzuć</strong> — Trwale odrzuci deklarację. Użytkownik zobaczy podany powód i będzie mógł złożyć nową.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% elif application.status == 'approved' %}
|
||||
<div class="alert alert-info">
|
||||
✓ Deklaracja zatwierdzona
|
||||
@ -622,6 +793,140 @@
|
||||
|
||||
{% block extra_js %}
|
||||
const appId = {{ application.id }};
|
||||
const appNip = '{{ application.nip or "" }}';
|
||||
let registryData = null;
|
||||
|
||||
// Pobieranie danych z rejestru
|
||||
async function lookupRegistry() {
|
||||
if (!appNip) {
|
||||
alert('Brak NIP w deklaracji');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btnLookupRegistry');
|
||||
const resultDiv = document.getElementById('registryResult');
|
||||
const diffDiv = document.getElementById('registryDiff');
|
||||
const titleEl = document.getElementById('registryResultTitle');
|
||||
const applyBtn = document.getElementById('btnApplyRegistry');
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="loading-spinner"></span> Sprawdzam...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/membership/lookup-nip', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ nip: appNip })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
resultDiv.classList.add('active');
|
||||
|
||||
if (result.success && result.data) {
|
||||
registryData = result.data;
|
||||
resultDiv.classList.remove('error');
|
||||
titleEl.textContent = `✓ Dane z ${result.source}`;
|
||||
|
||||
// Pokaż różnice
|
||||
const currentData = {
|
||||
company_name: '{{ application.company_name|e }}',
|
||||
address_postal_code: '{{ application.address_postal_code|e }}',
|
||||
address_city: '{{ application.address_city|e }}',
|
||||
address_street: '{{ application.address_street|e }}',
|
||||
address_number: '{{ application.address_number|e }}',
|
||||
regon: '{{ application.regon|e }}',
|
||||
krs_number: '{{ application.krs_number|e }}'
|
||||
};
|
||||
|
||||
let diffHtml = '';
|
||||
const fields = [
|
||||
{ key: 'name', label: 'Nazwa', current: currentData.company_name },
|
||||
{ key: 'address_postal_code', label: 'Kod pocztowy', current: currentData.address_postal_code },
|
||||
{ key: 'address_city', label: 'Miejscowość', current: currentData.address_city },
|
||||
{ key: 'address_street', label: 'Ulica', current: currentData.address_street },
|
||||
{ key: 'address_number', label: 'Nr budynku', current: currentData.address_number },
|
||||
{ key: 'regon', label: 'REGON', current: currentData.regon },
|
||||
{ key: 'krs', label: 'KRS', current: currentData.krs_number }
|
||||
];
|
||||
|
||||
let hasDifferences = false;
|
||||
fields.forEach(f => {
|
||||
const newVal = result.data[f.key] || '';
|
||||
const oldVal = f.current || '';
|
||||
if (newVal && newVal !== oldVal) {
|
||||
hasDifferences = true;
|
||||
diffHtml += `
|
||||
<div class="registry-diff-row">
|
||||
<span class="registry-diff-label">${f.label}:</span>
|
||||
<span>
|
||||
${oldVal ? `<span class="registry-diff-old">${oldVal}</span> → ` : ''}
|
||||
<span class="registry-diff-new">${newVal}</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
} else if (newVal) {
|
||||
diffHtml += `
|
||||
<div class="registry-diff-row">
|
||||
<span class="registry-diff-label">${f.label}:</span>
|
||||
<span>${newVal} ✓</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
diffDiv.innerHTML = diffHtml || '<p>Dane zgodne z rejestrem.</p>';
|
||||
applyBtn.style.display = hasDifferences ? 'inline-flex' : 'none';
|
||||
} else {
|
||||
registryData = null;
|
||||
resultDiv.classList.add('error');
|
||||
titleEl.textContent = '✗ Nie znaleziono w rejestrze';
|
||||
diffDiv.innerHTML = `<p>${result.message || 'Firma o podanym NIP nie została znaleziona w KRS ani CEIDG.'}</p>`;
|
||||
applyBtn.style.display = 'none';
|
||||
}
|
||||
} catch (e) {
|
||||
resultDiv.classList.add('active', 'error');
|
||||
titleEl.textContent = '✗ Błąd połączenia';
|
||||
diffDiv.innerHTML = '<p>Nie udało się połączyć z rejestrem. Spróbuj ponownie.</p>';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = `
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<path d="M21 21l-4.35-4.35"/>
|
||||
</svg>
|
||||
Pobierz aktualne dane z KRS/CEIDG
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async function applyRegistryData() {
|
||||
if (!registryData) {
|
||||
alert('Brak danych do zastosowania');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('Czy na pewno chcesz zaktualizować dane deklaracji danymi z rejestru?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/membership/${appId}/update-from-registry`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(registryData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
alert('Dane zostały zaktualizowane');
|
||||
location.reload();
|
||||
} else {
|
||||
alert(result.error || 'Błąd aktualizacji');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Błąd połączenia');
|
||||
}
|
||||
}
|
||||
|
||||
function openApproveModal() {
|
||||
document.getElementById('approveModal').classList.add('active');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user