nordabiz/templates/admin/companies.html
Maciej Pienczyn ff35ef6b43
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
feat: add admin company detail page with enrichment workflow
New admin page at /admin/companies/{id}/detail showing company data,
completeness score, and action buttons for registry data (CEIDG/KRS),
logo fetch, SEO audit, social media audit, and GBP audit.
Includes "Uzbrój firmę" master button for sequential execution.
Company list now links to detail page with "NOWA" badge for recent entries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 13:29:00 +01:00

1210 lines
46 KiB
HTML

{% extends "base.html" %}
{% block title %}Zarządzanie Firmami - Norda Biznes Partner{% endblock %}
{% block extra_css %}
<style>
.admin-header {
margin-bottom: var(--spacing-xl);
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.admin-header-content h1 {
font-size: var(--font-size-3xl);
color: var(--text-primary);
margin: 0;
}
.admin-header-content p {
margin: var(--spacing-xs) 0 0 0;
}
.header-buttons {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.btn-add {
display: inline-flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-lg);
background: var(--primary);
color: white;
border: none;
border-radius: var(--radius);
font-size: var(--font-size-base);
font-weight: 500;
cursor: pointer;
transition: var(--transition);
}
.btn-add:hover { opacity: 0.9; }
.btn-add svg { width: 20px; height: 20px; }
.btn-export {
display: inline-flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-lg);
background: var(--surface);
color: var(--text-primary);
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: var(--font-size-base);
font-weight: 500;
cursor: pointer;
transition: var(--transition);
text-decoration: none;
}
.btn-export:hover { background: var(--background); }
.btn-export svg { width: 20px; height: 20px; }
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: var(--spacing-lg);
margin-bottom: var(--spacing-2xl);
}
.stat-card {
background: var(--surface);
padding: var(--spacing-lg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
text-align: center;
}
.stat-value {
font-size: var(--font-size-3xl);
font-weight: 700;
color: var(--primary);
}
.stat-label {
color: var(--text-secondary);
font-size: var(--font-size-sm);
margin-top: var(--spacing-xs);
}
.filters-row {
display: flex;
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
flex-wrap: wrap;
align-items: center;
}
.filter-group {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.filter-group label {
font-size: var(--font-size-sm);
color: var(--text-secondary);
font-weight: 500;
}
.filter-select, .filter-input {
padding: var(--spacing-xs) var(--spacing-sm);
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: var(--font-size-sm);
background: var(--surface);
}
.filter-select:focus, .filter-input:focus {
outline: none;
border-color: var(--primary);
}
.section {
background: var(--surface);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
margin-bottom: var(--spacing-xl);
}
.section h2 {
font-size: var(--font-size-xl);
margin-bottom: var(--spacing-lg);
color: var(--text-primary);
border-bottom: 2px solid var(--border);
padding-bottom: var(--spacing-sm);
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th, .data-table td {
padding: var(--spacing-md);
text-align: left;
border-bottom: 1px solid var(--border);
}
.data-table th {
font-weight: 600;
color: var(--text-secondary);
font-size: var(--font-size-sm);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.data-table tr:hover { background: var(--background); }
.company-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.company-name {
font-weight: 500;
color: var(--text-primary);
text-decoration: none;
}
.company-name:hover {
text-decoration: underline;
color: var(--primary);
}
.badge-new {
background: #FEF3C7;
color: #92400E;
font-size: 9px;
margin-left: 4px;
vertical-align: middle;
}
.company-city {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
font-weight: 500;
text-transform: uppercase;
}
.badge-active { background: #D1FAE5; color: #065F46; }
.badge-pending { background: #FEF3C7; color: #92400E; }
.badge-inactive { background: #E5E7EB; color: #374151; }
.badge-archived { background: #FEE2E2; color: #991B1B; }
.badge-basic { background: #E5E7EB; color: #374151; }
.badge-enhanced { background: #DBEAFE; color: #1D4ED8; }
.badge-complete { background: #D1FAE5; color: #065F46; }
.action-buttons {
display: flex;
gap: var(--spacing-xs);
flex-wrap: wrap;
}
.btn-icon {
width: 32px;
height: 32px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius);
border: 1px solid var(--border);
background: var(--surface);
cursor: pointer;
transition: var(--transition);
}
.btn-icon:hover { background: var(--background); }
.btn-icon svg { width: 16px; height: 16px; }
.btn-icon.status-toggle { background: #D1FAE5; border-color: #10B981; color: #065F46; }
.btn-icon.status-toggle.inactive { background: #E5E7EB; border-color: #9CA3AF; color: #374151; }
.btn-icon.danger:hover { background: var(--error); border-color: var(--error); color: white; }
.empty-state {
text-align: center;
padding: var(--spacing-2xl);
color: var(--text-secondary);
}
/* Modal styles */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
align-items: center;
justify-content: center;
}
.modal.active { display: flex; }
.modal-content {
background: var(--surface);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
max-width: 500px;
width: 90%;
box-shadow: var(--shadow-lg);
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
font-size: var(--font-size-xl);
margin-bottom: var(--spacing-md);
color: var(--text-primary);
}
.modal-body { margin-bottom: var(--spacing-lg); }
.modal-footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-sm);
}
.form-group { margin-bottom: var(--spacing-md); }
.form-label {
display: block;
margin-bottom: var(--spacing-xs);
color: var(--text-secondary);
font-size: var(--font-size-sm);
font-weight: 500;
}
.form-control {
width: 100%;
padding: var(--spacing-sm);
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: var(--font-size-base);
font-family: inherit;
}
.form-control:focus {
outline: none;
border-color: var(--primary);
}
.btn {
padding: var(--spacing-sm) var(--spacing-md);
border: none;
border-radius: var(--radius);
font-size: var(--font-size-base);
font-weight: 500;
cursor: pointer;
transition: var(--transition);
}
.btn-primary { background: var(--primary); color: white; }
.btn-primary:hover { opacity: 0.9; }
.btn-secondary { background: var(--background); color: var(--text-secondary); }
.btn-secondary:hover { background: var(--border); }
.btn-danger { background: #EF4444; color: white; }
.btn-danger:hover { background: #DC2626; }
/* Toast styles */
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 2000;
display: flex;
flex-direction: column;
gap: 10px;
}
.toast {
background: var(--surface);
padding: var(--spacing-md) var(--spacing-lg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
display: flex;
align-items: center;
gap: var(--spacing-sm);
min-width: 280px;
max-width: 400px;
animation: slideIn 0.3s ease;
border-left: 4px solid var(--primary);
}
.toast.success { border-left-color: #10B981; }
.toast.error { border-left-color: #EF4444; }
.toast-icon { width: 24px; height: 24px; flex-shrink: 0; }
.toast-icon.success { color: #10B981; }
.toast-icon.error { color: #EF4444; }
.toast-message { flex: 1; color: var(--text-primary); }
.toast-close {
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 4px;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
/* Confirmation modal */
.modal-icon {
width: 48px;
height: 48px;
margin: 0 auto var(--spacing-md);
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.modal-icon.warning { background: #FEF3C7; color: #F59E0B; }
.modal-icon.danger { background: #FEE2E2; color: #EF4444; }
.modal-icon svg { width: 24px; height: 24px; }
.modal-title {
font-size: var(--font-size-lg);
font-weight: 600;
text-align: center;
margin-bottom: var(--spacing-sm);
}
.modal-description {
text-align: center;
color: var(--text-secondary);
margin-bottom: var(--spacing-lg);
}
/* People list in modal */
.people-list {
max-height: 300px;
overflow-y: auto;
}
.people-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-sm);
border-bottom: 1px solid var(--border);
}
.people-item:last-child { border-bottom: none; }
.people-info { flex: 1; }
.people-name { font-weight: 500; }
.people-role { font-size: var(--font-size-sm); color: var(--text-secondary); }
@media (max-width: 768px) {
.data-table { font-size: var(--font-size-sm); }
.data-table th:nth-child(3), .data-table td:nth-child(3),
.data-table th:nth-child(6), .data-table td:nth-child(6) { display: none; }
.filters-row { flex-direction: column; align-items: stretch; }
}
</style>
{% endblock %}
{% block content %}
<div class="admin-header">
<div class="admin-header-content">
<h1>Zarządzanie Firmami</h1>
<p class="text-muted">Przeglądaj i zarządzaj firmami w katalogu</p>
</div>
<div class="header-buttons">
<a href="{{ url_for('admin.admin_companies_export') }}" class="btn-export">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
Eksport CSV
</a>
<button class="btn-add" onclick="openAddModal()">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
Dodaj firmę
</button>
</div>
</div>
<!-- Stats Grid -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">{{ total_companies }}</div>
<div class="stat-label">Wszystkich</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color: #10B981;">{{ active_count }}</div>
<div class="stat-label">Aktywnych</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color: #F59E0B;">{{ pending_count }}</div>
<div class="stat-label">Oczekujących</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color: #6B7280;">{{ inactive_count }}</div>
<div class="stat-label">Nieaktywnych</div>
</div>
</div>
<!-- Filters -->
<form method="GET" class="filters-row">
<div class="filter-group">
<label>Status:</label>
<select name="status" class="filter-select" onchange="this.form.submit()">
<option value="all" {{ 'selected' if current_status == 'all' else '' }}>Wszystkie</option>
<option value="active" {{ 'selected' if current_status == 'active' else '' }}>Aktywne</option>
<option value="pending" {{ 'selected' if current_status == 'pending' else '' }}>Oczekujące</option>
<option value="inactive" {{ 'selected' if current_status == 'inactive' else '' }}>Nieaktywne</option>
<option value="archived" {{ 'selected' if current_status == 'archived' else '' }}>Zarchiwizowane</option>
</select>
</div>
<div class="filter-group">
<label>Kategoria:</label>
<select name="category" class="filter-select" onchange="this.form.submit()">
<option value="">Wszystkie</option>
{% for cat in categories %}
<option value="{{ cat.id }}" {{ 'selected' if current_category == cat.id|string else '' }}>{{ cat.name }}</option>
{% endfor %}
</select>
</div>
<div class="filter-group">
<label>Jakość:</label>
<select name="quality" class="filter-select" onchange="this.form.submit()">
<option value="">Wszystkie</option>
<option value="basic" {{ 'selected' if current_quality == 'basic' else '' }}>Podstawowa</option>
<option value="enhanced" {{ 'selected' if current_quality == 'enhanced' else '' }}>Rozszerzona</option>
<option value="complete" {{ 'selected' if current_quality == 'complete' else '' }}>Kompletna</option>
</select>
</div>
<div class="filter-group">
<label>Szukaj:</label>
<input type="text" name="q" class="filter-input" placeholder="Nazwa lub NIP..." value="{{ search_query }}" style="width: 200px;">
<button type="submit" class="btn btn-primary" style="padding: var(--spacing-xs) var(--spacing-sm);">Szukaj</button>
</div>
</form>
<!-- Companies Table -->
<div class="section">
<h2>Firmy ({{ companies|length }})</h2>
{% if companies %}
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>Firma</th>
<th>NIP</th>
<th>Kategoria</th>
<th>Status</th>
<th>Jakość</th>
<th>Użyt.</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
{% for company in companies %}
<tr data-company-id="{{ company.id }}">
<td>{{ company.id }}</td>
<td>
<div class="company-info">
<div style="display: flex; align-items: center; gap: 6px;">
<a href="/admin/companies/{{ company.id }}/detail" class="company-name">{{ company.name }}</a>
{% set age_days = ((now - company.created_at).total_seconds() / 86400)|int if company.created_at and now else 999 %}
{% if age_days <= 7 %}
<span class="badge badge-new">NOWA</span>
{% endif %}
{% if company.admin_notes %}
<span title="{{ company.admin_notes[:100] }}..." style="cursor: help; color: #f59e0b; font-size: 14px;">&#9998;</span>
{% endif %}
</div>
{% if company.address_city %}
<span class="company-city">{{ company.address_city }}</span>
{% endif %}
</div>
</td>
<td style="font-family: monospace;">{{ company.nip or '-' }}</td>
<td>{{ company.category.name if company.category else '-' }}</td>
<td>
<span class="badge badge-{{ company.status or 'pending' }}">{{ company.status or 'pending' }}</span>
</td>
<td>
<span class="badge badge-{{ company.data_quality or 'basic' }}">{{ company.data_quality or 'basic' }}</span>
</td>
<td>{{ company.users|length if company.users else 0 }}</td>
<td>
<div class="action-buttons">
<button class="btn-icon" onclick="openEditModal({{ company.id }})" title="Edytuj">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
</button>
<button class="btn-icon status-toggle {{ 'inactive' if company.status != 'active' else '' }}"
onclick="toggleStatus({{ company.id }})"
title="{{ 'Dezaktywuj' if company.status == 'active' else 'Aktywuj' }}">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</button>
<button class="btn-icon" onclick="openUsersModal({{ company.id }}, '{{ company.name|e }}')" title="Użytkownicy">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
</button>
<button class="btn-icon" onclick="openPeopleModal({{ company.id }}, '{{ company.name|e }}')" title="Osoby KRS">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
</svg>
</button>
{% if company.status == 'archived' and current_user.can_manage_users() %}
<button class="btn-icon danger" onclick="hardDeleteCompany({{ company.id }}, '{{ company.name|e }}')" title="Trwale usuń">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
</button>
{% elif company.status != 'archived' %}
<button class="btn-icon danger" onclick="deleteCompany({{ company.id }}, '{{ company.name|e }}')" title="Archiwizuj">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state">
<p>Brak firm spełniających kryteria</p>
</div>
{% endif %}
</div>
<!-- Add Company Modal -->
<div id="addModal" class="modal">
<div class="modal-content">
<div class="modal-header">Dodaj nową firmę</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">Nazwa firmy *</label>
<input type="text" id="addName" class="form-control" placeholder="Nazwa firmy" required>
</div>
<div class="form-group">
<label class="form-label">NIP</label>
<input type="text" id="addNip" class="form-control" placeholder="1234567890" maxlength="10">
</div>
<div class="form-group">
<label class="form-label">Kategoria</label>
<select id="addCategory" class="form-control">
<option value="">-- Wybierz --</option>
{% for cat in categories %}
<option value="{{ cat.id }}">{{ cat.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label class="form-label">Status</label>
<select id="addStatus" class="form-control">
<option value="pending">Oczekująca</option>
<option value="active">Aktywna</option>
<option value="inactive">Nieaktywna</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Email</label>
<input type="email" id="addEmail" class="form-control" placeholder="kontakt@firma.pl">
</div>
<div class="form-group">
<label class="form-label">Telefon</label>
<input type="text" id="addPhone" class="form-control" placeholder="+48 123 456 789">
</div>
<div class="form-group">
<label class="form-label">Miasto</label>
<input type="text" id="addCity" class="form-control" placeholder="Wejherowo">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeAddModal()">Anuluj</button>
<button class="btn btn-primary" onclick="confirmAdd()">Utwórz firmę</button>
</div>
</div>
</div>
<!-- Edit Company Modal -->
<div id="editModal" class="modal">
<div class="modal-content">
<div class="modal-header">Edytuj firmę</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">Nazwa firmy *</label>
<input type="text" id="editName" class="form-control" required>
</div>
<div class="form-group">
<label class="form-label">NIP</label>
<input type="text" id="editNip" class="form-control" maxlength="10">
</div>
<div class="form-group">
<label class="form-label">Kategoria</label>
<select id="editCategory" class="form-control">
<option value="">-- Wybierz --</option>
{% for cat in categories %}
<option value="{{ cat.id }}">{{ cat.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label class="form-label">Status</label>
<select id="editStatus" class="form-control">
<option value="pending">Oczekująca</option>
<option value="active">Aktywna</option>
<option value="inactive">Nieaktywna</option>
<option value="archived">Zarchiwizowana</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Email</label>
<input type="email" id="editEmail" class="form-control">
</div>
<div class="form-group">
<label class="form-label">Telefon</label>
<input type="text" id="editPhone" class="form-control">
</div>
<div class="form-group">
<label class="form-label">Miasto</label>
<input type="text" id="editCity" class="form-control">
</div>
<div class="form-group">
<label class="form-label">Ulica</label>
<input type="text" id="editStreet" class="form-control">
</div>
<div class="form-group">
<label class="form-label">Kod pocztowy</label>
<input type="text" id="editPostal" class="form-control" maxlength="6">
</div>
<div class="form-group" style="margin-top: var(--spacing-lg); padding-top: var(--spacing-md); border-top: 1px solid var(--border);">
<label class="form-label" style="color: var(--primary); font-weight: 600;">Notatki administracyjne</label>
<textarea id="editAdminNotes" class="form-control" rows="4" placeholder="Wewnętrzne notatki (widoczne tylko dla adminów)..." style="resize: vertical;"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeEditModal()">Anuluj</button>
<button class="btn btn-primary" onclick="saveEdit()">Zapisz zmiany</button>
</div>
</div>
</div>
<!-- People Modal -->
<div id="peopleModal" class="modal">
<div class="modal-content">
<div class="modal-header" id="peopleModalTitle">Osoby powiązane</div>
<div class="modal-body">
<div id="peopleList" class="people-list">
<div class="empty-state">Ładowanie...</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closePeopleModal()">Zamknij</button>
</div>
</div>
</div>
<!-- Users Modal -->
<div id="usersModal" class="modal">
<div class="modal-content">
<div class="modal-header" id="usersModalTitle">Użytkownicy firmy</div>
<div class="modal-body">
<div id="usersList" class="people-list">
<div class="empty-state">Ładowanie...</div>
</div>
<div style="margin-top: var(--spacing-md); padding-top: var(--spacing-md); border-top: 1px solid var(--border);">
<label class="form-label">Przypisz użytkownika</label>
<div style="display: flex; gap: var(--spacing-sm);">
<select id="assignUserSelect" class="form-control" style="flex: 1;"></select>
<button class="btn btn-primary" onclick="assignUserToCompany()">Przypisz</button>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeUsersModal()">Zamknij</button>
</div>
</div>
</div>
<!-- Confirm Modal -->
<div id="confirmModal" class="modal">
<div class="modal-content">
<div id="confirmIcon" class="modal-icon warning">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
</div>
<div id="confirmTitle" class="modal-title">Potwierdzenie</div>
<div id="confirmDescription" class="modal-description"></div>
<div class="modal-footer" style="justify-content: center;">
<button class="btn btn-secondary" onclick="closeConfirmModal()">Anuluj</button>
<button id="confirmAction" class="btn btn-danger">Potwierdź</button>
</div>
</div>
</div>
<!-- Toast Container -->
<div id="toastContainer" class="toast-container"></div>
{% endblock %}
{% block extra_js %}
const csrfToken = '{{ csrf_token() }}';
let editCompanyId = null;
let confirmCallback = null;
function showToast(message, type = 'success') {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const iconSvg = {
success: '<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>',
error: '<path d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>'
};
toast.innerHTML = `
<svg class="toast-icon ${type}" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
${iconSvg[type] || iconSvg.success}
</svg>
<span class="toast-message">${message}</span>
<button class="toast-close" onclick="this.parentElement.remove()">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
`;
container.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'slideOut 0.3s ease forwards';
setTimeout(() => toast.remove(), 300);
}, 5000);
}
// Add Modal
function openAddModal() {
document.getElementById('addName').value = '';
document.getElementById('addNip').value = '';
document.getElementById('addCategory').value = '';
document.getElementById('addStatus').value = 'pending';
document.getElementById('addEmail').value = '';
document.getElementById('addPhone').value = '';
document.getElementById('addCity').value = '';
document.getElementById('addModal').classList.add('active');
}
function closeAddModal() {
document.getElementById('addModal').classList.remove('active');
}
async function confirmAdd() {
const name = document.getElementById('addName').value.trim();
if (!name) {
showToast('Nazwa firmy jest wymagana', 'error');
return;
}
try {
const response = await fetch('/admin/companies/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
name: name,
nip: document.getElementById('addNip').value.trim() || null,
category_id: document.getElementById('addCategory').value || null,
status: document.getElementById('addStatus').value,
email: document.getElementById('addEmail').value.trim() || null,
phone: document.getElementById('addPhone').value.trim() || null,
address_city: document.getElementById('addCity').value.trim() || null
})
});
const data = await response.json();
if (data.success) {
closeAddModal();
showToast(data.message, 'success');
setTimeout(() => location.reload(), 1000);
} else {
showToast(data.error || 'Wystąpił błąd', 'error');
}
} catch (error) {
showToast('Błąd połączenia', 'error');
}
}
// Edit Modal
async function openEditModal(companyId) {
editCompanyId = companyId;
try {
const response = await fetch(`/admin/companies/${companyId}`);
const data = await response.json();
if (data.success) {
const c = data.company;
document.getElementById('editName').value = c.name || '';
document.getElementById('editNip').value = c.nip || '';
document.getElementById('editCategory').value = c.category_id || '';
document.getElementById('editStatus').value = c.status || 'pending';
document.getElementById('editEmail').value = c.email || '';
document.getElementById('editPhone').value = c.phone || '';
document.getElementById('editCity').value = c.address_city || '';
document.getElementById('editStreet').value = c.address_street || '';
document.getElementById('editPostal').value = c.address_postal || '';
document.getElementById('editAdminNotes').value = c.admin_notes || '';
document.getElementById('editModal').classList.add('active');
} else {
showToast(data.error || 'Nie można pobrać danych', 'error');
}
} catch (error) {
showToast('Błąd połączenia', 'error');
}
}
function closeEditModal() {
editCompanyId = null;
document.getElementById('editModal').classList.remove('active');
}
async function saveEdit() {
if (!editCompanyId) return;
const name = document.getElementById('editName').value.trim();
if (!name) {
showToast('Nazwa firmy jest wymagana', 'error');
return;
}
try {
const response = await fetch(`/admin/companies/${editCompanyId}/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
name: name,
nip: document.getElementById('editNip').value.trim() || null,
category_id: document.getElementById('editCategory').value || null,
status: document.getElementById('editStatus').value,
email: document.getElementById('editEmail').value.trim() || null,
phone: document.getElementById('editPhone').value.trim() || null,
address_city: document.getElementById('editCity').value.trim() || null,
address_street: document.getElementById('editStreet').value.trim() || null,
address_postal: document.getElementById('editPostal').value.trim() || null,
admin_notes: document.getElementById('editAdminNotes').value.trim() || null
})
});
const data = await response.json();
if (data.success) {
closeEditModal();
showToast(data.message, 'success');
setTimeout(() => location.reload(), 1000);
} else {
showToast(data.error || 'Wystąpił błąd', 'error');
}
} catch (error) {
showToast('Błąd połączenia', 'error');
}
}
// Toggle Status
async function toggleStatus(companyId) {
try {
const response = await fetch(`/admin/companies/${companyId}/toggle-status`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
});
const data = await response.json();
if (data.success) {
showToast(data.message, 'success');
setTimeout(() => location.reload(), 1000);
} else {
showToast(data.error || 'Wystąpił błąd', 'error');
}
} catch (error) {
showToast('Błąd połączenia', 'error');
}
}
// People Modal
async function openPeopleModal(companyId, companyName) {
document.getElementById('peopleModalTitle').textContent = `Osoby powiązane - ${companyName}`;
document.getElementById('peopleList').innerHTML = '<div class="empty-state">Ładowanie...</div>';
document.getElementById('peopleModal').classList.add('active');
try {
const response = await fetch(`/admin/companies/${companyId}/people`);
const data = await response.json();
if (data.success) {
if (data.people.length === 0) {
document.getElementById('peopleList').innerHTML = '<div class="empty-state">Brak powiązanych osób</div>';
} else {
let html = '';
data.people.forEach(p => {
html += `
<div class="people-item">
<div class="people-info">
<div class="people-name">${p.imiona} ${p.nazwisko}</div>
<div class="people-role">${p.role} (${p.role_category})${p.shares_percent ? ' - ' + p.shares_percent + '%' : ''}</div>
</div>
</div>
`;
});
document.getElementById('peopleList').innerHTML = html;
}
} else {
document.getElementById('peopleList').innerHTML = '<div class="empty-state">Błąd pobierania danych</div>';
}
} catch (error) {
document.getElementById('peopleList').innerHTML = '<div class="empty-state">Błąd połączenia</div>';
}
}
function closePeopleModal() {
document.getElementById('peopleModal').classList.remove('active');
}
// Users Modal
let usersModalCompanyId = null;
async function openUsersModal(companyId, companyName) {
usersModalCompanyId = companyId;
document.getElementById('usersModalTitle').textContent = `Użytkownicy - ${companyName}`;
document.getElementById('usersList').innerHTML = '<div class="empty-state">Ładowanie...</div>';
document.getElementById('usersModal').classList.add('active');
await loadCompanyUsers(companyId);
await loadAvailableUsers();
}
function closeUsersModal() {
usersModalCompanyId = null;
document.getElementById('usersModal').classList.remove('active');
}
async function loadCompanyUsers(companyId) {
try {
const response = await fetch(`/admin/companies/${companyId}/users`);
const data = await response.json();
if (data.success) {
if (data.users.length === 0) {
document.getElementById('usersList').innerHTML = '<div class="empty-state">Brak przypisanych użytkowników</div>';
} else {
let html = '';
data.users.forEach(u => {
html += `
<div class="people-item">
<div class="people-info">
<div class="people-name">${u.name || u.email}</div>
<div class="people-role">${u.email} &middot; ${u.role || 'user'}</div>
</div>
<button class="btn btn-secondary" style="padding: 4px 8px; font-size: var(--font-size-xs);" onclick="unassignUser(${companyId}, ${u.id}, '${u.email}')">Odepnij</button>
</div>
`;
});
document.getElementById('usersList').innerHTML = html;
}
} else {
document.getElementById('usersList').innerHTML = '<div class="empty-state">Błąd pobierania danych</div>';
}
} catch (error) {
document.getElementById('usersList').innerHTML = '<div class="empty-state">Błąd połączenia</div>';
}
}
async function loadAvailableUsers() {
try {
const response = await fetch('/admin/users/list-all');
const data = await response.json();
const select = document.getElementById('assignUserSelect');
select.innerHTML = '<option value="">-- Wybierz użytkownika --</option>';
if (data.success) {
data.users.filter(u => !u.company_id).forEach(u => {
const option = document.createElement('option');
option.value = u.id;
option.textContent = `${u.name || u.email} (${u.email})`;
select.appendChild(option);
});
}
} catch (error) {
console.error('Error loading users:', error);
}
}
async function assignUserToCompany() {
if (!usersModalCompanyId) return;
const userId = document.getElementById('assignUserSelect').value;
if (!userId) {
showToast('Wybierz użytkownika', 'error');
return;
}
try {
const response = await fetch(`/admin/companies/${usersModalCompanyId}/assign-user`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({ user_id: parseInt(userId) })
});
const data = await response.json();
if (data.success) {
showToast(data.message, 'success');
await loadCompanyUsers(usersModalCompanyId);
await loadAvailableUsers();
} else {
showToast(data.error || 'Wystąpił błąd', 'error');
}
} catch (error) {
showToast('Błąd połączenia', 'error');
}
}
async function unassignUser(companyId, userId, userEmail) {
try {
const response = await fetch(`/admin/companies/${companyId}/unassign-user`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({ user_id: userId })
});
const data = await response.json();
if (data.success) {
showToast(data.message, 'success');
await loadCompanyUsers(companyId);
await loadAvailableUsers();
} else {
showToast(data.error || 'Wystąpił błąd', 'error');
}
} catch (error) {
showToast('Błąd połączenia', 'error');
}
}
// Delete (Archive) Company
function deleteCompany(companyId, companyName) {
document.getElementById('confirmIcon').className = 'modal-icon warning';
document.getElementById('confirmTitle').textContent = 'Archiwizuj firmę';
document.getElementById('confirmDescription').textContent = `Czy na pewno chcesz zarchiwizować firmę "${companyName}"? Firma nie będzie widoczna w katalogu.`;
document.getElementById('confirmAction').textContent = 'Potwierdź';
document.getElementById('confirmAction').className = 'btn btn-danger';
confirmCallback = async () => {
try {
const response = await fetch(`/admin/companies/${companyId}/delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
});
const data = await response.json();
if (data.success) {
showToast(data.message, 'success');
setTimeout(() => location.reload(), 1000);
} else {
showToast(data.error || 'Wystąpił błąd', 'error');
}
} catch (error) {
showToast('Błąd połączenia', 'error');
}
};
document.getElementById('confirmModal').classList.add('active');
}
// Hard Delete (Permanent) - only for archived companies
function hardDeleteCompany(companyId, companyName) {
document.getElementById('confirmIcon').className = 'modal-icon danger';
document.getElementById('confirmTitle').textContent = 'Trwałe usunięcie firmy';
document.getElementById('confirmDescription').innerHTML =
`Czy na pewno chcesz <strong>trwale usunąć</strong> firmę "${companyName}"?<br><br>` +
'<strong style="color: var(--error);">Ta operacja jest NIEODWRACALNA!</strong><br>' +
'Wszystkie dane firmy zostaną permanentnie usunięte.';
document.getElementById('confirmAction').textContent = 'Trwale usuń';
document.getElementById('confirmAction').className = 'btn btn-danger';
confirmCallback = async () => {
try {
const response = await fetch(`/admin/companies/${companyId}/hard-delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
});
const data = await response.json();
if (data.success) {
const row = document.querySelector(`tr[data-company-id="${companyId}"]`);
if (row) row.remove();
showToast(data.message, 'success');
} else {
showToast(data.error || 'Wystąpił błąd', 'error');
}
} catch (error) {
showToast('Błąd połączenia', 'error');
}
};
document.getElementById('confirmModal').classList.add('active');
}
function closeConfirmModal() {
document.getElementById('confirmModal').classList.remove('active');
confirmCallback = null;
}
document.getElementById('confirmAction').addEventListener('click', function() {
if (confirmCallback) confirmCallback();
closeConfirmModal();
});
{% endblock %}