439 lines
11 KiB
Svelte
439 lines
11 KiB
Svelte
<script lang="ts">
|
||
import { applicationStore } from '$lib/ApplicationsStore.svelte';
|
||
import HasUser from '$lib/HasUser.svelte';
|
||
import { onMount } from 'svelte';
|
||
import NavBar from '../NavBar.svelte';
|
||
import Pie from './Pie.svelte';
|
||
import { statusStore } from '$lib/Types.svelte';
|
||
import { get } from '$lib/utils';
|
||
import PayRange from './PayRange.svelte';
|
||
import LineGraphs, { type LineGraphData } from './LineGraph.svelte';
|
||
import * as d3 from 'd3';
|
||
import { countReducer } from './utils';
|
||
|
||
onMount(() => {
|
||
applicationStore.loadAll();
|
||
statusStore.load();
|
||
});
|
||
|
||
let flairStats: 'loading' | Record<string, number> = $state('loading');
|
||
|
||
let createGraph: { xs: number[]; ys: number[] } = $state({ xs: [], ys: [] });
|
||
let viewGraph: { xs: number[]; ys: number[] } = $state({ xs: [], ys: [] });
|
||
let statusGraph: LineGraphData = $state([]);
|
||
|
||
let width: number = $state(300);
|
||
|
||
onMount(async () => {
|
||
(async () => {
|
||
const items: any[] = await get('flair/stats');
|
||
flairStats = items.reduce(
|
||
(acc, a) => {
|
||
acc[a.name] = a.count;
|
||
return acc;
|
||
},
|
||
{} as Record<string, number>
|
||
);
|
||
})();
|
||
|
||
(async () => {
|
||
type EventsStat = {
|
||
// application_id
|
||
a: string;
|
||
// created time
|
||
c: number;
|
||
// id
|
||
i: string;
|
||
// new_status_id
|
||
s?: string;
|
||
// Type
|
||
t: number;
|
||
};
|
||
const events: EventsStat[] = await get('events/');
|
||
|
||
const pre_created_graph = events
|
||
.filter((a) => a.t === 0)
|
||
.reduce(countReducer, {} as Record<number | string, number>);
|
||
|
||
const pre_created_graph_xs = Object.keys(pre_created_graph);
|
||
|
||
createGraph = {
|
||
xs: pre_created_graph_xs.map((a) => Number(a)),
|
||
ys: pre_created_graph_xs.map((a) => pre_created_graph[a])
|
||
};
|
||
|
||
const pre_views_graph = events
|
||
.filter((a) => a.t === 2)
|
||
.reduce(countReducer, {} as Record<number | string, number>);
|
||
|
||
const pre_view_graph_xs = Object.keys(pre_views_graph);
|
||
|
||
viewGraph = {
|
||
xs: pre_view_graph_xs.map((a) => Number(a)),
|
||
ys: pre_view_graph_xs.map((a) => pre_views_graph[a])
|
||
};
|
||
|
||
statusGraph = statusStore.nodes
|
||
.map((a, i) => {
|
||
const pre = events
|
||
.filter((e) => e.t === 1 && e.s === a.id)
|
||
.reduce(countReducer, {} as Record<number | string, number>);
|
||
const pre_xs = Object.keys(pre);
|
||
if (pre_xs.length === 0) return undefined;
|
||
return {
|
||
name: a.name,
|
||
color: d3.interpolateRainbow(i / statusStore.nodes.length),
|
||
xs: pre_xs.map((a) => Number(a)),
|
||
ys: pre_xs.map((a) => pre[a])
|
||
};
|
||
})
|
||
.filter((a) => a) as LineGraphData;
|
||
})();
|
||
});
|
||
|
||
const seniorities = ['intern', 'entry', 'junior', 'mid', 'senior', 'staff', 'lead'];
|
||
</script>
|
||
|
||
<HasUser redirect="/cv">
|
||
<div class="flex flex-col h-[100vh]">
|
||
<NavBar />
|
||
<div class="p-1 px-5">
|
||
<div class="bg-white p-3 rounded-lg gap-5 flex flex-col">
|
||
<div class="flex gap-5 flex-wrap">
|
||
<Pie
|
||
title={'Origin'}
|
||
data={applicationStore.all.reduce(
|
||
(acc, item) => {
|
||
if (item.url.includes('linkedin')) {
|
||
acc.linkedin += 1;
|
||
} else if (item.url.includes('glassdoor')) {
|
||
acc.glassdoor += 1;
|
||
} else {
|
||
acc.other += 1;
|
||
}
|
||
return acc;
|
||
},
|
||
{
|
||
linkedin: 0,
|
||
glassdoor: 0,
|
||
other: 0
|
||
}
|
||
)}
|
||
/>
|
||
<Pie
|
||
title={'Status'}
|
||
data={[...statusStore.nodes, 'Created' as const].reduce(
|
||
(acc, item) => {
|
||
if (item === 'Created') {
|
||
const count = applicationStore.all.filter(
|
||
(a) => a.status_id === null
|
||
).length;
|
||
if (count === 0) return acc;
|
||
acc[item] = count;
|
||
return acc;
|
||
}
|
||
const count = applicationStore.all.filter(
|
||
(a) => item.id === a.status_id
|
||
).length;
|
||
if (count === 0) return acc;
|
||
acc[item.name] = count;
|
||
return acc;
|
||
},
|
||
{} as Record<string, number>
|
||
)}
|
||
/>
|
||
<Pie
|
||
title={'Higher range Pay Range'}
|
||
data={applicationStore.all
|
||
.filter((a) => a.payrange.match(/\d/))
|
||
.map((a) => {
|
||
const payrange = a.payrange
|
||
.replace(/[kK]/g, '000')
|
||
.replace(/[^\d\-–]/g, '')
|
||
.replace(/–/g, '-')
|
||
.split('-');
|
||
return Number(payrange[payrange.length - 1]);
|
||
})
|
||
.reduce(
|
||
(acc, a) => {
|
||
const f = Math.floor(a / 10000);
|
||
let name = `${f * 10}K-${(f + 1) * 10}K`;
|
||
if (f == 0) {
|
||
name = '<10K';
|
||
}
|
||
if (acc[name]) {
|
||
acc[name] += 1;
|
||
} else {
|
||
acc[name] = 1;
|
||
}
|
||
return acc;
|
||
},
|
||
{} as Record<string, number>
|
||
)}
|
||
/>
|
||
<Pie
|
||
title={'Lower range Pay Range'}
|
||
data={applicationStore.all
|
||
.filter((a) => a.payrange.match(/\d/))
|
||
.map((a) => {
|
||
const payrange = a.payrange
|
||
.replace(/[kK]/g, '000')
|
||
// The first is a - the other is unicode 8211
|
||
.replace(/[^\d\-–]/g, '')
|
||
.replace(/–/g, '-')
|
||
.split('-');
|
||
return Number(payrange[0]) + Number(payrange[1] ?? payrange[0]);
|
||
})
|
||
.reduce(
|
||
(acc, a) => {
|
||
const f = Math.floor(a / 10000);
|
||
let name = `${f * 10}K-${(f + 1) * 10}K`;
|
||
if (f == 0) {
|
||
name = '<10K';
|
||
}
|
||
if (acc[name]) {
|
||
acc[name] += 1;
|
||
} else {
|
||
acc[name] = 1;
|
||
}
|
||
return acc;
|
||
},
|
||
{} as Record<string, number>
|
||
)}
|
||
/>
|
||
<Pie
|
||
title={'Company'}
|
||
sensitivity={0.01}
|
||
data={applicationStore.all.reduce(
|
||
(acc, item) => {
|
||
if (acc[item.company]) {
|
||
acc[item.company] += 1;
|
||
} else {
|
||
acc[item.company] = 1;
|
||
}
|
||
return acc;
|
||
},
|
||
{} as Record<string, number>
|
||
)}
|
||
/>
|
||
<Pie
|
||
title={'Location'}
|
||
sensitivity={0.01}
|
||
data={applicationStore.all.reduce(
|
||
(acc, item) => {
|
||
const l = item.location === '' ? 'Unknown' : item.location;
|
||
if (acc[l]) {
|
||
acc[l] += 1;
|
||
} else {
|
||
acc[l] = 1;
|
||
}
|
||
return acc;
|
||
},
|
||
{} as Record<string, number>
|
||
)}
|
||
/>
|
||
<Pie
|
||
title={'In Person type'}
|
||
sensitivity={0.01}
|
||
data={applicationStore.all.reduce(
|
||
(acc, item) => {
|
||
const l =
|
||
item.inperson_type === '' ? 'Unknown' : item.inperson_type;
|
||
if (acc[l]) {
|
||
acc[l] += 1;
|
||
} else {
|
||
acc[l] = 1;
|
||
}
|
||
return acc;
|
||
},
|
||
{} as Record<string, number>
|
||
)}
|
||
/>
|
||
<Pie
|
||
title={'Job Level'}
|
||
data={applicationStore.all.reduce(
|
||
(acc, a) => {
|
||
const job_level = a.job_level ? a.job_level : 'Unknown';
|
||
if (acc[job_level]) {
|
||
acc[job_level] += 1;
|
||
} else {
|
||
acc[job_level] = 1;
|
||
}
|
||
return acc;
|
||
},
|
||
{} as Record<string, number>
|
||
)}
|
||
sensitivity={0.02}
|
||
/>
|
||
<Pie
|
||
title={'Agency'}
|
||
data={applicationStore.all.reduce(
|
||
(acc, a) => {
|
||
const i = a.agency ? 'Agency' : 'Direct';
|
||
if (acc[i]) {
|
||
acc[i] += 1;
|
||
} else {
|
||
acc[i] = 1;
|
||
}
|
||
return acc;
|
||
},
|
||
{} as Record<string, number>
|
||
)}
|
||
sensitivity={0.02}
|
||
/>
|
||
</div>
|
||
{#if flairStats !== 'loading'}
|
||
<div class="flex gap-5">
|
||
<Pie title={'Flair stats'} data={flairStats} sensitivity={0.02} />
|
||
<div class="max-h-[500px] overflow-auto">
|
||
<ul>
|
||
{#each Object.keys(flairStats).toSorted((a, b) => flairStats[b] - flairStats[a]) as flair}
|
||
<li>{flair}: {flairStats[flair]}</li>
|
||
{/each}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
<div bind:clientWidth={width}>
|
||
<LineGraphs
|
||
data={[
|
||
{ name: 'Created Time', color: 'red', ...createGraph },
|
||
{ name: 'Views', color: 'blue', ...viewGraph }
|
||
]}
|
||
xIsDates
|
||
width={width - 50}
|
||
height={300}
|
||
/>
|
||
</div>
|
||
<div bind:clientWidth={width}>
|
||
<h1>Status Graph</h1>
|
||
<LineGraphs data={statusGraph} xIsDates width={width - 50} height={300} />
|
||
</div>
|
||
<h1>Payrange</h1>
|
||
<PayRange />
|
||
<div>
|
||
<h1>Per Seniority</h1>
|
||
{#each seniorities as level}
|
||
{@const fapps = applicationStore.all.filter(
|
||
(a) => a.payrange.match(/\d/) && a.job_level === level
|
||
)}
|
||
<h2 class="font-bold text-lg">
|
||
{level} (AVG pay: {(
|
||
fapps
|
||
.map((a) => {
|
||
const payrange = a.payrange
|
||
.replace(/[kK]/g, '000')
|
||
.replace(/[^\d\-–]/g, '')
|
||
.replace(/–/g, '-')
|
||
.split('-');
|
||
return (
|
||
Number(payrange[0]) + Number(payrange[1] ?? payrange[0])
|
||
);
|
||
})
|
||
.reduce((acc, a) => acc + a, 0) /
|
||
(fapps.length * 2)
|
||
).toLocaleString('en-GB', {
|
||
notation: 'compact',
|
||
style: 'currency',
|
||
currency: 'GBP'
|
||
})})
|
||
</h2>
|
||
<div class="flex gap-2">
|
||
<Pie
|
||
title={'Higher range Pay Range'}
|
||
data={fapps
|
||
.map((a) => {
|
||
const payrange = a.payrange
|
||
.replace(/[kK]/g, '000')
|
||
.replace(/[^\d\-–]/g, '')
|
||
.replace(/–/g, '-')
|
||
.split('-');
|
||
return Number(payrange[payrange.length - 1]);
|
||
})
|
||
.reduce(
|
||
(acc, a) => {
|
||
const f = Math.floor(a / 10000);
|
||
let name = `${f * 10}K-${(f + 1) * 10}K`;
|
||
if (f == 0) {
|
||
name = '<10K';
|
||
}
|
||
if (acc[name]) {
|
||
acc[name] += 1;
|
||
} else {
|
||
acc[name] = 1;
|
||
}
|
||
return acc;
|
||
},
|
||
{} as Record<string, number>
|
||
)}
|
||
/>
|
||
<Pie
|
||
title={'Lower range Pay Range'}
|
||
data={fapps
|
||
.map((a) => {
|
||
const payrange = a.payrange
|
||
.replace(/[kK]/g, '000')
|
||
// The first is a - the other is unicode 8211
|
||
.replace(/[^\d\-–]/g, '')
|
||
.replace(/–/g, '-')
|
||
.split('-');
|
||
return Number(payrange[0]);
|
||
})
|
||
.reduce(
|
||
(acc, a) => {
|
||
const f = Math.floor(a / 10000);
|
||
let name = `${f * 10}K-${(f + 1) * 10}K`;
|
||
if (f == 0) {
|
||
name = '<10K';
|
||
}
|
||
if (acc[name]) {
|
||
acc[name] += 1;
|
||
} else {
|
||
acc[name] = 1;
|
||
}
|
||
return acc;
|
||
},
|
||
{} as Record<string, number>
|
||
)}
|
||
/>
|
||
<Pie
|
||
title={'AVG Pay'}
|
||
data={fapps
|
||
.map((a) => {
|
||
const payrange = a.payrange
|
||
.replace(/[kK]/g, '000')
|
||
// The first is a - the other is unicode 8211
|
||
.replace(/[^\d\-–]/g, '')
|
||
.replace(/–/g, '-')
|
||
.split('-');
|
||
return (
|
||
(Number(payrange[0]) +
|
||
Number(payrange[1] ?? payrange[0])) /
|
||
2
|
||
);
|
||
})
|
||
.reduce(
|
||
(acc, a) => {
|
||
const f = Math.floor(a / 10000);
|
||
let name = `${f * 10}K-${(f + 1) * 10}K`;
|
||
if (f == 0) {
|
||
name = '<10K';
|
||
}
|
||
if (acc[name]) {
|
||
acc[name] += 1;
|
||
} else {
|
||
acc[name] = 1;
|
||
}
|
||
return acc;
|
||
},
|
||
{} as Record<string, number>
|
||
)}
|
||
/>
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</HasUser>
|