Skip to content

Commit c44dbbd

Browse files
tegiozcynthia-sg
andauthored
Add search appearances chart to job stats (#298)
Signed-off-by: Sergio Castaño Arteaga <[email protected]> Signed-off-by: Cintia Sánchez García <[email protected]> Co-authored-by: Cintia Sánchez García <[email protected]>
1 parent 80a6e6c commit c44dbbd

File tree

4 files changed

+190
-95
lines changed

4 files changed

+190
-95
lines changed

database/migrations/functions/dashboard/get_job_stats.sql

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,22 @@
22
create or replace function get_job_stats(p_job_id uuid)
33
returns json as $$
44
select json_strip_nulls(json_build_object(
5-
'views_total_last_month', (
5+
'search_appearances_daily', (
6+
select coalesce(json_agg(json_build_array(
7+
floor(extract(epoch from day) * 1000),
8+
total
9+
)), '[]'::json)
10+
from (
11+
select day, total
12+
from search_appearances
13+
where job_id = p_job_id
14+
and day >= current_date - '1 month'::interval
15+
order by day asc
16+
) daily_search_appearances
17+
),
18+
'search_appearances_total_last_month', (
619
select coalesce(sum(total), 0)
7-
from job_views
20+
from search_appearances
821
where job_id = p_job_id
922
and day >= current_date - '1 month'::interval
1023
),
@@ -20,6 +33,12 @@ returns json as $$
2033
and day >= current_date - '1 month'::interval
2134
order by day asc
2235
) daily_views
36+
),
37+
'views_total_last_month', (
38+
select coalesce(sum(total), 0)
39+
from job_views
40+
where job_id = p_job_id
41+
and day >= current_date - '1 month'::interval
2342
)
2443
));
2544
$$ language sql;

gitjobs-server/src/templates/dashboard/employer/jobs.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,11 +191,16 @@ impl Job {
191191
/// Statistics for a specific job.
192192
#[derive(Debug, Clone, Serialize, Deserialize)]
193193
pub(crate) struct JobStats {
194-
/// Total views in the last month.
195-
pub views_total_last_month: u64,
194+
/// Daily search appearances for the last month.
195+
/// Each entry is a tuple of (`timestamp_ms`, count).
196+
pub search_appearances_daily: Option<Vec<(u64, u64)>>,
197+
/// Total search appearances in the last month.
198+
pub search_appearances_total_last_month: u64,
196199
/// Daily views for the last month.
197200
/// Each entry is a tuple of (`timestamp_ms`, count).
198201
pub views_daily: Option<Vec<(u64, u64)>>,
202+
/// Total views in the last month.
203+
pub views_total_last_month: u64,
199204
}
200205

201206
/// Job status for employer dashboard jobs.

gitjobs-server/static/js/dashboard/jobs/list.js

Lines changed: 138 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,161 @@ import { prettifyNumber, toggleModalVisibility } from "/static/js/common/common.
33
import { showErrorAlert, showInfoAlert } from "/static/js/common/alerts.js";
44

55
/**
6-
* Function to render the statistics chart for a job
7-
* @param {string} id - The ID of the job to render stats for
6+
* Shows statistics for a specific job in a modal
7+
* @param {string} id - The ID of the job to display stats for
8+
*/
9+
export const showStats = async (id) => {
10+
// Get loading spinner reference
11+
const spinnerStats = document.getElementById(`spinner-stats-${id}`);
12+
13+
// Fetch job statistics from the API
14+
const response = await fetch(`/dashboard/employer/jobs/${id}/stats`, {
15+
method: "GET",
16+
});
17+
if (!response.ok) {
18+
if (spinnerStats) {
19+
spinnerStats.classList.add("hidden");
20+
}
21+
showErrorAlert("Something went wrong fetching the stats, please try again later.");
22+
return;
23+
}
24+
const data = await response.json();
25+
if (spinnerStats) {
26+
spinnerStats.classList.add("hidden");
27+
}
28+
29+
// Process and display the statistics data
30+
if (data) {
31+
const hasViewsData = data.views_daily && data.views_daily.length > 0;
32+
const hasSearchAppearancesData =
33+
data.search_appearances_daily && data.search_appearances_daily.length > 0;
34+
35+
if (hasViewsData || hasSearchAppearancesData) {
36+
// Open the statistics modal if we have data for at least one chart
37+
toggleModalVisibility(`stats-modal`, "open");
38+
39+
// Render views chart if data exists
40+
if (hasViewsData) {
41+
renderChart(data.views_daily, "job-chart-views", "views");
42+
if (data.views_total_last_month !== undefined) {
43+
const totalViewsElement = document.getElementById("total-views");
44+
if (totalViewsElement) {
45+
totalViewsElement.textContent = prettifyNumber(data.views_total_last_month);
46+
}
47+
}
48+
} else {
49+
// Hide views chart if no data is available
50+
const viewsChartWrapper = document.querySelector('[data-chart="views"]');
51+
if (viewsChartWrapper) {
52+
viewsChartWrapper.classList.add("hidden");
53+
}
54+
}
55+
56+
// Render search appearances chart if data exists
57+
if (hasSearchAppearancesData) {
58+
renderChart(data.search_appearances_daily, "job-chart-search-appearances", "search_appearances");
59+
if (data.search_appearances_total_last_month !== undefined) {
60+
const totalSearchElement = document.getElementById("total-search-appearances");
61+
if (totalSearchElement) {
62+
totalSearchElement.textContent = prettifyNumber(data.search_appearances_total_last_month);
63+
}
64+
}
65+
} else {
66+
// Hide search appearances chart if no data is available
67+
const searchAppearancesChartWrapper = document.querySelector('[data-chart="search-appearances"]');
68+
if (searchAppearancesChartWrapper) {
69+
searchAppearancesChartWrapper.classList.add("hidden");
70+
}
71+
}
72+
} else {
73+
// Show message when no data is available for either chart
74+
showInfoAlert(
75+
'We don\'t have statistics data for this job yet.<div class="mt-2">Please check again later.</div>',
76+
true,
77+
);
78+
}
79+
}
80+
};
81+
82+
/**
83+
* Closes the statistics modal and cleans up resources
84+
*/
85+
export const closeStats = () => {
86+
// Dispose of all chart instances to free up memory
87+
const chartIds = ["job-chart-views", "job-chart-search-appearances"];
88+
chartIds.forEach((id) => {
89+
const chartDom = document.getElementById(id);
90+
if (chartDom) {
91+
const chartInstance = echarts.getInstanceByDom(chartDom);
92+
if (chartInstance) {
93+
chartInstance.dispose();
94+
}
95+
}
96+
});
97+
98+
// Close the modal
99+
toggleModalVisibility(`stats-modal`, "close");
100+
101+
// Clear the statistics counters
102+
const totalViewsElement = document.getElementById(`total-views`);
103+
if (totalViewsElement) {
104+
totalViewsElement.textContent = "";
105+
}
106+
const totalSearchElement = document.getElementById(`total-search-appearances`);
107+
if (totalSearchElement) {
108+
totalSearchElement.textContent = "";
109+
}
110+
111+
// Display charts wrapper
112+
const viewsChartWrapper = document.querySelector('[data-chart="views"]');
113+
if (viewsChartWrapper) {
114+
viewsChartWrapper.classList.remove("hidden");
115+
}
116+
const searchAppearancesChartWrapper = document.querySelector('[data-chart="search-appearances"]');
117+
if (searchAppearancesChartWrapper) {
118+
searchAppearancesChartWrapper.classList.remove("hidden");
119+
}
120+
};
121+
122+
/**
123+
* Function to render a chart
124+
* @param {Array} data - The chart data
125+
* @param {string} chartId - The ID of the chart container
126+
* @param {string} chartType - The type of chart ('views' or 'search_appearances')
8127
* @private
9128
*/
10-
const renderStat = (data) => {
129+
const renderChart = (data, chartId, chartType) => {
130+
// Calculate date range for the chart (last 30 days)
11131
const today = Date.now();
12-
// Set the minimum date to one month ago
13132
const min = new Date();
14133
const month = min.getMonth();
15134
min.setMonth(min.getMonth() - 1);
16-
17-
// If today is the first day of the month, set it to the last day of
18-
// the previous month
135+
// Handle edge case when today is the first day of the month
19136
if (min.getMonth() == month) min.setDate(0);
20-
// Set the time to the start of the day
21137
min.setHours(0, 0, 0, 0);
22138

23-
const chartDom = document.getElementById(`job-stats`);
139+
// Get the chart container element
140+
const chartDom = document.getElementById(chartId);
24141
if (!chartDom) return;
25142

26-
const myChart = echarts.init(chartDom, "gitjobs", {
143+
// Initialize the ECharts instance
144+
const chart = echarts.init(chartDom, "gitjobs", {
27145
renderer: "svg",
28146
useDirtyRect: false,
29147
});
30-
myChart.clear();
148+
chart.clear();
31149

150+
// Add responsive resize handler
32151
window.addEventListener("resize", function () {
33-
myChart.resize();
152+
chart.resize();
34153
});
35154

155+
// Configure chart options
36156
const option = {
37157
...getBarStatsOptions(),
38158
dataset: [
39159
{
40-
dimensions: ["timestamp", "jobs"],
160+
dimensions: ["timestamp", "count"],
41161
source: data,
42162
},
43163
{
@@ -51,7 +171,8 @@ const renderStat = (data) => {
51171
...getBarStatsOptions().tooltip,
52172
formatter: (params) => {
53173
const chartdate = echarts.time.format(params.data[0], "{dd} {MMM}'{yy}");
54-
return `${chartdate}<br />Views: ${prettifyNumber(params.data[1])}`;
174+
const label = chartType === "views" ? "Views" : "Search appearances";
175+
return `${chartdate}<br />${label}: ${prettifyNumber(params.data[1])}`;
55176
},
56177
},
57178
xAxis: {
@@ -65,79 +186,14 @@ const renderStat = (data) => {
65186
},
66187
};
67188

68-
option && myChart.setOption(option);
69-
};
70-
71-
/**
72-
* Fetches and renders statistics for a specific job
73-
* @param {string} id - The ID of the job to fetch stats for
74-
*/
75-
export const fetchStats = async (id) => {
76-
const response = await fetch(`/dashboard/employer/jobs/${id}/stats`, {
77-
method: "GET",
78-
});
79-
const spinnerStats = document.getElementById(`spinner-stats-${id}`);
80-
if (!response.ok) {
81-
// Hide the spinner if it exists
82-
if (spinnerStats) {
83-
spinnerStats.classList.add("hidden");
84-
}
85-
86-
showErrorAlert("Something went wrong fetching the stats, please try again later.");
87-
return;
88-
}
89-
const data = await response.json();
90-
// Hide the spinner if it exists
91-
if (spinnerStats) {
92-
spinnerStats.classList.add("hidden");
93-
}
94-
if (data) {
95-
if (data.views_daily.length > 0) {
96-
// Show the stats modal
97-
toggleModalVisibility(`stats-modal`, "open");
98-
99-
// Render the stats chart
100-
renderStat(data.views_daily);
101-
if (data.views_total_last_month !== undefined) {
102-
const totalViewsElement = document.getElementById("total-views");
103-
if (totalViewsElement) {
104-
totalViewsElement.textContent = prettifyNumber(data.views_total_last_month);
105-
}
106-
}
107-
} else {
108-
showInfoAlert(
109-
'We don\'t have views data for this job yet.<div class="mt-2">Please check again later.</div>',
110-
true,
111-
);
112-
}
113-
}
114-
};
115-
116-
/**
117-
* Closes the stats modal and clears its content
118-
*/
119-
export const closeStatsModal = () => {
120-
const chartDom = document.getElementById("job-stats");
121-
if (chartDom) {
122-
const chartInstance = echarts.getInstanceByDom(chartDom);
123-
// Dispose of the chart instance before closing modal
124-
if (chartInstance) {
125-
chartInstance.dispose();
126-
}
127-
}
128-
// Close the stats modal
129-
toggleModalVisibility(`stats-modal`, "close");
130-
// Clear the total views element
131-
const totalViewsElement = document.getElementById(`total-views`);
132-
if (totalViewsElement) {
133-
totalViewsElement.textContent = "";
134-
}
189+
// Render the chart with the configured options
190+
option && chart.setOption(option);
135191
};
136192

137193
/**
138194
* Registers the GitJobs theme for ECharts
139195
*/
140196
export const registerEchartsTheme = () => {
141-
// Register the GitJobs theme
197+
// Register the custom GitJobs theme for consistent chart styling
142198
echarts.registerTheme("gitjobs", gitjobsChartTheme);
143199
};

gitjobs-server/templates/dashboard/employer/jobs/list.html

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@
154154
</button>
155155
<script type="module">
156156
import {
157-
fetchStats
157+
showStats
158158
} from '/static/js/dashboard/jobs/list.js';
159159

160160
const btnStats = document.getElementById('btn-stats-{{ job.job_id }}');
@@ -164,7 +164,7 @@
164164
if (spinnerStats) {
165165
spinnerStats.classList.remove('hidden');
166166
}
167-
fetchStats("{{job.job_id}}");
167+
showStats("{{job.job_id}}");
168168
});
169169
}
170170
</script>
@@ -331,12 +331,27 @@ <h3 class="text-xl font-semibold text-stone-900">Stats</h3>
331331

332332
{# Modal content -#}
333333
<div class="p-4 md:p-8">
334-
<div class="font-semibold text-stone-700 mb-4">
335-
Views over the last 30 days <span class="font-normal text-xs text-stone-500 uppercase ms-2">(total: <span id="total-views" class="font-bold text-stone-700"></span>)</span>
334+
{# Views chart -#}
335+
<div data-chart="views">
336+
<div class="font-semibold text-stone-700 mb-4">
337+
Views over the last 30 days <span class="font-normal text-xs text-stone-500 uppercase ms-2">(total: <span id="total-views" class="font-bold text-stone-700"></span>)</span>
338+
</div>
339+
340+
<div class="flex items-center justify-center h-[300px] border border-stone-200 mb-8"
341+
id="job-chart-views"></div>
336342
</div>
343+
{# End views chart -#}
344+
345+
{# Search appearances chart -#}
346+
<div data-chart="search-appearances">
347+
<div class="font-semibold text-stone-700 mb-4">
348+
Search appearances over the last 30 days <span class="font-normal text-xs text-stone-500 uppercase ms-2">(total: <span id="total-search-appearances" class="font-bold text-stone-700"></span>)</span>
349+
</div>
337350

338-
<div class="flex items-center justify-center h-[300px] border border-stone-200 mb-2"
339-
id="job-stats"></div>
351+
<div class="flex items-center justify-center h-[300px] border border-stone-200 mb-6"
352+
id="job-chart-search-appearances"></div>
353+
</div>
354+
{# End search appearances chart -#}
340355
</div>
341356
{# End modal content -#}
342357
</div>
@@ -346,21 +361,21 @@ <h3 class="text-xl font-semibold text-stone-900">Stats</h3>
346361
{# End stats modal -#}
347362
<script type="module">
348363
import {
349-
closeStatsModal,
364+
closeStats,
350365
} from '/static/js/dashboard/jobs/list.js';
351366

352367
const closeStatsModalBtn = document.getElementById('close-stats-modal');
353368
const backdropStatsModal = document.getElementById('backdrop-stats-modal');
354369

355370
if (closeStatsModalBtn) {
356371
closeStatsModalBtn.addEventListener('click', () => {
357-
closeStatsModal();
372+
closeStats();
358373
});
359374
}
360375

361376
if (backdropStatsModal) {
362377
backdropStatsModal.addEventListener('click', () => {
363-
closeStatsModal();
378+
closeStats();
364379
});
365380
}
366381
</script>

0 commit comments

Comments
 (0)