feat: finish #3 #4

This commit is contained in:
Andre Henriques 2024-10-16 08:38:12 +01:00
parent 62961363f3
commit f257bce4b0
9 changed files with 391 additions and 44 deletions

View File

@ -15,8 +15,8 @@ java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }
repositories { mavenCentral() }
dependencies {
implementation("org.postgresql:postgresql")
implementation("org.springframework.security:spring-security-crypto:6.0.3")
implementation("org.postgresql:postgresql")
implementation("org.springframework.security:spring-security-crypto:6.0.3")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-mustache")
implementation("org.springframework.boot:spring-boot-starter-web")
@ -32,20 +32,14 @@ dependencies {
implementation("org.bouncycastle:bcprov-jdk18on:1.76")
}
springBoot {
mainClass.set("com.andr3h3nriqu3s.applications.ApplicationsApplicationKt")
}
springBoot { mainClass.set("com.andr3h3nriqu3s.applications.ApplicationsApplicationKt") }
kotlin {
compilerOptions {
freeCompilerArgs.addAll("-Xjsr305=strict")
}
compilerOptions { freeCompilerArgs.addAll("-Xjsr305=strict") }
jvmToolchain(17)
jvmToolchain(17)
}
tasks.withType<Test> { useJUnitPlatform() }
tasks.withType<Test> {
useJUnitPlatform()
}
tasks.withType<Test> { useJUnitPlatform() }

View File

@ -42,6 +42,7 @@ data class Application(
var create_time: String,
var flairs: List<Flair>,
var views: List<View>,
var events: List<Event>,
) {
companion object : RowMapper<Application> {
override public fun mapRow(rs: ResultSet, rowNum: Int): Application {
@ -64,6 +65,7 @@ data class Application(
rs.getString("create_time"),
emptyList(),
emptyList(),
emptyList(),
)
}
}
@ -94,6 +96,7 @@ class ApplicationsController(
val applicationService: ApplicationService,
val flairService: FlairService,
val viewService: ViewService,
val eventService: EventService,
) {
@GetMapping(path = ["/cv/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
@ -106,6 +109,7 @@ class ApplicationsController(
if (user == null) {
viewService.create(application.id)
eventService.create(application.id, EventType.View)
}
val flairs = application.flairs.map { it.toFlairSimple() }
@ -141,6 +145,7 @@ class ApplicationsController(
"",
emptyList(),
emptyList(),
emptyList(),
)
if (!applicationService.createApplication(user, application)) {
@ -268,6 +273,7 @@ class ApplicationsController(
"",
emptyList(),
emptyList(),
emptyList(),
)
}
@ -311,21 +317,13 @@ class ApplicationsController(
throw NotFound()
}
if (application.status == info.status) {
return application;
}
application.status = info.status
val status_string = "${info.status}"
var status_history = application.status_history.split(",").filter { it.length >= 1 }
if (status_history.indexOf(status_string) == -1) {
status_history = status_history.plus("${info.status}")
}
application.status_history = status_history.joinToString(",") { it }
if (info.status == 4) {
val sdf = SimpleDateFormat("dd/MM/yyyy hh:mm:ss")
application.application_time = sdf.format(Date())
}
applicationService.update(application)
applicationService.updateStatus(application)
return application
}
@ -460,7 +458,8 @@ class ApplicationsController(
class ApplicationService(
val db: JdbcTemplate,
val flairService: FlairService,
val viewService: ViewService
val viewService: ViewService,
val eventService: EventService,
) {
public fun findApplicationByUrl(user: UserDb, url: String, unique_url: String?): Application? {
@ -510,6 +509,7 @@ class ApplicationService(
application.flairs = flairService.listFromLinkApplicationId(application.id)
application.views = viewService.listFromApplicationId(application.id)
application.events = eventService.listFromApplicationId(application.id).toList()
return application
}
@ -524,7 +524,7 @@ class ApplicationService(
var application = applications[0]
// Views are not needed for this request
// Views / Events are not needed for this request
application.flairs = flairService.listFromLinkApplicationId(application.id)
return application
@ -555,6 +555,8 @@ class ApplicationService(
application.application_time,
)
eventService.create(application.id, EventType.Creation)
return true
}
@ -594,10 +596,39 @@ class ApplicationService(
return iter.toList()
}
// Update the stauts on the application object before giving it to this function
public fun updateStatus(application: Application): Application {
val status_string = "${application.status}"
var status_history = application.status_history.split(",").filter { it.length >= 1 }
if (status_history.indexOf(status_string) == -1) {
status_history = status_history.plus(status_string)
}
application.status_history = status_history.joinToString(",") { it }
if (application.status == 4) {
val sdf = SimpleDateFormat("dd/MM/yyyy hh:mm:ss")
application.application_time = sdf.format(Date())
}
eventService.create(application.id, EventType.StatusUpdate, application.status)
db.update(
"update applications set status=?, status_history=?, application_time=? where id=?",
application.status,
application.status_history,
application.application_time,
application.id,
)
return application
}
// Note this does not update status
public fun update(application: Application): Application {
// I don't want ot update create_time
db.update(
"update applications set url=?, original_url=?, unique_url=?, title=?, user_id=?, extra_data=?, payrange=?, status=?, company=?, recruiter=?, message=?, linked_application=?, status_history=?, application_time=? where id=?",
"update applications set url=?, original_url=?, unique_url=?, title=?, user_id=?, extra_data=?, payrange=?, company=?, recruiter=?, message=?, linked_application=? where id=?",
application.url,
application.original_url,
application.unique_url,
@ -605,13 +636,10 @@ class ApplicationService(
application.user_id,
application.extra_data,
application.payrange,
application.status,
application.company,
application.recruiter,
application.message,
application.linked_application,
application.status_history,
application.application_time,
application.id,
)
return application

View File

@ -0,0 +1,117 @@
package com.andr3h3nriqu3s.applications
import java.sql.ResultSet
import java.sql.Timestamp
import java.util.Date
import java.util.UUID
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.jdbc.core.RowMapper
import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.http.MediaType
enum class EventType(val value: Int) {
Creation(0),
StatusUpdate(1),
View(2)
}
data class Event(
var id: String,
var application_id: String,
var event_type: Int,
var new_status: Int?,
var time: Timestamp
) {
companion object : RowMapper<Event> {
override public fun mapRow(rs: ResultSet, rowNum: Int): Event {
return Event(
rs.getString("id"),
rs.getString("application_id"),
rs.getInt("event_type"),
rs.getInt("new_status"),
rs.getTimestamp("time"),
)
}
}
}
@RestController
@ControllerAdvice
@RequestMapping("/api/events")
class EventController(
val sessionService: SessionService,
val applicationService: ApplicationService,
val eventService: EventService
) {
@GetMapping(path = ["/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun getCV(@PathVariable id: String, @RequestHeader("token") token: String): List<Event> {
val user = sessionService.verifyTokenThrow(token)
val application = applicationService.findApplicationById(user, id)
if (application == null) {
throw NotFound()
}
return application.events;
}
}
// Note I decided that events are read+delete only
@Service
public class EventService(val db: JdbcTemplate) {
public fun listFromApplicationId(id: String): Iterable<Event> =
db.query("select * from events where application_id=? order by time asc;", arrayOf(id), Event)
public fun getById(id: String): Event? {
val items = db.query("select * from events where id=?;", arrayOf(id), Event)
if (items.size == 0) {
return null
}
return items.first()
}
public fun deleteById(id: String): Event {
val event = this.getById(id)
if (event == null) {
throw NotFound()
}
db.update("delete from events where id=?", id)
return event
}
public fun create(
application_id: String,
event_type: EventType,
new_status: Int? = null
): Event {
val id = UUID.randomUUID().toString()
if (event_type == EventType.StatusUpdate && new_status == null) {
throw Exception("When event_type == StatusUpdate new_status must be set")
}
var new_event =
Event(id, application_id, event_type.value, new_status, Timestamp(Date().getTime()))
db.update(
"insert into events (id, application_id, event_type, new_status) values (?, ?, ? ,?)",
new_event.id,
new_event.application_id,
new_event.event_type,
new_event.new_status,
)
return new_event
}
}

View File

@ -49,3 +49,21 @@ create table if not exists flair_link (
application_id text not null,
flair_id text not null
);
create table if not exists events (
id text primary key,
application_id text not null,
--
-- Event Types
--
--
-- Creation(0),
-- StatusUpdate(1),
-- Page(2)
event_type integer not null,
-- This only matters when event_type == 1
new_status integer,
time timestamp default current_timestamp
);

View File

@ -15,6 +15,18 @@ export const ApplicationStatus = Object.freeze({
InterviewStep1: 8
});
export const ApplicationStatusIconMaping: Record<AsEnum<typeof ApplicationStatus>, string> = Object.freeze({
0: 'clock',
1: 'search',
2: 'trash3',
3: 'fire',
4: 'send',
5: 'hourglass-bottom',
6: 'list-check',
7: 'link-45deg',
8: 'person'
});
export const ApplicationStatusMaping: Record<AsEnum<typeof ApplicationStatus>, string> = Object.freeze({
0: 'To Apply',
1: 'Working On It',
@ -33,6 +45,20 @@ export type View = {
time: string;
};
export const EventType = Object.freeze({
Creation: 0,
StatusUpdate: 1,
View: 2,
});
export type ApplicationEvent = {
id: string,
application_id: string,
event_type: AsEnum<typeof EventType>,
new_status: number,
time: string
}
export type Application = {
id: string;
url: string;
@ -52,6 +78,7 @@ export type Application = {
status_history: string;
flairs: Flair[];
views: View[];
events: ApplicationEvent[];
};
function createApplicationStore() {

View File

@ -26,6 +26,7 @@
class="card p-2 my-2 bg-slate-100 w-full text-left"
onclick={async () => {
item.views = await get(`view/${item.id}`);
item.events = await get(`events/${item.id}`);
applicationStore.loadItem = item;
window.scrollTo({

View File

@ -94,7 +94,9 @@
{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>

View File

@ -0,0 +1,121 @@
<script lang="ts">
import {
type Application,
type ApplicationEvent,
ApplicationStatus,
EventType,
ApplicationStatusIconMaping,
ApplicationStatusMaping
} from '$lib/ApplicationsStore.svelte';
let { application }: { application: Application } = $props();
let events: (ApplicationEvent & { timeDiff: string })[] = $state([]);
function calcDiff(time: number): string {
// millis to secs
time = Math.floor(time / 1000);
const days = Math.floor(time / (24 * 60 * 60));
if (days == 1) {
return '1 Day';
} else if (days > 0) {
return `${days} Days`;
}
time = time % (24 * 60 * 60);
const hours = Math.floor(time / (60 * 60));
if (hours > 20) {
return '1 Day';
} else if (hours > 2) {
return `${hours} Hours`;
}
return '';
}
$effect(() => {
let status: number | undefined = undefined;
let lastEvent: ApplicationEvent | undefined = undefined;
let _events: typeof events = [];
let checkArray: (number | undefined)[] = [
ApplicationStatus.WorkingOnIt,
ApplicationStatus.ToApply
];
for (let event of application.events) {
let time = '';
if (lastEvent) {
let d1 = new Date(lastEvent.time).getTime();
let d2 = new Date(event.time).getTime();
time = calcDiff(d2 - d1);
}
if (event.event_type === EventType.Creation) {
status = ApplicationStatus.ToApply;
}
if (event.event_type !== EventType.StatusUpdate) {
_events.push({ ...event, timeDiff: time });
lastEvent = event;
continue;
}
if (checkArray.includes(status) && checkArray.includes(event.new_status) && lastEvent?.event_type !== EventType.Creation ) {
continue;
}
status = event.new_status;
_events.push({ ...event, timeDiff: time });
lastEvent = event;
}
events = _events;
});
let endable: number[] = [
ApplicationStatus.Expired,
ApplicationStatus.Ignore,
ApplicationStatus.ApplyedButSaidNo,
ApplicationStatus.LinkedApplication
];
</script>
{#if events.length > 0}
<div class="overflow-x-auto">
<div class="flex p-5 items-center">
{#each events as event, i}
{#if i === 0 && event.event_type !== EventType.Creation}
<div class="min-w-[70px] h-[15px] bg-blue-500 -mx-[10px]">
{event.timeDiff}
</div>
{/if}
<div
class="shadow-sm shadow-violet-500 border rounded-full min-w-[50px] min-h-[50px] grid place-items-center bg-white z-10"
>
{#if event.event_type == EventType.Creation}
<span class="bi bi-plus"></span>
{:else if event.event_type == EventType.View}
<span class="bi bi-eye"></span>
{:else}
<span title={ApplicationStatusMaping[event.new_status]} class="bi bi-{ApplicationStatusIconMaping[event.new_status]}"></span>
{/if}
</div>
{#if i != events.length - 1 || !endable.includes(event.new_status)}
<div class="min-w-[70px] h-[13px] bg-blue-500 -mx-[10px] flex-grow">
{event.timeDiff}
</div>
{#if i == events.length - 1}
<!--div class="h-[15px] w-[15px] bg-blue-500 rotate-45"></div-->
{/if}
{/if}
{/each}
</div>
</div>
{/if}

View File

@ -17,6 +17,7 @@
import LinkApplication from './LinkApplication.svelte';
import SearchApplication from './SearchApplication.svelte';
import NewApplication from './NewApplication.svelte';
import Timeline from './Timeline.svelte';
let activeItem: Application | undefined = $state();
@ -279,20 +280,40 @@
<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>
{#if !activeItem.unique_url || showExtraData}
<fieldset draggable="false" class="max-w-full min-w-0 overflow-hidden">
@ -341,7 +362,11 @@
</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>
@ -391,7 +416,9 @@
</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()}>
@ -400,7 +427,11 @@
{#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
@ -411,6 +442,7 @@
🔬
</button>
</div>
<Timeline application={activeItem} />
</div>
{#if applicationStore.dragging}
<div
@ -458,7 +490,9 @@
{#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)}
@ -487,10 +521,9 @@
>
Tasks To Do
</DropZone>
{/if}
{#if [ApplicationStatus.TasksToDo, ApplicationStatus.Applyed].includes(activeItem.status)}
{/if}
{#if [ApplicationStatus.TasksToDo, ApplicationStatus.Applyed].includes(activeItem.status)}
<!-- Tasks to do -->
<DropZone
icon="server text-confirm"
@ -555,6 +588,12 @@
/>
{/if}
<SearchApplication application={activeItem} onreload={(item) => (activeItem = item)} />
<SearchApplication
application={activeItem}
onreload={async (item) => {
item.events = await get(`events/${item.id}`);
activeItem = item;
}}
/>
<NewApplication onreload={activate} />