fix: Redirect loop in membership apply + add registry lookup for admin + action legends

This commit is contained in:
Maciej Pienczyn 2026-02-01 14:05:41 +01:00
parent 3a12c659ab
commit ebc3dd63d3
4 changed files with 507 additions and 2 deletions

View File

@ -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
# ============================================================

View File

@ -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
View 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())

View File

@ -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');