chore: things done a very nice and usable git commit
This commit is contained in:
parent
55434c590a
commit
c83b2fd541
@ -71,7 +71,7 @@ data class Application(
|
||||
|
||||
data class SubmitRequest(val text: String)
|
||||
|
||||
data class ListRequest(val status: Int?)
|
||||
data class ListRequest(val status: Int? = null, val views: Boolean? = null)
|
||||
|
||||
data class StatusRequest(val id: String, val status: Int)
|
||||
|
||||
@ -357,7 +357,11 @@ class ApplicationsController(
|
||||
}
|
||||
|
||||
if (application.unique_url != null) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Application already has unique_url", null)
|
||||
throw ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Application already has unique_url",
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
application.original_url = application.url
|
||||
@ -554,14 +558,13 @@ class ApplicationService(
|
||||
return true
|
||||
}
|
||||
|
||||
public fun findAll(user: UserDb, info: ListRequest): List<Application> {
|
||||
private fun internalFindAll(user: UserDb, info: ListRequest): Iterable<Application> {
|
||||
if (info.status == null) {
|
||||
return db.query(
|
||||
"select * from applications where user_id=? order by title asc;",
|
||||
arrayOf(user.id),
|
||||
Application
|
||||
)
|
||||
.toList()
|
||||
"select * from applications where user_id=? order by title asc;",
|
||||
arrayOf(user.id),
|
||||
Application
|
||||
)
|
||||
}
|
||||
|
||||
// If it's to apply also remove the linked_application to only show the main
|
||||
@ -571,7 +574,6 @@ class ApplicationService(
|
||||
arrayOf(user.id),
|
||||
Application
|
||||
)
|
||||
.toList()
|
||||
}
|
||||
|
||||
return db.query(
|
||||
@ -579,7 +581,17 @@ class ApplicationService(
|
||||
arrayOf(user.id, info.status),
|
||||
Application,
|
||||
)
|
||||
.toList()
|
||||
}
|
||||
|
||||
public fun findAll(user: UserDb, info: ListRequest): List<Application> {
|
||||
var iter = internalFindAll(user, info);
|
||||
if (info.views == true) {
|
||||
iter = iter.map {
|
||||
it.views = viewService.listFromApplicationId(it.id)
|
||||
it
|
||||
}
|
||||
}
|
||||
return iter.toList()
|
||||
}
|
||||
|
||||
public fun update(application: Application): Application {
|
||||
|
@ -1,24 +1,56 @@
|
||||
package com.andr3h3nriqu3s.applications
|
||||
|
||||
import java.sql.ResultSet
|
||||
import java.sql.Timestamp
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.jdbc.core.JdbcTemplate
|
||||
import org.springframework.jdbc.core.RowMapper
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestHeader
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
data class View(var id: String, var application_id: String, var time: Date) {
|
||||
data class View(var id: String, var application_id: String, var time: Timestamp) {
|
||||
companion object : RowMapper<View> {
|
||||
override public fun mapRow(rs: ResultSet, rowNum: Int): View {
|
||||
return View(
|
||||
rs.getString("id"),
|
||||
rs.getString("application_id"),
|
||||
rs.getDate("time"),
|
||||
rs.getTimestamp("time"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RestController
|
||||
@ControllerAdvice
|
||||
@RequestMapping("/api/view")
|
||||
class ViewController(
|
||||
val sessionService: SessionService,
|
||||
val applicationService: ApplicationService,
|
||||
val flairService: FlairService,
|
||||
val viewService: ViewService,
|
||||
) {
|
||||
|
||||
@GetMapping(path = ["/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||
public fun getCV(@PathVariable id: String, @RequestHeader("token") token: String): List<View> {
|
||||
val user = sessionService.verifyTokenThrow(token)
|
||||
|
||||
val application = applicationService.findApplicationById(user, id)
|
||||
|
||||
if (application == null) {
|
||||
throw NotFound()
|
||||
}
|
||||
|
||||
return application.views
|
||||
}
|
||||
}
|
||||
|
||||
@Service
|
||||
public class ViewService(val db: JdbcTemplate) {
|
||||
|
||||
@ -58,7 +90,7 @@ public class ViewService(val db: JdbcTemplate) {
|
||||
public fun create(application_id: String): View {
|
||||
val id = UUID.randomUUID().toString()
|
||||
|
||||
var new_view = View(id, application_id, Date())
|
||||
var new_view = View(id, application_id, Timestamp(Date().getTime()))
|
||||
|
||||
db.update(
|
||||
"insert into views (id, application_id) values (?, ?)",
|
||||
|
@ -45,7 +45,7 @@ @layer components {
|
||||
}
|
||||
|
||||
.finput {
|
||||
@apply rounded-lg w-full p-2 drop-shadow-lg border-gray-300 border;
|
||||
@apply rounded-lg w-full p-2 drop-shadow-lg border-gray-300 border mb-1;
|
||||
}
|
||||
|
||||
.finput[type='color'] {
|
||||
@ -74,17 +74,16 @@ @layer components {
|
||||
}
|
||||
|
||||
@print {
|
||||
@page :footer {
|
||||
display: none
|
||||
}
|
||||
@page :footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@page :header {
|
||||
display: none
|
||||
}
|
||||
@page :header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@page {
|
||||
size: auto;
|
||||
margin: 0;
|
||||
size: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
@ -10,8 +10,9 @@ export const ApplicationStatus = Object.freeze({
|
||||
ApplyedButSaidNo: 3,
|
||||
Applyed: 4,
|
||||
Expired: 5,
|
||||
TasksToDo: 6,
|
||||
LinkedApplication: 7
|
||||
TasksToDo: 6,
|
||||
LinkedApplication: 7,
|
||||
InterviewStep1: 8
|
||||
});
|
||||
|
||||
export const ApplicationStatusMaping: Record<
|
||||
@ -24,15 +25,16 @@ export const ApplicationStatusMaping: Record<
|
||||
3: 'Applyed But Said No',
|
||||
4: 'Applyed',
|
||||
5: 'Expired',
|
||||
6: 'Tasks To Do',
|
||||
7: 'Linked Application',
|
||||
6: 'Tasks To Do',
|
||||
7: 'Linked Application',
|
||||
8: 'Interview 1'
|
||||
});
|
||||
|
||||
export type View = {
|
||||
id: string,
|
||||
application_id: string,
|
||||
time: string,
|
||||
}
|
||||
id: string;
|
||||
application_id: string;
|
||||
time: string;
|
||||
};
|
||||
|
||||
export type Application = {
|
||||
id: string;
|
||||
@ -44,25 +46,25 @@ export type Application = {
|
||||
extra_data: string;
|
||||
payrange: string;
|
||||
status: AsEnum<typeof ApplicationStatus>;
|
||||
recruiter: string;
|
||||
company: string;
|
||||
message: string;
|
||||
linked_application: string;
|
||||
application_time: string;
|
||||
create_time: string;
|
||||
status_history: string;
|
||||
recruiter: string;
|
||||
company: string;
|
||||
message: string;
|
||||
linked_application: string;
|
||||
application_time: string;
|
||||
create_time: string;
|
||||
status_history: string;
|
||||
flairs: Flair[];
|
||||
views: View[];
|
||||
views: View[];
|
||||
};
|
||||
|
||||
function createApplicationStore() {
|
||||
let applications: Application[] = $state([]);
|
||||
let applyed: Application[] = $state([]);
|
||||
let tasksToDo: Application[] = $state([]);
|
||||
let all: Application[] = $state([]);
|
||||
|
||||
let dragApplication: Application | undefined = $state(undefined);
|
||||
|
||||
let loadItem: Application | undefined = $state(undefined);
|
||||
let loadItem: Application | undefined = $state(undefined);
|
||||
|
||||
return {
|
||||
/**
|
||||
@ -82,24 +84,27 @@ function createApplicationStore() {
|
||||
if (!force && applyed.length > 1) {
|
||||
return;
|
||||
}
|
||||
applyed = await post('application/list', { status: ApplicationStatus.Applyed });
|
||||
applyed = await post('application/list', { status: ApplicationStatus.Applyed, views: true });
|
||||
},
|
||||
|
||||
/**
|
||||
* @throws {Error}
|
||||
*/
|
||||
async loadTasksToDo(force = false) {
|
||||
if (!force && tasksToDo.length > 1) {
|
||||
async loadAll(force = false) {
|
||||
if (!force && all.length > 1) {
|
||||
return;
|
||||
}
|
||||
tasksToDo = await post('application/list', { status: ApplicationStatus.TasksToDo });
|
||||
all = await post('application/list', {});
|
||||
},
|
||||
|
||||
clear() {
|
||||
applications = [];
|
||||
},
|
||||
|
||||
dragStart(application: Application) {
|
||||
dragStart(application: Application | undefined) {
|
||||
if (!application) {
|
||||
return;
|
||||
}
|
||||
dragApplication = application;
|
||||
},
|
||||
|
||||
@ -119,17 +124,17 @@ function createApplicationStore() {
|
||||
return applyed;
|
||||
},
|
||||
|
||||
get tasksToDo() {
|
||||
return tasksToDo;
|
||||
},
|
||||
get all() {
|
||||
return all;
|
||||
},
|
||||
|
||||
get loadItem() {
|
||||
return loadItem;
|
||||
},
|
||||
get loadItem() {
|
||||
return loadItem;
|
||||
},
|
||||
|
||||
set loadItem(item: Application | undefined) {
|
||||
loadItem = item;
|
||||
},
|
||||
set loadItem(item: Application | undefined) {
|
||||
loadItem = item;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,8 @@
|
||||
import ApplicationsList from './ApplicationsList.svelte';
|
||||
import WorkArea from './work-area/WorkArea.svelte';
|
||||
import AppliyedList from './AppliyedList.svelte';
|
||||
import TasksToDoList from './TasksToDoList.svelte';
|
||||
import PApplicationList from './PApplicationList.svelte';
|
||||
import { ApplicationStatus } from '$lib/ApplicationsStore.svelte';
|
||||
</script>
|
||||
|
||||
<HasUser redirect="/cv">
|
||||
@ -15,7 +16,12 @@
|
||||
<ApplicationsList />
|
||||
<WorkArea />
|
||||
</div>
|
||||
<TasksToDoList />
|
||||
<PApplicationList status={ApplicationStatus.InterviewStep1}>
|
||||
Interview I
|
||||
</PApplicationList >
|
||||
<PApplicationList status={ApplicationStatus.TasksToDo}>
|
||||
Tasks To do
|
||||
</PApplicationList >
|
||||
<AppliyedList />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -33,7 +33,7 @@
|
||||
return x.match(f);
|
||||
}) as item}
|
||||
<div
|
||||
class="card p-2 my-2 bg-slate-100"
|
||||
class="card p-2 my-2 bg-slate-100 max-w-full"
|
||||
draggable="true"
|
||||
ondragstart={() => applicationStore.dragStart(item)}
|
||||
ondragend={() => {
|
||||
@ -43,21 +43,26 @@
|
||||
}}
|
||||
role="none"
|
||||
>
|
||||
<div class:animate-pulse={applicationStore.dragging?.id === item.id}>
|
||||
<h2 class="text-lg text-blue-500">
|
||||
{item.title}
|
||||
{#if item.company}
|
||||
<div class="text-violet-800" >
|
||||
@ {item.company}
|
||||
<div class="max-w-full" class:animate-pulse={applicationStore.dragging?.id === item.id}>
|
||||
<h2 class="text-lg text-blue-500 flex gap-2 max-w-full overflow-hidden">
|
||||
<div class="flex-grow max-w-[90%]">
|
||||
<div class="whitespace-nowrap overflow-hidden">
|
||||
{item.title}
|
||||
</div>
|
||||
{/if}
|
||||
{#if item.company}
|
||||
<div class="text-violet-800">
|
||||
@ {item.company}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
{#if item.url.includes('linkedin')}
|
||||
<span class="bi bi-linkedin"></span>
|
||||
{:else if item.url.includes('glassdoor')}
|
||||
<span class="bi bi-eyeglasses"></span>
|
||||
{/if}
|
||||
</div>
|
||||
</h2>
|
||||
<a
|
||||
href={item.url}
|
||||
class="text-violet-600 overflow-hidden whitespace-nowrap block"
|
||||
>
|
||||
{item.url}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
@ -5,14 +5,14 @@
|
||||
|
||||
<div class="p-7">
|
||||
<div class="card p-2 rounded-xl flex">
|
||||
<button class="text-secudanry hover:text-primary px-2" onclick={() => goto('/')}> Home </button>
|
||||
<button class="text-secudanry hover:text-primary px-2" onclick={() => goto('/submit')}>
|
||||
<button class="text-secudanry hover:text-violet-500 px-2" onclick={() => goto('/')}> Home </button>
|
||||
<button class="text-secudanry hover:text-violet-500 px-2" onclick={() => goto('/submit')}>
|
||||
Submit text
|
||||
</button>
|
||||
<button class="text-secudanry hover:text-primary px-2" onclick={() => goto('/flair')}>
|
||||
<button class="text-secudanry hover:text-violet-500 px-2" onclick={() => goto('/flair')}>
|
||||
Flair
|
||||
</button>
|
||||
<button class="text-secudanry hover:text-primary px-2" onclick={() => goto('/graphs')}>
|
||||
<button class="text-secudanry hover:text-violet-500 px-2" onclick={() => goto('/graphs')}>
|
||||
Graphs
|
||||
</button>
|
||||
<div class="flex-grow"></div>
|
||||
|
@ -1,20 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { applicationStore } from '$lib/ApplicationsStore.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
ApplicationStatus,
|
||||
applicationStore,
|
||||
type AsEnum
|
||||
} from '$lib/ApplicationsStore.svelte';
|
||||
import { get } from '$lib/utils';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
|
||||
let { children, status }: { children: Snippet; status: AsEnum<typeof ApplicationStatus> } =
|
||||
$props();
|
||||
|
||||
let applications = $derived(applicationStore.all.filter((item) => item.status == status))
|
||||
|
||||
onMount(() => {
|
||||
applicationStore.loadTasksToDo();
|
||||
applicationStore.loadAll();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if applicationStore.tasksToDo.length > 0}
|
||||
{#if applications.length > 0}
|
||||
<div class="card p-3 rounded-lg flex flex-col">
|
||||
<h1>Tasks To Do</h1>
|
||||
<h1>{@render children()}</h1>
|
||||
<div class="overflow-auto flex-grow">
|
||||
{#each applicationStore.tasksToDo as item}
|
||||
{#each applications as item}
|
||||
<button
|
||||
class="card p-2 my-2 bg-slate-100 w-full text-left"
|
||||
onclick={() => {
|
||||
onclick={async () => {
|
||||
item.views = await get(`view/${item.id}`);
|
||||
|
||||
applicationStore.loadItem = item;
|
||||
window.scrollTo({
|
||||
top: 0,
|
@ -154,7 +154,7 @@
|
||||
class="flex-grow text-blue-500 print:hidden"
|
||||
bind:value={otherSearch}
|
||||
/>
|
||||
<span class="hidden print:inline text-slate-600 text-sm"><a href="https://www.andr3h3nriqu3s.com/cv?id={id}">Looking for other skills?</a></span>
|
||||
<span class="hidden print:inline text-slate-600 text-sm"><a href="https://www.andr3h3nriqu3s.com/cv?id={id}">Search other skills!</a></span>
|
||||
{/if}
|
||||
</h1>
|
||||
<div class="flex flex-wrap gap-2 py-2">
|
||||
|
@ -9,242 +9,289 @@
|
||||
|
||||
let applications: Application[] = $state([]);
|
||||
|
||||
let chartDiv: HTMLDivElement;
|
||||
let chartDiv: HTMLDivElement | undefined = $state();
|
||||
|
||||
let showExpired = $state(false);
|
||||
let showIgnore = $state(false);
|
||||
let showLinked = $state(false);
|
||||
let showToApply = $state(false);
|
||||
|
||||
// Handle the graph creation
|
||||
$effect(() => {
|
||||
if (!chartDiv || applications.length == 0) return;
|
||||
|
||||
chartDiv.innerHTML = '';
|
||||
|
||||
type NodeType =
|
||||
| AsEnum<typeof ApplicationStatus>
|
||||
| 'Linkedin'
|
||||
| 'Glassdoor'
|
||||
| 'Direct Source';
|
||||
|
||||
let graph = {} as Record<string, Record<string, number>>;
|
||||
|
||||
function addGraph(inSource: NodeType, inTarget: NodeType) {
|
||||
const source = `${inSource}`;
|
||||
const target = `${inTarget}`;
|
||||
if (graph[source] == undefined) {
|
||||
graph[source] = {} as Record<NodeType, number>;
|
||||
}
|
||||
graph[source][target] = (graph[source][target] ?? 0) + 1;
|
||||
return target as NodeType;
|
||||
}
|
||||
|
||||
let sourceData: Record<string, number> = {
|
||||
'Direct Source': 0,
|
||||
Glassdoor: 0,
|
||||
Linkedin: 0
|
||||
};
|
||||
|
||||
applications.forEach((a) => {
|
||||
let source: NodeType;
|
||||
|
||||
if (!showExpired && a.status == ApplicationStatus.Expired) {
|
||||
return;
|
||||
}
|
||||
if (!showIgnore && a.status == ApplicationStatus.Ignore) {
|
||||
return;
|
||||
}
|
||||
if (!showLinked && a.status == ApplicationStatus.LinkedApplication) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!showToApply &&
|
||||
(a.status == ApplicationStatus.ToApply || a.status == ApplicationStatus.WorkingOnIt)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (a.url.includes('linkedin')) {
|
||||
source = 'Linkedin';
|
||||
sourceData['Linkedin'] += 1;
|
||||
} else if (a.url.includes('glassdoor')) {
|
||||
source = 'Glassdoor';
|
||||
sourceData['Glassdoor'] += 1;
|
||||
} else {
|
||||
source = 'Direct Source';
|
||||
sourceData['Direct Source'] += 1;
|
||||
}
|
||||
|
||||
if (
|
||||
(
|
||||
[
|
||||
ApplicationStatus.Ignore,
|
||||
ApplicationStatus.Expired,
|
||||
ApplicationStatus.LinkedApplication,
|
||||
ApplicationStatus.ToApply,
|
||||
ApplicationStatus.Applyed
|
||||
] as AsEnum<typeof ApplicationStatus>[]
|
||||
).includes(a.status)
|
||||
) {
|
||||
addGraph(source, a.status);
|
||||
return;
|
||||
}
|
||||
|
||||
// Edge case for working on it
|
||||
if (a.status === ApplicationStatus.WorkingOnIt) {
|
||||
addGraph(source, ApplicationStatus.ToApply);
|
||||
return;
|
||||
}
|
||||
|
||||
source = addGraph(source, ApplicationStatus.Applyed);
|
||||
|
||||
const history = a.status_history.split(',');
|
||||
if (history.includes(`${ApplicationStatus.TasksToDo}`)) {
|
||||
source = addGraph(source, ApplicationStatus.TasksToDo);
|
||||
if (a.status == ApplicationStatus.TasksToDo) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (history.includes(`${ApplicationStatus.InterviewStep1}`)) {
|
||||
source = addGraph(source, ApplicationStatus.InterviewStep1);
|
||||
if (a.status == ApplicationStatus.InterviewStep1) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
addGraph(source, ApplicationStatus.ApplyedButSaidNo);
|
||||
});
|
||||
|
||||
let inNodes: string[] = Object.keys(graph).reduce((acc, elm) => {
|
||||
const arr = Object.keys(graph[elm]).concat(elm);
|
||||
for (const i of arr) {
|
||||
if (!acc.includes(i)) {
|
||||
acc.push(i);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
|
||||
function getGraphValueFor(node: string): number {
|
||||
return Object.keys(graph).reduce((acc, i) => {
|
||||
if (i == node) return acc;
|
||||
if (graph[i][node] != undefined) {
|
||||
return acc + graph[i][node];
|
||||
}
|
||||
return acc;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
let nodes = inNodes.map((node, i) => {
|
||||
let name = '';
|
||||
if (Number.isNaN(Number(node))) {
|
||||
name = node;
|
||||
} else {
|
||||
name = ApplicationStatusMaping[Number(node) as AsEnum<typeof ApplicationStatus>];
|
||||
}
|
||||
const value = sourceData[node] ?? getGraphValueFor(node);
|
||||
const base = {
|
||||
value: value,
|
||||
originalValue: node,
|
||||
id: name,
|
||||
index: i,
|
||||
percentage: Math.trunc((value / applications.length) * 100)
|
||||
};
|
||||
return base;
|
||||
});
|
||||
|
||||
type Link = {
|
||||
source: number;
|
||||
target: number;
|
||||
value: number;
|
||||
};
|
||||
|
||||
const links = Object.keys(graph).reduce((acc, source) => {
|
||||
return acc.concat(
|
||||
Object.keys(graph[source]).map((target) => {
|
||||
const ns = inNodes.indexOf(`${source}`);
|
||||
const nt = inNodes.indexOf(`${target}`);
|
||||
return {
|
||||
source: ns,
|
||||
target: nt,
|
||||
value: graph[source][target]
|
||||
};
|
||||
})
|
||||
);
|
||||
}, [] as Link[]);
|
||||
|
||||
const bounding = chartDiv.getBoundingClientRect();
|
||||
|
||||
let sankey = mySankey()
|
||||
.nodeWidth(20)
|
||||
.nodePadding(10)
|
||||
.size([bounding.width, bounding.height - 20]);
|
||||
|
||||
let path = sankey.link();
|
||||
|
||||
sankey.nodes(nodes).links(links).layout(32);
|
||||
|
||||
const svg = d3
|
||||
.select(chartDiv)
|
||||
.append('svg')
|
||||
.attr('width', bounding.width)
|
||||
.attr('height', bounding.height)
|
||||
.attr('viewBox', [0, 0, bounding.width, bounding.height])
|
||||
.attr('style', 'max-width: 100%; height: auto; height: intrinsic;');
|
||||
|
||||
// let color = d3.schemeSpectral[nodes.length];
|
||||
// let color = d3.interpolateTurbo(nodes.length);
|
||||
|
||||
function getColor(index: number) {
|
||||
return d3.interpolateRainbow(index/nodes.length);
|
||||
}
|
||||
|
||||
// add in the links
|
||||
var link = svg
|
||||
.append('g')
|
||||
.selectAll('.link')
|
||||
.data(
|
||||
links as ((typeof links)[0] & {
|
||||
dy: number;
|
||||
source: (typeof nodes)[0];
|
||||
target: (typeof nodes)[0];
|
||||
})[]
|
||||
)
|
||||
.enter()
|
||||
.append('path')
|
||||
.attr('class', 'link')
|
||||
.attr('d', path)
|
||||
.style('stroke', function (d) {
|
||||
return d3.rgb(
|
||||
getColor(d.source.index)
|
||||
// color[d.source.index]
|
||||
).toString();
|
||||
})
|
||||
.style('stroke-width', function (d) {
|
||||
return Math.max(1, d.dy);
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
return b.dy - a.dy;
|
||||
});
|
||||
|
||||
// add the link titles
|
||||
link.append('title').text(function (d) {
|
||||
return d.source.id + ' → ' + d.target.id + '\n' + d.value;
|
||||
});
|
||||
|
||||
const node = svg
|
||||
.append('g')
|
||||
.selectAll('.node')
|
||||
.data(
|
||||
nodes as ((typeof nodes)[0] & {
|
||||
x: number;
|
||||
y: number;
|
||||
dy: number;
|
||||
dx: number;
|
||||
index: number;
|
||||
})[]
|
||||
)
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('class', 'node')
|
||||
.attr('transform', function (d) {
|
||||
return 'translate(' + d.x + ',' + d.y + ')';
|
||||
});
|
||||
|
||||
node.append('rect')
|
||||
.attr('height', function (d) {
|
||||
return d.dy;
|
||||
})
|
||||
.attr('width', sankey.getNodeWidth())
|
||||
.style('fill', function (d) {
|
||||
return getColor(d.index);
|
||||
//color[d.index];
|
||||
})
|
||||
.style('stroke', (d) => {
|
||||
return d3.rgb(
|
||||
getColor(d.index)
|
||||
//color[d.index]
|
||||
).darker(2).toString();
|
||||
})
|
||||
.append('title')
|
||||
.text(function (d) {
|
||||
return d.id + '\n' + d.value;
|
||||
});
|
||||
|
||||
node.append('text')
|
||||
.attr('x', -6)
|
||||
.attr('y', function (d) {
|
||||
return d.dy / 2;
|
||||
})
|
||||
.attr('dy', '.35em')
|
||||
.attr('text-anchor', 'end')
|
||||
.attr('transform', null)
|
||||
.text(function (d) {
|
||||
return `${d.id} (${d.value} ${d.percentage}%)`;
|
||||
})
|
||||
.filter(function (d) {
|
||||
return d.x < bounding.width / 2;
|
||||
})
|
||||
.attr('x', 6 + sankey.getNodeWidth())
|
||||
.attr('text-anchor', 'start');
|
||||
});
|
||||
|
||||
async function getData() {
|
||||
try {
|
||||
applications = await post('application/list', {});
|
||||
|
||||
chartDiv.innerHTML = '';
|
||||
|
||||
type NodeType =
|
||||
| AsEnum<typeof ApplicationStatus>
|
||||
| 'Linkedin'
|
||||
| 'Glassdoor'
|
||||
| 'Direct Source';
|
||||
|
||||
let graph = {} as Record<string, Record<string, number>>;
|
||||
|
||||
function addGraph(inSource: NodeType, inTarget: NodeType) {
|
||||
const source = `${inSource}`;
|
||||
const target = `${inTarget}`;
|
||||
if (graph[source] == undefined) {
|
||||
graph[source] = {} as Record<NodeType, number>;
|
||||
}
|
||||
graph[source][target] = (graph[source][target] ?? 0) + 1;
|
||||
return target as NodeType;
|
||||
}
|
||||
|
||||
applications.forEach((a) => {
|
||||
let source: NodeType;
|
||||
|
||||
if (a.url.includes('linkedin')) {
|
||||
source = 'Linkedin';
|
||||
} else if (a.url.includes('glassdoor')) {
|
||||
source = 'Glassdoor';
|
||||
} else {
|
||||
source = 'Direct Source';
|
||||
}
|
||||
|
||||
if (
|
||||
(
|
||||
[
|
||||
ApplicationStatus.Ignore,
|
||||
ApplicationStatus.Expired,
|
||||
ApplicationStatus.LinkedApplication,
|
||||
ApplicationStatus.ToApply,
|
||||
ApplicationStatus.Applyed
|
||||
] as AsEnum<typeof ApplicationStatus>[]
|
||||
).includes(a.status)
|
||||
) {
|
||||
addGraph(source, a.status);
|
||||
return;
|
||||
}
|
||||
|
||||
// Edge case for working on it
|
||||
if (a.status === ApplicationStatus.WorkingOnIt) {
|
||||
addGraph(source, ApplicationStatus.ToApply);
|
||||
return;
|
||||
}
|
||||
|
||||
if (a.status == ApplicationStatus.ApplyedButSaidNo) {
|
||||
const history = a.status_history.split(',');
|
||||
source = addGraph(source, ApplicationStatus.Applyed);
|
||||
if (history.includes(`${ApplicationStatus.TasksToDo}`)) {
|
||||
source = addGraph(source, ApplicationStatus.TasksToDo);
|
||||
}
|
||||
addGraph(source, ApplicationStatus.ApplyedButSaidNo);
|
||||
} else if (
|
||||
([ApplicationStatus.TasksToDo] as AsEnum<typeof ApplicationStatus>[]).includes(
|
||||
a.status
|
||||
)
|
||||
) {
|
||||
addGraph(source, ApplicationStatus.Applyed);
|
||||
addGraph(ApplicationStatus.Applyed, a.status);
|
||||
}
|
||||
});
|
||||
|
||||
let inNodes: string[] = Object.keys(graph).reduce((acc, elm) => {
|
||||
const arr = Object.keys(graph[elm]).concat(elm);
|
||||
for (const i of arr) {
|
||||
if (!acc.includes(i)) {
|
||||
acc.push(i);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
|
||||
function getGraphValueFor(node: string): number {
|
||||
return Object.keys(graph).reduce((acc, i) => {
|
||||
if (i == node) return acc;
|
||||
if (graph[i][node] != undefined) {
|
||||
return acc + graph[i][node];
|
||||
}
|
||||
return acc;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
let nodes = inNodes.map((node, i) => {
|
||||
let name = '';
|
||||
if (Number.isNaN(Number(node))) {
|
||||
name = node;
|
||||
} else {
|
||||
name = ApplicationStatusMaping[Number(node) as AsEnum<typeof ApplicationStatus>];
|
||||
}
|
||||
const value = getGraphValueFor(node);
|
||||
const base = {
|
||||
value: value,
|
||||
originalValue: node,
|
||||
id: name,
|
||||
index: i,
|
||||
percentage: Math.trunc((value / applications.length) * 100),
|
||||
end: true
|
||||
};
|
||||
return base;
|
||||
});
|
||||
|
||||
type Link = {
|
||||
source: number;
|
||||
target: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
const links = Object.keys(graph).reduce((acc, source) => {
|
||||
return acc.concat(Object.keys(graph[source]).map((target) => {
|
||||
const ns = inNodes.indexOf(`${source}`);
|
||||
const nt = inNodes.indexOf(`${target}`);
|
||||
return {
|
||||
source: ns,
|
||||
target: nt,
|
||||
value: graph[source][target]
|
||||
};
|
||||
}));
|
||||
}, [] as Link[])
|
||||
|
||||
const bounding = chartDiv.getBoundingClientRect();
|
||||
|
||||
let sankey = mySankey()
|
||||
.nodeWidth(20)
|
||||
.nodePadding(10)
|
||||
.size([bounding.width, bounding.height - 20]);
|
||||
|
||||
let path = sankey.link();
|
||||
|
||||
sankey.nodes(nodes).links(links).layout(32);
|
||||
|
||||
console.log("here2", nodes, links);
|
||||
|
||||
const svg = d3
|
||||
.select(chartDiv)
|
||||
.append('svg')
|
||||
.attr('width', bounding.width)
|
||||
.attr('height', bounding.height)
|
||||
.attr('viewBox', [0, 0, bounding.width, bounding.height])
|
||||
.attr('style', 'max-width: 100%; height: auto; height: intrinsic;');
|
||||
|
||||
let color = d3.schemeSpectral[nodes.length];
|
||||
|
||||
// add in the links
|
||||
var link = svg
|
||||
.append('g')
|
||||
.selectAll('.link')
|
||||
.data(
|
||||
links as ((typeof links)[0] & {
|
||||
dy: number;
|
||||
source: (typeof nodes)[0];
|
||||
target: (typeof nodes)[0];
|
||||
})[]
|
||||
)
|
||||
.enter()
|
||||
.append('path')
|
||||
.attr('class', 'link')
|
||||
.attr('d', path)
|
||||
.style('stroke', function(d) {
|
||||
return d3.rgb(color[d.source.index]).toString();
|
||||
})
|
||||
.style('stroke-width', function (d) {
|
||||
return Math.max(1, d.dy);
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
return b.dy - a.dy;
|
||||
});
|
||||
|
||||
// add the link titles
|
||||
link.append('title').text(function (d) {
|
||||
return d.source.id + ' → ' + d.target.id + '\n' + d.value;
|
||||
});
|
||||
|
||||
const node = svg
|
||||
.append('g')
|
||||
.selectAll('.node')
|
||||
.data(
|
||||
nodes as ((typeof nodes)[0] & {
|
||||
x: number;
|
||||
y: number;
|
||||
dy: number;
|
||||
dx: number;
|
||||
index: number;
|
||||
})[]
|
||||
)
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('class', 'node')
|
||||
.attr('transform', function (d) {
|
||||
return 'translate(' + d.x + ',' + d.y + ')';
|
||||
});
|
||||
|
||||
node.append('rect')
|
||||
.attr('height', function (d) {
|
||||
return d.dy;
|
||||
})
|
||||
.attr('width', sankey.getNodeWidth())
|
||||
.style('fill', function (d) {
|
||||
return color[d.index];
|
||||
})
|
||||
.style('stroke', (d) => {
|
||||
return d3.rgb(color[d.index]).darker(2).toString();
|
||||
})
|
||||
.append('title')
|
||||
.text(function (d) {
|
||||
return d.id + '\n' + d.value;
|
||||
});
|
||||
|
||||
node.append('text')
|
||||
.attr('x', -6)
|
||||
.attr('y', function (d) {
|
||||
return d.dy / 2;
|
||||
})
|
||||
.attr('dy', '.35em')
|
||||
.attr('text-anchor', 'end')
|
||||
.attr('transform', null)
|
||||
.text(function (d) {
|
||||
return `${d.id} (${d.value} ${d.end ? `${d.percentage}%` : ''})`;
|
||||
})
|
||||
.filter(function (d) {
|
||||
return d.x < bounding.width / 2;
|
||||
})
|
||||
.attr('x', 6 + sankey.getNodeWidth())
|
||||
.attr('text-anchor', 'start');
|
||||
} catch (e) {
|
||||
console.log('TODO, inform the user', e);
|
||||
}
|
||||
@ -258,7 +305,27 @@
|
||||
<HasUser redirect="/cv">
|
||||
<div class="flex flex-col h-[100vh]">
|
||||
<NavBar />
|
||||
<div class="flex flex-grow p-5">
|
||||
<div class="p-1 px-5">
|
||||
<div class="bg-white p-3 rounded-lg gap-5 flex">
|
||||
<fieldset>
|
||||
<label for="showIgnore">Show Ignore</label>
|
||||
<input id="showIgnore" type="checkbox" bind:checked={showIgnore} />
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="showExpired">Show Expired</label>
|
||||
<input id="showExpired" type="checkbox" bind:checked={showExpired} />
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="showLinked">Show Linked Applicaitons</label>
|
||||
<input id="showLinked" type="checkbox" bind:checked={showLinked} />
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="showToApply">Show To Apply</label>
|
||||
<input id="showToApply" type="checkbox" bind:checked={showToApply} />
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-grow flex-col p-5">
|
||||
<div class="bg-white p-3 rounded-lg" style="width: 100%; height: 100%">
|
||||
<div bind:this={chartDiv} style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
|
@ -1,10 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
ApplicationStatus,
|
||||
ApplicationStatusMaping,
|
||||
type Application
|
||||
} from '$lib/ApplicationsStore.svelte';
|
||||
import type { AsEnum } from '$lib/ApplicationsStore.svelte';
|
||||
import { ApplicationStatusMaping, type Application } from '$lib/ApplicationsStore.svelte';
|
||||
import { post } from '$lib/utils';
|
||||
|
||||
let {
|
||||
@ -24,12 +19,10 @@
|
||||
applications = await post('application/list', {});
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
getApplicationList();
|
||||
});
|
||||
|
||||
function docKey(e: KeyboardEvent) {
|
||||
if (e.ctrlKey && e.code === 'KeyK') {
|
||||
applications = [];
|
||||
getApplicationList();
|
||||
dialogElement.showModal();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
@ -53,6 +46,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-y-auto overflow-x-hidden flex-grow p-2">
|
||||
<!-- TODO loading screen -->
|
||||
{#each applications.filter((i) => {
|
||||
if (application && i.id == application.id) {
|
||||
return false;
|
||||
@ -62,8 +56,9 @@
|
||||
}
|
||||
|
||||
if (filter.includes('@') && i.company) {
|
||||
const f = new RegExp(filter.split('@')[0].trim(), 'ig');
|
||||
const c = new RegExp(filter.split('@')[1].trim(), 'ig');
|
||||
const splits = filter.split('@');
|
||||
const f = new RegExp(splits[0].trim(), 'ig');
|
||||
const c = new RegExp(splits[1].trim(), 'ig');
|
||||
return i.title.match(f) && i.company.match(c);
|
||||
}
|
||||
|
||||
@ -78,10 +73,14 @@
|
||||
return x.match(f);
|
||||
}) as item}
|
||||
<div class="card p-2 my-2 bg-slate-100 max-w-full" role="none">
|
||||
<button class="text-left max-w-full" type="button" onclick={() => {
|
||||
dialogElement.close()
|
||||
onreload(item)
|
||||
}}>
|
||||
<button
|
||||
class="text-left max-w-full"
|
||||
type="button"
|
||||
onclick={() => {
|
||||
dialogElement.close();
|
||||
onreload(item);
|
||||
}}
|
||||
>
|
||||
<h2 class="text-lg text-blue-500 flex justify-between">
|
||||
<div>
|
||||
{item.title}
|
||||
@ -95,9 +94,7 @@
|
||||
{ApplicationStatusMaping[item.status]}
|
||||
</div>
|
||||
</h2>
|
||||
<span
|
||||
class="text-violet-600 overflow-hidden whitespace-nowrap block max-w-full"
|
||||
>
|
||||
<span class="text-violet-600 overflow-hidden whitespace-nowrap block max-w-full">
|
||||
{item.url}
|
||||
</span>
|
||||
</button>
|
||||
|
@ -93,6 +93,21 @@
|
||||
);
|
||||
}
|
||||
|
||||
function docKey(e: KeyboardEvent) {
|
||||
if (activeItem && e.ctrlKey && e.code === 'KeyO') {
|
||||
openCV(activeItem.id);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
$effect(() => {
|
||||
document.addEventListener('keydown', docKey, false);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', docKey);
|
||||
};
|
||||
});
|
||||
|
||||
async function loadActive() {
|
||||
try {
|
||||
activeItem = await get('application/active');
|
||||
@ -264,48 +279,30 @@
|
||||
<div class="flex gap-2">
|
||||
<fieldset class="grow">
|
||||
<label class="flabel" for="title">Company</label>
|
||||
<input
|
||||
class="finput"
|
||||
id="title"
|
||||
bind:value={activeItem.company}
|
||||
onchange={save}
|
||||
/>
|
||||
<input class="finput" id="title" bind:value={activeItem.company} onchange={save} />
|
||||
</fieldset>
|
||||
<fieldset class="grow">
|
||||
<label class="flabel" for="title">Recruiter</label>
|
||||
<input
|
||||
class="finput"
|
||||
id="title"
|
||||
bind:value={activeItem.recruiter}
|
||||
onchange={save}
|
||||
/>
|
||||
<input class="finput" id="title" bind:value={activeItem.recruiter} onchange={save} />
|
||||
</fieldset>
|
||||
</div>
|
||||
<fieldset>
|
||||
<label class="flabel" for="title">Title</label>
|
||||
<input
|
||||
class="finput"
|
||||
id="title"
|
||||
bind:value={activeItem.title}
|
||||
onchange={save}
|
||||
/>
|
||||
<input class="finput" id="title" bind:value={activeItem.title} onchange={save} />
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label class="flabel" for="payrange">Pay Range</label>
|
||||
<input
|
||||
class="finput"
|
||||
id="payrange"
|
||||
bind:value={activeItem.payrange}
|
||||
onchange={save}
|
||||
/>
|
||||
<input class="finput" id="payrange" bind:value={activeItem.payrange} onchange={save} />
|
||||
</fieldset>
|
||||
<fieldset draggable="false" class="max-w-full min-w-0 overflow-hidden">
|
||||
<div class="flabel">Url</div>
|
||||
<div class="finput bg-white w-full break-keep">
|
||||
{activeItem.url}
|
||||
</div>
|
||||
</fieldset>
|
||||
{#if activeItem.unique_url && activeItem.unique_url !== activeItem.url}
|
||||
{#if !activeItem.unique_url || showExtraData}
|
||||
<fieldset draggable="false" class="max-w-full min-w-0 overflow-hidden">
|
||||
<div class="flabel">Url</div>
|
||||
<div class="finput bg-white w-full break-keep">
|
||||
{activeItem.url}
|
||||
</div>
|
||||
</fieldset>
|
||||
{/if}
|
||||
{#if activeItem.unique_url}
|
||||
<fieldset draggable="false">
|
||||
<div class="flabel">Unique Url</div>
|
||||
<div class="finput bg-white">
|
||||
@ -344,11 +341,7 @@
|
||||
</div>
|
||||
<fieldset>
|
||||
<label class="flabel" for="extra">Extra Info</label>
|
||||
<textarea
|
||||
class="finput"
|
||||
id="extra"
|
||||
bind:value={activeItem.extra_data}
|
||||
onchange={save}
|
||||
<textarea class="finput" id="extra" bind:value={activeItem.extra_data} onchange={save}
|
||||
></textarea>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
@ -398,9 +391,7 @@
|
||||
</button>
|
||||
{/if}
|
||||
{#if activeItem.original_url == null}
|
||||
<button class="btn-primary" onclick={() => changeUrl.showModal()}>
|
||||
Update Url
|
||||
</button>
|
||||
<button class="btn-primary" onclick={() => changeUrl.showModal()}> Update Url </button>
|
||||
{/if}
|
||||
<div class="px-10"></div>
|
||||
<button class="btn-primary" onclick={() => linkApplication.showModal()}>
|
||||
@ -409,11 +400,7 @@
|
||||
{#if activeItem.original_url != null}
|
||||
<button class="btn-danger" onclick={resetUrl}> Reset Url </button>
|
||||
{/if}
|
||||
<button
|
||||
class:btn-primary={drag}
|
||||
class:btn-danger={!drag}
|
||||
onclick={() => (drag = !drag)}
|
||||
>
|
||||
<button class:btn-primary={drag} class:btn-danger={!drag} onclick={() => (drag = !drag)}>
|
||||
👋
|
||||
</button>
|
||||
<button
|
||||
@ -435,7 +422,7 @@
|
||||
ondrop={() => {
|
||||
moveStatus(ApplicationStatus.ToApply);
|
||||
applicationStore.loadAplyed(true);
|
||||
applicationStore.loadTasksToDo(true);
|
||||
applicationStore.loadAll(true);
|
||||
}}
|
||||
>
|
||||
To apply
|
||||
@ -471,9 +458,7 @@
|
||||
|
||||
{#if activeItem.status === ApplicationStatus.WorkingOnIt}
|
||||
<!-- Repeated -->
|
||||
<DropZone icon="trash-fill text-danger" ondrop={() => remove()}
|
||||
>Delete it</DropZone
|
||||
>
|
||||
<DropZone icon="trash-fill text-danger" ondrop={() => remove()}>Delete it</DropZone>
|
||||
{/if}
|
||||
|
||||
{#if [ApplicationStatus.WorkingOnIt, ApplicationStatus.TasksToDo].includes(activeItem.status)}
|
||||
@ -482,8 +467,8 @@
|
||||
icon="server text-confirm"
|
||||
ondrop={async () => {
|
||||
await moveStatus(ApplicationStatus.Applyed);
|
||||
applicationStore.loadAll(true);
|
||||
applicationStore.loadAplyed(true);
|
||||
applicationStore.loadTasksToDo(true);
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
@ -496,12 +481,24 @@
|
||||
icon="server text-confirm"
|
||||
ondrop={async () => {
|
||||
await moveStatus(ApplicationStatus.TasksToDo);
|
||||
applicationStore.loadTasksToDo(true);
|
||||
applicationStore.loadAll(true);
|
||||
applicationStore.loadAplyed(true);
|
||||
}}
|
||||
>
|
||||
Tasks To Do
|
||||
</DropZone>
|
||||
|
||||
<!-- Tasks to do -->
|
||||
<DropZone
|
||||
icon="server text-confirm"
|
||||
ondrop={async () => {
|
||||
await moveStatus(ApplicationStatus.InterviewStep1);
|
||||
applicationStore.loadAll(true);
|
||||
applicationStore.loadAplyed(true);
|
||||
}}
|
||||
>
|
||||
Interview 1
|
||||
</DropZone>
|
||||
{/if}
|
||||
|
||||
<!-- Rejected -->
|
||||
@ -557,8 +554,4 @@
|
||||
|
||||
<SearchApplication application={activeItem} onreload={(item) => (activeItem = item)} />
|
||||
|
||||
<NewApplication
|
||||
onreload={(item) => {
|
||||
activate(item);
|
||||
}}
|
||||
/>
|
||||
<NewApplication onreload={activate} />
|
||||
|
Loading…
Reference in New Issue
Block a user