Skip to content

Feature/884 Team Skill Tracking over time view #888

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 36 commits into from
Jun 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
6016f5b
Setup controller, view, model and necessary routes. Add tab to navbar…
ManuelMoeri May 9, 2025
bff199b
Add basic structure of graph
ManuelMoeri May 9, 2025
3e7f920
Add selects for the needed attributes
ManuelMoeri May 12, 2025
6238dd9
Add basic functions of graph to correctly dummy data
ManuelMoeri May 12, 2025
cd51859
Add translations for new navbar tab
ManuelMoeri May 14, 2025
3d159a3
Add a bit of styling to dropdowns
ManuelMoeri May 14, 2025
2db3a3d
Correctly utilize rails helper and style selects with bootstrap
ManuelMoeri May 14, 2025
de57b78
Rename controller, route and everything else to better fit it's purpose
ManuelMoeri May 14, 2025
15bcb87
Rename controller to be plural like it is supossed to be
ManuelMoeri May 14, 2025
ba91c7f
Add missing translations and a small tes
ManuelMoeri May 14, 2025
bd3c5e8
Add a quick feature spec and clenaup code in various locations
ManuelMoeri May 16, 2025
55dfcc0
Add empty controller spec
ManuelMoeri May 19, 2025
2765939
Fix typo
ManuelMoeri May 19, 2025
f5c12dc
Normalize i18n translations and fix failing test
ManuelMoeri May 19, 2025
945b906
Make rubocop happy
ManuelMoeri May 19, 2025
c9070cc
Add missing ja translation
ManuelMoeri May 21, 2025
3a698d9
Add functionality to correctly track skills for each month
ManuelMoeri May 21, 2025
40ea33c
Cleanup code to make rubcocop happy
ManuelMoeri May 21, 2025
cb4e7cd
Use translation for prompt in selects
ManuelMoeri May 21, 2025
0326e67
Change chart type to bar and add minor improvement
ManuelMoeri May 22, 2025
4b2b196
Add lots of translations and make chart display message when no data …
ManuelMoeri May 22, 2025
4883548
Normalize i18n files
ManuelMoeri May 22, 2025
f61a8dc
Resolve conversations
ManuelMoeri May 22, 2025
b489f87
Add fixtures for new model and add a test aswell to cover the basics …
ManuelMoeri May 23, 2025
d4db95e
Fix test
ManuelMoeri May 23, 2025
262a0e7
Add auto_submit_controller for correctly rendering chart.js on reload
ManuelMoeri May 26, 2025
b5e8f19
Add short readme chapter
ManuelMoeri May 26, 2025
e80fdf0
Add first version of data on view
ManuelMoeri May 28, 2025
ae0704d
Modify code to change 3 numbers of each array randomly
ManuelMoeri May 28, 2025
e7e5381
Improve logic of normal distribution when seeding
ManuelMoeri Jun 12, 2025
e89bb21
Add option for dynamically setting chart type
ManuelMoeri Jun 19, 2025
8977fec
Normalize i18n files
ManuelMoeri Jun 19, 2025
421d8dc
Delete unecessary translations of months and use date_helper instead
ManuelMoeri Jun 19, 2025
e35df73
Remove console logand add frozen magic string
ManuelMoeri Jun 19, 2025
9902549
Make rubocop 😄
ManuelMoeri Jun 19, 2025
3466fed
Resovle conversations
ManuelMoeri Jun 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,17 @@ postgres:16 "docker-entrypoint.s…" 11 seconds ago Up 10 seconds

Access the web application by browser: http://localhost:3000 and enjoy the ride!

## Skill snapshot for departments
The core competence of this feature is to track how many people in a department have a given skill. This also includes tracking the level of the skill.
It works by running a monthly DelayedJob that creates these 'Snapshots' for each department.
You can then take a look at these Snapshots using `chart.js` to see how the amount people with certain skills have changed over the span of a year.
This also features a soft-delete for skills since we want to be able to access skills that have been a thing in the past.
For local development dynamically generated, extensive seeds are available for each department and skill.

**Make sure to start the delayed job worker**, otherwise the job won't be executed. You can find help on how to do this in
[the delayed job documentation](https://github.com/collectiveidea/delayed_job?tab=readme-ov-file#running-jobs)
or just simply run `rails jobs:work` to start working off queued delayed jobs.

## Debugging
To interact with `pry` inside a controller, you have to attach to the container first using `docker attach rails`.
This will show you any **new** logs, and if you encounter a `pry` prompt, you can interact with it.
Expand Down
63 changes: 63 additions & 0 deletions app/controllers/department_skill_snapshots_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# frozen_string_literal: true

include DateHelper

class DepartmentSkillSnapshotsController < CrudController
def index
@data = chart_data.to_json
super
end

private

def chart_data
{
labels: months.compact,
datasets: dataset_values.map.with_index(1) do |label, level|
build_dataset(label, level)
end.compact
}
end

def dataset_values
%w[Azubi Junior Senior Professional Expert]
end

# level corresponds to 1-5 (Azubi = 1, ..., Expert = 5)
def build_dataset(label, level)
return unless params[:department_id].present? &&
params[:skill_id].present? &&
params[:year].present?

{
label: label,
data: get_data_for_each_level(level),
fill: false,
tension: 0.1
}
end

def get_data_for_each_level(level)
monthly_data = Array.new(12, 0)
skill_id = params[:skill_id].to_s

find_snapshots_by_department_id_and_year.each do |snapshot|
month_index = snapshot.created_at.month - 1
levels = snapshot.department_skill_levels[skill_id] || []
monthly_data[month_index] += levels.count(level)
end

monthly_data
end

def find_snapshots_by_department_id_and_year
year = params[:year].to_i
start_date = Date.new(year, 1, 1)
end_date = start_date.end_of_year

DepartmentSkillSnapshot.where(
department_id: params[:department_id],
created_at: start_date..end_date
)
end
end
5 changes: 4 additions & 1 deletion app/helpers/tab_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,18 @@ def person_tabs(person)
]
end

# rubocop:disable Metrics/LineLength
def global_tabs
[
{ title: ti('navbar.profile'), path: people_path, admin_only: false },
{ title: ti('navbar.skill_search'), path: people_skills_path, admin_only: false },
{ title: ti('navbar.cv_search'), path: cv_search_index_path, admin_only: false },
{ title: ti('navbar.skillset'), path: skills_path, admin_only: false },
{ title: ti('navbar.certificates'), path: certificates_path, admin_only: true }
{ title: ti('navbar.certificates'), path: certificates_path, admin_only: true },
{ title: ti('navbar.skills_tracking'), path: department_skill_snapshots_path, admin_only: false }
]
end
# rubocop:enable Metrics/LineLength

def extract_path(regex)
request.path.match(regex)&.captures&.join
Expand Down
9 changes: 9 additions & 0 deletions app/javascript/controllers/auto_submit_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
connect() {
requestAnimationFrame(() => {
this.element.requestSubmit()
})
}
}
65 changes: 65 additions & 0 deletions app/javascript/controllers/chart_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Controller } from "@hotwired/stimulus"
import Chart from "chart.js/auto"
import { Colors } from 'chart.js';
import annotationPlugin from 'chartjs-plugin-annotation';

export default class extends Controller {
static targets = ["canvas"]
static values = {
dataset: String,
emptychart: String,
charttype: String
}

connect() {
Chart.register(annotationPlugin);
Chart.register(Colors);

const ctx = this.canvasTarget.getContext("2d");
const chartData = JSON.parse(this.datasetValue);

const isEmpty = !chartData?.datasets?.length || chartData.datasets.every(ds => !ds.data?.length);

const options = {
responsive: true,
scales: {
y: {
beginAtZero: true
}
}
};

if (isEmpty) {
options.plugins = {
annotation: {
annotations: {
noData: {
type: 'label',
content: [ this.emptychartValue ],
position: {
x: '50%',
y: '50%'
},
font: {
size: 32,
weight: 'bold'
},
color: 'gray',
textAlign: 'center'
}
}
}
};
}

this.chart = new Chart(ctx, {
type: this.charttypeValue.toLowerCase(),
data: chartData,
options: options
});
}

disconnect() {
this.chart?.destroy()
}
}
31 changes: 31 additions & 0 deletions app/views/department_skill_snapshots/index.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
= form_with url: department_skill_snapshots_path, method: :get, data: { turbo_frame: "team-skill-chart", controller: "auto-submit" } do |f|
.d-flex.mt-4.mb-4.gap-4
%div
= f.label :department_id, t('activerecord.models.department.one'), class: "text-secondary"
= f.select :department_id,
options_from_collection_for_select(Department.all, :id, :name, params[:department_id]),
{ prompt: ta(:please_select)},
{ onchange: "this.form.requestSubmit()", class: "form-select"}
%div
= f.label :skill_id, t('activerecord.models.skill.one'), class: "text-secondary"
= f.select :skill_id,
options_from_collection_for_select(Skill.all, :id, :title, params[:skill_id]),
{ prompt: ta(:please_select)},
{ onchange: "this.form.requestSubmit()", class: "form-select" }
%div
= f.label :year, t('global.date.year'), class: "text-secondary"
= f.select :year,
options_for_select((2025..Date.today.year).to_a.reverse, params[:year] || Date.today.year),
{ prompt: ta(:please_select)},
{ onchange: "this.form.requestSubmit()", class: "form-select"}

%div
= f.label :chart_type, ti('skills_tracking.chart_type'), class: "text-secondary"
= f.select :chart_type,
options_for_select([['Line Chart', 'line'], ['Bar Chart', 'bar']].map { |label, value| [ti("skills_tracking.#{value}-chart"), value] }, params[:chart_type] || 'line'),
{ prompt: ta(:please_select)},
{ onchange: "this.form.requestSubmit()", class: "form-select"}

%turbo-frame#team-skill-chart
%div.chart-container{data: {controller: "chart", "chart-dataset-value": @data, "chart-emptychart-value": t('errors.messages.chart_data_empty'), "chart-charttype-value": params[:chart_type]}}
%canvas{ "data-chart-target": "canvas", width: "3", height: "1" }
6 changes: 6 additions & 0 deletions config/locales/de-CH.yml
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ de-CH:
messages:
authorization_error: Du hesch ke Berächtigung, diä Funktion uszfüährä.
cannot_remove: darf nid glöscht wärdä.
chart_data_empty: Wähl e Abteilig, e Skill und es Jahr us um d date ds zeige.
invalid_date_range: muess vorem Änddatum si.
max_size_10MB: darf nid grösser aus 10MB si.
max_size_error: darf nid grösser aus 10MB si.
Expand Down Expand Up @@ -233,6 +234,7 @@ de-CH:
cv_search: CV Suechi
profile: Profiu
skill_search: Skill Suechi
skills_tracking: Skills tracking
skillset: Skillset
new: Neu
people_skills:
Expand Down Expand Up @@ -260,6 +262,10 @@ de-CH:
no_results: Kei Resultatä
no_skill: Füegä Sie ä Skill zur Suechi hinzu.
search_results: Suechergäbnissä
skills_tracking:
bar-chart: Bauke
chart_type: Diagramm Typ
line-chart: Liniä
'true': Ja
helpers:
cancel: Abbrächä
Expand Down
6 changes: 6 additions & 0 deletions config/locales/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ de:
messages:
authorization_error: Du hast keine Berechtigung, diese Funktion auszuführen.
cannot_remove: darf nicht gelöscht werden.
chart_data_empty: Wähle eine Abteilung, einen Skill und ein Jahr aus um Daten anzuzeigen.
invalid_date_range: muss vor dem Enddatum sein.
max_size_10MB: darf nicht grösser als 10MB sein.
max_size_error: darf nicht grösser als 10MB sein.
Expand Down Expand Up @@ -218,6 +219,7 @@ de:
cv_search: CV Suche
profile: Profil
skill_search: Skill Suche
skills_tracking: Skills Entwicklung
skillset: Skillset
new: Neu
people_skills:
Expand Down Expand Up @@ -245,6 +247,10 @@ de:
no_results: Keine Resultate gefunden
no_skill: Füge einen Skill zur Suche hinzu.
search_results: Suchergebnisse
skills_tracking:
bar-chart: Balken
chart_type: Diagramm Typ
line-chart: Linien
'true': Ja
helpers:
cancel: Abbrechen
Expand Down
6 changes: 6 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ en:
messages:
authorization_error: You do not have permission to execute this function.
cannot_remove: must not be deleted.
chart_data_empty: Select a department, skill and a year to display data
invalid_date_range: must be before the end date.
max_size_10MB: must not be larger than 10MB.
max_size_error: must not be larger than 10MB.
Expand Down Expand Up @@ -214,6 +215,7 @@ en:
cv_search: CV search
profile: Profile
skill_search: Skill search
skills_tracking: Skills tracking
skillset: Skillset
new: New
people_skills:
Expand Down Expand Up @@ -241,6 +243,10 @@ en:
no_results: No results found
no_skill: No results found, please add a skill to the search.
search_results: Search results
skills_tracking:
bar-chart: Bar
chart_type: Chart type
line-chart: Line
'true': 'Yes'
helpers:
cancel: Cancel
Expand Down
6 changes: 6 additions & 0 deletions config/locales/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ fr:
messages:
authorization_error: Vous n'avez pas les droits pour exécuter cette fonction.
cannot_remove: ne doit pas être supprimé.
chart_data_empty: Sélectionnez un département, une compétence et une année pour afficher les données.
invalid_date_range: doit être antérieure à la date de fin.
max_size_10MB: ne doit pas dépasser 10 Mo.
max_size_error: ne doit pas dépasser 10 Mo.
Expand Down Expand Up @@ -214,6 +215,7 @@ fr:
cv_search: Recherche de CV
profile: Profil
skill_search: Recherche de compétences
skills_tracking: Suivi des compétences
skillset: Kit de compétences
new: Nouveau
people_skills:
Expand Down Expand Up @@ -241,6 +243,10 @@ fr:
no_results: Aucun résultat trouvé
no_skill: Aucun résultat trouvé, veuillez ajouter une compétence à la recherche.
search_results: Résultats de la recherche
skills_tracking:
bar-chart: Barre
chart_type: Type de graphique
line-chart: Ligne
'true': Oui
helpers:
cancel: Annuler
Expand Down
6 changes: 6 additions & 0 deletions config/locales/it.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ it:
messages:
authorization_error: Non hai i diritti per eseguire questa funzione.
cannot_remove: non deve essere cancellato.
chart_data_empty: Selezionare un unità organizzativa, una competenza e un anno per visualizzare i dati.
invalid_date_range: deve essere precedente alla data di scadenza.
max_size_10MB: non deve essere più grande di 10 MB.
max_size_error: non deve essere più grande di 10 MB.
Expand Down Expand Up @@ -214,6 +215,7 @@ it:
cv_search: Ricerca di CV
profile: Profilo
skill_search: Ricerca di competenze
skills_tracking: Tracciamento delle competenze
skillset: Competenze
new: Nuovo
people_skills:
Expand Down Expand Up @@ -241,6 +243,10 @@ it:
no_results: Nessun risultato trovato
no_skill: Nessun risultato trovato, aggiungi una competenza alla ricerca.
search_results: Risultati della ricerca
skills_tracking:
bar-chart: Bar
chart_type: Tipo di grafico
line-chart: Linea
'true': Sì
helpers:
cancel: Annullamento
Expand Down
6 changes: 6 additions & 0 deletions config/locales/ja.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ ja:
messages:
authorization_error: この機能を実行する権限がありません。
cannot_remove: 削除できません。
chart_data_empty: データを表示するには、部署、スキル、年を選択してください
invalid_date_range: 終了日より前の日付でなければなりません。
max_size_10MB: 10MBを超えてはいけません。
max_size_error: 10MBを超えてはいけません。
Expand Down Expand Up @@ -214,6 +215,7 @@ ja:
cv_search: 履歴書検索
profile: プロフィール
skill_search: スキル検索
skills_tracking: スキルトラッキング
skillset: スキルセット
new: 新規
people_skills:
Expand Down Expand Up @@ -241,6 +243,10 @@ ja:
no_results: 結果が見つかりませんでした
no_skill: 検索にヒットするスキルがありません。スキルを追加してください。
search_results: 検索結果
skills_tracking:
bar-chart: 棒グラフ
chart_type: チャートの種類
line-chart: 折れ線グラフ
'true': はい
helpers:
cancel: キャンセル
Expand Down
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
end

resources :certificates

resources :department_skill_snapshots, only: [:index]
end


Expand Down
5 changes: 5 additions & 0 deletions db/seeds/development/05_department_skill_snapshots.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# encoding: utf-8

require Rails.root.join('db', 'seeds', 'support', 'department_skill_snapshot_seeder.rb')

DepartmentskillSnapshotSeeder.new.seed_department_skill_snapshots
Loading