Publish curated VacatureRadar source snapshot
This commit is contained in:
commit
e29774cb7f
338 files changed
+32699
No files matched your search
@@ -0,0 +1,3 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Geen toegang · VacatureRadar{% endblock %}
|
||||
{% block content %}<section class="empty-state"><div class="empty-icon"><svg class="icon"><use href="#icon-alert"></use></svg></div><p class="eyebrow">403 · Toegang geweigerd</p><h1>Deze intelligence is niet voor jou beschikbaar</h1><p>Je account heeft geen toegang tot dit onderdeel. Er zijn geen gegevens gewijzigd.</p><a class="button button-primary" href="{% url 'dashboard:today' %}">Terug naar overzicht</a></section>{% endblock %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Niet gevonden · VacatureRadar{% endblock %}
|
||||
{% block content %}<section class="empty-state"><div class="empty-icon"><svg class="icon"><use href="#icon-radar"></use></svg></div><p class="eyebrow">404 · Geen radarsignaal</p><h1>Deze pagina is niet gevonden</h1><p>De link kan verouderd zijn of het object bestaat niet meer.</p><a class="button button-primary" href="{% url 'dashboard:today' %}">Terug naar overzicht</a></section>{% endblock %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Systeemfout · VacatureRadar{% endblock %}
|
||||
{% block content %}<section class="empty-state"><div class="empty-icon"><svg class="icon"><use href="#icon-system"></use></svg></div><p class="eyebrow">500 · Systeemsignaal</p><h1>De radar kon dit verzoek niet verwerken</h1><p>Probeer het later opnieuw. Er worden hier geen technische details of gevoelige gegevens getoond.</p><a class="button button-primary" href="/">Terug naar overzicht</a></section>{% endblock %}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Activiteit · VacatureRadar{% endblock %}
|
||||
{% block content %}
|
||||
<header class="cockpit-heading"><div><p class="eyebrow">Radar audit trail</p><h1>Activiteit</h1><p>Betrouwbare gebeurtenissen uit bronruns, vacatureversies, feedback, sollicitaties, notificaties en profielrevisies.</p></div><div class="radar-status"><span class="status-beacon is-live" aria-hidden="true"></span><span>{{ event_page.paginator.count }} gebeurtenissen in deze selectie</span></div></header>
|
||||
<form class="activity-filterbar" method="get"><label class="grow"><span class="field-label">Zoek in activiteit</span><span class="input-with-icon"><svg class="icon"><use href="#icon-search"></use></svg><input type="search" name="q" value="{{ request.GET.q }}" placeholder="Vacature, werkgever of gebeurtenis"></span></label><label><span class="field-label">Type</span><select name="type">{% for value,label in activity_filters %}<option value="{{ value }}" {% if current_type == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label><button class="button button-secondary" type="submit"><svg class="icon"><use href="#icon-filter"></use></svg>Filter</button></form>
|
||||
|
||||
<section class="activity-timeline" aria-label="Activiteitstijdlijn">
|
||||
{% for event in event_page %}<article class="activity-event"><span class="activity-marker {% if event.tone == 'positive' %}is-positive{% elif event.tone == 'attention' %}is-attention{% endif %}"><svg class="icon"><use href="{% if event.event_type == 'source' %}#icon-sources{% elif event.event_type == 'application' %}#icon-pipeline{% elif event.event_type == 'profile' %}#icon-profile{% elif event.event_type == 'notification' %}#icon-mail{% else %}#icon-activity{% endif %}"></use></svg></span><div class="activity-card"><div class="activity-card-head"><div><span class="pill pill-{{ event.tone }}">{{ event.event_type }}</span><h2>{{ event.title }}</h2></div><time datetime="{{ event.occurred_at|date:'c' }}">{{ event.occurred_at|date:"d/m/Y H:i" }}</time></div><p class="activity-object">{{ event.object_label }}</p><div class="decision-note"><span class="field-label">Gebeurtenisdetails</span><p>{{ event.detail }}</p></div><a class="section-link" href="{{ event.url }}">Open betrokken onderdeel <svg class="icon"><use href="#icon-arrow"></use></svg></a></div></article>{% empty %}<div class="empty-state"><div class="empty-icon"><svg class="icon"><use href="#icon-activity"></use></svg></div><h2>Geen activiteit gevonden</h2><p>Deze filter bevat nog geen auditwaardige gebeurtenis.</p><a class="button button-ghost" href="{% url 'jobs:activity' %}">Alle activiteit tonen</a></div>{% endfor %}
|
||||
</section>
|
||||
{% if event_page.paginator.num_pages > 1 %}<nav class="pagination" aria-label="Activiteitspaginering">{% if event_page.has_previous %}<a class="button button-ghost" href="?page={{ event_page.previous_page_number }}&type={{ current_type }}&q={{ request.GET.q|urlencode }}">Vorige</a>{% endif %}<span>Pagina {{ event_page.number }} van {{ event_page.paginator.num_pages }}</span>{% if event_page.has_next %}<a class="button button-ghost" href="?page={{ event_page.next_page_number }}&type={{ current_type }}&q={{ request.GET.q|urlencode }}">Volgende</a>{% endif %}</nav>{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dossier · {{ object.job.original_title }}{% endblock %}
|
||||
{% block content %}
|
||||
<a class="back-link" href="{% url 'jobs:applications' %}">← Terug naar de pipeline</a>
|
||||
<header class="detail-hero application-hero"><div><p class="eyebrow">Sollicitatiedossier · {{ object.get_status_display }}</p><h1>{{ object.job.original_title }}</h1><p class="detail-employer">{{ object.job.employer_name }}</p><div class="job-meta"><span>{{ object.job.raw_location|default:"Locatie onbekend" }}</span>{% if object.applied_at %}<span>gestart {{ object.applied_at|date:"d/m/Y" }}</span>{% endif %}{% if object.follow_up_date %}<span>opvolging {{ object.follow_up_date|date:"d/m/Y" }}</span>{% endif %}</div></div><div class="page-actions"><a class="button button-ghost" href="{% url 'jobs:application-print' object.pk %}" target="_blank" rel="noopener">Afdrukweergave</a><a class="button button-secondary" href="{% url 'jobs:application-export' object.pk %}">Exporteren</a></div></header>
|
||||
<div class="application-editor-grid"><form method="post" class="form-panel">{% csrf_token %}<span class="section-kicker">Dossiergegevens</span><h2>Opvolging en contact</h2>{% if form.non_field_errors %}<div class="message error">{{ form.non_field_errors }}</div>{% endif %}<div class="form-grid">{% for field in form %}<label class="{% if field.name == 'notes' %}span-2{% endif %}"><span>{{ field.label }}</span>{{ field }}{% if field.help_text %}<small>{{ field.help_text }}</small>{% endif %}{% for error in field.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</label>{% endfor %}</div><div class="form-actions"><a class="button button-ghost" href="{% url 'jobs:applications' %}">Annuleren</a><button class="button button-primary" type="submit">Dossier opslaan</button></div></form><aside class="application-timeline panel"><span class="section-kicker">Audit trail</span><h2>Activiteit</h2><div class="timeline-compact">{% for event in object.timeline_events.all %}<article><span class="timeline-icon"><svg class="icon"><use href="#icon-activity"></use></svg></span><div><strong>{{ event.get_event_type_display }}</strong><time datetime="{{ event.created_at|date:'c' }}">{{ event.created_at|date:"d/m/Y H:i" }}</time>{% if event.metadata.from or event.metadata.to %}<small>{{ event.metadata.from|default:"—" }} → {{ event.metadata.to|default:"—" }}</small>{% endif %}</div></article>{% empty %}<p class="muted">Nog geen tijdlijngebeurtenis.</p>{% endfor %}</div></aside></div>
|
||||
<section class="panel danger-zone"><div><span class="section-kicker">Privacy</span><h2>Dossier verwijderen</h2><p class="muted">Verwijderen wist alleen jouw dossier en tijdlijn. De onderliggende vacature blijft in de radar.</p></div><form method="post" action="{% url 'jobs:application-delete' object.pk %}">{% csrf_token %}<button class="button button-danger" type="submit">Dossier verwijderen</button></form></section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Sollicitatiepipeline · VacatureRadar{% endblock %}
|
||||
{% block content %}
|
||||
<header class="cockpit-heading"><div><p class="eyebrow">Persoonlijke opvolging</p><h1>Sollicitatiepipeline</h1><p>Beheer je actieve trajecten, opvolging en dossierstatus. Iedere statuswijziging wordt in de tijdlijn vastgelegd.</p></div><div class="page-actions"><div class="view-toggle" data-view-controls aria-label="Weergave"><button type="button" data-view-switch="kanban" aria-pressed="true"><svg class="icon"><use href="#icon-pipeline"></use></svg>Kanban</button><button type="button" data-view-switch="list" aria-pressed="false"><svg class="icon"><use href="#icon-list"></use></svg>Lijst</button></div><a class="button button-primary" href="{% url 'jobs:list' %}"><svg class="icon"><use href="#icon-search"></use></svg>Vacature zoeken</a></div></header>
|
||||
|
||||
<section class="pipeline-summary" aria-label="Pipeline kerncijfers"><article><span class="field-label">Alle dossiers</span><strong>{{ applications|length }}</strong></article><article><span class="field-label">Actief</span><strong>{{ open_count }}</strong></article><article><span class="field-label">Opvolging nu</span><strong>{{ follow_up_count }}</strong></article></section>
|
||||
|
||||
{% if applications %}
|
||||
<section class="pipeline-board" data-view="kanban" aria-label="Sollicitaties per status">
|
||||
{% for column in pipeline_columns %}<div class="pipeline-column"><div class="pipeline-column-header"><span>{{ column.label }}</span><span class="pill">{{ column.applications|length }}</span></div><div>{% for application in column.applications %}<article class="application-card"><div class="title-row"><span class="pill pill-{{ application.status }}">{{ application.get_status_display }}</span>{% if application.follow_up_date and application.follow_up_date <= today %}<span class="pill pill-failed">Opvolgen</span>{% endif %}</div><h3><a href="{% url 'jobs:detail' application.job.pk %}">{{ application.job.original_title }}</a></h3><p class="muted">{{ application.job.employer_name }}{% if application.job.raw_location %} · {{ application.job.raw_location }}{% endif %}</p><div class="job-meta">{% if application.applied_at %}<span><svg class="icon"><use href="#icon-mail"></use></svg>{{ application.applied_at|date:"d/m/Y" }}</span>{% endif %}{% if application.follow_up_date %}<span><svg class="icon"><use href="#icon-calendar"></use></svg>{{ application.follow_up_date|date:"d/m/Y" }}</span>{% endif %}{% if application.contact_name %}<span>{{ application.contact_name }}</span>{% endif %}</div>{% if application.notes %}<p class="application-note">{{ application.notes|truncatewords:18 }}</p>{% endif %}<form method="post" action="{% url 'jobs:application-status' application.pk %}" class="status-form">{% csrf_token %}<label><span class="sr-only">Status voor {{ application.job.original_title }}</span><select name="status">{% for value,label in status_choices %}<option value="{{ value }}" {% if value == application.status %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label><button class="button button-secondary" type="submit">Bijwerken</button></form><a class="application-link" href="{% url 'jobs:application-edit' application.pk %}">Open dossier <svg class="icon"><use href="#icon-arrow"></use></svg></a></article>{% empty %}<div class="pipeline-empty">Geen dossier in deze fase.</div>{% endfor %}</div></div>{% endfor %}
|
||||
</section>
|
||||
|
||||
<section class="panel" data-view="list" hidden aria-label="Sollicitaties als lijst"><div class="data-table-wrap" tabindex="0"><table class="data-table"><thead><tr><th>Vacature</th><th>Status</th><th>Opvolging</th><th>Contact</th><th>Bijgewerkt</th><th><span class="sr-only">Actie</span></th></tr></thead><tbody>{% for application in applications %}<tr><td><strong>{{ application.job.original_title }}</strong><small>{{ application.job.employer_name }}</small></td><td><span class="pill pill-{{ application.status }}">{{ application.get_status_display }}</span></td><td>{{ application.follow_up_date|date:"d/m/Y"|default:"Niet gepland" }}</td><td>{{ application.contact_name|default:"Onbekend" }}</td><td>{{ application.updated_at|date:"d/m/Y H:i" }}</td><td><a class="button button-ghost" href="{% url 'jobs:application-edit' application.pk %}">Dossier</a></td></tr>{% endfor %}</tbody></table></div></section>
|
||||
{% else %}<div class="empty-state"><div class="empty-icon"><svg class="icon"><use href="#icon-pipeline"></use></svg></div><h2>Nog geen sollicitatiedossiers</h2><p>Markeer een relevante vacature als gesolliciteerd of start een dossier vanuit het detail.</p><a class="button button-primary" href="{% url 'jobs:list' %}">Nieuwe matches bekijken</a></div>{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,109 @@
|
||||
{% load static %}
|
||||
<!doctype html>
|
||||
<html lang="nl" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<meta name="theme-color" content="#071327" media="(prefers-color-scheme: dark)">
|
||||
<meta name="theme-color" content="#f4f7f7" media="(prefers-color-scheme: light)">
|
||||
<meta name="application-name" content="VacatureRadar">
|
||||
<title>{% block title %}{{ app_name }}{% endblock %}</title>
|
||||
<link rel="icon" href="{% static 'favicon.svg' %}?v={{ app_version }}" type="image/svg+xml" sizes="any">
|
||||
<link rel="stylesheet" href="{% static 'css/app.css' %}?v={{ app_version }}">
|
||||
<script src="{% static 'js/app.js' %}?v={{ app_version }}" defer></script>
|
||||
{% block extra_head %}{% endblock %}
|
||||
</head>
|
||||
<body class="{% block body_class %}{% endblock %}"{% if demo_read_only %} data-demo-read-only="true"{% endif %}>
|
||||
{% include "components/icon_sprite.html" %}
|
||||
<a class="skip-link" href="#main">Naar hoofdinhoud</a>
|
||||
{% if demo_read_only %}<span id="demo-read-only-note" class="sr-only">Niet beschikbaar in de alleen-lezen demo.</span>{% endif %}
|
||||
{% if user.is_authenticated %}
|
||||
<div class="cockpit" data-app-frame>
|
||||
<aside class="cockpit-sidebar" data-sidebar aria-label="Hoofdnavigatie">
|
||||
<div class="sidebar-heading">
|
||||
<a class="cockpit-brand" href="{% url 'dashboard:today' %}" aria-label="VacatureRadar dashboard">
|
||||
<img class="brand-radar" src="{% static 'img/vacatureradar-logo.svg' %}" alt="" width="42" height="42">
|
||||
<span class="brand-lockup"><strong>VacatureRadar</strong><small>Intelligence cockpit</small></span>
|
||||
</a>
|
||||
<button class="icon-button nav-close mobile-only" type="button" data-nav-close aria-label="Menu sluiten"><svg class="icon"><use href="#icon-close"></use></svg></button>
|
||||
</div>
|
||||
|
||||
<nav class="cockpit-nav">
|
||||
<p class="nav-caption">Intelligence</p>
|
||||
<a class="nav-item {% if request.resolver_match.namespace == 'dashboard' and request.resolver_match.url_name == 'today' %}is-active{% endif %}" href="{% url 'dashboard:today' %}"><svg class="icon"><use href="#icon-dashboard"></use></svg><span>Overzicht</span></a>
|
||||
<a class="nav-item {% if request.resolver_match.namespace == 'jobs' and request.resolver_match.url_name == 'list' %}is-active{% endif %}" href="{% url 'jobs:list' %}"><svg class="icon"><use href="#icon-radar"></use></svg><span>Nieuwe matches</span></a>
|
||||
<a class="nav-item {% if request.resolver_match.namespace == 'jobs' and request.resolver_match.url_name == 'detail' %}is-active{% endif %}" href="{% url 'jobs:list' %}?focus=all"><svg class="icon"><use href="#icon-briefcase"></use></svg><span>Alle vacatures</span></a>
|
||||
<a class="nav-item {% if request.resolver_match.namespace == 'jobs' and request.resolver_match.url_name == 'skill-insights' %}is-active{% endif %}" href="{% url 'jobs:skill-insights' %}"><svg class="icon"><use href="#icon-chart"></use></svg><span>Skillsradar</span></a>
|
||||
<a class="nav-item {% if request.resolver_match.namespace == 'jobs' and request.resolver_match.url_name == 'applications' or request.resolver_match.url_name == 'application-edit' %}is-active{% endif %}" href="{% url 'jobs:applications' %}"><svg class="icon"><use href="#icon-pipeline"></use></svg><span>Sollicitaties</span></a>
|
||||
<a class="nav-item {% if request.resolver_match.url_name == 'employers' or request.resolver_match.url_name == 'employer-detail' %}is-active{% endif %}" href="{% url 'jobs:employers' %}"><svg class="icon"><use href="#icon-building"></use></svg><span>Werkgevers</span></a>
|
||||
|
||||
<p class="nav-caption">Radarbeheer</p>
|
||||
<a class="nav-item {% if request.resolver_match.namespace == 'profiles' %}is-active{% endif %}" href="{% url 'profiles:list' %}"><svg class="icon"><use href="#icon-profile"></use></svg><span>Zoekprofiel</span></a>
|
||||
<a class="nav-item {% if request.resolver_match.namespace == 'sources' %}is-active{% endif %}" href="{% url 'sources:list' %}"><svg class="icon"><use href="#icon-sources"></use></svg><span>Bronnen</span></a>
|
||||
<a class="nav-item {% if request.resolver_match.namespace == 'dashboard' and request.resolver_match.url_name == 'system' %}is-active{% endif %}" href="{% url 'dashboard:system' %}"><svg class="icon"><use href="#icon-system"></use></svg><span>Automatisering</span></a>
|
||||
<a class="nav-item {% if request.resolver_match.url_name == 'activity' %}is-active{% endif %}" href="{% url 'jobs:activity' %}"><svg class="icon"><use href="#icon-activity"></use></svg><span>Activiteit</span></a>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<div class="sidebar-radar-card">
|
||||
<span class="status-beacon {% if navigation_profile %}is-live{% endif %}" aria-hidden="true"></span>
|
||||
<span><strong>{% if navigation_profile %}Profiel actief{% else %}Profiel nodig{% endif %}</strong><small>{% if navigation_profile %}{{ navigation_profile.name }}{% else %}Configureer je zoekprofiel{% endif %}</small></span>
|
||||
</div>
|
||||
{% include "components/developer_credit.html" %}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<button class="nav-scrim" type="button" data-nav-scrim aria-label="Menu sluiten" tabindex="-1"></button>
|
||||
<div class="cockpit-workspace">
|
||||
<header class="cockpit-topbar">
|
||||
<div class="topbar-inner">
|
||||
<div class="topbar-start">
|
||||
<button class="icon-button mobile-only" type="button" data-nav-toggle aria-label="Menu openen" aria-expanded="false"><svg class="icon"><use href="#icon-menu"></use></svg></button>
|
||||
<div class="workspace-identity desktop-label"><span>VacatureRadar</span><small>{{ page_context_label }}</small></div>
|
||||
<form class="command-search" method="get" action="{% url 'jobs:list' %}" role="search">
|
||||
<label class="sr-only" for="global-search">Zoek vacatures</label>
|
||||
<svg class="icon" aria-hidden="true"><use href="#icon-search"></use></svg>
|
||||
<input id="global-search" type="search" name="q" value="{% if request.resolver_match.namespace == 'jobs' and request.resolver_match.url_name == 'list' %}{{ request.GET.q }}{% endif %}" placeholder="Zoek vacatures op titel, werkgever of skill…" autocomplete="off">
|
||||
<kbd aria-hidden="true">/</kbd>
|
||||
</form>
|
||||
</div>
|
||||
<div class="topbar-actions">
|
||||
<a class="system-live" href="{% url 'dashboard:system' %}"><span class="status-beacon is-live" aria-hidden="true"></span><span class="desktop-label">Systeemstatus</span></a>
|
||||
<button class="icon-button" type="button" data-theme-toggle aria-label="Licht thema activeren" title="Thema wisselen"><svg class="icon theme-icon-dark"><use href="#icon-sun"></use></svg><svg class="icon theme-icon-light"><use href="#icon-moon"></use></svg></button>
|
||||
<details class="account-menu">
|
||||
<summary aria-label="Gebruikersmenu openen"><span class="avatar avatar-small" aria-hidden="true">{{ user.first_name|default:user.username|slice:":1"|upper }}</span><svg class="icon desktop-label"><use href="#icon-chevron"></use></svg></summary>
|
||||
<div class="account-popover"><strong>{{ user.get_full_name|default:user.username }}</strong><small>{{ user.username }}</small>{% if navigation_profile %}<a href="{% url 'profiles:edit' navigation_profile.pk %}">Zoekprofiel aanpassen</a>{% endif %}<form action="{% url 'logout' %}" method="post" data-demo-allow>{% csrf_token %}<button type="submit"><svg class="icon"><use href="#icon-logout"></use></svg>Afmelden</button></form></div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="main" class="cockpit-main" tabindex="-1">
|
||||
{% if demo_read_only %}
|
||||
<div class="demo-mode-banner" role="status" aria-label="Publieke demo, alleen-lezen">
|
||||
<svg class="icon" aria-hidden="true"><use href="#icon-eye"></use></svg>
|
||||
<span><strong>Publieke demo · alleen-lezen</strong><small>Je kunt alle schermen en analyses bekijken; wijzigingen en imports zijn uitgeschakeld.</small></span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if messages %}<div class="toast-stack" aria-live="polite" aria-atomic="true">{% for message in messages %}<div class="toast {{ message.tags }}"><svg class="icon" aria-hidden="true"><use href="{% if message.tags == 'error' %}#icon-alert{% else %}#icon-check{% endif %}"></use></svg><span>{{ message }}</span></div>{% endfor %}</div>{% endif %}
|
||||
{% else %}
|
||||
<main id="main" class="auth-shell" tabindex="-1">
|
||||
{% if messages %}<div class="toast-stack" aria-live="polite">{% for message in messages %}<div class="toast {{ message.tags }}">{{ message }}</div>{% endfor %}</div>{% endif %}
|
||||
{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
{% if user.is_authenticated %}
|
||||
<nav class="mobile-tabbar" aria-label="Mobiele hoofdnavigatie">
|
||||
<a class="{% if request.resolver_match.namespace == 'dashboard' %}is-active{% endif %}" href="{% url 'dashboard:today' %}"><svg class="icon"><use href="#icon-dashboard"></use></svg><span>Overzicht</span></a>
|
||||
<a class="{% if request.resolver_match.namespace == 'jobs' and request.resolver_match.url_name == 'list' %}is-active{% endif %}" href="{% url 'jobs:list' %}"><svg class="icon"><use href="#icon-radar"></use></svg><span>Matches</span></a>
|
||||
<a class="{% if request.resolver_match.url_name == 'applications' %}is-active{% endif %}" href="{% url 'jobs:applications' %}"><svg class="icon"><use href="#icon-pipeline"></use></svg><span>Pipeline</span></a>
|
||||
<a class="{% if request.resolver_match.namespace == 'profiles' %}is-active{% endif %}" href="{% url 'profiles:list' %}"><svg class="icon"><use href="#icon-profile"></use></svg><span>Profiel</span></a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,4 @@
|
||||
{% load static %}<div class="developer-credit" aria-label="Ontwikkeld door ITWorx.tech">
|
||||
<span>Ontwikkeld door</span>
|
||||
<img src="{% static 'img/itworx-wordmark.png' %}" alt="" width="113" height="25">
|
||||
</div>
|
||||
@@ -0,0 +1,35 @@
|
||||
<svg class="icon-sprite" aria-hidden="true" focusable="false">
|
||||
<symbol id="icon-radar" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><path d="M12 12 19 5M12 3v2M3 12h2M12 19v2M19 12h2"/></symbol>
|
||||
<symbol id="icon-dashboard" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></symbol>
|
||||
<symbol id="icon-search" viewBox="0 0 24 24"><circle cx="10.5" cy="10.5" r="6.5"/><path d="m15.5 15.5 5 5"/></symbol>
|
||||
<symbol id="icon-pipeline" viewBox="0 0 24 24"><path d="M5 5h14M5 12h9M5 19h5"/><circle cx="19" cy="12" r="2"/><circle cx="15" cy="19" r="2"/></symbol>
|
||||
<symbol id="icon-profile" viewBox="0 0 24 24"><circle cx="12" cy="8" r="4"/><path d="M4 21c.8-4.5 3.5-7 8-7s7.2 2.5 8 7"/></symbol>
|
||||
<symbol id="icon-sources" viewBox="0 0 24 24"><circle cx="5" cy="12" r="2"/><circle cx="19" cy="5" r="2"/><circle cx="19" cy="19" r="2"/><path d="m7 11 10-5M7 13l10 5"/></symbol>
|
||||
<symbol id="icon-system" viewBox="0 0 24 24"><path d="M4 19v-7M10 19V5M16 19v-9M22 19V2"/></symbol>
|
||||
<symbol id="icon-menu" viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h16"/></symbol>
|
||||
<symbol id="icon-close" viewBox="0 0 24 24"><path d="m5 5 14 14M19 5 5 19"/></symbol>
|
||||
<symbol id="icon-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></symbol>
|
||||
<symbol id="icon-moon" viewBox="0 0 24 24"><path d="M20 15.5A8.5 8.5 0 0 1 8.5 4 8.5 8.5 0 1 0 20 15.5Z"/></symbol>
|
||||
<symbol id="icon-logout" viewBox="0 0 24 24"><path d="M10 4H4v16h6M14 8l4 4-4 4M8 12h10"/></symbol>
|
||||
<symbol id="icon-arrow" viewBox="0 0 24 24"><path d="M5 12h14M14 7l5 5-5 5"/></symbol>
|
||||
<symbol id="icon-eye" viewBox="0 0 24 24"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></symbol>
|
||||
<symbol id="icon-eye-off" viewBox="0 0 24 24"><path d="M3 3l18 18M10.6 10.6a3 3 0 0 0 4.2 4.2M9.9 5.2A9.5 9.5 0 0 1 12 5c6.4 0 10 7 10 7a17.3 17.3 0 0 1-3.3 4.1M6.1 6.6A17.6 17.6 0 0 0 2 12s3.6 7 10 7a9.6 9.6 0 0 0 3-.5"/></symbol>
|
||||
<symbol id="icon-shield" viewBox="0 0 24 24"><path d="M12 3l7 3v5c0 4.5-3 8.3-7 10-4-1.7-7-5.5-7-10V6l7-3Z"/><path d="m9 12 2 2 4-4"/></symbol>
|
||||
<symbol id="icon-briefcase" viewBox="0 0 24 24"><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M9 7V4h6v3M3 12h18"/></symbol>
|
||||
<symbol id="icon-location" viewBox="0 0 24 24"><path d="M20 10c0 5-8 12-8 12S4 15 4 10a8 8 0 1 1 16 0Z"/><circle cx="12" cy="10" r="2.5"/></symbol>
|
||||
<symbol id="icon-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></symbol>
|
||||
<symbol id="icon-check" viewBox="0 0 24 24"><path d="m5 12 4 4L19 6"/></symbol>
|
||||
<symbol id="icon-alert" viewBox="0 0 24 24"><path d="M12 3 2.5 20h19L12 3Z"/><path d="M12 9v5M12 17h.01"/></symbol>
|
||||
<symbol id="icon-bookmark" viewBox="0 0 24 24"><path d="M6 3h12v18l-6-4-6 4V3Z"/></symbol>
|
||||
<symbol id="icon-external" viewBox="0 0 24 24"><path d="M14 4h6v6M20 4l-9 9"/><path d="M18 13v7H4V6h7"/></symbol>
|
||||
<symbol id="icon-chevron" viewBox="0 0 24 24"><path d="m7 9 5 5 5-5"/></symbol>
|
||||
<symbol id="icon-upload" viewBox="0 0 24 24"><path d="M12 16V4M7 9l5-5 5 5M4 20h16"/></symbol>
|
||||
<symbol id="icon-chart" viewBox="0 0 24 24"><path d="M4 19V9M10 19V4M16 19v-7M22 19V7"/></symbol>
|
||||
<symbol id="icon-activity" viewBox="0 0 24 24"><path d="M3 12h4l2-6 4 12 2-6h6"/></symbol>
|
||||
<symbol id="icon-building" viewBox="0 0 24 24"><path d="M4 21V5l8-3v19M12 8h8v13M7 7h2M7 11h2M7 15h2M15 11h2M15 15h2M2 21h20"/></symbol>
|
||||
<symbol id="icon-filter" viewBox="0 0 24 24"><path d="M3 5h18M6 12h12M10 19h4"/></symbol>
|
||||
<symbol id="icon-refresh" viewBox="0 0 24 24"><path d="M20 7v5h-5M4 17v-5h5"/><path d="M18.5 9A7 7 0 0 0 6 6.5L4 9M5.5 15A7 7 0 0 0 18 17.5l2-2.5"/></symbol>
|
||||
<symbol id="icon-list" viewBox="0 0 24 24"><path d="M9 6h12M9 12h12M9 18h12M3 6h.01M3 12h.01M3 18h.01"/></symbol>
|
||||
<symbol id="icon-calendar" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M8 3v4M16 3v4M3 10h18"/></symbol>
|
||||
<symbol id="icon-mail" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m4 7 8 6 8-6"/></symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,14 @@
|
||||
<div class="radar-illustration" data-radar-illustration aria-hidden="true">
|
||||
<svg viewBox="0 0 320 320" focusable="false">
|
||||
<defs>
|
||||
<radialGradient id="radar-glow" cx="50%" cy="50%" r="50%"><stop offset="0" stop-color="currentColor" stop-opacity=".16" /><stop offset="1" stop-color="currentColor" stop-opacity="0" /></radialGradient>
|
||||
<linearGradient id="radar-sweep" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="currentColor" stop-opacity=".55" /><stop offset="1" stop-color="currentColor" stop-opacity="0" /></linearGradient>
|
||||
</defs>
|
||||
<circle class="radar-illustration-glow" cx="160" cy="160" r="150" fill="url(#radar-glow)" />
|
||||
<g class="radar-illustration-grid" fill="none"><circle cx="160" cy="160" r="118" /><circle cx="160" cy="160" r="82" /><circle cx="160" cy="160" r="45" /><path d="M42 160h236M160 42v236" /></g>
|
||||
<path class="radar-illustration-sweep" d="M160 160V42a118 118 0 0 1 83.4 34.6Z" fill="url(#radar-sweep)" />
|
||||
<g class="radar-illustration-signals"><circle cx="224" cy="101" r="5" /><circle cx="103" cy="184" r="4" /><circle cx="201" cy="222" r="3" /></g>
|
||||
<circle class="radar-illustration-core" cx="160" cy="160" r="7" />
|
||||
</svg>
|
||||
<span class="radar-illustration-orbit orbit-one"></span><span class="radar-illustration-orbit orbit-two"></span>
|
||||
</div>
|
||||
@@ -0,0 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Vandaag · VacatureRadar{% endblock %}
|
||||
{% block content %}
|
||||
<header class="cockpit-heading dashboard-heading">
|
||||
<div><p class="eyebrow">Intelligence cockpit · vandaag</p><h1>Goedemorgen, {{ user.first_name|default:user.username }}.</h1><p>{{ day_conclusion }}</p></div>
|
||||
<div class="radar-status" aria-label="Profiel- en scanstatus"><span class="status-beacon {% if active_profile %}is-live{% endif %}" aria-hidden="true"></span><span>{% if active_profile %}Profiel actief · {{ active_profile.name }}{% else %}Geen actief profiel{% endif %}</span>{% if latest_scan %}<span aria-hidden="true">·</span><span>laatste URL-scan {{ latest_scan.started_at|timesince }} geleden</span>{% endif %}</div>
|
||||
</header>
|
||||
|
||||
<section class="stats-grid" aria-label="Radar kerncijfers">
|
||||
<article class="stat-card signal"><span class="kpi-label">Nieuwe IT-signalen</span><strong>{{ new_today }}</strong><small>vandaag ontdekt</small></article>
|
||||
<article class="stat-card"><span class="kpi-label">Sterke matches</span><strong>{{ strong_today }}</strong><small>vandaag · laatste profielscore</small></article>
|
||||
<article class="stat-card"><span class="kpi-label">Open trajecten</span><strong>{{ applications_open }}</strong><small>persoonlijke sollicitatiedossiers</small></article>
|
||||
<article class="stat-card"><span class="kpi-label">Radardekking</span><strong>{{ source_counts.active|default:0 }}/{{ source_counts.total|default:0 }}</strong><small>bronnen actief · {{ source_counts.attention|default:0 }} aandacht</small></article>
|
||||
</section>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<section class="dashboard-matches" aria-labelledby="best-matches-heading">
|
||||
<div class="section-heading"><div><span class="section-kicker">Persoonlijk gerangschikt</span><h2 id="best-matches-heading">Beste nieuwe matches</h2><p>De nieuwste score per vacature; datakwaliteit staat los van de inhoudelijke match.</p></div><a class="section-link" href="{% url 'jobs:list' %}">Open verkenner <svg class="icon"><use href="#icon-arrow"></use></svg></a></div>
|
||||
<div class="job-feed">
|
||||
{% for score in score_cards %}
|
||||
{% widthratio score.confidence 1 100 as confidence_percent %}
|
||||
<article class="job-card feed-card {% if score.recommendation == 'strong' %}strong-match{% endif %}">
|
||||
<div class="job-card-main">
|
||||
<div class="job-card-head"><div><div class="title-row"><span class="pill pill-{{ score.recommendation }}">{{ score.get_recommendation_display }}</span>{% if score.job.direct_employer %}<span class="pill">Directe werkgever</span>{% elif score.job.recruiter %}<span class="pill">Recruiter</span>{% endif %}</div><h3><a href="{% url 'jobs:detail' score.job.pk %}">{{ score.job.original_title }}</a></h3><p>{{ score.job.employer_name }}</p></div><div class="score-block"><strong>{{ score.score|floatformat:0 }}%</strong><small>match · data {{ confidence_percent }}%</small></div></div>
|
||||
<div class="job-meta"><span><svg class="icon"><use href="#icon-location"></use></svg>{{ score.job.raw_location|default:"Locatie onbekend" }}</span><span><svg class="icon"><use href="#icon-briefcase"></use></svg>{{ score.job.get_workplace_type_display }}</span>{% if score.job.valid_through %}<span><svg class="icon"><use href="#icon-calendar"></use></svg>sluit {{ score.job.valid_through|date:"d/m" }}</span>{% endif %}</div>
|
||||
{% if score.positives %}<p class="evidence-line positive-evidence"><svg class="icon"><use href="#icon-check"></use></svg>{{ score.positives.0 }}</p>{% endif %}
|
||||
{% if score.concerns %}<p class="evidence-line concern-evidence"><svg class="icon"><use href="#icon-alert"></use></svg>{{ score.concerns.0 }}</p>{% endif %}
|
||||
</div>
|
||||
<div class="feed-card-actions"><a class="button button-primary" href="{% url 'jobs:detail' score.job.pk %}">Analyse <svg class="icon"><use href="#icon-arrow"></use></svg></a><form method="post" action="{% url 'jobs:feedback' score.job.pk %}">{% csrf_token %}<input type="hidden" name="action" value="save"><input type="hidden" name="next" value="{{ request.path }}"><button class="button button-ghost" type="submit"><svg class="icon"><use href="#icon-bookmark"></use></svg>Bewaar</button></form></div>
|
||||
</article>
|
||||
{% empty %}
|
||||
<div class="empty-state compact-empty"><div class="empty-icon"><svg class="icon"><use href="#icon-radar"></use></svg></div><h2>Nog geen persoonlijke matches</h2><p>Na een geldige bronrun en profielscore verschijnen relevante IT-vacatures hier automatisch.</p><div class="page-actions"><a class="button button-primary" href="{% url 'sources:list' %}">Bronnen controleren</a><a class="button button-ghost" href="{% url 'profiles:list' %}">Profiel openen</a></div></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="dashboard-rail" aria-label="Radarintelligence">
|
||||
<section class="panel rail-panel radar-summary"><span class="section-kicker">Dagconclusie</span><h2>{{ day_conclusion }}</h2><div class="rail-list"><div class="rail-item"><span>Actieve IT-vacatures</span><strong>{{ active_jobs }}</strong></div><div class="rail-item"><span>Niet-IT gefilterd</span><strong>{{ filtered_non_it_jobs }}</strong></div><div class="rail-item"><span>Duplicaten herkend</span><strong>{{ duplicate_jobs }}</strong></div></div><a class="button button-secondary button-block" href="{% url 'jobs:list' %}">Bekijk alle matches</a></section>
|
||||
|
||||
<section class="panel rail-panel"><span class="section-kicker">Opvolging</span><h2>Actie vereist</h2><div class="rail-application-list">{% for application in priority_applications %}<a class="rail-application" href="{% url 'jobs:application-edit' application.pk %}"><span><strong>{{ application.job.original_title }}</strong><small>{{ application.job.employer_name }} · {{ application.get_status_display }}</small></span>{% if application.follow_up_date %}<time datetime="{{ application.follow_up_date|date:'Y-m-d' }}">{{ application.follow_up_date|date:"d/m" }}</time>{% else %}<svg class="icon"><use href="#icon-arrow"></use></svg>{% endif %}</a>{% empty %}<p class="muted small">Geen open dossier vraagt nu opvolging.</p>{% endfor %}</div><a class="button button-ghost button-block" href="{% url 'jobs:applications' %}">Open pipeline</a></section>
|
||||
|
||||
<section class="panel rail-panel"><span class="section-kicker">Recente activiteit</span><h2>Radarlog</h2><div class="mini-timeline">{% for event in recent_activity %}<div class="mini-event"><span class="status-beacon {% if event.tone == 'positive' %}is-live{% endif %}" aria-hidden="true"></span><span><strong>{{ event.title }}</strong><small>{{ event.detail }} · {{ event.occurred_at|timesince }} geleden</small></span></div>{% empty %}<p class="muted small">Nog geen betrouwbare activiteit geregistreerd.</p>{% endfor %}</div><a class="button button-ghost button-block" href="{% url 'dashboard:system' %}">Automation bekijken</a></section>
|
||||
</aside>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="nl"><body style="font-family:Arial,sans-serif;background:#f4f7fb;color:#152033;margin:0;padding:24px"><div style="max-width:720px;margin:auto;background:#fff;border-radius:18px;padding:28px"><p style="color:#4a62d8;font-weight:bold;text-transform:uppercase;letter-spacing:.08em">VacatureRadar</p><h1 style="margin:0 0 8px">Dagelijkse selectie</h1><p>{{ digest.strong_count }} sterke en {{ digest.possible_count }} mogelijke matches.</p>{% for score in digest.scores %}<div style="border:1px solid #dde3ee;border-radius:14px;padding:18px;margin:16px 0"><p style="margin:0;color:#4a62d8;font-weight:bold">{{ score.score|floatformat:0 }}/100</p><h2 style="margin:6px 0">{{ score.title }}</h2><p>{{ score.employer }} · {{ score.location }}</p>{% if score.positives %}<p><strong>Waarom:</strong> {{ score.positives|join:"; " }}</p>{% endif %}{% if score.concerns %}<p><strong>Aandachtspunt:</strong> {{ score.concerns.0 }}</p>{% endif %}<p><a href="{{ score.url }}">Open originele vacature</a></p></div>{% empty %}<p>Geen nieuwe aanbevelingen in deze digest.</p>{% endfor %}</div></body></html>
|
||||
@@ -0,0 +1,14 @@
|
||||
VacatureRadar — dagelijkse selectie
|
||||
|
||||
{{ digest.strong_count }} sterke matches
|
||||
{{ digest.possible_count }} mogelijke matches
|
||||
|
||||
{% for score in digest.scores %}{{ score.title }} — {{ score.employer }}
|
||||
Score {{ score.score|floatformat:0 }}/100 · {{ score.location }}
|
||||
{% if score.positives %}Waarom: {{ score.positives|join:"; " }}{% endif %}
|
||||
{% if score.concerns %}Aandachtspunt: {{ score.concerns.0 }}{% endif %}
|
||||
{{ score.url }}
|
||||
|
||||
{% empty %}Geen nieuwe aanbevelingen in deze digest.
|
||||
{% endfor %}
|
||||
Open VacatureRadar voor feedback en sollicitatieopvolging.
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="nl"><body style="font-family:Arial,sans-serif;background:#f4f7fb;color:#152033;margin:0;padding:24px"><div style="max-width:720px;margin:auto;background:#fff;border-radius:18px;padding:28px"><p style="color:#4a62d8;font-weight:bold;text-transform:uppercase;letter-spacing:.08em">VacatureRadar</p><h1 style="margin:0 0 8px">Herinnering</h1><p style="margin:0 0 16px;color:#0f1f3d">{{ reminder.job_title|default:"Vacature" }}</p><p style="margin:0 0 8px"><strong>Werkgever:</strong> {{ reminder.employer|default:"Onbekend" }}</p>{% if reminder.reminder_type == "closing" %}<p>Deze vacature sluit op: {{ reminder.valid_through }}</p>{% elif reminder.reminder_type == "follow_up" %}<p>Opvolgingsdatum: {{ reminder.follow_up_date }}</p><p>Status dossier: {{ reminder.job_status }}</p>{% elif reminder.reminder_type == "top_match" %}<p>Topmatch met score {{ reminder.score|floatformat:0 }}/100</p><p>Confidence: {{ reminder.confidence|floatformat:2 }}</p>{% endif %}<p><a href="{{ reminder.link }}">Open vacature</a></p></div></body></html>
|
||||
@@ -0,0 +1,18 @@
|
||||
VacatureRadar — {{ reminder.reminder_type|default:"herinnering" }}
|
||||
|
||||
{{ reminder.job_title|default:"Vacature" }}
|
||||
{{ reminder.employer|default:"Onbekende werkgever" }}
|
||||
|
||||
{% if reminder.reminder_type == "closing" %}
|
||||
Deze vacature sluit op: {{ reminder.valid_through }}.
|
||||
{% elif reminder.reminder_type == "follow_up" %}
|
||||
Opvolgingsdatum: {{ reminder.follow_up_date }}.
|
||||
Status dossier: {{ reminder.job_status }}.
|
||||
{% elif reminder.reminder_type == "top_match" %}
|
||||
Topmatch gevonden met score {{ reminder.score|floatformat:0 }}/100
|
||||
En confidence {{ reminder.confidence|floatformat:2 }}.
|
||||
{% endif %}
|
||||
|
||||
Bron: {{ reminder.link }}
|
||||
|
||||
Open vacature in VacatureRadar om dossierstatus bij te werken.
|
||||
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ employer.name }} intelligence · VacatureRadar{% endblock %}
|
||||
{% block content %}
|
||||
<a class="back-link" href="{% url 'jobs:employers' %}">← Terug naar werkgeversradar</a>
|
||||
<header class="detail-hero employer-detail-hero"><div><p class="eyebrow">Employer intelligence #{{ employer.pk }}</p><div class="title-row">{% if employer.is_recruiter %}<span class="pill">Recruiter</span>{% elif employer.is_direct_employer %}<span class="pill pill-success">Directe werkgever</span>{% endif %}{% if employer.employer_type %}<span class="pill">{{ employer.employer_type }}</span>{% endif %}</div><h1>{{ employer.name }}</h1><p class="detail-employer">{{ employer.domain|default:"Domein onbekend" }}</p></div><span class="employer-mark hero-mark">{{ employer.name|slice:":2"|upper }}</span></header>
|
||||
<section class="intelligence-strip"><div><span class="field-label">Actieve vacatures</span><strong>{{ employer.active_vacancies }}</strong><small>alle functies</small></div><div><span class="field-label">Relevante IT</span><strong>{{ employer.relevant_vacancies }}</strong><small>deterministisch gefilterd</small></div><div><span class="field-label">Gemiddelde match</span><strong>{% if employer.average_match is not None %}{{ employer.average_match|floatformat:0 }}%{% else %}—{% endif %}</strong><small>{% if active_profile %}{{ active_profile.name }}{% else %}geen actief profiel{% endif %}</small></div><div><span class="field-label">Brondekking</span><strong>{{ employer.source_count }}</strong><small>unieke bronnen</small></div></section>
|
||||
<div class="employer-detail-grid"><main><section class="detail-section"><div class="section-heading"><div><span class="section-kicker">Vacaturehistorie</span><h2>Recente vacatures</h2><p>Actief en historisch, uitsluitend uit opgeslagen bronrecords.</p></div></div><div class="employer-job-grid">{% for job in recent_jobs %}<article><div class="title-row"><span class="pill pill-{{ job.status }}">{{ job.get_status_display }}</span>{% if job.match_recommendation %}<span class="pill pill-{{ job.match_recommendation }}">{{ job.match_recommendation }}</span>{% endif %}</div><h3><a href="{% url 'jobs:detail' job.pk %}">{{ job.original_title }}</a></h3><p>{{ job.raw_location|default:"Locatie onbekend" }} · {{ job.get_workplace_type_display }}</p><div class="job-meta"><span>{{ job.first_seen|date:"d/m/Y" }}</span>{% if job.match_score is not None %}<strong>{{ job.match_score|floatformat:0 }}% match</strong>{% endif %}</div></article>{% empty %}<div class="empty-state compact-empty"><h2>Geen vacaturehistorie</h2></div>{% endfor %}</div></section>{% if match_history %}<section class="detail-section"><span class="section-kicker">Matchpatroon</span><h2>Recente profielbeoordelingen</h2><div class="match-history">{% for score in match_history %}<div><span>{{ score.created_at|date:"d/m" }}</span><i style="--match: {{ score.score }}%"></i><strong>{{ score.score|floatformat:0 }}%</strong><a href="{% url 'jobs:detail' score.job.pk %}">{{ score.job.original_title }}</a></div>{% endfor %}</div></section>{% endif %}</main><aside class="detail-sidebar"><section class="panel"><span class="section-kicker">Waargenomen context</span><h2>Werkgeversprofiel</h2><dl class="intel-facts"><div><dt>Locaties</dt><dd>{% if locations %}{{ locations|join:", " }}{% else %}Onbekend{% endif %}</dd></div><div><dt>Type</dt><dd>{{ employer.employer_type|default:"Onbekend" }}</dd></div><div><dt>Laatste signaal</dt><dd>{{ employer.latest_job_at|date:"d/m/Y"|default:"—" }}</dd></div><div><dt>Confidence</dt><dd>{% widthratio employer.confidence 1 100 %}%</dd></div></dl></section><section class="panel"><span class="section-kicker">Brongezondheid</span><h2>{{ sources|length }} gekoppelde bronnen</h2>{% for source in sources %}<a class="employer-source" href="{% url 'sources:list' %}"><span>{{ source.name }}</span><span class="pill {% if source.status == 'active' %}pill-success{% else %}pill-review{% endif %}">{{ source.get_status_display }}</span></a>{% empty %}<p class="muted">Geen gekoppelde bron opgeslagen.</p>{% endfor %}</section><section class="panel"><span class="section-kicker">Eerdere sollicitaties</span><h2>{{ applications|length }} dossier{% if applications|length != 1 %}s{% endif %}</h2>{% for application in applications %}<a class="employer-source" href="{% url 'jobs:application-edit' application.pk %}"><span>{{ application.job.original_title }}</span><span>{{ application.get_status_display }}</span></a>{% empty %}<p class="muted">Nog geen dossier bij deze werkgever.</p>{% endfor %}</section></aside></div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Werkgevers intelligence · VacatureRadar{% endblock %}
|
||||
{% block content %}
|
||||
<header class="cockpit-heading"><div><p class="eyebrow">Intelligent tracking</p><h1>Werkgeversradar</h1><p>Genormaliseerde werkgeversdata uit echte vacatures, bronnen, profielscores en sollicitatiedossiers.</p></div><div class="employer-kpis"><div><span>Gemonitord</span><strong>{{ employer_total }}</strong></div><div><span>Met actieve vacatures</span><strong>{{ employer_active_total }}</strong></div></div></header>
|
||||
<form class="activity-filterbar" method="get"><label class="grow"><span class="field-label">Zoek werkgevers</span><span class="input-with-icon"><svg class="icon"><use href="#icon-search"></use></svg><input type="search" name="q" value="{{ request.GET.q }}" placeholder="Naam, domein of locatie"></span></label><label><span class="field-label">Kanaal</span><select name="channel"><option value="">Alle kanalen</option><option value="direct" {% if request.GET.channel == 'direct' %}selected{% endif %}>Directe werkgever</option><option value="recruiter" {% if request.GET.channel == 'recruiter' %}selected{% endif %}>Recruiter</option></select></label><label class="toggle-field"><input type="checkbox" name="active" value="1" {% if request.GET.active == '1' %}checked{% endif %}><span>Alleen actief</span></label><button class="button button-secondary" type="submit">Filter</button></form>
|
||||
<div class="employer-radar-layout">
|
||||
<section class="employer-table-panel" aria-labelledby="employer-list-heading"><div class="section-heading"><div><span class="section-kicker">Gevolgde werkgevers</span><h2 id="employer-list-heading">{{ page_obj.paginator.count }} werkgevers</h2></div></div><div class="data-table-wrap" tabindex="0" aria-label="Werkgevers, horizontaal scrolbaar"><table class="data-table employer-table"><thead><tr><th>Werkgever</th><th>Type</th><th>Actief</th><th>Relevant IT</th><th>Bronnen</th><th>Gem. match</th><th>Laatste signaal</th></tr></thead><tbody>{% for item in employers %}<tr class="{% if selected_employer.pk == item.pk %}is-selected{% endif %}"><td><a href="?selected={{ item.pk }}&q={{ request.GET.q|urlencode }}&channel={{ request.GET.channel }}&active={{ request.GET.active }}"><span class="employer-mark">{{ item.name|slice:":2"|upper }}</span><span><strong>{{ item.name }}</strong><small>{{ item.domain|default:"Geen domein" }}</small></span></a></td><td>{% if item.is_recruiter %}<span class="pill">Recruiter</span>{% elif item.is_direct_employer %}<span class="pill pill-success">Direct</span>{% else %}<span class="pill">Onbekend</span>{% endif %}</td><td>{{ item.active_vacancies }}</td><td><strong class="signal-number">{{ item.relevant_vacancies }}</strong></td><td>{{ item.source_count }}</td><td>{% if item.average_match is not None %}{{ item.average_match|floatformat:0 }}%{% else %}—{% endif %}</td><td>{{ item.latest_job_at|date:"d/m/Y"|default:"—" }}</td></tr>{% empty %}<tr><td colspan="7">Geen werkgevers gevonden.</td></tr>{% endfor %}</tbody></table></div>{% if is_paginated %}<nav class="pagination" aria-label="Werkgeverspaginering">{% if page_obj.has_previous %}<a class="button button-ghost" href="?page={{ page_obj.previous_page_number }}&q={{ request.GET.q|urlencode }}&channel={{ request.GET.channel }}&active={{ request.GET.active }}">Vorige</a>{% endif %}<span>{{ page_obj.number }}/{{ page_obj.paginator.num_pages }}</span>{% if page_obj.has_next %}<a class="button button-ghost" href="?page={{ page_obj.next_page_number }}&q={{ request.GET.q|urlencode }}&channel={{ request.GET.channel }}&active={{ request.GET.active }}">Volgende</a>{% endif %}</nav>{% endif %}</section>
|
||||
|
||||
<aside class="employer-intelligence-rail" aria-label="Geselecteerde werkgever">{% if selected_employer %}<div class="employer-intel-header"><span class="section-kicker">Employer report #{{ selected_employer.pk }}</span><span class="employer-mark large">{{ selected_employer.name|slice:":2"|upper }}</span><h2>{{ selected_employer.name }}</h2><p>{{ selected_employer.domain|default:"Domein onbekend" }}</p></div><dl class="intel-facts"><div><dt>Actieve vacatures</dt><dd>{{ selected_employer.active_vacancies }}</dd></div><div><dt>Relevante IT</dt><dd>{{ selected_employer.relevant_vacancies }}</dd></div><div><dt>Eerdere dossiers</dt><dd>{{ selected_employer.previous_applications }}</dd></div><div><dt>Bronnen</dt><dd>{{ selected_employer.source_count }}</dd></div><div><dt>Gem. match</dt><dd>{% if selected_employer.average_match is not None %}{{ selected_employer.average_match|floatformat:0 }}%{% else %}Nog niet gemeten{% endif %}</dd></div><div><dt>Kanaal</dt><dd>{% if selected_employer.is_recruiter %}Recruiter{% elif selected_employer.is_direct_employer %}Direct{% else %}Onbekend{% endif %}</dd></div></dl><section class="employer-intel-section"><span class="field-label">Recente vacatures</span>{% for job in selected_detail.recent_jobs|slice:":4" %}<a class="employer-job-link" href="{% url 'jobs:detail' job.pk %}"><span><strong>{{ job.original_title }}</strong><small>{{ job.first_seen|date:"d/m/Y" }}</small></span>{% if job.match_score is not None %}<b>{{ job.match_score|floatformat:0 }}%</b>{% endif %}</a>{% empty %}<p class="muted">Nog geen vacaturehistorie.</p>{% endfor %}</section><section class="employer-intel-section"><span class="field-label">Waargenomen locaties</span><p>{% if selected_detail.locations %}{{ selected_detail.locations|join:", " }}{% else %}Onbekend{% endif %}</p></section><a class="button button-primary button-block" href="{% url 'jobs:employer-detail' selected_employer.pk %}">Volledig werkgeversrapport</a>{% else %}<div class="empty-state"><h2>Kies een werkgever</h2></div>{% endif %}</aside>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,38 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ job.original_title }} · VacatureRadar{% endblock %}
|
||||
{% block content %}
|
||||
<a class="back-link" href="{% url 'jobs:list' %}">← Terug naar de vacatureverkenner</a>
|
||||
<div class="job-detail-grid">
|
||||
<div class="detail-main">
|
||||
<header class="detail-hero">
|
||||
<div class="detail-hero-copy"><p class="eyebrow">Vacature intelligence · {{ job.get_status_display }}</p><div class="title-row">{% if score.evidence.it_relevance.relevant %}<span class="pill pill-it">IT-focus bevestigd</span>{% endif %}{% if job.direct_employer %}<span class="pill pill-success">Directe werkgever</span>{% elif job.recruiter %}<span class="pill">Recruiter</span>{% endif %}{% if duplicate_count %}<span class="pill">{{ duplicate_count }} duplicaat{% if duplicate_count != 1 %}signalen{% endif %}</span>{% endif %}</div><h1>{{ job.original_title }}</h1><p class="detail-employer">{{ job.employer_name }}</p><div class="job-meta"><span><svg class="icon"><use href="#icon-location"></use></svg>{{ job.raw_location|default:"Locatie onbekend" }}</span><span><svg class="icon"><use href="#icon-briefcase"></use></svg>{{ job.get_workplace_type_display }}</span>{% for type in job.employment_types %}<span>{{ type }}</span>{% endfor %}{% if job.hours_text %}<span>{{ job.hours_text }}</span>{% endif %}</div></div>
|
||||
{% if score %}{% widthratio score.confidence 1 100 as score_confidence_percent %}<div class="hero-score"><strong>{{ score.score|floatformat:0 }}%</strong><span>{{ score.get_recommendation_display }}</span><small>Datakwaliteit {{ score_confidence_percent }}%</small></div>{% else %}<div class="hero-score is-pending"><strong>—</strong><span>Nog niet gescoord</span><small>Brondata blijft inspecteerbaar</small></div>{% endif %}
|
||||
</header>
|
||||
|
||||
<section class="intelligence-strip" aria-label="Vacaturekerngegevens">
|
||||
<div><span class="field-label">Publicatie</span><strong>{{ job.date_posted|date:"d/m/Y"|default:"Onbekend" }}</strong><small>Eerst gezien {{ job.first_seen|date:"d/m/Y" }}</small></div>
|
||||
<div><span class="field-label">Deadline</span><strong>{{ job.valid_through|date:"d/m/Y"|default:"Niet vermeld" }}</strong><small>{% if job.valid_through %}Controleer altijd de bron{% else %}Geen einddatum in bron{% endif %}</small></div>
|
||||
<div><span class="field-label">Brondekking</span><strong>{{ source_count }}</strong><small>{{ version_count }} inhoudsversie{% if version_count != 1 %}s{% endif %}</small></div>
|
||||
{% widthratio job.extraction_confidence 1 100 as extraction_confidence_percent %}<div><span class="field-label">Extractie</span><strong>{{ extraction_confidence_percent }}%</strong><small>Datakwaliteit bronvelden</small></div>
|
||||
</section>
|
||||
|
||||
{% if score %}<section class="detail-section score-analysis"><div class="section-heading"><div><span class="section-kicker">Beslisuitleg</span><h2>Waarom deze match?</h2><p>Alle componenten zijn deterministisch. Ervaring en senioriteitslabels tellen niet mee.</p></div><span class="pill pill-{{ score.recommendation }}">{{ score.get_recommendation_display }}</span></div><div class="score-components">{% for name,value in score.components.items %}<div><span>{{ name|capfirst }}</span><div class="progress" role="img" aria-label="{{ name }}: {{ value|floatformat:1 }} procent"><i style="width: {{ value }}%"></i></div><strong>{{ value|floatformat:0 }}</strong></div>{% endfor %}</div><div class="signal-columns"><div><h3>Positieve signalen</h3><ul class="compact-list positive">{% for item in score.positives %}<li>{{ item }}</li>{% empty %}<li>Nog onvoldoende positieve signalen.</li>{% endfor %}</ul></div><div><h3>Aandachtspunten</h3><ul class="compact-list caution">{% for item in score.concerns %}<li>{{ item }}</li>{% empty %}<li>Geen specifiek aandachtspunt gevonden.</li>{% endfor %}</ul></div></div>{% if score.hard_exclusions %}<div class="message error"><strong>Harde uitsluiting:</strong> {{ score.hard_exclusions|join:", " }}</div>{% endif %}</section>{% endif %}
|
||||
|
||||
{% if job.skills_required or job.skills_preferred %}<section class="detail-section"><span class="section-kicker">Technologie & capability</span><h2>Gevraagde skills</h2><div class="skill-bands">{% if job.skills_required %}<div><span class="field-label">Expliciet vereist</span><div class="tag-cloud">{% for skill in job.skills_required %}<span class="pill pill-it">{{ skill }}</span>{% endfor %}</div></div>{% endif %}{% if job.skills_preferred %}<div><span class="field-label">Voorkeur in bron</span><div class="tag-cloud">{% for skill in job.skills_preferred %}<span class="pill">{{ skill }}</span>{% endfor %}</div></div>{% endif %}</div><a class="section-link" href="{% url 'jobs:skill-insights' %}">Vergelijk met de actuele skillsvraag <svg class="icon"><use href="#icon-arrow"></use></svg></a></section>{% endif %}
|
||||
|
||||
<section class="detail-section"><span class="section-kicker">Vacaturetekst</span><h2>Over de functie</h2>{% if job.description_html_sanitized %}<div class="prose">{{ job.description_html_sanitized|safe }}</div>{% elif job.description_text %}<div class="prose"><p>{{ job.description_text|linebreaksbr }}</p></div>{% else %}<p class="muted">Geen vacatureomschrijving beschikbaar.</p>{% endif %}{% if job.requirements %}<h3>Vereisten uit de bron</h3><ul class="compact-list">{% for requirement in job.requirements %}<li>{{ requirement }}</li>{% endfor %}</ul>{% endif %}{% if job.benefits %}<h3>Aanbod uit de bron</h3><ul class="compact-list">{% for benefit in job.benefits %}<li>{{ benefit }}</li>{% endfor %}</ul>{% endif %}</section>
|
||||
|
||||
<section class="detail-section"><div class="section-heading"><div><span class="section-kicker">Provenance</span><h2>Bronnen en betrouwbaarheid</h2><p>Ieder extern veld blijft herleidbaar tot de opgeslagen bron.</p></div><span class="pill">{{ source_count }} bron{% if source_count != 1 %}nen{% endif %}</span></div><ul class="source-list">{% for alias in job.source_aliases.all %}{% widthratio alias.extraction_confidence 1 100 as alias_confidence_percent %}<li><a href="{{ alias.url }}" target="_blank" rel="noopener noreferrer nofollow"><svg class="icon"><use href="#icon-external"></use></svg>{{ alias.source.name|default:"Onbekende bron" }}<span class="sr-only"> (opent in nieuw tabblad)</span></a><span>{{ alias.extraction_method|default:"Onbekende extractor" }} · data {{ alias_confidence_percent }}%</span></li>{% empty %}<li>Geen afzonderlijke bronalias opgeslagen.</li>{% endfor %}</ul>{% if provenance_rows %}<details class="provenance-details"><summary>Veldbewijs bekijken</summary><div class="data-table-wrap" tabindex="0" aria-label="Veldbewijs, horizontaal scrolbaar"><table class="data-table"><thead><tr><th>Veld</th><th>Methode</th><th>Confidence</th><th>Bewijs</th></tr></thead><tbody>{% for row in provenance_rows %}{% widthratio row.confidence 1 100 as row_confidence %}<tr><td>{{ row.field_name }}</td><td>{{ row.extraction_method }}</td><td>{{ row_confidence }}%</td><td>{{ row.evidence_excerpt|default:"Geen excerpt opgeslagen" }}</td></tr>{% endfor %}</tbody></table></div></details>{% endif %}</section>
|
||||
|
||||
<section class="detail-section"><span class="section-kicker">Wijzigingshistorie</span><h2>{{ version_count }} opgeslagen inhoudsversie{% if version_count != 1 %}s{% endif %}</h2><div class="version-list">{% for version in latest_versions %}<article><time datetime="{{ version.created_at|date:'c' }}">{{ version.created_at|date:"d/m/Y H:i" }}</time><span>{% if version.changed_fields %}Gewijzigd: {{ version.changed_fields|join:", " }}{% else %}Bronmoment vastgelegd{% endif %}</span></article>{% empty %}<p class="muted">Nog geen afzonderlijke wijzigingsversie opgeslagen.</p>{% endfor %}</div></section>
|
||||
</div>
|
||||
|
||||
<aside class="detail-sidebar" aria-label="Vacature intelligence zijpaneel">
|
||||
<section class="panel intelligence-summary"><span class="section-kicker">Intelligence</span><h2>Besliscontext</h2><dl class="intel-facts"><div><dt>Werkgever</dt><dd>{{ job.employer_name }}</dd></div><div><dt>Kanaal</dt><dd>{% if job.direct_employer %}Direct{% elif job.recruiter %}Recruiter{% else %}Onbekend{% endif %}</dd></div><div><dt>Werkmodel</dt><dd>{{ job.get_workplace_type_display }}</dd></div><div><dt>Locatie</dt><dd>{{ job.raw_location|default:"Onbekend" }}</dd></div>{% if job.compensation %}<div><dt>Vergoeding</dt><dd>{{ job.compensation }}</dd></div>{% endif %}<div><dt>Laatst gezien</dt><dd>{{ job.last_seen|date:"d/m/Y H:i" }}</dd></div></dl></section>
|
||||
<section class="panel"><span class="section-kicker">Jouw status</span><h2>{% if application %}{{ application.get_status_display }}{% elif latest_feedback %}{{ latest_feedback.get_action_display }}{% else %}Nog geen actie{% endif %}</h2><p class="muted">VacatureRadar bereidt je dossier voor, maar verstuurt nooit automatisch.</p>{% if application %}<a class="button button-secondary button-block" href="{% url 'jobs:application-edit' application.pk %}">Dossier openen</a>{% endif %}</section>
|
||||
<section class="panel"><span class="section-kicker">Broncontrole</span><h2>Transparant signaal</h2><dl class="intel-facts"><div><dt>Eerst gezien</dt><dd>{{ job.first_seen|date:"d/m/Y H:i" }}</dd></div><div><dt>Laatst gewijzigd</dt><dd>{{ job.last_changed|date:"d/m/Y H:i" }}</dd></div><div><dt>Status</dt><dd>{{ job.get_status_display }}</dd></div></dl>{% if job.canonical_url %}<a class="button button-ghost button-block" href="{{ job.canonical_url }}" target="_blank" rel="noopener noreferrer nofollow">Open originele vacature <svg class="icon"><use href="#icon-external"></use></svg><span class="sr-only"> (opent in nieuw tabblad)</span></a>{% endif %}</section>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div class="sticky-actions" aria-label="Vacatureacties"><form method="post" action="{% url 'jobs:feedback' job.pk %}">{% csrf_token %}<input type="hidden" name="action" value="interesting"><button class="button button-primary" type="submit"><svg class="icon"><use href="#icon-check"></use></svg>Interessant</button></form><form method="post" action="{% url 'jobs:feedback' job.pk %}">{% csrf_token %}<input type="hidden" name="action" value="save"><button class="button button-secondary" type="submit"><svg class="icon"><use href="#icon-bookmark"></use></svg>Bewaren</button></form><form method="post" action="{% url 'jobs:feedback' job.pk %}">{% csrf_token %}<input type="hidden" name="action" value="applied"><button class="button button-ghost" type="submit">Dossier starten</button></form><form method="post" action="{% url 'jobs:feedback' job.pk %}">{% csrf_token %}<input type="hidden" name="action" value="hide"><input type="hidden" name="reason" value="Niet relevant"><button class="button button-ghost" type="submit">Verbergen</button></form></div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,49 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Nieuwe matches · VacatureRadar{% endblock %}
|
||||
{% block content %}
|
||||
<header class="cockpit-heading"><div><p class="eyebrow">Vacature explorer</p><h1>Nieuwe matches</h1><p>Alleen aantoonbaar relevante IT-kansen staan standaard in de radar. Kies een resultaat voor directe intelligence.</p></div><div class="radar-status"><span class="status-beacon is-live" aria-hidden="true"></span><span>{% if current_focus == 'all' %}Volledig archief{% else %}IT-focus actief{% endif %}</span><span aria-hidden="true">·</span><strong>{{ page_obj.paginator.count }} resultaten</strong></div></header>
|
||||
|
||||
<form method="get" class="filter-console" aria-label="Vacaturefilters">
|
||||
<label class="grow"><span class="field-label">Zoeken</span><span class="input-with-icon"><svg class="icon"><use href="#icon-search"></use></svg><input type="search" name="q" value="{{ request.GET.q }}" placeholder="Titel, werkgever, locatie of skill"></span></label>
|
||||
<label><span class="field-label">Focus</span><select name="focus"><option value="it" {% if current_focus != 'all' %}selected{% endif %}>Alleen IT</option><option value="all" {% if current_focus == 'all' %}selected{% endif %}>Volledig archief</option></select></label>
|
||||
<label><span class="field-label">Match</span><select name="match"><option value="radar" {% if current_match == 'radar' %}selected{% endif %}>Radarselectie</option><option value="strong" {% if current_match == 'strong' %}selected{% endif %}>Sterk</option><option value="possible" {% if current_match == 'possible' %}selected{% endif %}>Mogelijk</option><option value="weak" {% if current_match == 'weak' %}selected{% endif %}>Laag</option><option value="hidden" {% if current_match == 'hidden' %}selected{% endif %}>Verborgen</option><option value="all" {% if current_match == 'all' %}selected{% endif %}>Alle scores</option></select></label>
|
||||
<label><span class="field-label">Kanaal</span><select name="channel"><option value="">Alle kanalen</option><option value="direct" {% if current_channel == 'direct' %}selected{% endif %}>Directe werkgever</option><option value="recruiter" {% if current_channel == 'recruiter' %}selected{% endif %}>Recruiter</option></select></label>
|
||||
<label><span class="field-label">Werkmodel</span><select name="workplace"><option value="">Elk werkmodel</option>{% for value,label in view.model.Workplace.choices %}<option value="{{ value }}" {% if request.GET.workplace == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label>
|
||||
<label><span class="field-label">Sortering</span><select name="sort"><option value="match" {% if current_sort == 'match' %}selected{% endif %}>Beste match</option><option value="newest" {% if current_sort == 'newest' %}selected{% endif %}>Nieuwste</option><option value="closing" {% if current_sort == 'closing' %}selected{% endif %}>Deadline</option></select></label>
|
||||
<input type="hidden" name="status" value="{{ request.GET.status|default:'active' }}">
|
||||
<button class="button button-primary" type="submit"><svg class="icon"><use href="#icon-filter"></use></svg>Filter</button>
|
||||
</form>
|
||||
|
||||
<div class="active-filters" aria-label="Actieve filters"><span class="pill pill-it">{% if current_focus == 'all' %}Archief{% else %}IT-focus{% endif %}</span><span class="pill">{{ current_match|default:"radar" }}</span>{% if request.GET.workplace %}<span class="pill">{{ request.GET.workplace }}</span>{% endif %}{% if current_channel %}<span class="pill">{{ current_channel }}</span>{% endif %}{% if request.GET.q %}<span class="pill">“{{ request.GET.q }}”</span>{% endif %}<a href="{% url 'jobs:list' %}">Filters wissen</a></div>
|
||||
|
||||
<div class="explorer-shell">
|
||||
<section class="explorer-results" aria-labelledby="results-heading">
|
||||
<div class="results-toolbar"><div><span class="section-kicker">Gerangschikte signalen</span><h2 id="results-heading">{{ page_obj.paginator.count }} vacatures gevonden</h2></div><span class="view-indicator"><svg class="icon"><use href="#icon-list"></use></svg>Pagina {{ page_obj.number }}</span></div>
|
||||
<div class="explorer-list">
|
||||
{% for job in jobs %}
|
||||
{% widthratio job.match_confidence 1 100 as confidence_percent %}
|
||||
<article class="explorer-card {% if selected_job.pk == job.pk %}is-selected{% endif %} {% if job.match_recommendation == 'strong' %}strong-match{% endif %}">
|
||||
<a class="explorer-card-link" href="?selected={{ job.pk }}&q={{ request.GET.q|urlencode }}&focus={{ current_focus }}&match={{ current_match }}&channel={{ current_channel }}&workplace={{ request.GET.workplace }}&status={{ request.GET.status|default:'active' }}&sort={{ current_sort }}&page={{ page_obj.number }}" aria-label="Toon intelligence voor {{ job.original_title }}"></a>
|
||||
<div class="explorer-score">{% if job.match_score is not None %}<strong>{{ job.match_score|floatformat:0 }}</strong><small>match</small>{% else %}<strong>—</strong><small>wacht</small>{% endif %}</div>
|
||||
<div class="explorer-card-body"><div class="title-row">{% if job.match_recommendation %}<span class="pill pill-{{ job.match_recommendation }}">{% if job.match_recommendation == 'strong' %}Sterk{% elif job.match_recommendation == 'possible' %}Mogelijk{% elif job.match_recommendation == 'weak' %}Laag{% else %}Verborgen{% endif %}</span>{% endif %}{% if job.direct_employer %}<span class="pill">Direct</span>{% elif job.recruiter %}<span class="pill">Recruiter</span>{% endif %}</div><h3>{{ job.original_title }}</h3><p>{{ job.employer_name }}</p><div class="job-meta"><span><svg class="icon"><use href="#icon-location"></use></svg>{{ job.raw_location|default:"Onbekend" }}</span><span>{{ job.get_workplace_type_display }}</span>{% if job.valid_through %}<span>sluit {{ job.valid_through|date:"d/m" }}</span>{% endif %}</div>{% if job.match_positives.0 %}<small class="card-signal">{{ job.match_positives.0 }}</small>{% endif %}</div>
|
||||
<svg class="icon explorer-arrow" aria-hidden="true"><use href="#icon-arrow"></use></svg>
|
||||
</article>
|
||||
{% empty %}<div class="empty-state compact-empty"><div class="empty-icon"><svg class="icon"><use href="#icon-search"></use></svg></div><h2>Geen passende vacatures</h2><p>Pas de filters aan of inspecteer bewust het volledige archief.</p><a class="button button-secondary" href="?focus=all&match=all&status=active&sort=match">Open archief</a></div>{% endfor %}
|
||||
</div>
|
||||
{% if is_paginated %}<nav class="pagination" aria-label="Paginering">{% if page_obj.has_previous %}<a class="button button-ghost" href="?page={{ page_obj.previous_page_number }}&q={{ request.GET.q|urlencode }}&status={{ request.GET.status }}&workplace={{ request.GET.workplace }}&channel={{ current_channel }}&focus={{ current_focus }}&match={{ current_match }}&sort={{ current_sort }}">Vorige</a>{% endif %}<span>Pagina {{ page_obj.number }} van {{ page_obj.paginator.num_pages }}</span>{% if page_obj.has_next %}<a class="button button-ghost" href="?page={{ page_obj.next_page_number }}&q={{ request.GET.q|urlencode }}&status={{ request.GET.status }}&workplace={{ request.GET.workplace }}&channel={{ current_channel }}&focus={{ current_focus }}&match={{ current_match }}&sort={{ current_sort }}">Volgende</a>{% endif %}</nav>{% endif %}
|
||||
</section>
|
||||
|
||||
<aside class="explorer-intelligence" aria-label="Geselecteerde vacature">
|
||||
{% if selected_job %}
|
||||
{% widthratio selected_job.match_confidence 1 100 as selected_confidence %}
|
||||
<div class="intelligence-panel">
|
||||
<div class="intelligence-hero"><span class="section-kicker">Vacature intelligence</span><div class="title-row"><span class="pill pill-{{ selected_job.match_recommendation }}">{{ selected_job.match_recommendation|default:"Niet gescoord" }}</span><span class="pill">{{ selected_job.get_status_display }}</span></div><h2>{{ selected_job.original_title }}</h2><p>{{ selected_job.employer_name }}</p><div class="intelligence-score"><strong>{% if selected_job.match_score is not None %}{{ selected_job.match_score|floatformat:0 }}%{% else %}—{% endif %}</strong><span>persoonlijke match<small>{% if selected_job.match_confidence %}Datakwaliteit {{ selected_confidence }}%{% else %}Nog niet beoordeeld{% endif %}</small></span></div></div>
|
||||
<dl class="intel-facts"><div><dt>Locatie</dt><dd>{{ selected_job.raw_location|default:"Onbekend" }}</dd></div><div><dt>Werkmodel</dt><dd>{{ selected_job.get_workplace_type_display }}</dd></div><div><dt>Gepubliceerd</dt><dd>{{ selected_job.date_posted|date:"d/m/Y"|default:"Onbekend" }}</dd></div><div><dt>Deadline</dt><dd>{{ selected_job.valid_through|date:"d/m/Y"|default:"Niet vermeld" }}</dd></div><div><dt>Bronnen</dt><dd>{{ selected_intelligence.source_count }}</dd></div><div><dt>Duplicaten</dt><dd>{{ selected_intelligence.duplicate_count }}</dd></div></dl>
|
||||
{% if selected_job.match_positives %}<div class="intel-signals"><span class="field-label">Positieve signalen</span><ul>{% for item in selected_job.match_positives|slice:":3" %}<li>{{ item }}</li>{% endfor %}</ul></div>{% endif %}
|
||||
{% if selected_job.match_concerns %}<div class="intel-signals concerns"><span class="field-label">Aandachtspunten</span><ul>{% for item in selected_job.match_concerns|slice:":2" %}<li>{{ item }}</li>{% endfor %}</ul></div>{% endif %}
|
||||
<div class="intelligence-actions"><a class="button button-primary button-block" href="{% url 'jobs:detail' selected_job.pk %}">Volledige analyse <svg class="icon"><use href="#icon-arrow"></use></svg></a><form method="post" action="{% url 'jobs:feedback' selected_job.pk %}">{% csrf_token %}<input type="hidden" name="action" value="save"><input type="hidden" name="next" value="{{ request.get_full_path }}"><button class="button button-ghost button-block" type="submit"><svg class="icon"><use href="#icon-bookmark"></use></svg>Bewaren</button></form></div>
|
||||
</div>
|
||||
{% else %}<div class="empty-state"><h2>Kies een vacature</h2><p>De intelligence verschijnt hier zonder je filters te verliezen.</p></div>{% endif %}
|
||||
</aside>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,71 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Skillsradar · VacatureRadar{% endblock %}
|
||||
{% block content %}
|
||||
<header class="page-header skills-page-header">
|
||||
<div>
|
||||
<p class="eyebrow">Marktvraag · persoonlijk leerkompas</p>
|
||||
<h1>Skillsradar</h1>
|
||||
<p>Zie wat in je actuele IT-radar terugkomt, wat je profiel al dekt en welke technologieën een gerichte bijscholing waard kunnen zijn.</p>
|
||||
</div>
|
||||
{% if active_profile %}<div class="radar-status" aria-label="Analysebasis"><span class="signal-dot" aria-hidden="true"></span><span>{{ active_profile.name }}</span><span aria-hidden="true">·</span><strong>profiel v{{ active_profile.version }}</strong></div>{% endif %}
|
||||
</header>
|
||||
|
||||
{% if report %}
|
||||
<section class="insight-hero" aria-labelledby="insight-summary-title">
|
||||
<div class="insight-orbit" aria-hidden="true"><span></span><span></span><span></span><strong>{{ report.coverage_percent }}%</strong></div>
|
||||
<div>
|
||||
<span class="section-kicker">Jouw dekking van de zichtbare vraag</span>
|
||||
<h2 id="insight-summary-title">{{ report.covered_count }} van {{ report.signal_count }} gevraagde skills staan al in je profiel</h2>
|
||||
<p>Gebaseerd op {{ report.total_jobs }} actieve, IT-relevante en niet-uitgesloten vacature{{ report.total_jobs|pluralize:"s" }}. Ervaringsjaren tellen nergens mee in deze analyse of je matchscore.</p>
|
||||
</div>
|
||||
<a class="button button-secondary" href="{% url 'profiles:edit' active_profile.pk %}">Skills in profiel beheren</a>
|
||||
</section>
|
||||
|
||||
<section class="skill-metrics" aria-label="Samenvatting skillsvraag">
|
||||
<article><span>Vacatures onderzocht</span><strong>{{ report.total_jobs }}</strong><small>huidige profielversie</small></article>
|
||||
<article><span>Met herkenbaar signaal</span><strong>{{ report.jobs_with_signals }}</strong><small>titel, skillveld of tekst</small></article>
|
||||
<article><span>Unieke skills gevraagd</span><strong>{{ report.signal_count }}</strong><small>deterministisch herkend</small></article>
|
||||
<article class="metric-accent"><span>Mogelijke leerkansen</span><strong>{{ report.gap_count }}</strong><small>gevraagd, niet aangevinkt</small></article>
|
||||
</section>
|
||||
|
||||
<form method="get" class="filter-console skills-filter-console">
|
||||
<label><span class="field-label">Skilldomein</span><select name="category"><option value="all">Alle domeinen</option>{% for value,label in report.categories %}<option value="{{ value }}" {% if report.selected_category == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label>
|
||||
<label><span class="field-label">Profieldekking</span><select name="coverage"><option value="all" {% if report.selected_coverage == 'all' %}selected{% endif %}>Alles</option><option value="gap" {% if report.selected_coverage == 'gap' %}selected{% endif %}>Nog niet in profiel</option><option value="covered" {% if report.selected_coverage == 'covered' %}selected{% endif %}>Al in profiel</option></select></label>
|
||||
<button class="button button-primary" type="submit">Analyse filteren</button>
|
||||
{% if report.selected_category != 'all' or report.selected_coverage != 'all' %}<a class="button button-ghost" href="{% url 'jobs:skill-insights' %}">Filters wissen</a>{% endif %}
|
||||
</form>
|
||||
|
||||
<div class="skills-insight-layout">
|
||||
<section aria-labelledby="demand-heading">
|
||||
<div class="results-toolbar"><div><span class="section-kicker">Vraagindicatoren</span><h2 id="demand-heading">{{ report.items|length }} skills in deze selectie</h2></div><span class="view-indicator">gesorteerd op vacaturevolume</span></div>
|
||||
<div class="skill-demand-list">
|
||||
{% for item in report.items %}
|
||||
<article class="skill-demand-card {% if not item.covered %}is-gap{% endif %}">
|
||||
<div class="skill-demand-main">
|
||||
<div class="skill-card-heading">
|
||||
<div><span class="skill-category">{{ item.category }}</span><h3>{{ item.label }}</h3></div>
|
||||
<span class="pill {% if item.covered %}pill-success{% else %}pill-review{% endif %}">{% if item.covered %}In je profiel{% else %}Leerkans{% endif %}</span>
|
||||
</div>
|
||||
<div class="demand-meter" aria-label="Komt voor in {{ item.share_percent }} procent van de onderzochte vacatures"><span style="width: {{ item.share_percent }}%"></span></div>
|
||||
<div class="skill-demand-stats"><strong>{{ item.vacancy_count }} vacature{{ item.vacancy_count|pluralize:"s" }}</strong><span>{{ item.share_percent }}% van je radar</span><span>{{ item.explicit_count }} in skillveld · {{ item.inferred_count }} via titel/tekst</span></div>
|
||||
{% if not item.covered %}<p class="learning-focus"><strong>Mogelijke volgende stap</strong>{{ item.learning_focus }}</p>{% endif %}
|
||||
{% if item.examples %}<ul class="skill-job-examples">{% for example in item.examples %}<li><a href="{% url 'jobs:detail' example.id %}">{{ example.title }}</a><span>{{ example.employer }}</span></li>{% endfor %}</ul>{% endif %}
|
||||
</div>
|
||||
<div class="skill-demand-action"><a class="button button-ghost" href="{% url 'jobs:list' %}?q={{ item.search_term|urlencode }}&status=active&focus=it&match=radar&sort=match">Vacatures bekijken</a></div>
|
||||
</article>
|
||||
{% empty %}
|
||||
<div class="empty-state compact-empty"><div class="empty-icon"><svg class="icon"><use href="#icon-radar"></use></svg></div><h2>Geen skills binnen deze filters</h2><p>Wis de filters of kies een ander skilldomein. De onderliggende vacatures blijven ongewijzigd.</p><a class="button button-secondary" href="{% url 'jobs:skill-insights' %}">Alle signalen tonen</a></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="skills-method-rail" aria-label="Uitleg bij de skillsanalyse">
|
||||
<section class="panel rail-panel"><span class="section-kicker">Wat betekent dit?</span><h2>Vraag, geen afwijzingsregel</h2><p class="muted">Een leerkans is een skill die in minstens één zichtbare vacature wordt genoemd maar nog niet in je profiel staat. Dit is nooit een blokkade en zegt niet dat je zonder die skill kansloos bent.</p></section>
|
||||
<section class="panel rail-panel"><span class="section-kicker">Bewijskwaliteit</span><h2>Feit versus tekstsignaal</h2><ul class="legend-list"><li><span class="legend-dot exact"></span><div><strong>Skillveld</strong><small>Door de bron als vereiste of voorkeur aangeleverd.</small></div></li><li><span class="legend-dot inferred"></span><div><strong>Titel/tekst</strong><small>Deterministisch aangetroffen; mogelijk context en geen harde eis.</small></div></li></ul></section>
|
||||
<section class="panel rail-panel"><span class="section-kicker">Selectiegrens</span><h2>Alleen jouw radar</h2><ol class="radar-steps"><li><strong>Actief</strong><span>Geen verlopen of verwijderde vacatures.</span></li><li><strong>IT-relevant</strong><span>Dezelfde deterministische IT-grens als de vacaturelijst.</span></li><li><strong>Niet uitgesloten</strong><span>Alleen scores van je huidige profielversie.</span></li></ol></section>
|
||||
</aside>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="empty-icon"><svg class="icon"><use href="#icon-profile"></use></svg></div><h2>Geen actief zoekprofiel</h2><p>Activeer eerst een profiel. De skillsradar blijft persoonlijk en mengt nooit gegevens van andere gebruikers.</p><a class="button button-primary" href="{% url 'profiles:list' %}">Profielen bekijken</a></div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,22 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Configureer je radar · VacatureRadar{% endblock %}
|
||||
{% block content %}
|
||||
<header class="cockpit-heading"><div><p class="eyebrow">Zoekprofiel · versie {{ object.version }}</p><h1>Configureer je radar</h1><p>Stuur rollen, technologiesignalen en harde grenzen met echte selecties. Ervaringsduur blijft uitsluitend informatieve context.</p></div><span class="pill {% if object.is_active %}pill-success{% endif %}">{% if object.is_active %}Live profiel{% else %}Gepauzeerd{% endif %}</span></header>
|
||||
<div class="profile-editor-layout">
|
||||
<form id="profile-form" method="post" class="radar-configurator">{% csrf_token %}
|
||||
{% if form.non_field_errors %}<div class="message error">{{ form.non_field_errors }}</div>{% endif %}
|
||||
<section class="form-section" id="basis"><div class="form-section-heading"><span class="source-icon"><svg class="icon"><use href="#icon-profile"></use></svg></span><div><span class="section-kicker">01 · Radaridentiteit</span><h2>Profielbasis</h2><p>Je postcode volstaat; gemeente en coördinaten worden uit de lokale gelicenseerde dataset afgeleid.</p></div></div><div class="form-grid"><label><span>{{ form.name.label }}</span>{{ form.name }}{% for error in form.name.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</label><label class="toggle-field">{{ form.is_active }}<span>{{ form.is_active.label }}</span></label><label><span>{{ form.home_postal_code.label }}</span>{{ form.home_postal_code }}<small>{{ form.home_postal_code.help_text }}</small>{% for error in form.home_postal_code.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</label><label><span>{{ form.max_distance_km.label }}</span>{{ form.max_distance_km }}{% for error in form.max_distance_km.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</label><label class="span-2"><span>{{ form.experience_years.label }}</span>{{ form.experience_years }}<small>{{ form.experience_years.help_text }}</small></label></div></section>
|
||||
|
||||
<section class="form-section" id="rollen"><div class="form-section-heading"><span class="source-icon"><svg class="icon"><use href="#icon-briefcase"></use></svg></span><div><span class="section-kicker">02 · Functierichting</span><h2>Gewenste en uitgesloten rollen</h2><p>Selecteer alle relevante richtingen; een uitgesloten titel blijft een harde grens.</p></div></div><fieldset class="choice-field"><legend>{{ form.desired_titles.label }}</legend>{{ form.desired_titles }}<small>{{ form.desired_titles.help_text }}</small>{% for error in form.desired_titles.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</fieldset><details class="config-disclosure"><summary>Uitgesloten functietitels <svg class="icon"><use href="#icon-chevron"></use></svg></summary><fieldset class="choice-field"><legend class="sr-only">{{ form.excluded_titles.label }}</legend>{{ form.excluded_titles }}<small>{{ form.excluded_titles.help_text }}</small>{% for error in form.excluded_titles.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</fieldset></details></section>
|
||||
|
||||
<section class="form-section" id="technologie"><div class="form-section-heading"><span class="source-icon"><svg class="icon"><use href="#icon-chart"></use></svg></span><div><span class="section-kicker">03 · Technologieën & capability</span><h2>Technologiesignalen</h2><p>Een skill telt alleen wanneer de vacaturetekst een expliciet signaal bevat. Meer gekozen cv-skills verwateren de score niet onbeperkt.</p></div></div><fieldset class="choice-field"><legend>{{ form.desired_skills.label }}</legend>{{ form.desired_skills }}<small>{{ form.desired_skills.help_text }}</small>{% for error in form.desired_skills.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</fieldset><details class="config-disclosure"><summary>Harde skilluitsluitingen <svg class="icon"><use href="#icon-chevron"></use></svg></summary><fieldset class="choice-field"><legend class="sr-only">{{ form.excluded_skills.label }}</legend>{{ form.excluded_skills }}{% for error in form.excluded_skills.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</fieldset></details></section>
|
||||
|
||||
<section class="form-section" id="werkvorm"><div class="form-section-heading"><span class="source-icon"><svg class="icon"><use href="#icon-location"></use></svg></span><div><span class="section-kicker">04 · Regio & organisatie</span><h2>Werkvorm en bereik</h2><p>Contract, werkmodel en regio worden als expliciete selecties opgeslagen.</p></div></div><div class="preference-grid"><fieldset class="choice-field"><legend>{{ form.allowed_employment_types.label }}</legend>{{ form.allowed_employment_types }}</fieldset><fieldset class="choice-field"><legend>{{ form.preferred_workplace.label }}</legend>{{ form.preferred_workplace }}</fieldset><fieldset class="choice-field"><legend>{{ form.preferred_regions.label }}</legend>{{ form.preferred_regions }}</fieldset><fieldset class="choice-field"><legend>{{ form.excluded_regions.label }}</legend>{{ form.excluded_regions }}{% for error in form.excluded_regions.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</fieldset></div></section>
|
||||
|
||||
<section class="form-section" id="automatisering"><div class="form-section-heading"><span class="source-icon"><svg class="icon"><use href="#icon-system"></use></svg></span><div><span class="section-kicker">05 · Rangschikking</span><h2>Drempels & samenvatting</h2><p>De topmatchdrempel ligt boven de aanbevelingsdrempel. Harde regels winnen altijd van de score.</p></div></div><div class="form-grid"><label><span>{{ form.recommendation_threshold.label }}</span>{{ form.recommendation_threshold }}{% for error in form.recommendation_threshold.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</label><label><span>{{ form.top_match_threshold.label }}</span>{{ form.top_match_threshold }}{% for error in form.top_match_threshold.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</label><label><span>{{ form.digest_time.label }}</span>{{ form.digest_time }}</label><label class="toggle-field">{{ form.learning_enabled }}<span>{{ form.learning_enabled.label }}</span><small>{{ form.learning_enabled.help_text }}</small></label></div></section>
|
||||
<div class="form-actions profile-savebar"><a class="button button-ghost" href="{% url 'profiles:list' %}">Annuleren</a><button class="button button-primary" type="submit">Instellingen opslaan</button></div>
|
||||
</form>
|
||||
|
||||
<aside class="profile-preview" aria-label="Radar preview"><section class="panel preview-panel"><div class="title-row"><span class="section-kicker">Radar preview</span><span class="pill {% if object.is_active %}pill-success{% endif %}">{% if object.is_active %}Live{% else %}Pauze{% endif %}</span></div>{% if preview_score %}{% widthratio preview_score.confidence 1 100 as preview_confidence %}<div class="preview-vacancy"><span class="field-label">Laatste echte beoordeling</span><h2>{{ preview_score.job.original_title }}</h2><p>{{ preview_score.job.employer_name }}</p><div class="intelligence-score"><strong>{{ preview_score.score|floatformat:0 }}%</strong><span>{{ preview_score.get_recommendation_display }}<small>Datakwaliteit {{ preview_confidence }}%</small></span></div>{% if preview_score.positives %}<ul class="compact-list positive">{% for item in preview_score.positives|slice:":3" %}<li>{{ item }}</li>{% endfor %}</ul>{% endif %}<a class="button button-ghost button-block" href="{% url 'jobs:detail' preview_score.job.pk %}">Bekijk onderbouwing</a></div>{% else %}<div class="empty-state compact-empty"><h2>Nog geen echte preview</h2><p>Na de volgende profielscore verschijnt hier een recente vacature, zonder demo-data.</p></div>{% endif %}<div class="rail-list"><div class="rail-item"><span>Postcode</span><strong>{{ object.home_postal_code|default:"—" }}</strong></div><div class="rail-item"><span>Rollen</span><strong>{{ object.desired_titles|length }}</strong></div><div class="rail-item"><span>Skills</span><strong>{{ object.desired_skills|length }}</strong></div><div class="rail-item"><span>Radius</span><strong>{{ object.max_distance_km }} km</strong></div></div><button class="button button-primary button-block" type="submit" form="profile-form">Radar opslaan</button></section><section class="panel"><span class="section-kicker">Revisies</span><h2>Configuratiehistorie</h2><div class="version-list">{% for revision in recent_revisions %}<article><time>{{ revision.created_at|date:"d/m/Y H:i" }}</time><span>Versie {{ revision.version }} · {{ revision.reason|default:"profielwijziging" }}</span></article>{% empty %}<p class="muted">Nog geen profielrevisie opgeslagen.</p>{% endfor %}</div></section></aside>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,21 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Zoekprofiel · VacatureRadar{% endblock %}
|
||||
{% block content %}
|
||||
<header class="cockpit-heading"><div><p class="eyebrow">Persoonlijke radar</p><h1>Zoekprofiel</h1><p>Je harde grenzen blijven deterministisch; ervaringsduur is alleen context en nooit een scorefactor.</p></div></header>
|
||||
<div class="profile-index-grid">
|
||||
{% for profile in profiles %}
|
||||
<article class="profile-index-card {% if profile.is_active %}strong-match{% endif %}">
|
||||
<div class="profile-index-content">
|
||||
<div class="profile-index-head"><span class="source-icon"><svg class="icon"><use href="#icon-profile"></use></svg></span><span class="pill {% if profile.is_active %}pill-success{% endif %}">{% if profile.is_active %}Live{% else %}Pauze{% endif %}</span></div>
|
||||
<h2>{{ profile.name }}</h2>
|
||||
<p class="muted">{{ profile.desired_titles|slice:":3"|join:", "|default:"Nog geen rollen geselecteerd" }}</p>
|
||||
<dl class="intel-facts"><div><dt>Postcode</dt><dd>{{ profile.home_postal_code|default:"—" }}</dd></div><div><dt>Radius</dt><dd>{{ profile.max_distance_km }} km</dd></div><div><dt>Skills</dt><dd>{{ profile.desired_skills|length }}</dd></div><div><dt>Versie</dt><dd>{{ profile.version }}</dd></div></dl>
|
||||
<div class="page-actions"><a class="button button-primary" href="{% url 'profiles:edit' profile.pk %}">Radar configureren</a>{% if not profile.is_active %}<form method="post" action="{% url 'profiles:activate' profile.pk %}">{% csrf_token %}<button class="button button-ghost" type="submit">Activeren</button></form>{% endif %}</div>
|
||||
</div>
|
||||
<div class="profile-index-art" title="Interactieve visualisatie van je persoonlijke radar">{% include "components/radar_illustration.html" %}</div>
|
||||
</article>
|
||||
{% empty %}
|
||||
<div class="empty-state illustrated-empty">{% include "components/radar_illustration.html" %}<h2>Geen profiel beschikbaar</h2><p>Initialiseer het persoonlijke profiel via de bootstrapopdracht.</p></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,123 @@
|
||||
{% load static %}<!doctype html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light">
|
||||
<meta name="theme-color" content="#071327">
|
||||
<meta name="application-name" content="VacatureRadar">
|
||||
<title>Inloggen · VacatureRadar</title>
|
||||
<link rel="icon" href="{% static 'favicon.svg' %}?v={{ app_version }}" type="image/svg+xml" sizes="any">
|
||||
<link rel="stylesheet" href="{% static 'css/auth.css' %}?v={{ app_version }}">
|
||||
</head>
|
||||
<body class="auth-page">
|
||||
{% include "components/icon_sprite.html" %}
|
||||
<main class="auth-shell">
|
||||
<div class="auth-brand">
|
||||
<img class="auth-brand-logo" src="{% static 'img/vacatureradar-logo.svg' %}" alt="VacatureRadar" width="72" height="72">
|
||||
<p class="auth-brand-name">VacatureRadar</p>
|
||||
<p class="auth-brand-tagline">Persoonlijke intelligence cockpit</p>
|
||||
</div>
|
||||
|
||||
<div class="auth-card">
|
||||
<h1>Inloggen</h1>
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="auth-alert {% if message.tags == 'error' %}is-error{% elif message.tags == 'success' %}is-success{% else %}is-info{% endif %}" role="alert">
|
||||
<svg class="icon" aria-hidden="true"><use href="{% if message.tags == 'error' %}#icon-alert{% else %}#icon-check{% endif %}"></use></svg>
|
||||
<span>{{ message }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if form.errors and not messages %}
|
||||
<div class="auth-alert is-error" role="alert">
|
||||
<svg class="icon" aria-hidden="true"><use href="#icon-alert"></use></svg>
|
||||
<span>E-mailadres of wachtwoord klopt niet. Probeer het opnieuw.</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" class="auth-form" novalidate>
|
||||
{% csrf_token %}
|
||||
<div class="field">
|
||||
<label for="id_username">E-mailadres</label>
|
||||
<input class="auth-input" id="id_username" name="username" type="email"
|
||||
autocomplete="username" autocapitalize="none" spellcheck="false"
|
||||
placeholder="naam@organisatie.nl" required
|
||||
{% if form.errors %}aria-invalid="true"{% endif %}
|
||||
value="{{ form.username.value|default:'' }}">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="field-label-row">
|
||||
<label for="id_password">Wachtwoord</label>
|
||||
<a class="field-link" href="{% url 'password_reset' %}">Wachtwoord vergeten?</a>
|
||||
</div>
|
||||
<div class="input-wrap">
|
||||
<input class="auth-input" id="id_password" name="password" type="password"
|
||||
autocomplete="current-password" required
|
||||
{% if form.errors %}aria-invalid="true"{% endif %}>
|
||||
<button class="pw-toggle" type="button" data-pw-toggle aria-label="Wachtwoord tonen">
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" data-eye><path d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z" stroke="currentColor" stroke-width="1.8"/><circle cx="12" cy="12" r="2.7" stroke="currentColor" stroke-width="1.8"/></svg>
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" data-eye-off hidden><path d="M3 3l18 18M10.6 6.1A10.7 10.7 0 0 1 12 6c6 0 9.5 6 9.5 6a17 17 0 0 1-2.8 3.4M6.2 6.2C3.8 8 2.5 12 2.5 12s3.5 6 9.5 6c1.2 0 2.3-.2 3.3-.6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M9.9 9.9a3 3 0 0 0 4.2 4.2" stroke="currentColor" stroke-width="1.8"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if next %}<input type="hidden" name="next" value="{{ next }}">{% endif %}
|
||||
|
||||
<button class="btn btn-primary" type="submit" data-submit>
|
||||
<span data-label>Inloggen</span>
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" data-arrow><path d="M5 12h14M14 7l5 5-5 5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
|
||||
{% if entra_enabled %}
|
||||
<div class="auth-separator"><span>Of ga door met</span></div>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
{% if entra_enabled %}
|
||||
<form method="post" action="{% url 'entra-login' %}">
|
||||
{% csrf_token %}
|
||||
{% if next %}<input type="hidden" name="next" value="{{ next }}">{% endif %}
|
||||
<button class="btn btn-sso" type="submit">
|
||||
<svg viewBox="0 0 23 23" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M1 1h10v10H1z" fill="#f35325"></path>
|
||||
<path d="M12 1h10v10H12z" fill="#81bc06"></path>
|
||||
<path d="M1 12h10v10H1z" fill="#05a6f0"></path>
|
||||
<path d="M12 12h10v10H12z" fill="#ffba08"></path>
|
||||
</svg>
|
||||
Inloggen met Entra ID
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if demo_enabled %}
|
||||
<div class="auth-secondary">
|
||||
<p>Nog geen account?</p>
|
||||
<form method="post" action="{% url 'demo-login' %}" class="btn-ghost inline-form">
|
||||
{% csrf_token %}
|
||||
{% if next %}<input type="hidden" name="next" value="{{ next }}">{% endif %}
|
||||
<button class="btn-ghost" type="submit">
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M5 12h14M14 7l5 5-5 5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
Bekijk demo
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% include "components/developer_credit.html" %}
|
||||
|
||||
<footer class="auth-footer">
|
||||
<div class="auth-badge">
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M12 3 5 6v5c0 4.7 2.8 8.2 7 10 4.2-1.8 7-5.3 7-10V6l-7-3Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg>
|
||||
<span>Local-first secure node</span>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<script src="{% static 'js/login.js' %}?v={{ app_version }}" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,36 @@
|
||||
{% load static %}<!doctype html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light">
|
||||
<title>Wachtwoord gewijzigd · VacatureRadar</title>
|
||||
<link rel="icon" href="{% static 'favicon.svg' %}?v={{ app_version }}" type="image/svg+xml" sizes="any">
|
||||
<link rel="stylesheet" href="{% static 'css/auth.css' %}?v={{ app_version }}">
|
||||
</head>
|
||||
<body class="auth-page">
|
||||
{% include "components/icon_sprite.html" %}
|
||||
<main class="auth-shell">
|
||||
<div class="auth-brand">
|
||||
<img class="auth-brand-logo" src="{% static 'img/vacatureradar-logo.svg' %}" alt="VacatureRadar" width="72" height="72">
|
||||
<p class="auth-brand-name">VacatureRadar</p>
|
||||
<p class="auth-brand-tagline">Persoonlijke intelligence cockpit</p>
|
||||
</div>
|
||||
|
||||
<div class="auth-card">
|
||||
<h1>Wachtwoord gewijzigd</h1>
|
||||
<div class="auth-alert is-success" role="status">
|
||||
<svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg>
|
||||
<span>Je wachtwoord is bijgewerkt. Je kunt nu inloggen met je nieuwe wachtwoord.</span>
|
||||
</div>
|
||||
<form method="get" action="{% url 'login' %}" class="auth-form">
|
||||
<button class="btn btn-primary" type="submit">
|
||||
Naar inloggen
|
||||
<svg class="icon" aria-hidden="true"><use href="#icon-arrow"></use></svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% include "components/developer_credit.html" %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,65 @@
|
||||
{% load static %}<!doctype html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light">
|
||||
<title>Nieuw wachtwoord · VacatureRadar</title>
|
||||
<link rel="icon" href="{% static 'favicon.svg' %}?v={{ app_version }}" type="image/svg+xml" sizes="any">
|
||||
<link rel="stylesheet" href="{% static 'css/auth.css' %}?v={{ app_version }}">
|
||||
</head>
|
||||
<body class="auth-page">
|
||||
{% include "components/icon_sprite.html" %}
|
||||
<main class="auth-shell">
|
||||
<div class="auth-brand">
|
||||
<img class="auth-brand-logo" src="{% static 'img/vacatureradar-logo.svg' %}" alt="VacatureRadar" width="72" height="72">
|
||||
<p class="auth-brand-name">VacatureRadar</p>
|
||||
<p class="auth-brand-tagline">Persoonlijke intelligence cockpit</p>
|
||||
</div>
|
||||
|
||||
<div class="auth-card">
|
||||
{% if validlink %}
|
||||
<h1>Kies een nieuw wachtwoord</h1>
|
||||
<p class="auth-lead">Gebruik een uniek, sterk wachtwoord dat je nergens anders gebruikt.</p>
|
||||
|
||||
{% if form.errors %}
|
||||
<div class="auth-alert is-error" role="alert">
|
||||
<svg class="icon" aria-hidden="true"><use href="#icon-alert"></use></svg>
|
||||
<span>Controleer de gemarkeerde velden en probeer het opnieuw.</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" class="auth-form" novalidate>
|
||||
{% csrf_token %}
|
||||
<div class="field">
|
||||
<label for="id_new_password1">Nieuw wachtwoord</label>
|
||||
<input class="auth-input" id="id_new_password1" name="new_password1" type="password"
|
||||
autocomplete="new-password" required
|
||||
{% if form.new_password1.errors %}aria-invalid="true"{% endif %}>
|
||||
{% if form.new_password1.errors %}<p class="field-error">{{ form.new_password1.errors|join:" " }}</p>{% endif %}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="id_new_password2">Herhaal wachtwoord</label>
|
||||
<input class="auth-input" id="id_new_password2" name="new_password2" type="password"
|
||||
autocomplete="new-password" required
|
||||
{% if form.new_password2.errors %}aria-invalid="true"{% endif %}>
|
||||
{% if form.new_password2.errors %}<p class="field-error">{{ form.new_password2.errors|join:" " }}</p>{% endif %}
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
Wachtwoord opslaan
|
||||
<svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg>
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<h1>Link is verlopen</h1>
|
||||
<div class="auth-alert is-error" role="alert">
|
||||
<svg class="icon" aria-hidden="true"><use href="#icon-alert"></use></svg>
|
||||
<span>Deze herstel-link is ongeldig of al gebruikt. Vraag een nieuwe link aan.</span>
|
||||
</div>
|
||||
<a class="auth-back" href="{% url 'password_reset' %}"><svg class="icon" aria-hidden="true"><use href="#icon-arrow"></use></svg>Nieuwe link aanvragen</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include "components/developer_credit.html" %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
{% load static %}<!doctype html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light">
|
||||
<title>Herstel-link verstuurd · VacatureRadar</title>
|
||||
<link rel="icon" href="{% static 'favicon.svg' %}?v={{ app_version }}" type="image/svg+xml" sizes="any">
|
||||
<link rel="stylesheet" href="{% static 'css/auth.css' %}?v={{ app_version }}">
|
||||
</head>
|
||||
<body class="auth-page">
|
||||
{% include "components/icon_sprite.html" %}
|
||||
<main class="auth-shell">
|
||||
<div class="auth-brand">
|
||||
<img class="auth-brand-logo" src="{% static 'img/vacatureradar-logo.svg' %}" alt="VacatureRadar" width="72" height="72">
|
||||
<p class="auth-brand-name">VacatureRadar</p>
|
||||
<p class="auth-brand-tagline">Persoonlijke intelligence cockpit</p>
|
||||
</div>
|
||||
|
||||
<div class="auth-card">
|
||||
<h1>Controleer je e-mail</h1>
|
||||
<div class="auth-alert is-info" role="status">
|
||||
<svg class="icon" aria-hidden="true"><use href="#icon-mail"></use></svg>
|
||||
<span>Bestaat er een account met dit e-mailadres, dan is er een herstel-link verstuurd. Volg de instructies in de mail om een nieuw wachtwoord in te stellen.</span>
|
||||
</div>
|
||||
<a class="auth-back" href="{% url 'login' %}"><svg class="icon" aria-hidden="true"><use href="#icon-arrow"></use></svg>Terug naar inloggen</a>
|
||||
</div>
|
||||
{% include "components/developer_credit.html" %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
{% autoescape off %}Hallo,
|
||||
|
||||
Je ontving deze e-mail omdat er een wachtwoordherstel is aangevraagd voor je
|
||||
VacatureRadar-account ({{ user.get_username }}).
|
||||
|
||||
Ga naar de volgende pagina om een nieuw wachtwoord te kiezen:
|
||||
|
||||
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
|
||||
|
||||
Heb je dit niet aangevraagd? Dan kun je deze e-mail negeren; je wachtwoord blijft
|
||||
ongewijzigd.
|
||||
|
||||
Met vriendelijke groet,
|
||||
VacatureRadar
|
||||
{% endautoescape %}
|
||||
@@ -0,0 +1,46 @@
|
||||
{% load static %}<!doctype html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light">
|
||||
<title>Wachtwoord herstellen · VacatureRadar</title>
|
||||
<link rel="icon" href="{% static 'favicon.svg' %}?v={{ app_version }}" type="image/svg+xml" sizes="any">
|
||||
<link rel="stylesheet" href="{% static 'css/auth.css' %}?v={{ app_version }}">
|
||||
</head>
|
||||
<body class="auth-page">
|
||||
{% include "components/icon_sprite.html" %}
|
||||
<main class="auth-shell">
|
||||
<div class="auth-brand">
|
||||
<img class="auth-brand-logo" src="{% static 'img/vacatureradar-logo.svg' %}" alt="VacatureRadar" width="72" height="72">
|
||||
<p class="auth-brand-name">VacatureRadar</p>
|
||||
<p class="auth-brand-tagline">Persoonlijke intelligence cockpit</p>
|
||||
</div>
|
||||
|
||||
<div class="auth-card">
|
||||
<h1>Wachtwoord herstellen</h1>
|
||||
<p class="auth-lead">Vul je e-mailadres in. Als er een account bij hoort, sturen we een herstel-link.</p>
|
||||
|
||||
<form method="post" class="auth-form" novalidate>
|
||||
{% csrf_token %}
|
||||
<div class="field">
|
||||
<label for="id_email">E-mailadres</label>
|
||||
<input class="auth-input" id="id_email" name="email" type="email"
|
||||
autocomplete="email" autocapitalize="none" spellcheck="false"
|
||||
placeholder="naam@organisatie.nl" required
|
||||
{% if form.email.errors %}aria-invalid="true"{% endif %}
|
||||
value="{{ form.email.value|default:'' }}">
|
||||
{% if form.email.errors %}<p class="field-error">{{ form.email.errors|join:" " }}</p>{% endif %}
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
Stuur herstel-link
|
||||
<svg class="icon" aria-hidden="true"><use href="#icon-mail"></use></svg>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<a class="auth-back" href="{% url 'login' %}"><svg class="icon" aria-hidden="true"><use href="#icon-arrow"></use></svg>Terug naar inloggen</a>
|
||||
</div>
|
||||
{% include "components/developer_credit.html" %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
VacatureRadar — wachtwoord herstellen
|
||||
@@ -0,0 +1,36 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% block title %}Bronnen & gezondheid · VacatureRadar{% endblock %}
|
||||
{% block content %}
|
||||
<header class="cockpit-heading"><div><p class="eyebrow">Bronbeheer & gezondheid</p><h1>Scan-endpoints</h1><p>Operationele status, bronbeleid en imports in één controlepaneel. Iedere netwerkactie passeert policy- en SSRF-validatie.</p></div><div class="page-actions"><a class="button button-ghost" href="{% url 'dashboard:system' %}"><svg class="icon"><use href="#icon-system"></use></svg>Automation</a><a class="button button-primary" href="#manual-import"><svg class="icon"><use href="#icon-upload"></use></svg>Importeren</a></div></header>
|
||||
|
||||
<section class="stats-grid" aria-label="Brongezondheid"><article class="stat-card signal"><span class="kpi-label">Actieve bronnen</span><strong>{{ active_source_count }}</strong><small>van {{ source_total_count }} zichtbare endpoints</small></article><article class="stat-card"><span class="kpi-label">Aandacht nodig</span><strong>{{ failed_source_count }}</strong><small>foutteller of quarantaine</small></article><article class="stat-card"><span class="kpi-label">Policyreview</span><strong>{{ review_pending_count }}</strong><small>wacht op expliciete beoordeling</small></article><article class="stat-card"><span class="kpi-label">Platformmailboxen</span><strong>{{ mailbox_connections|length }}</strong><small>{% if platform_alert_ingress_ready %}minstens één import actief{% else %}geen actieve ingress{% endif %}</small></article></section>
|
||||
|
||||
<section class="source-console" aria-labelledby="source-list-title">
|
||||
<div class="section-heading"><div><span class="section-kicker">Live bronregister</span><h2 id="source-list-title">Brongezondheid</h2><p>De status komt uit de bronconfiguratie en meest recente echte run.</p></div><form id="bulk-action-form" method="post" action="{% url 'sources:bulk' %}" class="inline-bulk-action">{% csrf_token %}<label><span class="sr-only">Bulkactie</span><select name="action"><option value="approve_to_trial">Naar proefrun</option><option value="allow">Toestaan</option><option value="pause">Pauzeren</option><option value="dismiss">Afwijzen</option></select></label><button class="button button-secondary" type="submit">Uitvoeren</button></form></div>
|
||||
<div class="source-health-list">
|
||||
{% for source in sources %}{% with latest_run=source.latest_runs.0 %}
|
||||
<article class="source-health-row">
|
||||
<div class="source-health-summary"><div class="source-identity"><span class="source-icon"><svg class="icon"><use href="#icon-sources"></use></svg></span><div><div class="title-row">{% if source.policy != 'deny' %}<input type="checkbox" name="source_ids" form="bulk-action-form" value="{{ source.pk }}" aria-label="Selecteer {{ source.name }}">{% endif %}<h2>{{ source.name }}</h2></div><p>{{ source.get_source_type_display }} · {{ source.domain|default:"Geen domein" }}</p></div></div><div class="source-health-metrics"><div><span>Vacatures</span><strong>{{ source.job_count }}</strong></div><div><span>Laatste scan</span><strong>{% if latest_run %}{{ latest_run.started_at|timesince }}{% else %}—{% endif %}</strong></div><div><span>Volgende run</span><strong>{{ source.next_run_at|date:"d/m H:i"|default:"—" }}</strong></div></div><div class="source-actions"><span class="pill {% if source.policy == 'deny' or source.status == 'quarantined' or latest_run and latest_run.status == 'failed' %}pill-failed{% elif latest_run and latest_run.status == 'success' %}pill-success{% elif latest_run and latest_run.status == 'partial' %}pill-review{% else %}pill-review{% endif %}">{% if source.policy == 'deny' %}Geblokkeerd{% elif source.status == 'quarantined' %}Quarantaine{% elif latest_run %}{{ latest_run.get_status_display }}{% else %}Geen run{% endif %}</span><button class="icon-button" type="button" data-disclosure aria-controls="source-details-{{ source.pk }}" aria-expanded="false" aria-label="Details van {{ source.name }} tonen"><svg class="icon disclosure-icon"><use href="#icon-chevron"></use></svg></button></div></div>
|
||||
<div class="source-health-details" id="source-details-{{ source.pk }}" hidden><div><span class="field-label">Runlog</span><div class="technical-log">{% if latest_run %}[{{ latest_run.started_at|date:"Y-m-d H:i:s" }}] {{ latest_run.get_status_display }}{% if latest_run.error_category %}<br>{{ latest_run.error_category }}: {{ latest_run.error_message }}{% else %}<br>Discovered {{ latest_run.discovered_count }} · Extracted {{ latest_run.extracted_count }} · Created {{ latest_run.created_count }} · Updated {{ latest_run.updated_count }} · Duplicate {{ latest_run.duplicate_count }}{% if latest_run.http_status %}<br>HTTP {{ latest_run.http_status }}{% endif %}{% endif %}{% else %}Nog geen runbewijs beschikbaar.{% endif %}</div>{% if source.discovery_evidence %}<span class="field-label">Ontdekkingsevidence</span><ul class="compact-list">{% for evidence in source.discovery_evidence %}<li>{{ evidence.label|default:"Ontdekt" }}: {{ evidence.url }}</li>{% endfor %}</ul>{% endif %}</div><dl class="intel-facts"><div><dt>Policy</dt><dd>{{ source.get_policy_display }}</dd></div><div><dt>Parser</dt><dd>{{ source.parser_key }}</dd></div><div><dt>Interval</dt><dd>{{ source.crawl_interval_minutes }} min</dd></div><div><dt>Failure count</dt><dd>{{ source.failure_count }}</dd></div></dl><div class="source-detail-actions">{% if source.policy != 'deny' %}<form method="post" action="{% url 'sources:retry' source.pk %}">{% csrf_token %}<button class="button button-primary" type="submit"><svg class="icon"><use href="#icon-refresh"></use></svg>Scan opnieuw</button></form>{% else %}<p class="message error">Bronbeleid blokkeert een nieuwe netwerkactie.</p>{% endif %}{% if source.base_url %}<a class="button button-ghost" href="{{ source.base_url }}" target="_blank" rel="noopener noreferrer nofollow">Publieke bron <svg class="icon"><use href="#icon-external"></use></svg></a>{% endif %}</div></div>
|
||||
</article>
|
||||
{% endwith %}{% empty %}<div class="empty-state"><div class="empty-icon"><svg class="icon"><use href="#icon-sources"></use></svg></div><h2>Nog geen bron geregistreerd</h2><p>Gebruik een gecontroleerde import of seedopdracht om een bron toe te voegen.</p></div>{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="source-management-grid">
|
||||
<section class="panel" aria-labelledby="platform-alerts-title"><div class="section-heading"><div><span class="section-kicker">Mail-alert ingress</span><h2 id="platform-alerts-title">Platformmailboxen</h2><p>Iedere jobsite kan een afzonderlijke mailbox en pollinterval gebruiken.</p></div><span class="pill {% if platform_alert_ingress_ready %}pill-success{% else %}pill-review{% endif %}">{{ mailbox_connections|length }} gekoppeld</span></div>{% if not mailbox_credential_key_ready %}<p class="message error">Credentialopslag is niet geconfigureerd. Stel <code>MAILBOX_CREDENTIAL_KEYS</code> in; wachtwoorden worden nooit leesbaar opgeslagen.</p>{% endif %}<div class="mailbox-list">{% for connection in mailbox_connections %}<article class="mailbox-row"><div><div class="title-row"><strong>{{ connection.get_platform_display }}</strong><span class="pill {% if connection.enabled and not connection.last_error_category %}pill-success{% elif connection.last_error_category %}pill-failed{% else %}pill-review{% endif %}">{% if connection.enabled and not connection.last_error_category %}Actief{% elif connection.last_error_category %}Aandacht{% else %}Pauze{% endif %}</span></div><p>{{ connection.get_provider_display }} · {{ connection.username }}</p><small>{% if connection.last_success_at %}Laatste import {{ connection.last_success_at|date:"d/m H:i" }}{% else %}Nog geen geslaagde import{% endif %} · interval {{ connection.get_poll_interval_minutes_display|lower }}</small></div><div class="page-actions">{% if connection.enabled %}<form method="post" action="{% url 'sources:sync_platform_alerts' connection.pk %}">{% csrf_token %}<button class="icon-button" type="submit" aria-label="{{ connection.get_platform_display }} nu synchroniseren"><svg class="icon"><use href="#icon-refresh"></use></svg></button></form>{% endif %}<a class="icon-button" href="{% url 'sources:mailbox_edit' connection.pk %}#mailbox-form" aria-label="{{ connection.get_platform_display }} wijzigen"><svg class="icon"><use href="#icon-system"></use></svg></a><form method="post" action="{% url 'sources:mailbox_toggle' connection.pk %}">{% csrf_token %}<button class="button button-ghost" type="submit">{% if connection.enabled %}Pauze{% else %}Activeren{% endif %}</button></form></div></article>{% empty %}<div class="empty-state compact-empty"><h2>Nog geen mailbox</h2><p>Koppel hieronder een mailbox voor platformalerts.</p></div>{% endfor %}</div>
|
||||
<details id="mailbox-form" class="import-disclosure" {% if editing_mailbox or mailbox_form.errors %}open{% endif %}><summary><span class="source-icon"><svg class="icon"><use href="#icon-mail"></use></svg></span><span><span class="section-kicker">Versleutelde IMAP</span><strong>{% if editing_mailbox %}Mailbox wijzigen{% else %}Platformmailbox koppelen{% endif %}</strong><small>Gmail, Outlook / Microsoft 365 of aangepaste IMAP</small></span><svg class="icon disclosure-icon"><use href="#icon-chevron"></use></svg></summary><div class="import-disclosure-body"><form method="post" action="{% if editing_mailbox %}{% url 'sources:mailbox_edit' editing_mailbox.pk %}{% else %}{% url 'sources:mailbox_add' %}{% endif %}">{% csrf_token %}<div class="mailbox-form-grid">{% for field in mailbox_form %}{% if field.name != 'enabled' %}<label><span>{{ field.label }}</span>{{ field }}{% if field.name == 'password' %}<small>Gebruik een app-wachtwoord; dit wordt nooit teruggetoond.</small>{% endif %}{% for error in field.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</label>{% endif %}{% endfor %}</div><label class="toggle-field">{{ mailbox_form.enabled }}<span>{{ mailbox_form.enabled.label }}</span></label>{% for error in mailbox_form.non_field_errors %}<div class="message error">{{ error }}</div>{% endfor %}<div class="form-actions"><button class="button button-primary" type="submit" {% if not mailbox_credential_key_ready %}disabled{% endif %}>Opslaan en testen</button>{% if editing_mailbox %}<a class="button button-ghost" href="{% url 'sources:list' %}">Annuleren</a>{% endif %}</div></form></div></details>
|
||||
</section>
|
||||
|
||||
<section class="panel" id="manual-import"><div class="section-heading"><div><span class="section-kicker">Gecontroleerde invoer</span><h2>Handmatige import</h2><p>Voeg één publieke URL of vacaturetekst toe. URL's blijven onder bronbeleid en SSRF-controle.</p></div><svg class="icon"><use href="#icon-upload"></use></svg></div><form id="manual-import-form" method="post" action="{% url 'sources:manual_import' %}" data-paste-limit="{{ manual_import_paste_max_bytes }}">{% csrf_token %}<div class="import-stack"><label><span>{{ manual_import_form.source_url.label }}</span>{{ manual_import_form.source_url }}{% for error in manual_import_form.source_url.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</label><label><span>{{ manual_import_form.pasted_text.label }}</span>{{ manual_import_form.pasted_text }}<small>Nog maximaal <span id="manual-import-byte-count">{{ manual_import_paste_max_bytes }}</span> bytes.</small>{% for error in manual_import_form.pasted_text.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</label></div>{% for error in manual_import_form.non_field_errors %}<div class="message error">{{ error }}</div>{% endfor %}<div class="form-actions"><button class="button button-primary" type="submit"><svg class="icon"><use href="#icon-upload"></use></svg>Import uitvoeren</button><button id="copy-bookmarklet" class="button button-ghost" type="button">Bookmarklet kopiëren</button><a id="manual-bookmarklet" class="button button-link" href="#" data-manual-import-endpoint="{{ manual_import_bookmarklet_endpoint }}">Bookmarklet installeren</a></div><textarea id="manual-bookmarklet-code" rows="2" readonly class="sr-only" aria-label="Bookmarklet endpoint">{{ manual_import_bookmarklet_endpoint }}</textarea></form>{% if manual_import_result %}<div class="import-result"><span class="pill pill-success">Importresultaat</span><h3>{{ manual_import_result.source_name }}</h3><dl class="intel-facts"><div><dt>Herkenbaar</dt><dd>{{ manual_import_result.extracted_count }}</dd></div><div><dt>Nieuw</dt><dd>{{ manual_import_result.created_count }}</dd></div><div><dt>Duplicaat</dt><dd>{{ manual_import_result.duplicate_count }}</dd></div></dl>{% if manual_import_result.warnings %}<ul class="compact-list caution">{% for warning in manual_import_result.warnings %}<li>{{ warning }}</li>{% endfor %}</ul>{% endif %}{% if manual_import_result.jobs %}<ul class="source-list">{% for item in manual_import_result.jobs %}<li>{% if item.id %}<a href="{% url 'jobs:detail' item.id %}">{{ item.title }}</a>{% else %}{{ item.title }}{% endif %}<span>{{ item.employer }}</span></li>{% endfor %}</ul>{% endif %}</div>{% endif %}</section>
|
||||
</div>
|
||||
|
||||
{% if watchlist_sources %}<section class="panel"><div class="section-heading"><div><span class="section-kicker">Werkgeversradar · regio {{ active_search_profile.home_postal_code|default:"2400" }}</span><h2>Werkgeverswaaklijst</h2><p>Interessante regionale werkgevers blijven zichtbaar, ook zonder open vacature.</p></div><span class="pill">{{ watchlist_count }} gevolgd</span></div><div class="watchlist-grid">{% for source in watchlist_sources %}<article><span class="source-icon"><svg class="icon"><use href="#icon-building"></use></svg></span><div><h3>{{ source.name }}</h3><p>{{ source.metadata.municipality_scope|default:"Locatie niet opgeslagen" }}</p><small>{{ source.metadata.current_observation|default:source.metadata.watchlist_reason }}</small></div>{% if source.metadata.public_job_page or source.base_url %}<a class="icon-button" href="{{ source.metadata.public_job_page|default:source.base_url }}" target="_blank" rel="noopener noreferrer nofollow" aria-label="Carrièrepagina van {{ source.name }} openen"><svg class="icon"><use href="#icon-external"></use></svg></a>{% endif %}</article>{% endfor %}</div></section>{% endif %}
|
||||
|
||||
<details class="panel platform-catalog"><summary><span><span class="section-kicker">Beschikbare alertkanalen</span><strong>Jobplatformen configureren</strong></span><svg class="icon disclosure-icon"><use href="#icon-chevron"></use></svg></summary><div class="platform-grid">{% for platform in platform_alerts %}<article><h3>{{ platform.label }}-alert instellen</h3><p>{{ platform.help_text }} VacatureRadar verwerkt alleen ontvangen vacaturelinks en logt niet in op het platform.</p><a class="button button-ghost" href="{{ platform.alert_url }}" target="_blank" rel="noopener noreferrer">Open {{ platform.label }}</a></article>{% endfor %}</div></details>
|
||||
{% if active_search_profile %}<p class="regional-source-note"><strong>2400 Mol</strong> · maximaal {{ active_search_profile.max_distance_km }} km. {% if active_search_profile.home_latitude == None or active_search_profile.home_longitude == None %}Coördinaten ontbreken nog; vacatureafstanden worden niet geschat.{% endif %}</p>{% endif %}
|
||||
<span class="sr-only">Mailbox koppelen en testen; de scheduler controleert elke vijf minuten welke mailbox aan de beurt is. Interessante werkgeverswaaklijst.</span>
|
||||
{% if manual_import_result %}<p class="import-audit-line">Modus: {{ manual_import_result.mode }} · Herkenbare items: {{ manual_import_result.extracted_count }} · Nieuwe vacatures: {{ manual_import_result.created_count }} · Duplicaten: {{ manual_import_result.duplicate_count }}</p>{% endif %}
|
||||
<script src="{% static 'js/manual_import.js' %}?v={{ app_version }}" defer></script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,12 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Automation center · VacatureRadar{% endblock %}
|
||||
{% block content %}
|
||||
<header class="cockpit-heading"><div><p class="eyebrow">Automation center</p><h1>Radarbesturing</h1><p>Echte 24-uursverwerking, bronruns, notificaties en configuratiestatus. Niet-gemeten runtime-informatie blijft expliciet onbekend.</p></div><div class="page-actions"><a class="button button-ghost" href="{% url 'jobs:activity' %}"><svg class="icon"><use href="#icon-activity"></use></svg>Activiteitenlog</a><a class="button button-primary" href="{% url 'sources:list' %}"><svg class="icon"><use href="#icon-sources"></use></svg>Bronnen beheren</a></div></header>
|
||||
<section class="automation-status {% if health.ok and runtime_heartbeat.healthy %}is-healthy{% endif %}"><div><span class="field-label">Systeemstatus</span><h2><span class="status-beacon {% if health.ok %}is-live{% endif %}" aria-hidden="true"></span>{% if health.ok %}Database operationeel{% else %}Databasecontrole vereist{% endif %}</h2></div><dl><div><dt>Runtime</dt><dd>{{ runtime_heartbeat.label }}</dd></div><div><dt>Queue</dt><dd>{{ queue_state }}</dd></div><div><dt>AI</dt><dd>{{ ai_state }}{% if ai_profiles %} · {{ ai_profiles }} profiel{% endif %}</dd></div></dl></section>
|
||||
|
||||
<section class="automation-pipeline" aria-labelledby="pipeline-title"><span class="section-kicker">Verwerkingspipeline · laatste 24 uur</span><h2 id="pipeline-title" class="sr-only">Verwerkingspipeline</h2>{% for phase in phase_rows %}<article><span class="pipeline-phase-index">0{{ forloop.counter }}</span><svg class="icon"><use href="{% if phase.key == 'discover' %}#icon-radar{% elif phase.key == 'fetch' %}#icon-refresh{% elif phase.key == 'notify' %}#icon-mail{% else %}#icon-system{% endif %}"></use></svg><strong>{{ phase.label }}</strong><b>{{ phase.value }}</b><small>{{ phase.unit }}</small></article>{% endfor %}</section>
|
||||
|
||||
<section class="automation-kpis"><article><span class="field-label">Bronruns / 24u</span><strong>{{ run_count }}</strong><small>{{ run_metrics.failures|default:0 }} mislukt</small></article><article><span class="field-label">Vacatures gevonden</span><strong>{{ run_metrics.discovered|default:0 }}</strong><small>{{ run_metrics.extracted|default:0 }} geëxtraheerd</small></article><article><span class="field-label">IT-vacatures actief</span><strong>{{ active_jobs }}</strong><small>{{ review_jobs }} beoordeelde kansen</small></article><article><span class="field-label">Notificatie-outbox</span><strong>{{ notification_counts.pending }}</strong><small>{{ notification_counts.failed }} mislukt · {{ notification_counts.sent_24h }} verzonden</small></article></section>
|
||||
|
||||
<div class="automation-layout"><section class="automation-source-table"><div class="section-heading"><div><span class="section-kicker">Brongezondheid</span><h2>{{ source_active }}/{{ source_total }} actief</h2></div><a class="section-link" href="{% url 'sources:list' %}">Alle bronnen <svg class="icon"><use href="#icon-arrow"></use></svg></a></div><div class="data-table-wrap" tabindex="0"><table class="data-table"><thead><tr><th>Bron</th><th>Status</th><th>Runs</th><th>Succesratio</th><th>Extractie</th><th>Latency</th></tr></thead><tbody>{% for row in source_health %}<tr><td><strong>{{ row.source_name }}</strong><small>{{ row.source_type }}</small></td><td><span class="pill {% if row.success_ratio >= 0.8 %}pill-success{% elif row.monitored_runs == 0 %}pill-review{% else %}pill-failed{% endif %}">{% if row.monitored_runs == 0 %}Geen meting{% else %}{{ row.source_status }}{% endif %}</span></td><td>{{ row.monitored_runs }}</td><td>{% widthratio row.success_ratio 1 100 %}%</td><td>{{ row.extracted_count }}</td><td>{% if row.avg_latency_ms is not None %}{{ row.avg_latency_ms|floatformat:0 }} ms{% else %}—{% endif %}</td></tr>{% empty %}<tr><td colspan="6">Nog geen brontelemetrie.</td></tr>{% endfor %}</tbody></table></div></section><aside class="technical-log-panel"><div class="section-heading"><div><span class="section-kicker">Technische logs</span><h2>Recente bronruns</h2></div></div><div class="live-log">{% for run in recent_runs %}<article class="{% if run.status == 'failed' %}is-error{% elif run.status == 'success' %}is-success{% endif %}"><time>[{{ run.started_at|date:"H:i:s" }}]</time><strong>{{ run.get_status_display|upper }}</strong><span>{{ run.source.name }}</span><small>{% if run.error_category %}{{ run.error_category }} · {{ run.error_message }}{% else %}{{ run.extracted_count }} geëxtraheerd · {{ run.duplicate_count }} duplicaat{% endif %}</small></article>{% empty %}<p class="muted">Nog geen echte bronrun geregistreerd.</p>{% endfor %}</div><a class="button button-ghost button-block" href="{% url 'jobs:activity' %}?type=source">Volledig logboek</a></aside></div>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user