feat: personalize scoring and add skills radar
This commit is contained in:
@@ -34,6 +34,9 @@
|
||||
<a class="nav-link {% if request.resolver_match.namespace == 'jobs' and request.resolver_match.url_name == 'list' %}active{% endif %}" href="{% url 'jobs:list' %}">
|
||||
<svg class="icon"><use href="#icon-search"></use></svg><span>Vacatures<small>zoeken en filteren</small></span>
|
||||
</a>
|
||||
<a class="nav-link {% if request.resolver_match.namespace == 'jobs' and request.resolver_match.url_name == 'skill-insights' %}active{% endif %}" href="{% url 'jobs:skill-insights' %}">
|
||||
<svg class="icon"><use href="#icon-radar"></use></svg><span>Skillsradar<small>vraag en leerkansen</small></span>
|
||||
</a>
|
||||
<a class="nav-link {% if request.resolver_match.namespace == 'jobs' and request.resolver_match.url_name == 'applications' or request.resolver_match.url_name == 'application-edit' %}active{% endif %}" href="{% url 'jobs:applications' %}">
|
||||
<svg class="icon"><use href="#icon-pipeline"></use></svg><span>Sollicitaties<small>persoonlijke pipeline</small></span>
|
||||
</a>
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
|
||||
<aside class="jobs-insight-rail" aria-label="Radarcontext">
|
||||
<section class="panel rail-panel radar-summary"><span class="section-kicker">Radarbereik</span><h2>IT-focus</h2><p class="muted">Titels krijgen eerst een deterministische sectorcontrole. AI kan deze grens niet wijzigen.</p><div class="rail-list"><div class="rail-item"><span>Actieve IT-vacatures</span><strong>{{ it_job_count }}</strong></div><div class="rail-item"><span>Niet-IT uit beeld</span><strong>{{ filtered_non_it_count }}</strong></div><div class="rail-item"><span>Volledig actief archief</span><strong>{{ active_job_count }}</strong></div></div><a class="button button-ghost button-block" href="?focus={% if current_focus == 'all' %}it{% else %}all{% endif %}&match={% if current_focus == 'all' %}radar{% else %}all{% endif %}&status=active&sort=match">{% if current_focus == 'all' %}Terug naar IT-focus{% else %}Volledig archief openen{% endif %}</a></section>
|
||||
<section class="panel rail-panel"><span class="section-kicker">Actief zoekprofiel</span><h2>{{ active_profile.name|default:"Geen profiel actief" }}</h2>{% if active_profile %}<div class="profile-signal-grid"><div><span>Rollen</span><strong>{{ active_profile.desired_titles|length }}</strong></div><div><span>Skills</span><strong>{{ active_profile.desired_skills|length }}</strong></div><div><span>Radius</span><strong>{{ active_profile.max_distance_km }} km</strong></div><div><span>Vanaf</span><strong>{{ active_profile.recommendation_threshold }}%</strong></div></div><a class="button button-secondary button-block" href="{% url 'profiles:edit' active_profile.pk %}">Profiel verfijnen</a>{% else %}<p class="muted">Activeer een zoekprofiel om persoonlijke scores te berekenen.</p>{% endif %}</section>
|
||||
<section class="panel rail-panel"><span class="section-kicker">Actief zoekprofiel</span><h2>{{ active_profile.name|default:"Geen profiel actief" }}</h2>{% if active_profile %}<div class="profile-signal-grid"><div><span>Rollen</span><strong>{{ active_profile.desired_titles|length }}</strong></div><div><span>Skills</span><strong>{{ active_profile.desired_skills|length }}</strong></div><div><span>Radius</span><strong>{{ active_profile.max_distance_km }} km</strong></div><div><span>Vanaf</span><strong>{{ active_profile.recommendation_threshold }}%</strong></div></div><div class="stack-actions"><a class="button button-secondary button-block" href="{% url 'jobs:skill-insights' %}">Bekijk vraag & leerkansen</a><a class="button button-ghost button-block" href="{% url 'profiles:edit' active_profile.pk %}">Profiel verfijnen</a></div>{% else %}<p class="muted">Activeer een zoekprofiel om persoonlijke scores te berekenen.</p>{% endif %}</section>
|
||||
<section class="panel rail-panel"><span class="section-kicker">Hoe rangschikking werkt</span><h2>Deterministisch eerst</h2><ol class="radar-steps"><li><strong>IT-titel</strong><span>Sectorfilter op functietitel.</span></li><li><strong>Harde regels</strong><span>Afstand en uitsluitingen winnen altijd.</span></li><li><strong>Matchscore</strong><span>Rollen, skills en voorkeuren bepalen de volgorde.</span></li></ol></section>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -7,14 +7,14 @@
|
||||
{% if form.non_field_errors %}<div class="message error">{{ form.non_field_errors }}</div>{% endif %}
|
||||
<div class="form-grid">
|
||||
{% for field in form %}
|
||||
{% if field.name == 'name' %}<div class="form-section"><span class="form-section-icon"><svg class="icon"><use href="#icon-profile"></use></svg></span><div><h2>Profielbasis</h2><p>Je postcode volstaat; technische locatievelden worden op de achtergrond afgeleid.</p></div></div>{% elif field.name == 'desired_titles' %}<div class="form-section"><span class="form-section-icon"><svg class="icon"><use href="#icon-briefcase"></use></svg></span><div><h2>Gewenste rollen & technologie</h2><p>Kies herkenbare opties. Uitsluitingen blijven altijd doorslaggevend.</p></div></div>{% elif field.name == 'allowed_employment_types' %}<div class="form-section"><span class="form-section-icon"><svg class="icon"><use href="#icon-location"></use></svg></span><div><h2>Werkvorm, regio & reistijd</h2><p>Selecteer contractvormen, werkmodellen en regio's zonder technische notatie.</p></div></div>{% elif field.name == 'recommendation_threshold' %}<div class="form-section"><span class="form-section-icon"><svg class="icon"><use href="#icon-radar"></use></svg></span><div><h2>Scoring & automatisering</h2><p>Een topmatch ligt altijd boven de gewone aanbevelingsdrempel.</p></div></div>{% endif %}
|
||||
{% if field.name == 'name' %}<div class="form-section"><span class="form-section-icon"><svg class="icon"><use href="#icon-profile"></use></svg></span><div><h2>Profielbasis</h2><p>Je postcode volstaat voor de afstand. De ervaringsband is alleen profielcontext en heeft geen invloed op score of uitsluiting.</p></div></div>{% elif field.name == 'desired_titles' %}<div class="form-section"><span class="form-section-icon"><svg class="icon"><use href="#icon-briefcase"></use></svg></span><div><h2>Gewenste rollen & technologie</h2><p>Kies per categorie wat echt bij je profiel past. De beste titelmatch en maximaal vier aangetroffen skills bepalen de inhoudelijke aansluiting.</p></div></div>{% elif field.name == 'allowed_employment_types' %}<div class="form-section"><span class="form-section-icon"><svg class="icon"><use href="#icon-location"></use></svg></span><div><h2>Werkvorm, regio & reistijd</h2><p>Selecteer contractvormen, werkmodellen en regio's zonder technische notatie.</p></div></div>{% elif field.name == 'recommendation_threshold' %}<div class="form-section"><span class="form-section-icon"><svg class="icon"><use href="#icon-radar"></use></svg></span><div><h2>Scoring & automatisering</h2><p>Een topmatch ligt altijd boven de gewone aanbevelingsdrempel.</p></div></div>{% endif %}
|
||||
{% if field.field.widget.allow_multiple_selected %}<fieldset class="choice-field span-2"><legend>{{ field.label }}</legend>{{ field }}{% if field.help_text %}<small>{{ field.help_text }}</small>{% endif %}{% for error in field.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</fieldset>{% else %}<label><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>{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="form-actions"><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="editor-rail" aria-label="Profielsamenvatting">
|
||||
<section class="panel radar-preview"><div class="title-row"><span class="section-kicker">Radarprofiel</span><span class="pill {% if object.is_active %}pill-active{% endif %}">{% if object.is_active %}Live{% else %}Pauze{% endif %}</span></div><h2>{{ object.name }}</h2><p class="muted">De huidige opgeslagen configuratie.</p><div class="preview-score"><strong>{{ object.top_match_threshold }}%</strong><span>Topmatch vanaf</span></div><div class="rail-list"><div class="rail-item"><span>Thuislocatie</span><strong>{% if object.home_postal_code %}{{ object.home_postal_code }}{% if object.home_municipality %} · {{ object.home_municipality }}{% endif %}{% else %}Niet ingesteld{% endif %}</strong></div><div class="rail-item"><span>Gewenste rollen</span><strong>{{ object.desired_titles|length }}</strong></div><div class="rail-item"><span>Gewenste skills</span><strong>{{ object.desired_skills|length }}</strong></div><div class="rail-item"><span>Maximumafstand</span><strong>{{ object.max_distance_km }} km</strong></div><div class="rail-item"><span>Gecontroleerd leren</span><strong>{% if object.learning_enabled %}Aan{% else %}Uit{% endif %}</strong></div></div></section>
|
||||
<section class="panel radar-preview"><div class="title-row"><span class="section-kicker">Radarprofiel</span><span class="pill {% if object.is_active %}pill-active{% endif %}">{% if object.is_active %}Live{% else %}Pauze{% endif %}</span></div><h2>{{ object.name }}</h2><p class="muted">De huidige opgeslagen configuratie.</p><div class="preview-score"><strong>{{ object.top_match_threshold }}%</strong><span>Topmatch vanaf</span></div><div class="rail-list"><div class="rail-item"><span>Thuislocatie</span><strong>{% if object.home_postal_code %}{{ object.home_postal_code }}{% if object.home_municipality %} · {{ object.home_municipality }}{% endif %}{% else %}Niet ingesteld{% endif %}</strong></div><div class="rail-item"><span>IT-ervaring · informatief</span><strong>{{ object.get_experience_years_display }}</strong></div><div class="rail-item"><span>Gewenste rollen</span><strong>{{ object.desired_titles|length }}</strong></div><div class="rail-item"><span>Gewenste skills</span><strong>{{ object.desired_skills|length }}</strong></div><div class="rail-item"><span>Maximumafstand</span><strong>{{ object.max_distance_km }} km</strong></div><div class="rail-item"><span>Gecontroleerd leren</span><strong>{% if object.learning_enabled %}Aan{% else %}Uit{% endif %}</strong></div></div></section>
|
||||
<section class="panel rail-panel"><span class="section-kicker">Veilige grenzen</span><h2>Deterministisch eerst</h2><p class="muted">Harde uitsluitingen en bronbeleid veranderen niet door AI of feedback.</p><button class="button button-primary button-block" type="submit" form="profile-form">Instellingen opslaan</button></section>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user