parent
							
								
									62961363f3
								
							
						
					
					
						commit
						f257bce4b0
					
				@ -15,8 +15,8 @@ java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }
 | 
				
			|||||||
repositories { mavenCentral() }
 | 
					repositories { mavenCentral() }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dependencies {
 | 
					dependencies {
 | 
				
			||||||
	implementation("org.postgresql:postgresql")
 | 
					    implementation("org.postgresql:postgresql")
 | 
				
			||||||
	implementation("org.springframework.security:spring-security-crypto:6.0.3")
 | 
					    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-data-jpa")
 | 
				
			||||||
    implementation("org.springframework.boot:spring-boot-starter-mustache")
 | 
					    implementation("org.springframework.boot:spring-boot-starter-mustache")
 | 
				
			||||||
    implementation("org.springframework.boot:spring-boot-starter-web")
 | 
					    implementation("org.springframework.boot:spring-boot-starter-web")
 | 
				
			||||||
@ -32,20 +32,14 @@ dependencies {
 | 
				
			|||||||
    implementation("org.bouncycastle:bcprov-jdk18on:1.76")
 | 
					    implementation("org.bouncycastle:bcprov-jdk18on:1.76")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
springBoot {
 | 
					springBoot { mainClass.set("com.andr3h3nriqu3s.applications.ApplicationsApplicationKt") }
 | 
				
			||||||
    mainClass.set("com.andr3h3nriqu3s.applications.ApplicationsApplicationKt")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
kotlin {
 | 
					kotlin {
 | 
				
			||||||
	compilerOptions {
 | 
					    compilerOptions { freeCompilerArgs.addAll("-Xjsr305=strict") }
 | 
				
			||||||
		freeCompilerArgs.addAll("-Xjsr305=strict")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	jvmToolchain(17)
 | 
					    jvmToolchain(17)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
tasks.withType<Test> { useJUnitPlatform() }
 | 
					tasks.withType<Test> { useJUnitPlatform() }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
tasks.withType<Test> {
 | 
					tasks.withType<Test> { useJUnitPlatform() }
 | 
				
			||||||
	useJUnitPlatform()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -42,6 +42,7 @@ data class Application(
 | 
				
			|||||||
        var create_time: String,
 | 
					        var create_time: String,
 | 
				
			||||||
        var flairs: List<Flair>,
 | 
					        var flairs: List<Flair>,
 | 
				
			||||||
        var views: List<View>,
 | 
					        var views: List<View>,
 | 
				
			||||||
 | 
					        var events: List<Event>,
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
    companion object : RowMapper<Application> {
 | 
					    companion object : RowMapper<Application> {
 | 
				
			||||||
        override public fun mapRow(rs: ResultSet, rowNum: Int): Application {
 | 
					        override public fun mapRow(rs: ResultSet, rowNum: Int): Application {
 | 
				
			||||||
@ -64,6 +65,7 @@ data class Application(
 | 
				
			|||||||
                    rs.getString("create_time"),
 | 
					                    rs.getString("create_time"),
 | 
				
			||||||
                    emptyList(),
 | 
					                    emptyList(),
 | 
				
			||||||
                    emptyList(),
 | 
					                    emptyList(),
 | 
				
			||||||
 | 
					                    emptyList(),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -94,6 +96,7 @@ class ApplicationsController(
 | 
				
			|||||||
        val applicationService: ApplicationService,
 | 
					        val applicationService: ApplicationService,
 | 
				
			||||||
        val flairService: FlairService,
 | 
					        val flairService: FlairService,
 | 
				
			||||||
        val viewService: ViewService,
 | 
					        val viewService: ViewService,
 | 
				
			||||||
 | 
					        val eventService: EventService,
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @GetMapping(path = ["/cv/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
 | 
					    @GetMapping(path = ["/cv/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
 | 
				
			||||||
@ -106,6 +109,7 @@ class ApplicationsController(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        if (user == null) {
 | 
					        if (user == null) {
 | 
				
			||||||
            viewService.create(application.id)
 | 
					            viewService.create(application.id)
 | 
				
			||||||
 | 
					            eventService.create(application.id, EventType.View)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        val flairs = application.flairs.map { it.toFlairSimple() }
 | 
					        val flairs = application.flairs.map { it.toFlairSimple() }
 | 
				
			||||||
@ -141,6 +145,7 @@ class ApplicationsController(
 | 
				
			|||||||
                        "",
 | 
					                        "",
 | 
				
			||||||
                        emptyList(),
 | 
					                        emptyList(),
 | 
				
			||||||
                        emptyList(),
 | 
					                        emptyList(),
 | 
				
			||||||
 | 
					                        emptyList(),
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!applicationService.createApplication(user, application)) {
 | 
					        if (!applicationService.createApplication(user, application)) {
 | 
				
			||||||
@ -268,6 +273,7 @@ class ApplicationsController(
 | 
				
			|||||||
                            "",
 | 
					                            "",
 | 
				
			||||||
                            emptyList(),
 | 
					                            emptyList(),
 | 
				
			||||||
                            emptyList(),
 | 
					                            emptyList(),
 | 
				
			||||||
 | 
					                            emptyList(),
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -311,21 +317,13 @@ class ApplicationsController(
 | 
				
			|||||||
            throw NotFound()
 | 
					            throw NotFound()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (application.status == info.status) {
 | 
				
			||||||
 | 
					            return application;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        application.status = info.status
 | 
					        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 }
 | 
					        applicationService.updateStatus(application)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (info.status == 4) {
 | 
					 | 
				
			||||||
            val sdf = SimpleDateFormat("dd/MM/yyyy hh:mm:ss")
 | 
					 | 
				
			||||||
            application.application_time = sdf.format(Date())
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        applicationService.update(application)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return application
 | 
					        return application
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -460,7 +458,8 @@ class ApplicationsController(
 | 
				
			|||||||
class ApplicationService(
 | 
					class ApplicationService(
 | 
				
			||||||
        val db: JdbcTemplate,
 | 
					        val db: JdbcTemplate,
 | 
				
			||||||
        val flairService: FlairService,
 | 
					        val flairService: FlairService,
 | 
				
			||||||
        val viewService: ViewService
 | 
					        val viewService: ViewService,
 | 
				
			||||||
 | 
					        val eventService: EventService,
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public fun findApplicationByUrl(user: UserDb, url: String, unique_url: String?): Application? {
 | 
					    public fun findApplicationByUrl(user: UserDb, url: String, unique_url: String?): Application? {
 | 
				
			||||||
@ -510,6 +509,7 @@ class ApplicationService(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        application.flairs = flairService.listFromLinkApplicationId(application.id)
 | 
					        application.flairs = flairService.listFromLinkApplicationId(application.id)
 | 
				
			||||||
        application.views = viewService.listFromApplicationId(application.id)
 | 
					        application.views = viewService.listFromApplicationId(application.id)
 | 
				
			||||||
 | 
					        application.events = eventService.listFromApplicationId(application.id).toList()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return application
 | 
					        return application
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -524,7 +524,7 @@ class ApplicationService(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        var application = applications[0]
 | 
					        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)
 | 
					        application.flairs = flairService.listFromLinkApplicationId(application.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return application
 | 
					        return application
 | 
				
			||||||
@ -555,6 +555,8 @@ class ApplicationService(
 | 
				
			|||||||
                application.application_time,
 | 
					                application.application_time,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        eventService.create(application.id, EventType.Creation)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return true
 | 
					        return true
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -594,10 +596,39 @@ class ApplicationService(
 | 
				
			|||||||
        return iter.toList()
 | 
					        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 {
 | 
					    public fun update(application: Application): Application {
 | 
				
			||||||
        // I don't want ot update create_time
 | 
					        // I don't want ot update create_time
 | 
				
			||||||
        db.update(
 | 
					        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.url,
 | 
				
			||||||
                application.original_url,
 | 
					                application.original_url,
 | 
				
			||||||
                application.unique_url,
 | 
					                application.unique_url,
 | 
				
			||||||
@ -605,13 +636,10 @@ class ApplicationService(
 | 
				
			|||||||
                application.user_id,
 | 
					                application.user_id,
 | 
				
			||||||
                application.extra_data,
 | 
					                application.extra_data,
 | 
				
			||||||
                application.payrange,
 | 
					                application.payrange,
 | 
				
			||||||
                application.status,
 | 
					 | 
				
			||||||
                application.company,
 | 
					                application.company,
 | 
				
			||||||
                application.recruiter,
 | 
					                application.recruiter,
 | 
				
			||||||
                application.message,
 | 
					                application.message,
 | 
				
			||||||
                application.linked_application,
 | 
					                application.linked_application,
 | 
				
			||||||
                application.status_history,
 | 
					 | 
				
			||||||
                application.application_time,
 | 
					 | 
				
			||||||
                application.id,
 | 
					                application.id,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        return application
 | 
					        return application
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										117
									
								
								api/src/main/kotlin/com/andr3h3nriqu3s/applications/Events.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								api/src/main/kotlin/com/andr3h3nriqu3s/applications/Events.kt
									
									
									
									
									
										Normal 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
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -49,3 +49,21 @@ create table if not exists flair_link (
 | 
				
			|||||||
	application_id text not null,
 | 
						application_id text not null,
 | 
				
			||||||
	flair_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
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
				
			|||||||
@ -15,6 +15,18 @@ export const ApplicationStatus = Object.freeze({
 | 
				
			|||||||
    InterviewStep1: 8
 | 
					    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({
 | 
					export const ApplicationStatusMaping: Record<AsEnum<typeof ApplicationStatus>, string> = Object.freeze({
 | 
				
			||||||
    0: 'To Apply',
 | 
					    0: 'To Apply',
 | 
				
			||||||
    1: 'Working On It',
 | 
					    1: 'Working On It',
 | 
				
			||||||
@ -33,6 +45,20 @@ export type View = {
 | 
				
			|||||||
    time: string;
 | 
					    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 = {
 | 
					export type Application = {
 | 
				
			||||||
    id: string;
 | 
					    id: string;
 | 
				
			||||||
    url: string;
 | 
					    url: string;
 | 
				
			||||||
@ -52,6 +78,7 @@ export type Application = {
 | 
				
			|||||||
    status_history: string;
 | 
					    status_history: string;
 | 
				
			||||||
    flairs: Flair[];
 | 
					    flairs: Flair[];
 | 
				
			||||||
    views: View[];
 | 
					    views: View[];
 | 
				
			||||||
 | 
					    events: ApplicationEvent[];
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function createApplicationStore() {
 | 
					function createApplicationStore() {
 | 
				
			||||||
 | 
				
			|||||||
@ -26,6 +26,7 @@
 | 
				
			|||||||
					class="card p-2 my-2 bg-slate-100 w-full text-left"
 | 
										class="card p-2 my-2 bg-slate-100 w-full text-left"
 | 
				
			||||||
					onclick={async () => {
 | 
										onclick={async () => {
 | 
				
			||||||
                        item.views = await get(`view/${item.id}`);
 | 
					                        item.views = await get(`view/${item.id}`);
 | 
				
			||||||
 | 
					                        item.events = await get(`events/${item.id}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
						applicationStore.loadItem = item;
 | 
											applicationStore.loadItem = item;
 | 
				
			||||||
						window.scrollTo({
 | 
											window.scrollTo({
 | 
				
			||||||
 | 
				
			|||||||
@ -94,7 +94,9 @@
 | 
				
			|||||||
							{ApplicationStatusMaping[item.status]}
 | 
												{ApplicationStatusMaping[item.status]}
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
					</h2>
 | 
										</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}
 | 
											{item.url}
 | 
				
			||||||
					</span>
 | 
										</span>
 | 
				
			||||||
				</button>
 | 
									</button>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										121
									
								
								site/src/routes/work-area/Timeline.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								site/src/routes/work-area/Timeline.svelte
									
									
									
									
									
										Normal 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}
 | 
				
			||||||
@ -17,6 +17,7 @@
 | 
				
			|||||||
	import LinkApplication from './LinkApplication.svelte';
 | 
						import LinkApplication from './LinkApplication.svelte';
 | 
				
			||||||
	import SearchApplication from './SearchApplication.svelte';
 | 
						import SearchApplication from './SearchApplication.svelte';
 | 
				
			||||||
	import NewApplication from './NewApplication.svelte';
 | 
						import NewApplication from './NewApplication.svelte';
 | 
				
			||||||
 | 
						import Timeline from './Timeline.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let activeItem: Application | undefined = $state();
 | 
						let activeItem: Application | undefined = $state();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -279,20 +280,40 @@
 | 
				
			|||||||
				<div class="flex gap-2">
 | 
									<div class="flex gap-2">
 | 
				
			||||||
					<fieldset class="grow">
 | 
										<fieldset class="grow">
 | 
				
			||||||
						<label class="flabel" for="title">Company</label>
 | 
											<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>
 | 
				
			||||||
					<fieldset class="grow">
 | 
										<fieldset class="grow">
 | 
				
			||||||
						<label class="flabel" for="title">Recruiter</label>
 | 
											<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>
 | 
										</fieldset>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
				<fieldset>
 | 
									<fieldset>
 | 
				
			||||||
					<label class="flabel" for="title">Title</label>
 | 
										<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>
 | 
				
			||||||
				<fieldset>
 | 
									<fieldset>
 | 
				
			||||||
					<label class="flabel" for="payrange">Pay Range</label>
 | 
										<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>
 | 
				
			||||||
				{#if !activeItem.unique_url || showExtraData}
 | 
									{#if !activeItem.unique_url || showExtraData}
 | 
				
			||||||
					<fieldset draggable="false" class="max-w-full min-w-0 overflow-hidden">
 | 
										<fieldset draggable="false" class="max-w-full min-w-0 overflow-hidden">
 | 
				
			||||||
@ -341,7 +362,11 @@
 | 
				
			|||||||
				</div>
 | 
									</div>
 | 
				
			||||||
				<fieldset>
 | 
									<fieldset>
 | 
				
			||||||
					<label class="flabel" for="extra">Extra Info</label>
 | 
										<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>
 | 
										></textarea>
 | 
				
			||||||
				</fieldset>
 | 
									</fieldset>
 | 
				
			||||||
				<fieldset>
 | 
									<fieldset>
 | 
				
			||||||
@ -391,7 +416,9 @@
 | 
				
			|||||||
					</button>
 | 
										</button>
 | 
				
			||||||
				{/if}
 | 
									{/if}
 | 
				
			||||||
				{#if activeItem.original_url == null}
 | 
									{#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}
 | 
									{/if}
 | 
				
			||||||
				<div class="px-10"></div>
 | 
									<div class="px-10"></div>
 | 
				
			||||||
				<button class="btn-primary" onclick={() => linkApplication.showModal()}>
 | 
									<button class="btn-primary" onclick={() => linkApplication.showModal()}>
 | 
				
			||||||
@ -400,7 +427,11 @@
 | 
				
			|||||||
				{#if activeItem.original_url != null}
 | 
									{#if activeItem.original_url != null}
 | 
				
			||||||
					<button class="btn-danger" onclick={resetUrl}> Reset Url </button>
 | 
										<button class="btn-danger" onclick={resetUrl}> Reset Url </button>
 | 
				
			||||||
				{/if}
 | 
									{/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>
 | 
				
			||||||
				<button
 | 
									<button
 | 
				
			||||||
@ -411,6 +442,7 @@
 | 
				
			|||||||
					🔬
 | 
										🔬
 | 
				
			||||||
				</button>
 | 
									</button>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
 | 
					            <Timeline application={activeItem} />
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
		{#if applicationStore.dragging}
 | 
							{#if applicationStore.dragging}
 | 
				
			||||||
			<div
 | 
								<div
 | 
				
			||||||
@ -458,7 +490,9 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
				{#if activeItem.status === ApplicationStatus.WorkingOnIt}
 | 
									{#if activeItem.status === ApplicationStatus.WorkingOnIt}
 | 
				
			||||||
					<!-- Repeated -->
 | 
										<!-- 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}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				{#if [ApplicationStatus.WorkingOnIt, ApplicationStatus.TasksToDo].includes(activeItem.status)}
 | 
									{#if [ApplicationStatus.WorkingOnIt, ApplicationStatus.TasksToDo].includes(activeItem.status)}
 | 
				
			||||||
@ -487,10 +521,9 @@
 | 
				
			|||||||
					>
 | 
										>
 | 
				
			||||||
						Tasks To Do
 | 
											Tasks To Do
 | 
				
			||||||
					</DropZone>
 | 
										</DropZone>
 | 
				
			||||||
                {/if}
 | 
									{/if}
 | 
				
			||||||
 | 
					 | 
				
			||||||
                {#if [ApplicationStatus.TasksToDo, ApplicationStatus.Applyed].includes(activeItem.status)}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									{#if [ApplicationStatus.TasksToDo, ApplicationStatus.Applyed].includes(activeItem.status)}
 | 
				
			||||||
					<!-- Tasks to do -->
 | 
										<!-- Tasks to do -->
 | 
				
			||||||
					<DropZone
 | 
										<DropZone
 | 
				
			||||||
						icon="server text-confirm"
 | 
											icon="server text-confirm"
 | 
				
			||||||
@ -555,6 +588,12 @@
 | 
				
			|||||||
	/>
 | 
						/>
 | 
				
			||||||
{/if}
 | 
					{/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} />
 | 
					<NewApplication onreload={activate} />
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user