diff --git a/api/build.gradle.kts b/api/build.gradle.kts index bb19656..19bed78 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -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 { useJUnitPlatform() } -tasks.withType { - useJUnitPlatform() -} +tasks.withType { useJUnitPlatform() } diff --git a/api/src/main/kotlin/com/andr3h3nriqu3s/applications/ApplicationsController.kt b/api/src/main/kotlin/com/andr3h3nriqu3s/applications/ApplicationsController.kt index 335b967..a52f0a9 100644 --- a/api/src/main/kotlin/com/andr3h3nriqu3s/applications/ApplicationsController.kt +++ b/api/src/main/kotlin/com/andr3h3nriqu3s/applications/ApplicationsController.kt @@ -42,6 +42,7 @@ data class Application( var create_time: String, var flairs: List, var views: List, + var events: List, ) { companion object : RowMapper { 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 diff --git a/api/src/main/kotlin/com/andr3h3nriqu3s/applications/Events.kt b/api/src/main/kotlin/com/andr3h3nriqu3s/applications/Events.kt new file mode 100644 index 0000000..98db4ad --- /dev/null +++ b/api/src/main/kotlin/com/andr3h3nriqu3s/applications/Events.kt @@ -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 { + 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 { + 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 = + 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 + } +} diff --git a/api/src/main/resources/schema.sql b/api/src/main/resources/schema.sql index 72881ee..e9b794e 100644 --- a/api/src/main/resources/schema.sql +++ b/api/src/main/resources/schema.sql @@ -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 +); diff --git a/site/src/lib/ApplicationsStore.svelte.ts b/site/src/lib/ApplicationsStore.svelte.ts index 81c51f0..90d3b5d 100644 --- a/site/src/lib/ApplicationsStore.svelte.ts +++ b/site/src/lib/ApplicationsStore.svelte.ts @@ -15,6 +15,18 @@ export const ApplicationStatus = Object.freeze({ InterviewStep1: 8 }); +export const ApplicationStatusIconMaping: Record, 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, 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, + 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() { diff --git a/site/src/routes/PApplicationList.svelte b/site/src/routes/PApplicationList.svelte index 68a2add..9aa0a48 100644 --- a/site/src/routes/PApplicationList.svelte +++ b/site/src/routes/PApplicationList.svelte @@ -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({ diff --git a/site/src/routes/work-area/SearchApplication.svelte b/site/src/routes/work-area/SearchApplication.svelte index 05a77a8..fbf8a68 100644 --- a/site/src/routes/work-area/SearchApplication.svelte +++ b/site/src/routes/work-area/SearchApplication.svelte @@ -94,7 +94,9 @@ {ApplicationStatusMaping[item.status]} - + {item.url} diff --git a/site/src/routes/work-area/Timeline.svelte b/site/src/routes/work-area/Timeline.svelte new file mode 100644 index 0000000..1a46c75 --- /dev/null +++ b/site/src/routes/work-area/Timeline.svelte @@ -0,0 +1,121 @@ + + +{#if events.length > 0} +
+
+ {#each events as event, i} + {#if i === 0 && event.event_type !== EventType.Creation} +
+ {event.timeDiff} +
+ {/if} + +
+ {#if event.event_type == EventType.Creation} + + {:else if event.event_type == EventType.View} + + {:else} + + {/if} +
+ {#if i != events.length - 1 || !endable.includes(event.new_status)} +
+ {event.timeDiff} +
+ {#if i == events.length - 1} + + {/if} + {/if} + {/each} +
+
+{/if} diff --git a/site/src/routes/work-area/WorkArea.svelte b/site/src/routes/work-area/WorkArea.svelte index 01b4fda..ba3effb 100644 --- a/site/src/routes/work-area/WorkArea.svelte +++ b/site/src/routes/work-area/WorkArea.svelte @@ -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 @@
- +
- +
- +
- +
{#if !activeItem.unique_url || showExtraData}
@@ -341,7 +362,11 @@
-
@@ -391,7 +416,9 @@ {/if} {#if activeItem.original_url == null} - + {/if}
{/if} -