diff --git a/api/src/main/kotlin/com/andr3h3nriqu3s/applications/ApplicationsController.kt b/api/src/main/kotlin/com/andr3h3nriqu3s/applications/ApplicationsController.kt index ba40a3f..335b967 100644 --- a/api/src/main/kotlin/com/andr3h3nriqu3s/applications/ApplicationsController.kt +++ b/api/src/main/kotlin/com/andr3h3nriqu3s/applications/ApplicationsController.kt @@ -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 { + private fun internalFindAll(user: UserDb, info: ListRequest): Iterable { 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 { + 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 { diff --git a/api/src/main/kotlin/com/andr3h3nriqu3s/applications/View.kt b/api/src/main/kotlin/com/andr3h3nriqu3s/applications/View.kt index 4382111..89eba09 100644 --- a/api/src/main/kotlin/com/andr3h3nriqu3s/applications/View.kt +++ b/api/src/main/kotlin/com/andr3h3nriqu3s/applications/View.kt @@ -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 { 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 { + 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 (?, ?)", diff --git a/site/src/app.css b/site/src/app.css index 70f0819..8c78d30 100644 --- a/site/src/app.css +++ b/site/src/app.css @@ -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; } diff --git a/site/src/lib/ApplicationsStore.svelte.ts b/site/src/lib/ApplicationsStore.svelte.ts index f27eefe..4fcf292 100644 --- a/site/src/lib/ApplicationsStore.svelte.ts +++ b/site/src/lib/ApplicationsStore.svelte.ts @@ -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; - 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; + } }; } diff --git a/site/src/routes/+page.svelte b/site/src/routes/+page.svelte index 07e897c..3858c71 100644 --- a/site/src/routes/+page.svelte +++ b/site/src/routes/+page.svelte @@ -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'; @@ -15,7 +16,12 @@ - + + Interview I + + + Tasks To do + diff --git a/site/src/routes/ApplicationsList.svelte b/site/src/routes/ApplicationsList.svelte index 63df8e8..bafb2e5 100644 --- a/site/src/routes/ApplicationsList.svelte +++ b/site/src/routes/ApplicationsList.svelte @@ -33,7 +33,7 @@ return x.match(f); }) as item}
applicationStore.dragStart(item)} ondragend={() => { @@ -43,21 +43,26 @@ }} role="none" > -
-

- {item.title} - {#if item.company} -
- @ {item.company} +
+

+
+
+ {item.title}
- {/if} + {#if item.company} +
+ @ {item.company} +
+ {/if} +
+
+ {#if item.url.includes('linkedin')} + + {:else if item.url.includes('glassdoor')} + + {/if} +

- - {item.url} -
{/each} diff --git a/site/src/routes/NavBar.svelte b/site/src/routes/NavBar.svelte index b283946..5712cc6 100644 --- a/site/src/routes/NavBar.svelte +++ b/site/src/routes/NavBar.svelte @@ -5,14 +5,14 @@
- - + - -
diff --git a/site/src/routes/TasksToDoList.svelte b/site/src/routes/PApplicationList.svelte similarity index 54% rename from site/src/routes/TasksToDoList.svelte rename to site/src/routes/PApplicationList.svelte index 6f1d702..68a2add 100644 --- a/site/src/routes/TasksToDoList.svelte +++ b/site/src/routes/PApplicationList.svelte @@ -1,20 +1,32 @@ -{#if applicationStore.tasksToDo.length > 0} +{#if applications.length > 0}
-

Tasks To Do

+

{@render children()}

- {#each applicationStore.tasksToDo as item} + {#each applications as item}

diff --git a/site/src/routes/graphs/+page.svelte b/site/src/routes/graphs/+page.svelte index 8500602..08c662a 100644 --- a/site/src/routes/graphs/+page.svelte +++ b/site/src/routes/graphs/+page.svelte @@ -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 + | 'Linkedin' + | 'Glassdoor' + | 'Direct Source'; + + let graph = {} as Record>; + + function addGraph(inSource: NodeType, inTarget: NodeType) { + const source = `${inSource}`; + const target = `${inTarget}`; + if (graph[source] == undefined) { + graph[source] = {} as Record; + } + graph[source][target] = (graph[source][target] ?? 0) + 1; + return target as NodeType; + } + + let sourceData: Record = { + '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[] + ).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]; + } + 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 - | 'Linkedin' - | 'Glassdoor' - | 'Direct Source'; - - let graph = {} as Record>; - - function addGraph(inSource: NodeType, inTarget: NodeType) { - const source = `${inSource}`; - const target = `${inTarget}`; - if (graph[source] == undefined) { - graph[source] = {} as Record; - } - 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[] - ).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[]).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]; - } - 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 @@
-
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
diff --git a/site/src/routes/work-area/SearchApplication.svelte b/site/src/routes/work-area/SearchApplication.svelte index 03a53db..05a77a8 100644 --- a/site/src/routes/work-area/SearchApplication.svelte +++ b/site/src/routes/work-area/SearchApplication.svelte @@ -1,10 +1,5 @@